mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat(enterprise): add auditing to SCIM (#13614)
This commit is contained in:
@ -15,6 +15,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
agpl "github.com/coder/coder/v2/coderd"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
@ -118,6 +119,11 @@ type SCIMUser struct {
|
||||
} `json:"meta"`
|
||||
}
|
||||
|
||||
var SCIMAuditAdditionalFields = map[string]string{
|
||||
"automatic_actor": "coder",
|
||||
"automatic_subsystem": "scim",
|
||||
}
|
||||
|
||||
// scimPostUser creates a new user, or returns the existing user if it exists.
|
||||
//
|
||||
// @Summary SCIM 2.0: Create new user
|
||||
@ -135,6 +141,16 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
auditor := *api.AGPL.Auditor.Load()
|
||||
aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
|
||||
Audit: auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionCreate,
|
||||
AdditionalFields: SCIMAuditAdditionalFields,
|
||||
})
|
||||
defer commitAudit()
|
||||
|
||||
var sUser SCIMUser
|
||||
err := json.NewDecoder(r.Body).Decode(&sUser)
|
||||
if err != nil {
|
||||
@ -170,7 +186,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if sUser.Active && dbUser.Status == database.UserStatusSuspended {
|
||||
//nolint:gocritic
|
||||
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
|
||||
newUser, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
|
||||
ID: dbUser.ID,
|
||||
// The user will get transitioned to Active after logging in.
|
||||
Status: database.UserStatusDormant,
|
||||
@ -180,8 +196,13 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = handlerutil.WriteError(rw, err)
|
||||
return
|
||||
}
|
||||
aReq.New = newUser
|
||||
} else {
|
||||
aReq.New = dbUser
|
||||
}
|
||||
|
||||
aReq.Old = dbUser
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, sUser)
|
||||
return
|
||||
}
|
||||
@ -223,6 +244,8 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = handlerutil.WriteError(rw, err)
|
||||
return
|
||||
}
|
||||
aReq.New = dbUser
|
||||
aReq.UserID = dbUser.ID
|
||||
|
||||
sUser.ID = dbUser.ID.String()
|
||||
sUser.UserName = dbUser.Username
|
||||
@ -248,6 +271,15 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
auditor := *api.AGPL.Auditor.Load()
|
||||
aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
|
||||
Audit: auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
})
|
||||
defer commitAudit()
|
||||
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var sUser SCIMUser
|
||||
@ -270,6 +302,8 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = handlerutil.WriteError(rw, err)
|
||||
return
|
||||
}
|
||||
aReq.Old = dbUser
|
||||
aReq.UserID = dbUser.ID
|
||||
|
||||
var status database.UserStatus
|
||||
if sUser.Active {
|
||||
@ -280,7 +314,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
//nolint:gocritic // needed for SCIM
|
||||
_, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
|
||||
userNew, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
|
||||
ID: dbUser.ID,
|
||||
Status: status,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
@ -289,6 +323,7 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
|
||||
_ = handlerutil.WriteError(rw, err)
|
||||
return
|
||||
}
|
||||
aReq.New = userNew
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, sUser)
|
||||
}
|
||||
|
@ -11,6 +11,9 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/coder/v2/enterprise/coderd"
|
||||
@ -109,21 +112,34 @@ func TestScim(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
scimAPIKey := []byte("hi")
|
||||
mockAudit := audit.NewMock()
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
SCIMAPIKey: scimAPIKey,
|
||||
Options: &coderdtest.Options{Auditor: mockAudit},
|
||||
SCIMAPIKey: scimAPIKey,
|
||||
AuditLogging: true,
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
AccountID: "coolin",
|
||||
Features: license.Features{
|
||||
codersdk.FeatureSCIM: 1,
|
||||
codersdk.FeatureSCIM: 1,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
mockAudit.ResetLogs()
|
||||
|
||||
sUser := makeScimUser(t)
|
||||
res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
aLogs := mockAudit.AuditLogs()
|
||||
require.Len(t, aLogs, 1)
|
||||
af := map[string]string{}
|
||||
err = json.Unmarshal([]byte(aLogs[0].AdditionalFields), &af)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderd.SCIMAuditAdditionalFields, af)
|
||||
assert.Equal(t, database.AuditActionCreate, aLogs[0].Action)
|
||||
|
||||
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
|
||||
require.NoError(t, err)
|
||||
@ -306,21 +322,27 @@ func TestScim(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
scimAPIKey := []byte("hi")
|
||||
mockAudit := audit.NewMock()
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
SCIMAPIKey: scimAPIKey,
|
||||
Options: &coderdtest.Options{Auditor: mockAudit},
|
||||
SCIMAPIKey: scimAPIKey,
|
||||
AuditLogging: true,
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
AccountID: "coolin",
|
||||
Features: license.Features{
|
||||
codersdk.FeatureSCIM: 1,
|
||||
codersdk.FeatureSCIM: 1,
|
||||
codersdk.FeatureAuditLog: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
mockAudit.ResetLogs()
|
||||
|
||||
sUser := makeScimUser(t)
|
||||
res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
mockAudit.ResetLogs()
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&sUser)
|
||||
require.NoError(t, err)
|
||||
@ -333,6 +355,10 @@ func TestScim(t *testing.T) {
|
||||
_ = res.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
aLogs := mockAudit.AuditLogs()
|
||||
require.Len(t, aLogs, 1)
|
||||
assert.Equal(t, database.AuditActionWrite, aLogs[0].Action)
|
||||
|
||||
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, userRes.Users, 1)
|
||||
|
Reference in New Issue
Block a user