diff --git a/src/backend.go b/src/backend.go index 50d4e83..f8817b5 100644 --- a/src/backend.go +++ b/src/backend.go @@ -160,6 +160,9 @@ type Backend interface { // Retrieve audit record contents for given IDs. GetAuditLogRecords(ctx context.Context, ids iter.Seq2[AuditID, error]) iter.Seq2[*AuditRecord, error] + // Detach an audit record from its blobs. + DetachAuditRecord(ctx context.Context, id AuditID) error + // Delete an audit record with a given ID. ExpireAuditRecord(ctx context.Context, id AuditID) error } diff --git a/src/backend_fs.go b/src/backend_fs.go index 78dbe52..b506ca5 100644 --- a/src/backend_fs.go +++ b/src/backend_fs.go @@ -484,6 +484,10 @@ func (fs *FSBackend) HaveDomainsChanged(ctx context.Context, since time.Time) (b return true, nil // not implemented } +func auditDetachedName(id AuditID) string { + return fmt.Sprintf("%s.detached", id) +} + func (fs *FSBackend) AppendAuditLog(ctx context.Context, id AuditID, record *AuditRecord) error { if _, err := fs.auditRoot.Stat(id.String()); err == nil { panic(fmt.Errorf("audit ID collision: %s", id)) @@ -498,6 +502,11 @@ func (fs *FSBackend) QueryAuditLog(ctx context.Context, id AuditID) (*AuditRecor } else if record, err := DecodeAuditRecord(data); err != nil { return nil, fmt.Errorf("decode: %w", err) } else { + if _, err := fs.auditRoot.Stat(auditDetachedName(id)); err == nil { + record.Manifest = nil + } else if !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("stat detached marker: %w", err) + } return record, nil } } @@ -514,6 +523,8 @@ func (fs *FSBackend) SearchAuditLog( var id AuditID if err != nil { // report error + } else if strings.Contains(path, ".") { + return nil // skip } else if id, err = ParseAuditID(path); err != nil { // report error } else if !opts.Since.IsZero() && id.CompareTime(opts.Since) < 0 { @@ -546,6 +557,10 @@ func (fs *FSBackend) GetAuditLogRecords( } } +func (fs *FSBackend) DetachAuditRecord(ctx context.Context, id AuditID) error { + return fs.auditRoot.WriteFile(auditDetachedName(id), []byte{}, 0o644) +} + func (fs *FSBackend) ExpireAuditRecord(ctx context.Context, id AuditID) error { return fs.auditRoot.Remove(id.String()) } diff --git a/src/backend_s3.go b/src/backend_s3.go index b4f24ba..20ac957 100644 --- a/src/backend_s3.go +++ b/src/backend_s3.go @@ -827,6 +827,10 @@ func auditObjectName(id AuditID) string { return fmt.Sprintf("audit/%s", id) } +func auditDetachedObjectName(id AuditID) string { + return fmt.Sprintf("audit/%s.detached", id) +} + func (s3 *S3Backend) AppendAuditLog(ctx context.Context, id AuditID, record *AuditRecord) error { logc.Printf(ctx, "s3: append audit %s\n", id) @@ -858,7 +862,20 @@ func (s3 *S3Backend) QueryAuditLog(ctx context.Context, id AuditID) (*AuditRecor return nil, err } - return DecodeAuditRecord(data) + record, err := DecodeAuditRecord(data) + if err != nil { + return nil, err + } + + _, err = s3.client.StatObject(ctx, s3.bucket, auditDetachedObjectName(id), + minio.StatObjectOptions{}) + if err == nil { + record.Manifest = nil + } else if errResp := minio.ToErrorResponse(err); err != nil && errResp.Code != "NoSuchKey" { + return nil, err + } + + return record, nil } func (s3 *S3Backend) SearchAuditLog( @@ -878,6 +895,8 @@ func (s3 *S3Backend) SearchAuditLog( var err error if object.Err != nil { err = object.Err + } else if strings.Contains(object.Key, ".") { + continue } else if id, err = ParseAuditID(strings.TrimPrefix(object.Key, prefix)); err != nil { // report error } else if !opts.Since.IsZero() && id.CompareTime(opts.Since) < 0 { @@ -929,6 +948,14 @@ func (s3 *S3Backend) GetAuditLogRecords( } } +func (s3 *S3Backend) DetachAuditRecord(ctx context.Context, id AuditID) error { + logc.Printf(ctx, "s3: detach audit record %s\n", id) + + _, err := s3.client.PutObject(ctx, s3.bucket, auditDetachedObjectName(id), + &bytes.Reader{}, 0, minio.PutObjectOptions{}) + return err +} + func (s3 *S3Backend) ExpireAuditRecord(ctx context.Context, id AuditID) error { logc.Printf(ctx, "s3: expire audit record %s\n", id) diff --git a/src/main.go b/src/main.go index 009b18a..7e01e98 100644 --- a/src/main.go +++ b/src/main.go @@ -193,7 +193,9 @@ func usage() { fmt.Fprintf(os.Stderr, "(admin) "+ "git-pages {-freeze-domain |-unfreeze-domain }\n") fmt.Fprintf(os.Stderr, "(audit) "+ - "git-pages {-audit-log|-audit-read |-audit-server [args...]}\n") + "git-pages {-audit-log|-audit-server [args...]}\n") + fmt.Fprintf(os.Stderr, "(audit) "+ + "git-pages {-audit-read|-audit-rollback|-audit-detach} \n") fmt.Fprintf(os.Stderr, "(audit) "+ "git-pages {-audit-expire }\n") fmt.Fprintf(os.Stderr, "(maint) "+ @@ -237,6 +239,8 @@ func Main(versionInfo string) { "extract contents of audit record `id` to files '-*'") auditRollback := flag.String("audit-rollback", "", "restore site from contents of audit record `id`") + auditDetach := flag.String("audit-detach", "", + "detach all blobs of audit record `id`") auditServer := flag.String("audit-server", "", "listen for notifications on `endpoint` and spawn a process for each audit event") auditExpire := flag.String("audit-expire", "", @@ -269,6 +273,7 @@ func Main(versionInfo string) { *auditLog, *auditRead != "", *auditRollback != "", + *auditDetach != "", *auditServer != "", *auditExpire != "", *runMigration != "", @@ -282,8 +287,8 @@ func Main(versionInfo string) { if cliOperations > 1 { logc.Fatalln(ctx, "-list-blobs, -list-manifests, -get-blob, -get-manifest, -get-archive, "+ "-update-site, -freeze-domain, -unfreeze-domain, -audit-log, -audit-read, "+ - "-audit-rollback, -audit-server, -run-migration, -size-histogram, "+ - "and -trace-garbage are mutually exclusive") + "-audit-rollback, -audit-detach, -audit-server, -audit-expire, -run-migration, "+ + "-size-histogram, and -trace-garbage are mutually exclusive") } if *configTomlPath != "" && *noConfig { @@ -334,15 +339,15 @@ func Main(versionInfo string) { logc.Fatalln(ctx, err) } - if domainCache, err = CreateDomainCache(ctx); err != nil { - logc.Fatalln(ctx, err) - } - // The server has its own logic for creating the backend. if cliOperations > 0 { if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } + + if domainCache, err = CreateDomainCache(ctx); err != nil { + logc.Fatalln(ctx, err) + } } switch { @@ -553,6 +558,17 @@ func Main(versionInfo string) { logc.Fatalln(ctx, err) } + case *auditDetach != "": + id, err := ParseAuditID(*auditDetach) + if err != nil { + logc.Fatalln(ctx, err) + } + + err = backend.DetachAuditRecord(ctx, id) + if err != nil { + logc.Fatalln(ctx, err) + } + case *auditServer != "": if flag.NArg() < 1 { logc.Fatalln(ctx, "handler path not provided") @@ -692,6 +708,10 @@ func Main(versionInfo string) { } backend = NewObservedBackend(backend) + if domainCache, err = CreateDomainCache(ctx); err != nil { + logc.Fatalln(ctx, err) + } + middleware := chainHTTPMiddleware( panicHandler, remoteAddrMiddleware, diff --git a/src/observe.go b/src/observe.go index ec61c61..5a46ef8 100644 --- a/src/observe.go +++ b/src/observe.go @@ -398,6 +398,13 @@ func (backend *observedBackend) GetAuditLogRecords( } } +func (backend *observedBackend) DetachAuditRecord(ctx context.Context, id AuditID) (err error) { + span, ctx := ObserveFunction(ctx, "DetachAuditRecord", "audit.id", id) + err = backend.inner.DetachAuditRecord(ctx, id) + span.Finish() + return +} + func (backend *observedBackend) ExpireAuditRecord(ctx context.Context, id AuditID) (err error) { span, ctx := ObserveFunction(ctx, "ExpireAuditRecord", "audit.id", id) err = backend.inner.ExpireAuditRecord(ctx, id)