Skip to content

Commit c146fe2

Browse files
committed
feat(audit): add member create/update/delete audit events
1 parent 18f7abd commit c146fe2

4 files changed

Lines changed: 459 additions & 0 deletions

File tree

src/core/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import (
6868
"github.com/goharbor/harbor/src/pkg/audit"
6969
_ "github.com/goharbor/harbor/src/pkg/auditext/event/config"
7070
_ "github.com/goharbor/harbor/src/pkg/auditext/event/login"
71+
_ "github.com/goharbor/harbor/src/pkg/auditext/event/member"
7172
_ "github.com/goharbor/harbor/src/pkg/auditext/event/user"
7273
dbCfg "github.com/goharbor/harbor/src/pkg/config/db"
7374
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright Project Harbor Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package member
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"net/http"
21+
"regexp"
22+
"strconv"
23+
"strings"
24+
"time"
25+
26+
"github.com/goharbor/harbor/src/common"
27+
"github.com/goharbor/harbor/src/common/rbac"
28+
ctlevent "github.com/goharbor/harbor/src/controller/event"
29+
"github.com/goharbor/harbor/src/controller/event/metadata/commonevent"
30+
"github.com/goharbor/harbor/src/controller/event/model"
31+
"github.com/goharbor/harbor/src/lib/config"
32+
"github.com/goharbor/harbor/src/lib/log"
33+
"github.com/goharbor/harbor/src/lib/orm"
34+
"github.com/goharbor/harbor/src/pkg"
35+
ext "github.com/goharbor/harbor/src/pkg/auditext/event"
36+
pkgMember "github.com/goharbor/harbor/src/pkg/member"
37+
"github.com/goharbor/harbor/src/pkg/notifier/event"
38+
)
39+
40+
const (
41+
memberCreatePattern = `/api/v2\.0/projects/[^/]+/members$`
42+
memberActionPattern = `/api/v2\.0/projects/[^/]+/members/\d+$`
43+
extractPattern = `/api/v2\.0/projects/([^/]+)/members(?:/(\d+))?$`
44+
)
45+
46+
var extractRe = regexp.MustCompile(extractPattern)
47+
48+
// overridable for testing
49+
var (
50+
lookupMemberFn = lookupMember
51+
resolveProjectFn = resolveProject
52+
)
53+
54+
// custom resolver for project member events, extracts project id and user/group ids
55+
func init() {
56+
r := &resolver{}
57+
commonevent.RegisterResolver(memberCreatePattern, r)
58+
commonevent.RegisterResolver(memberActionPattern, r)
59+
}
60+
61+
type resolver struct{}
62+
63+
func (r *resolver) PreCheck(ctx context.Context, url string, method string) (bool, string) {
64+
operation := ext.MethodToOperation(method)
65+
if len(operation) == 0 {
66+
return false, ""
67+
}
68+
if !config.AuditLogEventEnabled(ctx, fmt.Sprintf("%v_%v", operation, rbac.ResourceMember.String())) {
69+
return false, ""
70+
}
71+
// for DELETE, resolve member info before the resource is deleted
72+
if method == http.MethodDelete {
73+
if m := extractRe.FindStringSubmatch(url); len(m) >= 3 && len(m[2]) > 0 {
74+
name, typ := lookupMemberFn(m[1], m[2])
75+
if len(typ) > 0 {
76+
return true, typ + ":" + name
77+
}
78+
return true, name
79+
}
80+
}
81+
return true, ""
82+
}
83+
84+
func (r *resolver) Resolve(ce *commonevent.Metadata, evt *event.Event) error {
85+
operation := ext.MethodToOperation(ce.RequestMethod)
86+
if len(operation) == 0 {
87+
return nil
88+
}
89+
matches := extractRe.FindStringSubmatch(ce.RequestURL)
90+
if len(matches) < 2 {
91+
return nil
92+
}
93+
94+
projectID, projectName := resolveProjectFn(matches[1])
95+
96+
e := &model.CommonEvent{
97+
Operator: ce.Username,
98+
ResourceType: rbac.ResourceMember.String(),
99+
Operation: operation,
100+
ProjectID: projectID,
101+
OcurrAt: time.Now(),
102+
IsSuccessful: true,
103+
}
104+
105+
var entityName, entityType string
106+
switch operation {
107+
case "create":
108+
e.IsSuccessful = ce.ResponseCode == http.StatusCreated
109+
if m := extractRe.FindStringSubmatch(ce.ResponseLocation); len(m) >= 3 && len(m[2]) > 0 {
110+
entityName, entityType = lookupMemberFn(m[1], m[2])
111+
}
112+
case "delete":
113+
e.IsSuccessful = ce.ResponseCode == http.StatusOK
114+
entityName, entityType = parsePreResolved(ce.ResourceName)
115+
case "update":
116+
e.IsSuccessful = ce.ResponseCode == http.StatusOK
117+
if len(matches) >= 3 && len(matches[2]) > 0 {
118+
entityName, entityType = lookupMemberFn(matches[1], matches[2])
119+
}
120+
}
121+
122+
e.ResourceName = entityName
123+
label := "member"
124+
noun := ""
125+
if entityType == common.GroupMember {
126+
label = "group"
127+
noun = " member"
128+
} else if entityType == common.UserMember {
129+
label = "user"
130+
noun = " member"
131+
}
132+
preposition := "in"
133+
if operation == "delete" {
134+
preposition = "from"
135+
}
136+
e.OperationDescription = fmt.Sprintf("%s %s%s %s %s project %s",
137+
operation, label, noun, entityName, preposition, projectName)
138+
139+
evt.Topic = ctlevent.TopicCommonEvent
140+
evt.Data = e
141+
return nil
142+
}
143+
144+
func lookupMember(projectNameOrID, memberIDStr string) (string, string) {
145+
ctx := orm.Context()
146+
projectID, _ := resolveProjectFn(projectNameOrID)
147+
if projectID == 0 {
148+
return memberIDStr, ""
149+
}
150+
memberID, err := strconv.Atoi(memberIDStr)
151+
if err != nil {
152+
return memberIDStr, ""
153+
}
154+
m, err := pkgMember.Mgr.Get(ctx, projectID, memberID)
155+
if err != nil {
156+
log.Errorf("failed to get member %d in project %d: %v", memberID, projectID, err)
157+
return memberIDStr, ""
158+
}
159+
return m.Entityname, m.EntityType
160+
}
161+
162+
// resolveProject resolves a project name or ID string to (projectID, projectName).
163+
func resolveProject(projectNameOrID string) (int64, string) {
164+
ctx := orm.Context()
165+
if id, err := strconv.ParseInt(projectNameOrID, 10, 64); err == nil {
166+
if p, err := pkg.ProjectMgr.Get(ctx, id); err == nil && p != nil {
167+
return p.ProjectID, p.Name
168+
}
169+
}
170+
p, err := pkg.ProjectMgr.Get(ctx, projectNameOrID)
171+
if err != nil {
172+
log.Errorf("failed to resolve project %s: %v", projectNameOrID, err)
173+
return 0, projectNameOrID
174+
}
175+
return p.ProjectID, p.Name
176+
}
177+
178+
func parsePreResolved(info string) (string, string) {
179+
if parts := strings.SplitN(info, ":", 2); len(parts) == 2 {
180+
return parts[1], parts[0]
181+
}
182+
return info, ""
183+
}

0 commit comments

Comments
 (0)