Implement -audit-log option.

Also, record the principal of `git-pages -{freeze,unfreeze}-domain`
and `git-pages -update-site` as the CLI administrator.
This commit is contained in:
Catherine
2025-12-04 15:58:14 +00:00
parent 4161013fc0
commit 886635ce5e
11 changed files with 130 additions and 47 deletions

View File

@@ -43,7 +43,7 @@
"-s -w"
];
vendorHash = "sha256-LkHC/gFiSfYz9Z4bYMq1QNdapPYp8h1DSMRfFU9f7mw=";
vendorHash = "sha256-40LyEXdJDpWPe9UvqM2siqXdpbae1ba7kN7FtySPpBc=";
};
in
{

3
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/KimMachineGun/automemlimit v0.7.5
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/creasty/defaults v1.8.0
github.com/fatih/color v1.18.0
github.com/getsentry/sentry-go v0.40.0
github.com/getsentry/sentry-go/slog v0.40.0
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0
@@ -43,6 +44,8 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/leodido/go-syslog/v4 v4.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect

9
go.sum
View File

@@ -33,6 +33,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/getsentry/sentry-go/slog v0.40.0 h1:uR2EPL9w6uHw3XB983IAqzqM9mP+fjJpNY9kfob3/Z8=
@@ -81,6 +83,11 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-syslog/v4 v4.3.0 h1:bbSpI/41bYK9iSdlYzcwvlxuLOE8yi4VTFmedtnghdA=
github.com/leodido/go-syslog/v4 v4.3.0/go.mod h1:eJ8rUfDN5OS6dOkCOBYlg2a+hbAg6pJa99QXXgMrd98=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
@@ -150,6 +157,8 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=

View File

@@ -87,6 +87,37 @@ func DecodeAuditRecord(data []byte) (record *AuditRecord, err error) {
return
}
func (record *AuditRecord) GetAuditID() AuditID {
return AuditID(record.GetId())
}
func (record *AuditRecord) DescribePrincipal() string {
var items []string
if record.Principal != nil {
if record.Principal.GetIpAddress() != "" {
items = append(items, record.Principal.GetIpAddress())
}
if record.Principal.GetCliAdmin() {
items = append(items, "<cli-admin>")
}
}
if len(items) > 0 {
return strings.Join(items, ";")
} else {
return "<unknown>"
}
}
func (record *AuditRecord) DescribeResource() string {
desc := "<unknown>"
if record.Domain != nil && record.Project != nil {
desc = fmt.Sprintf("%s/%s", *record.Domain, *record.Project)
} else if record.Domain != nil {
desc = *record.Domain
}
return desc
}
type AuditRecordScope int
const (

View File

@@ -52,7 +52,7 @@ type ModifyManifestOptions struct {
IfMatch string
}
type QueryAuditLogOptions struct {
type SearchAuditLogOptions struct {
// Inclusive lower bound on returned audit records, per their Snowflake ID (which may differ
// slightly from the embedded timestamp). If zero, audit records are returned since beginning
// of time.
@@ -63,7 +63,7 @@ type QueryAuditLogOptions struct {
Until time.Time
}
type QueryAuditLogResult struct {
type SearchAuditLogResult struct {
ID AuditID
Err error
}
@@ -130,17 +130,17 @@ type Backend interface {
QueryAuditLog(ctx context.Context, id AuditID) (record *AuditRecord, err error)
// Retrieve records from the audit log by time range.
SearchAuditLog(ctx context.Context, opts QueryAuditLogOptions) iter.Seq[QueryAuditLogResult]
SearchAuditLog(ctx context.Context, opts SearchAuditLogOptions) iter.Seq2[AuditID, error]
}
func CreateBackend(config *StorageConfig) (backend Backend, err error) {
func CreateBackend(ctx context.Context, config *StorageConfig) (backend Backend, err error) {
switch config.Type {
case "fs":
if backend, err = NewFSBackend(context.Background(), &config.FS); err != nil {
if backend, err = NewFSBackend(ctx, &config.FS); err != nil {
err = fmt.Errorf("fs backend: %w", err)
}
case "s3":
if backend, err = NewS3Backend(context.Background(), &config.S3); err != nil {
if backend, err = NewS3Backend(ctx, &config.S3); err != nil {
err = fmt.Errorf("s3 backend: %w", err)
}
default:

View File

@@ -434,30 +434,28 @@ func (fs *FSBackend) QueryAuditLog(ctx context.Context, id AuditID) (*AuditRecor
}
func (fs *FSBackend) SearchAuditLog(
ctx context.Context, opts QueryAuditLogOptions,
) iter.Seq[QueryAuditLogResult] {
return func(yield func(QueryAuditLogResult) bool) {
ctx context.Context, opts SearchAuditLogOptions,
) iter.Seq2[AuditID, error] {
return func(yield func(AuditID, error) bool) {
iofs.WalkDir(fs.auditRoot.FS(), ".",
func(path string, entry iofs.DirEntry, err error) error {
if path == "." {
return nil
return nil // skip
}
var result QueryAuditLogResult
var id AuditID
if err != nil {
result.Err = err
} else if id, err := ParseAuditID(path); err != nil {
result.Err = err
// report error
} else if id, err = ParseAuditID(path); err != nil {
// report error
} else if !opts.Since.IsZero() && id.CompareTime(opts.Since) < 0 {
return nil
return nil // skip
} else if !opts.Until.IsZero() && id.CompareTime(opts.Until) > 0 {
return nil
} else {
result.ID = id
return nil // skip
}
if !yield(result) {
return iofs.SkipAll
if !yield(id, err) {
return iofs.SkipAll // break
} else {
return nil
return nil // continue
}
})
}

View File

@@ -734,9 +734,9 @@ func (s3 *S3Backend) QueryAuditLog(ctx context.Context, id AuditID) (*AuditRecor
}
func (s3 *S3Backend) SearchAuditLog(
ctx context.Context, opts QueryAuditLogOptions,
) iter.Seq[QueryAuditLogResult] {
return func(yield func(QueryAuditLogResult) bool) {
ctx context.Context, opts SearchAuditLogOptions,
) iter.Seq2[AuditID, error] {
return func(yield func(AuditID, error) bool) {
logc.Printf(ctx, "s3: query audit\n")
ctx, cancel := context.WithCancel(ctx)
@@ -746,15 +746,14 @@ func (s3 *S3Backend) SearchAuditLog(
for object := range s3.client.ListObjectsIter(ctx, s3.bucket, minio.ListObjectsOptions{
Prefix: prefix,
}) {
var result QueryAuditLogResult
var id AuditID
var err error
if object.Err != nil {
result.Err = object.Err
} else if id, err := ParseAuditID(strings.TrimPrefix(object.Key, prefix)); err != nil {
result.Err = err
err = object.Err
} else {
result.ID = id
id, err = ParseAuditID(strings.TrimPrefix(object.Key, prefix))
}
if !yield(result) {
if !yield(id, err) {
break
}
}

View File

@@ -20,8 +20,10 @@ import (
automemlimit "github.com/KimMachineGun/automemlimit/memlimit"
"github.com/c2h5oh/datasize"
"github.com/fatih/color"
"github.com/kankanreno/go-snowflake"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/protobuf/proto"
)
var config *Config
@@ -175,7 +177,7 @@ func usage() {
fmt.Fprintf(os.Stderr, "(admin) "+
"git-pages {-run-migration <name>|-freeze-domain <domain>|-unfreeze-domain <domain>}\n")
fmt.Fprintf(os.Stderr, "(audit) "+
"git-pages {-audit-read <id>}\n")
"git-pages {-audit-log|-audit-read <id>}\n")
fmt.Fprintf(os.Stderr, "(info) "+
"git-pages {-print-config-env-vars|-print-config}\n")
fmt.Fprintf(os.Stderr, "(cli) "+
@@ -209,6 +211,8 @@ func Main() {
"prevent any site uploads to a given `domain`")
unfreezeDomain := flag.String("unfreeze-domain", "",
"allow site uploads to a `domain` again after it has been frozen")
auditLog := flag.Bool("audit-log", false,
"display audit log")
auditRead := flag.String("audit-read", "",
"extract contents of audit record `id` to files '<id>-*'")
flag.Parse()
@@ -222,6 +226,7 @@ func Main() {
*updateSite != "",
*freezeDomain != "",
*unfreezeDomain != "",
*auditLog,
*auditRead != "",
} {
if selected {
@@ -270,7 +275,7 @@ func Main() {
switch {
case *runMigration != "":
if backend, err = CreateBackend(&config.Storage); err != nil {
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
@@ -279,7 +284,7 @@ func Main() {
}
case *getBlob != "":
if backend, err = CreateBackend(&config.Storage); err != nil {
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
@@ -290,7 +295,7 @@ func Main() {
io.Copy(fileOutputArg(), reader)
case *getManifest != "":
if backend, err = CreateBackend(&config.Storage); err != nil {
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
@@ -302,7 +307,7 @@ func Main() {
fmt.Fprintln(fileOutputArg(), string(ManifestJSON(manifest)))
case *getArchive != "":
if backend, err = CreateBackend(&config.Storage); err != nil {
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
@@ -317,7 +322,10 @@ func Main() {
}
case *updateSite != "":
if backend, err = CreateBackend(&config.Storage); err != nil {
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
@@ -382,6 +390,9 @@ func Main() {
}
case *freezeDomain != "" || *unfreezeDomain != "":
ctx = WithPrincipal(ctx)
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
var domain string
var freeze bool
if *freezeDomain != "" {
@@ -392,7 +403,7 @@ func Main() {
freeze = false
}
if backend, err = CreateBackend(&config.Storage); err != nil {
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
@@ -405,8 +416,30 @@ func Main() {
logc.Println(ctx, "thawed")
}
case *auditLog:
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
for id, err := range backend.SearchAuditLog(ctx, SearchAuditLogOptions{}) {
if err != nil {
logc.Fatalln(ctx, err)
}
record, err := backend.QueryAuditLog(ctx, id)
if err != nil {
logc.Fatalln(ctx, err)
}
fmt.Fprintf(color.Output, "%s %s %s %s %s\n",
record.GetAuditID().String(),
color.HiWhiteString(record.GetTimestamp().AsTime().UTC().Format(time.RFC3339)),
color.HiMagentaString(record.DescribePrincipal()),
color.HiGreenString(record.DescribeResource()),
record.GetEvent(),
)
}
case *auditRead != "":
if backend, err = CreateBackend(&config.Storage); err != nil {
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
@@ -475,7 +508,7 @@ func Main() {
caddyListener := listen(ctx, "caddy", config.Server.Caddy)
metricsListener := listen(ctx, "metrics", config.Server.Metrics)
if backend, err = CreateBackend(&config.Storage); err != nil {
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
logc.Fatalln(ctx, err)
}
backend = NewObservedBackend(backend)

View File

@@ -457,15 +457,15 @@ func (backend *observedBackend) QueryAuditLog(ctx context.Context, id AuditID) (
}
func (backend *observedBackend) SearchAuditLog(
ctx context.Context, opts QueryAuditLogOptions,
) iter.Seq[QueryAuditLogResult] {
return func(yield func(QueryAuditLogResult) bool) {
ctx context.Context, opts SearchAuditLogOptions,
) iter.Seq2[AuditID, error] {
return func(yield func(AuditID, error) bool) {
span, ctx := ObserveFunction(ctx, "SearchAuditLog",
"audit.search.since", opts.Since,
"audit.search.until", opts.Until,
)
for result := range backend.inner.SearchAuditLog(ctx, opts) {
if !yield(result) {
for id, err := range backend.inner.SearchAuditLog(ctx, opts) {
if !yield(id, err) {
break
}
}

View File

@@ -749,6 +749,7 @@ func (x *AuditRecord) GetManifest() *Manifest {
type Principal struct {
state protoimpl.MessageState `protogen:"open.v1"`
IpAddress *string `protobuf:"bytes,1,opt,name=ip_address,json=ipAddress" json:"ip_address,omitempty"`
CliAdmin *bool `protobuf:"varint,2,opt,name=cli_admin,json=cliAdmin" json:"cli_admin,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -790,6 +791,13 @@ func (x *Principal) GetIpAddress() string {
return ""
}
func (x *Principal) GetCliAdmin() bool {
if x != nil && x.CliAdmin != nil {
return *x.CliAdmin
}
return false
}
var File_schema_proto protoreflect.FileDescriptor
const file_schema_proto_rawDesc = "" +
@@ -845,10 +853,11 @@ const file_schema_proto_rawDesc = "" +
"\x06domain\x18\n" +
" \x01(\tR\x06domain\x12\x18\n" +
"\aproject\x18\v \x01(\tR\aproject\x12%\n" +
"\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"*\n" +
"\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"G\n" +
"\tPrincipal\x12\x1d\n" +
"\n" +
"ip_address\x18\x01 \x01(\tR\tipAddress*V\n" +
"ip_address\x18\x01 \x01(\tR\tipAddress\x12\x1b\n" +
"\tcli_admin\x18\x02 \x01(\bR\bcliAdmin*V\n" +
"\x04Type\x12\x10\n" +
"\fInvalidEntry\x10\x00\x12\r\n" +
"\tDirectory\x10\x01\x12\x0e\n" +

View File

@@ -131,4 +131,5 @@ message AuditRecord {
message Principal {
string ip_address = 1;
bool cli_admin = 2;
}