Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.clearfolio.viewer.audit;

import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Component;

import com.clearfolio.viewer.auth.TenantContext;
import com.clearfolio.viewer.config.ConversionProperties;
import com.clearfolio.viewer.security.AuditPseudonymizer;

/**
* Emits structured, privacy-safe administrative authorization evidence.
*
* <p>Actor, tenant, and conversion-job identifiers are pseudonymized in
* separate HMAC domains. The logger never emits raw claim headers, subject
* identifiers, tenant identifiers, job identifiers, tokens, filenames, job
* messages, or document content.</p>
*/
@Component
public final class AdministrativeAuditLogger {

private static final Logger LOGGER = LoggerFactory.getLogger(AdministrativeAuditLogger.class);
private static final int NO_RESULT_COUNT = -1;

private final AuditPseudonymizer actorPseudonymizer;
private final AuditPseudonymizer tenantPseudonymizer;
private final AuditPseudonymizer jobPseudonymizer;

/**
* Administrative actions represented in authorization evidence.
*/
public enum Action {
/** Lists tenant-owned conversion jobs. */
LIST_JOBS,
/** Deletes one tenant-owned conversion job. */
DELETE_JOB,
/** Retries one tenant-owned dead-lettered conversion job. */
RETRY_JOB
}

/**
* Stable outcomes represented in authorization evidence.
*/
public enum Outcome {
/** Authorization and the requested operation succeeded. */
ALLOWED,
/** Authentication or permission evaluation denied the request. */
DENIED,
/** The resource was absent or intentionally concealed. */
NOT_FOUND,
/** The resource existed but its state rejected the operation. */
NOT_ELIGIBLE,
/** Authorization succeeded but the operation failed unexpectedly. */
FAILED
}

/**
* Creates the logger from the dedicated audit pseudonym configuration.
*
* @param properties conversion and audit configuration
*/
public AdministrativeAuditLogger(ConversionProperties properties) {
String secret = properties.getAuditPseudonymSecret();
String keyVersion = properties.getAuditPseudonymKeyVersion();
this.actorPseudonymizer = AuditPseudonymizer.forAdministrativeActor(secret, keyVersion);
this.tenantPseudonymizer = AuditPseudonymizer.forAdministrativeTenant(secret, keyVersion);
this.jobPseudonymizer = AuditPseudonymizer.forAdministrativeJob(secret, keyVersion);
}

/**
* Returns a privacy-safe actor identifier suitable for retry provenance.
*
* @param context authenticated tenant context, or null when unavailable
* @return administrative actor fingerprint
*/
public String actorFingerprint(TenantContext context) {
return actorPseudonymizer.fingerprint(context == null ? null : context.subjectId());
}

/**
* Records an authorization decision using authenticated context values.
*
* @param context authenticated tenant context, or null when unavailable
* @param action administrative action
* @param outcome decision outcome
* @param status response status
* @param jobId optional job identifier used only as a keyed HMAC input
* @param resultCount optional list result count
*/
public void record(
TenantContext context,
Action action,
Outcome outcome,
HttpStatusCode status,
UUID jobId,
Integer resultCount
) {
recordIdentifiers(
context == null ? null : context.tenantId(),
context == null ? null : context.subjectId(),
action,
outcome,
status,
jobId,
resultCount
);
}

/**
* Records a denied request using untrusted headers only as HMAC inputs.
*
* @param headers request headers, or null when unavailable
* @param action administrative action
* @param outcome decision outcome
* @param status response status
* @param jobId optional job identifier used only as a keyed HMAC input
*/
public void recordHeaders(
HttpHeaders headers,
Action action,
Outcome outcome,
HttpStatusCode status,
UUID jobId
) {
recordIdentifiers(
firstHeader(headers, TenantContext.TENANT_ID_HEADER),
firstHeader(headers, TenantContext.SUBJECT_ID_HEADER),
action,
outcome,
status,
jobId,
null
);
}

private void recordIdentifiers(
String tenantId,
String subjectId,
Action action,
Outcome outcome,
HttpStatusCode status,
UUID jobId,
Integer resultCount
) {
LOGGER.info(
"Administrative access decision action={} outcome={} status={} "
+ "tenantFingerprint={} actorFingerprint={} jobFingerprint={} resultCount={}",
action,
outcome,
status.value(),
tenantPseudonymizer.fingerprint(tenantId),
actorPseudonymizer.fingerprint(subjectId),
jobPseudonymizer.fingerprint(jobId == null ? null : jobId.toString()),
resultCount == null ? NO_RESULT_COUNT : resultCount
);
}

private String firstHeader(HttpHeaders headers, String name) {
return headers == null ? null : headers.getFirst(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public final class AuditPseudonymizer {
private static final String HMAC_SHA_256 = "HmacSHA256";
private static final String DEFAULT_KEY_VERSION = "v1";
private static final String APPROVER_DOMAIN = "clearfolio:audit-approver:v1";
private static final String ADMIN_ACTOR_DOMAIN = "clearfolio:audit-admin-actor:v1";
private static final String ADMIN_TENANT_DOMAIN = "clearfolio:audit-admin-tenant:v1";
private static final String ADMIN_JOB_DOMAIN = "clearfolio:audit-admin-job:v1";
private static final int FINGERPRINT_BYTES = 16;
private static final int MIN_SECRET_BYTES = 32;
private static final int MAX_KEY_VERSION_LENGTH = 32;
Expand All @@ -41,6 +44,63 @@ public AuditPseudonymizer(String secret, String keyVersion) {
this(secret, keyVersion, APPROVER_DOMAIN);
}

/**
* Creates a pseudonymizer for administrative actor identifiers.
*
* @param secret dedicated audit pseudonym secret; when configured, it must
* contain at least 32 UTF-8 bytes; null or blank disables
* correlation and produces the fixed unavailable marker
* @param keyVersion non-sensitive key-rotation identifier; null selects
* {@code v1}
* @return actor-domain pseudonymizer
* @throws IllegalArgumentException when a configured secret is shorter than
* 32 UTF-8 bytes, or when the key version is
* empty, longer than 32 characters, or
* contains characters outside letters,
* digits, period, underscore, and hyphen
*/
public static AuditPseudonymizer forAdministrativeActor(String secret, String keyVersion) {
return new AuditPseudonymizer(secret, keyVersion, ADMIN_ACTOR_DOMAIN);
}

/**
* Creates a pseudonymizer for administrative tenant identifiers.
*
* @param secret dedicated audit pseudonym secret; when configured, it must
* contain at least 32 UTF-8 bytes; null or blank disables
* correlation and produces the fixed unavailable marker
* @param keyVersion non-sensitive key-rotation identifier; null selects
* {@code v1}
* @return tenant-domain pseudonymizer
* @throws IllegalArgumentException when a configured secret is shorter than
* 32 UTF-8 bytes, or when the key version is
* empty, longer than 32 characters, or
* contains characters outside letters,
* digits, period, underscore, and hyphen
*/
public static AuditPseudonymizer forAdministrativeTenant(String secret, String keyVersion) {
return new AuditPseudonymizer(secret, keyVersion, ADMIN_TENANT_DOMAIN);
}

/**
* Creates a pseudonymizer for administrative conversion-job identifiers.
*
* @param secret dedicated audit pseudonym secret; when configured, it must
* contain at least 32 UTF-8 bytes; null or blank disables
* correlation and produces the fixed unavailable marker
* @param keyVersion non-sensitive key-rotation identifier; null selects
* {@code v1}
* @return job-domain pseudonymizer
* @throws IllegalArgumentException when a configured secret is shorter than
* 32 UTF-8 bytes, or when the key version is
* empty, longer than 32 characters, or
* contains characters outside letters,
* digits, period, underscore, and hyphen
*/
public static AuditPseudonymizer forAdministrativeJob(String secret, String keyVersion) {
return new AuditPseudonymizer(secret, keyVersion, ADMIN_JOB_DOMAIN);
}

/**
* Creates a pseudonymizer with an explicit domain for isolated internal use
* and domain-separation verification.
Expand Down
Loading
Loading