Skip to content

Commit

Permalink
Add user event and config event
Browse files Browse the repository at this point in the history
Signed-off-by: stonezdj <[email protected]>
  • Loading branch information
stonezdj committed Feb 7, 2025
1 parent 28896b1 commit 0b1fe4b
Show file tree
Hide file tree
Showing 9 changed files with 647 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
157 changes: 157 additions & 0 deletions src/pkg/auditext/event/basic.go
Original file line number Diff line number Diff line change
@@ -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 (
create = "create"
update = "update"
delete = "delete"
resourceIDPattern = `^%v/(\d+)$`
)

// ResolveIDToNameFunc is the function to resolve the resource name from resource id
type ResolveIDToNameFunc func(string) string

type EventResolver 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
HasResourceName 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 *EventResolver) 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.HasResourceName {
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 *EventResolver) 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 create:
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.HasResourceName && resourceName != "" {
evt.ResourceName = resourceName
}

case delete:
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.HasResourceName && ce.ResourceName != "" {
evt.ResourceName = ce.ResourceName
}

case update:
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.HasResourceName && 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 create
case http.MethodDelete:
return delete
case http.MethodPut:
return update
}
return ""
}
66 changes: 66 additions & 0 deletions src/pkg/auditext/event/basic_test.go
Original file line number Diff line number Diff line change
@@ -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
HasResourceName 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}, HasResourceName: 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}, HasResourceName: 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}, HasResourceName: true}, args{context.Background(), "/api/v2.0/tests/234", "GET"}, true, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &EventResolver{
BaseURLPattern: tt.fields.BaseURLPattern,
ResourceType: tt.fields.ResourceType,
SucceedCodes: tt.fields.SucceedCodes,
SensitiveAttributes: tt.fields.SensitiveAttributes,
HasResourceName: tt.fields.HasResourceName,
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 changes: 71 additions & 0 deletions src/pkg/auditext/event/config/config.go
Original file line number Diff line number Diff line change
@@ -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"},
}
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, url 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())), ""
}
Loading

0 comments on commit 0b1fe4b

Please sign in to comment.