feat(pki-server): expose security control administration

Expose principals, scoped assignments, direct grants, approvals,
break-glass access, disclosure policy and one-time capabilities through
the unified administrative HTTPS operation gateway.

Preserve default-deny authorization, immutable PKI authority, PII
boundaries and uncertainty-aware secret delivery.
This commit is contained in:
2026-08-04 21:23:01 +02:00
parent d7793e5c49
commit 5b896ee2a2
22 changed files with 2027 additions and 93 deletions

View File

@@ -0,0 +1,59 @@
/*******************************************************************************
* 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.Objects;
import zeroecho.pki.application.PkiOperation;
/** One closed administrative namespace spanning PKI and server-control families. */
public sealed interface AdministrativeOperation permits AdministrativeOperation.Pki,
AdministrativeOperation.Control {
/** @return stable operation identity */
String name();
/** Existing transport-neutral PKI operation family. */
record Pki(PkiOperation operation) implements AdministrativeOperation {
/** Validates the typed PKI operation. */
public Pki { Objects.requireNonNull(operation, "operation"); }
@Override public String name() { return operation.name(); }
}
/** Server security-control operation family. */
record Control(ServerControlOperation operation) implements AdministrativeOperation {
/** Validates the typed control operation. */
public Control { Objects.requireNonNull(operation, "operation"); }
@Override public String name() { return operation.name(); }
}
}

View File

