Allow specifying a reason for a mutation done via the CLI.

The reason is appended to the audit record and is surfaced in the log.
This commit is contained in:
miyuko
2026-09-20 10:12:15 +01:00
parent 6743cd73e9
commit 6c2f34c54d
4 changed files with 44 additions and 20 deletions
+19
View File
@@ -50,6 +50,22 @@ func GetPrincipal(ctx context.Context) *Principal {
return nil
}
type reasonKey struct{}
var ReasonKey = reasonKey{}
func WithReason(ctx context.Context) context.Context {
reason := new(string)
return context.WithValue(ctx, ReasonKey, reason)
}
func GetReason(ctx context.Context) *string {
if reason, ok := ctx.Value(ReasonKey).(*string); ok {
return reason
}
return nil
}
var AuditSnowflakeStartTime = time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC)
type AuditID int64
@@ -300,6 +316,9 @@ func (audited *auditedBackend) appendNewAuditRecord(ctx context.Context, record
record.Id = proto.Int64(int64(id))
record.Timestamp = timestamppb.Now()
record.Principal = GetPrincipal(ctx)
if reason := GetReason(ctx); reason != nil && *reason != "" {
record.Reason = GetReason(ctx)
}
err = audited.Backend.AppendAuditLog(ctx, id, record)
if err != nil {
+13 -18
View File
@@ -280,6 +280,8 @@ func Main(versionInfo string) {
"display aggregate storage used per domain (argument is output mode and one of: text, json)")
traceGarbage := flag.Bool("trace-garbage", false,
"estimate total size of unreachable blobs")
reason := flag.String("reason", "",
"specify a reason for administrative operations in the audit log")
dryRun := flag.Bool("dry-run", false,
"print what would be performed instead of executing it")
version := flag.Bool("version", false,
@@ -386,6 +388,12 @@ func Main(versionInfo string) {
if existenceCache, err = CreateExistenceCache(ctx); err != nil {
logc.Fatalln(ctx, err)
}
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
ctx = WithReason(ctx)
*GetReason(ctx) = *reason
}
switch {
@@ -451,9 +459,6 @@ func Main(versionInfo string) {
}
case *updateSite != "":
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
if flag.NArg() != 1 {
logc.Fatalln(ctx, "update source must be provided as the argument")
}
@@ -515,9 +520,6 @@ func Main(versionInfo string) {
}
case *deleteSite != "":
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
webRoot := webRootArg(*deleteSite)
err := backend.DeleteManifest(ctx, webRoot, ModifyManifestOptions{})
if err != nil {
@@ -527,9 +529,6 @@ func Main(versionInfo string) {
logc.Println(ctx, "deleted")
case *freezeDomain != "" || *unfreezeDomain != "":
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
var domain string
var freeze bool
if *freezeDomain != "" {
@@ -553,9 +552,6 @@ func Main(versionInfo string) {
}
case *purgeDomain != "":
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
purgeCount := 0
for metadata, err := range backend.EnumerateManifests(ctx) {
if err != nil {
@@ -606,6 +602,11 @@ func Main(versionInfo string) {
parts = append(parts,
color.HiGreenString("%s", record.DescribePrincipal()),
)
if record.GetReason() != "" {
parts = append(parts,
color.HiCyanString("%q", record.GetReason()),
)
}
if record.IsDetached() {
parts = append(parts,
color.HiYellowString("(detached)"),
@@ -630,9 +631,6 @@ func Main(versionInfo string) {
}
case *auditRollback != "":
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
id, err := ParseAuditID(*auditRollback)
if err != nil {
logc.Fatalln(ctx, err)
@@ -736,9 +734,6 @@ func Main(versionInfo string) {
logc.Printf(ctx, "audit: expired %d records\n", count)
case *expireSites:
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
if !config.Feature("expiration") {
logc.Fatalf(ctx, "expire: feature disabled")
}
+11 -2
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.9
// protoc v6.30.2
// source: schema.proto
@@ -783,6 +783,7 @@ type AuditRecord struct {
Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp" json:"timestamp,omitempty"`
Event *AuditEvent `protobuf:"varint,3,opt,name=event,enum=AuditEvent" json:"event,omitempty"`
Principal *Principal `protobuf:"bytes,4,opt,name=principal" json:"principal,omitempty"`
Reason *string `protobuf:"bytes,13,opt,name=reason" json:"reason,omitempty"`
// Affected resource.
Domain *string `protobuf:"bytes,10,opt,name=domain" json:"domain,omitempty"`
Project *string `protobuf:"bytes,11,opt,name=project" json:"project,omitempty"` // only for `*Manifest` events
@@ -850,6 +851,13 @@ func (x *AuditRecord) GetPrincipal() *Principal {
return nil
}
func (x *AuditRecord) GetReason() string {
if x != nil && x.Reason != nil {
return *x.Reason
}
return ""
}
func (x *AuditRecord) GetDomain() string {
if x != nil && x.Domain != nil {
return *x.Domain
@@ -1054,13 +1062,14 @@ const file_schema_proto_rawDesc = "" +
"\bproblems\x18\a \x03(\v2\b.ProblemR\bproblems\x1aC\n" +
"\rContentsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x1c\n" +
"\x05value\x18\x02 \x01(\v2\x06.EntryR\x05value:\x028\x01\"\xfd\x01\n" +
"\x05value\x18\x02 \x01(\v2\x06.EntryR\x05value:\x028\x01\"\x95\x02\n" +
"\vAuditRecord\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x128\n" +
"\ttimestamp\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12!\n" +
"\x05event\x18\x03 \x01(\x0e2\v.AuditEventR\x05event\x12(\n" +
"\tprincipal\x18\x04 \x01(\v2\n" +
".PrincipalR\tprincipal\x12\x16\n" +
"\x06reason\x18\r \x01(\tR\x06reason\x12\x16\n" +
"\x06domain\x18\n" +
" \x01(\tR\x06domain\x12\x18\n" +
"\aproject\x18\v \x01(\tR\aproject\x12%\n" +
+1
View File
@@ -136,6 +136,7 @@ message AuditRecord {
google.protobuf.Timestamp timestamp = 2;
AuditEvent event = 3;
Principal principal = 4;
string reason = 13;
// Affected resource.
string domain = 10;