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
+31
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 (
+6 -6
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:
+13 -15
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
}
})
}
+8 -9
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
}
}
+42 -9
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)
+5 -5
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
}
}
+11 -2
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" +
+1
View File
@@ -131,4 +131,5 @@ message AuditRecord {
message Principal {
string ip_address = 1;
bool cli_admin = 2;
}