Skip to content

Commit d7eadee

Browse files
authored
chore: insert audit log entries for organization CRUD (#13660)
* chore: insert audit log entries for organization CRUD
1 parent 9c1a6a2 commit d7eadee

File tree

6 files changed

+71
-9
lines changed

6 files changed

+71
-9
lines changed

coderd/audit/diff.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ type Auditable interface {
2323
database.OAuth2ProviderApp |
2424
database.OAuth2ProviderAppSecret |
2525
database.CustomRole |
26-
database.AuditableOrganizationMember
26+
database.AuditableOrganizationMember |
27+
database.Organization
2728
}
2829

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

coderd/audit/request.go

+8
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ func ResourceTarget[T Auditable](tgt T) string {
107107
return typed.Name
108108
case database.AuditableOrganizationMember:
109109
return typed.Username
110+
case database.Organization:
111+
return typed.Name
110112
default:
111113
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
112114
}
@@ -148,6 +150,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
148150
return typed.ID
149151
case database.AuditableOrganizationMember:
150152
return typed.UserID
153+
case database.Organization:
154+
return typed.ID
151155
default:
152156
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
153157
}
@@ -187,6 +191,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
187191
return database.ResourceTypeCustomRole
188192
case database.AuditableOrganizationMember:
189193
return database.ResourceTypeOrganizationMember
194+
case database.Organization:
195+
return database.ResourceTypeOrganization
190196
default:
191197
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
192198
}
@@ -227,6 +233,8 @@ func ResourceRequiresOrgID[T Auditable]() bool {
227233
return true
228234
case database.AuditableOrganizationMember:
229235
return true
236+
case database.Organization:
237+
return true
230238
default:
231239
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
232240
}

coderd/organizations.go

+49-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/google/uuid"
1010
"golang.org/x/xerrors"
1111

12+
"github.com/coder/coder/v2/coderd/audit"
1213
"github.com/coder/coder/v2/coderd/database"
1314
"github.com/coder/coder/v2/coderd/database/dbtime"
1415
"github.com/coder/coder/v2/coderd/httpapi"
@@ -41,8 +42,22 @@ func (*API) organization(rw http.ResponseWriter, r *http.Request) {
4142
// @Success 201 {object} codersdk.Organization
4243
// @Router /organizations [post]
4344
func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
44-
ctx := r.Context()
45-
apiKey := httpmw.APIKey(r)
45+
var (
46+
// organizationID is required before the audit log entry is created.
47+
organizationID = uuid.New()
48+
ctx = r.Context()
49+
apiKey = httpmw.APIKey(r)
50+
auditor = api.Auditor.Load()
51+
aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
52+
Audit: *auditor,
53+
Log: api.Logger,
54+
Request: r,
55+
Action: database.AuditActionCreate,
56+
OrganizationID: organizationID,
57+
})
58+
)
59+
aReq.Old = database.Organization{}
60+
defer commitAudit()
4661

4762
var req codersdk.CreateOrganizationRequest
4863
if !httpapi.Read(ctx, rw, r, &req) {
@@ -78,7 +93,7 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
7893
}
7994

8095
organization, err = tx.InsertOrganization(ctx, database.InsertOrganizationParams{
81-
ID: uuid.New(),
96+
ID: organizationID,
8297
Name: req.Name,
8398
DisplayName: req.DisplayName,
8499
Description: req.Description,
@@ -119,6 +134,7 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
119134
return
120135
}
121136

137+
aReq.New = organization
122138
httpapi.Write(ctx, rw, http.StatusCreated, convertOrganization(organization))
123139
}
124140

@@ -133,8 +149,20 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
133149
// @Success 200 {object} codersdk.Organization
134150
// @Router /organizations/{organization} [patch]
135151
func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) {
136-
ctx := r.Context()
137-
organization := httpmw.OrganizationParam(r)
152+
var (
153+
ctx = r.Context()
154+
organization = httpmw.OrganizationParam(r)
155+
auditor = api.Auditor.Load()
156+
aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
157+
Audit: *auditor,
158+
Log: api.Logger,
159+
Request: r,
160+
Action: database.AuditActionWrite,
161+
OrganizationID: organization.ID,
162+
})
163+
)
164+
aReq.Old = organization
165+
defer commitAudit()
138166

139167
var req codersdk.UpdateOrganizationRequest
140168
if !httpapi.Read(ctx, rw, r, &req) {
@@ -208,6 +236,7 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) {
208236
return
209237
}
210238

239+
aReq.New = organization
211240
httpapi.Write(ctx, rw, http.StatusOK, convertOrganization(organization))
212241
}
213242

@@ -220,8 +249,20 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) {
220249
// @Success 200 {object} codersdk.Response
221250
// @Router /organizations/{organization} [delete]
222251
func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) {
223-
ctx := r.Context()
224-
organization := httpmw.OrganizationParam(r)
252+
var (
253+
ctx = r.Context()
254+
organization = httpmw.OrganizationParam(r)
255+
auditor = api.Auditor.Load()
256+
aReq, commitAudit = audit.InitRequest[database.Organization](rw, &audit.RequestParams{
257+
Audit: *auditor,
258+
Log: api.Logger,
259+
Request: r,
260+
Action: database.AuditActionDelete,
261+
OrganizationID: organization.ID,
262+
})
263+
)
264+
aReq.Old = organization
265+
defer commitAudit()
225266

226267
if organization.IsDefault {
227268
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -239,6 +280,7 @@ func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) {
239280
return
240281
}
241282

283+
aReq.New = database.Organization{}
242284
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
243285
Message: "Organization has been deleted.",
244286
})

coderd/users_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ func TestPostUsers(t *testing.T) {
525525

526526
require.Len(t, auditor.AuditLogs(), numLogs)
527527
require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action)
528-
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action)
528+
require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-3].Action)
529529

530530
require.Len(t, user.OrganizationIDs, 1)
531531
assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])

docs/admin/audit-logs.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ We track the following resources:
2020
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
2121
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>callback_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
2222
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
23+
| Organization<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>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
2324
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
2425
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
2526
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |

enterprise/audit/table.go

+10
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ var auditableResourcesTypes = map[any]map[string]Action{
254254
"app_id": ActionIgnore,
255255
"secret_prefix": ActionIgnore,
256256
},
257+
&database.Organization{}: {
258+
"id": ActionIgnore,
259+
"name": ActionTrack,
260+
"description": ActionTrack,
261+
"created_at": ActionIgnore,
262+
"updated_at": ActionTrack,
263+
"is_default": ActionTrack,
264+
"display_name": ActionTrack,
265+
"icon": ActionTrack,
266+
},
257267
}
258268

259269
// auditMap converts a map of struct pointers to a map of struct names as

0 commit comments

Comments
 (0)