mirror of https://github.com/coder/coder.git
feat: add group mapping option for group sync (#6705)
* feat: add group mapping option for group sync * fixup! feat: add group mapping option for group sync
This commit is contained in:
parent
120bc4b750
commit
00860cf1c8
|
@ -796,6 +796,7 @@ flags, and YAML configuration. The precedence is as follows:
|
|||
AllowSignups: cfg.OIDC.AllowSignups.Value(),
|
||||
UsernameField: cfg.OIDC.UsernameField.String(),
|
||||
GroupField: cfg.OIDC.GroupField.String(),
|
||||
GroupMapping: cfg.OIDC.GroupMapping.Value,
|
||||
SignInText: cfg.OIDC.SignInText.String(),
|
||||
IconURL: cfg.OIDC.IconURL.String(),
|
||||
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(),
|
||||
|
|
|
@ -7138,6 +7138,9 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"group_mapping": {
|
||||
"type": "object"
|
||||
},
|
||||
"groups_field": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
@ -6392,6 +6392,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"group_mapping": {
|
||||
"type": "object"
|
||||
},
|
||||
"groups_field": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
@ -223,7 +223,12 @@ func New(options *Options) *API {
|
|||
options.SSHConfig.HostnamePrefix = "coder."
|
||||
}
|
||||
if options.SetUserGroups == nil {
|
||||
options.SetUserGroups = func(context.Context, database.Store, uuid.UUID, []string) error { return nil }
|
||||
options.SetUserGroups = func(ctx context.Context, _ database.Store, id uuid.UUID, groups []string) error {
|
||||
options.Logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
|
||||
slog.F("id", id), slog.F("groups", groups),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if options.TemplateScheduleStore == nil {
|
||||
options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()
|
||||
|
|
|
@ -481,6 +481,10 @@ type OIDCConfig struct {
|
|||
// groups. If the group field is the empty string, then no group updates
|
||||
// will ever come from the OIDC provider.
|
||||
GroupField string
|
||||
// GroupMapping controls how groups returned by the OIDC provider get mapped
|
||||
// to groups within Coder.
|
||||
// map[oidcGroupName]coderGroupName
|
||||
GroupMapping map[string]string
|
||||
// SignInText is the text to display on the OIDC login button
|
||||
SignInText string
|
||||
// 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) {
|
|||
})
|
||||
return
|
||||
}
|
||||
|
||||
if mappedGroup, ok := api.OIDCConfig.GroupMapping[group]; ok {
|
||||
group = mappedGroup
|
||||
}
|
||||
|
||||
groups = append(groups, group)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -199,7 +198,7 @@ func ParseSSHConfigOption(opt string) (key string, value string, err error) {
|
|||
return r == ' ' || r == '='
|
||||
})
|
||||
if idx == -1 {
|
||||
return "", "", fmt.Errorf("invalid config-ssh option %q", opt)
|
||||
return "", "", xerrors.Errorf("invalid config-ssh option %q", opt)
|
||||
}
|
||||
return opt[:idx], opt[idx+1:], nil
|
||||
}
|
||||
|
@ -248,17 +247,18 @@ type OAuth2GithubConfig struct {
|
|||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
AllowSignups clibase.Bool `json:"allow_signups" typescript:",notnull"`
|
||||
ClientID clibase.String `json:"client_id" typescript:",notnull"`
|
||||
ClientSecret clibase.String `json:"client_secret" typescript:",notnull"`
|
||||
EmailDomain clibase.Strings `json:"email_domain" typescript:",notnull"`
|
||||
IssuerURL clibase.String `json:"issuer_url" typescript:",notnull"`
|
||||
Scopes clibase.Strings `json:"scopes" typescript:",notnull"`
|
||||
IgnoreEmailVerified clibase.Bool `json:"ignore_email_verified" typescript:",notnull"`
|
||||
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
|
||||
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
|
||||
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
|
||||
IconURL clibase.URL `json:"icon_url" typescript:",notnull"`
|
||||
AllowSignups clibase.Bool `json:"allow_signups" typescript:",notnull"`
|
||||
ClientID clibase.String `json:"client_id" typescript:",notnull"`
|
||||
ClientSecret clibase.String `json:"client_secret" typescript:",notnull"`
|
||||
EmailDomain clibase.Strings `json:"email_domain" typescript:",notnull"`
|
||||
IssuerURL clibase.String `json:"issuer_url" typescript:",notnull"`
|
||||
Scopes clibase.Strings `json:"scopes" typescript:",notnull"`
|
||||
IgnoreEmailVerified clibase.Bool `json:"ignore_email_verified" typescript:",notnull"`
|
||||
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
|
||||
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
|
||||
GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
|
||||
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
|
||||
IconURL clibase.URL `json:"icon_url" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type TelemetryConfig struct {
|
||||
|
@ -875,6 +875,16 @@ when required by your organization's security policy.`,
|
|||
Group: &deploymentGroupOIDC,
|
||||
YAML: "groupField",
|
||||
},
|
||||
{
|
||||
Name: "OIDC Group Mapping",
|
||||
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.",
|
||||
Flag: "oidc-group-mapping",
|
||||
Env: "OIDC_GROUP_MAPPING",
|
||||
Default: "{}",
|
||||
Value: &c.OIDC.GroupMapping,
|
||||
Group: &deploymentGroupOIDC,
|
||||
YAML: "groupMapping",
|
||||
},
|
||||
{
|
||||
Name: "OpenID Connect sign in text",
|
||||
Description: "The text to show on the OpenID Connect sign in button",
|
||||
|
|
|
@ -197,4 +197,20 @@ CODER_OIDC_SCOPES=openid,profile,email,groups
|
|||
On login, users will automatically be assigned to groups that have matching
|
||||
names in Coder and removed from groups that the user no longer belongs to.
|
||||
|
||||
For cases when an OIDC provider only returns group IDs ([Azure AD][azure-gids])
|
||||
or you want to have different group names in Coder than in your OIDC provider,
|
||||
you can configure mapping between the two.
|
||||
|
||||
```console
|
||||
# as an environment variable
|
||||
CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}'
|
||||
# as a flag
|
||||
--oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}'
|
||||
```
|
||||
|
||||
From the example above, users that belong to the `myOIDCGroupID` group in your
|
||||
OIDC provider will be added to the `myCoderGroupName` group in Coder.
|
||||
|
||||
> **Note:** Groups are only updated on login.
|
||||
|
||||
[azure-gids]: https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195
|
||||
|
|
|
@ -234,6 +234,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
|||
"client_id": "string",
|
||||
"client_secret": "string",
|
||||
"email_domain": ["string"],
|
||||
"group_mapping": {},
|
||||
"groups_field": "string",
|
||||
"icon_url": {
|
||||
"forceQuery": true,
|
||||
|
|
|
@ -1766,6 +1766,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"client_id": "string",
|
||||
"client_secret": "string",
|
||||
"email_domain": ["string"],
|
||||
"group_mapping": {},
|
||||
"groups_field": "string",
|
||||
"icon_url": {
|
||||
"forceQuery": true,
|
||||
|
@ -2110,6 +2111,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"client_id": "string",
|
||||
"client_secret": "string",
|
||||
"email_domain": ["string"],
|
||||
"group_mapping": {},
|
||||
"groups_field": "string",
|
||||
"icon_url": {
|
||||
"forceQuery": true,
|
||||
|
@ -2771,6 +2773,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"client_id": "string",
|
||||
"client_secret": "string",
|
||||
"email_domain": ["string"],
|
||||
"group_mapping": {},
|
||||
"groups_field": "string",
|
||||
"icon_url": {
|
||||
"forceQuery": true,
|
||||
|
@ -2801,6 +2804,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
| `client_id` | string | false | | |
|
||||
| `client_secret` | string | false | | |
|
||||
| `email_domain` | array of string | false | | |
|
||||
| `group_mapping` | object | false | | |
|
||||
| `groups_field` | string | false | | |
|
||||
| `icon_url` | [clibase.URL](#clibaseurl) | false | | |
|
||||
| `ignore_email_verified` | boolean | false | | |
|
||||
|
|
|
@ -67,6 +67,51 @@ func TestUserOIDC(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, group.Members, 1)
|
||||
})
|
||||
t.Run("AssignsMapped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, _ := testutil.Context(t)
|
||||
conf := coderdtest.NewOIDCConfig(t, "")
|
||||
|
||||
oidcGroupName := "pingpong"
|
||||
coderGroupName := "bingbong"
|
||||
|
||||
config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
|
||||
cfg.GroupMapping = map[string]string{oidcGroupName: coderGroupName}
|
||||
})
|
||||
config.AllowSignups = true
|
||||
|
||||
client := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
OIDCConfig: config,
|
||||
},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
|
||||
AllFeatures: true,
|
||||
})
|
||||
|
||||
admin, err := client.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, admin.OrganizationIDs, 1)
|
||||
|
||||
group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{
|
||||
Name: coderGroupName,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, group.Members, 0)
|
||||
|
||||
resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
|
||||
"email": "colin@coder.com",
|
||||
"groups": []string{oidcGroupName},
|
||||
}))
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
||||
group, err = client.Group(ctx, group.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, group.Members, 1)
|
||||
})
|
||||
|
||||
t.Run("AddThenRemove", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
4
go.sum
4
go.sum
|
@ -376,10 +376,6 @@ github.com/coder/retry v1.3.1-0.20230210155434-e90a2e1e091d h1:09JG37IgTB6n3ouX9
|
|||
github.com/coder/retry v1.3.1-0.20230210155434-e90a2e1e091d/go.mod h1:r+1J5i/989wt6CUeNSuvFKKA9hHuKKPMxdzDbTuvwwk=
|
||||
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 h1:tN5GKFT68YLVzJoA8AHuiMNJ0qlhoD3pGN3JY9gxSko=
|
||||
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
|
||||
github.com/coder/tailscale v1.1.1-0.20230314023417-d9efcc0ac972 h1:193YGsJz8hc4yxqAclE36paKl+9CQ6KGLgdleIguCVE=
|
||||
github.com/coder/tailscale v1.1.1-0.20230314023417-d9efcc0ac972/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
|
||||
github.com/coder/tailscale v1.1.1-0.20230321164649-3362540e3026 h1:6YnWw08eQEGc/7KyweGWP8urOb9TDlo6S35ZqNm8qsQ=
|
||||
github.com/coder/tailscale v1.1.1-0.20230321164649-3362540e3026/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
|
||||
github.com/coder/tailscale v1.1.1-0.20230321171725-fed359a0cafa h1:EjRGgTz7BUECmbV8jHTi1/rKdDjJESGSlm1Jp7evvCQ=
|
||||
github.com/coder/tailscale v1.1.1-0.20230321171725-fed359a0cafa/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
|
||||
github.com/coder/terraform-provider-coder v0.6.20 h1:bVyITX9JlbnGzKzTj0qi/JziUCGqD2DiN3cXaWyDcxE=
|
||||
|
|
|
@ -507,6 +507,9 @@ export interface OIDCConfig {
|
|||
readonly ignore_email_verified: boolean
|
||||
readonly username_field: string
|
||||
readonly groups_field: string
|
||||
// Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
||||
readonly group_mapping: any
|
||||
readonly sign_in_text: string
|
||||
readonly icon_url: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue