Skip to content

Commit 00860cf

Browse files
authored
feat: add group mapping option for group sync (coder#6705)
* feat: add group mapping option for group sync * fixup! feat: add group mapping option for group sync
1 parent 120bc4b commit 00860cf

File tree

12 files changed

+114
-18
lines changed

12 files changed

+114
-18
lines changed

cli/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ flags, and YAML configuration. The precedence is as follows:
796796
AllowSignups: cfg.OIDC.AllowSignups.Value(),
797797
UsernameField: cfg.OIDC.UsernameField.String(),
798798
GroupField: cfg.OIDC.GroupField.String(),
799+
GroupMapping: cfg.OIDC.GroupMapping.Value,
799800
SignInText: cfg.OIDC.SignInText.String(),
800801
IconURL: cfg.OIDC.IconURL.String(),
801802
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(),

coderd/apidoc/docs.go

Lines changed: 3 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: 3 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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,12 @@ func New(options *Options) *API {
223223
options.SSHConfig.HostnamePrefix = "coder."
224224
}
225225
if options.SetUserGroups == nil {
226-
options.SetUserGroups = func(context.Context, database.Store, uuid.UUID, []string) error { return nil }
226+
options.SetUserGroups = func(ctx context.Context, _ database.Store, id uuid.UUID, groups []string) error {
227+
options.Logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
228+
slog.F("id", id), slog.F("groups", groups),
229+
)
230+
return nil
231+
}
227232
}
228233
if options.TemplateScheduleStore == nil {
229234
options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()

coderd/userauth.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,10 @@ type OIDCConfig struct {
481481
// groups. If the group field is the empty string, then no group updates
482482
// will ever come from the OIDC provider.
483483
GroupField string
484+
// GroupMapping controls how groups returned by the OIDC provider get mapped
485+
// to groups within Coder.
486+
// map[oidcGroupName]coderGroupName
487+
GroupMapping map[string]string
484488
// SignInText is the text to display on the OIDC login button
485489
SignInText string
486490
// IconURL points to the URL of an icon to display on the OIDC login button
@@ -651,6 +655,11 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
651655
})
652656
return
653657
}
658+
659+
if mappedGroup, ok := api.OIDCConfig.GroupMapping[group]; ok {
660+
group = mappedGroup
661+
}
662+
654663
groups = append(groups, group)
655664
}
656665
} else {

codersdk/deployment.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"flag"
7-
"fmt"
87
"math"
98
"net/http"
109
"os"
@@ -199,7 +198,7 @@ func ParseSSHConfigOption(opt string) (key string, value string, err error) {
199198
return r == ' ' || r == '='
200199
})
201200
if idx == -1 {
202-
return "", "", fmt.Errorf("invalid config-ssh option %q", opt)
201+
return "", "", xerrors.Errorf("invalid config-ssh option %q", opt)
203202
}
204203
return opt[:idx], opt[idx+1:], nil
205204
}
@@ -248,17 +247,18 @@ type OAuth2GithubConfig struct {
248247
}
249248

250249
type OIDCConfig struct {
251-
AllowSignups clibase.Bool `json:"allow_signups" typescript:",notnull"`
252-
ClientID clibase.String `json:"client_id" typescript:",notnull"`
253-
ClientSecret clibase.String `json:"client_secret" typescript:",notnull"`
254-
EmailDomain clibase.Strings `json:"email_domain" typescript:",notnull"`
255-
IssuerURL clibase.String `json:"issuer_url" typescript:",notnull"`
256-
Scopes clibase.Strings `json:"scopes" typescript:",notnull"`
257-
IgnoreEmailVerified clibase.Bool `json:"ignore_email_verified" typescript:",notnull"`
258-
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
259-
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
260-
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
261-
IconURL clibase.URL `json:"icon_url" typescript:",notnull"`
250+
AllowSignups clibase.Bool `json:"allow_signups" typescript:",notnull"`
251+
ClientID clibase.String `json:"client_id" typescript:",notnull"`
252+
ClientSecret clibase.String `json:"client_secret" typescript:",notnull"`
253+
EmailDomain clibase.Strings `json:"email_domain" typescript:",notnull"`
254+
IssuerURL clibase.String `json:"issuer_url" typescript:",notnull"`
255+
Scopes clibase.Strings `json:"scopes" typescript:",notnull"`
256+
IgnoreEmailVerified clibase.Bool `json:"ignore_email_verified" typescript:",notnull"`
257+
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
258+
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
259+
GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
260+
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
261+
IconURL clibase.URL `json:"icon_url" typescript:",notnull"`
262262
}
263263

