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

Skip to content

Commit 514d5b5

Browse files
committed
feat(enterprise): add auditing to SCIM
1 parent a1db6d8 commit 514d5b5

File tree

7 files changed

+138
-18
lines changed

7 files changed

+138
-18
lines changed

coderd/audit/request.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"strconv"
1212

13+
"github.com/davecgh/go-spew/spew"
1314
"github.com/google/uuid"
1415
"github.com/sqlc-dev/pqtype"
1516
"go.opentelemetry.io/otel/baggage"
@@ -31,7 +32,7 @@ type RequestParams struct {
3132
OrganizationID uuid.UUID
3233
Request *http.Request
3334
Action database.AuditAction
34-
AdditionalFields json.RawMessage
35+
AdditionalFields interface{}
3536
}
3637

3738
type Request[T Auditable] struct {
@@ -274,6 +275,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
274275
if sw.Status < 400 &&
275276
req.params.Action != database.AuditActionLogin && req.params.Action != database.AuditActionLogout {
276277
diff := Diff(p.Audit, req.Old, req.New)
278+
fmt.Println("DIFFF", diff)
277279

278280
var err error
279281
diffRaw, err = json.Marshal(diff)
@@ -283,8 +285,15 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
283285
}
284286
}
285287

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

290299
var userID uuid.UUID
@@ -319,9 +328,11 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
319328
Diff: diffRaw,
320329
StatusCode: int32(sw.Status),
321330
RequestID: httpmw.RequestID(p.Request),
322-
AdditionalFields: p.AdditionalFields,
331+
AdditionalFields: additionalFieldsRaw,
323332
OrganizationID: requireOrgID[T](logCtx, p.OrganizationID, p.Log),
324333
}
334+
fmt.Println("export")
335+
spew.Dump(auditLog)
325336
err := p.Audit.Export(ctx, auditLog)
326337
if err != nil {
327338
p.Log.Error(logCtx, "export audit log",

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

+39-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ 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"
2122
"github.com/coder/coder/v2/coderd/httpapi"
2223
"github.com/coder/coder/v2/codersdk"
2324
)
2425

26+
var SCIMAuditUserID = uuid.MustParse("1f688bd1-8d6a-4a17-ae93-d0761f3b0a09")
27+
2528
func (api *API) scimEnabledMW(next http.Handler) http.Handler {
2629
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
2730
api.entitlementsMu.RLock()
@@ -118,6 +121,11 @@ type SCIMUser struct {
118121
} `json:"meta"`
119122
}
120123

124+
var SCIMAuditAdditionalFields = map[string]string{
125+
"automatic_actor": "coder",
126+
"automatic_subsystem": "scim",
127+
}
128+
121129
// scimPostUser creates a new user, or returns the existing user if it exists.
122130
//
123131
// @Summary SCIM 2.0: Create new user
@@ -135,6 +143,16 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
135143
return
136144
}
137145

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

171189
if sUser.Active && dbUser.Status == database.UserStatusSuspended {
172190
//nolint:gocritic
173-
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
191+
newUser, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
174192
ID: dbUser.ID,
175193
// The user will get transitioned to Active after logging in.
176194
Status: database.UserStatusDormant,
@@ -180,8 +198,13 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
180198
_ = handlerutil.WriteError(rw, err)
181199
return
182200
}
201+
aReq.New = newUser
202+
} else {
203+
aReq.New = dbUser
183204
}
184205

206+
aReq.Old = dbUser
207+
185208
httpapi.Write(ctx, rw, http.StatusOK, sUser)
186209
return
187210
}
@@ -223,6 +246,8 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
223246
_ = handlerutil.WriteError(rw, err)
224247
return
225248
}
249+
aReq.New = dbUser
250+
aReq.UserID = dbUser.ID
226251

227252
sUser.ID = dbUser.ID.String()
228253
sUser.UserName = dbUser.Username
@@ -248,6 +273,15 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
248273
return
249274
}
250275

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

253287
var sUser SCIMUser
@@ -270,6 +304,8 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
270304
_ = handlerutil.WriteError(rw, err)
271305
return
272306
}
307+
aReq.Old = dbUser
308+
aReq.UserID = dbUser.ID
273309

274310
var status database.UserStatus
275311
if sUser.Active {
@@ -280,7 +316,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
280316
}
281317

282318
//nolint:gocritic // needed for SCIM
283-
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
319+
userNew, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
284320
ID: dbUser.ID,
285321
Status: status,
286322
UpdatedAt: dbtime.Now(),
@@ -289,6 +325,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
289325
_ = handlerutil.WriteError(rw, err)
290326
return
291327
}
328+
aReq.New = userNew
292329

293330
httpapi.Write(ctx, rw, http.StatusOK, sUser)
294331
}

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}", "");

site/src/testHelpers/entities.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2285,6 +2285,7 @@ export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
22852285
budget: 100,
22862286
};
22872287

2288+
22882289
export const MockGroup: TypesGen.Group = {
22892290
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
22902291
name: "Front-End",

0 commit comments

Comments
 (0)