Skip to content

Commit 65b9f9b

Browse files
authored
chore: audit organization member add/delete/edit (#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> |

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy