feat: implement SCIM handler for SCIM 2.0 compliance (#25572)

Rewrites the SCIM 2.0 user provisioning handler to be RFC 7644
compliant. Verified against an external IdP Okta.

Behavior is OPT IN
This commit is contained in:
Steven Masley
2026-05-28 10:00:37 -05:00
committed by GitHub
parent 6df1536256
commit 4591212482
26 changed files with 2664 additions and 1091 deletions
+31 -1
View File
@@ -742,6 +742,29 @@ var (
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
subjectSCIM = rbac.Subject{
Type: rbac.SubjectTypeSCIMProvisioner,
FriendlyName: "SCIM Provisioner",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "scim"},
DisplayName: "SCIM",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceSystem.Type: {policy.ActionRead}, // Required for idp config reads, this should be fixed
rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(),
rbac.ResourceAssignOrgRole.Type: rbac.ResourceAssignOrgRole.AvailableActions(),
rbac.ResourceUser.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionRead, policy.ActionUpdatePersonal},
rbac.ResourceOrganization.Type: {policy.ActionRead},
rbac.ResourceOrganizationMember.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionUpdate},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
)
// AsProvisionerd returns a context with an actor that has permissions required
@@ -872,6 +895,12 @@ func AsAIProviderMetadataReader(ctx context.Context) context.Context {
return As(ctx, subjectAIProviderMetadataReader)
}
// AsSCIMProvisioner returns a context with an actor that has permissions required for
// handling the /scim/v2 routes and provisioning users via SCIM.
func AsSCIMProvisioner(ctx context.Context) context.Context {
return As(ctx, subjectSCIM)
}
var AsRemoveActor = rbac.Subject{
ID: "remove-actor",
}
@@ -4659,7 +4688,8 @@ func (q *querier) GetUserCodeDiffDisplayMode(ctx context.Context, userID uuid.UU
}
func (q *querier) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
// If you can read every user, then you can read the count of users.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil {
return 0, err
}
return q.db.GetUserCount(ctx, includeSystem)
+1 -1
View File
@@ -4578,7 +4578,7 @@ func (s *MethodTestSuite) TestSystemFunctions() {
}))
s.Run("GetUserCount", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().GetUserCount(gomock.Any(), false).Return(int64(0), nil).AnyTimes()
check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0))
check.Args(false).Asserts(rbac.ResourceUser, policy.ActionRead).Returns(int64(0))
}))
s.Run("GetTemplates", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().GetTemplates(gomock.Any()).Return([]database.Template{}, nil).AnyTimes()