diff --git a/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkOperation.java b/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkOperation.java new file mode 100644 index 000000000..af900d50a --- /dev/null +++ b/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkOperation.java @@ -0,0 +1,16 @@ +package com.ke.bella.openapi.apikey; + +public enum AkOperation { + QUERY, + RESET, + RENAME, + UPDATE_ROLE, + CERTIFY, + UPDATE_QUOTA, + UPDATE_QPS, + CHANGE_STATUS, + CREATE_CHILD, + TRANSFER, + VIEW_TRANSFER_HISTORY, + BIND_SERVICE +} diff --git a/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkPermissionMatrix.java b/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkPermissionMatrix.java new file mode 100644 index 000000000..a89125e3d --- /dev/null +++ b/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkPermissionMatrix.java @@ -0,0 +1,82 @@ +package com.ke.bella.openapi.apikey; + +import com.ke.bella.openapi.common.EntityConstants; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public final class AkPermissionMatrix { + + private AkPermissionMatrix() {} + + static final Set ALL_OPS = + Collections.unmodifiableSet(EnumSet.allOf(AkOperation.class)); + static final Set NO_OPS = + Collections.unmodifiableSet(EnumSet.noneOf(AkOperation.class)); + + private static final Map>> MATRIX; + + static { + Map>> m = new HashMap<>(); + + // all:全部放行 + Map> allMap = new EnumMap<>(AkRelation.class); + for (AkRelation r : AkRelation.values()) allMap.put(r, ALL_OPS); + m.put(EntityConstants.ALL, Collections.unmodifiableMap(allMap)); + + // console + Set consoleSameOrg = Collections.unmodifiableSet(EnumSet.of( + AkOperation.QUERY, AkOperation.RESET, AkOperation.RENAME, AkOperation.CHANGE_STATUS)); + Set consoleUnrelated = Collections.unmodifiableSet(EnumSet.of(AkOperation.QUERY)); + Map> consoleMap = new EnumMap<>(AkRelation.class); + consoleMap.put(AkRelation.OWNER, ALL_OPS); + consoleMap.put(AkRelation.MANAGER, ALL_OPS); + consoleMap.put(AkRelation.SAME_ORG, consoleSameOrg); + consoleMap.put(AkRelation.UNRELATED, consoleUnrelated); + m.put(EntityConstants.CONSOLE, Collections.unmodifiableMap(consoleMap)); + + // high + Set highOwner = Collections.unmodifiableSet(EnumSet.of( + AkOperation.QUERY, AkOperation.RESET, AkOperation.RENAME, + AkOperation.CHANGE_STATUS, AkOperation.CREATE_CHILD, AkOperation.TRANSFER, + AkOperation.VIEW_TRANSFER_HISTORY)); + Map> highMap = new EnumMap<>(AkRelation.class); + highMap.put(AkRelation.OWNER, highOwner); + highMap.put(AkRelation.MANAGER, ALL_OPS); + highMap.put(AkRelation.SAME_ORG, NO_OPS); + highMap.put(AkRelation.UNRELATED, NO_OPS); + m.put(EntityConstants.HIGH, Collections.unmodifiableMap(highMap)); + + // low + Set lowOwner = Collections.unmodifiableSet(EnumSet.of( + AkOperation.QUERY, AkOperation.RESET, AkOperation.RENAME, + AkOperation.CHANGE_STATUS, AkOperation.CREATE_CHILD, AkOperation.TRANSFER, + AkOperation.VIEW_TRANSFER_HISTORY)); + Map> lowMap = new EnumMap<>(AkRelation.class); + lowMap.put(AkRelation.OWNER, lowOwner); + lowMap.put(AkRelation.MANAGER, ALL_OPS); + lowMap.put(AkRelation.SAME_ORG, NO_OPS); + lowMap.put(AkRelation.UNRELATED, NO_OPS); + m.put(EntityConstants.LOW, Collections.unmodifiableMap(lowMap)); + + MATRIX = Collections.unmodifiableMap(m); + } + + public static boolean isAllowed(String roleCode, AkRelation relation, AkOperation operation) { + Map> inner = MATRIX.get(roleCode); + if (inner == null) return false; + Set ops = inner.get(relation); + return ops != null && ops.contains(operation); + } + + public static Set getAllowedOps(String roleCode, AkRelation relation) { + Map> inner = MATRIX.get(roleCode); + if (inner == null) return NO_OPS; + Set ops = inner.get(relation); + return ops != null ? ops : NO_OPS; + } +} diff --git a/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkRelation.java b/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkRelation.java new file mode 100644 index 000000000..470ccf4b0 --- /dev/null +++ b/api/sdk/src/main/java/com/ke/bella/openapi/apikey/AkRelation.java @@ -0,0 +1,19 @@ +package com.ke.bella.openapi.apikey; + +public enum AkRelation { + /** 调用方是目标 AK 的所有者(ownerCode 相同) */ + OWNER, + /** + * 调用方被授权为目标 AK 的 manager。 + * 当前版本预留,resolveRelation 中暂不实际解析,始终跳过。 + * 未来只需实现 isManager() 并取消注释即可生效。 + */ + MANAGER, + /** + * 调用方与目标 AK 同属一个 org。 + * 沿用现有 TODO 逻辑(orgCodes 始终为空),当前不会命中此分支。 + */ + SAME_ORG, + /** 调用方与目标 AK 无关联 */ + UNRELATED +} diff --git a/api/server/src/main/java/com/ke/bella/openapi/service/AkPermissionChecker.java b/api/server/src/main/java/com/ke/bella/openapi/service/AkPermissionChecker.java new file mode 100644 index 000000000..dd5767589 --- /dev/null +++ b/api/server/src/main/java/com/ke/bella/openapi/service/AkPermissionChecker.java @@ -0,0 +1,146 @@ +package com.ke.bella.openapi.service; + +import com.ke.bella.openapi.BellaContext; +import com.ke.bella.openapi.EndpointContext; +import com.ke.bella.openapi.Operator; +import com.ke.bella.openapi.apikey.AkOperation; +import com.ke.bella.openapi.apikey.AkPermissionMatrix; +import com.ke.bella.openapi.apikey.AkRelation; +import com.ke.bella.openapi.apikey.ApikeyInfo; +import com.ke.bella.openapi.common.exception.BellaException; +import com.ke.bella.openapi.tables.pojos.ApikeyDB; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import static com.ke.bella.openapi.common.EntityConstants.CONSOLE; +import static com.ke.bella.openapi.common.EntityConstants.ORG; +import static com.ke.bella.openapi.common.EntityConstants.PERSON; +import static com.ke.bella.openapi.common.EntityConstants.SYSTEM; + +@Component +public class AkPermissionChecker { + + /** operator(Console 登录用户)作为 AK 所有者时,可执行的操作集合 */ + private static final Set OPERATOR_OWNER_OPS = Collections.unmodifiableSet( + EnumSet.of( + AkOperation.QUERY, + AkOperation.RESET, + AkOperation.RENAME, + AkOperation.CERTIFY, + AkOperation.UPDATE_QPS, + AkOperation.CHANGE_STATUS, + AkOperation.TRANSFER, + AkOperation.VIEW_TRANSFER_HISTORY + ) + ); + + /** + * 主入口:接受 ApikeyDB(从 queryByUniqueKey 得到) + */ + public void check(ApikeyDB targetDb, AkOperation operation) { + ApikeyInfo caller = EndpointContext.getApikeyIgnoreNull(); + + if (caller == null) { + checkOperatorPermission(targetDb, operation); + return; + } + + // system ownerType:目标非 system 则全放行,否则拒绝 + if (SYSTEM.equals(caller.getOwnerType())) { + if (SYSTEM.equals(targetDb.getOwnerType())) { + throw new BellaException.AuthorizationException("没有操作权限"); + } + return; + } + + AkRelation relation = resolveRelation(caller, targetDb); + if (!AkPermissionMatrix.isAllowed(caller.getRoleCode(), relation, operation)) { + throw new BellaException.AuthorizationException("没有操作权限"); + } + } + + /** + * 重载:接受 ApikeyInfo(从 queryByCode 得到,transferApikeyOwner / getTransferHistory 使用) + */ + public void check(ApikeyInfo targetInfo, AkOperation operation) { + ApikeyDB db = new ApikeyDB(); + db.setOwnerType(targetInfo.getOwnerType()); + db.setOwnerCode(targetInfo.getOwnerCode()); + check(db, operation); + } + + /** + * operator(Console 登录用户,无 AK)的独立权限分支。 + * ownerCode == userId 且 ownerType ∈ {PERSON, CONSOLE} → 允许 OPERATOR_OWNER_OPS 中的操作。 + * 其余情况拒绝(预留:未来 Manager 关系在此扩展)。 + */ + private void checkOperatorPermission(ApikeyDB targetDb, AkOperation operation) { + Operator op = BellaContext.getOperator(); + String userId = op.getUserId().toString(); + + boolean isOwner = (PERSON.equals(targetDb.getOwnerType()) || CONSOLE.equals(targetDb.getOwnerType())) + && userId.equals(targetDb.getOwnerCode()); + + if (isOwner) { + if (!OPERATOR_OWNER_OPS.contains(operation)) { + throw new BellaException.AuthorizationException("没有操作权限"); + } + return; + } + + // 非自己的 AK:当前不允许(TODO: isManager 时开放部分权限) + throw new BellaException.AuthorizationException("没有操作权限"); + } + + /** + * 解析调用方 AK 与目标 AK 的关系。 + * 优先级:OWNER > MANAGER(预留)> SAME_ORG > UNRELATED + */ + AkRelation resolveRelation(ApikeyInfo caller, ApikeyDB target) { + if (isOwner(caller, target)) { + return AkRelation.OWNER; + } + + // MANAGER 预留扩展点:实现 isManager() 后取消注释 + // if (isManager(caller, target)) { return AkRelation.MANAGER; } + + if (isSameOrg(caller, target)) { + return AkRelation.SAME_ORG; + } + + return AkRelation.UNRELATED; + } + + private static final Set PERSONAL_OWNER_TYPES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(PERSON, CONSOLE))); + + private boolean isOwner(ApikeyInfo caller, ApikeyDB target) { + if (!caller.getOwnerCode().equals(target.getOwnerCode())) { + return false; + } + // person 和 console 同属一个自然人,ownerType 可以互通 + if (PERSONAL_OWNER_TYPES.contains(caller.getOwnerType()) + && PERSONAL_OWNER_TYPES.contains(target.getOwnerType())) { + return true; + } + return caller.getOwnerType().equals(target.getOwnerType()); + } + + /** + * SAME_ORG 判断:保留现有 TODO 结构,orgCodes 始终为空集,当前不会命中。 + * 未来在此填充 org 查询逻辑即可。 + */ + private boolean isSameOrg(ApikeyInfo caller, ApikeyDB target) { + if (!ORG.equals(target.getOwnerType())) { + return false; + } + // TODO: 获取调用方所属的所有 orgCodes + Set orgCodes = new HashSet<>(); + return orgCodes.contains(target.getOwnerCode()); + } +} diff --git a/api/server/src/main/java/com/ke/bella/openapi/service/ApikeyService.java b/api/server/src/main/java/com/ke/bella/openapi/service/ApikeyService.java index 42b633854..64771ba8b 100644 --- a/api/server/src/main/java/com/ke/bella/openapi/service/ApikeyService.java +++ b/api/server/src/main/java/com/ke/bella/openapi/service/ApikeyService.java @@ -7,11 +7,11 @@ import com.alicp.jetcache.anno.CacheUpdate; import com.alicp.jetcache.anno.Cached; import com.alicp.jetcache.template.QuickConfig; -import com.google.common.collect.Sets; import com.ke.bella.openapi.BellaContext; import com.ke.bella.openapi.EndpointContext; import com.ke.bella.openapi.Operator; import com.ke.bella.openapi.PermissionCondition; +import com.ke.bella.openapi.apikey.AkOperation; import com.ke.bella.openapi.apikey.ApikeyCreateOp; import com.ke.bella.openapi.apikey.ApikeyInfo; import com.ke.bella.openapi.apikey.ApikeyOps; @@ -100,6 +100,8 @@ public class ApikeyService { private ApplicationEventPublisher eventPublisher; @Autowired private ISafetyAuditService safetyAuditService; + @Autowired + private AkPermissionChecker akPermissionChecker; private static final String apikeyCacheKey = "apikey:sha:"; @PostConstruct @@ -141,12 +143,9 @@ public String apply(ApikeyOps.ApplyOp op) { @Transactional public String createByParentCode(ApikeyCreateOp op) { - ApikeyInfo cur = EndpointContext.getApikey(); ApikeyInfo apikey = queryByCode(op.getParentCode(), true); - if(!apikey.getOwnerCode().equals(cur.getOwnerCode())) { - throw new BellaException.AuthorizationException("请求使用AK和父AK必须属于同一个人"); - } Assert.notNull(apikey, "父AK不存在或已停用"); + checkPermission(op.getParentCode(), AkOperation.CREATE_CHILD); Assert.isTrue(StringUtils.isEmpty(apikey.getParentCode()), "当前AK无创建子AK权限"); if(StringUtils.isNotEmpty(op.getRoleCode())) { apikeyRoleRepo.checkExist(op.getRoleCode(), true); @@ -182,13 +181,10 @@ public String createByParentCode(ApikeyCreateOp op) { @Transactional public boolean updateSubApikey(SubApikeyUpdateOp op) { - ApikeyInfo cur = EndpointContext.getApikey(); ApikeyInfo subApikey = apikeyRepo.queryByCode(op.getCode()); ApikeyInfo apikey = queryByCode(subApikey.getParentCode(), false); Assert.notNull(apikey, "只可以修改子ak"); - if(!apikey.getOwnerCode().equals(cur.getOwnerCode())) { - throw new BellaException.AuthorizationException("只能修改自己的apikey"); - } + checkPermission(subApikey.getParentCode(), AkOperation.CREATE_CHILD); if(StringUtils.isNotEmpty(op.getRoleCode())) { apikeyRoleRepo.checkExist(op.getRoleCode(), true); } @@ -208,7 +204,7 @@ public boolean updateSubApikey(SubApikeyUpdateOp op) { @Transactional public String reset(ApikeyOps.CodeOp op) { apikeyRepo.checkExist(op.getCode(), true); - checkPermission(op.getCode()); + checkPermission(op.getCode(), AkOperation.RESET); String ak = UUID.randomUUID().toString(); String sha = EncryptUtils.sha256(ak); String display = EncryptUtils.desensitize(ak); @@ -232,7 +228,7 @@ public void bindService(ApikeyOps.ServiceOp op) { @Transactional public void updateRole(ApikeyOps.RoleOp op) { apikeyRepo.checkExist(op.getCode(), true); - checkPermission(op.getCode()); + checkPermission(op.getCode(), AkOperation.UPDATE_ROLE); if(StringUtils.isNotEmpty(op.getRoleCode())) { apikeyRoleRepo.checkExist(op.getRoleCode(), true); } else { @@ -249,7 +245,7 @@ public void updateRole(ApikeyOps.RoleOp op) { @Transactional public void certify(ApikeyOps.CertifyOp op) { apikeyRepo.checkExist(op.getCode(), true); - checkPermission(op.getCode()); + checkPermission(op.getCode(), AkOperation.CERTIFY); Byte level = safetyAuditService.fetchLevelByCertifyCode(op.getCertifyCode()); ApikeyDB db = new ApikeyDB(); db.setCertifyCode(op.getCertifyCode()); @@ -260,21 +256,21 @@ public void certify(ApikeyOps.CertifyOp op) { @Transactional public void updateQuota(ApikeyOps.QuotaOp op) { apikeyRepo.checkExist(op.getCode(), true); - checkPermission(op.getCode()); + checkPermission(op.getCode(), AkOperation.UPDATE_QUOTA); apikeyRepo.update(op, op.getCode()); } @Transactional public void updateQpsLimit(ApikeyOps.QpsLimitOp op) { apikeyRepo.checkExist(op.getCode(), true); - checkPermission(op.getCode()); + checkPermission(op.getCode(), AkOperation.UPDATE_QPS); apikeyRepo.update(op, op.getCode()); } @Transactional public void changeStatus(ApikeyOps.CodeOp op, boolean active) { apikeyRepo.checkExist(op.getCode(), true); - checkPermission(op.getCode()); + checkPermission(op.getCode(), AkOperation.CHANGE_STATUS); String status = active ? ACTIVE : INACTIVE; apikeyRepo.updateStatus(op.getCode(), status); } @@ -354,33 +350,9 @@ public List queryBillingsByAkCode(String akCode) { return apikeyCostRepo.queryByAkCode(akCode); } - private void checkPermission(String code) { + private void checkPermission(String code, AkOperation operation) { ApikeyDB db = apikeyRepo.queryByUniqueKey(code); - ApikeyInfo apikeyInfo = EndpointContext.getApikeyIgnoreNull(); - // all roleCode 的超级管理员可以操作任意 AK - if(apikeyInfo != null && EntityConstants.ALL.equals(apikeyInfo.getRoleCode())) { - return; - } - if(apikeyInfo == null) { - Operator op = BellaContext.getOperator(); - Assert.isTrue( - (db.getOwnerType().equals(PERSON) || db.getOwnerType().equals(CONSOLE)) && db.getOwnerCode().equals(op.getUserId().toString()), - "没有操作权限"); - return; - } - if(apikeyInfo.getOwnerType().equals(SYSTEM)) { - return; - } - // todo: 获取所有 org - Set orgCodes = new HashSet<>(); - if(db.getOwnerType().equals(SYSTEM)) { - throw new BellaException.AuthorizationException("没有操作权限"); - } - if(db.getOwnerType().equals(ORG)) { - validateOrgPermission(apikeyInfo, Sets.newHashSet(db.getOwnerCode()), orgCodes); - } else { - validateUserPermission(apikeyInfo, db.getOwnerCode()); - } + akPermissionChecker.check(db, operation); } public Page pageApikey(ApikeyOps.ApikeyCondition condition) { @@ -479,12 +451,8 @@ public boolean transferApikeyOwner(TransferApikeyOwnerOp op, Operator currentOpe throw new BellaException.AuthorizationException("只有个人类型的API Key才能转移"); } - // 2. 验证当前用户权限:所有者可以转移,all roleCode 的超级管理员也可以代为转移 - ApikeyInfo operatorApikeyInfo = EndpointContext.getApikeyIgnoreNull(); - boolean isSuperAdmin = operatorApikeyInfo != null && EntityConstants.ALL.equals(operatorApikeyInfo.getRoleCode()); - if(!isSuperAdmin && !apikeyInfo.getOwnerCode().equals(currentOperator.getUserId().toString())) { - throw new BellaException.AuthorizationException("只有API Key所有者才能执行转移操作"); - } + // 2. 统一权限检查 + akPermissionChecker.check(apikeyInfo, AkOperation.TRANSFER); // 3. 查找并验证目标用户 UserDB targetUser = findAndValidateTargetUser(op); @@ -561,12 +529,7 @@ public List getTransferHistory(String akCode) { throw new BellaException.AuthorizationException("API Key不存在"); } - ApikeyInfo currentApikey = EndpointContext.getApikey(); - boolean isSuperAdmin = EntityConstants.ALL.equals(currentApikey.getRoleCode()); - if(!isSuperAdmin && !apikeyInfo.getOwnerCode().equals(currentApikey.getOwnerCode()) - && !SYSTEM.equals(currentApikey.getOwnerType())) { - throw new BellaException.AuthorizationException("没有权限查看转移历史"); - } + akPermissionChecker.check(apikeyInfo, AkOperation.VIEW_TRANSFER_HISTORY); return apikeyTransferLogRepo.queryByAkCode(akCode); }