{"record":{"id":"5e62123905e706e9","repo":"apolloconfig/apollo","slug":"you-don-t-have-the-permission-to-modify-namespace","errorCode":null,"errorMessage":"You don't have the permission to modify namespace: %s","messagePattern":"You don't have the permission to modify namespace: (.+?)","errorType":"exception","errorClass":"AccessDeniedException","httpStatus":403,"severity":"error","filePath":"apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ItemController.java","lineNumber":391,"sourceCode":"      return false;\n    }\n    return true;\n  }\n\n  private void checkSyncPermissions(OpenNamespaceSyncDTO model) {\n    OpenNamespaceIdentifier noPermissionNamespace = null;\n    boolean hasPermission = true;\n    for (OpenNamespaceIdentifier namespaceIdentifier : model.getSyncToNamespaces()) {\n      hasPermission = unifiedPermissionValidator.hasModifyNamespacePermission(\n          namespaceIdentifier.getAppId(), namespaceIdentifier.getEnv(),\n          namespaceIdentifier.getClusterName(), namespaceIdentifier.getNamespaceName());\n      if (!hasPermission) {\n        noPermissionNamespace = namespaceIdentifier;\n        break;\n      }\n    }\n    if (!hasPermission) {\n      throw new AccessDeniedException(String\n          .format(\"You don't have the permission to modify namespace: %s\", noPermissionNamespace));\n    }\n  }\n\n  private String decodeBase64(String key) {\n    try {\n      return decodeBase64(key, Base64.getDecoder());\n    } catch (IllegalArgumentException standardBase64Exception) {\n      try {\n        return decodeBase64(key, Base64.getUrlDecoder());\n      } catch (IllegalArgumentException urlBase64Exception) {\n        throw new BadRequestException(\"Invalid encoded key\");\n      }\n    }\n  }\n\n  private String decodeBase64(String key, Base64.Decoder decoder) {\n    return new String(decoder.decode(key), StandardCharsets.UTF_8);","sourceCodeStart":373,"sourceCodeEnd":409,"githubUrl":"https://github.com/apolloconfig/apollo/blob/d95fc18d112589efc09ddcbe1507047584d55251/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ItemController.java#L373-L409","documentation":"HTTP 403 (AccessDeniedException). Thrown by ItemController.checkSyncPermissions when a bulk item-sync request (syncItems) targets one or more destination namespaces on which the authenticated caller lacks namespace-modify permission. The validator UnifiedPermissionValidator.hasModifyNamespacePermission is evaluated per target namespace; the FIRST namespace that fails is reported back in the message (its OpenNamespaceIdentifier toString). Portal SSO users and OpenAPI consumer tokens are both subject to this check.","triggerScenarios":"POST /openapi/v1/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items/syncItems with an OpenNamespaceSyncDTO whose syncToNamespaces lists a namespace the caller cannot modify. Triggered when the caller has modify rights on the source namespace but not on at least one destination, or when a destination namespace was renamed/deleted and the permission grant is stale.","commonSituations":"Syncing config across environments (DEV->FAT->PRO) with a token scoped to only one env; a destination cluster/namespace name typo so the permission lookup misses; an admin revoked the ModifyNamespace role after the sync job was configured; cross-app sync where the token lacks the target app's role.","solutions":["Inspect the noPermissionNamespace value in the 403 body and grant ModifyNamespace (or EditNamespace) role on that exact appId/env/cluster/namespace to the caller.","Remove or correct the offending entry from syncToNamespaces in the request payload so every target matches a namespace the caller is authorized to modify.","If using an OpenAPI consumer token, recreate/extend the token's app scope to include every destination namespace.","Retry the sync only after re-checking each target with the OpenAPI permission/role endpoints."],"exampleFix":"// before: token only has modify on DEV\nsyncDTO.getSyncToNamespaces().add(ns(PROD)); // 403 on PROD\n\n// after: drop targets the caller cannot modify, or grant the role first\nList<OpenNamespaceIdentifier> targets = syncDTO.getSyncToNamespaces().stream()\n    .filter(t -> hasModifyPermission(token, t)) // client-side pre-check\n    .collect(Collectors.toList());\nsyncDTO.setSyncToNamespaces(targets);","handlingStrategy":"validation","validationCode":"// Before syncItems, pre-check modify permission on every target namespace.\nboolean allOk = true;\nOpenNamespaceIdentifier blocked = null;\nfor (OpenNamespaceIdentifier t : syncDTO.getSyncToNamespaces()) {\n  // GET /openapi/v1/apps/{appId}/envs/{env}/clusters/{cluster}/namespaces/{ns}/role\n  // or use a dedicated hasModify permission probe exposed by the portal.\n  if (!canModify(token, t.getAppId(), t.getEnv(), t.getClusterName(), t.getNamespaceName())) {\n    allOk = false; blocked = t; break;\n  }\n}\nif (!allOk) { /* drop blocked or grant role, do NOT call syncItems */ }","typeGuard":"null","tryCatchPattern":"// Distinguish 403 (permission) from other failures.\ntry {\n  client.syncItems(appId, env, cluster, ns, syncDTO);\n} catch (HttpServerErrorException.Forbidden e) {\n  // body contains \"You don't have the permission to modify namespace: <id>\"\n  String ns = extractNamespaceFrom403(e.getResponseBodyAsString());\n  log.warn(\"Missing modify permission on {}, grant role or drop target\", ns);\n}","preventionTips":["Grant ModifyNamespace on every destination namespace before configuring cross-env sync.","Keep the token's app/env scope aligned with all sync targets.","Sync from the least-privileged source; never assume destination rights from source rights."],"tags":["apollo-portal","openapi","permission","access-denied","namespace","item-sync"],"backgroundTag":null,"analyzedSha":"d95fc18d112589efc09ddcbe1507047584d55251","analyzedAt":"2026-08-14T04:00:05.477Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}