refactor(pki): make status authority transactional
Persist immutable status records and STATUS_OBJECT_RECORD content-owner edges atomically through the transactional metadata store. Remove the former status/by-id file authority and scan-derived content retention while keeping CRL payloads external and publication as a separate post-commit lifecycle.
This commit is contained in:
@@ -169,8 +169,13 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
/* package */ static final String CURRENT_STORE_VERSION = "v3";
|
/* package */ static final String CURRENT_STORE_VERSION = "v3";
|
||||||
private static final String SIGN_RECORD_NAMESPACE = "io.zeroecho.pki.signing-record";
|
private static final String SIGN_RECORD_NAMESPACE = "io.zeroecho.pki.signing-record";
|
||||||
private static final String SIGN_OWNER_NAMESPACE = "io.zeroecho.pki.signing-owner";
|
private static final String SIGN_OWNER_NAMESPACE = "io.zeroecho.pki.signing-owner";
|
||||||
|
private static final String STATUS_RECORD_NAMESPACE = "io.zeroecho.pki.status-object-record";
|
||||||
|
private static final String STATUS_OWNER_NAMESPACE = "io.zeroecho.pki.status-object-owner";
|
||||||
private static final int CURRENT_SIGN_RECORD_VERSION = 2;
|
private static final int CURRENT_SIGN_RECORD_VERSION = 2;
|
||||||
private static final int SIGN_OWNER_VALUE_VERSION = 1;
|
private static final int SIGN_OWNER_VALUE_VERSION = 1;
|
||||||
|
private static final int STATUS_OWNER_VALUE_VERSION = 1;
|
||||||
|
private static final int METADATA_TRANSFER_BUFFER_BYTES = 16 * 1024;
|
||||||
|
private static final ThreadLocal<StatusCommitFaultPoint> STATUS_COMMIT_FAULT = new ThreadLocal<>();
|
||||||
private static final String SIGN_FINGERPRINT_PREFIX = "signfp:v1:";
|
private static final String SIGN_FINGERPRINT_PREFIX = "signfp:v1:";
|
||||||
private static final int SIGN_FINGERPRINT_HEX_LENGTH = 64;
|
private static final int SIGN_FINGERPRINT_HEX_LENGTH = 64;
|
||||||
private static final long INITIAL_FENCE = 0L;
|
private static final long INITIAL_FENCE = 0L;
|
||||||
@@ -250,7 +255,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
FsOperations.ensureDir(this.paths.transactionalMetadataLog().getParent());
|
FsOperations.ensureDir(this.paths.transactionalMetadataLog().getParent());
|
||||||
openedMetadata = openMetadataStore();
|
openedMetadata = openMetadataStore();
|
||||||
this.metadataStore = openedMetadata;
|
this.metadataStore = openedMetadata;
|
||||||
this.stagedContent.bindSigningOwnership(this::findSigningOwner);
|
this.stagedContent.bindTransactionalOwnership(this::findTransactionalOwners);
|
||||||
this.credentialContentTransactions = new CredentialContentTransaction(this.paths, this.stagedContent);
|
this.credentialContentTransactions = new CredentialContentTransaction(this.paths, this.stagedContent);
|
||||||
this.signingTimeWatermark = new AtomicLong(loadSigningTimeWatermark());
|
this.signingTimeWatermark = new AtomicLong(loadSigningTimeWatermark());
|
||||||
this.historySeq = new AtomicLong(0L);
|
this.historySeq = new AtomicLong(0L);
|
||||||
@@ -313,6 +318,17 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<DurableContentOwner> findTransactionalOwners(DurableContentReference reference) throws IOException {
|
||||||
|
Set<DurableContentOwner> owners = new HashSet<>(findSigningOwner(reference));
|
||||||
|
for (StoredStatus stored : listStoredStatuses()) {
|
||||||
|
if (reference.equals(stored.status().content())) {
|
||||||
|
owners.add(new DurableContentOwner(DurableContentOwner.Category.STATUS_OBJECT_RECORD,
|
||||||
|
stored.status().statusObjectId().value()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set.copyOf(owners);
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<DurableContentOwner> matchingSigningOwner(MetadataSnapshot.Record owner,
|
private Optional<DurableContentOwner> matchingSigningOwner(MetadataSnapshot.Record owner,
|
||||||
DurableContentReference reference)
|
DurableContentReference reference)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -330,13 +346,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
|
|
||||||
private void recoverStagedContent() throws IOException {
|
private void recoverStagedContent() throws IOException {
|
||||||
credentialContentTransactions.recover();
|
credentialContentTransactions.recover();
|
||||||
|
List<StoredStatus> statuses = listStoredStatuses();
|
||||||
try (TemporaryUniqueIndex retained = stagedContent.beginUniqueIndex();
|
try (TemporaryUniqueIndex retained = stagedContent.beginUniqueIndex();
|
||||||
TemporaryUniqueIndex retainedOwners = stagedContent.beginOwnerIndex()) {
|
TemporaryUniqueIndex retainedOwners = stagedContent.beginOwnerIndex();
|
||||||
|
TemporaryUniqueIndex transactionalOwners = stagedContent.beginUniqueIndex()) {
|
||||||
try {
|
try {
|
||||||
addPersistedCredentialReferences(retained, retainedOwners);
|
addPersistedCredentialReferences(retained, retainedOwners);
|
||||||
addPersistedStatusReferences(retained);
|
addTransactionalStatusOwnership(transactionalOwners, statuses);
|
||||||
addPendingSigningReferences(retained);
|
addPendingSigningReferences(retained);
|
||||||
stagedContent.recoverContent(retained, retainedOwners);
|
stagedContent.recoverContent(retained, retainedOwners, transactionalOwners);
|
||||||
} catch (IllegalStateException | PkiException malformedDurableState) {
|
} catch (IllegalStateException | PkiException malformedDurableState) {
|
||||||
// Recovery cannot prove abandonment while durable metadata is
|
// Recovery cannot prove abandonment while durable metadata is
|
||||||
// corrupt. Preserve content so the normal owning subsystem can
|
// corrupt. Preserve content so the normal owning subsystem can
|
||||||
@@ -366,18 +384,11 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPersistedStatusReferences(TemporaryUniqueIndex retained) throws IOException {
|
private static void addTransactionalStatusOwnership(TemporaryUniqueIndex transactionalOwners,
|
||||||
Path root = paths.root().resolve("status").resolve("by-id");
|
List<StoredStatus> statuses)
|
||||||
if (!Files.isDirectory(root)) {
|
throws IOException {
|
||||||
return;
|
for (StoredStatus stored : statuses) {
|
||||||
}
|
addRetained(transactionalOwners, stored.status().content());
|
||||||
try (Stream<Path> pathsStream = Files.list(root)) {
|
|
||||||
java.util.Iterator<Path> iterator = pathsStream.filter(Files::isRegularFile).iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
StatusObject status = FsCodec.decode(FsCodec.STATUS_OBJECT, FsOperations.readAll(iterator.next()),
|
|
||||||
stagedContent);
|
|
||||||
addRetained(retained, status.content());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,15 +441,17 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
/* default */ Set<String> snapshotNonCredentialContentIds() {
|
/* default */ Set<String> snapshotNonCredentialContentIds() {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Set<String> contentIds = new HashSet<>();
|
Set<String> contentIds = new HashSet<>();
|
||||||
for (StatusObject status : listBinaryFiles(paths.statusRoot(), FsCodec.STATUS_OBJECT)) {
|
|
||||||
contentIds.add(status.content().contentId());
|
|
||||||
}
|
|
||||||
for (StoredSign stored : listStoredSigns()) {
|
for (StoredSign stored : listStoredSigns()) {
|
||||||
stored.reference().map(DurableContentReference::contentId).ifPresent(contentIds::add);
|
stored.reference().map(DurableContentReference::contentId).ifPresent(contentIds::add);
|
||||||
}
|
}
|
||||||
return Set.copyOf(contentIds);
|
return Set.copyOf(contentIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* default */ List<StatusObject> snapshotStatusObjects() {
|
||||||
|
requireStoreUsable();
|
||||||
|
return listStoredStatuses().stream().map(StoredStatus::status).toList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void putCa(final CaRecord record) {
|
public void putCa(final CaRecord record) {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
@@ -596,16 +609,45 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
public void putStatusObject(final StatusObject object) {
|
public void putStatusObject(final StatusObject object) {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Objects.requireNonNull(object, "object");
|
Objects.requireNonNull(object, "object");
|
||||||
PkiId id = object.statusObjectId();
|
byte[] encoded = validateStatusObject(object);
|
||||||
writeOnce(this.paths.statusObjectPath(id), FsCodec.encode(FsCodec.STATUS_OBJECT, object), "STATUS_OBJECT",
|
DurableContentReference reference = object.content();
|
||||||
FsUtil.safeId(id));
|
try (FilesystemStagedContentStore.TransactionalReservation reservation =
|
||||||
|
stagedContent.reserveTransactionalPublication(reference)) {
|
||||||
|
validateStatusContent(reference);
|
||||||
|
MetadataCommitResult result = createStatusMetadata(object, encoded);
|
||||||
|
if (result.outcome() == MetadataCommitResult.Outcome.COMMITTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.outcome() == MetadataCommitResult.Outcome.UNKNOWN) {
|
||||||
|
reservation.preserveUntilRecovery();
|
||||||
|
durabilityUncertain.set(true);
|
||||||
|
throw new PkiException("Store durability unconfirmed: code=STORE_DURABILITY_UNCONFIRMED");
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Status object already exists");
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("Status object content is invalid", exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<StatusObject> getStatusObject(final PkiId statusObjectId) {
|
public Optional<StatusObject> getStatusObject(final PkiId statusObjectId) {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Objects.requireNonNull(statusObjectId, "statusObjectId");
|
Objects.requireNonNull(statusObjectId, "statusObjectId");
|
||||||
return readOptional(this.paths.statusObjectPath(statusObjectId), FsCodec.STATUS_OBJECT);
|
try (MetadataSnapshot snapshot = metadataStore.snapshot()) {
|
||||||
|
Optional<MetadataSnapshot.Record> record = snapshot.get(statusRecordKey(statusObjectId));
|
||||||
|
if (record.isEmpty()) {
|
||||||
|
Optional<MetadataSnapshot.Record> owner = snapshot.get(statusOwnerKey(statusObjectId));
|
||||||
|
if (owner.isPresent()) {
|
||||||
|
try (MetadataSnapshot.Record ignored = owner.orElseThrow()) {
|
||||||
|
throw new IllegalStateException("Status owner exists without its record");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(decodeStoredStatus(snapshot, record.orElseThrow()).status());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("Failed to read authoritative status object", exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -613,13 +655,9 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Objects.requireNonNull(issuerCaId, "issuerCaId");
|
Objects.requireNonNull(issuerCaId, "issuerCaId");
|
||||||
|
|
||||||
// Deterministic but coarse: scan all and filter by issuer id.
|
|
||||||
// This is acceptable for a reference implementation; indexes can be added
|
|
||||||
// later.
|
|
||||||
Path byId = this.paths.root().resolve("status").resolve("by-id");
|
|
||||||
List<StatusObject> all = listBinaryFiles(byId, FsCodec.STATUS_OBJECT);
|
|
||||||
List<StatusObject> out = new ArrayList<>();
|
List<StatusObject> out = new ArrayList<>();
|
||||||
for (StatusObject o : all) {
|
for (StoredStatus stored : listStoredStatuses()) {
|
||||||
|
StatusObject o = stored.status();
|
||||||
if (issuerCaId.equals(o.issuerCaId())) {
|
if (issuerCaId.equals(o.issuerCaId())) {
|
||||||
out.add(o);
|
out.add(o);
|
||||||
}
|
}
|
||||||
@@ -896,8 +934,8 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
stagedContent);
|
stagedContent);
|
||||||
DurableContentReference reference = continuation.content();
|
DurableContentReference reference = continuation.content();
|
||||||
MetadataCommitResult result;
|
MetadataCommitResult result;
|
||||||
try (FilesystemStagedContentStore.SigningReservation ignoredReservation =
|
try (FilesystemStagedContentStore.TransactionalReservation ignoredReservation =
|
||||||
stagedContent.reserveSigningPublication(reference);
|
stagedContent.reserveTransactionalPublication(reference);
|
||||||
RepeatableContent ignoredContent = stagedContent.openContent(reference)) {
|
RepeatableContent ignoredContent = stagedContent.openContent(reference)) {
|
||||||
stagedContent.restoreReference(reference.storeId(), reference.contentId(), reference.encoding(),
|
stagedContent.restoreReference(reference.storeId(), reference.contentId(), reference.encoding(),
|
||||||
reference.length(), reference.sha256(), reference.lifecycle());
|
reference.length(), reference.sha256(), reference.lifecycle());
|
||||||
@@ -975,6 +1013,128 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<StoredStatus> listStoredStatuses() {
|
||||||
|
List<StoredStatus> statuses = new ArrayList<>();
|
||||||
|
Set<String> recordIdentities = new HashSet<>();
|
||||||
|
try (MetadataSnapshot snapshot = metadataStore.snapshot()) {
|
||||||
|
try (MetadataCursor cursor = snapshot.scan(MetadataSnapshot.KeyRange.all(STATUS_RECORD_NAMESPACE),
|
||||||
|
CancellationSignal.NONE)) {
|
||||||
|
Optional<MetadataSnapshot.Record> next;
|
||||||
|
while ((next = cursor.next(CancellationSignal.NONE)).isPresent()) {
|
||||||
|
StoredStatus stored = decodeStoredStatus(snapshot, next.orElseThrow());
|
||||||
|
statuses.add(stored);
|
||||||
|
recordIdentities.add(stored.status().statusObjectId().value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (MetadataCursor cursor = snapshot.scan(MetadataSnapshot.KeyRange.all(STATUS_OWNER_NAMESPACE),
|
||||||
|
CancellationSignal.NONE)) {
|
||||||
|
Optional<MetadataSnapshot.Record> next;
|
||||||
|
while ((next = cursor.next(CancellationSignal.NONE)).isPresent()) {
|
||||||
|
try (MetadataSnapshot.Record owner = next.orElseThrow()) {
|
||||||
|
if (!recordIdentities.contains(owner.key().key())) {
|
||||||
|
throw new IllegalStateException("Status owner exists without its record");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(statuses);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("Failed to list authoritative status objects", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoredStatus decodeStoredStatus(MetadataSnapshot snapshot, MetadataSnapshot.Record storedRecord)
|
||||||
|
throws IOException {
|
||||||
|
try (storedRecord) {
|
||||||
|
StatusObject status = FsCodec.decode(FsCodec.STATUS_OBJECT, readMetadataValue(storedRecord),
|
||||||
|
stagedContent);
|
||||||
|
PkiId statusId = status.statusObjectId();
|
||||||
|
if (!statusRecordKey(statusId).equals(storedRecord.key())) {
|
||||||
|
throw new IOException("Status record metadata key mismatch");
|
||||||
|
}
|
||||||
|
validateStatusObject(status);
|
||||||
|
Optional<MetadataSnapshot.Record> storedOwner = snapshot.get(statusOwnerKey(statusId));
|
||||||
|
try (MetadataSnapshot.Record owner = storedOwner.orElseThrow(
|
||||||
|
() -> new IOException("Status record owner edge is missing"))) {
|
||||||
|
DurableContentReference reference = decodeStatusOwner(owner, statusId);
|
||||||
|
if (!reference.equals(status.content())) {
|
||||||
|
throw new IOException("Status record owner reference mismatch");
|
||||||
|
}
|
||||||
|
validateStatusContent(reference);
|
||||||
|
return new StoredStatus(status, storedRecord.recordRevision(), owner.recordRevision());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] validateStatusObject(StatusObject object) {
|
||||||
|
DurableContentReference reference = object.content();
|
||||||
|
if (!stagedContent.contentStoreId().equals(reference.storeId())
|
||||||
|
|| reference.lifecycle() != DurableContentReference.Lifecycle.PERSISTED) {
|
||||||
|
throw new IllegalArgumentException("Status content reference is not persistent store authority");
|
||||||
|
}
|
||||||
|
byte[] encoded = FsCodec.encode(FsCodec.STATUS_OBJECT, object);
|
||||||
|
StatusObject canonical = FsCodec.decode(FsCodec.STATUS_OBJECT, encoded, stagedContent);
|
||||||
|
if (!object.statusObjectId().equals(canonical.statusObjectId())
|
||||||
|
|| !Arrays.equals(encoded, FsCodec.encode(FsCodec.STATUS_OBJECT, canonical))) {
|
||||||
|
throw new IllegalArgumentException("Status object is not canonical");
|
||||||
|
}
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateStatusContent(DurableContentReference reference) throws IOException {
|
||||||
|
stagedContent.restoreReference(reference.storeId(), reference.contentId(), reference.encoding(),
|
||||||
|
reference.length(), reference.sha256(), reference.lifecycle());
|
||||||
|
try (RepeatableContent content = stagedContent.openContent(reference);
|
||||||
|
InputStream input = content.openStream()) {
|
||||||
|
byte[] buffer = new byte[METADATA_TRANSFER_BUFFER_BYTES];
|
||||||
|
while (true) {
|
||||||
|
int count = input.read(buffer);
|
||||||
|
if (count < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
throw new IOException("Status content validation made no progress");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataCommitResult createStatusMetadata(StatusObject status, byte[] encoded) throws IOException {
|
||||||
|
PkiId statusId = status.statusObjectId();
|
||||||
|
try (MetadataTransaction transaction = metadataStore.beginTransaction()) {
|
||||||
|
transaction.create(statusRecordKey(statusId), byteContent(encoded), CancellationSignal.NONE);
|
||||||
|
transaction.create(statusOwnerKey(statusId),
|
||||||
|
byteContent(encodeStatusOwner(statusId, status.content())), CancellationSignal.NONE);
|
||||||
|
if (STATUS_COMMIT_FAULT.get() == StatusCommitFaultPoint.BEFORE_COMMIT) {
|
||||||
|
throw new IOException("Injected status commit failure");
|
||||||
|
}
|
||||||
|
MetadataCommitResult result = transaction.commit();
|
||||||
|
if (result.outcome() == MetadataCommitResult.Outcome.COMMITTED
|
||||||
|
&& STATUS_COMMIT_FAULT.get() == StatusCommitFaultPoint.AFTER_COMMIT_AS_UNKNOWN) {
|
||||||
|
return new MetadataCommitResult(result.transactionId(), MetadataCommitResult.Outcome.UNKNOWN,
|
||||||
|
OptionalLong.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ static void installStatusCommitFault(StatusCommitFaultPoint point) {
|
||||||
|
STATUS_COMMIT_FAULT.set(Objects.requireNonNull(point, "point"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ static void clearStatusCommitFault() {
|
||||||
|
STATUS_COMMIT_FAULT.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only status-publication boundary; never part of production API. */
|
||||||
|
/* default */
|
||||||
|
enum StatusCommitFaultPoint {
|
||||||
|
/** Fails after admission but before the metadata commit attempt. */
|
||||||
|
BEFORE_COMMIT,
|
||||||
|
/** Reports uncertainty only after a real durable commit. */
|
||||||
|
AFTER_COMMIT_AS_UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<SignWorkflowStore.Record> tryClaimSign(PkiId submissionId, long expectedRevision, Duration lease) {
|
public Optional<SignWorkflowStore.Record> tryClaimSign(PkiId submissionId, long expectedRevision, Duration lease) {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
@@ -1693,6 +1853,14 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
return new MetadataKey(SIGN_OWNER_NAMESPACE, parsed.id().value());
|
return new MetadataKey(SIGN_OWNER_NAMESPACE, parsed.id().value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MetadataKey statusRecordKey(PkiId statusObjectId) {
|
||||||
|
return new MetadataKey(STATUS_RECORD_NAMESPACE, statusObjectId.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MetadataKey statusOwnerKey(PkiId statusObjectId) {
|
||||||
|
return new MetadataKey(STATUS_OWNER_NAMESPACE, statusObjectId.value());
|
||||||
|
}
|
||||||
|
|
||||||
private static RepeatableContent byteContent(byte[] value) {
|
private static RepeatableContent byteContent(byte[] value) {
|
||||||
return new ByteValueContent(value);
|
return new ByteValueContent(value);
|
||||||
}
|
}
|
||||||
@@ -1700,7 +1868,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
private static byte[] readMetadataValue(MetadataSnapshot.Record record) throws IOException {
|
private static byte[] readMetadataValue(MetadataSnapshot.Record record) throws IOException {
|
||||||
long length = record.length().orElseThrow();
|
long length = record.length().orElseThrow();
|
||||||
if (length < 0L || length > FsCodec.MAX_COMPONENT_BYTES) {
|
if (length < 0L || length > FsCodec.MAX_COMPONENT_BYTES) {
|
||||||
throw new IOException("Signing metadata value length is invalid");
|
throw new IOException("Metadata value length is invalid");
|
||||||
}
|
}
|
||||||
byte[] result = new byte[Math.toIntExact(length)];
|
byte[] result = new byte[Math.toIntExact(length)];
|
||||||
try (InputStream input = record.openStream()) {
|
try (InputStream input = record.openStream()) {
|
||||||
@@ -1708,21 +1876,33 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
while (offset < result.length) {
|
while (offset < result.length) {
|
||||||
int count = input.read(result, offset, result.length - offset);
|
int count = input.read(result, offset, result.length - offset);
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
throw new IOException("Signing metadata value is truncated");
|
throw new IOException("Metadata value is truncated");
|
||||||
}
|
}
|
||||||
offset += count;
|
offset += count;
|
||||||
}
|
}
|
||||||
if (input.read() >= 0) {
|
if (input.read() >= 0) {
|
||||||
throw new IOException("Signing metadata value has trailing data");
|
throw new IOException("Metadata value has trailing data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] encodeSigningOwner(PkiId submissionId, DurableContentReference reference) {
|
private static byte[] encodeSigningOwner(PkiId submissionId, DurableContentReference reference) {
|
||||||
|
return encodeTransactionalOwner(SIGN_OWNER_VALUE_VERSION, DurableContentOwner.Category.SIGNING_OPERATION,
|
||||||
|
submissionId, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encodeStatusOwner(PkiId statusObjectId, DurableContentReference reference) {
|
||||||
|
new DurableContentOwner(DurableContentOwner.Category.STATUS_OBJECT_RECORD, statusObjectId.value());
|
||||||
|
return encodeTransactionalOwner(STATUS_OWNER_VALUE_VERSION,
|
||||||
|
DurableContentOwner.Category.STATUS_OBJECT_RECORD, statusObjectId, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encodeTransactionalOwner(int version, DurableContentOwner.Category category,
|
||||||
|
PkiId ownerId, DurableContentReference reference) {
|
||||||
byte[][] fields = {
|
byte[][] fields = {
|
||||||
DurableContentOwner.Category.SIGNING_OPERATION.name().getBytes(StandardCharsets.US_ASCII),
|
category.name().getBytes(StandardCharsets.US_ASCII),
|
||||||
submissionId.value().getBytes(StandardCharsets.UTF_8),
|
ownerId.value().getBytes(StandardCharsets.UTF_8),
|
||||||
reference.storeId().getBytes(StandardCharsets.UTF_8),
|
reference.storeId().getBytes(StandardCharsets.UTF_8),
|
||||||
reference.contentId().getBytes(StandardCharsets.UTF_8),
|
reference.contentId().getBytes(StandardCharsets.UTF_8),
|
||||||
reference.encoding().name().getBytes(StandardCharsets.US_ASCII),
|
reference.encoding().name().getBytes(StandardCharsets.US_ASCII),
|
||||||
@@ -1734,7 +1914,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
size = Math.addExact(size, Math.addExact(Integer.BYTES, field.length));
|
size = Math.addExact(size, Math.addExact(Integer.BYTES, field.length));
|
||||||
}
|
}
|
||||||
ByteBuffer output = ByteBuffer.allocate(size);
|
ByteBuffer output = ByteBuffer.allocate(size);
|
||||||
output.putInt(SIGN_OWNER_VALUE_VERSION);
|
output.putInt(version);
|
||||||
for (byte[] field : fields) {
|
for (byte[] field : fields) {
|
||||||
output.putInt(field.length).put(field);
|
output.putInt(field.length).put(field);
|
||||||
}
|
}
|
||||||
@@ -1744,10 +1924,23 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
|
|
||||||
private DurableContentReference decodeSigningOwner(MetadataSnapshot.Record owner, PkiId expectedId)
|
private DurableContentReference decodeSigningOwner(MetadataSnapshot.Record owner, PkiId expectedId)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
return decodeTransactionalOwner(owner, expectedId, SIGN_OWNER_VALUE_VERSION,
|
||||||
|
DurableContentOwner.Category.SIGNING_OPERATION, signingOwnerKey(expectedId), "signing");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DurableContentReference decodeStatusOwner(MetadataSnapshot.Record owner, PkiId expectedId)
|
||||||
|
throws IOException {
|
||||||
|
return decodeTransactionalOwner(owner, expectedId, STATUS_OWNER_VALUE_VERSION,
|
||||||
|
DurableContentOwner.Category.STATUS_OBJECT_RECORD, statusOwnerKey(expectedId), "status");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DurableContentReference decodeTransactionalOwner(MetadataSnapshot.Record owner, PkiId expectedId,
|
||||||
|
int expectedVersion, DurableContentOwner.Category expectedCategory, MetadataKey expectedKey,
|
||||||
|
String authority) throws IOException {
|
||||||
ByteBuffer input = ByteBuffer.wrap(readMetadataValue(owner));
|
ByteBuffer input = ByteBuffer.wrap(readMetadataValue(owner));
|
||||||
try {
|
try {
|
||||||
if (input.getInt() != SIGN_OWNER_VALUE_VERSION) {
|
if (input.getInt() != expectedVersion) {
|
||||||
throw new IOException("Unsupported signing owner metadata");
|
throw new IOException("Unsupported " + authority + " owner metadata");
|
||||||
}
|
}
|
||||||
DurableContentOwner.Category category = DurableContentOwner.Category.valueOf(readOwnerField(input));
|
DurableContentOwner.Category category = DurableContentOwner.Category.valueOf(readOwnerField(input));
|
||||||
String ownerId = readOwnerField(input);
|
String ownerId = readOwnerField(input);
|
||||||
@@ -1758,18 +1951,19 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
DurableContentReference.Lifecycle lifecycle = DurableContentReference.Lifecycle.valueOf(
|
DurableContentReference.Lifecycle lifecycle = DurableContentReference.Lifecycle.valueOf(
|
||||||
readOwnerField(input));
|
readOwnerField(input));
|
||||||
long length = input.getLong();
|
long length = input.getLong();
|
||||||
if (input.hasRemaining() || category != DurableContentOwner.Category.SIGNING_OPERATION
|
if (input.hasRemaining() || category != expectedCategory
|
||||||
|| !ownerId.equals(expectedId.value())) {
|
|| !ownerId.equals(expectedId.value())) {
|
||||||
throw new IOException("Signing owner metadata identity mismatch");
|
throw new IOException("Transactional owner metadata identity mismatch");
|
||||||
}
|
}
|
||||||
|
new DurableContentOwner(category, ownerId);
|
||||||
DurableContentReference restored = stagedContent.restoreReference(storeId, contentId, encoding,
|
DurableContentReference restored = stagedContent.restoreReference(storeId, contentId, encoding,
|
||||||
length, digest, lifecycle);
|
length, digest, lifecycle);
|
||||||
if (!signingOwnerKey(expectedId).equals(owner.key())) {
|
if (!expectedKey.equals(owner.key())) {
|
||||||
throw new IOException("Signing owner metadata key mismatch");
|
throw new IOException("Transactional owner metadata key mismatch");
|
||||||
}
|
}
|
||||||
return restored;
|
return restored;
|
||||||
} catch (IllegalArgumentException | java.nio.BufferUnderflowException exception) {
|
} catch (IllegalArgumentException | java.nio.BufferUnderflowException exception) {
|
||||||
throw new IOException("Malformed signing owner metadata", exception);
|
throw new IOException("Malformed transactional owner metadata", exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1791,6 +1985,9 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
OptionalLong ownerRevision, Optional<DurableContentReference> reference) {
|
OptionalLong ownerRevision, Optional<DurableContentReference> reference) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record StoredStatus(StatusObject status, long recordRevision, long ownerRevision) {
|
||||||
|
}
|
||||||
|
|
||||||
/** Store-owned bounded finite control metadata used only for synchronous admission. */
|
/** Store-owned bounded finite control metadata used only for synchronous admission. */
|
||||||
private static final class ByteValueContent implements RepeatableContent {
|
private static final class ByteValueContent implements RepeatableContent {
|
||||||
private final byte[] value;
|
private final byte[] value;
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
private final Path root;
|
private final Path root;
|
||||||
private final String storeId;
|
private final String storeId;
|
||||||
private final ReentrantLock[] ownerLocks;
|
private final ReentrantLock[] ownerLocks;
|
||||||
private final ConcurrentMap<String, Integer> signingReservations;
|
private final ConcurrentMap<String, Integer> transactionalReservations;
|
||||||
private final AtomicReference<SigningOwnership> signingOwnership;
|
private final AtomicReference<TransactionalOwnership> transactionalOwnership;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a staged-content store.
|
* Creates a staged-content store.
|
||||||
@@ -118,8 +118,8 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
this.root = Objects.requireNonNull(root, "root").toAbsolutePath().normalize();
|
this.root = Objects.requireNonNull(root, "root").toAbsolutePath().normalize();
|
||||||
this.storeId = requireStoreIdentifier(storeId);
|
this.storeId = requireStoreIdentifier(storeId);
|
||||||
this.ownerLocks = new ReentrantLock[OWNER_LOCK_COUNT];
|
this.ownerLocks = new ReentrantLock[OWNER_LOCK_COUNT];
|
||||||
this.signingReservations = new ConcurrentHashMap<>();
|
this.transactionalReservations = new ConcurrentHashMap<>();
|
||||||
this.signingOwnership = new AtomicReference<>(reference -> Set.of());
|
this.transactionalOwnership = new AtomicReference<>(reference -> Set.of());
|
||||||
for (int index = 0; index < ownerLocks.length; index++) {
|
for (int index = 0; index < ownerLocks.length; index++) {
|
||||||
ownerLocks[index] = new ReentrantLock();
|
ownerLocks[index] = new ReentrantLock();
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
public boolean retainContent(DurableContentReference reference, DurableContentOwner owner) throws IOException {
|
public boolean retainContent(DurableContentReference reference, DurableContentOwner owner) throws IOException {
|
||||||
DurableContentReference exact = requireOwned(reference);
|
DurableContentReference exact = requireOwned(reference);
|
||||||
Objects.requireNonNull(owner, "owner");
|
Objects.requireNonNull(owner, "owner");
|
||||||
SigningOwnershipIo.requireSidecarOwner(owner);
|
TransactionalOwnershipIo.requireSidecarOwner(owner);
|
||||||
ReentrantLock lock = ownerLock(exact.contentId());
|
ReentrantLock lock = ownerLock(exact.contentId());
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
@@ -204,7 +204,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
public boolean releaseContent(DurableContentReference reference, DurableContentOwner owner) throws IOException {
|
public boolean releaseContent(DurableContentReference reference, DurableContentOwner owner) throws IOException {
|
||||||
DurableContentReference exact = requireOwned(reference);
|
DurableContentReference exact = requireOwned(reference);
|
||||||
Objects.requireNonNull(owner, "owner");
|
Objects.requireNonNull(owner, "owner");
|
||||||
SigningOwnershipIo.requireSidecarOwner(owner);
|
TransactionalOwnershipIo.requireSidecarOwner(owner);
|
||||||
ReentrantLock lock = ownerLock(exact.contentId());
|
ReentrantLock lock = ownerLock(exact.contentId());
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
@@ -217,7 +217,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
writeOwners(exact.contentId(), owners);
|
writeOwners(exact.contentId(), owners);
|
||||||
SigningOwnershipIo.retireIfUnowned(this, exact, owners);
|
TransactionalOwnershipIo.retireIfUnowned(this, exact, owners);
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -232,7 +232,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
try {
|
try {
|
||||||
requireExactMetadata(exact);
|
requireExactMetadata(exact);
|
||||||
Set<DurableContentOwner> owners = readOwners(exact.contentId());
|
Set<DurableContentOwner> owners = readOwners(exact.contentId());
|
||||||
owners.addAll(signingOwnership.get().findOwners(exact));
|
owners.addAll(transactionalOwnership.get().findOwners(exact));
|
||||||
return Set.copyOf(owners);
|
return Set.copyOf(owners);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -246,7 +246,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
requireExactMetadata(exact);
|
requireExactMetadata(exact);
|
||||||
SigningOwnershipIo.requireUnowned(this, exact);
|
TransactionalOwnershipIo.requireUnowned(this, exact);
|
||||||
retireFiles(exact);
|
retireFiles(exact);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -255,7 +255,15 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recoverContent(TemporaryUniqueIndex retained, TemporaryUniqueIndex retainedOwners) throws IOException {
|
public void recoverContent(TemporaryUniqueIndex retained, TemporaryUniqueIndex retainedOwners) throws IOException {
|
||||||
StoreIo.recoverContent(this, retained, retainedOwners);
|
try (TemporaryUniqueIndex transactionalOwners = beginUniqueIndex()) {
|
||||||
|
StoreIo.recoverContent(this, retained, retainedOwners, transactionalOwners);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recovery input separating transactional authority from general references and sidecars. */
|
||||||
|
/* package */ void recoverContent(TemporaryUniqueIndex retained, TemporaryUniqueIndex retainedOwners,
|
||||||
|
TemporaryUniqueIndex transactionalOwners) throws IOException {
|
||||||
|
StoreIo.recoverContent(this, retained, retainedOwners, transactionalOwners);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -316,47 +324,52 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void writeOwners(String contentId, Set<DurableContentOwner> owners) throws IOException {
|
private void writeOwners(String contentId, Set<DurableContentOwner> owners) throws IOException {
|
||||||
SigningOwnershipIo.requireSidecarOwners(owners);
|
TransactionalOwnershipIo.requireSidecarOwners(owners);
|
||||||
StoreIo.writeOwners(this, contentId, owners);
|
StoreIo.writeOwners(this, contentId, owners);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void retireSigningContent(DurableContentReference reference) throws IOException {
|
/* package */ void retireSigningContent(DurableContentReference reference) throws IOException {
|
||||||
SigningOwnershipIo.retireSigningContent(this, reference);
|
TransactionalOwnershipIo.retireTransactionalContent(this, reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void bindSigningOwnership(SigningOwnership ownership) {
|
/* package */ void bindTransactionalOwnership(TransactionalOwnership ownership) {
|
||||||
signingOwnership.set(Objects.requireNonNull(ownership, "ownership"));
|
transactionalOwnership.set(Objects.requireNonNull(ownership, "ownership"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ SigningReservation reserveSigningPublication(DurableContentReference reference)
|
/* package */ TransactionalReservation reserveTransactionalPublication(DurableContentReference reference)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return SigningOwnershipIo.reserveSigningPublication(this, reference);
|
return TransactionalOwnershipIo.reserveTransactionalPublication(this, reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal callback to the transactional signing-owner authority. */
|
/** Internal callback to signing and status transactional owner authority. */
|
||||||
/* default */
|
/* default */
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
interface SigningOwnership {
|
interface TransactionalOwnership {
|
||||||
/** Finds every exact signing owner for a durable content reference. */
|
/** Finds every exact transactional owner for a durable content reference. */
|
||||||
Set<DurableContentOwner> findOwners(DurableContentReference reference) throws IOException;
|
Set<DurableContentOwner> findOwners(DurableContentReference reference) throws IOException;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Short-lived reference-counted reservation spanning validation and commit. */
|
/** Short-lived reference-counted reservation spanning validation and commit. */
|
||||||
/* package */ final class SigningReservation implements AutoCloseable {
|
/* package */ final class TransactionalReservation implements AutoCloseable {
|
||||||
private final String contentId;
|
private final String contentId;
|
||||||
private final AtomicBoolean closed = new AtomicBoolean();
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean recoveryRequired = new AtomicBoolean();
|
||||||
|
|
||||||
private SigningReservation(String contentId) {
|
private TransactionalReservation(String contentId) {
|
||||||
this.contentId = contentId;
|
this.contentId = contentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package */ void preserveUntilRecovery() {
|
||||||
|
recoveryRequired.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (closed.compareAndSet(false, true)) {
|
if (!recoveryRequired.get() && closed.compareAndSet(false, true)) {
|
||||||
ReentrantLock lock = ownerLock(contentId);
|
ReentrantLock lock = ownerLock(contentId);
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
signingReservations.computeIfPresent(contentId,
|
transactionalReservations.computeIfPresent(contentId,
|
||||||
(ignored, count) -> count == 1 ? null : count - 1);
|
(ignored, count) -> count == 1 ? null : count - 1);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -688,11 +701,13 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
/** Mechanical home for filesystem metadata branches kept outside the store coordinator. */
|
/** Mechanical home for filesystem metadata branches kept outside the store coordinator. */
|
||||||
private static final class StoreIo {
|
private static final class StoreIo {
|
||||||
private static void recoverContent(FilesystemStagedContentStore store, TemporaryUniqueIndex retained,
|
private static void recoverContent(FilesystemStagedContentStore store, TemporaryUniqueIndex retained,
|
||||||
TemporaryUniqueIndex retainedOwners) throws IOException {
|
TemporaryUniqueIndex retainedOwners, TemporaryUniqueIndex transactionalOwners) throws IOException {
|
||||||
Objects.requireNonNull(retained, "retained");
|
Objects.requireNonNull(retained, "retained");
|
||||||
Objects.requireNonNull(retainedOwners, "retainedOwners");
|
Objects.requireNonNull(retainedOwners, "retainedOwners");
|
||||||
|
Objects.requireNonNull(transactionalOwners, "transactionalOwners");
|
||||||
retained.validateNamespace();
|
retained.validateNamespace();
|
||||||
retainedOwners.validateNamespace();
|
retainedOwners.validateNamespace();
|
||||||
|
transactionalOwners.validateNamespace();
|
||||||
try (java.util.stream.Stream<Path> paths = Files.list(store.root)) {
|
try (java.util.stream.Stream<Path> paths = Files.list(store.root)) {
|
||||||
java.util.Iterator<Path> iterator = paths
|
java.util.Iterator<Path> iterator = paths
|
||||||
.filter(path -> path.getFileName().toString().endsWith(".meta")).iterator();
|
.filter(path -> path.getFileName().toString().endsWith(".meta")).iterator();
|
||||||
@@ -700,7 +715,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
Path metadata = iterator.next();
|
Path metadata = iterator.next();
|
||||||
String name = metadata.getFileName().toString();
|
String name = metadata.getFileName().toString();
|
||||||
String contentId = name.substring(0, name.length() - ".meta".length());
|
String contentId = name.substring(0, name.length() - ".meta".length());
|
||||||
recoverContent(store, retained, retainedOwners, contentId);
|
recoverContent(store, retained, retainedOwners, transactionalOwners, contentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
store.cleanupOrphanedCompletedFiles();
|
store.cleanupOrphanedCompletedFiles();
|
||||||
@@ -708,7 +723,8 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void recoverContent(FilesystemStagedContentStore store, TemporaryUniqueIndex retained,
|
private static void recoverContent(FilesystemStagedContentStore store, TemporaryUniqueIndex retained,
|
||||||
TemporaryUniqueIndex retainedOwners, String contentId) throws IOException {
|
TemporaryUniqueIndex retainedOwners, TemporaryUniqueIndex transactionalOwners, String contentId)
|
||||||
|
throws IOException {
|
||||||
ReentrantLock lock = store.ownerLock(contentId);
|
ReentrantLock lock = store.ownerLock(contentId);
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
@@ -719,8 +735,10 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
store.writeOwners(contentId, owners);
|
store.writeOwners(contentId, owners);
|
||||||
}
|
}
|
||||||
boolean referenced = retained.contains(contentId.getBytes(StandardCharsets.US_ASCII));
|
boolean referenced = retained.contains(contentId.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
boolean transactionallyOwned = transactionalOwners.contains(
|
||||||
|
contentId.getBytes(StandardCharsets.US_ASCII));
|
||||||
boolean keep = reference.lifecycle() != DurableContentReference.Lifecycle.TEMPORARY
|
boolean keep = reference.lifecycle() != DurableContentReference.Lifecycle.TEMPORARY
|
||||||
&& (referenced || !owners.isEmpty());
|
&& (referenced || transactionallyOwned || !owners.isEmpty());
|
||||||
if (!keep || !isRegularFile(store.completePath(reference))) {
|
if (!keep || !isRegularFile(store.completePath(reference))) {
|
||||||
store.retireFiles(reference);
|
store.retireFiles(reference);
|
||||||
}
|
}
|
||||||
@@ -863,7 +881,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
}
|
}
|
||||||
for (int index = 0; index < count; index++) {
|
for (int index = 0; index < count; index++) {
|
||||||
DurableContentOwner owner = parseOwner(input.readUTF());
|
DurableContentOwner owner = parseOwner(input.readUTF());
|
||||||
if (owner.category() == DurableContentOwner.Category.SIGNING_OPERATION) {
|
if (TransactionalOwnershipIo.isTransactionalOwner(owner)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!owners.add(owner)) {
|
if (!owners.add(owner)) {
|
||||||
@@ -933,8 +951,8 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Isolates transactional signing-ownership lifecycle decisions. */
|
/** Isolates transactional staged-content ownership lifecycle decisions. */
|
||||||
private static final class SigningOwnershipIo {
|
private static final class TransactionalOwnershipIo {
|
||||||
private static void requireSidecarOwners(Set<DurableContentOwner> owners) {
|
private static void requireSidecarOwners(Set<DurableContentOwner> owners) {
|
||||||
for (DurableContentOwner owner : owners) {
|
for (DurableContentOwner owner : owners) {
|
||||||
requireSidecarOwner(owner);
|
requireSidecarOwner(owner);
|
||||||
@@ -942,37 +960,42 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void requireSidecarOwner(DurableContentOwner owner) {
|
private static void requireSidecarOwner(DurableContentOwner owner) {
|
||||||
if (owner.category() == DurableContentOwner.Category.SIGNING_OPERATION) {
|
if (isTransactionalOwner(owner)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Signing-operation ownership is authoritative transactional metadata");
|
"Transactional ownership is authoritative metadata");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SigningReservation reserveSigningPublication(FilesystemStagedContentStore store,
|
private static boolean isTransactionalOwner(DurableContentOwner owner) {
|
||||||
|
return owner.category() == DurableContentOwner.Category.SIGNING_OPERATION
|
||||||
|
|| owner.category() == DurableContentOwner.Category.STATUS_OBJECT_RECORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TransactionalReservation reserveTransactionalPublication(FilesystemStagedContentStore store,
|
||||||
DurableContentReference reference) throws IOException {
|
DurableContentReference reference) throws IOException {
|
||||||
DurableContentReference exact = store.requireOwned(reference);
|
DurableContentReference exact = store.requireOwned(reference);
|
||||||
ReentrantLock lock = store.ownerLock(exact.contentId());
|
ReentrantLock lock = store.ownerLock(exact.contentId());
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
store.requireExactMetadata(exact);
|
store.requireExactMetadata(exact);
|
||||||
store.signingReservations.merge(exact.contentId(), 1, Math::addExact);
|
store.transactionalReservations.merge(exact.contentId(), 1, Math::addExact);
|
||||||
return store.new SigningReservation(exact.contentId());
|
return store.new TransactionalReservation(exact.contentId());
|
||||||
} catch (ArithmeticException exception) {
|
} catch (ArithmeticException exception) {
|
||||||
throw new IOException("Signing content reservation limit exceeded", exception);
|
throw new IOException("Transactional content reservation limit exceeded", exception);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hasSigningAuthority(FilesystemStagedContentStore store,
|
private static boolean hasTransactionalAuthority(FilesystemStagedContentStore store,
|
||||||
DurableContentReference reference) throws IOException {
|
DurableContentReference reference) throws IOException {
|
||||||
return store.signingReservations.containsKey(reference.contentId())
|
return store.transactionalReservations.containsKey(reference.contentId())
|
||||||
|| !store.signingOwnership.get().findOwners(reference).isEmpty();
|
|| !store.transactionalOwnership.get().findOwners(reference).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void retireIfUnowned(FilesystemStagedContentStore store,
|
private static void retireIfUnowned(FilesystemStagedContentStore store,
|
||||||
DurableContentReference reference, Set<DurableContentOwner> owners) throws IOException {
|
DurableContentReference reference, Set<DurableContentOwner> owners) throws IOException {
|
||||||
if (owners.isEmpty() && !hasSigningAuthority(store, reference)) {
|
if (owners.isEmpty() && !hasTransactionalAuthority(store, reference)) {
|
||||||
store.retireFiles(reference);
|
store.retireFiles(reference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -980,12 +1003,12 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
private static void requireUnowned(FilesystemStagedContentStore store,
|
private static void requireUnowned(FilesystemStagedContentStore store,
|
||||||
DurableContentReference reference) throws IOException {
|
DurableContentReference reference) throws IOException {
|
||||||
if (!store.readOwners(reference.contentId()).isEmpty()
|
if (!store.readOwners(reference.contentId()).isEmpty()
|
||||||
|| hasSigningAuthority(store, reference)) {
|
|| hasTransactionalAuthority(store, reference)) {
|
||||||
throw new IOException("Staged content remains durably owned");
|
throw new IOException("Staged content remains durably owned");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void retireSigningContent(FilesystemStagedContentStore store,
|
private static void retireTransactionalContent(FilesystemStagedContentStore store,
|
||||||
DurableContentReference reference) throws IOException {
|
DurableContentReference reference) throws IOException {
|
||||||
DurableContentReference exact = store.requireOwned(reference);
|
DurableContentReference exact = store.requireOwned(reference);
|
||||||
ReentrantLock lock = store.ownerLock(exact.contentId());
|
ReentrantLock lock = store.ownerLock(exact.contentId());
|
||||||
@@ -993,7 +1016,7 @@ public final class FilesystemStagedContentStore implements StagedContentStore {
|
|||||||
try {
|
try {
|
||||||
store.requireExactMetadata(exact);
|
store.requireExactMetadata(exact);
|
||||||
if (store.readOwners(exact.contentId()).isEmpty()
|
if (store.readOwners(exact.contentId()).isEmpty()
|
||||||
&& !hasSigningAuthority(store, exact)) {
|
&& !hasTransactionalAuthority(store, exact)) {
|
||||||
store.retireFiles(exact);
|
store.retireFiles(exact);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -177,19 +177,6 @@ final class FsPaths {
|
|||||||
return this.root.resolve("revocation-snapshots");
|
return this.root.resolve("revocation-snapshots");
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Status objects (immutable .bin)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/* default */ Path statusObjectPath(final PkiId statusObjectId) {
|
|
||||||
Objects.requireNonNull(statusObjectId, "statusObjectId");
|
|
||||||
return this.root.resolve("status").resolve(BY_ID).resolve(FsUtil.safeId(statusObjectId) + ".bin");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* default */ Path statusRoot() {
|
|
||||||
return this.root.resolve("status").resolve(BY_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Policy traces (immutable .bin)
|
// Policy traces (immutable .bin)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import zeroecho.pki.api.PkiId;
|
|||||||
import zeroecho.pki.api.ca.CaRecord;
|
import zeroecho.pki.api.ca.CaRecord;
|
||||||
import zeroecho.pki.api.content.DurableContentReference;
|
import zeroecho.pki.api.content.DurableContentReference;
|
||||||
import zeroecho.pki.api.credential.Credential;
|
import zeroecho.pki.api.credential.Credential;
|
||||||
|
import zeroecho.pki.api.status.StatusObject;
|
||||||
import zeroecho.pki.impl.ProfileLifecycleFailure;
|
import zeroecho.pki.impl.ProfileLifecycleFailure;
|
||||||
import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
|
import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
|
||||||
import zeroecho.pki.spi.store.ContentSink;
|
import zeroecho.pki.spi.store.ContentSink;
|
||||||
@@ -134,7 +135,7 @@ final class FsSnapshotExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DurableContentReference transferCredentialContent(FilesystemPkiStore source,
|
private static DurableContentReference transferContent(FilesystemPkiStore source,
|
||||||
FilesystemPkiStore target, DurableContentReference sourceReference) throws IOException {
|
FilesystemPkiStore target, DurableContentReference sourceReference) throws IOException {
|
||||||
try (RepeatableContent content = source.stagedContent().openContent(sourceReference);
|
try (RepeatableContent content = source.stagedContent().openContent(sourceReference);
|
||||||
InputStream input = content.openStream();
|
InputStream input = content.openStream();
|
||||||
@@ -212,7 +213,12 @@ final class FsSnapshotExporter {
|
|||||||
private SnapshotAuthority plan(Instant at) throws IOException {
|
private SnapshotAuthority plan(Instant at) throws IOException {
|
||||||
CredentialInventory inventory = inventoryCredentials();
|
CredentialInventory inventory = inventoryCredentials();
|
||||||
List<CaRecord> cas = selectCas(at, inventory.credentials());
|
List<CaRecord> cas = selectCas(at, inventory.credentials());
|
||||||
return new SnapshotAuthority(cas, inventory.credentials(), inventory.contentIds());
|
List<StatusObject> statuses = source.snapshotStatusObjects();
|
||||||
|
Set<String> remintedContentIds = new HashSet<>(inventory.contentIds());
|
||||||
|
for (StatusObject status : statuses) {
|
||||||
|
remintedContentIds.add(status.content().contentId());
|
||||||
|
}
|
||||||
|
return new SnapshotAuthority(cas, inventory.credentials(), statuses, remintedContentIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CredentialInventory inventoryCredentials() throws IOException {
|
private CredentialInventory inventoryCredentials() throws IOException {
|
||||||
@@ -336,12 +342,11 @@ final class FsSnapshotExporter {
|
|||||||
copyFile(sourceRoot.resolve("SIGNING_NAMESPACE"), targetRoot.resolve("SIGNING_NAMESPACE"));
|
copyFile(sourceRoot.resolve("SIGNING_NAMESPACE"), targetRoot.resolve("SIGNING_NAMESPACE"));
|
||||||
copyFile(sourceRoot.resolve("SIGNING_TIME_WATERMARK"), targetRoot.resolve("SIGNING_TIME_WATERMARK"));
|
copyFile(sourceRoot.resolve("SIGNING_TIME_WATERMARK"), targetRoot.resolve("SIGNING_TIME_WATERMARK"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("requests"), targetRoot.resolve("requests"));
|
copyTreeIfExists(sourceRoot.resolve("requests"), targetRoot.resolve("requests"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("status"), targetRoot.resolve("status"));
|
|
||||||
copyTreeIfExists(sourceRoot.resolve("policy"), targetRoot.resolve("policy"));
|
copyTreeIfExists(sourceRoot.resolve("policy"), targetRoot.resolve("policy"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("publications"), targetRoot.resolve("publications"));
|
copyTreeIfExists(sourceRoot.resolve("publications"), targetRoot.resolve("publications"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("sign-workflows"), targetRoot.resolve("sign-workflows"));
|
copyTreeIfExists(sourceRoot.resolve("sign-workflows"), targetRoot.resolve("sign-workflows"));
|
||||||
copyStagedContent(sourceRoot.resolve("staged-content"), targetRoot.resolve("staged-content"),
|
copyStagedContent(sourceRoot.resolve("staged-content"), targetRoot.resolve("staged-content"),
|
||||||
plan.authority().sourceCredentialContentIds(), plan.nonCredentialContentIds());
|
plan.authority().remintedContentIds(), plan.nonCredentialContentIds());
|
||||||
copyTreeIfExists(sourceRoot.resolve("revocations"), targetRoot.resolve("revocations"));
|
copyTreeIfExists(sourceRoot.resolve("revocations"), targetRoot.resolve("revocations"));
|
||||||
copyImportedProfilesAsOf(plan.profiles(), targetRoot.resolve("profiles"));
|
copyImportedProfilesAsOf(plan.profiles(), targetRoot.resolve("profiles"));
|
||||||
reconstructMutableTree(sourceRoot.resolve("workflows"), targetRoot.resolve("workflows"), at,
|
reconstructMutableTree(sourceRoot.resolve("workflows"), targetRoot.resolve("workflows"), at,
|
||||||
@@ -384,6 +389,9 @@ final class FsSnapshotExporter {
|
|||||||
persistCa(target, ca);
|
persistCa(target, ca);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (StatusObject status : authority.statuses()) {
|
||||||
|
persistStatus(target, status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +428,12 @@ final class FsSnapshotExporter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void persistStatus(FilesystemPkiStore target, StatusObject sourceStatus) throws IOException {
|
||||||
|
try (StatusTransfer transfer = new StatusTransfer(source, target, sourceStatus)) {
|
||||||
|
transfer.persist();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Owns a completed target reference until its credential is persisted. */
|
/** Owns a completed target reference until its credential is persisted. */
|
||||||
@@ -433,7 +447,7 @@ final class FsSnapshotExporter {
|
|||||||
Credential sourceCredential) throws IOException {
|
Credential sourceCredential) throws IOException {
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.sourceCredential = sourceCredential;
|
this.sourceCredential = sourceCredential;
|
||||||
this.targetReference = transferCredentialContent(source, target, sourceCredential.content());
|
this.targetReference = transferContent(source, target, sourceCredential.content());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persist() {
|
private void persist() {
|
||||||
@@ -449,6 +463,35 @@ final class FsSnapshotExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Owns reminted target content until its status authority is committed. */
|
||||||
|
private static final class StatusTransfer implements AutoCloseable {
|
||||||
|
private final FilesystemPkiStore target;
|
||||||
|
private final StatusObject sourceStatus;
|
||||||
|
private final DurableContentReference targetReference;
|
||||||
|
private boolean persisted;
|
||||||
|
|
||||||
|
private StatusTransfer(FilesystemPkiStore source, FilesystemPkiStore target, StatusObject sourceStatus)
|
||||||
|
throws IOException {
|
||||||
|
this.target = target;
|
||||||
|
this.sourceStatus = sourceStatus;
|
||||||
|
this.targetReference = transferContent(source, target, sourceStatus.content());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persist() {
|
||||||
|
target.putStatusObject(new StatusObject(sourceStatus.statusObjectId(), sourceStatus.formatId(),
|
||||||
|
sourceStatus.issuerCaId(), sourceStatus.type(), sourceStatus.thisUpdate(),
|
||||||
|
sourceStatus.nextUpdate(), targetReference, sourceStatus.attributes()));
|
||||||
|
persisted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (!persisted) {
|
||||||
|
target.stagedContent().retireUnownedContent(targetReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void deleteOwnedTree(Path root) throws IOException {
|
private static void deleteOwnedTree(Path root) throws IOException {
|
||||||
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
return;
|
return;
|
||||||
@@ -683,11 +726,12 @@ final class FsSnapshotExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private record SnapshotAuthority(List<CaRecord> cas, Map<PkiId, Credential> credentials,
|
private record SnapshotAuthority(List<CaRecord> cas, Map<PkiId, Credential> credentials,
|
||||||
Set<String> sourceCredentialContentIds) {
|
List<StatusObject> statuses, Set<String> remintedContentIds) {
|
||||||
private SnapshotAuthority {
|
private SnapshotAuthority {
|
||||||
cas = List.copyOf(cas);
|
cas = List.copyOf(cas);
|
||||||
credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
|
credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
|
||||||
sourceCredentialContentIds = Set.copyOf(sourceCredentialContentIds);
|
statuses = List.copyOf(statuses);
|
||||||
|
remintedContentIds = Set.copyOf(remintedContentIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@@ -49,6 +54,7 @@ import java.time.Instant;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalLong;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -56,12 +62,16 @@ import java.util.stream.Collectors;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import zeroecho.core.io.CancellationSignal;
|
||||||
|
import zeroecho.core.io.RepeatableContent;
|
||||||
import zeroecho.pki.api.EncodedObject;
|
import zeroecho.pki.api.EncodedObject;
|
||||||
import zeroecho.pki.api.content.DurableContentReference;
|
import zeroecho.pki.api.content.DurableContentReference;
|
||||||
|
import zeroecho.pki.api.content.DurableContentOwner;
|
||||||
import zeroecho.pki.api.Encoding;
|
import zeroecho.pki.api.Encoding;
|
||||||
import zeroecho.pki.api.FormatId;
|
import zeroecho.pki.api.FormatId;
|
||||||
import zeroecho.pki.api.IssuerRef;
|
import zeroecho.pki.api.IssuerRef;
|
||||||
import zeroecho.pki.api.KeyRef;
|
import zeroecho.pki.api.KeyRef;
|
||||||
|
import zeroecho.pki.api.PkiException;
|
||||||
import zeroecho.pki.api.PkiId;
|
import zeroecho.pki.api.PkiId;
|
||||||
import zeroecho.pki.api.SubjectRef;
|
import zeroecho.pki.api.SubjectRef;
|
||||||
import zeroecho.pki.api.Validity;
|
import zeroecho.pki.api.Validity;
|
||||||
@@ -102,6 +112,10 @@ import zeroecho.pki.api.revocation.RevocationJournal;
|
|||||||
import zeroecho.pki.api.revocation.RevocationReason;
|
import zeroecho.pki.api.revocation.RevocationReason;
|
||||||
import zeroecho.pki.api.status.StatusObject;
|
import zeroecho.pki.api.status.StatusObject;
|
||||||
import zeroecho.pki.api.status.StatusObjectType;
|
import zeroecho.pki.api.status.StatusObjectType;
|
||||||
|
import zeroecho.pki.spi.store.MetadataCommitResult;
|
||||||
|
import zeroecho.pki.spi.store.MetadataKey;
|
||||||
|
import zeroecho.pki.spi.store.MetadataSnapshot;
|
||||||
|
import zeroecho.pki.spi.store.MetadataTransaction;
|
||||||
import zeroecho.pki.spi.store.PkiStore;
|
import zeroecho.pki.spi.store.PkiStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,6 +200,317 @@ public final class FilesystemPkiStoreTest {
|
|||||||
System.out.println("...ok");
|
System.out.println("...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void statusAuthorityIsAtomicDurableWriteOnceAndProtectsContent() throws Exception {
|
||||||
|
System.out.println("statusAuthorityIsAtomicDurableWriteOnceAndProtectsContent");
|
||||||
|
Path root = tmp.resolve("store-status-authority");
|
||||||
|
PkiId firstId = new PkiId("crl:status-authority-one");
|
||||||
|
PkiId secondId = new PkiId("crl:status-authority-two");
|
||||||
|
DurableContentReference firstReference;
|
||||||
|
StatusObject firstStatus;
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(root, FsPkiStoreOptions.defaults())) {
|
||||||
|
firstReference = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(store.stagedContent(), Encoding.DER,
|
||||||
|
new byte[] { 1, 2, 3 });
|
||||||
|
firstStatus = new StatusObject(firstId, new FormatId("fmt-x509"), new PkiId("ca-status"),
|
||||||
|
StatusObjectType.CRL, Instant.EPOCH, Optional.empty(), firstReference,
|
||||||
|
TestObjects.emptyAttributes());
|
||||||
|
store.putStatusObject(firstStatus);
|
||||||
|
assertThrows(IllegalStateException.class, () -> store.putStatusObject(firstStatus));
|
||||||
|
assertThrows(IOException.class, () -> store.stagedContent().retireUnownedContent(firstReference));
|
||||||
|
|
||||||
|
DurableContentReference conflictingReference = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(
|
||||||
|
store.stagedContent(), Encoding.DER, new byte[] { 7, 8, 9 });
|
||||||
|
StatusObject conflicting = new StatusObject(firstId, new FormatId("fmt-x509"),
|
||||||
|
new PkiId("ca-status"), StatusObjectType.CRL, Instant.EPOCH.plusSeconds(2L), Optional.empty(),
|
||||||
|
conflictingReference, TestObjects.emptyAttributes());
|
||||||
|
assertThrows(IllegalStateException.class, () -> store.putStatusObject(conflicting));
|
||||||
|
assertEquals(firstReference, store.getStatusObject(firstId).orElseThrow().content());
|
||||||
|
store.stagedContent().retireUnownedContent(conflictingReference);
|
||||||
|
|
||||||
|
DurableContentReference secondReference = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(
|
||||||
|
store.stagedContent(), Encoding.DER, new byte[] { 4, 5, 6 });
|
||||||
|
store.putStatusObject(new StatusObject(secondId, new FormatId("fmt-x509"), new PkiId("ca-status"),
|
||||||
|
StatusObjectType.CRL, Instant.EPOCH.plusSeconds(1L), Optional.empty(), secondReference,
|
||||||
|
TestObjects.emptyAttributes()));
|
||||||
|
assertEquals(2, store.listStatusObjects(new PkiId("ca-status")).size());
|
||||||
|
assertTrue(store.listPublicationRecords().isEmpty());
|
||||||
|
assertFalse(Files.exists(root.resolve("staged-content").resolve(firstReference.contentId() + ".owners")));
|
||||||
|
}
|
||||||
|
|
||||||
|
PkiId legacyId = new PkiId("crl:legacy-distinct");
|
||||||
|
StatusObject legacy = new StatusObject(legacyId, firstStatus.formatId(), firstStatus.issuerCaId(),
|
||||||
|
firstStatus.type(), firstStatus.thisUpdate(), firstStatus.nextUpdate(), firstReference,
|
||||||
|
firstStatus.attributes());
|
||||||
|
Path obsolete = root.resolve("status").resolve("by-id").resolve(FsUtil.safeId(legacyId) + ".bin");
|
||||||
|
Files.createDirectories(obsolete.getParent());
|
||||||
|
Files.write(obsolete, FsCodec.encode(FsCodec.STATUS_OBJECT, legacy));
|
||||||
|
try (FilesystemPkiStore reopened = new FilesystemPkiStore(root, FsPkiStoreOptions.defaults())) {
|
||||||
|
assertEquals(firstId, reopened.getStatusObject(firstId).orElseThrow().statusObjectId());
|
||||||
|
assertEquals(secondId, reopened.getStatusObject(secondId).orElseThrow().statusObjectId());
|
||||||
|
assertTrue(reopened.getStatusObject(legacyId).isEmpty());
|
||||||
|
assertEquals(2, reopened.listStatusObjects(new PkiId("ca-status")).size());
|
||||||
|
assertThrows(IOException.class, () -> reopened.stagedContent().retireUnownedContent(firstReference));
|
||||||
|
}
|
||||||
|
System.out.println("...statusRecords=2");
|
||||||
|
System.out.println("statusAuthorityIsAtomicDurableWriteOnceAndProtectsContent...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void statusRecoveryRejectsRecordOwnerAsymmetry() throws Exception {
|
||||||
|
System.out.println("statusRecoveryRejectsRecordOwnerAsymmetry");
|
||||||
|
Path missingOwnerRoot = tmp.resolve("store-status-missing-owner");
|
||||||
|
PkiId missingOwnerId = persistFixtureStatus(missingOwnerRoot, "missing-owner").statusObjectId();
|
||||||
|
deleteMetadataRecord(missingOwnerRoot,
|
||||||
|
new MetadataKey("io.zeroecho.pki.status-object-owner", missingOwnerId.value()));
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> new FilesystemPkiStore(missingOwnerRoot, FsPkiStoreOptions.defaults()));
|
||||||
|
|
||||||
|
Path orphanOwnerRoot = tmp.resolve("store-status-orphan-owner");
|
||||||
|
PkiId orphanOwnerId = persistFixtureStatus(orphanOwnerRoot, "orphan-owner").statusObjectId();
|
||||||
|
deleteMetadataRecord(orphanOwnerRoot,
|
||||||
|
new MetadataKey("io.zeroecho.pki.status-object-record", orphanOwnerId.value()));
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> new FilesystemPkiStore(orphanOwnerRoot, FsPkiStoreOptions.defaults()));
|
||||||
|
System.out.println("...rejected=missing-owner,orphan-owner");
|
||||||
|
System.out.println("statusRecoveryRejectsRecordOwnerAsymmetry...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void statusRecoveryRejectsMalformedOrMismatchedMetadata() throws Exception {
|
||||||
|
System.out.println("statusRecoveryRejectsMalformedOrMismatchedMetadata");
|
||||||
|
Path wrongIdentityRoot = tmp.resolve("store-status-wrong-owner-id");
|
||||||
|
StatusObject wrongIdentity = persistFixtureStatus(wrongIdentityRoot, "wrong-owner-id");
|
||||||
|
replaceMetadataRecord(wrongIdentityRoot,
|
||||||
|
new MetadataKey("io.zeroecho.pki.status-object-owner", wrongIdentity.statusObjectId().value()),
|
||||||
|
encodeStatusOwner("crl:different", wrongIdentity.content()));
|
||||||
|
assertStoreOpenFails(wrongIdentityRoot);
|
||||||
|
|
||||||
|
Path wrongReferenceRoot = tmp.resolve("store-status-wrong-owner-reference");
|
||||||
|
StatusObject wrongReference;
|
||||||
|
DurableContentReference alternate;
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(wrongReferenceRoot, FsPkiStoreOptions.defaults())) {
|
||||||
|
DurableContentReference original = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(
|
||||||
|
store.stagedContent(), Encoding.DER, new byte[] { 1 });
|
||||||
|
wrongReference = new StatusObject(new PkiId("crl:wrong-owner-reference"), new FormatId("fmt-x509"),
|
||||||
|
new PkiId("ca-status"), StatusObjectType.CRL, Instant.EPOCH, Optional.empty(), original,
|
||||||
|
TestObjects.emptyAttributes());
|
||||||
|
store.putStatusObject(wrongReference);
|
||||||
|
alternate = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(store.stagedContent(), Encoding.DER,
|
||||||
|
new byte[] { 2 });
|
||||||
|
}
|
||||||
|
replaceMetadataRecord(wrongReferenceRoot,
|
||||||
|
new MetadataKey("io.zeroecho.pki.status-object-owner", wrongReference.statusObjectId().value()),
|
||||||
|
encodeStatusOwner(wrongReference.statusObjectId().value(), alternate));
|
||||||
|
assertStoreOpenFails(wrongReferenceRoot);
|
||||||
|
|
||||||
|
Path malformedRecordRoot = tmp.resolve("store-status-malformed-record");
|
||||||
|
StatusObject malformedRecord = persistFixtureStatus(malformedRecordRoot, "malformed-record");
|
||||||
|
replaceMetadataRecord(malformedRecordRoot,
|
||||||
|
new MetadataKey("io.zeroecho.pki.status-object-record", malformedRecord.statusObjectId().value()),
|
||||||
|
new byte[] { 1, 2, 3 });
|
||||||
|
assertStoreOpenFails(malformedRecordRoot);
|
||||||
|
|
||||||
|
Path malformedOwnerRoot = tmp.resolve("store-status-malformed-owner");
|
||||||
|
StatusObject malformedOwner = persistFixtureStatus(malformedOwnerRoot, "malformed-owner");
|
||||||
|
replaceMetadataRecord(malformedOwnerRoot,
|
||||||
|
new MetadataKey("io.zeroecho.pki.status-object-owner", malformedOwner.statusObjectId().value()),
|
||||||
|
new byte[] { 4, 5, 6 });
|
||||||
|
assertStoreOpenFails(malformedOwnerRoot);
|
||||||
|
System.out.println("...rejected=identity,reference,record,owner");
|
||||||
|
System.out.println("statusRecoveryRejectsMalformedOrMismatchedMetadata...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void statusRecoveryRejectsMissingOrCorruptContentAndSidecarRepair() throws Exception {
|
||||||
|
System.out.println("statusRecoveryRejectsMissingOrCorruptContentAndSidecarRepair");
|
||||||
|
Path missingMetadataRoot = tmp.resolve("store-status-missing-meta");
|
||||||
|
StatusObject missingMetadata = persistFixtureStatus(missingMetadataRoot, "missing-meta");
|
||||||
|
Files.delete(missingMetadataRoot.resolve("staged-content")
|
||||||
|
.resolve(missingMetadata.content().contentId() + ".meta"));
|
||||||
|
assertStoreOpenFails(missingMetadataRoot);
|
||||||
|
|
||||||
|
Path missingContentRoot = tmp.resolve("store-status-missing-content");
|
||||||
|
StatusObject missingContent = persistFixtureStatus(missingContentRoot, "missing-content");
|
||||||
|
Files.delete(missingContentRoot.resolve("staged-content")
|
||||||
|
.resolve(missingContent.content().contentId() + ".content"));
|
||||||
|
assertStoreOpenFails(missingContentRoot);
|
||||||
|
|
||||||
|
Path corruptContentRoot = tmp.resolve("store-status-corrupt-content");
|
||||||
|
StatusObject corruptContent = persistFixtureStatus(corruptContentRoot, "corrupt-content");
|
||||||
|
Files.write(corruptContentRoot.resolve("staged-content")
|
||||||
|
.resolve(corruptContent.content().contentId() + ".content"),
|
||||||
|
new byte[Math.toIntExact(corruptContent.content().length())]);
|
||||||
|
assertStoreOpenFails(corruptContentRoot);
|
||||||
|
|
||||||
|
Path sidecarRoot = tmp.resolve("store-status-sidecar-repair");
|
||||||
|
StatusObject sidecar = persistFixtureStatus(sidecarRoot, "sidecar-repair");
|
||||||
|
deleteMetadataRecord(sidecarRoot,
|
||||||
|
new MetadataKey("io.zeroecho.pki.status-object-owner", sidecar.statusObjectId().value()));
|
||||||
|
writeStatusSidecar(sidecarRoot, sidecar);
|
||||||
|
assertStoreOpenFails(sidecarRoot);
|
||||||
|
System.out.println("...rejected=missing-meta,missing-content,corrupt-content,sidecar-repair");
|
||||||
|
System.out.println("statusRecoveryRejectsMissingOrCorruptContentAndSidecarRepair...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void statusCommitFaultsPreserveAtomicityAndUnknownRecovery() throws Exception {
|
||||||
|
System.out.println("statusCommitFaultsPreserveAtomicityAndUnknownRecovery");
|
||||||
|
Path beforeCommitRoot = tmp.resolve("store-status-before-commit-failure");
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(beforeCommitRoot, FsPkiStoreOptions.defaults())) {
|
||||||
|
DurableContentReference reference = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(
|
||||||
|
store.stagedContent(), Encoding.DER, new byte[] { 3, 4 });
|
||||||
|
StatusObject status = new StatusObject(new PkiId("crl:before-commit"), new FormatId("fmt-x509"),
|
||||||
|
new PkiId("ca-status"), StatusObjectType.CRL, Instant.EPOCH, Optional.empty(), reference,
|
||||||
|
TestObjects.emptyAttributes());
|
||||||
|
FilesystemPkiStore.installStatusCommitFault(
|
||||||
|
FilesystemPkiStore.StatusCommitFaultPoint.BEFORE_COMMIT);
|
||||||
|
try {
|
||||||
|
assertThrows(IllegalStateException.class, () -> store.putStatusObject(status));
|
||||||
|
} finally {
|
||||||
|
FilesystemPkiStore.clearStatusCommitFault();
|
||||||
|
}
|
||||||
|
assertTrue(store.getStatusObject(status.statusObjectId()).isEmpty());
|
||||||
|
store.stagedContent().retireUnownedContent(reference);
|
||||||
|
assertThrows(IOException.class, () -> store.stagedContent().openContent(reference));
|
||||||
|
}
|
||||||
|
|
||||||
|
Path unknownRoot = tmp.resolve("store-status-after-commit-unknown");
|
||||||
|
PkiId unknownId = new PkiId("crl:after-commit-unknown");
|
||||||
|
DurableContentReference committedReference;
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(unknownRoot, FsPkiStoreOptions.defaults())) {
|
||||||
|
committedReference = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(store.stagedContent(),
|
||||||
|
Encoding.DER, new byte[] { 5, 6 });
|
||||||
|
StatusObject status = new StatusObject(unknownId, new FormatId("fmt-x509"),
|
||||||
|
new PkiId("ca-status"), StatusObjectType.CRL, Instant.EPOCH, Optional.empty(),
|
||||||
|
committedReference, TestObjects.emptyAttributes());
|
||||||
|
FilesystemPkiStore.installStatusCommitFault(
|
||||||
|
FilesystemPkiStore.StatusCommitFaultPoint.AFTER_COMMIT_AS_UNKNOWN);
|
||||||
|
try {
|
||||||
|
PkiException unknown = assertThrows(PkiException.class, () -> store.putStatusObject(status));
|
||||||
|
assertTrue(unknown.getMessage().contains("STORE_DURABILITY_UNCONFIRMED"));
|
||||||
|
} finally {
|
||||||
|
FilesystemPkiStore.clearStatusCommitFault();
|
||||||
|
}
|
||||||
|
assertThrows(PkiException.class, () -> store.getStatusObject(unknownId));
|
||||||
|
assertThrows(IOException.class, () -> store.stagedContent().retireUnownedContent(committedReference));
|
||||||
|
} finally {
|
||||||
|
FilesystemPkiStore.clearStatusCommitFault();
|
||||||
|
}
|
||||||
|
try (FilesystemPkiStore reopened = new FilesystemPkiStore(unknownRoot, FsPkiStoreOptions.defaults())) {
|
||||||
|
assertEquals(committedReference,
|
||||||
|
reopened.getStatusObject(unknownId).orElseThrow().content());
|
||||||
|
}
|
||||||
|
System.out.println("...faults=before-commit,after-commit-unknown");
|
||||||
|
System.out.println("statusCommitFaultsPreserveAtomicityAndUnknownRecovery...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
private StatusObject persistFixtureStatus(Path root, String suffix) throws Exception {
|
||||||
|
PkiId statusId = new PkiId("crl:" + suffix);
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(root, FsPkiStoreOptions.defaults())) {
|
||||||
|
DurableContentReference reference = zeroecho.pki.testkit.PkiTestRuntime.fixtureReference(
|
||||||
|
store.stagedContent(), Encoding.DER, suffix.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
StatusObject status = new StatusObject(statusId, new FormatId("fmt-x509"), new PkiId("ca-status"),
|
||||||
|
StatusObjectType.CRL, Instant.EPOCH, Optional.empty(), reference, TestObjects.emptyAttributes());
|
||||||
|
store.putStatusObject(status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteMetadataRecord(Path root, MetadataKey key) throws Exception {
|
||||||
|
FsPaths paths = new FsPaths(root);
|
||||||
|
try (PosixTransactionalMetadataStore metadata = PosixTransactionalMetadataStore.open(
|
||||||
|
paths.transactionalMetadataLog())) {
|
||||||
|
long revision;
|
||||||
|
try (MetadataSnapshot snapshot = metadata.snapshot();
|
||||||
|
MetadataSnapshot.Record record = snapshot.get(key).orElseThrow()) {
|
||||||
|
revision = record.recordRevision();
|
||||||
|
}
|
||||||
|
try (MetadataTransaction transaction = metadata.beginTransaction()) {
|
||||||
|
transaction.delete(key, revision);
|
||||||
|
assertEquals(MetadataCommitResult.Outcome.COMMITTED, transaction.commit().outcome());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void replaceMetadataRecord(Path root, MetadataKey key, byte[] value) throws Exception {
|
||||||
|
FsPaths paths = new FsPaths(root);
|
||||||
|
try (PosixTransactionalMetadataStore metadata = PosixTransactionalMetadataStore.open(
|
||||||
|
paths.transactionalMetadataLog())) {
|
||||||
|
long revision;
|
||||||
|
try (MetadataSnapshot snapshot = metadata.snapshot();
|
||||||
|
MetadataSnapshot.Record record = snapshot.get(key).orElseThrow()) {
|
||||||
|
revision = record.recordRevision();
|
||||||
|
}
|
||||||
|
try (MetadataTransaction transaction = metadata.beginTransaction()) {
|
||||||
|
transaction.replace(key, revision, byteContent(value), CancellationSignal.NONE);
|
||||||
|
assertEquals(MetadataCommitResult.Outcome.COMMITTED, transaction.commit().outcome());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RepeatableContent byteContent(byte[] value) {
|
||||||
|
byte[] immutable = value.clone();
|
||||||
|
return new RepeatableContent() {
|
||||||
|
@Override
|
||||||
|
public InputStream openStream() {
|
||||||
|
return new ByteArrayInputStream(immutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OptionalLong length() {
|
||||||
|
return OptionalLong.of(immutable.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String contentId() {
|
||||||
|
return "test-status-metadata";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Immutable in-memory test content owns no external resource.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encodeStatusOwner(String ownerId, DurableContentReference reference) {
|
||||||
|
byte[][] fields = {
|
||||||
|
DurableContentOwner.Category.STATUS_OBJECT_RECORD.name().getBytes(StandardCharsets.US_ASCII),
|
||||||
|
ownerId.getBytes(StandardCharsets.UTF_8), reference.storeId().getBytes(StandardCharsets.UTF_8),
|
||||||
|
reference.contentId().getBytes(StandardCharsets.UTF_8),
|
||||||
|
reference.encoding().name().getBytes(StandardCharsets.US_ASCII),
|
||||||
|
reference.sha256().getBytes(StandardCharsets.US_ASCII),
|
||||||
|
reference.lifecycle().name().getBytes(StandardCharsets.US_ASCII)
|
||||||
|
};
|
||||||
|
int size = Integer.BYTES + Long.BYTES;
|
||||||
|
for (byte[] field : fields) {
|
||||||
|
size = Math.addExact(size, Math.addExact(Integer.BYTES, field.length));
|
||||||
|
}
|
||||||
|
ByteBuffer output = ByteBuffer.allocate(size);
|
||||||
|
output.putInt(1);
|
||||||
|
for (byte[] field : fields) {
|
||||||
|
output.putInt(field.length).put(field);
|
||||||
|
}
|
||||||
|
output.putLong(reference.length());
|
||||||
|
return output.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeStatusSidecar(Path root, StatusObject status) throws IOException {
|
||||||
|
Path owners = root.resolve("staged-content").resolve(status.content().contentId() + ".owners");
|
||||||
|
DurableContentOwner owner = new DurableContentOwner(DurableContentOwner.Category.STATUS_OBJECT_RECORD,
|
||||||
|
status.statusObjectId().value());
|
||||||
|
try (DataOutputStream output = new DataOutputStream(Files.newOutputStream(owners))) {
|
||||||
|
output.writeByte(2);
|
||||||
|
output.writeUTF(status.content().contentId());
|
||||||
|
output.writeInt(1);
|
||||||
|
output.writeUTF(owner.canonicalForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertStoreOpenFails(Path root) {
|
||||||
|
assertThrows(IllegalStateException.class, () -> new FilesystemPkiStore(root, FsPkiStoreOptions.defaults()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void writeOnceCredentialRejected() throws Exception {
|
void writeOnceCredentialRejected() throws Exception {
|
||||||
System.out.println("writeOnceCredentialRejected");
|
System.out.println("writeOnceCredentialRejected");
|
||||||
@@ -365,12 +690,13 @@ public final class FilesystemPkiStoreTest {
|
|||||||
Credential credential = restored.getCredential(credentialId).orElseThrow();
|
Credential credential = restored.getCredential(credentialId).orElseThrow();
|
||||||
StatusObject status = restored.getStatusObject(statusId).orElseThrow();
|
StatusObject status = restored.getStatusObject(statusId).orElseThrow();
|
||||||
assertFalse(sourceContentId.equals(credential.content().contentId()));
|
assertFalse(sourceContentId.equals(credential.content().contentId()));
|
||||||
assertEquals(sourceContentId, status.content().contentId());
|
assertFalse(sourceContentId.equals(status.content().contentId()));
|
||||||
|
assertFalse(credential.content().contentId().equals(status.content().contentId()));
|
||||||
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, credential.content()));
|
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, credential.content()));
|
||||||
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, status.content()));
|
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, status.content()));
|
||||||
}
|
}
|
||||||
assertTrue(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".content")));
|
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".content")));
|
||||||
assertTrue(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".meta")));
|
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".meta")));
|
||||||
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".owners")));
|
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".owners")));
|
||||||
System.out.println("snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner...ok");
|
System.out.println("snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner...ok");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,13 +146,16 @@ final class FilesystemStagedContentStoreTest {
|
|||||||
FilesystemStagedContentStore store = new FilesystemStagedContentStore(directory, STORE_ID);
|
FilesystemStagedContentStore store = new FilesystemStagedContentStore(directory, STORE_ID);
|
||||||
DurableContentReference reference = stage(store, DurableContentReference.Lifecycle.OPERATION, "sign-input");
|
DurableContentReference reference = stage(store, DurableContentReference.Lifecycle.OPERATION, "sign-input");
|
||||||
DurableContentOwner authority = signingOwner("authority");
|
DurableContentOwner authority = signingOwner("authority");
|
||||||
store.bindSigningOwnership(candidate -> candidate.equals(reference) ? Set.of(authority) : Set.of());
|
store.bindTransactionalOwnership(candidate -> candidate.equals(reference) ? Set.of(authority) : Set.of());
|
||||||
assertThrows(IOException.class, () -> store.retireUnownedContent(reference));
|
assertThrows(IOException.class, () -> store.retireUnownedContent(reference));
|
||||||
try (zeroecho.core.io.RepeatableContent content = store.openContent(reference)) {
|
try (zeroecho.core.io.RepeatableContent content = store.openContent(reference)) {
|
||||||
assertEquals(reference.length(), content.length().orElseThrow());
|
assertEquals(reference.length(), content.length().orElseThrow());
|
||||||
}
|
}
|
||||||
assertThrows(IllegalArgumentException.class,
|
assertThrows(IllegalArgumentException.class,
|
||||||
() -> store.retainContent(reference, signingOwner("authority")));
|
() -> store.retainContent(reference, signingOwner("authority")));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> store.retainContent(reference, new DurableContentOwner(
|
||||||
|
DurableContentOwner.Category.STATUS_OBJECT_RECORD, "status-authority")));
|
||||||
System.out.println("transactionalSigningOwnerPreventsGeneralRetirement...ok");
|
System.out.println("transactionalSigningOwnerPreventsGeneralRetirement...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +164,10 @@ final class FilesystemStagedContentStoreTest {
|
|||||||
System.out.println("sharedSigningPublicationReservationsAreReferenceCounted");
|
System.out.println("sharedSigningPublicationReservationsAreReferenceCounted");
|
||||||
FilesystemStagedContentStore store = new FilesystemStagedContentStore(directory, STORE_ID);
|
FilesystemStagedContentStore store = new FilesystemStagedContentStore(directory, STORE_ID);
|
||||||
DurableContentReference reference = stage(store, DurableContentReference.Lifecycle.OPERATION, "shared");
|
DurableContentReference reference = stage(store, DurableContentReference.Lifecycle.OPERATION, "shared");
|
||||||
try (FilesystemStagedContentStore.SigningReservation first = store.reserveSigningPublication(reference);
|
try (FilesystemStagedContentStore.TransactionalReservation first =
|
||||||
FilesystemStagedContentStore.SigningReservation second = store.reserveSigningPublication(reference)) {
|
store.reserveTransactionalPublication(reference);
|
||||||
|
FilesystemStagedContentStore.TransactionalReservation second =
|
||||||
|
store.reserveTransactionalPublication(reference)) {
|
||||||
first.close();
|
first.close();
|
||||||
assertThrows(IOException.class, () -> store.retireUnownedContent(reference));
|
assertThrows(IOException.class, () -> store.retireUnownedContent(reference));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user