Skip to content

feat: add auto group create from OIDC #8884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions cli/clibase/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,40 @@ func TestOptionSet_ParseFlags(t *testing.T) {
err := os.FlagSet().Parse([]string{"--some-unknown", "foo"})
require.Error(t, err)
})

t.Run("RegexValid", func(t *testing.T) {
t.Parallel()

var regexpString clibase.Regexp

os := clibase.OptionSet{
clibase.Option{
Name: "RegexpString",
Value: &regexpString,
Flag: "regexp-string",
},
}

err := os.FlagSet().Parse([]string{"--regexp-string", "$test^"})
require.NoError(t, err)
})

t.Run("RegexInvalid", func(t *testing.T) {
t.Parallel()

var regexpString clibase.Regexp

os := clibase.OptionSet{
clibase.Option{
Name: "RegexpString",
Value: &regexpString,
Flag: "regexp-string",
},
}

err := os.FlagSet().Parse([]string{"--regexp-string", "(("})
require.Error(t, err)
})
}

func TestOptionSet_ParseEnv(t *testing.T) {
Expand Down
38 changes: 38 additions & 0 deletions cli/clibase/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -461,6 +462,43 @@ func (e *Enum) String() string {
return *e.Value
}

type Regexp regexp.Regexp

func (r *Regexp) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: r.String(),
}, nil
}

func (r *Regexp) UnmarshalYAML(n *yaml.Node) error {
return r.Set(n.Value)
}

func (r *Regexp) Set(v string) error {
exp, err := regexp.Compile(v)
if err != nil {
return xerrors.Errorf("invalid regex expression: %w", err)
}
*r = Regexp(*exp)
return nil
}

func (r Regexp) String() string {
return r.Value().String()
}

func (r *Regexp) Value() *regexp.Regexp {
if r == nil {
return nil
}
return (*regexp.Regexp)(r)
}

func (Regexp) Type() string {
return "regexp"
}

var _ pflag.Value = (*YAMLConfigPath)(nil)

// YAMLConfigPath is a special value type that encodes a path to a YAML
Expand Down
2 changes: 2 additions & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
AuthURLParams: cfg.OIDC.AuthURLParams.Value,
IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(),
GroupField: cfg.OIDC.GroupField.String(),
GroupFilter: cfg.OIDC.GroupRegexFilter.Value(),
CreateMissingGroups: cfg.OIDC.GroupAutoCreate.Value(),
GroupMapping: cfg.OIDC.GroupMapping.Value,
UserRoleField: cfg.OIDC.UserRoleField.String(),
UserRoleMapping: cfg.OIDC.UserRoleMapping.Value,
Expand Down
8 changes: 8 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ can safely ignore these settings.
GitHub.

OIDC Options
--oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false)
Automatically creates missing groups from a user's groups claim.

--oidc-allow-signups bool, $CODER_OIDC_ALLOW_SIGNUPS (default: true)
Whether new users can sign up with OIDC.

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

--oidc-group-regex-filter regexp, $CODER_OIDC_GROUP_REGEX_FILTER (default: .*)
If provided any group name not matching the regex is ignored. This
allows for filtering out groups that are not needed. This filter is
applied after the group mapping.

--oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email)
Scopes to grant when authenticating with OIDC.

Expand Down
8 changes: 8 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ oidc:
# for when OIDC providers only return group IDs.
# (default: {}, type: struct[map[string]string])
groupMapping: {}
# Automatically creates missing groups from a user's groups claim.
# (default: false, type: bool)
enableGroupAutoCreate: false
# If provided any group name not matching the regex is ignored. This allows for
# filtering out groups that are not needed. This filter is applied after the group
# mapping.
# (default: .*, type: regexp)
groupRegexFilter: .*
# This field must be set if using the user roles sync feature. Set this to the
# name of the claim used to store the user's role. The roles should be sent as an
# array of strings.
Expand Down
23 changes: 23 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ type Options struct {
BaseDERPMap *tailcfg.DERPMap
DERPMapUpdateFrequency time.Duration
SwaggerEndpoint bool
SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
SetUserSiteRoles func(ctx context.Context, tx database.Store, userID uuid.UUID, roles []string) error
SetUserGroups func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, groupNames []string, createMissingGroups bool) error
SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
Expand Down Expand Up @@ -262,16 +262,16 @@ func New(options *Options) *API {
options.TracerProvider = trace.NewNoopTracerProvider()
}
if options.SetUserGroups == nil {
options.SetUserGroups = func(ctx context.Context, _ database.Store, userID uuid.UUID, groups []string) error {
options.Logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
slog.F("user_id", userID), slog.F("groups", groups),
options.SetUserGroups = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, groups []string, createMissingGroups bool) error {
logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
slog.F("user_id", userID), slog.F("groups", groups), slog.F("create_missing_groups", createMissingGroups),
)
return nil
}
}
if options.SetUserSiteRoles == nil {
options.SetUserSiteRoles = func(ctx context.Context, _ database.Store, userID uuid.UUID, roles []string) error {
options.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
options.SetUserSiteRoles = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, roles []string) error {
logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
slog.F("user_id", userID), slog.F("roles", roles),
)
return nil
Expand Down
7 changes: 7 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,13 @@ func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseP
return q.db.InsertLicense(ctx, arg)
}

func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.InsertMissingGroups(ctx, arg)
}

func (q *querier) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
return insert(q.log, q.auth, rbac.ResourceOrganization, q.db.InsertOrganization)(ctx, arg)
}
Expand Down
40 changes: 40 additions & 0 deletions coderd/database/dbfake/dbfake.go
Original file line number Diff line number Diff line change
Expand Up @@ -3641,6 +3641,7 @@ func (q *FakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar
OrganizationID: arg.OrganizationID,
AvatarURL: arg.AvatarURL,
QuotaAllowance: arg.QuotaAllowance,
Source: database.GroupSourceUser,
}

q.groups = append(q.groups, group)
Expand Down Expand Up @@ -3693,6 +3694,45 @@ func (q *FakeQuerier) InsertLicense(
return l, nil
}

func (q *FakeQuerier) InsertMissingGroups(_ context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) {
err := validateDatabaseType(arg)
if err != nil {
return nil, err
}

groupNameMap := make(map[string]struct{})
for _, g := range arg.GroupNames {
groupNameMap[g] = struct{}{}
}

q.mutex.Lock()
defer q.mutex.Unlock()

for _, g := range q.groups {
if g.OrganizationID != arg.OrganizationID {
continue
}
delete(groupNameMap, g.Name)
}

newGroups := make([]database.Group, 0, len(groupNameMap))
for k := range groupNameMap {
g := database.Group{
ID: uuid.New(),
Name: k,
OrganizationID: arg.OrganizationID,
AvatarURL: "",
QuotaAllowance: 0,
DisplayName: "",
Source: arg.Source,
}
q.groups = append(q.groups, g)
newGroups = append(newGroups, g)
}

return newGroups, nil
}

func (q *FakeQuerier) InsertOrganization(_ context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
if err := validateDatabaseType(arg); err != nil {
return database.Organization{}, err
Expand Down
7 changes: 7 additions & 0 deletions coderd/database/dbmetrics/dbmetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading