Skip to content

Commit f4122fa

Browse files
authored
feat: add auto group create from OIDC (#8884)
* add flag for auto create groups * fixup! add flag for auto create groups * sync missing groups Also added a regex filter to filter out groups that are not important
1 parent 4a987e9 commit f4122fa

35 files changed

+887
-128
lines changed

cli/clibase/option_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,40 @@ func TestOptionSet_ParseFlags(t *testing.T) {
7272
err := os.FlagSet().Parse([]string{"--some-unknown", "foo"})
7373
require.Error(t, err)
7474
})
75+
76+
t.Run("RegexValid", func(t *testing.T) {
77+
t.Parallel()
78+
79+
var regexpString clibase.Regexp
80+
81+
os := clibase.OptionSet{
82+
clibase.Option{
83+
Name: "RegexpString",
84+
Value: &regexpString,
85+
Flag: "regexp-string",
86+
},
87+
}
88+
89+
err := os.FlagSet().Parse([]string{"--regexp-string", "$test^"})
90+
require.NoError(t, err)
91+
})
92+
93+
t.Run("RegexInvalid", func(t *testing.T) {
94+
t.Parallel()
95+
96+
var regexpString clibase.Regexp
97+
98+
os := clibase.OptionSet{
99+
clibase.Option{
100+
Name: "RegexpString",
101+
Value: &regexpString,
102+
Flag: "regexp-string",
103+
},
104+
}
105+
106+
err := os.FlagSet().Parse([]string{"--regexp-string", "(("})
107+
require.Error(t, err)
108+
})
75109
}
76110

77111
func TestOptionSet_ParseEnv(t *testing.T) {

cli/clibase/values.go

+38
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88
"net/url"
99
"reflect"
10+
"regexp"
1011
"strconv"
1112
"strings"
1213
"time"
@@ -461,6 +462,43 @@ func (e *Enum) String() string {
461462
return *e.Value
462463
}
463464

465+
type Regexp regexp.Regexp
466+
467+
func (r *Regexp) MarshalYAML() (interface{}, error) {
468+
return yaml.Node{
469+
Kind: yaml.ScalarNode,
470+
Value: r.String(),
471+
}, nil
472+
}
473+
474+
func (r *Regexp) UnmarshalYAML(n *yaml.Node) error {
475+
return r.Set(n.Value)
476+
}
477+
478+
func (r *Regexp) Set(v string) error {
479+
exp, err := regexp.Compile(v)
480+
if err != nil {
481+
return xerrors.Errorf("invalid regex expression: %w", err)
482+
}
483+
*r = Regexp(*exp)
484+
return nil
485+
}
486+
487+
func (r Regexp) String() string {
488+
return r.Value().String()
489+
}
490+
491+
func (r *Regexp) Value() *regexp.Regexp {
492+
if r == nil {
493+
return nil
494+
}
495+
return (*regexp.Regexp)(r)
496+
}
497+
498+
func (Regexp) Type() string {
499+
return "regexp"
500+
}
501+
464502
var _ pflag.Value = (*YAMLConfigPath)(nil)
465503

466504
// YAMLConfigPath is a special value type that encodes a path to a YAML

cli/server.go

+2
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
597597
AuthURLParams: cfg.OIDC.AuthURLParams.Value,
598598
IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(),
599599
GroupField: cfg.OIDC.GroupField.String(),
600+
GroupFilter: cfg.OIDC.GroupRegexFilter.Value(),
601+
CreateMissingGroups: cfg.OIDC.GroupAutoCreate.Value(),
600602
GroupMapping: cfg.OIDC.GroupMapping.Value,
601603
UserRoleField: cfg.OIDC.UserRoleField.String(),
602604
UserRoleMapping: cfg.OIDC.UserRoleMapping.Value,

cli/testdata/coder_server_--help.golden

+8
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ can safely ignore these settings.
298298
GitHub.
299299

300300
OIDC Options
301+
--oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false)
302+
Automatically creates missing groups from a user's groups claim.
303+
301304
--oidc-allow-signups bool, $CODER_OIDC_ALLOW_SIGNUPS (default: true)
302305
Whether new users can sign up with OIDC.
303306

@@ -334,6 +337,11 @@ can safely ignore these settings.
334337
--oidc-issuer-url string, $CODER_OIDC_ISSUER_URL
335338
Issuer URL to use for Login with OIDC.
336339

340+
--oidc-group-regex-filter regexp, $CODER_OIDC_GROUP_REGEX_FILTER (default: .*)
341+
If provided any group name not matching the regex is ignored. This
342+
allows for filtering out groups that are not needed. This filter is
343+
applied after the group mapping.
344+
337345
--oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email)
338346
Scopes to grant when authenticating with OIDC.
339347

cli/testdata/server-config.yaml.golden

