mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-14 03:01:48 +00:00
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:
@@ -43,7 +43,7 @@
|
||||
"-s -w"
|
||||
];
|
||||
|
||||
vendorHash = "sha256-LkHC/gFiSfYz9Z4bYMq1QNdapPYp8h1DSMRfFU9f7mw=";
|
||||
vendorHash = "sha256-40LyEXdJDpWPe9UvqM2siqXdpbae1ba7kN7FtySPpBc=";
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
3
go.mod
3
go.mod
@@ -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
9
go.sum
@@ -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=
|
||||
|
||||
31
src/audit.go
31
src/audit.go
@@ -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 (
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
51
src/main.go
51
src/main.go
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -131,4 +131,5 @@ message AuditRecord {
|
||||
|
||||
message Principal {
|
||||
string ip_address = 1;
|
||||
bool cli_admin = 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user