{"record":{"id":"fa9496bc710f74e6","repo":"apache/pulsar","slug":"unauthorized-to-validatetenantoperation-for-origin","errorCode":null,"errorMessage":"Unauthorized to validateTenantOperation for originalPrincipal [%s] and clientAppId [%s] about operation [%s] on tenant [%s]","messagePattern":"Unauthorized to validateTenantOperation for originalPrincipal \\[(.+?)\\] and clientAppId \\[(.+?)\\] about operation \\[(.+?)\\] on tenant \\[(.+?)\\]","errorType":"http","errorClass":"RestException","httpStatus":401,"severity":"warning","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java","lineNumber":1000,"sourceCode":"    }\n\n    public void validateTenantOperation(String tenant, TenantOperation operation) {\n        sync(()-> validateTenantOperationAsync(tenant, operation));\n    }\n\n    public CompletableFuture<Void> validateTenantOperationAsync(String tenant, TenantOperation operation) {\n        if (pulsar().getConfiguration().isAuthenticationEnabled()\n                && pulsar().getBrokerService().isAuthorizationEnabled()) {\n            if (!isClientAuthenticated(clientAppId())) {\n                return FutureUtil.failedFuture(\n                        new RestException(Status.UNAUTHORIZED, \"Need to authenticate to perform the request\"));\n            }\n\n            return pulsar().getBrokerService().getAuthorizationService()\n                    .allowTenantOperationAsync(tenant, operation, originalPrincipal(), clientAppId(), clientAuthData())\n                    .thenAccept(isAuthorized -> {\n                        if (!isAuthorized) {\n                            throw new RestException(Status.UNAUTHORIZED,\n                                    String.format(\"Unauthorized to validateTenantOperation for\"\n                                                    + \" originalPrincipal [%s] and clientAppId [%s] \"\n                                                    + \"about operation [%s] on tenant [%s]\",\n                                            originalPrincipal(), clientAppId(), operation.toString(), tenant));\n                        }\n                    });\n        }\n        return CompletableFuture.completedFuture(null);\n    }\n\n    public void validateNamespaceOperation(NamespaceName namespaceName, NamespaceOperation operation) {\n        sync(()-> validateNamespaceOperationAsync(namespaceName, operation));\n    }\n\n    public CompletableFuture<Void> validateNamespaceOperationAsync(NamespaceName namespaceName,\n                                                              NamespaceOperation operation) {\n        if (pulsar().getConfiguration().isAuthenticationEnabled()\n            && pulsar().getBrokerService().isAuthorizationEnabled()) {","sourceCodeStart":982,"sourceCodeEnd":1018,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/PulsarWebResource.java#L982-L1018","documentation":"HTTP 401 UNAUTHORIZED thrown by validateTenantOperationAsync: the authorization service denied the requested tenant-level operation for the originalPrincipal/clientAppId pair. The authenticated identity is not authorized to perform the tenant admin action (e.g. create namespace, list tenants under it).","triggerScenarios":"Any admin API performing validateTenantOperation (tenant update, namespace creation in tenant, tenant policies) where allowTenantOperationAsync returns false for the caller's role.","commonSituations":"User lacks the tenant admin role (tenant's adminRoles); proxy forwarded originalPrincipal not granted tenant access; keycloak/OIDC role mapping changed; app id restricted by authorization provider policy.","solutions":["Add the role to the tenant's adminRoles: admin.tenants().updateTenant(tenant, TenantInfo with the role in adminRoles)","Confirm originalPrincipal and clientAppId in the error/log match the intended identity and grant accordingly","If operating through a proxy, ensure the proxy forwards the original principal and the broker's proxy-role trust config is correct","Check the authorization provider implementation for extra constraints (e.g. pattern checks) being applied"],"exampleFix":"// before: caller not in tenant adminRoles\nadmin.tenants().createNamespace(\"my-tenant/ns1\");\n// after (as superuser)\nSet<String> roles = new HashSet<>(tenantInfo.getAdminRoles());\nroles.add(\"user-role\");\nadmin.tenants().updateTenant(\"my-tenant\", new TenantInfoImpl(roles, tenantInfo.getAllowedClusters()));","handlingStrategy":"try-catch","validationCode":"TenantInfo ti = admin.tenants().getTenantInfo(tenant);\nif (!ti.getAdminRoles().contains(myRole)) throw new IllegalStateException(\"role not a tenant admin\");","typeGuard":null,"tryCatchPattern":"try {\n    tenantOp(tenant);\n} catch (PulsarAdminException e) {\n    if (e.getStatusCode() == 401 && e.getMessage().contains(\"validateTenantOperation\")) {\n        throw new SecurityException(\"request tenant adminRoles grant for role\", e);\n    } else throw e;\n}","preventionTips":["Add service accounts to tenant adminRoles at provisioning time","Check originalPrincipal propagation when behind a proxy","Re-grant after SSO/OIDC role remapping"],"tags":["http-401","authorization","tenant","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"}