mirror of
https://github.com/goharbor/harbor
synced 2025-04-15 01:08:04 +00:00
Add user event and config event (#21455)
Signed-off-by: stonezdj <stone.zhang@broadcom.com>
This commit is contained in:
parent
cc966435a5
commit
6965cab0c5
|
@ -63,7 +63,9 @@ import (
|
|||
_ "github.com/goharbor/harbor/src/pkg/accessory/model/sbom"
|
||||
_ "github.com/goharbor/harbor/src/pkg/accessory/model/subject"
|
||||
"github.com/goharbor/harbor/src/pkg/audit"
|
||||
_ "github.com/goharbor/harbor/src/pkg/auditext/event/config"
|
||||
_ "github.com/goharbor/harbor/src/pkg/auditext/event/login"
|
||||
_ "github.com/goharbor/harbor/src/pkg/auditext/event/user"
|
||||
dbCfg "github.com/goharbor/harbor/src/pkg/config/db"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||
"github.com/goharbor/harbor/src/pkg/notification"
|
||||
|
|
157
src/pkg/auditext/event/basic.go
Normal file
157
src/pkg/auditext/event/basic.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
|
||||
ctlevent "github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
|
||||
"github.com/goharbor/harbor/src/controller/event/model"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
)
|
||||
|
||||
const (
|
||||
createOp = "create"
|
||||
updateOp = "update"
|
||||
deleteOp = "delete"
|
||||
resourceIDPattern = `^%v/(\d+)$`
|
||||
)
|
||||
|
||||
// ResolveIDToNameFunc is the function to resolve the resource name from resource id
|
||||
type ResolveIDToNameFunc func(string) string
|
||||
|
||||
type Resolver struct {
|
||||
BaseURLPattern string
|
||||
ResourceType string
|
||||
SucceedCodes []int
|
||||
// SensitiveAttributes is the attributes that need to be redacted
|
||||
SensitiveAttributes []string
|
||||
// HasResourceName indicates if the resource has name, if true, need to resolve the resource name before delete
|
||||
ShouldResolveName bool
|
||||
// IDToNameFunc is used to resolve the resource name from resource id
|
||||
IDToNameFunc ResolveIDToNameFunc
|
||||
}
|
||||
|
||||
// PreCheck check if the event should be captured and resolve the resource name if needed, if need to resolve the resource name, return the resource name
|
||||
func (e *Resolver) PreCheck(ctx context.Context, url string, method string) (capture bool, resourceName string) {
|
||||
capture = config.AuditLogEventEnabled(ctx, fmt.Sprintf("%v_%v", MethodToOperation(method), e.ResourceType))
|
||||
if !capture {
|
||||
return false, ""
|
||||
}
|
||||
// for delete operation on a resource has name, need to resolve the resource id to resource name before delete
|
||||
resName := ""
|
||||
if capture && method == http.MethodDelete && e.ShouldResolveName {
|
||||
re := regexp.MustCompile(fmt.Sprintf(resourceIDPattern, e.BaseURLPattern))
|
||||
m := re.FindStringSubmatch(url)
|
||||
if len(m) == 2 && e.IDToNameFunc != nil {
|
||||
resName = e.IDToNameFunc(m[1])
|
||||
}
|
||||
}
|
||||
return true, resName
|
||||
}
|
||||
|
||||
// Resolve ...
|
||||
func (e *Resolver) Resolve(ce *commonevent.Metadata, event *event.Event) error {
|
||||
if ce.RequestMethod != http.MethodPost && ce.RequestMethod != http.MethodDelete && ce.RequestMethod != http.MethodPut {
|
||||
return nil
|
||||
}
|
||||
evt := &model.CommonEvent{
|
||||
Operator: ce.Username,
|
||||
ResourceType: e.ResourceType,
|
||||
OcurrAt: time.Now(),
|
||||
IsSuccessful: true,
|
||||
}
|
||||
resourceName := ""
|
||||
operation := MethodToOperation(ce.RequestMethod)
|
||||
if len(operation) == 0 {
|
||||
return nil
|
||||
}
|
||||
evt.Operation = operation
|
||||
|
||||
switch evt.Operation {
|
||||
case createOp:
|
||||
if len(ce.ResponseLocation) > 0 {
|
||||
// extract resource id from response location
|
||||
re := regexp.MustCompile(fmt.Sprintf(resourceIDPattern, e.BaseURLPattern))
|
||||
m := re.FindStringSubmatch(ce.ResponseLocation)
|
||||
if len(m) != 2 {
|
||||
return nil
|
||||
}
|
||||
evt.ResourceName = m[1]
|
||||
if e.IDToNameFunc != nil {
|
||||
resourceName = e.IDToNameFunc(m[1])
|
||||
}
|
||||
}
|
||||
if e.ShouldResolveName && resourceName != "" {
|
||||
evt.ResourceName = resourceName
|
||||
}
|
||||
|
||||
case deleteOp:
|
||||
re := regexp.MustCompile(fmt.Sprintf(resourceIDPattern, e.BaseURLPattern))
|
||||
m := re.FindStringSubmatch(ce.RequestURL)
|
||||
if len(m) != 2 {
|
||||
return nil
|
||||
}
|
||||
evt.ResourceName = m[1]
|
||||
if e.ShouldResolveName && ce.ResourceName != "" {
|
||||
evt.ResourceName = ce.ResourceName
|
||||
}
|
||||
|
||||
case updateOp:
|
||||
re := regexp.MustCompile(fmt.Sprintf(resourceIDPattern, e.BaseURLPattern))
|
||||
m := re.FindStringSubmatch(ce.RequestURL)
|
||||
if len(m) != 2 {
|
||||
return nil
|
||||
}
|
||||
evt.ResourceName = m[1]
|
||||
if e.IDToNameFunc != nil {
|
||||
resourceName = e.IDToNameFunc(m[1])
|
||||
}
|
||||
if e.ShouldResolveName && resourceName != "" {
|
||||
evt.ResourceName = resourceName
|
||||
}
|
||||
}
|
||||
|
||||
evt.OperationDescription = fmt.Sprintf("%s %s with name: %s", evt.Operation, e.ResourceType, evt.ResourceName)
|
||||
|
||||
if !slices.Contains(e.SucceedCodes, ce.ResponseCode) {
|
||||
evt.IsSuccessful = false
|
||||
}
|
||||
|
||||
event.Topic = ctlevent.TopicCommonEvent
|
||||
event.Data = evt
|
||||
return nil
|
||||
}
|
||||
|
||||
// MethodToOperation converts HTTP method to operation
|
||||
func MethodToOperation(method string) string {
|
||||
switch method {
|
||||
case http.MethodPost:
|
||||
return createOp
|
||||
case http.MethodDelete:
|
||||
return deleteOp
|
||||
case http.MethodPut:
|
||||
return updateOp
|
||||
}
|
||||
return ""
|
||||
}
|
66
src/pkg/auditext/event/basic_test.go
Normal file
66
src/pkg/auditext/event/basic_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEventResolver_PreCheck(t *testing.T) {
|
||||
type fields struct {
|
||||
BaseURLPattern string
|
||||
ResourceType string
|
||||
SucceedCodes []int
|
||||
SensitiveAttributes []string
|
||||
ShouldResolveName bool
|
||||
IDToNameFunc ResolveIDToNameFunc
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
url string
|
||||
method string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantCapture bool
|
||||
wantResourceName string
|
||||
}{
|
||||
{"test normal", fields{BaseURLPattern: `/api/v2.0/tests`, ResourceType: "test", SucceedCodes: []int{200}, ShouldResolveName: true, IDToNameFunc: func(string) string { return "test" }}, args{context.Background(), "/api/v2.0/tests/123", "DELETE"}, true, "test"},
|
||||
{"test resource name", fields{BaseURLPattern: `/api/v2.0/tests`, ResourceType: "test", SucceedCodes: []int{200}, ShouldResolveName: true, IDToNameFunc: func(string) string { return "test_resource_name" }}, args{context.Background(), "/api/v2.0/tests/234", "DELETE"}, true, "test_resource_name"},
|
||||
{"test no resource name", fields{BaseURLPattern: `/api/v2.0/tests`, ResourceType: "test", SucceedCodes: []int{200}, ShouldResolveName: true}, args{context.Background(), "/api/v2.0/tests/234", "GET"}, true, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := &Resolver{
|
||||
BaseURLPattern: tt.fields.BaseURLPattern,
|
||||
ResourceType: tt.fields.ResourceType,
|
||||
SucceedCodes: tt.fields.SucceedCodes,
|
||||
SensitiveAttributes: tt.fields.SensitiveAttributes,
|
||||
ShouldResolveName: tt.fields.ShouldResolveName,
|
||||
IDToNameFunc: tt.fields.IDToNameFunc,
|
||||
}
|
||||
gotCapture, gotResourceName := e.PreCheck(tt.args.ctx, tt.args.url, tt.args.method)
|
||||
if gotCapture != tt.wantCapture {
|
||||
t.Errorf("EventResolver.PreCheck() gotCapture = %v, want %v", gotCapture, tt.wantCapture)
|
||||
}
|
||||
if gotResourceName != tt.wantResourceName {
|
||||
t.Errorf("EventResolver.PreCheck() gotResourceName = %v, want %v", gotResourceName, tt.wantResourceName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
71
src/pkg/auditext/event/config/config.go
Normal file
71
src/pkg/auditext/event/config/config.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
ctlevent "github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
|
||||
"github.com/goharbor/harbor/src/controller/event/model"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
ext "github.com/goharbor/harbor/src/pkg/auditext/event"
|
||||
"github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var configureEventResolver = &resolver{
|
||||
SensitiveAttributes: []string{"ldap_password", "oidc_client_secret"}, // all user config items with PasswordType defined in the metadatalist.go should be defined in SensitiveAttributes
|
||||
}
|
||||
commonevent.RegisterResolver(`/api/v2.0/configurations`, configureEventResolver)
|
||||
}
|
||||
|
||||
const payloadSizeLimit = 450
|
||||
|
||||
// resolver used to resolve the configuration event
|
||||
type resolver struct {
|
||||
SensitiveAttributes []string
|
||||
}
|
||||
|
||||
func (c *resolver) Resolve(ce *commonevent.Metadata, evt *event.Event) error {
|
||||
e := &model.CommonEvent{}
|
||||
e.Operation = "update"
|
||||
e.Operator = ce.Username
|
||||
e.ResourceType = rbac.ResourceConfiguration.String()
|
||||
e.ResourceName = rbac.ResourceConfiguration.String()
|
||||
e.Payload = ext.Redact(ce.RequestPayload, c.SensitiveAttributes)
|
||||
e.OcurrAt = time.Now()
|
||||
if len(ce.RequestPayload) > payloadSizeLimit {
|
||||
ce.RequestPayload = fmt.Sprintf("%v...", ce.RequestPayload[:payloadSizeLimit])
|
||||
}
|
||||
e.OperationDescription = fmt.Sprintf("update configuration: %v", ce.RequestPayload)
|
||||
if ce.ResponseCode == http.StatusOK {
|
||||
e.IsSuccessful = true
|
||||
}
|
||||
evt.Topic = ctlevent.TopicCommonEvent
|
||||
evt.Data = e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *resolver) PreCheck(ctx context.Context, _ string, method string) (bool, string) {
|
||||
if method != http.MethodPut {
|
||||
return false, ""
|
||||
}
|
||||
return config.AuditLogEventEnabled(ctx, fmt.Sprintf("%v_%v", ext.MethodToOperation(method), rbac.ResourceConfiguration.String())), ""
|
||||
}
|
113
src/pkg/auditext/event/config/config_test.go
Normal file
113
src/pkg/auditext/event/config/config_test.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
|
||||
"github.com/goharbor/harbor/src/controller/event/model"
|
||||
"github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||
)
|
||||
|
||||
func TestConfigureEventResolver_Resolve(t *testing.T) {
|
||||
type fields struct {
|
||||
SensitiveAttributes []string
|
||||
}
|
||||
type args struct {
|
||||
ce *commonevent.Metadata
|
||||
evt *event.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
wantOperation string
|
||||
wantResourceType string
|
||||
wantOperationDescription string
|
||||
}{
|
||||
{"test normal", fields{[]string{}}, args{
|
||||
ce: &commonevent.Metadata{
|
||||
Username: "test",
|
||||
RequestURL: "/api/v2.0/configurations",
|
||||
RequestMethod: "PUT",
|
||||
}, evt: &event.Event{}}, false, "update", "configuration", "update configuration"},
|
||||
{"test error", fields{[]string{}}, args{ce: &commonevent.Metadata{
|
||||
Username: "test",
|
||||
RequestURL: "/api/v2.0/configurations",
|
||||
RequestMethod: "POST",
|
||||
}, evt: &event.Event{}}, false, "", "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &resolver{
|
||||
SensitiveAttributes: tt.fields.SensitiveAttributes,
|
||||
}
|
||||
if err := c.Resolve(tt.args.ce, tt.args.evt); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ConfigureEventResolver.Resolve() error = %v, wantErr %v", err, tt.wantErr)
|
||||
if tt.args.evt.Data != nil {
|
||||
data := tt.args.evt.Data.(*model.CommonEvent)
|
||||
if data.Operation != tt.wantOperation {
|
||||
t.Errorf("ConfigureEventResolver.Resolve() Operation = %v, want %v", data.Operation, tt.wantOperation)
|
||||
}
|
||||
if data.ResourceType != tt.wantResourceType {
|
||||
t.Errorf("ConfigureEventResolver.Resolve() ResourceType = %v, want %v", data.ResourceType, tt.wantResourceType)
|
||||
}
|
||||
if data.OperationDescription != tt.wantOperationDescription {
|
||||
t.Errorf("ConfigureEventResolver.Resolve() OperationDescription = %v, want %v", data.OperationDescription, tt.wantOperationDescription)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureEventResolver_PreCheck(t *testing.T) {
|
||||
type fields struct {
|
||||
SensitiveAttributes []string
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
url string
|
||||
method string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
wantTopic string
|
||||
}{
|
||||
{"test normal", fields{[]string{}}, args{context.Background(), "/api/v2.0/configurations", "PUT"}, true, ""},
|
||||
{"test error", fields{[]string{}}, args{context.Background(), "/api/v2.0/configurations", "POST"}, false, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &resolver{
|
||||
SensitiveAttributes: tt.fields.SensitiveAttributes,
|
||||
}
|
||||
got, got1 := c.PreCheck(tt.args.ctx, tt.args.url, tt.args.method)
|
||||
if got != tt.want {
|
||||
t.Errorf("ConfigureEventResolver.PreCheck() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.wantTopic {
|
||||
t.Errorf("ConfigureEventResolver.PreCheck() got1 = %v, want %v", got1, tt.wantTopic)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
ctlEvent "github.com/goharbor/harbor/src/controller/event"
|
||||
ctlevent "github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
|
||||
"github.com/goharbor/harbor/src/controller/event/model"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
|
@ -69,7 +69,7 @@ func (l *loginResolver) Resolve(ce *commonevent.Metadata, event *event.Event) er
|
|||
if ce.ResponseCode != http.StatusOK {
|
||||
e.IsSuccessful = false
|
||||
}
|
||||
event.Topic = ctlEvent.TopicCommonEvent
|
||||
event.Topic = ctlevent.TopicCommonEvent
|
||||
event.Data = e
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
ctlEvent "github.com/goharbor/harbor/src/controller/event"
|
||||
ctlevent "github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
|
||||
"github.com/goharbor/harbor/src/controller/event/model"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
|
@ -44,7 +44,7 @@ func (l *logoutResolver) Resolve(ce *commonevent.Metadata, event *event.Event) e
|
|||
if ce.ResponseCode != http.StatusOK {
|
||||
e.IsSuccessful = false
|
||||
}
|
||||
event.Topic = ctlEvent.TopicCommonEvent
|
||||
event.Topic = ctlevent.TopicCommonEvent
|
||||
event.Data = e
|
||||
return nil
|
||||
}
|
||||
|
|
64
src/pkg/auditext/event/user/user.go
Normal file
64
src/pkg/auditext/event/user/user.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/orm"
|
||||
"github.com/goharbor/harbor/src/pkg/auditext/event"
|
||||
pkgUser "github.com/goharbor/harbor/src/pkg/user"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var userResolver = &userEventResolver{
|
||||
Resolver: event.Resolver{
|
||||
BaseURLPattern: "/api/v2.0/users",
|
||||
ResourceType: rbac.ResourceUser.String(),
|
||||
SucceedCodes: []int{http.StatusCreated, http.StatusOK},
|
||||
SensitiveAttributes: []string{"password"},
|
||||
ShouldResolveName: true,
|
||||
IDToNameFunc: userIDToName,
|
||||
},
|
||||
}
|
||||
commonevent.RegisterResolver(`/api/v2.0/users$`, userResolver)
|
||||
commonevent.RegisterResolver(`^/api/v2.0/users/\d+/password$`, userResolver)
|
||||
commonevent.RegisterResolver(`^/api/v2.0/users/\d+/sysadmin$`, userResolver)
|
||||
commonevent.RegisterResolver(`^/api/v2.0/users/\d+$`, userResolver)
|
||||
}
|
||||
|
||||
type userEventResolver struct {
|
||||
event.Resolver
|
||||
}
|
||||
|
||||
// userIDToName convert user id to user name
|
||||
func userIDToName(userID string) string {
|
||||
id, err := strconv.ParseInt(userID, 10, 32)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse userID: %v to int", userID)
|
||||
return ""
|
||||
}
|
||||
// use different context to so that the user is visible before the transaction is committed
|
||||
user, err := pkgUser.Mgr.Get(orm.Context(), int(id))
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse userID: %v to int, err %v", userID, err)
|
||||
return ""
|
||||
}
|
||||
return user.Username
|
||||
}
|
59
src/pkg/auditext/event/utils.go
Normal file
59
src/pkg/auditext/event/utils.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
)
|
||||
|
||||
// Redact replaces sensitive attributes in the JSON payload with "***"
|
||||
func Redact(payload string, sensitiveAttributes []string) string {
|
||||
if len(payload) == 0 {
|
||||
return ""
|
||||
}
|
||||
var jsonData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(payload), &jsonData); err != nil {
|
||||
log.Fatalf("Error parsing JSON: %v", err)
|
||||
return ""
|
||||
}
|
||||
replacePassword(jsonData, sensitiveAttributes)
|
||||
// Convert the modified map back to JSON
|
||||
modifiedJSON, err := json.MarshalIndent(jsonData, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("Error converting to JSON: %v", err)
|
||||
return ""
|
||||
}
|
||||
return string(modifiedJSON)
|
||||
}
|
||||
|
||||
// replacePassword recursively replaces attribute in maskAttributes's value with "***"
|
||||
func replacePassword(data map[string]interface{}, maskAttributes []string) {
|
||||
for key, value := range data {
|
||||
if slices.Contains(maskAttributes, key) {
|
||||
data[key] = "***"
|
||||
} else if nestedMap, ok := value.(map[string]interface{}); ok {
|
||||
replacePassword(nestedMap, maskAttributes)
|
||||
} else if nestedArray, ok := value.([]interface{}); ok {
|
||||
for _, item := range nestedArray {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
replacePassword(itemMap, maskAttributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
68
src/pkg/auditext/event/utils_test.go
Normal file
68
src/pkg/auditext/event/utils_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedact(t *testing.T) {
|
||||
type args struct {
|
||||
payload string
|
||||
sensitiveAttributes []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"normal case", args{`{"password":"123456"}`, []string{"password"}}, "{\n \"password\": \"***\"\n}"},
|
||||
{"no sensitive case", args{`{"ldap_base_dn":"dc=example,dc=com"}`, []string{"password"}}, "{\n \"ldap_base_dn\": \"dc=example,dc=com\"\n}"},
|
||||
{"empty case", args{"", []string{"password"}}, ""},
|
||||
{"mixed attribute", args{`{"ldap_base_dn":"dc=example,dc=com", "ldap_search_passwd": "admin"}`, []string{"ldap_search_passwd"}}, "{\n \"ldap_base_dn\": \"dc=example,dc=com\",\n \"ldap_search_passwd\": \"***\"\n}"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Redact(tt.args.payload, tt.args.sensitiveAttributes); got != tt.want {
|
||||
t.Errorf("Redact() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_replacePassword(t *testing.T) {
|
||||
type args struct {
|
||||
data map[string]interface{}
|
||||
maskAttributes []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{"normal case", args{map[string]interface{}{"password": "123456"}, []string{"password"}}},
|
||||
{"nested case", args{map[string]interface{}{"master": map[string]interface{}{"password": "123456"}}, []string{"password"}}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
replacePassword(tt.args.data, tt.args.maskAttributes)
|
||||
if val, ok := tt.args.data["password"]; ok && val != nil && tt.args.data["password"] != "***" {
|
||||
t.Errorf("replacePassword() = %v, want %v", tt.args.data["password"], "***")
|
||||
}
|
||||
if val, ok := tt.args.data["master"]; ok && val != nil && tt.args.data["master"].(map[string]interface{})["password"] != "***" {
|
||||
t.Errorf("replacePassword() = %v, want %v", tt.args.data["master"].(map[string]interface{})["password"], "***")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user