264264
type TelemetryConfig struct {
@@ -875,6 +875,16 @@ when required by your organization's security policy.`,
875875
Group: &deploymentGroupOIDC,
876876
YAML: "groupField",
877877
},
878+
{
879+
Name: "OIDC Group Mapping",
880+
Description: "A map of OIDC group IDs and the group in Coder it should map to. This is useful for when OIDC providers only return group IDs.",
881+
Flag: "oidc-group-mapping",
882+
Env: "OIDC_GROUP_MAPPING",
883+
Default: "{}",
884+
Value: &c.OIDC.GroupMapping,
885+
Group: &deploymentGroupOIDC,
886+
YAML: "groupMapping",
887+
},
878888
{
879889
Name: "OpenID Connect sign in text",
880890
Description: "The text to show on the OpenID Connect sign in button",

docs/admin/auth.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,20 @@ CODER_OIDC_SCOPES=openid,profile,email,groups
197197
On login, users will automatically be assigned to groups that have matching
198198
names in Coder and removed from groups that the user no longer belongs to.
199199

200+
For cases when an OIDC provider only returns group IDs ([Azure AD][azure-gids])
201+
or you want to have different group names in Coder than in your OIDC provider,
202+
you can configure mapping between the two.
203+
204+
```console
205+
# as an environment variable
206+
CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}'
207+
# as a flag
208+
--oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}'
209+
```
210+
211+
From the example above, users that belong to the `myOIDCGroupID` group in your
212+
OIDC provider will be added to the `myCoderGroupName` group in Coder.
213+
200214
> **Note:** Groups are only updated on login.
215+
216+
[azure-gids]: https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195

docs/api/general.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
234234
"client_id": "string",
235235
"client_secret": "string",
236236
"email_domain": ["string"],
237+
"group_mapping": {},
237238
"groups_field": "string",
238239
"icon_url": {
239240
"forceQuery": true,

docs/api/schemas.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,6 +1766,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
17661766
"client_id": "string",
17671767
"client_secret": "string",
17681768
"email_domain": ["string"],
1769+
"group_mapping": {},
17691770
"groups_field": "string",
17701771
"icon_url": {
17711772
"forceQuery": true,
@@ -2110,6 +2111,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
21102111
"client_id": "string",
21112112
"client_secret": "string",
21122113
"email_domain": ["string"],
2114+
"group_mapping": {},
21132115
"groups_field": "string",
21142116
"icon_url": {
21152117
"forceQuery": true,
@@ -2771,6 +2773,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
27712773
"client_id": "string",
27722774
"client_secret": "string",
27732775
"email_domain": ["string"],
2776+
"group_mapping": {},
27742777
"groups_field": "string",
27752778
"icon_url": {
27762779
"forceQuery": true,
@@ -2801,6 +2804,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
28012804
| `client_id` | string | false | | |
28022805
| `client_secret` | string | false | | |
28032806
| `email_domain` | array of string | false | | |
2807+
| `group_mapping` | object | false | | |
28042808
| `groups_field` | string | false | | |
28052809
| `icon_url` | [clibase.URL](#clibaseurl) | false | | |
28062810
| `ignore_email_verified` | boolean | false | | |

enterprise/coderd/userauth_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,51 @@ func TestUserOIDC(t *testing.T) {
6767
require.NoError(t, err)
6868
require.Len(t, group.Members, 1)
6969
})
70+
t.Run("AssignsMapped", func(t *testing.T) {
71+
t.Parallel()
72+
73+
ctx, _ := testutil.Context(t)
74+
conf := coderdtest.NewOIDCConfig(t, "")
75+
76+
oidcGroupName := "pingpong"
77+
coderGroupName := "bingbong"
78+
79+
config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
80+
cfg.GroupMapping = map[string]string{oidcGroupName: coderGroupName}
81+
})
82+
config.AllowSignups = true
83+
84+
client := coderdenttest.New(t, &coderdenttest.Options{
85+
Options: &coderdtest.Options{
86+
OIDCConfig: config,
87+
},
88+
})
89+
_ = coderdtest.CreateFirstUser(t, client)
90+
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
91+
AllFeatures: true,
92+
})
93+
94+
admin, err := client.User(ctx, "me")
95+
require.NoError(t, err)
96+
require.Len(t, admin.OrganizationIDs, 1)
97+
98+
group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{
99+
Name: coderGroupName,
100+
})
101+
require.NoError(t, err)
102+
require.Len(t, group.Members, 0)
103+
104+
resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
105+
"email": "colin@coder.com",
106+
"groups": []string{oidcGroupName},
107+
}))
108+
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
109+
110+
group, err = client.Group(ctx, group.ID)
111+
require.NoError(t, err)
112+
require.Len(t, group.Members, 1)
113+
})
114+
70115
t.Run("AddThenRemove", func(t *testing.T) {
71116
t.Parallel()
72117

go.sum

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,10 +376,6 @@ github.com/coder/retry v1.3.1-0.20230210155434-e90a2e1e091d h1:09JG37IgTB6n3ouX9
376376
github.com/coder/retry v1.3.1-0.20230210155434-e90a2e1e091d/go.mod h1:r+1J5i/989wt6CUeNSuvFKKA9hHuKKPMxdzDbTuvwwk=
377377
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 h1:tN5GKFT68YLVzJoA8AHuiMNJ0qlhoD3pGN3JY9gxSko=
378378
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
379-
github.com/coder/tailscale v1.1.1-0.20230314023417-d9efcc0ac972 h1:193YGsJz8hc4yxqAclE36paKl+9CQ6KGLgdleIguCVE=
380-
github.com/coder/tailscale v1.1.1-0.20230314023417-d9efcc0ac972/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
381-
github.com/coder/tailscale v1.1.1-0.20230321164649-3362540e3026 h1:6YnWw08eQEGc/7KyweGWP8urOb9TDlo6S35ZqNm8qsQ=
382-
github.com/coder/tailscale v1.1.1-0.20230321164649-3362540e3026/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
383379
github.com/coder/tailscale v1.1.1-0.20230321171725-fed359a0cafa h1:EjRGgTz7BUECmbV8jHTi1/rKdDjJESGSlm1Jp7evvCQ=
384380
github.com/coder/tailscale v1.1.1-0.20230321171725-fed359a0cafa/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
385381
github.com/coder/terraform-provider-coder v0.6.20 h1:bVyITX9JlbnGzKzTj0qi/JziUCGqD2DiN3cXaWyDcxE=

site/src/api/typesGenerated.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,9 @@ export interface OIDCConfig {
507507
readonly ignore_email_verified: boolean
508508
readonly username_field: string
509509
readonly groups_field: string
510+
// Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
511+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
512+
readonly group_mapping: any
510513
readonly sign_in_text: string
511514
readonly icon_url: string
512515
}

0 commit comments

Comments
 (0)