{"record":{"id":"92a77fe21f75a260","repo":"apache/pulsar","slug":"unauthorized-to-validatenamespacepolicyoperation-f","errorCode":null,"errorMessage":"Unauthorized to validateNamespacePolicyOperation for operation [%s] on namespace [%s] on policy [%s]","messagePattern":"Unauthorized to validateNamespacePolicyOperation for operation \\[(.+?)\\] on namespace \\[(.+?)\\] on policy \\[(.+?)\\]","errorType":"http","errorClass":"RestException","httpStatus":403,"severity":"warning","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java","lineNumber":1058,"sourceCode":"        sync(()-> validateNamespacePolicyOperationAsync(namespaceName, policy, operation));\n    }\n\n    public CompletableFuture<Void> validateNamespacePolicyOperationAsync(NamespaceName namespaceName,\n                                                 PolicyName policy,\n                                                 PolicyOperation operation) {\n        if (pulsar().getConfiguration().isAuthenticationEnabled()\n                && pulsar().getBrokerService().isAuthorizationEnabled()) {\n            if (!isClientAuthenticated(clientAppId())) {\n                return FutureUtil.failedFuture(\n                        new RestException(Status.FORBIDDEN, \"Need to authenticate to perform the request\"));\n            }\n\n            return pulsar().getBrokerService().getAuthorizationService()\n                    .allowNamespacePolicyOperationAsync(namespaceName, policy, operation,\n                            originalPrincipal(), clientAppId(), clientAuthData())\n                    .thenAccept(isAuthorized -> {\n                        if (!isAuthorized) {\n                            throw new RestException(Status.FORBIDDEN,\n                                    String.format(\"Unauthorized to validateNamespacePolicyOperation for\"\n                                                    + \" operation [%s] on namespace [%s] on policy [%s]\",\n                                            operation.toString(), namespaceName, policy.toString()));\n                        }\n                    });\n        }\n        return CompletableFuture.completedFuture(null);\n    }\n\n    protected CompletableFuture<Void> canUpdateCluster(String tenant, Set<String> oldClusters,\n            Set<String> newClusters) {\n        // Check if any clusters are being removed\n        Set<String> removedClusters = new java.util.HashSet<>(oldClusters);\n        removedClusters.removeAll(newClusters);\n        if (removedClusters.isEmpty()) {\n            return CompletableFuture.completedFuture(null);\n        }\n","sourceCodeStart":1040,"sourceCodeEnd":1076,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java#L1040-L1076","documentation":"HTTP 403 FORBIDDEN thrown by validateNamespacePolicyOperationAsync: the authorization service denied the requested policy-level operation on the namespace (e.g. modifying retention, backlog quota, replication, or auto-creation policies). Policy operations are checked separately from generic namespace operations so finer-grained policy RBAC can be enforced.","triggerScenarios":"Any admin API that mutates or reads a specific namespace policy (retention, persistence, deduplication, offload, backlog quotas, etc.) where allowNamespacePolicyOperationAsync returns false for the role/policy pair.","commonSituations":"Role has general namespace grants but not the specific policy operation; security hardening introduced per-policy authorization (AuthorizationService.allowNamespacePolicyOperationAsync) and legacy grants no longer suffice; automation accounts missing the new policy action.","solutions":["Grant the policy operation: admin.namespaces().grantPermissionOnNamespace or use grantPermissionOnSubscription/PoliciesOperation grants per your authorization provider","Update automation/service accounts to include the required PoliciesOperation for the policies they manage","Verify which policy is being denied from the message's policy field and check getPermissions output","If the broker upgraded to per-policy checks, re-audit role grants against the new operation taxonomy"],"exampleFix":"// before: denied updating retention policy\nadmin.namespaces().setRetention(\"public/default\", new RetentionPolicies(7, 1024));\n// after: ensure role has the retention policy grant per configured AuthorizationService\nadmin.namespaces().grantPermissionOnNamespace(\"public/default\", \"data-eng-role\",\n    EnumSet.of(PoliciesOperation.SET_RETENTION));","handlingStrategy":"validation","validationCode":"Set<String> grants = admin.namespaces().getPermissions(ns).getOrDefault(myRole, Set.of());\n// confirm the role covers the specific policy operation before mutating policies","typeGuard":null,"tryCatchPattern":"try {\n    policyOp(ns);\n} catch (PulsarAdminException e) {\n    if (e.getStatusCode() == 403 && e.getMessage().contains(\"validateNamespacePolicyOperation\")) {\n        throw new SecurityException(\"grant the PoliciesOperation for \" + policy, e);\n    } else throw e;\n}","preventionTips":["Map each automation job to the exact PoliciesOperation it needs","Re-run grant audits after upgrading brokers with per-policy authorization","Keep policy grants separate from general namespace grants"],"tags":["http-403","authorization","namespace-policies","rbac"],"backgroundTag":"missing-permission-grant","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}