Skip to content

Commit f827829

Browse files
authored
feat: synchronize oidc user roles (coder#8595)
* feat: oidc user role sync User roles come from oidc claims. Prevent manual user role changes if set. * allow mapping 1:many
1 parent 94541d2 commit f827829

38 files changed

+596
-46
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Generated files
22
coderd/apidoc/docs.go linguist-generated=true
3+
docs/api/*.md linguist-generated=true
4+
docs/cli/*.md linguist-generated=true
35
coderd/apidoc/swagger.json linguist-generated=true
46
coderd/database/dump.sql linguist-generated=true
57
peerbroker/proto/*.go linguist-generated=true
@@ -9,3 +11,4 @@ provisionersdk/proto/*.go linguist-generated=true
911
*.tfstate.json linguist-generated=true
1012
*.tfstate.dot linguist-generated=true
1113
*.tfplan.dot linguist-generated=true
14+

cli/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
596596
IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(),
597597
GroupField: cfg.OIDC.GroupField.String(),
598598
GroupMapping: cfg.OIDC.GroupMapping.Value,
599+
UserRoleField: cfg.OIDC.UserRoleField.String(),
600+
UserRoleMapping: cfg.OIDC.UserRoleMapping.Value,
601+
UserRolesDefault: cfg.OIDC.UserRolesDefault.GetSlice(),
599602
SignInText: cfg.OIDC.SignInText.String(),
600603
IconURL: cfg.OIDC.IconURL.String(),
601604
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(),

cli/server_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,8 @@ func TestServer(t *testing.T) {
10951095
require.False(t, deploymentConfig.Values.OIDC.IgnoreUserInfo.Value())
10961096
require.Empty(t, deploymentConfig.Values.OIDC.GroupField.Value())
10971097
require.Empty(t, deploymentConfig.Values.OIDC.GroupMapping.Value)
1098+
require.Empty(t, deploymentConfig.Values.OIDC.UserRoleField.Value())
1099+
require.Empty(t, deploymentConfig.Values.OIDC.UserRoleMapping.Value)
10981100
require.Equal(t, "OpenID Connect", deploymentConfig.Values.OIDC.SignInText.Value())
10991101
require.Empty(t, deploymentConfig.Values.OIDC.IconURL.Value())
11001102
})

cli/testdata/coder_server_--help.golden

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,20 @@ can safely ignore these settings.
337337
--oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email)
338338
Scopes to grant when authenticating with OIDC.
339339

340+
--oidc-user-role-default string-array, $CODER_OIDC_USER_ROLE_DEFAULT
341+
If user role sync is enabled, these roles are always included for all
342+
authenticated users. The 'member' role is always assigned.
343+
344+
--oidc-user-role-field string, $CODER_OIDC_USER_ROLE_FIELD
345+
This field must be set if using the user roles sync feature. Set this
346+
to the name of the claim used to store the user's role. The roles
347+
should be sent as an array of strings.
348+
349+
--oidc-user-role-mapping struct[map[string][]string], $CODER_OIDC_USER_ROLE_MAPPING (default: {})
350+
A map of the OIDC passed in user roles and the groups in Coder it
351+
should map to. This is useful if the group names do not match. If
352+
mapped to the empty string, the role will ignored.
353+
340354
--oidc-username-field string, $CODER_OIDC_USERNAME_FIELD (default: preferred_username)
341355
OIDC claim field to use as the username.
342356

cli/testdata/coder_users_list_--output_json.golden

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"display_name": "Owner"
1616
}
1717
],
18-
"avatar_url": ""
18+
"avatar_url": "",
19+
"login_type": "password"
1920
},
2021
{
2122
"id": "[second user ID]",
@@ -28,6 +29,7 @@
2829
"[first org ID]"
2930
],
3031
"roles": [],
31-
"avatar_url": ""
32+
"avatar_url": "",
33+
"login_type": "password"
3234
}
3335
]

cli/testdata/server-config.yaml.golden

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,20 @@ oidc:
268268
# for when OIDC providers only return group IDs.
269269
# (default: {}, type: struct[map[string]string])
270270
groupMapping: {}
271+
# This field must be set if using the user roles sync feature. Set this to the
272+
# name of the claim used to store the user's role. The roles should be sent as an
273+
# array of strings.
274+
# (default: <unset>, type: string)
275+
userRoleField: ""
276+
# A map of the OIDC passed in user roles and the groups in Coder it should map to.
277+
# This is useful if the group names do not match. If mapped to the empty string,
278+
# the role will ignored.
279+
# (default: {}, type: struct[map[string][]string])
280+
userRoleMapping: {}
281+
# If user role sync is enabled, these roles are always included for all
282+
# authenticated users. The 'member' role is always assigned.
283+
# (default: <unset>, type: string-array)
284+
userRoleDefault: []
271285
# The text to show on the OpenID Connect sign in button.
272286
# (default: OpenID Connect, type: string)
273287
signInText: OpenID Connect

coderd/apidoc/docs.go

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ type Options struct {
124124
DERPMap *tailcfg.DERPMap
125125
SwaggerEndpoint bool
126126
SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
127+
SetUserSiteRoles func(ctx context.Context, tx database.Store, userID uuid.UUID, roles []string) error
127128
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
128129
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
129130
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
@@ -258,6 +259,14 @@ func New(options *Options) *API {
258259
return nil
259260
}
260261
}
262+
if options.SetUserSiteRoles == nil {
263+
options.SetUserSiteRoles = func(ctx context.Context, _ database.Store, userID uuid.UUID, roles []string) error {
264+
options.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
265+
slog.F("user_id", userID), slog.F("roles", roles),
266+
)
267+
return nil
268+
}
269+
}
261270
if options.TemplateScheduleStore == nil {
262271
options.TemplateScheduleStore = &atomic.Pointer[schedule.TemplateScheduleStore]{}
263272
}

coderd/database/db2sdk/db2sdk.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
115115
OrganizationIDs: organizationIDs,
116116
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
117117
AvatarURL: user.AvatarURL.String,
118+
LoginType: codersdk.LoginType(user.LoginType),
118119
}
119120

120121
for _, roleName := range user.RBACRoles {

0 commit comments

Comments
 (0)