+8
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,14 @@ oidc:
271271
# for when OIDC providers only return group IDs.
272272
# (default: {}, type: struct[map[string]string])
273273
groupMapping: {}
274+
# Automatically creates missing groups from a user's groups claim.
275+
# (default: false, type: bool)
276+
enableGroupAutoCreate: false
277+
# If provided any group name not matching the regex is ignored. This allows for
278+
# filtering out groups that are not needed. This filter is applied after the group
279+
# mapping.
280+
# (default: .*, type: regexp)
281+
groupRegexFilter: .*
274282
# This field must be set if using the user roles sync feature. Set this to the
275283
# name of the claim used to store the user's role. The roles should be sent as an
276284
# array of strings.

coderd/apidoc/docs.go

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ type Options struct {
127127
BaseDERPMap *tailcfg.DERPMap
128128
DERPMapUpdateFrequency time.Duration
129129
SwaggerEndpoint bool
130-
SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
131-
SetUserSiteRoles func(ctx context.Context, tx database.Store, userID uuid.UUID, roles []string) error
130+
SetUserGroups func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, groupNames []string, createMissingGroups bool) error
131+
SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error
132132
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
133133
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
134134
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
@@ -262,16 +262,16 @@ func New(options *Options) *API {
262262
options.TracerProvider = trace.NewNoopTracerProvider()
263263
}
264264
if options.SetUserGroups == nil {
265-
options.SetUserGroups = func(ctx context.Context, _ database.Store, userID uuid.UUID, groups []string) error {
266-
options.Logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
267-
slog.F("user_id", userID), slog.F("groups", groups),
265+
options.SetUserGroups = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, groups []string, createMissingGroups bool) error {
266+
logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
267+
slog.F("user_id", userID), slog.F("groups", groups), slog.F("create_missing_groups", createMissingGroups),
268268
)
269269
return nil
270270
}
271271
}
272272
if options.SetUserSiteRoles == nil {
273-
options.SetUserSiteRoles = func(ctx context.Context, _ database.Store, userID uuid.UUID, roles []string) error {
274-
options.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
273+
options.SetUserSiteRoles = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, roles []string) error {
274+
logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
275275
slog.F("user_id", userID), slog.F("roles", roles),
276276
)
277277
return nil

coderd/database/dbauthz/dbauthz.go

+7
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,13 @@ func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseP
18531853
return q.db.InsertLicense(ctx, arg)
18541854
}
18551855

1856+
func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) {
1857+
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
1858+
return nil, err
1859+
}
1860+
return q.db.InsertMissingGroups(ctx, arg)
1861+
}
1862+
18561863
func (q *querier) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
18571864
return insert(q.log, q.auth, rbac.ResourceOrganization, q.db.InsertOrganization)(ctx, arg)
18581865
}

coderd/database/dbfake/dbfake.go

+40
Original file line numberDiff line numberDiff line change
@@ -3641,6 +3641,7 @@ func (q *FakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar
36413641
OrganizationID: arg.OrganizationID,
36423642
AvatarURL: arg.AvatarURL,
36433643
QuotaAllowance: arg.QuotaAllowance,
3644+
Source: database.GroupSourceUser,
36443645
}
36453646

36463647
q.groups = append(q.groups, group)
@@ -3693,6 +3694,45 @@ func (q *FakeQuerier) InsertLicense(
36933694
return l, nil
36943695
}
36953696

3697+
func (q *FakeQuerier) InsertMissingGroups(_ context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) {
3698+
err := validateDatabaseType(arg)
3699+
if err != nil {
3700+
return nil, err
3701+
}
3702+
3703+
groupNameMap := make(map[string]struct{})
3704+
for _, g := range arg.GroupNames {
3705+
groupNameMap[g] = struct{}{}
3706+
}
3707+
3708+
q.mutex.Lock()
3709+
defer q.mutex.Unlock()
3710+
3711+
for _, g := range q.groups {
3712+
if g.OrganizationID != arg.OrganizationID {
3713+
continue
3714+
}
3715+
delete(groupNameMap, g.Name)
3716+
}
3717+
3718+
newGroups := make([]database.Group, 0, len(groupNameMap))
3719+
for k := range groupNameMap {
3720+
g := database.Group{
3721+
ID: uuid.New(),
3722+
Name: k,
3723+
OrganizationID: arg.OrganizationID,
3724+
AvatarURL: "",
3725+
QuotaAllowance: 0,
3726+
DisplayName: "",
3727+
Source: arg.Source,
3728+
}
3729+
q.groups = append(q.groups, g)
3730+
newGroups = append(newGroups, g)
3731+
}
3732+
3733+
return newGroups, nil
3734+
}
3735+
36963736
func (q *FakeQuerier) InsertOrganization(_ context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
36973737
if err := validateDatabaseType(arg); err != nil {
36983738
return database.Organization{}, err

coderd/database/dbmetrics/dbmetrics.go

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dump.sql

+9-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)