+1
-1
flake.nix
+1
-1
flake.nix
+3
go.mod
+3
go.mod
···
8
8
github.com/KimMachineGun/automemlimit v0.7.5
9
9
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
10
10
github.com/creasty/defaults v1.8.0
11
+
github.com/fatih/color v1.18.0
11
12
github.com/getsentry/sentry-go v0.40.0
12
13
github.com/getsentry/sentry-go/slog v0.40.0
13
14
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0
···
43
44
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
44
45
github.com/klauspost/crc32 v1.3.0 // indirect
45
46
github.com/leodido/go-syslog/v4 v4.3.0 // indirect
47
+
github.com/mattn/go-colorable v0.1.13 // indirect
48
+
github.com/mattn/go-isatty v0.0.20 // indirect
46
49
github.com/minio/crc64nvme v1.1.0 // indirect
47
50
github.com/minio/md5-simd v1.1.2 // indirect
48
51
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+9
go.sum
+9
go.sum
···
33
33
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
34
34
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
35
35
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
36
+
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
37
+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
36
38
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
37
39
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
38
40
github.com/getsentry/sentry-go/slog v0.40.0 h1:uR2EPL9w6uHw3XB983IAqzqM9mP+fjJpNY9kfob3/Z8=
···
81
83
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
82
84
github.com/leodido/go-syslog/v4 v4.3.0 h1:bbSpI/41bYK9iSdlYzcwvlxuLOE8yi4VTFmedtnghdA=
83
85
github.com/leodido/go-syslog/v4 v4.3.0/go.mod h1:eJ8rUfDN5OS6dOkCOBYlg2a+hbAg6pJa99QXXgMrd98=
86
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
87
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
88
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
89
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
90
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
84
91
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
85
92
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
86
93
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
···
150
157
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
151
158
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
152
159
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
160
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
153
162
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
154
163
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
155
164
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
+31
src/audit.go
+31
src/audit.go
···
87
87
return
88
88
}
89
89
90
+
func (record *AuditRecord) GetAuditID() AuditID {
91
+
return AuditID(record.GetId())
92
+
}
93
+
94
+
func (record *AuditRecord) DescribePrincipal() string {
95
+
var items []string
96
+
if record.Principal != nil {
97
+
if record.Principal.GetIpAddress() != "" {
98
+
items = append(items, record.Principal.GetIpAddress())
99
+
}
100
+
if record.Principal.GetCliAdmin() {
101
+
items = append(items, "<cli-admin>")
102
+
}
103
+
}
104
+
if len(items) > 0 {
105
+
return strings.Join(items, ";")
106
+
} else {
107
+
return "<unknown>"
108
+
}
109
+
}
110
+
111
+
func (record *AuditRecord) DescribeResource() string {
112
+
desc := "<unknown>"
113
+
if record.Domain != nil && record.Project != nil {
114
+
desc = fmt.Sprintf("%s/%s", *record.Domain, *record.Project)
115
+
} else if record.Domain != nil {
116
+
desc = *record.Domain
117
+
}
118
+
return desc
119
+
}
120
+
90
121
type AuditRecordScope int
91
122
92
123
const (
+6
-6
src/backend.go
+6
-6
src/backend.go
···
52
52
IfMatch string
53
53
}
54
54
55
-
type QueryAuditLogOptions struct {
55
+
type SearchAuditLogOptions struct {
56
56
// Inclusive lower bound on returned audit records, per their Snowflake ID (which may differ
57
57
// slightly from the embedded timestamp). If zero, audit records are returned since beginning
58
58
// of time.
···
63
63
Until time.Time
64
64
}
65
65
66
-
type QueryAuditLogResult struct {
66
+
type SearchAuditLogResult struct {
67
67
ID AuditID
68
68
Err error
69
69
}
···
130
130
QueryAuditLog(ctx context.Context, id AuditID) (record *AuditRecord, err error)
131
131
132
132
// Retrieve records from the audit log by time range.
133
-
SearchAuditLog(ctx context.Context, opts QueryAuditLogOptions) iter.Seq[QueryAuditLogResult]
133
+
SearchAuditLog(ctx context.Context, opts SearchAuditLogOptions) iter.Seq2[AuditID, error]
134
134
}
135
135
136
-
func CreateBackend(config *StorageConfig) (backend Backend, err error) {
136
+
func CreateBackend(ctx context.Context, config *StorageConfig) (backend Backend, err error) {
137
137
switch config.Type {
138
138
case "fs":
139
-
if backend, err = NewFSBackend(context.Background(), &config.FS); err != nil {
139
+
if backend, err = NewFSBackend(ctx, &config.FS); err != nil {
140
140
err = fmt.Errorf("fs backend: %w", err)
141
141
}
142
142
case "s3":
143
-
if backend, err = NewS3Backend(context.Background(), &config.S3); err != nil {
143
+
if backend, err = NewS3Backend(ctx, &config.S3); err != nil {
144
144
err = fmt.Errorf("s3 backend: %w", err)
145
145
}
146
146
default:
+13
-15
src/backend_fs.go
+13
-15
src/backend_fs.go
···
434
434
}
435
435
436
436
func (fs *FSBackend) SearchAuditLog(
437
-
ctx context.Context, opts QueryAuditLogOptions,
438
-
) iter.Seq[QueryAuditLogResult] {
439
-
return func(yield func(QueryAuditLogResult) bool) {
437
+
ctx context.Context, opts SearchAuditLogOptions,
438
+
) iter.Seq2[AuditID, error] {
439
+
return func(yield func(AuditID, error) bool) {
440
440
iofs.WalkDir(fs.auditRoot.FS(), ".",
441
441
func(path string, entry iofs.DirEntry, err error) error {
442
442
if path == "." {
443
-
return nil
443
+
return nil // skip
444
444
}
445
-
var result QueryAuditLogResult
445
+
var id AuditID
446
446
if err != nil {
447
-
result.Err = err
448
-
} else if id, err := ParseAuditID(path); err != nil {
449
-
result.Err = err
447
+
// report error
448
+
} else if id, err = ParseAuditID(path); err != nil {
449
+
// report error
450
450
} else if !opts.Since.IsZero() && id.CompareTime(opts.Since) < 0 {
451
-
return nil
451
+
return nil // skip
452
452
} else if !opts.Until.IsZero() && id.CompareTime(opts.Until) > 0 {
453
-
return nil
454
-
} else {
455
-
result.ID = id
453
+
return nil // skip
456
454
}
457
-
if !yield(result) {
458
-
return iofs.SkipAll
455
+
if !yield(id, err) {
456
+
return iofs.SkipAll // break
459
457
} else {
460
-
return nil
458
+
return nil // continue
461
459
}
462
460
})
463
461
}
+8
-9
src/backend_s3.go
+8
-9
src/backend_s3.go
···
734
734
}
735
735
736
736
func (s3 *S3Backend) SearchAuditLog(
737
-
ctx context.Context, opts QueryAuditLogOptions,
738
-
) iter.Seq[QueryAuditLogResult] {
739
-
return func(yield func(QueryAuditLogResult) bool) {
737
+
ctx context.Context, opts SearchAuditLogOptions,
738
+
) iter.Seq2[AuditID, error] {
739
+
return func(yield func(AuditID, error) bool) {
740
740
logc.Printf(ctx, "s3: query audit\n")
741
741
742
742
ctx, cancel := context.WithCancel(ctx)
···
746
746
for object := range s3.client.ListObjectsIter(ctx, s3.bucket, minio.ListObjectsOptions{
747
747
Prefix: prefix,
748
748
}) {
749
-
var result QueryAuditLogResult
749
+
var id AuditID
750
+
var err error
750
751
if object.Err != nil {
751
-
result.Err = object.Err
752
-
} else if id, err := ParseAuditID(strings.TrimPrefix(object.Key, prefix)); err != nil {
753
-
result.Err = err
752
+
err = object.Err
754
753
} else {
755
-
result.ID = id
754
+
id, err = ParseAuditID(strings.TrimPrefix(object.Key, prefix))
756
755
}
757
-
if !yield(result) {
756
+
if !yield(id, err) {
758
757
break
759
758
}
760
759
}
+42
-9
src/main.go
+42
-9
src/main.go
···
20
20
21
21
automemlimit "github.com/KimMachineGun/automemlimit/memlimit"
22
22
"github.com/c2h5oh/datasize"
23
+
"github.com/fatih/color"
23
24
"github.com/kankanreno/go-snowflake"
24
25
"github.com/prometheus/client_golang/prometheus/promhttp"
26
+
"google.golang.org/protobuf/proto"
25
27
)
26
28
27
29
var config *Config
···
175
177
fmt.Fprintf(os.Stderr, "(admin) "+
176
178
"git-pages {-run-migration <name>|-freeze-domain <domain>|-unfreeze-domain <domain>}\n")
177
179
fmt.Fprintf(os.Stderr, "(audit) "+
178
-
"git-pages {-audit-read <id>}\n")
180
+
"git-pages {-audit-log|-audit-read <id>}\n")
179
181
fmt.Fprintf(os.Stderr, "(info) "+
180
182
"git-pages {-print-config-env-vars|-print-config}\n")
181
183
fmt.Fprintf(os.Stderr, "(cli) "+
···
209
211
"prevent any site uploads to a given `domain`")
210
212
unfreezeDomain := flag.String("unfreeze-domain", "",
211
213
"allow site uploads to a `domain` again after it has been frozen")
214
+
auditLog := flag.Bool("audit-log", false,
215
+
"display audit log")
212
216
auditRead := flag.String("audit-read", "",
213
217
"extract contents of audit record `id` to files '<id>-*'")
214
218
flag.Parse()
···
222
226
*updateSite != "",
223
227
*freezeDomain != "",
224
228
*unfreezeDomain != "",
229
+
*auditLog,
225
230
*auditRead != "",
226
231
} {
227
232
if selected {
···
270
275
271
276
switch {
272
277
case *runMigration != "":
273
-
if backend, err = CreateBackend(&config.Storage); err != nil {
278
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
274
279
logc.Fatalln(ctx, err)
275
280
}
276
281
···
279
284
}
280
285
281
286
case *getBlob != "":
282
-
if backend, err = CreateBackend(&config.Storage); err != nil {
287
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
283
288
logc.Fatalln(ctx, err)
284
289
}
285
290
···
290
295
io.Copy(fileOutputArg(), reader)
291
296
292
297
case *getManifest != "":
293
-
if backend, err = CreateBackend(&config.Storage); err != nil {
298
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
294
299
logc.Fatalln(ctx, err)
295
300
}
296
301
···
302
307
fmt.Fprintln(fileOutputArg(), string(ManifestJSON(manifest)))
303
308
304
309
case *getArchive != "":
305
-
if backend, err = CreateBackend(&config.Storage); err != nil {
310
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
306
311
logc.Fatalln(ctx, err)
307
312
}
308
313
···
317
322
}
318
323
319
324
case *updateSite != "":
320
-
if backend, err = CreateBackend(&config.Storage); err != nil {
325
+
ctx = WithPrincipal(ctx)
326
+
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
327
+
328
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
321
329
logc.Fatalln(ctx, err)
322
330
}
323
331
···
382
390
}
383
391
384
392
case *freezeDomain != "" || *unfreezeDomain != "":
393
+
ctx = WithPrincipal(ctx)
394
+
GetPrincipal(ctx).CliAdmin = proto.Bool(true)
395
+
385
396
var domain string
386
397
var freeze bool
387
398
if *freezeDomain != "" {
···
392
403
freeze = false
393
404
}
394
405
395
-
if backend, err = CreateBackend(&config.Storage); err != nil {
406
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
396
407
logc.Fatalln(ctx, err)
397
408
}
398
409
···
405
416
logc.Println(ctx, "thawed")
406
417
}
407
418
419
+
case *auditLog:
420
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
421
+
logc.Fatalln(ctx, err)
422
+
}
423
+
424
+
for id, err := range backend.SearchAuditLog(ctx, SearchAuditLogOptions{}) {
425
+
if err != nil {
426
+
logc.Fatalln(ctx, err)
427
+
}
428
+
record, err := backend.QueryAuditLog(ctx, id)
429
+
if err != nil {
430
+
logc.Fatalln(ctx, err)
431
+
}
432
+
fmt.Fprintf(color.Output, "%s %s %s %s %s\n",
433
+
record.GetAuditID().String(),
434
+
color.HiWhiteString(record.GetTimestamp().AsTime().UTC().Format(time.RFC3339)),
435
+
color.HiMagentaString(record.DescribePrincipal()),
436
+
color.HiGreenString(record.DescribeResource()),
437
+
record.GetEvent(),
438
+
)
439
+
}
440
+
408
441
case *auditRead != "":
409
-
if backend, err = CreateBackend(&config.Storage); err != nil {
442
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
410
443
logc.Fatalln(ctx, err)
411
444
}
412
445
···
475
508
caddyListener := listen(ctx, "caddy", config.Server.Caddy)
476
509
metricsListener := listen(ctx, "metrics", config.Server.Metrics)
477
510
478
-
if backend, err = CreateBackend(&config.Storage); err != nil {
511
+
if backend, err = CreateBackend(ctx, &config.Storage); err != nil {
479
512
logc.Fatalln(ctx, err)
480
513
}
481
514
backend = NewObservedBackend(backend)
+5
-5
src/observe.go
+5
-5
src/observe.go
···
457
457
}
458
458
459
459
func (backend *observedBackend) SearchAuditLog(
460
-
ctx context.Context, opts QueryAuditLogOptions,
461
-
) iter.Seq[QueryAuditLogResult] {
462
-
return func(yield func(QueryAuditLogResult) bool) {
460
+
ctx context.Context, opts SearchAuditLogOptions,
461
+
) iter.Seq2[AuditID, error] {
462
+
return func(yield func(AuditID, error) bool) {
463
463
span, ctx := ObserveFunction(ctx, "SearchAuditLog",
464
464
"audit.search.since", opts.Since,
465
465
"audit.search.until", opts.Until,
466
466
)
467
-
for result := range backend.inner.SearchAuditLog(ctx, opts) {
468
-
if !yield(result) {
467
+
for id, err := range backend.inner.SearchAuditLog(ctx, opts) {
468
+
if !yield(id, err) {
469
469
break
470
470
}
471
471
}
+11
-2
src/schema.pb.go
+11
-2
src/schema.pb.go
···
749
749
type Principal struct {
750
750
state protoimpl.MessageState `protogen:"open.v1"`
751
751
IpAddress *string `protobuf:"bytes,1,opt,name=ip_address,json=ipAddress" json:"ip_address,omitempty"`
752
+
CliAdmin *bool `protobuf:"varint,2,opt,name=cli_admin,json=cliAdmin" json:"cli_admin,omitempty"`
752
753
unknownFields protoimpl.UnknownFields
753
754
sizeCache protoimpl.SizeCache
754
755
}
···
788
789
return *x.IpAddress
789
790
}
790
791
return ""
792
+
}
793
+
794
+
func (x *Principal) GetCliAdmin() bool {
795
+
if x != nil && x.CliAdmin != nil {
796
+
return *x.CliAdmin
797
+
}
798
+
return false
791
799
}
792
800
793
801
var File_schema_proto protoreflect.FileDescriptor
···
845
853
"\x06domain\x18\n" +
846
854
" \x01(\tR\x06domain\x12\x18\n" +
847
855
"\aproject\x18\v \x01(\tR\aproject\x12%\n" +
848
-
"\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"*\n" +
856
+
"\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"G\n" +
849
857
"\tPrincipal\x12\x1d\n" +
850
858
"\n" +
851
-
"ip_address\x18\x01 \x01(\tR\tipAddress*V\n" +
859
+
"ip_address\x18\x01 \x01(\tR\tipAddress\x12\x1b\n" +
860
+
"\tcli_admin\x18\x02 \x01(\bR\bcliAdmin*V\n" +
852
861
"\x04Type\x12\x10\n" +
853
862
"\fInvalidEntry\x10\x00\x12\r\n" +
854
863
"\tDirectory\x10\x01\x12\x0e\n" +