Files
ZeroEcho/pki-server/src/main/java/zeroecho/pki/server/ServerOperationGateway.java
Leo Galambos 44cbb0a37d feat(pki-server): add OCSP responder and close server release
Add durable multi-authority OCSP responders with strict request parsing,
issuer-bound serial lookup, stable revocation views, signed responses,
nonce policies and bounded protocol execution.

Complete in-process and packaged OCSP validation and close the PKI server
after the final architecture, security and release audit.
2026-08-05 18:32:00 +02:00

610 lines
37 KiB
Java

/*******************************************************************************
* 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.
*
* <p>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.</p>
*/
@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<String> transportPrincipalId,
AdministrativeOperation operation,
Permission.Resource resource, Permission.Relationship relationship, Permission.Context context,
Optional<String> 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<String> 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<String> 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<OperationSecurityDescriptors.ApprovalCategory, ApprovalService.Policy> 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<RepositoryAliasService> repositoryAliases,
OperationSecurityDescriptors descriptors, PkiOperationExecutor executor,
PkiResourceScopeResolver resourceScopes,
Map<OperationSecurityDescriptors.ApprovalCategory, ApprovalService.Policy> 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<OperationSecurityDescriptors.ApprovalCategory, ApprovalService.Policy> 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<Permission.Grant> 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<String> 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<Permission.Grant> 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<Permission.Grant> 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<OperationSecurityDescriptors.Descriptor> discoverableOperations(String principalId) {
openCheck.run();
SecurityPrincipal principal = control.requirePrincipal(principalId);
List<Permission.Grant> 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<Permission.Grant> grants(SecurityPrincipal principal) {
List<Permission.Grant> 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<Permission.Grant> grants,
java.util.Set<String> 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<Permission.Grant> grants, java.util.Set<String> 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<PkiOperationValue> allowed = list.values().stream().filter(item -> authorityAllowed(item, principal,
grants, breakGlassIds, context)).toList();
Map<String, PkiOperationValue> 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<Permission.Grant> grants, java.util.Set<String> 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<PkiId> 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<String, String> gatewayDetails(Request request, String resultName, String result) {
Map<String, String> 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);
}
}