{"record":{"id":"0b7800f269072eb4","repo":"apolloconfig/apollo","slug":"app-admin-permission-is-required","errorCode":null,"errorMessage":"App admin permission is required","messagePattern":"App admin permission is required","errorType":"exception","errorClass":"AccessDeniedException","httpStatus":403,"severity":"error","filePath":"apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java","lineNumber":120,"sourceCode":"  }\n\n  private void requireDeleteClusterPermission(String appId) {\n    String authType = UserIdentityContextHolder.getAuthType();\n    if (UserIdentityConstants.USER.equals(authType)) {\n      // Keep Portal UI behavior aligned with the legacy WebAPI delete path, which required\n      // super-admin permission for cluster deletion.\n      if (unifiedPermissionValidator.isSuperAdmin()) {\n        return;\n      }\n      throw new AccessDeniedException(\"Super admin permission is required\");\n    }\n    if (UserIdentityConstants.CONSUMER.equals(authType)) {\n      // Existing OpenAPI consumers use app-scoped authorization here. Preserve that public\n      // token boundary while keeping the Portal USER path compatible with the legacy WebAPI.\n      if (unifiedPermissionValidator.isAppAdmin(appId)) {\n        return;\n      }\n      throw new AccessDeniedException(\"App admin permission is required\");\n    }\n    if (UserIdentityConstants.USER_TOKEN.equals(authType)) {\n      if (unifiedPermissionValidator.isSuperAdmin()) {\n        return;\n      }\n      throw new AccessDeniedException(\"Super admin permission is required\");\n    }\n    throw new AccessDeniedException(\"Access is denied\");\n  }\n\n  private void requireReadApplicationPermissionForUserToken(String appId) {\n    if (UserIdentityConstants.USER_TOKEN.equals(UserIdentityContextHolder.getAuthType())\n        && !unifiedPermissionValidator.hasReadApplicationPermission(appId)) {\n      throw new AccessDeniedException(\"Access is denied\");\n    }\n  }\n\n  private void requireCreateClusterPermissionForUserToken(String appId, String env,","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/apolloconfig/apollo/blob/d95fc18d112589efc09ddcbe1507047584d55251/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java#L102-L138","documentation":"Thrown by ClusterController.requireDeleteClusterPermission in the CONSUMER branch when an OpenAPI consumer token attempts to delete a cluster but does not have app-admin permission on the target app. Apollo allows CONSUMER (app-scoped token) deletion only if the token's associated role grants app-admin via isAppAdmin(appId), which is isSuperAdmin() || hasAssignRolePermission(appId). Maps to HTTP 403 AccessDeniedException.","triggerScenarios":"DELETE /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName} called with a CONSUMER token (traditional OpenAPI consumer auth) that has only read or release permissions but not app-admin on that appId.","commonSituations":"A CI/CD pipeline consumer token was granted namespace-level permissions (config:modify, config:release) but not the assign-role/app-admin permission needed for cluster deletion. The token can read and release configs but cannot delete clusters.","solutions":["Grant the CONSUMER token's associated role app-admin permission (assign-role) on the target appId via the Portal's permission management page.","Use a super-admin-level token or a USER_TOKEN with system:admin scope for cluster deletion operations.","If the deletion is a one-time operation, have a super admin perform it interactively through the Portal."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before delete-cluster with CONSUMER token, verify app-admin permission\n// Check via the Portal permission API whether the token's role has assign-role permission\nif (!consumerTokenHasAppAdmin(appId)) {\n    throw new IllegalStateException(\n        \"Consumer token lacks app-admin permission on \" + appId + \". Grant assign-role to proceed.\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    client.delete(\"/openapi/v1/envs/\" + env + \"/apps/\" + appId + \"/clusters/\" + clusterName);\n} catch (AccessDeniedException e) {\n    logger.error(\"Consumer token not app-admin for appId={}. Grant app-admin role.\", appId);\n    throw e;\n}","preventionTips":["When provisioning CONSUMER tokens for cluster management, grant app-admin (assign-role) permission.","Document which tokens have app-admin vs namespace-level permissions.","Use role naming conventions that make permission scope obvious (e.g. 'appA-admin', 'appA-release-only')."],"tags":["authorization","openapi","cluster","permissions","consumer","app-admin","access-control","http-403"],"backgroundTag":null,"analyzedSha":"d95fc18d112589efc09ddcbe1507047584d55251","analyzedAt":"2026-08-14T04:00:05.477Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}