Skip to content

Commit 65b9f9b

Browse files
authored
chore: audit organization member add/delete/edit (coder#13620)
* chore: audit organization member add/removals
1 parent 9463973 commit 65b9f9b

File tree

11 files changed

+86
-14
lines changed

11 files changed

+86
-14
lines changed

coderd/audit/diff.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ type Auditable interface {
2222
database.HealthSettings |
2323
database.OAuth2ProviderApp |
2424
database.OAuth2ProviderAppSecret |
25-
database.CustomRole
25+
database.CustomRole |
26+
database.AuditableOrganizationMember
2627
}
2728

2829
// Map is a map of changed fields in an audited resource. It maps field names to

coderd/audit/request.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ func ResourceTarget[T Auditable](tgt T) string {
105105
return typed.DisplaySecret
106106
case database.CustomRole:
107107
return typed.Name
108+
case database.AuditableOrganizationMember:
109+
return typed.Username
108110
default:
109111
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
110112
}
@@ -144,6 +146,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
144146
return typed.ID
145147
case database.CustomRole:
146148
return typed.ID
149+
case database.AuditableOrganizationMember:
150+
return typed.UserID
147151
default:
148152
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
149153
}
@@ -181,6 +185,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
181185
return database.ResourceTypeOauth2ProviderAppSecret
182186
case database.CustomRole:
183187
return database.ResourceTypeCustomRole
188+
case database.AuditableOrganizationMember:
189+
return database.ResourceTypeOrganizationMember
184190
default:
185191
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
186192
}
@@ -219,6 +225,8 @@ func ResourceRequiresOrgID[T Auditable]() bool {
219225
return false
220226
case database.CustomRole:
221227
return true
228+
case database.AuditableOrganizationMember:
229+
return true
222230
default:
223231
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
224232
}

coderd/database/dump.sql

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/migrations/000220_audit_org_member.down.sql

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'organization_member';

coderd/database/modelmethods.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ func (s WorkspaceAgentStatus) Valid() bool {
6060
}
6161
}
6262

63+
type AuditableOrganizationMember struct {
64+
OrganizationMember
65+
Username string `json:"username"`
66+
}
67+
68+
func (m OrganizationMember) Auditable(username string) AuditableOrganizationMember {
69+
return AuditableOrganizationMember{
70+
OrganizationMember: m,
71+
Username: username,
72+
}
73+
}
74+
6375
type AuditableGroup struct {
6476
Group
6577
Members []GroupMember `json:"members"`

coderd/database/models.go

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/members.go

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/google/uuid"
88
"golang.org/x/xerrors"
99

10+
"github.com/coder/coder/v2/coderd/audit"
1011
"github.com/coder/coder/v2/coderd/database"
1112
"github.com/coder/coder/v2/coderd/database/db2sdk"
1213
"github.com/coder/coder/v2/coderd/database/dbtime"
@@ -27,10 +28,19 @@ import (
2728
// @Router /organizations/{organization}/members/{user} [post]
2829
func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request) {
2930
var (
30-
ctx = r.Context()
31-
organization = httpmw.OrganizationParam(r)
32-
user = httpmw.UserParam(r)
31+
ctx = r.Context()
32+
organization = httpmw.OrganizationParam(r)
33+
user = httpmw.UserParam(r)
34+
auditor = api.Auditor.Load()
35+
aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{
36+
Audit: *auditor,
37+
Log: api.Logger,
38+
Request: r,
39+
Action: database.AuditActionCreate,
40+
})
3341
)
42+
aReq.Old = database.AuditableOrganizationMember{}
43+
defer commitAudit()
3444

3545
member, err := api.Database.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
3646
OrganizationID: organization.ID,
@@ -54,6 +64,7 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request)
5464
return
5565
}
5666

67+
aReq.New = member.Auditable(user.Username)
5768
resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{member})
5869
if err != nil {
5970
httpapi.InternalServerError(rw, err)
@@ -79,10 +90,19 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request)
7990
// @Router /organizations/{organization}/members/{user} [delete]
8091
func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request) {
8192
var (
82-
ctx = r.Context()
83-
organization = httpmw.OrganizationParam(r)
84-
member = httpmw.OrganizationMemberParam(r)
93+
ctx = r.Context()
94+
organization = httpmw.OrganizationParam(r)
95+
member = httpmw.OrganizationMemberParam(r)
96+
auditor = api.Auditor.Load()
97+
aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{
98+
Audit: *auditor,
99+
Log: api.Logger,
100+
Request: r,
101+
Action: database.AuditActionDelete,
102+
})
85103
)
104+
aReq.Old = member.OrganizationMember.Auditable(member.Username)
105+
defer commitAudit()
86106

87107
err := api.Database.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{
88108
OrganizationID: organization.ID,
@@ -97,6 +117,7 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request
97117
return
98118
}
99119

120+
aReq.New = database.AuditableOrganizationMember{}
100121
httpapi.Write(ctx, rw, http.StatusOK, "organization member removed")
101122
}
102123

@@ -149,13 +170,22 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) {
149170
// @Router /organizations/{organization}/members/{user}/roles [put]
150171
func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
151172
var (
152-
ctx = r.Context()
153-
organization = httpmw.OrganizationParam(r)
154-
member = httpmw.OrganizationMemberParam(r)
155-
apiKey = httpmw.APIKey(r)
173+
ctx = r.Context()
174+
organization = httpmw.OrganizationParam(r)
175+
member = httpmw.OrganizationMemberParam(r)
176+
apiKey = httpmw.APIKey(r)
177+
auditor = api.Auditor.Load()
178+
aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{
179+
Audit: *auditor,
180+
Log: api.Logger,
181+
Request: r,
182+
Action: database.AuditActionWrite,
183+
})
156184
)
185+
aReq.Old = member.OrganizationMember.Auditable(member.Username)
186+
defer commitAudit()
157187

158-
if apiKey.UserID == member.UserID {
188+
if apiKey.UserID == member.OrganizationMember.UserID {
159189
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
160190
Message: "You cannot change your own organization roles.",
161191
})
@@ -182,6 +212,10 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
182212
})
183213
return
184214
}
215+
aReq.New = database.AuditableOrganizationMember{
216+
OrganizationMember: updatedUser,
217+
Username: member.Username,
218+
}
185219

186220
resp, err := convertOrganizationMembers(ctx, api.Database, []database.OrganizationMember{updatedUser})
187221
if err != nil {

codersdk/audit.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const (
3131
// nolint:gosec // This is not a secret.
3232
ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret"
3333
ResourceTypeCustomRole ResourceType = "custom_role"
34+
ResourceTypeOrganizationMember = "organization_member"
3435
)
3536

3637
func (r ResourceType) FriendlyString() string {
@@ -69,6 +70,8 @@ func (r ResourceType) FriendlyString() string {
6970
return "oauth2 app secret"
7071
case ResourceTypeCustomRole:
7172
return "custom role"
73+
case ResourceTypeOrganizationMember:
74+
return "organization member"
7275
default:
7376
return "unknown"
7477
}

docs/admin/audit-logs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ We track the following resources:
1313
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
1414
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
1515
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
16+
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>true</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
1617
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>true</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
1718
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
1819
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |

enterprise/audit/table.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ type Table map[string]map[string]Action
5050
var AuditableResources = auditMap(auditableResourcesTypes)
5151

5252
var auditableResourcesTypes = map[any]map[string]Action{
53+
&database.AuditableOrganizationMember{}: {
54+
"username": ActionTrack,
55+
"user_id": ActionTrack,
56+
"organization_id": ActionTrack,
57+
"created_at": ActionTrack,
58+
"updated_at": ActionTrack,
59+
"roles": ActionTrack,
60+
},
5361
&database.CustomRole{}: {
5462
"name": ActionTrack,
5563
"display_name": ActionTrack,

0 commit comments

Comments
 (0)