/*******************************************************************************
* 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.io.IOException;
import java.nio.file.Files;
import java.security.SecureRandom;
import java.time.Clock;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicReference;
import zeroecho.pki.application.PkiSession;
import zeroecho.pki.application.PkiOperation;
import zeroecho.pki.application.PkiOperationOutcome;
import zeroecho.pki.application.PkiSessionRuntimeDependencies;
import zeroecho.pki.impl.fs.PosixTransactionalMetadataStore;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
import zeroecho.pki.server.acme.AcmeControlStore;
/**
* Lifecycle owner for one server realm, one long-lived PKI session, and one
* dedicated durable server-control authority.
*
*
The context is transport-neutral and synchronous. It creates no execution
* lane, queue, thread, scheduler, retry loop, HTTP type, or ACME object. A future
* transport can retain one instance for its process lifetime and submit existing
* typed operations through {@link #gateway()}.
*/
@SuppressWarnings({ "PMD.CommentDefaultAccessModifier", "PMD.CloseResource", "PMD.UseProperClassLoader",
"PMD.AvoidCatchingGenericException", "PMD.LinguisticNaming", "PMD.ControlStatementBraces",
"PMD.PreserveStackTrace", "PMD.SignatureDeclareThrowsException", "PMD.CommentRequired",
"PMD.AvoidSynchronizedAtMethodLevel", "PMD.AvoidInstantiatingObjectsInLoops" })
public final class ServerRealmContext implements AutoCloseable {
/** Closed lifecycle states. */
public enum State { OPEN, CLOSING, CLOSED }
private final ServerRealmConfiguration configuration;
private final ServerControlStore control;
private final PkiSession session;
private final RoleTemplateCatalog roles;
private final AuthorizationEngine authorization;
private final ApprovalService approvals;
private final BreakGlassService breakGlass;
private final DisclosureService disclosure;
private final RepositoryAliasService repositoryAliases;
private final AcmeControlStore acmeControl;
private final PublicRepositoryGateway publicRepository;
private final AuditorViews auditorViews;
private final ServerOperationGateway gateway;
private final SafeAudit audit;
private final AtomicReference state = new AtomicReference<>(State.OPEN);
private ServerRealmContext(ServerRealmConfiguration configuration, ServerControlStore control,
PkiSession session, RoleTemplateCatalog roles, AuthorizationEngine authorization,
ApprovalService approvals, BreakGlassService breakGlass, DisclosureService disclosure,
RepositoryAliasService repositoryAliases,
AuditorViews auditorViews, AuditSink auditSink, Clock clock) {
this.configuration = configuration;
this.control = control;
this.session = session;
this.roles = roles;
this.authorization = authorization;
this.approvals = approvals;
this.breakGlass = breakGlass;
this.disclosure = disclosure;
this.repositoryAliases = repositoryAliases;
this.acmeControl = new AcmeControlStore(control);
this.acmeControl.validateAndRecover(clock);
this.publicRepository = new PublicRepositoryGateway(configuration.realmId(), configuration.authorityExposure(),
session.repository(), control, roles, authorization, breakGlass, disclosure, repositoryAliases,
this::requireOpen);
this.auditorViews = auditorViews;
this.audit = new SafeAudit(clock, auditSink);
this.gateway = new ServerOperationGateway(configuration.realmId(), configuration.authorityExposure(),
control, roles, authorization, approvals, breakGlass, disclosure, auditorViews,
Optional.of(repositoryAliases),
new OperationSecurityDescriptors(),
session.operations(), session.resourceScopes(), configuration.approvalPolicies(), clock, auditSink,
this::requireOpen);
}
/**
* Opens one production realm using explicit process-local capabilities.
*
* @param configuration validated immutable realm configuration
* @param dependencies key-access capabilities, never secret values
* @return fully recovered realm context
*/
public static ServerRealmContext open(ServerRealmConfiguration configuration,
PkiSessionRuntimeDependencies dependencies) {
return open(configuration, dependencies, Clock.systemUTC(), new SecureRandom());
}
static ServerRealmContext open(ServerRealmConfiguration configuration,
PkiSessionRuntimeDependencies dependencies, Clock clock, SecureRandom random) {
ServerRealmConfiguration exact = Objects.requireNonNull(configuration, "configuration");
PkiSessionRuntimeDependencies runtime = Objects.requireNonNull(dependencies, "dependencies");
Objects.requireNonNull(clock, "clock");
Objects.requireNonNull(random, "random");
if (Files.isSymbolicLink(exact.controlLogPath())) {
throw new IllegalArgumentException("Server-control log must not be a symbolic link");
}
ServerControlStore control = null;
SharedAuditSink audit = null;
PkiSession session = null;
try {
control = new ServerControlStore(openControl(exact));
ServerControlStore.RealmRecord realm = new ServerControlStore.RealmRecord(exact.realmId(),
exact.displayName(), exact.authorityExposure(), exact.authorizationCommitment(),
exact.approvalCommitment(), exact.disclosureCommitment(), exact.controlStoreId());
control.ensureRealm(realm);
RoleTemplateCatalog roles = RoleTemplateCatalog.load(ServerRealmContext.class.getClassLoader());
control.validateAll();
control.validateReferences(roles);
audit = new SharedAuditSink(PkiBootstrap.openAudit(exact.pkiSessionConfiguration().audit()));
session = PkiSession.open(exact.pkiSessionConfiguration(), runtime.withAuditSink(audit));
validateExposure(exact.authorityExposure(), session);
AuthorizationEngine authorization = new AuthorizationEngine(clock);
ApprovalService approvals = new ApprovalService(control, clock, audit);
BreakGlassService breakGlass = new BreakGlassService(control, clock, audit);
DisclosureService disclosure = new DisclosureService(exact.realmId(), control,
exact.disclosureDefaults(), clock, random, audit);
RepositoryAliasService aliases = new RepositoryAliasService(exact.realmId(), control,
session.repository(), disclosure, clock, audit);
AuditorViews views = new AuditorViews(clock, audit);
ServerRealmContext result = new ServerRealmContext(exact, control, session, roles, authorization,
approvals, breakGlass, disclosure, aliases, views, audit, clock);
result.audit.record("REALM_OPEN", "system", Optional.empty(), Map.of("realm", exact.realmId().value()));
return result;
} catch (RuntimeException | Error primary) {
closeAfterFailure(session, audit, control, primary);
throw primary;
}
}
/** @return exact realm configuration without rendering provider values */
public ServerRealmConfiguration configuration() { requireOpen(); return configuration; }
/** @return the one long-lived PKI session */
public PkiSession session() { requireOpen(); return session; }
/** @return immutable built-in role template catalog */
public RoleTemplateCatalog roleTemplates() { requireOpen(); return roles; }
/** @return deterministic authorization engine */
public AuthorizationEngine authorization() { requireOpen(); return authorization; }
/** @return durable approval service */
public ApprovalService approvals() { requireOpen(); return approvals; }
/** @return durable break-glass service */
public BreakGlassService breakGlass() { requireOpen(); return breakGlass; }
/** @return durable disclosure decision service */
public DisclosureService disclosure() { requireOpen(); return disclosure; }
/** @return durable non-authoritative public repository alias service */
public RepositoryAliasService repositoryAliases() { requireOpen(); return repositoryAliases; }
/** @return typed ACME records in the realm's sole durable control authority */
public AcmeControlStore acmeControl() { requireOpen(); return acmeControl; }
/** @return read-only disclosed public repository gateway */
public PublicRepositoryGateway publicRepository() { requireOpen(); return publicRepository; }
/** @return explicit auditor projection service */
public AuditorViews auditorViews() { requireOpen(); return auditorViews; }
/** @return authorized typed-operation gateway */
public ServerOperationGateway gateway() { requireOpen(); return gateway; }
/**
* Resolves one persisted principal for transport authentication.
*
* @param principalId canonical principal identity
* @return persisted identity metadata without credentials or grants
* @throws IllegalArgumentException when the principal is unavailable
* @throws IllegalStateException when this realm is not open
*/
public SecurityPrincipal principal(String principalId) {
requireOpen();
return control.requirePrincipal(principalId);
}
/**
* Records one transport-safe lifecycle or request classification through the
* shared realm audit authority.
*
* @param action stable transport action
* @param principalId authenticated principal or {@code system}
* @param safeDetails finite pre-redacted details
*/
public void auditTransport(String action, String principalId, Map safeDetails) {
requireOpen();
Permission.requireBounded(action, 128, "audit action");
Permission.requirePrincipal(principalId);
audit.record(action, principalId, Optional.empty(), Map.copyOf(safeDetails));
}
/** @return current lifecycle state */
public State state() { return state.get(); }
/** Creates a principal and records only safe administrative audit metadata. */
public void createPrincipal(SecurityPrincipal principal, String actorPrincipalId) {
requireOpen();
control.createPrincipal(principal);
audit.record("PRINCIPAL_CREATE", actorPrincipalId, Optional.empty(),
Map.of("principal", principal.principalId(), "enabled", Boolean.toString(principal.enabled())));
}
/** Changes only the enabled state of an existing principal. */
public SecurityPrincipal setPrincipalEnabled(String principalId, boolean enabled, String actorPrincipalId) {
requireOpen();
SecurityPrincipal current = control.requirePrincipal(principalId);
SecurityPrincipal updated = new SecurityPrincipal(current.principalId(), current.type(),
current.displayName(), current.organization(), current.attributes(), enabled);
control.replacePrincipal(current, updated);
audit.record("PRINCIPAL_STATE", actorPrincipalId, Optional.empty(),
Map.of("principal", principalId, "enabled", Boolean.toString(enabled)));
return updated;
}
/** Creates one scoped role assignment; templates remain non-authoritative alone. */
public void assignRole(RoleTemplateCatalog.Assignment assignment, String actorPrincipalId) {
requireOpen();
roles.instantiate(assignment);
control.requirePrincipal(assignment.principalId());
control.createAssignment(assignment);
audit.record("ROLE_ASSIGNMENT_CREATE", actorPrincipalId, Optional.empty(),
Map.of("assignment", assignment.assignmentId(), "template", assignment.templateId()));
}
/** Creates one explicit scoped direct grant. */
public void grant(Permission.Grant grant, String actorPrincipalId) {
requireOpen();
control.requirePrincipal(grant.principalId());
control.createGrant(grant);
audit.record("PERMISSION_GRANT_CREATE", actorPrincipalId, Optional.empty(),
Map.of("grant", grant.grantId(), "effect", grant.effect().name()));
}
/**
* Rejects new work and closes session then server-control authority. Repeated
* calls are harmless and failure suppression preserves causal order.
*/
@Override
public void close() throws Exception {
if (!state.compareAndSet(State.OPEN, State.CLOSING)) return;
Throwable primary = null;
try {
audit.record("REALM_CLOSE", "system", Optional.empty(), Map.of("realm", configuration.realmId().value()));
} catch (Throwable failure) {
primary = failure;
}
primary = closeOne(session, primary);
primary = closeOne(control, primary);
state.set(State.CLOSED);
rethrow(primary);
}
private void requireOpen() {
if (state.get() != State.OPEN) throw new IllegalStateException("Server realm is not open");
}
private static PosixTransactionalMetadataStore openControl(ServerRealmConfiguration configuration) {
try {
if (Files.exists(configuration.controlLogPath())) {
PosixTransactionalMetadataStore opened = PosixTransactionalMetadataStore.open(
configuration.controlLogPath(), OptionalLong.of(1_048_576));
if (!opened.id().equals(configuration.controlStoreId())) {
opened.close();
throw new IllegalStateException("Server-control store identity differs");
}
return opened;
}
return PosixTransactionalMetadataStore.create(configuration.controlLogPath(),
configuration.controlStoreId(), OptionalLong.of(1_048_576));
} catch (IOException failure) {
throw new IllegalStateException("Server-control metadata authority cannot be opened");
}
}
private static void validateExposure(AuthorityExposurePolicy exposure, PkiSession session) {
if (exposure.mode() == AuthorityExposurePolicy.Mode.EXPLICIT_AUTHORITIES) {
for (zeroecho.pki.api.PkiId authorityId : exposure.authorityIds()) {
PkiOperationOutcome outcome = session.operations().execute(
new PkiOperation.InspectAuthority(authorityId), zeroecho.core.io.CancellationSignal.NONE);
if (!(outcome instanceof PkiOperationOutcome.Success)) {
throw new IllegalStateException("Configured authority exposure contains an unavailable authority");
}
}
}
}
private static void closeAfterFailure(PkiSession session, AuditSink audit, ServerControlStore control,
Throwable primary) {
Throwable result = closeOne(session, primary);
if (session == null) result = closeOne(audit, result);
closeOne(control, result);
}
private static Throwable closeOne(AutoCloseable resource, Throwable primary) {
if (resource == null) return primary;
try {
resource.close();
} catch (Throwable failure) {
if (primary == null) return failure;
primary.addSuppressed(failure);
}
return primary;
}
private static void rethrow(Throwable failure) throws Exception {
if (failure == null) return;
if (failure instanceof Exception exception) throw exception;
if (failure instanceof Error error) throw error;
throw new IllegalStateException("Unexpected realm close failure");
}
private static final class SharedAuditSink implements AuditSink {
private final AuditSink delegate;
private boolean closed;
private SharedAuditSink(AuditSink delegate) { this.delegate = Objects.requireNonNull(delegate, "delegate"); }
@Override public synchronized void record(zeroecho.pki.api.audit.AuditEvent event) {
if (closed) throw new IllegalStateException("Audit sink is closed");
delegate.record(event);
}
@Override public synchronized void close() {
if (!closed) { closed = true; delegate.close(); }
}
}
}