Files
tiantian-system/app/pages/developer/resources/README_RESOURCE_COLLAB.md
2026-04-08 17:10:58 +08:00

5.3 KiB
Raw Blame History

资源中心协作权限设计

一、数据库变更

-- 扩展 app_resource 表,添加协作权限相关字段
ALTER TABLE app_resource
    ADD COLUMN owner_user_id BIGINT NULL COMMENT '资源创建者 userId创建时自动设置' AFTER user_id,
    ADD COLUMN access_level   TINYINT DEFAULT 1 COMMENT '默认协作者访问级别: 1=基础查看 2=连接查看 3=完全权限' AFTER owner_user_id;

-- 数据修复:将现有数据的 owner_user_id 设置为 user_id
UPDATE app_resource SET owner_user_id = user_id WHERE owner_user_id IS NULL;

-- 索引
CREATE INDEX idx_resource_owner ON app_resource(owner_user_id);

二、权限级别定义

级别 名称 可见信息 适用角色
0 无权限 非团队成员
1 基础查看 名称、IP、端口、状态 所有团队成员
2 连接查看 + 用户名、Host、连接方式 技术负责人
3 完全权限 + 密码、私钥、编辑删除 资源创建者 (Owner)

三、后端实现要点

3.1 创建资源时自动设置 owner_user_id

// AppResourceServiceImpl.java
@Override
public void save(AppResource resource) {
    // 创建时自动设置所有者
    Long currentUserId = StpUtil.getLoginIdAsLong();
    if (resource.getOwnerUserId() == null) {
        resource.setOwnerUserId(currentUserId);
    }
    super.save(resource);
}

3.2 查询时计算 accessLevel 并过滤敏感字段

// AppResourceController.java - 查询列表/分页时处理权限
private void enrichWithPermission(List<AppResource> resources) {
    Long currentUserId = StpUtil.getLoginIdAsLong();
    for (AppResource resource : resources) {
        // 计算访问级别
        ResourceAccessLevel level = checkAccessLevel(currentUserId, resource);
        resource.setAccessLevel(level.getValue());
        resource.setOwner(currentUserId.equals(resource.getOwnerUserId()));
        
        // 过滤敏感字段
        if (level.getValue() < 3) {
            resource.setSshPassword(hasContent(resource.getSshPassword()) ? "******" : null);
            resource.setAdminPassword(hasContent(resource.getAdminPassword()) ? "******" : null);
            resource.setDbPassword(hasContent(resource.getDbPassword()) ? "******" : null);
            resource.setPrivateKey(null);  // 私钥完全不返回给非Owner
        }
        if (level.getValue() < 2) {
            resource.setSshUsername(null);
            resource.setDbUsername(null);
        }
    }
}

private ResourceAccessLevel checkAccessLevel(Long userId, AppResource resource) {
    // Owner 拥有完全权限
    if (userId.equals(resource.getOwnerUserId())) {
        return ResourceAccessLevel.FULL;
    }
    
    // 检查应用团队权限(通过 app_invite 表)
    AppInvite invite = appInviteService.getByAppAndUser(resource.getAppId(), userId);
    if (invite == null) {
        return ResourceAccessLevel.NONE;
    }
    
    // 根据角色返回权限级别
    String role = invite.getRole();
    if ("technical_lead".equals(role) || "admin".equals(role)) {
        return ResourceAccessLevel.VIEW_CONNECTION;
    }
    return ResourceAccessLevel.VIEW_BASIC;
}

3.3 accessLevel 枚举

public enum ResourceAccessLevel {
    NONE(0), VIEW_BASIC(1), VIEW_CONNECTION(2), FULL(3);
    
    private final int value;
    
    ResourceAccessLevel(int value) { this.value = value; }
    
    public int getValue() { return value; }
}

3.4 修改/删除时权限检查

@PutMapping
public ApiResult<Void> update(@RequestBody AppResource resource) {
    AppResource existing = service.getById(resource.getResourceId());
    Long currentUserId = StpUtil.getLoginIdAsLong();
    
    if (!currentUserId.equals(existing.getOwnerUserId())) {
        return ApiResult.fail("只有资源创建者才能修改");
    }
    
    service.updateById(resource);
    return ApiResult.ok();
}

四、前端已完成的改动

文件 改动说明
app/composables/useResourceAccess.ts 权限工具库级别判断、enrichment、isOwner
app/api/app/appResource/model/index.ts 模型新增 ownerUserId / accessLevel / isOwner 字段
app/pages/developer/resources/index.vue 总览页协作说明banner、权限列标识
app/pages/developer/resources/servers.vue 服务器操作按权限显示SSH/Panel 需连接权限
app/pages/developer/resources/databases.vue 数据库用户名列权限脱敏操作按isOwner控制
app/pages/developer/resources/ssl.vue SSL私钥完全不显示给协作者编辑/删除仅Owner
app/pages/developer/resources/storage.vue 云存储操作按isOwner控制
app/pages/developer/resources/domains.vue 域名操作按isOwner控制协作者标识

五、降级策略(后端尚未改造时)

前端的 enrichResourcesWithPermission() 提供了前端降级逻辑:

  • 若后端未返回 accessLevel,前端根据 ownerUserId vs localStorage UserId 判断
  • ownerUserId === userId → accessLevel=3完全权限
  • 否则 → accessLevel=1基础查看

注意:前端降级逻辑仅用于开发阶段。生产环境必须由后端计算 accessLevel 并过滤敏感字段,前端无法保证数据安全。