Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 889daf2

Browse files
authored
feat(enterprise): add auditing to SCIM (#13614)
1 parent c4656d7 commit 889daf2

File tree

6 files changed

+131
-18
lines changed

6 files changed

+131
-18
lines changed

coderd/audit/request.go

+11-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type RequestParams struct {
3131
OrganizationID uuid.UUID
3232
Request *http.Request
3333
Action database.AuditAction
34-
AdditionalFields json.RawMessage
34+
AdditionalFields interface{}
3535
}
3636

3737
type Request[T Auditable] struct {
@@ -283,8 +283,15 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
283283
}
284284
}
285285

286-
if p.AdditionalFields == nil {
287-
p.AdditionalFields = json.RawMessage("{}")
286+
additionalFieldsRaw := json.RawMessage("{}")
287+
288+
if p.AdditionalFields != nil {
289+
data, err := json.Marshal(p.AdditionalFields)
290+
if err != nil {
291+
p.Log.Warn(logCtx, "marshal additional fields", slog.Error(err))
292+
} else {
293+
additionalFieldsRaw = json.RawMessage(data)
294+
}
288295
}
289296

290297
var userID uuid.UUID
@@ -319,7 +326,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
319326
Diff: diffRaw,
320327
StatusCode: int32(sw.Status),
321328
RequestID: httpmw.RequestID(p.Request),
322-
AdditionalFields: p.AdditionalFields,
329+
AdditionalFields: additionalFieldsRaw,
323330
OrganizationID: requireOrgID[T](logCtx, p.OrganizationID, p.Log),
324331
}
325332
err := p.Audit.Export(ctx, auditLog)

coderd/workspaces.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -361,17 +361,12 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
361361
}
362362
)
363363

364-
wriBytes, err := json.Marshal(workspaceResourceInfo)
365-
if err != nil {
366-
api.Logger.Warn(ctx, "marshal workspace owner name")
367-
}
368-
369364
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
370365
Audit: *auditor,
371366
Log: api.Logger,
372367
Request: r,
373368
Action: database.AuditActionCreate,
374-
AdditionalFields: wriBytes,
369+
AdditionalFields: workspaceResourceInfo,
375370
OrganizationID: organization.ID,
376371
})
377372

enterprise/coderd/scim.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"golang.org/x/xerrors"
1616

1717
agpl "github.com/coder/coder/v2/coderd"
18+
"github.com/coder/coder/v2/coderd/audit"
1819
"github.com/coder/coder/v2/coderd/database"
1920
"github.com/coder/coder/v2/coderd/database/dbauthz"
2021
"github.com/coder/coder/v2/coderd/database/dbtime"
@@ -118,6 +119,11 @@ type SCIMUser struct {
118119
} `json:"meta"`
119120
}
120121

122+
var SCIMAuditAdditionalFields = map[string]string{
123+
"automatic_actor": "coder",
124+
"automatic_subsystem": "scim",
125+
}
126+
121127
// scimPostUser creates a new user, or returns the existing user if it exists.
122128
//
123129
// @Summary SCIM 2.0: Create new user
@@ -135,6 +141,16 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
135141
return
136142
}
137143

144+
auditor := *api.AGPL.Auditor.Load()
145+
aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
146+
Audit: auditor,
147+
Log: api.Logger,
148+
Request: r,
149+
Action: database.AuditActionCreate,
150+
AdditionalFields: SCIMAuditAdditionalFields,
151+
})
152+
defer commitAudit()
153+
138154
var sUser SCIMUser
139155
err := json.NewDecoder(r.Body).Decode(&sUser)
140156
if err != nil {
@@ -170,7 +186,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
170186

171187
if sUser.Active && dbUser.Status == database.UserStatusSuspended {
172188
//nolint:gocritic
173-
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
189+
newUser, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
174190
ID: dbUser.ID,
175191
// The user will get transitioned to Active after logging in.
176192
Status: database.UserStatusDormant,
@@ -180,8 +196,13 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
180196
_ = handlerutil.WriteError(rw, err)
181197
return
182198
}
199+
aReq.New = newUser
200+
} else {
201+
aReq.New = dbUser
183202
}
184203

204+
aReq.Old = dbUser
205+
185206
httpapi.Write(ctx, rw, http.StatusOK, sUser)
186207
return
187208
}
@@ -223,6 +244,8 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
223244
_ = handlerutil.WriteError(rw, err)
224245
return
225246
}
247+
aReq.New = dbUser
248+
aReq.UserID = dbUser.ID
226249

227250
sUser.ID = dbUser.ID.String()
228251
sUser.UserName = dbUser.Username
@@ -248,6 +271,15 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
248271
return
249272
}
250273

274+
auditor := *api.AGPL.Auditor.Load()
275+
aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
276+
Audit: auditor,
277+
Log: api.Logger,
278+
Request: r,
279+
Action: database.AuditActionWrite,
280+
})
281+
defer commitAudit()
282+
251283
id := chi.URLParam(r, "id")
252284

253285
var sUser SCIMUser
@@ -270,6 +302,8 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
270302
_ = handlerutil.WriteError(rw, err)
271303
return
272304
}
305+
aReq.Old = dbUser
306+
aReq.UserID = dbUser.ID
273307

274308
var status database.UserStatus
275309
if sUser.Active {
@@ -280,7 +314,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
280314
}
281315

282316
//nolint:gocritic // needed for SCIM
283-
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
317+
userNew, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
284318
ID: dbUser.ID,
285319
Status: status,
286320
UpdatedAt: dbtime.Now(),
@@ -289,6 +323,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
289323
_ = handlerutil.WriteError(rw, err)
290324
return
291325
}
326+
aReq.New = userNew
292327

293328
httpapi.Write(ctx, rw, http.StatusOK, sUser)
294329
}

enterprise/coderd/scim_test.go

+31-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313

