feat(pki-server): add secure administrative HTTPS API

Add the mutually authenticated administrative HTTPS server with strict
typed JSON, bounded request execution, multi-authority authorization,
approval enforcement, safe auditing and finite shutdown.

Reuse one long-lived realm and PKI session without duplicating backend
authority or operation semantics.
This commit is contained in:
2026-08-04 20:16:34 +02:00
parent 8a5cbb61b3
commit d7793e5c49
30 changed files with 3866 additions and 1 deletions

View File

@@ -58,7 +58,7 @@ 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.ExcessiveParameterList", "PMD.CyclomaticComplexity", "PMD.AvoidInstantiatingObjectsInLoops" })
public final class ServerOperationGateway {
/**
* Complete transport-neutral request admission input.
@@ -218,6 +218,92 @@ public final class ServerOperationGateway {
return new Outcome.Executed(backend);
}
/**
* Authorizes one non-PKI administrative control resource through the same
* default-deny engine used for typed operations.
*
* @param principalId authenticated persisted principal
* @param action exact control action
* @param resource exact realm control resource
* @param context safe closed-condition context
* @param correlationId bounded request correlation identity
* @return safe authorization decision
*/
public AuthorizationEngine.Decision authorizeControl(String principalId, Permission.Action action,
Permission.Resource resource, Permission.Context context, String correlationId) {
openCheck.run();
Permission.requirePrincipal(principalId);
Permission.requireBounded(correlationId, 256, "correlation ID");
SecurityPrincipal principal = control.requirePrincipal(principalId);
List<Permission.Grant> grants = grants(principal);
BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principalId);
grants.addAll(emergency.grants());
AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(realmId,
exposure, principal, action, resource, Permission.Relationship.ANY,
Permission.DataView.METADATA_REDACTED, context, grants, emergency.grantIds()));
audit.record(decision.allowed() ? "AUTHORIZATION_ALLOW" : "AUTHORIZATION_DENY", principalId,
Optional.empty(), Map.of("action", action.name(), "code", decision.code().name(),
"correlation", correlationId));
if (decision.usedBreakGlass()) breakGlass.auditUse(principalId);
return decision;
}
/**
* Returns operation descriptors discoverable by one principal without
* returning grant scopes or protected resource identities.
*
* @param principalId authenticated persisted principal
* @return descriptors ordered by stable operation identity
*/
public List<OperationSecurityDescriptors.Descriptor> discoverableOperations(String principalId) {
openCheck.run();
SecurityPrincipal principal = control.requirePrincipal(principalId);
List<Permission.Grant> grants = grants(principal);
BreakGlassService.ActiveGrants emergency = breakGlass.activeFor(principalId);
grants.addAll(emergency.grants());
return descriptors.descriptors().values().stream()
.filter(descriptor -> discoverable(descriptor, principal, grants, emergency.grantIds()))
.sorted(java.util.Comparator.comparing(OperationSecurityDescriptors.Descriptor::operationId))
.toList();
}
/**
* Resolves the existing security descriptor for a decoded typed operation.
*
* @param operation decoded transport-neutral operation
* @return immutable descriptor from the sole gateway descriptor authority
* @throws SecurityException when the operation is not remotely exposed
*/
public OperationSecurityDescriptors.Descriptor descriptor(PkiOperation operation) {
openCheck.run();
return descriptors.require(operation);
}
private List<Permission.Grant> grants(SecurityPrincipal principal) {
List<Permission.Grant> grants = new ArrayList<>(control.grantsFor(principal.principalId()));
for (RoleTemplateCatalog.Assignment assignment : control.assignmentsFor(principal.principalId())) {
grants.addAll(roles.instantiate(assignment));
}
return grants;
}
private boolean discoverable(OperationSecurityDescriptors.Descriptor descriptor,
SecurityPrincipal principal, List<Permission.Grant> grants,
java.util.Set<String> breakGlassIds) {
for (Permission.Grant grant : grants) {
if (grant.action() != descriptor.action() || grant.resourceType() != descriptor.resourceType()) {
continue;
}
Permission.Resource resource = new Permission.Resource(descriptor.resourceType(), grant.scope(),
Optional.empty(), Optional.empty());
AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(
realmId, exposure, principal, descriptor.action(), resource, Permission.Relationship.ANY,
descriptor.dataView(), Permission.Context.empty(), grants, breakGlassIds));
if (decision.allowed()) return true;
}
return false;
}
private PkiOperationOutcome filterAuthorities(PkiOperationOutcome outcome, SecurityPrincipal principal,
List<Permission.Grant> grants, java.util.Set<String> breakGlassIds, Permission.Context context) {
if (!(outcome instanceof PkiOperationOutcome.Success success)) return outcome;