/******************************************************************************* * Copyright (C) 2026, Leo Galambos * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. All advertising materials mentioning features or use of this software must * display the following acknowledgement: * This product includes software developed by the Egothor project. * * 4. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ package zeroecho.pki.server; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import zeroecho.core.io.CancellationSignal; import zeroecho.pki.api.PkiId; import zeroecho.pki.application.PkiOperation; import zeroecho.pki.application.PkiOperationExecutor; import zeroecho.pki.application.PkiOperationOutcome; import zeroecho.pki.application.PkiOperationResult; import zeroecho.pki.application.PkiOperationValue; import zeroecho.pki.server.acme.AcmeService; import zeroecho.pki.application.PkiResourceScopeResolver; /** * In-process transport-neutral security gateway for existing typed PKI * operations. * *

The gateway owns no executor, thread, queue, retry, or PKI business rule. It * serially validates realm/scope, authorizes, applies approval policy, delegates * exactly once to the session executor, and preserves the returned outcome.

*/ @SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.FieldDeclarationsShouldBeAtStartOfClass", "PMD.ExcessiveParameterList", "PMD.CyclomaticComplexity", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.NcssCount", "PMD.ConfusingTernary", "PMD.ExceptionAsFlowControl", "PMD.CouplingBetweenObjects" }) public final class ServerOperationGateway { /** * Complete transport-neutral request admission input. * * @param realmId exact realm identity * @param principalId authenticated end-client principal identity * @param authenticationMode explicit administrative authentication mode * @param transportPrincipalId trusted proxy identity, present only in proxy mode * @param operation existing typed operation * @param resource exact non-bearer resource reference * @param relationship established object relationship * @param context safe closed-condition context * @param approvalId optional durable approval reference * @param correlationId safe finite request correlation identity */ public record Request(RealmId realmId, String principalId, AdministrativeAuthenticationMode authenticationMode, Optional transportPrincipalId, AdministrativeOperation operation, Permission.Resource resource, Permission.Relationship relationship, Permission.Context context, Optional approvalId, String correlationId) { /** Validates the immutable request. */ public Request { Objects.requireNonNull(realmId, "realmId"); Permission.requirePrincipal(principalId); Objects.requireNonNull(authenticationMode, "authenticationMode"); transportPrincipalId = Objects.requireNonNull(transportPrincipalId, "transportPrincipalId"); transportPrincipalId.ifPresent(Permission::requirePrincipal); if (authenticationMode == AdministrativeAuthenticationMode.DIRECT_MTLS && transportPrincipalId.isPresent() || authenticationMode == AdministrativeAuthenticationMode.TRUSTED_REVERSE_PROXY && transportPrincipalId.isEmpty()) { throw new IllegalArgumentException("Gateway authentication identity context is invalid"); } Objects.requireNonNull(operation, "operation"); Objects.requireNonNull(resource, "resource"); Objects.requireNonNull(relationship, "relationship"); Objects.requireNonNull(context, "context"); approvalId = Objects.requireNonNull(approvalId, "approvalId"); Permission.requireBounded(correlationId, 256, "correlation ID"); } /** Wraps an existing PKI operation for source-compatible embedded callers. */ public Request(RealmId realmId, String principalId, PkiOperation operation, Permission.Resource resource, Permission.Relationship relationship, Permission.Context context, Optional approvalId, String correlationId) { this(realmId, principalId, AdministrativeAuthenticationMode.DIRECT_MTLS, Optional.empty(), new AdministrativeOperation.Pki(operation), resource, relationship, context, approvalId, correlationId); } /** Creates a direct-mTLS request for existing embedded control callers. */ public Request(RealmId realmId, String principalId, AdministrativeOperation operation, Permission.Resource resource, Permission.Relationship relationship, Permission.Context context, Optional approvalId, String correlationId) { this(realmId, principalId, AdministrativeAuthenticationMode.DIRECT_MTLS, Optional.empty(), operation, resource, relationship, context, approvalId, correlationId); } } /** Closed gateway outcome preserving typed backend results. */ public sealed interface Outcome permits Outcome.Executed, Outcome.ControlExecuted, Outcome.Denied, Outcome.ApprovalRequired { /** Successfully admitted operation and exact backend outcome. */ record Executed(PkiOperationOutcome outcome) implements Outcome { /** Validates the backend result. */ public Executed { Objects.requireNonNull(outcome, "outcome"); } } /** Successfully admitted server-control result. */ record ControlExecuted(ServerControlOperationOutcome outcome) implements Outcome { /** Validates the control result. */ public ControlExecuted { Objects.requireNonNull(outcome, "outcome"); } } /** Safe denial without protected-object existence information. */ record Denied(AuthorizationEngine.Code code) implements Outcome { /** Validates the safe code. */ public Denied { Objects.requireNonNull(code, "code"); } } /** High-risk operation requires a separately created durable approval. */ record ApprovalRequired(String operationCommitment) implements Outcome { /** Validates the safe exact-operation commitment. */ public ApprovalRequired { if (operationCommitment == null || !operationCommitment.matches("[0-9a-f]{64}")) throw new IllegalArgumentException("Operation commitment is invalid"); } } } private final RealmId realmId; private final AuthorityExposurePolicy exposure; private final ServerControlStore control; private final RoleTemplateCatalog roles; private final AuthorizationEngine authorization; private final ApprovalService approvals; private final BreakGlassService breakGlass; private final OperationSecurityDescriptors descriptors; private final PkiOperationExecutor executor; private final ServerControlOperationExecutor controlExecutor; private final PkiResourceScopeResolver resourceScopes; private final Map approvalPolicies; private final SafeAudit audit; private final Runnable openCheck; /** Creates one gateway bound to one realm and one session executor. */ public ServerOperationGateway(RealmId realmId, AuthorityExposurePolicy exposure, ServerControlStore control, RoleTemplateCatalog roles, AuthorizationEngine authorization, ApprovalService approvals, BreakGlassService breakGlass, DisclosureService disclosure, AuditorViews auditorViews, Optional repositoryAliases, OperationSecurityDescriptors descriptors, PkiOperationExecutor executor, PkiResourceScopeResolver resourceScopes, Map approvalPolicies, java.time.Clock clock, zeroecho.pki.spi.audit.AuditSink auditSink, Runnable openCheck) { this.realmId = Objects.requireNonNull(realmId, "realmId"); this.exposure = Objects.requireNonNull(exposure, "exposure"); this.control = Objects.requireNonNull(control, "control"); this.roles = Objects.requireNonNull(roles, "roles"); this.authorization = Objects.requireNonNull(authorization, "authorization"); this.approvals = Objects.requireNonNull(approvals, "approvals"); this.breakGlass = Objects.requireNonNull(breakGlass, "breakGlass"); this.descriptors = Objects.requireNonNull(descriptors, "descriptors"); this.executor = Objects.requireNonNull(executor, "executor"); this.controlExecutor = new ServerControlOperationExecutor(realmId, exposure, control, roles, authorization, approvals, breakGlass, disclosure, auditorViews, repositoryAliases, descriptors, approvalPolicies); this.resourceScopes = Objects.requireNonNull(resourceScopes, "resourceScopes"); this.approvalPolicies = Map.copyOf(Objects.requireNonNull(approvalPolicies, "approvalPolicies")); this.audit = new SafeAudit(clock, auditSink); this.openCheck = Objects.requireNonNull(openCheck, "openCheck"); } /** Installs the optional configured ACME control capability once before listener readiness. */ public void installAcme(AcmeService service) { controlExecutor.installAcme(service); } /** Installs the realm-owned OCSP administration capability before readiness. */ public void installOcsp(OcspResponderService service) { controlExecutor.installOcsp(service); } /** Creates the pre-control-plane gateway surface for embedded source compatibility. */ public ServerOperationGateway(RealmId realmId, AuthorityExposurePolicy exposure, ServerControlStore control, RoleTemplateCatalog roles, AuthorizationEngine authorization, ApprovalService approvals, BreakGlassService breakGlass, OperationSecurityDescriptors descriptors, PkiOperationExecutor executor, PkiResourceScopeResolver resourceScopes, Map approvalPolicies, java.time.Clock clock, zeroecho.pki.spi.audit.AuditSink auditSink, Runnable openCheck) { this(realmId, exposure, control, roles, authorization, approvals, breakGlass, new DisclosureService(realmId, control, DisclosureService.Defaults.recommended(), clock, new java.security.SecureRandom(), auditSink), new AuditorViews(clock, auditSink), Optional.empty(), descriptors, executor, resourceScopes, approvalPolicies, clock, auditSink, openCheck); } /** * Admits and synchronously executes one operation. * * @param request complete security request * @param cancellation cooperative cancellation signal passed unchanged to PKI * @return safe gateway outcome */ public Outcome execute(Request request, CancellationSignal cancellation) { openCheck.run(); Request exact = Objects.requireNonNull(request, "request"); Objects.requireNonNull(cancellation, "cancellation"); if (!realmId.equals(exact.realmId()) || !realmId.equals(exact.resource().scope().realmId())) { return denied(exact, AuthorizationEngine.Code.OUTSIDE_REALM); } OperationSecurityDescriptors.Descriptor descriptor; try { descriptor = descriptors.require(exact.operation()); if (exact.operation() instanceof AdministrativeOperation.Pki pki) { descriptors.validateResource(pki.operation(), exact.resource()); validateAuthoritativeScope(pki.operation(), exact.resource()); } else if (descriptor.resourceType() != exact.resource().type()) { throw new SecurityException("Control operation resource type differs"); } else { validateControlScope(((AdministrativeOperation.Control) exact.operation()).operation(), exact.resource()); } } catch (SecurityException | IllegalArgumentException invalid) { return denied(exact, AuthorizationEngine.Code.NO_MATCHING_GRANT); } if (exact.operation() instanceof AdministrativeOperation.Pki pki && pki.operation() instanceof PkiOperation.CreateAuthority && !exposure.authorityCreationPermitted()) { return denied(exact, AuthorizationEngine.Code.OUTSIDE_AUTHORITY_SCOPE); } SecurityPrincipal principal; try { principal = control.requirePrincipal(exact.principalId()); } catch (IllegalArgumentException unavailable) { return denied(exact, AuthorizationEngine.Code.NO_MATCHING_GRANT); } List grants = new ArrayList<>(control.grantsFor(principal.principalId())); for (RoleTemplateCatalog.Assignment assignment : control.assignmentsFor(principal.principalId())) { grants.addAll(roles.instantiate(assignment)); } BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principal.principalId()); grants.addAll(emergency.grants()); Permission.Action action = exact.operation() instanceof AdministrativeOperation.Pki pki ? action(pki.operation(), descriptor.action()) : descriptor.action(); Permission.Context conditionContext = approvalContext(exact); AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(realmId, exposure, principal, action, exact.resource(), exact.relationship(), descriptor.dataView(), conditionContext, grants, emergency.grantIds())); boolean filterableList = exact.operation() instanceof AdministrativeOperation.Pki pki && pki.operation() instanceof PkiOperation.ListAuthorities && decision.code() == AuthorizationEngine.Code.NO_MATCHING_GRANT; if (!decision.allowed() && !filterableList) { return denied(exact, decision.code()); } if (exact.operation() instanceof AdministrativeOperation.Control controlOperation && controlOperation.operation() instanceof ServerControlOperation.RequestApproval requestApproval) { OperationSecurityDescriptors.Descriptor target = descriptors.require(requestApproval.targetOperation()); if (target.resourceType() != requestApproval.targetResource().type()) { return denied(exact, AuthorizationEngine.Code.NO_MATCHING_GRANT); } if (requestApproval.targetOperation() instanceof AdministrativeOperation.Pki targetPki) { try { descriptors.validateResource(targetPki.operation(), requestApproval.targetResource()); } catch (SecurityException mismatch) { return denied(exact, AuthorizationEngine.Code.NO_MATCHING_GRANT); } } AuthorizationEngine.Decision targetDecision = authorization.authorize(new AuthorizationEngine.Request( realmId, exposure, principal, target.action(), requestApproval.targetResource(), exact.relationship(), target.dataView(), exact.context(), grants, emergency.grantIds())); if (!targetDecision.allowed()) return denied(exact, targetDecision.code()); } if (decision.usedBreakGlass()) breakGlass.auditUse(principal.principalId()); String commitment = descriptors.commitment(realmId, exact.operation(), exact.resource()); Optional claimedApproval = Optional.empty(); if (descriptor.approvalCategory() == OperationSecurityDescriptors.ApprovalCategory.HIGH_RISK) { ApprovalService.Policy expected = approvalPolicies.get(descriptor.approvalCategory()); if (expected == null) throw new IllegalStateException("High-risk approval policy is not configured"); if (exact.approvalId().isEmpty()) return new Outcome.ApprovalRequired(commitment); ApprovalService.Request current = approvals.requireCurrent(exact.approvalId().orElseThrow()); if (!current.policyCommitment().equals(expected.commitment())) { return denied(exact, AuthorizationEngine.Code.APPROVAL_REQUIRED); } approvals.claim(current.approvalId(), exact.operation().name(), commitment, exact.resource().scope()); claimedApproval = Optional.of(current.approvalId()); } if (exact.operation() instanceof AdministrativeOperation.Pki pki) { PkiOperationOutcome backend = executor.execute(pki.operation(), cancellation); if (pki.operation() instanceof PkiOperation.ListAuthorities) { backend = filterAuthorities(backend, principal, grants, emergency.grantIds(), exact.context()); } if (claimedApproval.isPresent()) approvals.complete(claimedApproval.orElseThrow(), classification(backend)); audit.record("GATEWAY_RESULT", principal.principalId(), exact.resource().objectId(), gatewayDetails(exact, "result", classification(backend))); return new Outcome.Executed(backend); } ServerControlOperation controlOperation = ((AdministrativeOperation.Control) exact.operation()).operation(); ServerControlOperationOutcome controlResult = controlExecutor.execute(controlOperation, principal.principalId(), exact.resource(), exact.approvalId(), cancellation); String result = classification(controlResult); if (claimedApproval.isPresent()) approvals.complete(claimedApproval.orElseThrow(), result); audit.record("GATEWAY_RESULT", principal.principalId(), exact.resource().objectId(), gatewayDetails(exact, "result", result)); return new Outcome.ControlExecuted(controlResult); } /** * Authorizes one non-PKI administrative control resource through the same * default-deny engine used for typed operations. * * @param principalId authenticated persisted principal * @param action exact control action * @param resource exact realm control resource * @param context safe closed-condition context * @param correlationId bounded request correlation identity * @return safe authorization decision */ public AuthorizationEngine.Decision authorizeControl(String principalId, Permission.Action action, Permission.Resource resource, Permission.Context context, String correlationId) { openCheck.run(); Permission.requirePrincipal(principalId); Permission.requireBounded(correlationId, 256, "correlation ID"); SecurityPrincipal principal = control.requirePrincipal(principalId); List grants = grants(principal); BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principalId); grants.addAll(emergency.grants()); AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(realmId, exposure, principal, action, resource, Permission.Relationship.ANY, Permission.DataView.METADATA_REDACTED, context, grants, emergency.grantIds())); audit.record(decision.allowed() ? "AUTHORIZATION_ALLOW" : "AUTHORIZATION_DENY", principalId, Optional.empty(), Map.of("action", action.name(), "code", decision.code().name(), "correlation", correlationId)); if (decision.usedBreakGlass()) breakGlass.auditUse(principalId); return decision; } /** * Authorizes a dedicated service principal to attest one forwarded client * identity. Break-glass grants are deliberately excluded from this transport * trust decision. * * @param principalId authenticated backend TLS peer identity * @param correlationId bounded request correlation identity * @return default-deny decision with explicit-deny precedence */ public AuthorizationEngine.Decision authorizeForwardedIdentity(String principalId, String correlationId) { openCheck.run(); Permission.requirePrincipal(principalId); Permission.requireBounded(correlationId, 256, "correlation ID"); SecurityPrincipal principal = control.requirePrincipal(principalId); if (principal.type() != SecurityPrincipal.Type.SERVICE) { return new AuthorizationEngine.Decision(AuthorizationEngine.Code.NO_MATCHING_GRANT, false); } List principalGrants = grants(principal); if (principalGrants.stream().anyMatch(grant -> grant.enabled() && grant.effect() == Permission.Effect.ALLOW && grant.action() != Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY) || !breakGlass.activeFor(principalId).grants().isEmpty()) { return new AuthorizationEngine.Decision(AuthorizationEngine.Code.NO_MATCHING_GRANT, false); } Permission.Resource resource = new Permission.Resource(Permission.ResourceType.REALM, new Permission.Scope(realmId, Optional.empty(), Optional.empty(), Optional.empty()), Optional.empty(), Optional.empty()); AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(realmId, exposure, principal, Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY, resource, Permission.Relationship.ANY, Permission.DataView.METADATA_REDACTED, Permission.Context.empty(), principalGrants, java.util.Set.of())); audit.record(decision.allowed() ? "PROXY_FORWARD_ALLOW" : "PROXY_FORWARD_DENY", principalId, Optional.empty(), Map.of("action", Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY.name(), "code", decision.code().name(), "correlation", correlationId)); return decision; } /** * Fails startup when a configured proxy service principal has any enabled * allowing authority beyond forwarded-identity attestation. * * @param principalId configured trusted proxy principal */ public void validateForwardingPrincipal(String principalId) { openCheck.run(); SecurityPrincipal principal = control.requirePrincipal(principalId); if (!principal.enabled() || principal.type() != SecurityPrincipal.Type.SERVICE || grants(principal).stream().anyMatch(grant -> grant.enabled() && grant.effect() == Permission.Effect.ALLOW && grant.action() != Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY)) { throw new IllegalArgumentException("Trusted proxy principal authority is not narrowly scoped"); } } /** * Returns operation descriptors discoverable by one principal without * returning grant scopes or protected resource identities. * * @param principalId authenticated persisted principal * @return descriptors ordered by stable operation identity */ public List discoverableOperations(String principalId) { openCheck.run(); SecurityPrincipal principal = control.requirePrincipal(principalId); List grants = grants(principal); BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principalId); grants.addAll(emergency.grants()); return descriptors.descriptors().values().stream() .filter(descriptor -> discoverable(descriptor, principal, grants, emergency.grantIds())) .sorted(java.util.Comparator.comparing(OperationSecurityDescriptors.Descriptor::operationId)) .toList(); } /** * Resolves the existing security descriptor for a decoded typed operation. * * @param operation decoded transport-neutral operation * @return immutable descriptor from the sole gateway descriptor authority * @throws SecurityException when the operation is not remotely exposed */ public OperationSecurityDescriptors.Descriptor descriptor(PkiOperation operation) { openCheck.run(); return descriptors.require(operation); } /** Resolves one descriptor by its immutable administrative operation family. */ public OperationSecurityDescriptors.Descriptor descriptor(AdministrativeOperation operation) { openCheck.run(); return descriptors.require(operation); } /** Resolves one descriptor by stable ID before closed typed decoding. */ public OperationSecurityDescriptors.Descriptor descriptor(String operationId) { openCheck.run(); return descriptors.require(operationId); } private List grants(SecurityPrincipal principal) { List grants = new ArrayList<>(control.grantsFor(principal.principalId())); for (RoleTemplateCatalog.Assignment assignment : control.assignmentsFor(principal.principalId())) { grants.addAll(roles.instantiate(assignment)); } return grants; } private boolean discoverable(OperationSecurityDescriptors.Descriptor descriptor, SecurityPrincipal principal, List grants, java.util.Set breakGlassIds) { for (Permission.Grant grant : grants) { if (grant.action() != descriptor.action() || grant.resourceType() != descriptor.resourceType()) { continue; } Permission.Resource resource = new Permission.Resource(descriptor.resourceType(), grant.scope(), Optional.empty(), Optional.empty()); AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request( realmId, exposure, principal, descriptor.action(), resource, Permission.Relationship.ANY, descriptor.dataView(), Permission.Context.empty(), grants, breakGlassIds)); if (decision.allowed()) return true; } return false; } private PkiOperationOutcome filterAuthorities(PkiOperationOutcome outcome, SecurityPrincipal principal, List grants, java.util.Set breakGlassIds, Permission.Context context) { if (!(outcome instanceof PkiOperationOutcome.Success success)) return outcome; PkiOperationValue value = success.result().fields().get("authorities"); if (!(value instanceof PkiOperationValue.ListValue list)) return outcome; List allowed = list.values().stream().filter(item -> authorityAllowed(item, principal, grants, breakGlassIds, context)).toList(); Map fields = new LinkedHashMap<>(success.result().fields()); fields.put("count", new PkiOperationValue.IntegerValue(allowed.size())); fields.put("authorities", new PkiOperationValue.ListValue(allowed)); return new PkiOperationOutcome.Success(new PkiOperationResult(success.result().operationName(), fields)); } private boolean authorityAllowed(PkiOperationValue value, SecurityPrincipal principal, List grants, java.util.Set breakGlassIds, Permission.Context context) { if (!(value instanceof PkiOperationValue.ObjectValue object) || !(object.fields().get("caId") instanceof PkiOperationValue.Text id)) return false; PkiId authorityId = new PkiId(id.value()); if (!exposure.allows(authorityId)) return false; Permission.Resource resource = new Permission.Resource(Permission.ResourceType.AUTHORITY, new Permission.Scope(realmId, Optional.of(authorityId), Optional.empty(), Optional.empty()), Optional.of(authorityId), Optional.empty()); return authorization.authorize(new AuthorizationEngine.Request(realmId, exposure, principal, Permission.Action.AUTHORITY_LIST, resource, Permission.Relationship.ANY, Permission.DataView.METADATA_REDACTED, context, grants, breakGlassIds)).allowed(); } private Permission.Context approvalContext(Request request) { return new Permission.Context(request.context().reason(), request.approvalId().isPresent(), request.context().authorityActive(), request.context().safeAttributes()); } private void validateAuthoritativeScope(PkiOperation operation, Permission.Resource resource) { Optional actual = switch (operation) { case PkiOperation.InspectCredential value -> resourceScopes.credentialAuthority(value.credentialId()); case PkiOperation.RevokeCredential value -> resourceScopes.credentialAuthority(value.credentialId()); case PkiOperation.ReadRevocationHistory value -> resourceScopes.credentialAuthority(value.credentialId()); case PkiOperation.InspectPublication value -> resourceScopes.publicationAuthority(value.publicationId()); case PkiOperation.ProcessPublication value -> resourceScopes.publicationAuthority(value.publicationId()); default -> resource.scope().authorityId(); }; if (requiresResolvedAuthority(operation) && (actual.isEmpty() || !actual.equals(resource.scope().authorityId()))) { throw new SecurityException("Authoritative object scope does not match the request"); } } private void validateControlScope(ServerControlOperation operation, Permission.Resource resource) { Permission.Scope actual = switch (operation) { case ServerControlOperation.CreateRoleAssignment value -> value.assignment().scope(); case ServerControlOperation.InspectRoleAssignment value -> control.requireAssignment(value.assignmentId()).scope(); case ServerControlOperation.RevokeRoleAssignment value -> control.requireAssignment(value.assignmentId()).scope(); case ServerControlOperation.CreateGrant value -> value.grant().scope(); case ServerControlOperation.InspectGrant value -> control.requireGrant(value.grantId()).scope(); case ServerControlOperation.RevokeGrant value -> control.requireGrant(value.grantId()).scope(); case ServerControlOperation.EvaluateAuthorization value -> value.resource().scope(); case ServerControlOperation.RequestApproval value -> value.targetResource().scope(); case ServerControlOperation.InspectApproval value -> approvals.requireCurrent(value.approvalId()).scope(); case ServerControlOperation.DecideApproval value -> approvals.requireCurrent(value.approvalId()).scope(); case ServerControlOperation.CancelApproval value -> approvals.requireCurrent(value.approvalId()).scope(); case ServerControlOperation.CreateBreakGlass value -> value.grant().scope(); case ServerControlOperation.ActivateBreakGlass value -> breakGlass.requireCurrent(value.breakGlassId()).grant().scope(); case ServerControlOperation.InspectBreakGlass value -> breakGlass.requireCurrent(value.breakGlassId()).grant().scope(); case ServerControlOperation.RevokeBreakGlass value -> breakGlass.requireCurrent(value.breakGlassId()).grant().scope(); case ServerControlOperation.RegisterAcmeDirectory value -> new Permission.Scope(realmId, Optional.of(value.registration().authorityId()), Optional.empty(), Optional.of(value.registration().profileId())); case ServerControlOperation.InspectAcmeDirectory value -> controlExecutor.acmeScope(value); case ServerControlOperation.SetAcmeDirectoryActive value -> controlExecutor.acmeScope(value); case ServerControlOperation.InspectAcmeAccount value -> controlExecutor.acmeScope(value); case ServerControlOperation.DeactivateAcmeAccount value -> controlExecutor.acmeScope(value); case ServerControlOperation.RegisterOcspResponder value -> new Permission.Scope(realmId, Optional.of(value.registration().authorityId()), Optional.of(value.registration().issuerId()), Optional.empty()); case ServerControlOperation.InspectOcspResponder value -> { OcspResponderService.Responder responder = controlExecutor.ocspResponder(value.responderId()); yield new Permission.Scope(realmId, Optional.of(responder.authorityId()), Optional.of(responder.issuerId()), Optional.empty()); } case ServerControlOperation.SetOcspResponderActive value -> { OcspResponderService.Responder responder = controlExecutor.ocspResponder(value.responderId()); yield new Permission.Scope(realmId, Optional.of(responder.authorityId()), Optional.of(responder.issuerId()), Optional.empty()); } default -> resource.scope(); }; if (!actual.equals(resource.scope())) throw new SecurityException("Control scope differs"); } private static boolean requiresResolvedAuthority(PkiOperation operation) { return operation instanceof PkiOperation.InspectCredential || operation instanceof PkiOperation.RevokeCredential || operation instanceof PkiOperation.ReadRevocationHistory || operation instanceof PkiOperation.InspectPublication || operation instanceof PkiOperation.ProcessPublication; } private Outcome denied(Request request, AuthorizationEngine.Code code) { audit.record("AUTHORIZATION_DENY", request.principalId(), Optional.empty(), gatewayDetails(request, "code", code.name())); return new Outcome.Denied(code); } private static Map gatewayDetails(Request request, String resultName, String result) { Map details = new LinkedHashMap<>(); details.put("operation", request.operation().name()); details.put(resultName, result); details.put("correlation", request.correlationId()); details.put("authenticationMode", request.authenticationMode().name()); details.put("endClientPrincipal", request.principalId()); request.transportPrincipalId().ifPresent(value -> details.put("transportPrincipal", value)); return Map.copyOf(details); } private static Permission.Action action(PkiOperation operation, Permission.Action defaultAction) { if (operation instanceof PkiOperation.TransitionAuthority transition) { return switch (transition.state()) { case ACTIVE -> Permission.Action.AUTHORITY_ACTIVATE; case RETIRED -> Permission.Action.AUTHORITY_RETIRE; case COMPROMISED, DISABLED -> Permission.Action.AUTHORITY_SUSPEND; }; } return defaultAction; } private static String classification(PkiOperationOutcome outcome) { return switch (outcome) { case PkiOperationOutcome.Success ignored -> "SUCCEEDED"; case PkiOperationOutcome.Failure failure -> failure.classification().name(); }; } private static String classification(ServerControlOperationOutcome outcome) { if (outcome instanceof ServerControlOperationOutcome.OneTimeSensitive) return "SUCCEEDED"; PkiOperationOutcome ordinary = ((ServerControlOperationOutcome.Ordinary) outcome).outcome(); return classification(ordinary); } }