@@ -43,17 +43,18 @@ import java.util.Optional;
/** Durable, scoped, mandatory-expiry emergency authorization service. */
@SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.FieldDeclarationsShouldBeAtStartOfClass",
"PMD.UseObjectForClearerAPI", "PMD.AvoidSynchronizedAtMethodLevel" })
"PMD.UseObjectForClearerAPI", "PMD.AvoidSynchronizedAtMethodLevel",
"PMD.ExcessiveParameterList" })
public final class BreakGlassService {
/** Break-glass lifecycle states. */
public enum State {
ACTIVE(1), REVOKED(2), EXPIRED(3);
ACTIVE(1), REVOKED(2), EXPIRED(3), PENDING(4);
private final int code;
State(int code) { this.code = code; }
/** @return stable persistence code */ public int code() { return code; }
/** Resolves one stable persistence code. */
public static State fromCode(int code) {
return switch (code) { case 1 -> ACTIVE; case 2 -> REVOKED; case 3 -> EXPIRED;
return switch (code) { case 1 -> ACTIVE; case 2 -> REVOKED; case 3 -> EXPIRED; case 4 -> PENDING;
default -> throw new IllegalArgumentException("Unknown break-glass state code"); };
}
}
@@ -74,7 +75,7 @@ public final class BreakGlassService {
* @param auditCommitment stable safe commitment
*/
public record Record(String breakGlassId, String principalId, Permission.Grant grant, String reason,
String issuerPrincipalId, Optional<String> approvalId, Instant createdAt, Instant activatedAt,
String issuerPrincipalId, Optional<String> approvalId, Instant createdAt, Optional<Instant> activatedAt,
Instant expiresAt, State state, String auditCommitment) {
/** Validates the strict time-limited scoped record. */
public Record {
@@ -91,17 +92,30 @@ public final class BreakGlassService {
}
approvalId = Objects.requireNonNull(approvalId, "approvalId");
Objects.requireNonNull(createdAt, "createdAt");
Objects.requireNonNull(activatedAt, "activatedAt");
activatedAt = Objects.requireNonNull(activatedAt, "activatedAt");
Objects.requireNonNull(expiresAt, "expiresAt");
if (activatedAt.isBefore(createdAt) || !expiresAt.isAfter(activatedAt)
|| Duration.between(activatedAt, expiresAt).compareTo(Duration.ofHours(24)) > 0) {
Instant lifetimeStart = activatedAt.orElse(createdAt);
if (activatedAt.filter(value -> value.isBefore(createdAt)).isPresent() || !expiresAt.isAfter(lifetimeStart)
|| Duration.between(lifetimeStart, expiresAt).compareTo(Duration.ofHours(24)) > 0) {
throw new IllegalArgumentException("Break-glass lifetime is invalid");
}
Objects.requireNonNull(state, "state");
if (state == State.PENDING && activatedAt.isPresent()
|| state == State.ACTIVE && activatedAt.isEmpty()) {
throw new IllegalArgumentException("Break-glass activation state is invalid");
}
if (auditCommitment == null || !auditCommitment.matches("[0-9a-f]{64}")) {
throw new IllegalArgumentException("Break-glass audit commitment is invalid");
}
}
/** Creates an activated record for existing embedded callers. */
public Record(String breakGlassId, String principalId, Permission.Grant grant, String reason,
String issuerPrincipalId, Optional<String> approvalId, Instant createdAt, Instant activatedAt,
Instant expiresAt, State state, String auditCommitment) {
this(breakGlassId, principalId, grant, reason, issuerPrincipalId, approvalId, createdAt,
Optional.of(activatedAt), expiresAt, state, auditCommitment);
}
}
/** Active grants plus their identities for authorization and use auditing. */
@@ -124,21 +138,38 @@ public final class BreakGlassService {
this.audit = new SafeAudit(clock, auditSink);
}
/** Creates and immediately activates one independently issued emergency grant. */
public synchronized Record activate(String breakGlassId, String principalId, Permission.Grant grant, String reason,
/** Creates one durable pending emergency grant without activating it. */
public synchronized Record create(String breakGlassId, String principalId, Permission.Grant grant, String reason,
String issuerPrincipalId, Optional<String> approvalId, Instant expiresAt) {
store.requirePrincipal(principalId);
store.requirePrincipal(issuerPrincipalId);
store.requirePrincipal(principalId); store.requirePrincipal(issuerPrincipalId);
requireExecutingApproval(approvalId, ServerControlOperation.CreateBreakGlass.CREATE, grant.scope());
Instant now = clock.instant();
String commitment = ApprovalService.digest(breakGlassId + '|' + principalId + '|' + grant.grantId()
+ '|' + issuerPrincipalId + '|' + now + '|' + expiresAt);
Record record = new Record(breakGlassId, principalId, grant, reason, issuerPrincipalId, approvalId,
now, now, expiresAt, State.ACTIVE, commitment);
now, Optional.empty(), expiresAt, State.PENDING, commitment);
store.createBreakGlass(record);
audit.record("BREAK_GLASS_ACTIVATE", issuerPrincipalId, Optional.empty(), Map.of("state", "ACTIVE"));
audit.record("BREAK_GLASS_CREATE", issuerPrincipalId, Optional.empty(), Map.of("state", "PENDING"));
return record;
}
/** Activates one independently approved pending emergency grant. */
public synchronized Record activatePending(String breakGlassId, String actorPrincipalId,
Optional<String> approvalId) {
Record current = requireCurrent(breakGlassId);
if (current.state() != State.PENDING) {
throw new IllegalStateException("Break-glass record is not approved for activation");
}
requireExecutingApproval(approvalId, ServerControlOperation.ActivateBreakGlass.ACTIVATE,
current.grant().scope());
Record active = new Record(current.breakGlassId(), current.principalId(), current.grant(), current.reason(),
current.issuerPrincipalId(), approvalId, current.createdAt(), Optional.of(clock.instant()),
current.expiresAt(), State.ACTIVE, current.auditCommitment());
store.replaceBreakGlass(current, active);
audit.record("BREAK_GLASS_ACTIVATE", actorPrincipalId, Optional.empty(), Map.of("state", "ACTIVE"));
return active;
}
/** Revokes an active grant. */
public synchronized Record revoke(String breakGlassId, String actorPrincipalId) {
Record current = requireCurrent(breakGlassId);
@@ -152,7 +183,8 @@ public final class BreakGlassService {
/** Returns a current record, durably applying expiry when observed. */
public synchronized Record requireCurrent(String breakGlassId) {
Record current = store.requireBreakGlass(breakGlassId);
if (current.state() == State.ACTIVE && !clock.instant().isBefore(current.expiresAt())) {
if ((current.state() == State.ACTIVE || current.state() == State.PENDING)
&& !clock.instant().isBefore(current.expiresAt())) {
Record expired = copy(current, State.EXPIRED);
store.replaceBreakGlass(current, expired);
audit.record("BREAK_GLASS_EXPIRY", current.principalId(), Optional.empty(), Map.of("state", "EXPIRED"));
@@ -180,4 +212,14 @@ public final class BreakGlassService {
source.issuerPrincipalId(), source.approvalId(), source.createdAt(), source.activatedAt(),
source.expiresAt(), state, source.auditCommitment());
}
private void requireExecutingApproval(Optional<String> approvalId, String operationId,
Permission.Scope scope) {
ApprovalService.Request approval = store.requireApproval(approvalId
.orElseThrow(() -> new IllegalStateException("Break-glass approval is required")));
if (approval.state() != ApprovalService.State.EXECUTING || !approval.operationId().equals(operationId)
|| !approval.scope().equals(scope)) {
throw new IllegalStateException("Break-glass approval is not executable");
}
}
}

View File

@@ -51,6 +51,18 @@ import zeroecho.pki.api.PkiId;
@SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.AvoidLiteralsInIfCondition",
"PMD.FieldDeclarationsShouldBeAtStartOfClass", "PMD.AvoidSynchronizedAtMethodLevel" })
public final class DisclosureService {
/** Durable one-time delivery classification without retaining the raw token. */
public enum DeliveryState {
PENDING(1), DELIVERED(2), DELIVERY_UNKNOWN(3);
private final int code;
DeliveryState(int code) { this.code = code; }
/** @return stable persistence code */ public int code() { return code; }
/** Resolves one stable persistence code. */
public static DeliveryState fromCode(int code) {
return switch (code) { case 1 -> PENDING; case 2 -> DELIVERED; case 3 -> DELIVERY_UNKNOWN;
default -> throw new IllegalArgumentException("Unknown capability delivery code"); };
}
}
/** Persisted disclosure states, independent of issuance and publication. */
public enum Policy {
PUBLIC(1), PUBLIC_UNLISTED(2), AUTHENTICATED(3), OWNER_ONLY(4), RESTRICTED(5), NOT_PUBLISHED(6);
@@ -133,32 +145,58 @@ public final class DisclosureService {
/** Persisted commitment-only capability authority. */
public record Capability(String capabilityId, RealmId realmId, PkiId objectId, String action,
byte[] tokenCommitment, Instant expiresAt, boolean revoked) {
String issuerPrincipalId, byte[] tokenCommitment, Instant expiresAt,
DeliveryState deliveryState, boolean revoked) {
/** Validates and snapshots capability metadata. */
public Capability {
Permission.requireId(capabilityId, "capability");
Objects.requireNonNull(realmId, "realmId");
Objects.requireNonNull(objectId, "objectId");
Permission.requireBounded(action, 128, "capability action");
Permission.requirePrincipal(issuerPrincipalId);
tokenCommitment = Objects.requireNonNull(tokenCommitment, "tokenCommitment").clone();
if (tokenCommitment.length != 32) throw new IllegalArgumentException("Capability commitment length");
Objects.requireNonNull(expiresAt, "expiresAt");
Objects.requireNonNull(deliveryState, "deliveryState");
}
/** Returns a defensive commitment copy. */
@Override public byte[] tokenCommitment() { return tokenCommitment.clone(); }
/** Creates legacy test metadata with an explicit non-secret system issuer. */
public Capability(String capabilityId, RealmId realmId, PkiId objectId, String action,
byte[] tokenCommitment, Instant expiresAt, boolean revoked) {
this(capabilityId, realmId, objectId, action, "system", tokenCommitment, expiresAt, revoked);
}
/** Creates capability metadata in the delivered state for embedded compatibility. */
public Capability(String capabilityId, RealmId realmId, PkiId objectId, String action,
String issuerPrincipalId, byte[] tokenCommitment, Instant expiresAt, boolean revoked) {
this(capabilityId, realmId, objectId, action, issuerPrincipalId, tokenCommitment, expiresAt,
DeliveryState.DELIVERED, revoked);
}
}
/** One-time raw-token issuance result. */
public record IssuedCapability(Capability capability, byte[] token) {
public static final class IssuedCapability {
private final Capability capability;
private final byte[] token;
private boolean consumed;
/** Validates and snapshots the one-time token. */
public IssuedCapability {
Objects.requireNonNull(capability, "capability");
token = Objects.requireNonNull(token, "token").clone();
public IssuedCapability(Capability capability, byte[] token) {
this.capability = Objects.requireNonNull(capability, "capability");
this.token = Objects.requireNonNull(token, "token").clone();
if (token.length != 32) throw new IllegalArgumentException("Capability token length");
}
/** Returns a defensive one-time token copy. */
@Override public byte[] token() { return token.clone(); }
/** @return persisted commitment-only metadata */ public Capability capability() { return capability; }
/** Returns and clears the raw token exactly once. */
public synchronized byte[] token() {
if (consumed) throw new IllegalStateException("Capability token was already consumed");
consumed = true;
byte[] result = token.clone();
java.util.Arrays.fill(token, (byte) 0);
return result;
}
}
/** Safe retrieval result that reveals no protected-object existence. */
@@ -244,6 +282,7 @@ public final class DisclosureService {
/** Issues 256 random capability bits and persists only their commitment. */
public synchronized IssuedCapability issueCapability(PkiId objectId, Instant expiresAt,
String actorPrincipalId) {
store.requirePrincipal(actorPrincipalId);
Record record = store.requireDisclosure(objectId);
if (record.policy() != Policy.PUBLIC_UNLISTED || !expiresAt.isAfter(clock.instant())) {
throw new IllegalStateException("Capability issuance is unavailable for this object");
@@ -253,8 +292,8 @@ public final class DisclosureService {
random.nextBytes(token);
random.nextBytes(identity);
String capabilityId = "cap-" + HexFormat.of().formatHex(identity);
Capability capability = new Capability(capabilityId, realmId, objectId, "RETRIEVE",
capabilityCommitment(objectId, expiresAt, token), expiresAt, false);
Capability capability = new Capability(capabilityId, realmId, objectId, "RETRIEVE", actorPrincipalId,
capabilityCommitment(objectId, expiresAt, token), expiresAt, DeliveryState.PENDING, false);
store.createCapability(capability);
audit.record("CAPABILITY_ISSUE", actorPrincipalId, Optional.of(objectId), Map.of("issued", "true"));
return new IssuedCapability(capability, token);
@@ -264,12 +303,26 @@ public final class DisclosureService {
public synchronized Capability revokeCapability(String capabilityId, String actorPrincipalId) {
Capability current = store.requireCapability(capabilityId);
Capability revoked = new Capability(current.capabilityId(), current.realmId(), current.objectId(),
current.action(), current.tokenCommitment(), current.expiresAt(), true);
current.action(), current.issuerPrincipalId(), current.tokenCommitment(), current.expiresAt(),
current.deliveryState(), true);
store.replaceCapability(current, revoked);
audit.record("CAPABILITY_REVOKE", actorPrincipalId, Optional.of(current.objectId()), Map.of("revoked", "true"));
return revoked;
}
/** Persists the safe post-write delivery classification without changing token validity. */
public synchronized Capability classifyDelivery(String capabilityId, boolean delivered) {
Capability current = store.requireCapability(capabilityId);
if (current.deliveryState() != DeliveryState.PENDING) return current;
Capability updated = new Capability(current.capabilityId(), current.realmId(), current.objectId(),
current.action(), current.issuerPrincipalId(), current.tokenCommitment(), current.expiresAt(),
delivered ? DeliveryState.DELIVERED : DeliveryState.DELIVERY_UNKNOWN, current.revoked());
store.replaceCapability(current, updated);
audit.record(delivered ? "CAPABILITY_DELIVERED" : "CAPABILITY_DELIVERY_UNKNOWN",
current.issuerPrincipalId(), Optional.of(current.objectId()), Map.of("delivered", Boolean.toString(delivered)));
return updated;
}
private boolean validCapability(PkiId objectId, byte[] token) {
if (token == null || token.length != 32) return false;
for (Capability capability : store.capabilitiesFor(objectId)) {

View File

@@ -41,8 +41,11 @@ import java.util.Objects;
import zeroecho.pki.application.PkiOperation;
/** Immutable security descriptors keyed by existing typed-operation identities. */
@SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.FieldDeclarationsShouldBeAtStartOfClass" })
@SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.FieldDeclarationsShouldBeAtStartOfClass",
"PMD.AvoidDuplicateLiterals" })
public final class OperationSecurityDescriptors {
/** Internally fixed execution family; callers cannot select it. */
public enum Family { PKI_OPERATION, SERVER_CONTROL_OPERATION }
/** Approval treatment for one operation. */
public enum ApprovalCategory { NONE, HIGH_RISK }
@@ -60,18 +63,27 @@ public final class OperationSecurityDescriptors {
* @param approvalCategory approval treatment
* @param eligibility future transport eligibility metadata
*/
public record Descriptor(String operationId, Permission.Action action, Permission.ResourceType resourceType,
public record Descriptor(String operationId, Family family, Permission.Action action, Permission.ResourceType resourceType,
boolean mutating, Permission.DataView dataView, ApprovalCategory approvalCategory,
Eligibility eligibility) {
/** Validates one finite descriptor. */
public Descriptor {
Permission.requireBounded(operationId, 256, "operation ID");
Objects.requireNonNull(family, "family");
Objects.requireNonNull(action, "action");
Objects.requireNonNull(resourceType, "resourceType");
Objects.requireNonNull(dataView, "dataView");
Objects.requireNonNull(approvalCategory, "approvalCategory");
Objects.requireNonNull(eligibility, "eligibility");
}
/** Creates an existing PKI-family descriptor for embedded callers. */
public Descriptor(String operationId, Permission.Action action, Permission.ResourceType resourceType,
boolean mutating, Permission.DataView dataView, ApprovalCategory approvalCategory,
Eligibility eligibility) {
this(operationId, Family.PKI_OPERATION, action, resourceType, mutating, dataView,
approvalCategory, eligibility);
}
}
private final Map<String, Descriptor> descriptors;
@@ -94,7 +106,41 @@ public final class OperationSecurityDescriptors {
read(PkiOperation.InspectPublication.NAME, Permission.Action.PUBLICATION_READ, Permission.ResourceType.PUBLICATION),
mutate(PkiOperation.ProcessPublication.NAME, Permission.Action.PUBLICATION_PROCESS, Permission.ResourceType.PUBLICATION, false),
read(PkiOperation.ListAlgorithmBindings.NAME, Permission.Action.X509_BINDING_READ, Permission.ResourceType.X509_BINDING),
read(PkiOperation.InspectAlgorithmBinding.NAME, Permission.Action.X509_BINDING_READ, Permission.ResourceType.X509_BINDING)));
read(PkiOperation.InspectAlgorithmBinding.NAME, Permission.Action.X509_BINDING_READ, Permission.ResourceType.X509_BINDING),
control(ServerControlOperation.RegisterPrincipal.NAME, Permission.Action.PRINCIPAL_MANAGE, Permission.ResourceType.PRINCIPAL, true, false),
control(ServerControlOperation.InspectPrincipal.NAME, Permission.Action.PRINCIPAL_MANAGE, Permission.ResourceType.PRINCIPAL, false, false),
control(ServerControlOperation.ListPrincipals.NAME, Permission.Action.PRINCIPAL_MANAGE, Permission.ResourceType.PRINCIPAL, false, false),
control(ServerControlOperation.SetPrincipalEnabled.ENABLE, Permission.Action.PRINCIPAL_MANAGE, Permission.ResourceType.PRINCIPAL, true, false),
control(ServerControlOperation.SetPrincipalEnabled.DISABLE, Permission.Action.PRINCIPAL_MANAGE, Permission.ResourceType.PRINCIPAL, true, true),
control(ServerControlOperation.ListRoleTemplates.NAME, Permission.Action.ROLE_MANAGE, Permission.ResourceType.ROLE, false, false),
control(ServerControlOperation.InspectRoleTemplate.NAME, Permission.Action.ROLE_MANAGE, Permission.ResourceType.ROLE, false, false),
control(ServerControlOperation.CreateRoleAssignment.NAME, Permission.Action.ROLE_MANAGE, Permission.ResourceType.ROLE, true, true),
control(ServerControlOperation.InspectRoleAssignment.NAME, Permission.Action.ROLE_MANAGE, Permission.ResourceType.ROLE, false, false),
control(ServerControlOperation.ListRoleAssignments.NAME, Permission.Action.ROLE_MANAGE, Permission.ResourceType.ROLE, false, false),
control(ServerControlOperation.RevokeRoleAssignment.NAME, Permission.Action.ROLE_MANAGE, Permission.ResourceType.ROLE, true, false),
control(ServerControlOperation.CreateGrant.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.GRANT, true, true),
control(ServerControlOperation.InspectGrant.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.GRANT, false, false),
control(ServerControlOperation.ListGrants.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.GRANT, false, false),
control(ServerControlOperation.RevokeGrant.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.GRANT, true, false),
control(ServerControlOperation.EvaluateAuthorization.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.GRANT, false, false),
control(ServerControlOperation.RequestApproval.NAME, Permission.Action.REQUEST_SUBMIT, Permission.ResourceType.APPROVAL, true, false),
control(ServerControlOperation.InspectApproval.NAME, Permission.Action.REQUEST_READ_ANY, Permission.ResourceType.APPROVAL, false, false),
control(ServerControlOperation.ListApprovals.NAME, Permission.Action.REQUEST_READ_ANY, Permission.ResourceType.APPROVAL, false, false),
control(ServerControlOperation.DecideApproval.APPROVE, Permission.Action.REQUEST_APPROVE, Permission.ResourceType.APPROVAL, true, false),
control(ServerControlOperation.DecideApproval.REJECT, Permission.Action.REQUEST_REJECT, Permission.ResourceType.APPROVAL, true, false),
control(ServerControlOperation.CancelApproval.NAME, Permission.Action.REQUEST_CANCEL, Permission.ResourceType.APPROVAL, true, false),
control(ServerControlOperation.CreateBreakGlass.CREATE, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.BREAK_GLASS, true, true),
control(ServerControlOperation.ActivateBreakGlass.ACTIVATE, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.BREAK_GLASS, true, true),
control(ServerControlOperation.InspectBreakGlass.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.BREAK_GLASS, false, false),
control(ServerControlOperation.ListBreakGlass.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.BREAK_GLASS, false, false),
control(ServerControlOperation.RevokeBreakGlass.NAME, Permission.Action.PERMISSION_GRANT, Permission.ResourceType.BREAK_GLASS, true, false),
control(ServerControlOperation.InspectDisclosure.NAME, Permission.Action.DISCLOSURE_READ, Permission.ResourceType.DISCLOSURE, false, false),
control(ServerControlOperation.SetDisclosure.NAME, Permission.Action.DISCLOSURE_CHANGE, Permission.ResourceType.DISCLOSURE, true, true),
control(ServerControlOperation.IssueCapability.NAME, Permission.Action.DISCLOSURE_CAPABILITY_ISSUE, Permission.ResourceType.CAPABILITY, true, false),
control(ServerControlOperation.RevokeCapability.NAME, Permission.Action.DISCLOSURE_CAPABILITY_REVOKE, Permission.ResourceType.CAPABILITY, true, false),
control(ServerControlOperation.InspectAuditView.REDACTED, Permission.Action.AUDIT_READ_REDACTED, Permission.ResourceType.AUDIT, false, false),
control(ServerControlOperation.InspectAuditView.FULL, Permission.Action.AUDIT_READ_FULL, Permission.ResourceType.AUDIT, false, false),
control(ServerControlOperation.InspectAuditView.PII, Permission.Action.AUDIT_READ_PII, Permission.ResourceType.AUDIT, false, false)));
}
/** Creates a registry and rejects duplicate operation identities. */
@@ -117,11 +163,30 @@ public final class OperationSecurityDescriptors {
return descriptor;
}
/** Resolves either operation family through the sole descriptor authority. */
public Descriptor require(AdministrativeOperation operation) {
Objects.requireNonNull(operation, "operation");
Descriptor descriptor = descriptors.get(operation.name());
if (descriptor == null) throw new SecurityException("Operation is not exposed by the server gateway");
Family expected = operation instanceof AdministrativeOperation.Pki ? Family.PKI_OPERATION
: Family.SERVER_CONTROL_OPERATION;
if (descriptor.family() != expected) throw new SecurityException("Operation family differs");
return descriptor;
}
/** @return immutable descriptor map keyed by operation ID */
public Map<String, Descriptor> descriptors() {
return descriptors;
}
/** Resolves one descriptor by stable ID for strict transport decoding. */
public Descriptor require(String operationId) {
Permission.requireBounded(operationId, 256, "operation ID");
Descriptor descriptor = descriptors.get(operationId);
if (descriptor == null) throw new SecurityException("Operation is not exposed by the server gateway");
return descriptor;
}
/** Computes an exact deterministic safe operation commitment. */
public String commitment(RealmId realmId, PkiOperation operation, Permission.Resource resource) {
Descriptor descriptor = require(operation);
@@ -129,6 +194,15 @@ public final class OperationSecurityDescriptors {
+ canonicalArguments(operation) + '|' + canonicalResource(resource));
}
/** Computes an exact deterministic commitment for either closed operation family. */
public String commitment(RealmId realmId, AdministrativeOperation operation, Permission.Resource resource) {
require(operation);
String arguments = operation instanceof AdministrativeOperation.Pki pki
? canonicalArguments(pki.operation()) : canonicalControl(((AdministrativeOperation.Control) operation).operation());
return ApprovalService.digest(realmId.value() + '|' + operation.name() + '|' + arguments + '|'
+ canonicalResource(resource));
}
/** Verifies that caller-supplied security scope matches operation identities. */
public void validateResource(PkiOperation operation, Permission.Resource resource) {
Descriptor descriptor = require(operation);
@@ -214,19 +288,78 @@ public final class OperationSecurityDescriptors {
}
private static Descriptor read(String id, Permission.Action action, Permission.ResourceType type) {
return new Descriptor(id, action, type, false, Permission.DataView.METADATA_REDACTED,
return new Descriptor(id, Family.PKI_OPERATION, action, type, false, Permission.DataView.METADATA_REDACTED,
ApprovalCategory.NONE, Eligibility.ADMINISTRATIVE);
}
private static Descriptor mutate(String id, Permission.Action action, Permission.ResourceType type,
boolean highRisk) {
return new Descriptor(id, action, type, true, Permission.DataView.METADATA_REDACTED,
return new Descriptor(id, Family.PKI_OPERATION, action, type, true, Permission.DataView.METADATA_REDACTED,
highRisk ? ApprovalCategory.HIGH_RISK : ApprovalCategory.NONE, Eligibility.ADMINISTRATIVE);
}
private static void validateOperationType(PkiOperation operation, Descriptor descriptor) {
if (!operation.name().equals(descriptor.operationId())) {
if (descriptor.family() != Family.PKI_OPERATION || !operation.name().equals(descriptor.operationId())) {
throw new SecurityException("Operation identity and descriptor differ");
}
}
private static Descriptor control(String id, Permission.Action action, Permission.ResourceType type,
boolean mutating, boolean highRisk) {
Permission.DataView view = action == Permission.Action.AUDIT_READ_PII
? Permission.DataView.PII_FULL : action == Permission.Action.AUDIT_READ_FULL
? Permission.DataView.METADATA_FULL : Permission.DataView.METADATA_REDACTED;
return new Descriptor(id, Family.SERVER_CONTROL_OPERATION, action, type, mutating,
view, highRisk ? ApprovalCategory.HIGH_RISK
: ApprovalCategory.NONE, Eligibility.ADMINISTRATIVE);
}
private static String canonicalControl(ServerControlOperation operation) {
return switch (operation) {
case ServerControlOperation.RegisterPrincipal value -> "principal=" + atom(value.principal().principalId());
case ServerControlOperation.InspectPrincipal value -> "principal=" + atom(value.principalId());
case ServerControlOperation.ListPrincipals value -> "offset=" + value.offset() + ";limit=" + value.limit();
case ServerControlOperation.SetPrincipalEnabled value -> "principal=" + atom(value.principalId())
+ ";enabled=" + value.enabled();
case ServerControlOperation.ListRoleTemplates ignored -> "all";
case ServerControlOperation.InspectRoleTemplate value -> "template=" + atom(value.templateId())
+ ";version=" + value.version();
case ServerControlOperation.CreateRoleAssignment value -> "assignment="
+ atom(value.assignment().assignmentId()) + ";principal=" + atom(value.assignment().principalId());
case ServerControlOperation.InspectRoleAssignment value -> "assignment=" + atom(value.assignmentId());
case ServerControlOperation.ListRoleAssignments value -> "offset=" + value.offset() + ";limit=" + value.limit();
case ServerControlOperation.RevokeRoleAssignment value -> "assignment=" + atom(value.assignmentId());
case ServerControlOperation.CreateGrant value -> "grant=" + atom(value.grant().grantId())
+ ";principal=" + atom(value.grant().principalId()) + ";action=" + value.grant().action().name();
case ServerControlOperation.InspectGrant value -> "grant=" + atom(value.grantId());
case ServerControlOperation.ListGrants value -> "offset=" + value.offset() + ";limit=" + value.limit();
case ServerControlOperation.RevokeGrant value -> "grant=" + atom(value.grantId());
case ServerControlOperation.EvaluateAuthorization value -> "principal=" + atom(value.principalId())
+ ";action=" + value.action().name();
case ServerControlOperation.RequestApproval value -> "approval=" + atom(value.approvalId())
+ ";target=" + atom(value.targetOperation().name());
case ServerControlOperation.InspectApproval value -> "approval=" + atom(value.approvalId());
case ServerControlOperation.ListApprovals value -> "offset=" + value.offset() + ";limit=" + value.limit()
+ ";state=" + atom(value.state().map(Enum::name).orElse("")) + ";requester="
+ atom(value.requesterPrincipalId().orElse("")) + ";target="
+ atom(value.targetOperationId().orElse(""));
case ServerControlOperation.DecideApproval value -> "approval=" + atom(value.approvalId())
+ ";choice=" + value.choice().name();
case ServerControlOperation.CancelApproval value -> "approval=" + atom(value.approvalId());
case ServerControlOperation.CreateBreakGlass value -> "breakGlass=" + atom(value.breakGlassId())
+ ";principal=" + atom(value.principalId()) + ";expiry=" + value.expiresAt();
case ServerControlOperation.ActivateBreakGlass value -> "breakGlass=" + atom(value.breakGlassId());
case ServerControlOperation.InspectBreakGlass value -> "breakGlass=" + atom(value.breakGlassId());
case ServerControlOperation.ListBreakGlass value -> "offset=" + value.offset() + ";limit=" + value.limit();
case ServerControlOperation.RevokeBreakGlass value -> "breakGlass=" + atom(value.breakGlassId());
case ServerControlOperation.InspectDisclosure value -> "object=" + atom(value.objectId().value());
case ServerControlOperation.SetDisclosure value -> "object=" + atom(value.objectId().value())
+ ";policy=" + value.policy().name();
case ServerControlOperation.IssueCapability value -> "object=" + atom(value.objectId().value())
+ ";expiry=" + value.expiresAt();
case ServerControlOperation.RevokeCapability value -> "capability=" + atom(value.capabilityId());
case ServerControlOperation.InspectAuditView value -> "object=" + atom(value.objectId().value())
+ ";view=" + value.view().name();
};
}
}

View File

@@ -67,7 +67,9 @@ public final class Permission {
OCSP_ADMINISTER(87), PUBLICATION_REGISTER(100), PUBLICATION_READ(101),
PUBLICATION_PROCESS(102), PUBLICATION_RETRY(103), PUBLICATION_RECONCILE(104),
AUDIT_READ_REDACTED(120), AUDIT_READ_FULL(121), AUDIT_READ_PII(122), AUDIT_EXPORT(123),
AUDIT_INTEGRITY_VERIFY(124), BACKUP_EXPORT(140), BACKUP_VERIFY(141), RESTORE_EXECUTE(142);
AUDIT_INTEGRITY_VERIFY(124), BACKUP_EXPORT(140), BACKUP_VERIFY(141), RESTORE_EXECUTE(142),
DISCLOSURE_READ(160), DISCLOSURE_CHANGE(161), DISCLOSURE_CAPABILITY_ISSUE(162),
DISCLOSURE_CAPABILITY_REVOKE(163);
private final int code;
@@ -109,7 +111,7 @@ public final class Permission {
REALM(1), SERVER_CONFIGURATION(2), PRINCIPAL(3), ROLE(4), GRANT(5), AUTHORITY(10),
ISSUER(11), PROFILE(12), POLICY(13), X509_BINDING(14), REQUEST(20), CERTIFICATE(21),
REVOCATION(22), STATUS_OBJECT(23), PUBLICATION(24), AUDIT(30), BACKUP(31), RESTORE(32),
DISCLOSURE(33), CAPABILITY(34);
DISCLOSURE(33), CAPABILITY(34), APPROVAL(35), BREAK_GLASS(36);
private final int code;
ResourceType(int code) { this.code = code; }
/** @return stable code */ public int code() { return code; }

View File

@@ -70,7 +70,8 @@ public final class RoleTemplateCatalog {
/** Persistable scoped role assignment; the template itself grants nothing. */
public record Assignment(String assignmentId, String principalId, String templateId,
int templateVersion, Permission.Scope scope, boolean enabled) {
int templateVersion, Permission.Scope scope, java.util.Optional<java.time.Instant> expiresAt,
boolean enabled) {
/** Validates the exact scoped assignment. */
public Assignment {
Permission.requireId(assignmentId, "assignment");
@@ -78,6 +79,13 @@ public final class RoleTemplateCatalog {
Permission.requireId(templateId, "role template");
if (templateVersion <= 0) throw new IllegalArgumentException("Template version must be positive");
Objects.requireNonNull(scope, "scope");
expiresAt = Objects.requireNonNull(expiresAt, "expiresAt");
}
/** Creates an assignment without an expiry for existing embedded callers. */
public Assignment(String assignmentId, String principalId, String templateId,
int templateVersion, Permission.Scope scope, boolean enabled) {
this(assignmentId, principalId, templateId, templateVersion, scope, java.util.Optional.empty(), enabled);
}
}
@@ -169,7 +177,7 @@ public final class RoleTemplateCatalog {
return template.actions().stream().sorted(Comparator.comparingInt(Permission.Action::code))
.map(action -> new Permission.Grant(exact.assignmentId() + ":" + action.code(), exact.principalId(),
Permission.Effect.ALLOW, action, resourceType(action), exact.scope(),
relationship(template, action), dataView(action), Set.of(), java.util.Optional.empty(), true))
relationship(template, action), dataView(action), Set.of(), exact.expiresAt(), true))
.toList();
}
@@ -188,6 +196,8 @@ public final class RoleTemplateCatalog {
if (name.startsWith("AUDIT")) return Permission.ResourceType.AUDIT;
if (name.startsWith("BACKUP")) return Permission.ResourceType.BACKUP;
if (name.startsWith("RESTORE")) return Permission.ResourceType.RESTORE;
if (name.startsWith("DISCLOSURE_CAPABILITY")) return Permission.ResourceType.CAPABILITY;
if (name.startsWith("DISCLOSURE")) return Permission.ResourceType.DISCLOSURE;
if (name.startsWith("PRINCIPAL")) return Permission.ResourceType.PRINCIPAL;
if (name.startsWith("ROLE")) return Permission.ResourceType.ROLE;
if (name.startsWith("PERMISSION")) return Permission.ResourceType.GRANT;

View File

@@ -0,0 +1,280 @@
/*******************************************************************************
* 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.time.Instant;
import java.util.Objects;
import java.util.Optional;
import zeroecho.pki.api.PkiId;
/** Closed transport-neutral server-control administration operation hierarchy. */
@SuppressWarnings("PMD.ControlStatementBraces")
public sealed interface ServerControlOperation permits ServerControlOperation.RegisterPrincipal,
ServerControlOperation.InspectPrincipal, ServerControlOperation.ListPrincipals,
ServerControlOperation.SetPrincipalEnabled, ServerControlOperation.ListRoleTemplates,
ServerControlOperation.InspectRoleTemplate, ServerControlOperation.CreateRoleAssignment,
ServerControlOperation.InspectRoleAssignment, ServerControlOperation.ListRoleAssignments,
ServerControlOperation.RevokeRoleAssignment, ServerControlOperation.CreateGrant,
ServerControlOperation.InspectGrant, ServerControlOperation.ListGrants,
ServerControlOperation.RevokeGrant, ServerControlOperation.EvaluateAuthorization,
ServerControlOperation.RequestApproval, ServerControlOperation.InspectApproval,
ServerControlOperation.ListApprovals, ServerControlOperation.DecideApproval,
ServerControlOperation.CancelApproval, ServerControlOperation.CreateBreakGlass,
ServerControlOperation.ActivateBreakGlass,
ServerControlOperation.InspectBreakGlass, ServerControlOperation.ListBreakGlass,
ServerControlOperation.RevokeBreakGlass, ServerControlOperation.InspectDisclosure,
ServerControlOperation.SetDisclosure, ServerControlOperation.IssueCapability,
ServerControlOperation.RevokeCapability, ServerControlOperation.InspectAuditView {
/** @return stable operation identity */
String name();
/** Principal registration. */
record RegisterPrincipal(SecurityPrincipal principal) implements ServerControlOperation {
public static final String NAME = "security.principal.register";
public RegisterPrincipal { Objects.requireNonNull(principal, "principal"); }
@Override public String name() { return NAME; }
}
/** Principal lookup. */
record InspectPrincipal(String principalId) implements ServerControlOperation {
public static final String NAME = "security.principal.inspect";
public InspectPrincipal { Permission.requirePrincipal(principalId); }
@Override public String name() { return NAME; }
}
/** Bounded principal page. */
record ListPrincipals(int offset, int limit) implements ServerControlOperation {
public static final String NAME = "security.principal.list";
public ListPrincipals { page(offset, limit); }
@Override public String name() { return NAME; }
}
/** Principal enablement transition. */
record SetPrincipalEnabled(String principalId, boolean enabled) implements ServerControlOperation {
public static final String ENABLE = "security.principal.enable";
public static final String DISABLE = "security.principal.disable";
public SetPrincipalEnabled { Permission.requirePrincipal(principalId); }
@Override public String name() { return enabled ? ENABLE : DISABLE; }
}
/** Immutable role-template list. */
record ListRoleTemplates() implements ServerControlOperation {
public static final String NAME = "security.role-template.list";
@Override public String name() { return NAME; }
}
/** Immutable role-template lookup. */
record InspectRoleTemplate(String templateId, int version) implements ServerControlOperation {
public static final String NAME = "security.role-template.inspect";
public InspectRoleTemplate { Permission.requireId(templateId, "role template"); if (version <= 0) throw invalid(); }
@Override public String name() { return NAME; }
}
/** Scoped assignment creation. */
record CreateRoleAssignment(RoleTemplateCatalog.Assignment assignment) implements ServerControlOperation {
public static final String NAME = "security.role-assignment.create";
public CreateRoleAssignment { Objects.requireNonNull(assignment, "assignment"); }
@Override public String name() { return NAME; }
}
/** Scoped assignment lookup. */
record InspectRoleAssignment(String assignmentId) implements ServerControlOperation {
public static final String NAME = "security.role-assignment.inspect";
public InspectRoleAssignment { Permission.requireId(assignmentId, "assignment"); }
@Override public String name() { return NAME; }
}
/** Bounded assignment page. */
record ListRoleAssignments(int offset, int limit) implements ServerControlOperation {
public static final String NAME = "security.role-assignment.list";
public ListRoleAssignments { page(offset, limit); }
@Override public String name() { return NAME; }
}
/** Durable assignment revocation. */
record RevokeRoleAssignment(String assignmentId) implements ServerControlOperation {
public static final String NAME = "security.role-assignment.revoke";
public RevokeRoleAssignment { Permission.requireId(assignmentId, "assignment"); }
@Override public String name() { return NAME; }
}
/** Direct grant creation. */
record CreateGrant(Permission.Grant grant) implements ServerControlOperation {
public static final String NAME = "security.grant.create";
public CreateGrant { Objects.requireNonNull(grant, "grant"); }
@Override public String name() { return NAME; }
}
/** Direct grant lookup. */
record InspectGrant(String grantId) implements ServerControlOperation {
public static final String NAME = "security.grant.inspect";
public InspectGrant { Permission.requireId(grantId, "grant"); }
@Override public String name() { return NAME; }
}
/** Bounded direct-grant page. */
record ListGrants(int offset, int limit) implements ServerControlOperation {
public static final String NAME = "security.grant.list";
public ListGrants { page(offset, limit); }
@Override public String name() { return NAME; }
}
/** Durable direct-grant revocation. */
record RevokeGrant(String grantId) implements ServerControlOperation {
public static final String NAME = "security.grant.revoke";
public RevokeGrant { Permission.requireId(grantId, "grant"); }
@Override public String name() { return NAME; }
}
/** Side-effect-free authorization simulation. */
record EvaluateAuthorization(String principalId, Permission.Action action, Permission.Resource resource,
Permission.Relationship relationship, Permission.DataView dataView,
Permission.Context context) implements ServerControlOperation {
public static final String NAME = "security.authorization.evaluate";
public EvaluateAuthorization { Permission.requirePrincipal(principalId); Objects.requireNonNull(action);
Objects.requireNonNull(resource); Objects.requireNonNull(relationship); Objects.requireNonNull(dataView);
Objects.requireNonNull(context); }
@Override public String name() { return NAME; }
}
/** Exact future-operation approval request. */
record RequestApproval(String approvalId, AdministrativeOperation targetOperation,
Permission.Resource targetResource) implements ServerControlOperation {
public static final String NAME = "security.approval.request";
public RequestApproval { Permission.requireId(approvalId, "approval");
Objects.requireNonNull(targetOperation); Objects.requireNonNull(targetResource);
if (targetOperation instanceof AdministrativeOperation.Control control
&& control.operation() instanceof RequestApproval) throw invalid(); }
@Override public String name() { return NAME; }
}
/** Approval lookup. */
record InspectApproval(String approvalId) implements ServerControlOperation {
public static final String NAME = "security.approval.inspect";
public InspectApproval { Permission.requireId(approvalId, "approval"); }
@Override public String name() { return NAME; }
}
/** Bounded approval page. */
record ListApprovals(int offset, int limit, Optional<ApprovalService.State> state,
Optional<String> requesterPrincipalId, Optional<String> targetOperationId)
implements ServerControlOperation {
public static final String NAME = "security.approval.list";
public ListApprovals { page(offset, limit); state = Objects.requireNonNull(state);
requesterPrincipalId = Objects.requireNonNull(requesterPrincipalId)
.map(Permission::requirePrincipal);
targetOperationId = Objects.requireNonNull(targetOperationId)
.map(value -> Permission.requireBounded(value, 256, "target operation")); }
/** Creates an unfiltered bounded approval page. */
public ListApprovals(int offset, int limit) {
this(offset, limit, Optional.empty(), Optional.empty(), Optional.empty());
}
@Override public String name() { return NAME; }
}
/** Approval or rejection decision. */
record DecideApproval(String approvalId, ApprovalService.Choice choice,
String justification) implements ServerControlOperation {
public static final String APPROVE = "security.approval.approve";
public static final String REJECT = "security.approval.reject";
public DecideApproval { Permission.requireId(approvalId, "approval"); Objects.requireNonNull(choice);
Permission.requireBounded(justification, 2048, "justification"); }
@Override public String name() { return choice == ApprovalService.Choice.APPROVE ? APPROVE : REJECT; }
}
/** Approval cancellation. */
record CancelApproval(String approvalId) implements ServerControlOperation {
public static final String NAME = "security.approval.cancel";
public CancelApproval { Permission.requireId(approvalId, "approval"); }
@Override public String name() { return NAME; }
}
/** Independently approved emergency grant creation. */
record CreateBreakGlass(String breakGlassId, String principalId, Permission.Grant grant,
String reason, Instant expiresAt) implements ServerControlOperation {
public static final String CREATE = "security.break-glass.create";
public CreateBreakGlass { Permission.requireId(breakGlassId, "break-glass");
Permission.requirePrincipal(principalId); Objects.requireNonNull(grant);
Permission.requireBounded(reason, 2048, "reason"); Objects.requireNonNull(expiresAt); }
@Override public String name() { return CREATE; }
}
/** Activation of one already-created approved emergency grant. */
record ActivateBreakGlass(String breakGlassId) implements ServerControlOperation {
public static final String ACTIVATE = "security.break-glass.activate";
public ActivateBreakGlass { Permission.requireId(breakGlassId, "break-glass"); }
@Override public String name() { return ACTIVATE; }
}
/** Break-glass lookup. */
record InspectBreakGlass(String breakGlassId) implements ServerControlOperation {
public static final String NAME = "security.break-glass.inspect";
public InspectBreakGlass { Permission.requireId(breakGlassId, "break-glass"); }
@Override public String name() { return NAME; }
}
/** Bounded break-glass page. */
record ListBreakGlass(int offset, int limit) implements ServerControlOperation {
public static final String NAME = "security.break-glass.list";
public ListBreakGlass { page(offset, limit); }
@Override public String name() { return NAME; }
}
/** Break-glass revocation. */
record RevokeBreakGlass(String breakGlassId) implements ServerControlOperation {
public static final String NAME = "security.break-glass.revoke";
public RevokeBreakGlass { Permission.requireId(breakGlassId, "break-glass"); }
@Override public String name() { return NAME; }
}
/** Disclosure metadata lookup. */
record InspectDisclosure(PkiId objectId) implements ServerControlOperation {
public static final String NAME = "security.disclosure.inspect";
public InspectDisclosure { Objects.requireNonNull(objectId); }
@Override public String name() { return NAME; }
}
/** Disclosure-only state transition. */
record SetDisclosure(PkiId objectId, DisclosureService.Policy policy) implements ServerControlOperation {
public static final String NAME = "security.disclosure.set";
public SetDisclosure { Objects.requireNonNull(objectId); Objects.requireNonNull(policy); }
@Override public String name() { return NAME; }
}
/** One-time unlisted retrieval capability issuance. */
record IssueCapability(PkiId objectId, Instant expiresAt) implements ServerControlOperation {
public static final String NAME = "security.disclosure.capability.issue";
public IssueCapability { Objects.requireNonNull(objectId); Objects.requireNonNull(expiresAt); }
@Override public String name() { return NAME; }
}
/** Capability revocation by non-bearer record identity. */
record RevokeCapability(String capabilityId) implements ServerControlOperation {
public static final String NAME = "security.disclosure.capability.revoke";
public RevokeCapability { Permission.requireId(capabilityId, "capability"); }
@Override public String name() { return NAME; }
}
/** Existing safe auditor projection over one server-control object. */
record InspectAuditView(PkiId objectId, Permission.DataView view,
Optional<String> reasonReference) implements ServerControlOperation {
public static final String REDACTED = "security.audit-view.inspect-redacted";
public static final String FULL = "security.audit-view.inspect-full";
public static final String PII = "security.audit-view.inspect-pii";
public InspectAuditView { Objects.requireNonNull(objectId); Objects.requireNonNull(view);
reasonReference = Objects.requireNonNull(reasonReference); }
@Override public String name() { return switch (view) { case METADATA_REDACTED -> REDACTED;
case METADATA_FULL, CONTENT_FULL -> FULL; case PII_FULL -> PII; }; }
}
private static void page(int offset, int limit) {
if (offset < 0 || limit <= 0 || limit > 256) throw invalid();
}
private static IllegalArgumentException invalid() {
return new IllegalArgumentException("Server-control operation input is invalid");
}
}

View File

@@ -0,0 +1,370 @@
/*******************************************************************************
* 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.Comparator;
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.application.PkiOperationFailure;
import zeroecho.pki.application.PkiOperationOutcome;
import zeroecho.pki.application.PkiOperationResult;
import zeroecho.pki.application.PkiOperationValue;
/** Sole closed dispatcher for transport-neutral server-control operations. */
@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.ExcessiveParameterList",
"PMD.ControlStatementBraces" })
public final class ServerControlOperationExecutor {
private final RealmId realmId;
private final AuthorityExposurePolicy exposure;
private final ServerControlStore store;
private final RoleTemplateCatalog roles;
private final AuthorizationEngine authorization;
private final ApprovalService approvals;
private final BreakGlassService breakGlass;
private final DisclosureService disclosure;
private final AuditorViews auditorViews;
private final OperationSecurityDescriptors descriptors;
private final Map<OperationSecurityDescriptors.ApprovalCategory, ApprovalService.Policy> approvalPolicies;
/** Creates the one control dispatcher over existing durable authorities. */
public ServerControlOperationExecutor(RealmId realmId, AuthorityExposurePolicy exposure,
ServerControlStore store, RoleTemplateCatalog roles, AuthorizationEngine authorization,
ApprovalService approvals, BreakGlassService breakGlass, DisclosureService disclosure,
AuditorViews auditorViews,
OperationSecurityDescriptors descriptors,
Map<OperationSecurityDescriptors.ApprovalCategory, ApprovalService.Policy> approvalPolicies) {
this.realmId = Objects.requireNonNull(realmId, "realmId");
this.exposure = Objects.requireNonNull(exposure, "exposure");
this.store = Objects.requireNonNull(store, "store");
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.disclosure = Objects.requireNonNull(disclosure, "disclosure");
this.auditorViews = Objects.requireNonNull(auditorViews, "auditorViews");
this.descriptors = Objects.requireNonNull(descriptors, "descriptors");
this.approvalPolicies = Map.copyOf(Objects.requireNonNull(approvalPolicies, "approvalPolicies"));
}
/** Executes exactly one already-authorized control operation synchronously. */
public ServerControlOperationOutcome execute(ServerControlOperation operation, String actorPrincipalId,
Permission.Resource authorizedResource, Optional<String> approvalId, CancellationSignal cancellation) {
Objects.requireNonNull(operation, "operation");
Permission.requirePrincipal(actorPrincipalId);
Objects.requireNonNull(authorizedResource, "authorizedResource");
if (Objects.requireNonNull(cancellation, "cancellation").isCancelled()) {
return failure(operation, PkiOperationFailure.CANCELLED, "CONTROL_OPERATION_CANCELLED");
}
try {
return dispatch(operation, actorPrincipalId, authorizedResource,
Objects.requireNonNull(approvalId, "approvalId"));
} catch (IllegalArgumentException unavailable) {
return failure(operation, PkiOperationFailure.NOT_FOUND, "CONTROL_OBJECT_UNAVAILABLE");
} catch (SecurityException denied) {
return failure(operation, PkiOperationFailure.POLICY_REJECTION, "CONTROL_POLICY_REJECTED");
} catch (IllegalStateException conflict) {
return failure(operation, PkiOperationFailure.CONFLICT, "CONTROL_STATE_CONFLICT");
}
}
private ServerControlOperationOutcome dispatch(ServerControlOperation operation, String actor,
Permission.Resource authorizedResource, Optional<String> approvalId) {
return switch (operation) {
case ServerControlOperation.RegisterPrincipal value -> ordinary(operation, register(value));
case ServerControlOperation.InspectPrincipal value -> ordinary(operation,
principal(store.requirePrincipal(value.principalId())));
case ServerControlOperation.ListPrincipals value -> ordinary(operation,
page(store.principals(value.offset(), value.limit()), ServerControlOperationExecutor::principal));
case ServerControlOperation.SetPrincipalEnabled value -> ordinary(operation,
principal(changePrincipalEnabled(value)));
case ServerControlOperation.ListRoleTemplates ignored -> ordinary(operation, templatesValue());
case ServerControlOperation.InspectRoleTemplate value -> ordinary(operation,
template(roles.require(value.templateId(), value.version())));
case ServerControlOperation.CreateRoleAssignment value -> ordinary(operation,
assignment(createAssignment(value.assignment())));
case ServerControlOperation.InspectRoleAssignment value -> ordinary(operation,
assignment(store.requireAssignment(value.assignmentId())));
case ServerControlOperation.ListRoleAssignments value -> ordinary(operation,
scopedPage(store.assignments(value.offset(), value.limit()), authorizedResource.scope(),
RoleTemplateCatalog.Assignment::scope, ServerControlOperationExecutor::assignment));
case ServerControlOperation.RevokeRoleAssignment value -> ordinary(operation,
assignment(revokeAssignment(value.assignmentId())));
case ServerControlOperation.CreateGrant value -> ordinary(operation,
grant(createGrant(value.grant())));
case ServerControlOperation.InspectGrant value -> ordinary(operation,
grant(store.requireGrant(value.grantId())));
case ServerControlOperation.ListGrants value -> ordinary(operation,
scopedPage(store.grants(value.offset(), value.limit()), authorizedResource.scope(),
Permission.Grant::scope, ServerControlOperationExecutor::grant));
case ServerControlOperation.RevokeGrant value -> ordinary(operation, grant(revokeGrant(value.grantId())));
case ServerControlOperation.EvaluateAuthorization value -> ordinary(operation, evaluate(value));
case ServerControlOperation.RequestApproval value -> ordinary(operation, approval(request(value, actor)));
case ServerControlOperation.InspectApproval value -> ordinary(operation,
approval(approvals.requireCurrent(value.approvalId())));
case ServerControlOperation.ListApprovals value -> ordinary(operation,
approvalPage(store.approvals(value.offset(), value.limit()), authorizedResource.scope(), value));
case ServerControlOperation.DecideApproval value -> ordinary(operation,
approval(approvals.decide(value.approvalId(), actor, value.choice(), value.justification())));
case ServerControlOperation.CancelApproval value -> ordinary(operation,
approval(approvals.cancel(value.approvalId(), actor)));
case ServerControlOperation.CreateBreakGlass value -> ordinary(operation,
breakGlass(breakGlass.create(value.breakGlassId(), value.principalId(), value.grant(),
value.reason(), actor, approvalId, value.expiresAt())));
case ServerControlOperation.ActivateBreakGlass value -> ordinary(operation,
breakGlass(breakGlass.activatePending(value.breakGlassId(), actor, approvalId)));
case ServerControlOperation.InspectBreakGlass value -> ordinary(operation,
breakGlass(breakGlass.requireCurrent(value.breakGlassId())));
case ServerControlOperation.ListBreakGlass value -> ordinary(operation,
scopedPage(store.breakGlass(value.offset(), value.limit()), authorizedResource.scope(),
item -> item.grant().scope(), ServerControlOperationExecutor::breakGlass));
case ServerControlOperation.RevokeBreakGlass value -> ordinary(operation,
breakGlass(breakGlass.revoke(value.breakGlassId(), actor)));
case ServerControlOperation.InspectDisclosure value -> ordinary(operation,
disclosure(store.requireDisclosure(value.objectId())));
case ServerControlOperation.SetDisclosure value -> ordinary(operation,
disclosure(disclosure.change(value.objectId(), value.policy(), true, approvalId.isPresent(), actor)));
case ServerControlOperation.IssueCapability value -> capability(operation,
disclosure.issueCapability(value.objectId(), value.expiresAt(), actor));
case ServerControlOperation.RevokeCapability value -> ordinary(operation,
capability(disclosure.revokeCapability(value.capabilityId(), actor)));
case ServerControlOperation.InspectAuditView value -> ordinary(operation, auditView(value, actor));
};
}
private PkiOperationValue register(ServerControlOperation.RegisterPrincipal operation) {
try { store.createPrincipal(operation.principal()); }
catch (IllegalStateException existing) {
if (!store.requirePrincipal(operation.principal().principalId()).equals(operation.principal())) throw existing;
}
return principal(operation.principal());
}
private SecurityPrincipal changePrincipalEnabled(ServerControlOperation.SetPrincipalEnabled operation) {
SecurityPrincipal current = store.requirePrincipal(operation.principalId());
SecurityPrincipal updated = new SecurityPrincipal(current.principalId(), current.type(),
current.displayName(), current.organization(), current.attributes(), operation.enabled());
store.replacePrincipal(current, updated);
return updated;
}
private RoleTemplateCatalog.Assignment createAssignment(RoleTemplateCatalog.Assignment value) {
if (!value.scope().realmId().equals(realmId)) throw new SecurityException("Assignment realm differs");
roles.instantiate(value); store.requirePrincipal(value.principalId()); store.createAssignment(value); return value;
}
private RoleTemplateCatalog.Assignment revokeAssignment(String id) {
RoleTemplateCatalog.Assignment current = store.requireAssignment(id);
RoleTemplateCatalog.Assignment revoked = new RoleTemplateCatalog.Assignment(current.assignmentId(),
current.principalId(), current.templateId(), current.templateVersion(), current.scope(),
current.expiresAt(), false);
store.replaceAssignment(current, revoked); return revoked;
}
private Permission.Grant createGrant(Permission.Grant value) {
if (!value.scope().realmId().equals(realmId)) throw new SecurityException("Grant realm differs");
store.requirePrincipal(value.principalId()); store.createGrant(value); return value;
}
private Permission.Grant revokeGrant(String id) {
Permission.Grant current = store.requireGrant(id);
Permission.Grant revoked = new Permission.Grant(current.grantId(), current.principalId(), current.effect(),
current.action(), current.resourceType(), current.scope(), current.relationship(), current.dataView(),
current.conditions(), current.expiresAt(), false);
store.replaceGrant(current, revoked); return revoked;
}
private PkiOperationValue evaluate(ServerControlOperation.EvaluateAuthorization value) {
SecurityPrincipal principal = store.requirePrincipal(value.principalId());
List<Permission.Grant> grants = grants(principal.principalId());
BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principal.principalId());
grants.addAll(emergency.grants());
AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(realmId,
exposure, principal, value.action(), value.resource(), value.relationship(), value.dataView(),
value.context(), grants, emergency.grantIds()));
return object("allowed", bool(decision.allowed()), "code", text(decision.code().name()));
}
private ApprovalService.Request request(ServerControlOperation.RequestApproval value, String actor) {
OperationSecurityDescriptors.Descriptor descriptor = descriptors.require(value.targetOperation());
if (!descriptor.mutating() || descriptor.approvalCategory() == OperationSecurityDescriptors.ApprovalCategory.NONE) {
throw new IllegalStateException("Target operation does not require approval");
}
ApprovalService.Policy policy = approvalPolicies.get(descriptor.approvalCategory());
if (policy == null) throw new IllegalStateException("Approval policy is unavailable");
String commitment = descriptors.commitment(realmId, value.targetOperation(), value.targetResource());
return approvals.request(value.approvalId(), value.targetOperation().name(), commitment,
value.targetResource().scope(), actor, policy);
}
private List<Permission.Grant> grants(String principalId) {
List<Permission.Grant> result = new ArrayList<>(store.grantsFor(principalId));
for (RoleTemplateCatalog.Assignment assignment : store.assignmentsFor(principalId)) {
result.addAll(roles.instantiate(assignment));
}
return result;
}
private PkiOperationValue auditView(ServerControlOperation.InspectAuditView value, String actor) {
DisclosureService.Record record = store.requireDisclosure(value.objectId());
AuditorViews.Source source = new AuditorViews.Source(record.objectId(), Permission.ResourceType.DISCLOSURE,
new Permission.Scope(realmId, Optional.empty(), Optional.empty(), Optional.empty()),
record.policy().name(), record.updatedAt(), "NOT_APPLICABLE", Optional.empty(),
record.policyCommitment(), false, 0, Optional.empty(), List.of(), Optional.empty());
Map<String, PkiOperationValue> fields = new LinkedHashMap<>();
fields.put("objectId", text(record.objectId().value())); fields.put("objectType", text(record.objectType().name()));
fields.put("policy", text(record.policy().name())); fields.put("updatedAt", text(record.updatedAt().toString()));
if (value.view() == Permission.DataView.PII_FULL) {
String reason = value.reasonReference().orElseThrow(() -> new SecurityException("PII reason is required"));
auditorViews.pii(source, store.requirePrincipal(actor), reason, true, true);
} else if (value.view() == Permission.DataView.METADATA_FULL) {
auditorViews.fullMetadata(source, true);
} else {
auditorViews.redacted(source);
}
fields.put("view", text(value.view().name())); return new PkiOperationValue.ObjectValue(fields);
}
private static ServerControlOperationOutcome ordinary(ServerControlOperation operation,
PkiOperationValue value) {
return new ServerControlOperationOutcome.Ordinary(new PkiOperationOutcome.Success(
new PkiOperationResult(operation.name(), Map.of("value", value))));
}
private static ServerControlOperationOutcome capability(ServerControlOperation operation,
DisclosureService.IssuedCapability issued) {
PkiOperationResult safe = new PkiOperationResult(operation.name(), Map.of(
"capabilityId", text(issued.capability().capabilityId()),
"objectId", text(issued.capability().objectId().value()),
"expiresAt", text(issued.capability().expiresAt().toString())));
byte[] token = issued.token();
try {
return new ServerControlOperationOutcome.OneTimeSensitive(safe, token);
} finally {
java.util.Arrays.fill(token, (byte) 0);
}
}
private static ServerControlOperationOutcome failure(ServerControlOperation operation,
PkiOperationFailure failure, String code) {
return new ServerControlOperationOutcome.Ordinary(new PkiOperationOutcome.Failure(
operation.name(), failure, code));
}
private static PkiOperationValue principal(SecurityPrincipal value) {
return object("principalId", text(value.principalId()), "type", text(value.type().name()),
"displayName", text(value.displayName()), "enabled", bool(value.enabled()));
}
private static PkiOperationValue template(RoleTemplateCatalog.Template value) {
List<PkiOperationValue> actions = value.actions().stream().sorted(Comparator.comparingInt(Permission.Action::code))
.map(item -> (PkiOperationValue) text(item.name())).toList();
return object("templateId", text(value.templateId()), "version", integer(value.version()),
"actions", new PkiOperationValue.ListValue(actions));
}
private PkiOperationValue templatesValue() {
return new PkiOperationValue.ListValue(roles.templates().stream()
.map(ServerControlOperationExecutor::template).toList());
}
private static PkiOperationValue assignment(RoleTemplateCatalog.Assignment value) {
return object("assignmentId", text(value.assignmentId()), "principalId", text(value.principalId()),
"templateId", text(value.templateId()), "enabled", bool(value.enabled()));
}
private static PkiOperationValue grant(Permission.Grant value) {
return object("grantId", text(value.grantId()), "principalId", text(value.principalId()),
"effect", text(value.effect().name()), "action", text(value.action().name()),
"enabled", bool(value.enabled()));
}
private static PkiOperationValue approval(ApprovalService.Request value) {
return object("approvalId", text(value.approvalId()), "operation", text(value.operationId()),
"requesterId", text(value.requesterPrincipalId()), "state", text(value.state().name()),
"expiresAt", text(value.expiresAt().toString()));
}
private static PkiOperationValue breakGlass(BreakGlassService.Record value) {
return object("breakGlassId", text(value.breakGlassId()), "principalId", text(value.principalId()),
"state", text(value.state().name()), "expiresAt", text(value.expiresAt().toString()));
}
private static PkiOperationValue disclosure(DisclosureService.Record value) {
return object("objectId", text(value.objectId().value()), "objectType", text(value.objectType().name()),
"policy", text(value.policy().name()), "policyCommitment", text(value.policyCommitment()));
}
private static PkiOperationValue capability(DisclosureService.Capability value) {
return object("capabilityId", text(value.capabilityId()), "objectId", text(value.objectId().value()),
"expiresAt", text(value.expiresAt().toString()), "revoked", bool(value.revoked()));
}
private static <T> PkiOperationValue page(ServerControlStore.Page<T> page,
java.util.function.Function<T, PkiOperationValue> renderer) {
return object("items", new PkiOperationValue.ListValue(page.values().stream().map(renderer).toList()),
"nextOffset", integer(page.nextOffset()), "hasMore", bool(page.hasMore()));
}
private static <T> PkiOperationValue scopedPage(ServerControlStore.Page<T> page,
Permission.Scope requested, java.util.function.Function<T, Permission.Scope> scope,
java.util.function.Function<T, PkiOperationValue> renderer) {
List<PkiOperationValue> values = page.values().stream()
.filter(item -> contains(requested, scope.apply(item))).map(renderer).toList();
return object("items", new PkiOperationValue.ListValue(values), "nextOffset",
integer(page.nextOffset()), "hasMore", bool(page.hasMore()));
}
private static PkiOperationValue approvalPage(ServerControlStore.Page<ApprovalService.Request> page,
Permission.Scope requested, ServerControlOperation.ListApprovals filter) {
List<PkiOperationValue> values = page.values().stream()
.filter(item -> contains(requested, item.scope()))
.filter(item -> filter.state().map(item.state()::equals).orElse(true))
.filter(item -> filter.requesterPrincipalId().map(item.requesterPrincipalId()::equals).orElse(true))
.filter(item -> filter.targetOperationId().map(item.operationId()::equals).orElse(true))
.map(ServerControlOperationExecutor::approval).toList();
return object("items", new PkiOperationValue.ListValue(values), "nextOffset",
integer(page.nextOffset()), "hasMore", bool(page.hasMore()));
}
private static boolean contains(Permission.Scope requested, Permission.Scope actual) {
return requested.realmId().equals(actual.realmId())
&& requested.authorityId().map(value -> actual.authorityId().filter(value::equals).isPresent()).orElse(true)
&& requested.issuerId().map(value -> actual.issuerId().filter(value::equals).isPresent()).orElse(true)
&& requested.profileId().map(value -> actual.profileId().filter(value::equals).isPresent()).orElse(true);
}
private static PkiOperationValue.ObjectValue object(Object... entries) {
Map<String, PkiOperationValue> fields = new LinkedHashMap<>();
for (int index = 0; index < entries.length; index += 2) {
fields.put((String) entries[index], (PkiOperationValue) entries[index + 1]);
}
return new PkiOperationValue.ObjectValue(fields);
}
private static PkiOperationValue.Text text(String value) { return new PkiOperationValue.Text(value); }
private static PkiOperationValue.BooleanValue bool(boolean value) { return new PkiOperationValue.BooleanValue(value); }
private static PkiOperationValue.IntegerValue integer(long value) { return new PkiOperationValue.IntegerValue(value); }
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* 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.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import zeroecho.pki.application.PkiOperationOutcome;
import zeroecho.pki.application.PkiOperationResult;
/** Closed server-control result with explicit one-time-sensitive handling. */
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", "PMD.ControlStatementBraces" })
public sealed interface ServerControlOperationOutcome permits ServerControlOperationOutcome.Ordinary,
ServerControlOperationOutcome.OneTimeSensitive {
/** Response handling classification. */
enum Classification { ORDINARY, ONE_TIME_SENSITIVE }
/** @return response handling classification */
Classification classification();
/** Ordinary safe replayable operation result. */
record Ordinary(PkiOperationOutcome outcome) implements ServerControlOperationOutcome {
public Ordinary { Objects.requireNonNull(outcome, "outcome"); }
@Override public Classification classification() { return Classification.ORDINARY; }
}
/** Single-consumption capability delivery result, never a PKI operation value. */
final class OneTimeSensitive implements ServerControlOperationOutcome {
private final PkiOperationResult safeResult;
private final byte[] token;
private final AtomicBoolean consumed = new AtomicBoolean();
/** Creates a one-time result after its commitment has been durably persisted. */
public OneTimeSensitive(PkiOperationResult safeResult, byte[] token) {
this.safeResult = Objects.requireNonNull(safeResult, "safeResult");
this.token = Objects.requireNonNull(token, "token").clone();
if (token.length != 32) throw new IllegalArgumentException("Capability token length is invalid");
}
@Override public Classification classification() { return Classification.ONE_TIME_SENSITIVE; }
/** @return safe non-secret metadata */ public PkiOperationResult safeResult() { return safeResult; }
/** Consumes the raw token exactly once and clears the retained copy. */
public byte[] consumeToken() {
if (!consumed.compareAndSet(false, true)) throw new IllegalStateException("Capability token consumed");
byte[] result = token.clone();
Arrays.fill(token, (byte) 0);
return result;
}
}
}

View File

@@ -91,7 +91,7 @@ public final class ServerControlStore implements AutoCloseable {
/** Stable namespace for capability commitments. */ public static final String CAPABILITY = "io.zeroecho.server.capability";
private static final int MAGIC = 0x5a455331;
private static final int SCHEMA = 1;
private static final int SCHEMA = 2;
private static final int MAXIMUM_RECORD_BYTES = 1_048_576;
private static final int MAXIMUM_STRING_BYTES = 16_384;
private static final int MAXIMUM_COLLECTION = 4_096;
@@ -179,11 +179,34 @@ public final class ServerControlStore implements AutoCloseable {
.orElseThrow(() -> new IllegalArgumentException("Principal is unavailable"));
}
/** Returns a bounded stable page of principals in metadata-key order. */
public synchronized Page<SecurityPrincipal> principals(int offset, int limit) {
return scanPage(PRINCIPAL, KIND_PRINCIPAL, ServerControlStore::readPrincipal, offset, limit);
}
/** Creates one scoped role assignment. */
public synchronized void createAssignment(RoleTemplateCatalog.Assignment value) {
create(ASSIGNMENT, value.assignmentId(), output -> writeAssignment(output, value));
}
/** Returns one role assignment. */
public synchronized RoleTemplateCatalog.Assignment requireAssignment(String assignmentId) {
return read(ASSIGNMENT, assignmentId, KIND_ASSIGNMENT, ServerControlStore::readAssignment)
.orElseThrow(() -> new IllegalArgumentException("Role assignment is unavailable"));
}
/** Replaces one exact role assignment. */
public synchronized void replaceAssignment(RoleTemplateCatalog.Assignment current,
RoleTemplateCatalog.Assignment updated) {
requireSame(current.assignmentId(), updated.assignmentId());
replace(ASSIGNMENT, current.assignmentId(), output -> writeAssignment(output, updated));
}
/** Returns a bounded stable page of role assignments. */
public synchronized Page<RoleTemplateCatalog.Assignment> assignments(int offset, int limit) {
return scanPage(ASSIGNMENT, KIND_ASSIGNMENT, ServerControlStore::readAssignment, offset, limit);
}
/** Lists assignments for one principal from one stable metadata snapshot. */
public synchronized List<RoleTemplateCatalog.Assignment> assignmentsFor(String principalId) {
return scan(ASSIGNMENT, KIND_ASSIGNMENT, ServerControlStore::readAssignment).stream()
@@ -195,6 +218,17 @@ public final class ServerControlStore implements AutoCloseable {
create(GRANT, value.grantId(), output -> writeGrant(output, value));
}
/** Returns one direct grant. */
public synchronized Permission.Grant requireGrant(String grantId) {
return read(GRANT, grantId, KIND_GRANT, ServerControlStore::readGrant)
.orElseThrow(() -> new IllegalArgumentException("Permission grant is unavailable"));
}
/** Returns a bounded stable page of direct grants. */
public synchronized Page<Permission.Grant> grants(int offset, int limit) {
return scanPage(GRANT, KIND_GRANT, ServerControlStore::readGrant, offset, limit);
}
/** Replaces one direct grant. */
public synchronized void replaceGrant(Permission.Grant current, Permission.Grant updated) {
requireSame(current.grantId(), updated.grantId());
@@ -224,6 +258,11 @@ public final class ServerControlStore implements AutoCloseable {
.orElseThrow(() -> new IllegalArgumentException("Approval is unavailable"));
}
/** Returns a bounded stable page of approval records. */
public synchronized Page<ApprovalService.Request> approvals(int offset, int limit) {
return scanPage(APPROVAL, KIND_APPROVAL, ServerControlStore::readApproval, offset, limit);
}
/** Creates one break-glass record. */
public synchronized void createBreakGlass(BreakGlassService.Record value) {
create(BREAK_GLASS, value.breakGlassId(), output -> writeBreakGlass(output, value));
@@ -247,6 +286,11 @@ public final class ServerControlStore implements AutoCloseable {
.filter(item -> item.principalId().equals(principalId)).toList();
}
/** Returns a bounded stable page of break-glass records. */
public synchronized Page<BreakGlassService.Record> breakGlass(int offset, int limit) {
return scanPage(BREAK_GLASS, KIND_BREAK_GLASS, ServerControlStore::readBreakGlass, offset, limit);
}
/** Creates one disclosure record. */
public synchronized void createDisclosure(DisclosureService.Record value) {
create(DISCLOSURE, value.objectId().value(), output -> writeDisclosure(output, value));
@@ -264,6 +308,11 @@ public final class ServerControlStore implements AutoCloseable {
.orElseThrow(() -> new IllegalArgumentException("Disclosure record is unavailable"));
}
/** Returns a bounded stable page of disclosure records. */
public synchronized Page<DisclosureService.Record> disclosures(int offset, int limit) {
return scanPage(DISCLOSURE, KIND_DISCLOSURE, ServerControlStore::readDisclosure, offset, limit);
}
/** Creates one commitment-only capability record. */
public synchronized void createCapability(DisclosureService.Capability value) {
create(CAPABILITY, value.capabilityId(), output -> writeCapability(output, value));
@@ -282,6 +331,15 @@ public final class ServerControlStore implements AutoCloseable {
.orElseThrow(() -> new IllegalArgumentException("Capability is unavailable"));
}
/** Finite page returned without materializing the complete namespace. */
public record Page<T>(List<T> values, int nextOffset, boolean hasMore) {
/** Snapshots the bounded page. */
public Page {
values = List.copyOf(Objects.requireNonNull(values, "values"));
if (nextOffset < 0) throw new IllegalArgumentException("Next offset is negative");
}
}
/** Lists capability commitments bound to one object. */
public synchronized List<DisclosureService.Capability> capabilitiesFor(PkiId objectId) {
return scan(CAPABILITY, KIND_CAPABILITY, ServerControlStore::readCapability).stream()
@@ -330,6 +388,10 @@ public final class ServerControlStore implements AutoCloseable {
: scan(DISCLOSURE, KIND_DISCLOSURE, ServerControlStore::readDisclosure)) {
record.ownerPrincipalId().ifPresent(owner -> requirePrincipalReference(principals, owner));
}
for (DisclosureService.Capability capability
: scan(CAPABILITY, KIND_CAPABILITY, ServerControlStore::readCapability)) {
requirePrincipalReference(principals, capability.issuerPrincipalId());
}
}
/** Opens no sidecar authority and closes the sole metadata authority idempotently. */
@@ -379,6 +441,34 @@ public final class ServerControlStore implements AutoCloseable {
}
}
private <T> Page<T> scanPage(String namespace, int kind, Decoder<T> decoder, int offset, int limit) {
requireOpen();
if (offset < 0 || limit <= 0 || limit > 256) {
throw new IllegalArgumentException("Control page bounds are invalid");
}
List<T> values = new ArrayList<>(limit);
int seen = 0;
boolean more = false;
try (MetadataSnapshot snapshot = metadata.snapshot();
MetadataCursor cursor = snapshot.scan(MetadataSnapshot.KeyRange.all(namespace),
CancellationSignal.NONE)) {
Optional<MetadataSnapshot.Record> next;
while ((next = cursor.next(CancellationSignal.NONE)).isPresent()) {
MetadataSnapshot.Record record = next.orElseThrow();
T decoded = decode(record, kind, decoder);
if (!record.key().equals(key(namespace, identityOf(decoded)))) {
throw new IllegalStateException("Server-control metadata key mismatch");
}
if (seen++ < offset) continue;
if (values.size() == limit) { more = true; break; }
values.add(decoded);
}
return new Page<>(values, Math.addExact(offset, values.size()), more);
} catch (IOException failure) {
throw new IllegalStateException("Server-control metadata scan failed");
}
}
private void create(String namespace, String identity, Encoder encoder) {
mutate(namespace, identity, true, encoder);
}
@@ -513,12 +603,12 @@ public final class ServerControlStore implements AutoCloseable {
private static void writeAssignment(DataOutputStream out, RoleTemplateCatalog.Assignment value) throws IOException {
out.writeInt(KIND_ASSIGNMENT); writeString(out, value.assignmentId()); writeString(out, value.principalId());
writeString(out, value.templateId()); out.writeInt(value.templateVersion()); writeScope(out, value.scope());
out.writeBoolean(value.enabled());
writeOptionalInstant(out, value.expiresAt()); out.writeBoolean(value.enabled());
}
private static RoleTemplateCatalog.Assignment readAssignment(DataInputStream in) throws IOException {
return new RoleTemplateCatalog.Assignment(readString(in), readString(in), readString(in), in.readInt(),
readScope(in), in.readBoolean());
readScope(in), readOptionalInstant(in), in.readBoolean());
}
private static void writeGrant(DataOutputStream out, Permission.Grant value) throws IOException {
@@ -580,13 +670,13 @@ public final class ServerControlStore implements AutoCloseable {
private static void writeBreakGlass(DataOutputStream out, BreakGlassService.Record value) throws IOException {
out.writeInt(KIND_BREAK_GLASS); writeString(out, value.breakGlassId()); writeString(out, value.principalId());
writeGrantBody(out, value.grant()); writeString(out, value.reason()); writeString(out, value.issuerPrincipalId());
writeOptionalString(out, value.approvalId()); writeInstant(out, value.createdAt()); writeInstant(out, value.activatedAt());
writeOptionalString(out, value.approvalId()); writeInstant(out, value.createdAt()); writeOptionalInstant(out, value.activatedAt());
writeInstant(out, value.expiresAt()); out.writeInt(value.state().code()); writeString(out, value.auditCommitment());
}
private static BreakGlassService.Record readBreakGlass(DataInputStream in) throws IOException {
return new BreakGlassService.Record(readString(in), readString(in), readGrantBody(in), readString(in),
readString(in), readOptionalString(in), readInstant(in), readInstant(in), readInstant(in),
readString(in), readOptionalString(in), readInstant(in), readOptionalInstant(in), readInstant(in),
BreakGlassService.State.fromCode(in.readInt()), readString(in));
}
@@ -603,13 +693,15 @@ public final class ServerControlStore implements AutoCloseable {
private static void writeCapability(DataOutputStream out, DisclosureService.Capability value) throws IOException {
out.writeInt(KIND_CAPABILITY); writeString(out, value.capabilityId()); writeString(out, value.realmId().value());
writeString(out, value.objectId().value()); writeString(out, value.action()); writeBytes(out, value.tokenCommitment());
writeInstant(out, value.expiresAt()); out.writeBoolean(value.revoked());
writeString(out, value.objectId().value()); writeString(out, value.action());
writeString(out, value.issuerPrincipalId()); writeBytes(out, value.tokenCommitment());
writeInstant(out, value.expiresAt()); out.writeInt(value.deliveryState().code()); out.writeBoolean(value.revoked());
}
private static DisclosureService.Capability readCapability(DataInputStream in) throws IOException {
return new DisclosureService.Capability(readString(in), new RealmId(readString(in)), new PkiId(readString(in)),
readString(in), readBytes(in, 32), readInstant(in), in.readBoolean());
readString(in), readString(in), readBytes(in, 32), readInstant(in),
DisclosureService.DeliveryState.fromCode(in.readInt()), in.readBoolean());
}
private static void writeScope(DataOutputStream out, Permission.Scope value) throws IOException {

View File

@@ -58,7 +58,8 @@ import zeroecho.pki.application.PkiResourceScopeResolver;
* exactly once to the session executor, and preserves the returned outcome.</p>
*/
@SuppressWarnings({ "PMD.ControlStatementBraces", "PMD.FieldDeclarationsShouldBeAtStartOfClass",
"PMD.ExcessiveParameterList", "PMD.CyclomaticComplexity", "PMD.AvoidInstantiatingObjectsInLoops" })
"PMD.ExcessiveParameterList", "PMD.CyclomaticComplexity", "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.NcssCount", "PMD.ConfusingTernary", "PMD.ExceptionAsFlowControl" })
public final class ServerOperationGateway {
/**
* Complete transport-neutral request admission input.
@@ -72,7 +73,7 @@ public final class ServerOperationGateway {
* @param approvalId optional durable approval reference
* @param correlationId safe finite request correlation identity
*/
public record Request(RealmId realmId, String principalId, PkiOperation operation,
public record Request(RealmId realmId, String principalId, AdministrativeOperation operation,
Permission.Resource resource, Permission.Relationship relationship, Permission.Context context,
Optional<String> approvalId, String correlationId) {
/** Validates the immutable request. */
@@ -86,14 +87,27 @@ public final class ServerOperationGateway {
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, new AdministrativeOperation.Pki(operation), resource, relationship,
context, approvalId, correlationId);
}
}
/** Closed gateway outcome preserving typed backend results. */
public sealed interface Outcome permits Outcome.Executed, Outcome.Denied, Outcome.ApprovalRequired {
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"); }
@@ -117,6 +131,7 @@ public final class ServerOperationGateway {
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;
@@ -125,7 +140,8 @@ public final class ServerOperationGateway {
/** 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, OperationSecurityDescriptors descriptors, PkiOperationExecutor executor,
BreakGlassService breakGlass, DisclosureService disclosure, AuditorViews auditorViews,
OperationSecurityDescriptors descriptors, PkiOperationExecutor executor,
PkiResourceScopeResolver resourceScopes,
Map<OperationSecurityDescriptors.ApprovalCategory, ApprovalService.Policy> approvalPolicies,
java.time.Clock clock, zeroecho.pki.spi.audit.AuditSink auditSink, Runnable openCheck) {
@@ -138,12 +154,27 @@ public final class ServerOperationGateway {
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, 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");
}
/** 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),
descriptors, executor, resourceScopes, approvalPolicies, clock, auditSink, openCheck);
}
/**
* Admits and synchronously executes one operation.
*
@@ -161,12 +192,21 @@ public final class ServerOperationGateway {
OperationSecurityDescriptors.Descriptor descriptor;
try {
descriptor = descriptors.require(exact.operation());
descriptors.validateResource(exact.operation(), exact.resource());
validateAuthoritativeScope(exact.operation(), exact.resource());
} catch (SecurityException invalid) {
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 PkiOperation.CreateAuthority && !exposure.authorityCreationPermitted()) {
if (exact.operation() instanceof AdministrativeOperation.Pki pki
&& pki.operation() instanceof PkiOperation.CreateAuthority
&& !exposure.authorityCreationPermitted()) {
return denied(exact, AuthorizationEngine.Code.OUTSIDE_AUTHORITY_SCOPE);
}
SecurityPrincipal principal;
@@ -181,16 +221,36 @@ public final class ServerOperationGateway {
}
BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principal.principalId());
grants.addAll(emergency.grants());
Permission.Action action = action(exact.operation(), descriptor.action());
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 PkiOperation.ListAuthorities
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();
@@ -205,17 +265,25 @@ public final class ServerOperationGateway {
approvals.claim(current.approvalId(), exact.operation().name(), commitment, exact.resource().scope());
claimedApproval = Optional.of(current.approvalId());
}
PkiOperationOutcome backend = executor.execute(exact.operation(), cancellation);
if (exact.operation() instanceof PkiOperation.ListAuthorities) {
backend = filterAuthorities(backend, principal, grants, emergency.grantIds(), exact.context());
}
if (claimedApproval.isPresent()) {
approvals.complete(claimedApproval.orElseThrow(), classification(backend));
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(),
Map.of("operation", exact.operation().name(), "result", classification(backend),
"correlation", exact.correlationId()));
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(),
Map.of("operation", exact.operation().name(), "result", classification(backend),
"correlation", exact.correlationId()));
return new Outcome.Executed(backend);
Map.of("operation", exact.operation().name(), "result", result, "correlation", exact.correlationId()));
return new Outcome.ControlExecuted(controlResult);
}
/**
@@ -279,6 +347,18 @@ public final class ServerOperationGateway {
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())) {
@@ -351,6 +431,28 @@ public final class ServerOperationGateway {
}
}
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();
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
@@ -383,4 +485,10 @@ public final class ServerOperationGateway {
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);
}
}

View File

@@ -96,7 +96,8 @@ public final class ServerRealmContext implements AutoCloseable {
this.auditorViews = auditorViews;
this.audit = new SafeAudit(clock, auditSink);
this.gateway = new ServerOperationGateway(configuration.realmId(), configuration.authorityExposure(),
control, roles, authorization, approvals, breakGlass, new OperationSecurityDescriptors(),
control, roles, authorization, approvals, breakGlass, disclosure, auditorViews,
new OperationSecurityDescriptors(),
session.operations(), session.resourceScopes(), configuration.approvalPolicies(), clock, auditSink,
this::requireOpen);
}

View File

@@ -335,9 +335,11 @@ final class AdminHttpHandler implements HttpHandler {
private static PkiOperationValue.ObjectValue descriptorValue(OperationSecurityDescriptors.Descriptor value) {
Map<String, PkiOperationValue> fields = new LinkedHashMap<>();
fields.put("operation", new PkiOperationValue.Text(value.operationId()));
fields.put("family", new PkiOperationValue.Text(value.family().name()));
fields.put("mutating", new PkiOperationValue.BooleanValue(value.mutating()));
fields.put("resourceType", new PkiOperationValue.Text(value.resourceType().name()));
fields.put("approval", new PkiOperationValue.Text(value.approvalCategory().name()));
fields.put("dataView", new PkiOperationValue.Text(value.dataView().name()));
return new PkiOperationValue.ObjectValue(fields);
}
@@ -370,20 +372,49 @@ final class AdminHttpHandler implements HttpHandler {
"MALFORMED_REQUEST", "MALFORMED_REQUEST", false, false));
}
private static void send(HttpExchange exchange, String requestId, HttpResponses.Response response)
private void send(HttpExchange exchange, String requestId, HttpResponses.Response response)
throws IOException {
byte[] body = response.body();
exchange.getResponseHeaders().set("Content-Type", JSON);
exchange.getResponseHeaders().set("Cache-Control", "no-store");
if (response.oneTimeSensitive()) {
exchange.getResponseHeaders().set("Pragma", "no-cache");
exchange.getResponseHeaders().set("Referrer-Policy", "no-referrer");
}
exchange.getResponseHeaders().set(RequestIds.HEADER, requestId);
exchange.sendResponseHeaders(response.statusCode(), body.length);
try (java.io.OutputStream output = exchange.getResponseBody()) {
output.write(body);
classifyCapabilityDelivery(response, true, requestId);
} catch (IOException failure) {
if (response.oneTimeSensitive()) {
try {
classifyCapabilityDelivery(response, false, requestId);
} catch (RuntimeException classificationFailure) {
failure.addSuppressed(classificationFailure);
}
realm.auditTransport("CAPABILITY_DELIVERY_UNKNOWN", "system",
Map.of("request", requestId, "classification", "CAPABILITY_DELIVERY_UNKNOWN"));
}
throw failure;
} finally {
java.util.Arrays.fill(body, (byte) 0);
response.clearSensitiveBody();
exchange.close();
}
}
private void classifyCapabilityDelivery(HttpResponses.Response response, boolean delivered,
String requestId) {
try {
response.capabilityId().ifPresent(value -> realm.disclosure().classifyDelivery(value, delivered));
} catch (RuntimeException failure) {
realm.auditTransport("CAPABILITY_DELIVERY_UNKNOWN", "system",
Map.of("request", requestId, "classification", "CAPABILITY_DELIVERY_UNKNOWN"));
if (!delivered) throw failure;
}
}
private static final class TransportFailure extends RuntimeException {
private static final long serialVersionUID = 1L;
private final transient HttpResponses.Response response;

View File

@@ -34,6 +34,7 @@
package zeroecho.pki.server.http;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
@@ -50,21 +51,27 @@ import zeroecho.pki.api.status.StatusObjectType;
import zeroecho.pki.application.PkiOperation;
import zeroecho.pki.application.PkiOperationValue;
import zeroecho.pki.server.OperationSecurityDescriptors;
import zeroecho.pki.server.AdministrativeOperation;
import zeroecho.pki.server.ApprovalService;
import zeroecho.pki.server.DisclosureService;
import zeroecho.pki.server.Permission;
import zeroecho.pki.server.RealmId;
import zeroecho.pki.server.RoleTemplateCatalog;
import zeroecho.pki.server.SecurityPrincipal;
import zeroecho.pki.server.ServerControlOperation;
/** Closed strict HTTP decoding for operations exposed by the existing gateway. */
@SuppressWarnings("PMD")
final class HttpOperationCodec {
/** Fully decoded gateway input excluding authenticated principal and request ID. */
record Decoded(PkiOperation operation, Permission.Resource resource, Permission.Context context,
record Decoded(AdministrativeOperation operation, Permission.Resource resource, Permission.Context context,
Optional<String> approvalId, Optional<Duration> requestedDeadline) { }
private HttpOperationCodec() {
}
static Decoded decode(String operationId, byte[] document, int maximumBytes, RealmId realmId,
java.util.function.Function<PkiOperation, OperationSecurityDescriptors.Descriptor> descriptors) {
java.util.function.Function<String, OperationSecurityDescriptors.Descriptor> descriptors) {
Fields root = Fields.of(StrictJson.parse(document, maximumBytes));
root.allowed(Set.of("version", "authorityId", "approvalId", "deadline", "arguments"));
if (root.integer("version") != 1) throw new IllegalArgumentException("Request version is unsupported");
@@ -73,22 +80,34 @@ final class HttpOperationCodec {
approval.ifPresent(value -> requireId(value, "approval ID"));
Optional<Duration> deadline = root.optionalText("deadline").map(HttpOperationCodec::duration);
Fields arguments = root.object("arguments");
PkiOperation operation = operation(operationId, arguments);
OperationSecurityDescriptors.Descriptor descriptor = descriptors.apply(operationId);
AdministrativeOperation operation;
Permission.Scope scope;
Optional<PkiId> objectId;
if (descriptor.family() == OperationSecurityDescriptors.Family.PKI_OPERATION) {
PkiOperation pki = operation(operationId, arguments);
operation = new AdministrativeOperation.Pki(pki);
boolean realmWide = realmWide(pki);
if (realmWide == authority.isPresent()) {
throw new IllegalArgumentException(realmWide
? "Realm-wide operation must not specify authorityId"
: "Authority-scoped operation requires authorityId");
}
validateAuthority(pki, authority);
scope = new Permission.Scope(realmId, authority, Optional.empty(), profile(pki));
objectId = objectId(pki);
} else {
ServerControlOperation control = control(operationId, arguments, realmId, authority, descriptors);
operation = new AdministrativeOperation.Control(control);
scope = controlScope(control, realmId, authority);
objectId = controlObject(control);
}
arguments.complete();
root.complete();
OperationSecurityDescriptors.Descriptor descriptor = descriptors.apply(operation);
boolean realmWide = realmWide(operation);
if (realmWide == authority.isPresent()) {
throw new IllegalArgumentException(realmWide
? "Realm-wide operation must not specify authorityId"
: "Authority-scoped operation requires authorityId");
}
validateAuthority(operation, authority);
Permission.Scope scope = new Permission.Scope(realmId, authority, Optional.empty(), profile(operation));
Optional<PkiId> objectId = objectId(operation);
Permission.Resource resource = new Permission.Resource(descriptor.resourceType(), scope, objectId,
Optional.empty());
Optional<String> reason = operation instanceof PkiOperation.TransitionAuthority transition
Optional<String> reason = operation instanceof AdministrativeOperation.Pki pki
&& pki.operation() instanceof PkiOperation.TransitionAuthority transition
? Optional.of(transition.reason()) : Optional.empty();
Permission.Context context = new Permission.Context(reason, approval.isPresent(), false, Map.of());
return new Decoded(operation, resource, context, approval, deadline);
@@ -167,6 +186,212 @@ final class HttpOperationCodec {
};
}
private static ServerControlOperation control(String id, Fields fields, RealmId realmId,
Optional<PkiId> authority,
java.util.function.Function<String, OperationSecurityDescriptors.Descriptor> descriptors) {
return switch (id) {
case ServerControlOperation.RegisterPrincipal.NAME -> {
fields.exact("principalId", "type", "displayName", "enabled");
yield new ServerControlOperation.RegisterPrincipal(new SecurityPrincipal(fields.text("principalId"),
SecurityPrincipal.Type.valueOf(fields.text("type")), fields.text("displayName"),
Optional.empty(), Map.of(), fields.bool("enabled")));
}
case ServerControlOperation.InspectPrincipal.NAME -> {
fields.exact("principalId"); yield new ServerControlOperation.InspectPrincipal(fields.text("principalId"));
}
case ServerControlOperation.ListPrincipals.NAME -> {
fields.exact("offset", "limit"); yield new ServerControlOperation.ListPrincipals(
fields.integer("offset"), fields.integer("limit"));
}
case ServerControlOperation.SetPrincipalEnabled.ENABLE,
ServerControlOperation.SetPrincipalEnabled.DISABLE -> {
fields.exact("principalId"); yield new ServerControlOperation.SetPrincipalEnabled(
fields.text("principalId"), id.equals(ServerControlOperation.SetPrincipalEnabled.ENABLE));
}
case ServerControlOperation.ListRoleTemplates.NAME -> {
fields.exact(); yield new ServerControlOperation.ListRoleTemplates();
}
case ServerControlOperation.InspectRoleTemplate.NAME -> {
fields.exact("templateId", "version"); yield new ServerControlOperation.InspectRoleTemplate(
fields.text("templateId"), fields.integer("version"));
}
case ServerControlOperation.CreateRoleAssignment.NAME -> {
fields.allowed(Set.of("assignmentId", "principalId", "templateId", "templateVersion",
"profileId", "expiresAt"));
Permission.Scope scope = new Permission.Scope(realmId, authority, Optional.empty(),
fields.optionalText("profileId"));
yield new ServerControlOperation.CreateRoleAssignment(new RoleTemplateCatalog.Assignment(
fields.text("assignmentId"), fields.text("principalId"), fields.text("templateId"),
fields.integer("templateVersion"), scope, fields.optionalInstant("expiresAt"), true));
}
case ServerControlOperation.InspectRoleAssignment.NAME -> {
fields.exact("assignmentId"); yield new ServerControlOperation.InspectRoleAssignment(
fields.text("assignmentId"));
}
case ServerControlOperation.ListRoleAssignments.NAME -> {
fields.exact("offset", "limit"); yield new ServerControlOperation.ListRoleAssignments(
fields.integer("offset"), fields.integer("limit"));
}
case ServerControlOperation.RevokeRoleAssignment.NAME -> {
fields.exact("assignmentId"); yield new ServerControlOperation.RevokeRoleAssignment(
fields.text("assignmentId"));
}
case ServerControlOperation.CreateGrant.NAME -> {
fields.allowed(Set.of("grantId", "principalId", "effect", "action", "resourceType",
"issuerId", "profileId", "relationship", "dataView", "conditions", "expiresAt"));
Permission.Scope scope = new Permission.Scope(realmId, authority,
fields.optionalText("issuerId").map(PkiId::new), fields.optionalText("profileId"));
yield new ServerControlOperation.CreateGrant(new Permission.Grant(fields.text("grantId"),
fields.text("principalId"), Permission.Effect.valueOf(fields.text("effect")),
Permission.Action.valueOf(fields.text("action")),
Permission.ResourceType.valueOf(fields.text("resourceType")), scope,
Permission.Relationship.valueOf(fields.text("relationship")),
Permission.DataView.valueOf(fields.text("dataView")),
fields.enumSet("conditions", Permission.Condition.class), fields.optionalInstant("expiresAt"), true));
}
case ServerControlOperation.InspectGrant.NAME -> {
fields.exact("grantId"); yield new ServerControlOperation.InspectGrant(fields.text("grantId"));
}
case ServerControlOperation.ListGrants.NAME -> {
fields.exact("offset", "limit"); yield new ServerControlOperation.ListGrants(
fields.integer("offset"), fields.integer("limit"));
}
case ServerControlOperation.RevokeGrant.NAME -> {
fields.exact("grantId"); yield new ServerControlOperation.RevokeGrant(fields.text("grantId"));
}
case ServerControlOperation.EvaluateAuthorization.NAME -> {
fields.exact("principalId", "action", "resourceType", "relationship", "dataView");
Permission.Resource resource = new Permission.Resource(
Permission.ResourceType.valueOf(fields.text("resourceType")),
new Permission.Scope(realmId, authority, Optional.empty(), Optional.empty()),
Optional.empty(), Optional.empty());
yield new ServerControlOperation.EvaluateAuthorization(fields.text("principalId"),
Permission.Action.valueOf(fields.text("action")), resource,
Permission.Relationship.valueOf(fields.text("relationship")),
Permission.DataView.valueOf(fields.text("dataView")), Permission.Context.empty());
}
case ServerControlOperation.RequestApproval.NAME -> {
fields.allowed(Set.of("approvalId", "targetOperation", "targetArguments", "targetAuthorityId"));
String targetId = fields.text("targetOperation");
Fields targetFields = fields.object("targetArguments");
Optional<PkiId> targetAuthority = fields.optionalText("targetAuthorityId").map(PkiId::new);
OperationSecurityDescriptors.Descriptor targetDescriptor = descriptors.apply(targetId);
AdministrativeOperation targetOperation;
Permission.Scope targetScope;
Optional<PkiId> targetObject;
if (targetDescriptor.family() == OperationSecurityDescriptors.Family.PKI_OPERATION) {
PkiOperation targetPki = operation(targetId, targetFields);
targetOperation = new AdministrativeOperation.Pki(targetPki);
targetScope = new Permission.Scope(realmId, targetAuthority, Optional.empty(), profile(targetPki));
targetObject = objectId(targetPki);
} else {
ServerControlOperation targetControl = control(targetId, targetFields, realmId,
targetAuthority, descriptors);
targetOperation = new AdministrativeOperation.Control(targetControl);
targetScope = controlScope(targetControl, realmId, targetAuthority);
targetObject = controlObject(targetControl);
}
targetFields.complete();
Permission.Resource targetResource = new Permission.Resource(targetDescriptor.resourceType(),
targetScope, targetObject, Optional.empty());
yield new ServerControlOperation.RequestApproval(fields.text("approvalId"),
targetOperation, targetResource);
}
case ServerControlOperation.InspectApproval.NAME -> {
fields.exact("approvalId"); yield new ServerControlOperation.InspectApproval(fields.text("approvalId"));
}
case ServerControlOperation.ListApprovals.NAME -> {
fields.allowed(Set.of("offset", "limit", "state", "requesterPrincipalId", "targetOperationId"));
yield new ServerControlOperation.ListApprovals(fields.integer("offset"), fields.integer("limit"),
fields.optionalText("state").map(ApprovalService.State::valueOf),
fields.optionalText("requesterPrincipalId"), fields.optionalText("targetOperationId"));
}
case ServerControlOperation.DecideApproval.APPROVE,
ServerControlOperation.DecideApproval.REJECT -> {
fields.exact("approvalId", "justification"); yield new ServerControlOperation.DecideApproval(
fields.text("approvalId"), id.equals(ServerControlOperation.DecideApproval.APPROVE)
? ApprovalService.Choice.APPROVE : ApprovalService.Choice.REJECT,
fields.text("justification"));
}
case ServerControlOperation.CancelApproval.NAME -> {
fields.exact("approvalId"); yield new ServerControlOperation.CancelApproval(fields.text("approvalId"));
}
case ServerControlOperation.CreateBreakGlass.CREATE -> {
fields.exact("breakGlassId", "principalId", "grantId", "action", "resourceType", "reason", "expiresAt");
Permission.Scope scope = new Permission.Scope(realmId, authority, Optional.empty(), Optional.empty());
Permission.Grant grant = new Permission.Grant(fields.text("grantId"), fields.text("principalId"),
Permission.Effect.ALLOW, Permission.Action.valueOf(fields.text("action")),
Permission.ResourceType.valueOf(fields.text("resourceType")), scope,
Permission.Relationship.ANY, Permission.DataView.METADATA_REDACTED, Set.of(),
Optional.of(fields.instant("expiresAt")), true);
yield new ServerControlOperation.CreateBreakGlass(fields.text("breakGlassId"),
fields.text("principalId"), grant, fields.text("reason"), fields.instant("expiresAt"));
}
case ServerControlOperation.ActivateBreakGlass.ACTIVATE -> {
fields.exact("breakGlassId");
yield new ServerControlOperation.ActivateBreakGlass(fields.text("breakGlassId"));
}
case ServerControlOperation.InspectBreakGlass.NAME -> {
fields.exact("breakGlassId"); yield new ServerControlOperation.InspectBreakGlass(fields.text("breakGlassId"));
}
case ServerControlOperation.ListBreakGlass.NAME -> {
fields.exact("offset", "limit"); yield new ServerControlOperation.ListBreakGlass(
fields.integer("offset"), fields.integer("limit"));
}
case ServerControlOperation.RevokeBreakGlass.NAME -> {
fields.exact("breakGlassId"); yield new ServerControlOperation.RevokeBreakGlass(fields.text("breakGlassId"));
}
case ServerControlOperation.InspectDisclosure.NAME -> {
fields.exact("objectId"); yield new ServerControlOperation.InspectDisclosure(fields.pkiId("objectId"));
}
case ServerControlOperation.SetDisclosure.NAME -> {
fields.exact("objectId", "policy"); yield new ServerControlOperation.SetDisclosure(
fields.pkiId("objectId"), DisclosureService.Policy.valueOf(fields.text("policy")));
}
case ServerControlOperation.IssueCapability.NAME -> {
fields.exact("objectId", "expiresAt"); yield new ServerControlOperation.IssueCapability(
fields.pkiId("objectId"), fields.instant("expiresAt"));
}
case ServerControlOperation.RevokeCapability.NAME -> {
fields.exact("capabilityId"); yield new ServerControlOperation.RevokeCapability(fields.text("capabilityId"));
}
case ServerControlOperation.InspectAuditView.REDACTED,
ServerControlOperation.InspectAuditView.FULL,
ServerControlOperation.InspectAuditView.PII -> {
fields.allowed(Set.of("objectId", "reasonReference"));
Permission.DataView view = id.equals(ServerControlOperation.InspectAuditView.REDACTED)
? Permission.DataView.METADATA_REDACTED
: id.equals(ServerControlOperation.InspectAuditView.FULL)
? Permission.DataView.METADATA_FULL : Permission.DataView.PII_FULL;
yield new ServerControlOperation.InspectAuditView(fields.pkiId("objectId"), view,
fields.optionalText("reasonReference"));
}
default -> throw new SecurityException("Control operation is not exposed");
};
}
private static Permission.Scope controlScope(ServerControlOperation operation, RealmId realmId,
Optional<PkiId> authority) {
return switch (operation) {
case ServerControlOperation.CreateRoleAssignment value -> value.assignment().scope();
case ServerControlOperation.CreateGrant value -> value.grant().scope();
case ServerControlOperation.EvaluateAuthorization value -> value.resource().scope();
case ServerControlOperation.RequestApproval value -> value.targetResource().scope();
case ServerControlOperation.CreateBreakGlass value -> value.grant().scope();
default -> new Permission.Scope(realmId, authority, Optional.empty(), Optional.empty());
};
}
private static Optional<PkiId> controlObject(ServerControlOperation operation) {
return switch (operation) {
case ServerControlOperation.InspectDisclosure value -> Optional.of(value.objectId());
case ServerControlOperation.SetDisclosure value -> Optional.of(value.objectId());
case ServerControlOperation.IssueCapability value -> Optional.of(value.objectId());
case ServerControlOperation.InspectAuditView value -> Optional.of(value.objectId());
default -> Optional.empty();
};
}
private static boolean realmWide(PkiOperation operation) {
return operation instanceof PkiOperation.ListAuthorities
|| operation instanceof PkiOperation.CreateAuthority
@@ -247,7 +472,24 @@ final class HttpOperationCodec {
long longValue(String name) { consumed.add(name); PkiOperationValue value = require(name);
if (!(value instanceof PkiOperationValue.IntegerValue integer)) throw type(); return integer.value(); }
int integer(String name) { return Math.toIntExact(longValue(name)); }
boolean bool(String name) { consumed.add(name); PkiOperationValue value = require(name);
if (!(value instanceof PkiOperationValue.BooleanValue bool)) throw type(); return bool.value(); }
PkiId pkiId(String name) { return new PkiId(text(name)); }
Instant instant(String name) { try { return Instant.parse(text(name)); }
catch (java.time.format.DateTimeParseException failure) { throw type(); } }
Optional<Instant> optionalInstant(String name) { return fields.containsKey(name)
? Optional.of(instant(name)) : Optional.empty(); }
<E extends Enum<E>> Set<E> enumSet(String name, Class<E> type) {
consumed.add(name); PkiOperationValue value = require(name);
if (!(value instanceof PkiOperationValue.ListValue list)) throw type();
Set<E> result = new LinkedHashSet<>();
for (PkiOperationValue item : list.values()) {
if (!(item instanceof PkiOperationValue.Text text) || !result.add(Enum.valueOf(type, text.value()))) {
throw type();
}
}
return Set.copyOf(result);
}
Fields object(String name) { consumed.add(name); return of(require(name)); }
void complete() { if (!Objects.equals(consumed, fields.keySet()))
throw new IllegalArgumentException("Request fields were not consumed"); }

View File

@@ -43,13 +43,18 @@ import zeroecho.pki.application.PkiOperationFailure;
import zeroecho.pki.application.PkiOperationOutcome;
import zeroecho.pki.application.PkiOperationValue;
import zeroecho.pki.server.ServerOperationGateway;
import zeroecho.pki.server.ServerControlOperationOutcome;
/** Deterministic versioned transport response and status mapping. */
@SuppressWarnings("PMD")
final class HttpResponses {
record Response(int statusCode, byte[] body) {
Response { body = Objects.requireNonNull(body, "body").clone(); }
record Response(int statusCode, byte[] body, boolean oneTimeSensitive,
java.util.Optional<String> capabilityId) {
Response { body = Objects.requireNonNull(body, "body").clone();
capabilityId = Objects.requireNonNull(capabilityId, "capabilityId"); }
Response(int statusCode, byte[] body) { this(statusCode, body, false, java.util.Optional.empty()); }
@Override public byte[] body() { return body.clone(); }
void clearSensitiveBody() { if (oneTimeSensitive) java.util.Arrays.fill(body, (byte) 0); }
}
private static final JsonFactory JSON = JsonFactory.builder().build();
@@ -61,6 +66,8 @@ final class HttpResponses {
return switch (outcome) {
case ServerOperationGateway.Outcome.Executed executed -> backend(requestId, operation,
executed.outcome());
case ServerOperationGateway.Outcome.ControlExecuted executed -> control(requestId, operation,
executed.outcome());
case ServerOperationGateway.Outcome.Denied denied -> failure(403, requestId, operation,
"DENIED", "AUTHORIZATION_DENIED", denied.code().name(), false, false);
case ServerOperationGateway.Outcome.ApprovalRequired required -> successLikeFailure(409, requestId,
@@ -68,6 +75,27 @@ final class HttpResponses {
};
}
private static Response control(String requestId, String operation, ServerControlOperationOutcome outcome) {
if (outcome instanceof ServerControlOperationOutcome.Ordinary ordinary) {
return backend(requestId, operation, ordinary.outcome());
}
ServerControlOperationOutcome.OneTimeSensitive sensitive =
(ServerControlOperationOutcome.OneTimeSensitive) outcome;
byte[] token = sensitive.consumeToken();
try {
MapBuilder result = new MapBuilder(sensitive.safeResult().fields());
result.put("token", new PkiOperationValue.Text(java.util.Base64.getUrlEncoder()
.withoutPadding().encodeToString(token)));
Response encoded = encode(200, requestId, operation, "SUCCEEDED",
new PkiOperationValue.ObjectValue(result.values()), null);
String capabilityId = ((PkiOperationValue.Text) sensitive.safeResult().fields()
.get("capabilityId")).value();
return new Response(encoded.statusCode(), encoded.body(), true, java.util.Optional.of(capabilityId));
} finally {
java.util.Arrays.fill(token, (byte) 0);
}
}
static Response failure(int status, String requestId, String operation, String classification,
String code, String messageKey, boolean recoveryRequired, boolean reconciliationRequired) {
return encode(status, requestId, operation, "FAILED", null,
@@ -153,4 +181,11 @@ final class HttpResponses {
private record Failure(String classification, String code, String messageKey,
boolean recoveryRequired, boolean reconciliationRequired) { }
private static final class MapBuilder {
private final java.util.Map<String, PkiOperationValue> values = new java.util.LinkedHashMap<>();
private MapBuilder(java.util.Map<String, PkiOperationValue> initial) { values.putAll(initial); }
private void put(String key, PkiOperationValue value) { values.put(key, value); }
private java.util.Map<String, PkiOperationValue> values() { return values; }
}
}

View File

@@ -2,7 +2,7 @@
"schemaVersion": 1,
"roles": [
{"id":"platform-operator","version":1,"actions":["REALM_READ","SERVER_HEALTH_READ","SERVER_CONFIGURATION_READ","SERVER_CONFIGURATION_UPDATE"]},
{"id":"security-administrator","version":1,"actions":["REALM_READ","IDENTITY_PROVIDER_MANAGE","PRINCIPAL_MANAGE","ROLE_MANAGE","PERMISSION_GRANT"]},
{"id":"security-administrator","version":1,"actions":["REALM_READ","IDENTITY_PROVIDER_MANAGE","PRINCIPAL_MANAGE","ROLE_MANAGE","PERMISSION_GRANT","DISCLOSURE_READ","DISCLOSURE_CHANGE","DISCLOSURE_CAPABILITY_ISSUE","DISCLOSURE_CAPABILITY_REVOKE"]},
{"id":"ca-security-officer","version":1,"actions":["AUTHORITY_LIST","AUTHORITY_READ","AUTHORITY_CREATE","AUTHORITY_IMPORT","AUTHORITY_ACTIVATE","AUTHORITY_SUSPEND","AUTHORITY_RETIRE","ISSUER_CREATE","ISSUER_ROTATE","ISSUER_RETIRE","CA_CHAIN_DOWNLOAD"]},
{"id":"profile-policy-manager","version":1,"actions":["PROFILE_READ","PROFILE_REGISTER","PROFILE_VALIDATE","PROFILE_ACTIVATE","PROFILE_DEACTIVATE","POLICY_READ","POLICY_UPDATE","X509_BINDING_READ","X509_BINDING_PROVIDER_ENABLE"]},
{"id":"enrollment-officer","version":1,"actions":["REQUEST_SUBMIT","REQUEST_READ_ANY","CERTIFICATE_ISSUE","CERTIFICATE_READ_METADATA","CERTIFICATE_DOWNLOAD"]},
@@ -12,8 +12,8 @@
{"id":"publication-operator","version":1,"actions":["PUBLICATION_REGISTER","PUBLICATION_READ","PUBLICATION_PROCESS","PUBLICATION_RETRY","PUBLICATION_RECONCILE"]},
{"id":"backup-operator","version":1,"actions":["BACKUP_EXPORT","BACKUP_VERIFY"]},
{"id":"recovery-officer","version":1,"actions":["RESTORE_EXECUTE"]},
{"id":"auditor","version":1,"actions":["AUDIT_READ_REDACTED","AUDIT_INTEGRITY_VERIFY","REALM_READ","AUTHORITY_LIST","AUTHORITY_READ","PROFILE_READ","CERTIFICATE_READ_METADATA","REVOCATION_HISTORY_READ","PUBLICATION_READ"]},
{"id":"privileged-auditor","version":1,"actions":["AUDIT_READ_REDACTED","AUDIT_READ_FULL","AUDIT_READ_PII","AUDIT_EXPORT","AUDIT_INTEGRITY_VERIFY","CERTIFICATE_READ_METADATA","CERTIFICATE_READ_CONTENT","CERTIFICATE_READ_PII"]},
{"id":"auditor","version":1,"actions":["AUDIT_READ_REDACTED","AUDIT_INTEGRITY_VERIFY","REALM_READ","AUTHORITY_LIST","AUTHORITY_READ","PROFILE_READ","CERTIFICATE_READ_METADATA","REVOCATION_HISTORY_READ","PUBLICATION_READ","DISCLOSURE_READ"]},
{"id":"privileged-auditor","version":1,"actions":["AUDIT_READ_REDACTED","AUDIT_READ_FULL","AUDIT_READ_PII","AUDIT_EXPORT","AUDIT_INTEGRITY_VERIFY","CERTIFICATE_READ_METADATA","CERTIFICATE_READ_CONTENT","CERTIFICATE_READ_PII","DISCLOSURE_READ"]},
{"id":"acme-administrator","version":1,"actions":["REALM_READ","AUTHORITY_READ","PROFILE_READ","POLICY_READ"]},
{"id":"public-principal","version":1,"actions":["REALM_READ","CA_CHAIN_DOWNLOAD","CRL_DOWNLOAD"]}
]