14+
"github.com/coder/coder/v2/coderd/audit"
15+
"github.com/coder/coder/v2/coderd/coderdtest"
16+
"github.com/coder/coder/v2/coderd/database"
1417
"github.com/coder/coder/v2/codersdk"
1518
"github.com/coder/coder/v2/cryptorand"
1619
"github.com/coder/coder/v2/enterprise/coderd"
@@ -109,21 +112,34 @@ func TestScim(t *testing.T) {
109112
defer cancel()
110113

111114
scimAPIKey := []byte("hi")
115+
mockAudit := audit.NewMock()
112116
client, _ := coderdenttest.New(t, &coderdenttest.Options{
113-
SCIMAPIKey: scimAPIKey,
117+
Options: &coderdtest.Options{Auditor: mockAudit},
118+
SCIMAPIKey: scimAPIKey,
119+
AuditLogging: true,
114120
LicenseOptions: &coderdenttest.LicenseOptions{
115121
AccountID: "coolin",
116122
Features: license.Features{
117-
codersdk.FeatureSCIM: 1,
123+
codersdk.FeatureSCIM: 1,
124+
codersdk.FeatureAuditLog: 1,
118125
},
119126
},
120127
})
128+
mockAudit.ResetLogs()
121129

122130
sUser := makeScimUser(t)
123131
res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
124132
require.NoError(t, err)
125133
defer res.Body.Close()
126-
assert.Equal(t, http.StatusOK, res.StatusCode)
134+
require.Equal(t, http.StatusOK, res.StatusCode)
135+
136+
aLogs := mockAudit.AuditLogs()
137+
require.Len(t, aLogs, 1)
138+
af := map[string]string{}
139+
err = json.Unmarshal([]byte(aLogs[0].AdditionalFields), &af)
140+
require.NoError(t, err)
141+
assert.Equal(t, coderd.SCIMAuditAdditionalFields, af)
142+
assert.Equal(t, database.AuditActionCreate, aLogs[0].Action)
127143

128144
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
129145
require.NoError(t, err)
@@ -306,21 +322,27 @@ func TestScim(t *testing.T) {
306322
defer cancel()
307323

308324
scimAPIKey := []byte("hi")
325+
mockAudit := audit.NewMock()
309326
client, _ := coderdenttest.New(t, &coderdenttest.Options{
310-
SCIMAPIKey: scimAPIKey,
327+
Options: &coderdtest.Options{Auditor: mockAudit},
328+
SCIMAPIKey: scimAPIKey,
329+
AuditLogging: true,
311330
LicenseOptions: &coderdenttest.LicenseOptions{
312331
AccountID: "coolin",
313332
Features: license.Features{
314-
codersdk.FeatureSCIM: 1,
333+
codersdk.FeatureSCIM: 1,
334+
codersdk.FeatureAuditLog: 1,
315335
},
316336
},
317337
})
338+
mockAudit.ResetLogs()
318339

319340
sUser := makeScimUser(t)
320341
res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
321342
require.NoError(t, err)
322343
defer res.Body.Close()
323344
assert.Equal(t, http.StatusOK, res.StatusCode)
345+
mockAudit.ResetLogs()
324346

325347
err = json.NewDecoder(res.Body).Decode(&sUser)
326348
require.NoError(t, err)
@@ -333,6 +355,10 @@ func TestScim(t *testing.T) {
333355
_ = res.Body.Close()
334356
assert.Equal(t, http.StatusOK, res.StatusCode)
335357

358+
aLogs := mockAudit.AuditLogs()
359+
require.Len(t, aLogs, 1)
360+
assert.Equal(t, database.AuditActionWrite, aLogs[0].Action)
361+
336362
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
337363
require.NoError(t, err)
338364
require.Len(t, userRes.Users, 1)

site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.stories.tsx

+42
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,45 @@ export const UnsuccessfulLoginForUnknownUser: Story = {
5656
auditLog: MockAuditLogUnsuccessfulLoginKnownUser,
5757
},
5858
};
59+
60+
export const CreateUser: Story = {
61+
args: {
62+
auditLog: {
63+
...MockAuditLog,
64+
resource_type: "user",
65+
resource_target: "colin",
66+
description: "{user} created user {target}",
67+
},
68+
},
69+
};
70+
71+
export const SCIMCreateUser: Story = {
72+
args: {
73+
auditLog: {
74+
...MockAuditLog,
75+
resource_type: "user",
76+
resource_target: "colin",
77+
description: "{user} created user {target}",
78+
additional_fields: {
79+
automatic_actor: "coder",
80+
automatic_subsystem: "scim",
81+
},
82+
},
83+
},
84+
};
85+
86+
export const SCIMUpdateUser: Story = {
87+
args: {
88+
auditLog: {
89+
...MockAuditLog,
90+
action: "write",
91+
resource_type: "user",
92+
resource_target: "colin",
93+
description: "{user} updated user {target}",
94+
additional_fields: {
95+
automatic_actor: "coder",
96+
automatic_subsystem: "scim",
97+
},
98+
},
99+
},
100+
};

site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const AuditLogDescription: FC<AuditLogDescriptionProps> = ({
1212
auditLog,
1313
}) => {
1414
let target = auditLog.resource_target.trim();
15-
const user = auditLog.user?.username.trim();
15+
let user = auditLog.user?.username.trim();
1616

1717
if (auditLog.resource_type === "workspace_build") {
1818
return <BuildAuditDescription auditLog={auditLog} />;
@@ -23,6 +23,14 @@ export const AuditLogDescription: FC<AuditLogDescriptionProps> = ({
2323
target = "";
2424
}
2525

26+
// This occurs when SCIM creates a user.
27+
if (
28+
auditLog.resource_type === "user" &&
29+
auditLog.additional_fields?.automatic_actor === "coder"
30+
) {
31+
user = "Coder automatically";
32+
}
33+
2634
const truncatedDescription = auditLog.description
2735
.replace("{user}", `${user}`)
2836
.replace("{target}", "");

0 commit comments

Comments
 (0)