mirror of https://github.com/coder/coder.git
feat: synchronize oidc user roles (#8595)
* feat: oidc user role sync User roles come from oidc claims. Prevent manual user role changes if set. * allow mapping 1:many
This commit is contained in:
parent
94541d201f
commit
f827829afe
|
@ -1,5 +1,7 @@
|
|||
# Generated files
|
||||
coderd/apidoc/docs.go linguist-generated=true
|
||||
docs/api/*.md linguist-generated=true
|
||||
docs/cli/*.md linguist-generated=true
|
||||
coderd/apidoc/swagger.json linguist-generated=true
|
||||
coderd/database/dump.sql linguist-generated=true
|
||||
peerbroker/proto/*.go linguist-generated=true
|
||||
|
@ -9,3 +11,4 @@ provisionersdk/proto/*.go linguist-generated=true
|
|||
*.tfstate.json linguist-generated=true
|
||||
*.tfstate.dot linguist-generated=true
|
||||
*.tfplan.dot linguist-generated=true
|
||||
|
||||
|
|
|
@ -596,6 +596,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||
IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(),
|
||||
GroupField: cfg.OIDC.GroupField.String(),
|
||||
GroupMapping: cfg.OIDC.GroupMapping.Value,
|
||||
UserRoleField: cfg.OIDC.UserRoleField.String(),
|
||||
UserRoleMapping: cfg.OIDC.UserRoleMapping.Value,
|
||||
UserRolesDefault: cfg.OIDC.UserRolesDefault.GetSlice(),
|
||||
SignInText: cfg.OIDC.SignInText.String(),
|
||||
IconURL: cfg.OIDC.IconURL.String(),
|
||||
IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(),
|
||||
|
|
|
@ -1095,6 +1095,8 @@ func TestServer(t *testing.T) {
|
|||
require.False(t, deploymentConfig.Values.OIDC.IgnoreUserInfo.Value())
|
||||
require.Empty(t, deploymentConfig.Values.OIDC.GroupField.Value())
|
||||
require.Empty(t, deploymentConfig.Values.OIDC.GroupMapping.Value)
|
||||
require.Empty(t, deploymentConfig.Values.OIDC.UserRoleField.Value())
|
||||
require.Empty(t, deploymentConfig.Values.OIDC.UserRoleMapping.Value)
|
||||
require.Equal(t, "OpenID Connect", deploymentConfig.Values.OIDC.SignInText.Value())
|
||||
require.Empty(t, deploymentConfig.Values.OIDC.IconURL.Value())
|
||||
})
|
||||
|
|
|
@ -337,6 +337,20 @@ can safely ignore these settings.
|
|||
--oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email)
|
||||
Scopes to grant when authenticating with OIDC.
|
||||
|
||||
--oidc-user-role-default string-array, $CODER_OIDC_USER_ROLE_DEFAULT
|
||||
If user role sync is enabled, these roles are always included for all
|
||||
authenticated users. The 'member' role is always assigned.
|
||||
|
||||
--oidc-user-role-field string, $CODER_OIDC_USER_ROLE_FIELD
|
||||
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.
|
||||
|
||||
--oidc-user-role-mapping struct[map[string][]string], $CODER_OIDC_USER_ROLE_MAPPING (default: {})
|
||||
A map of the OIDC passed in user roles and the groups in Coder it
|
||||
should map to. This is useful if the group names do not match. If
|
||||
mapped to the empty string, the role will ignored.
|
||||
|
||||
--oidc-username-field string, $CODER_OIDC_USERNAME_FIELD (default: preferred_username)
|
||||
OIDC claim field to use as the username.
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
"display_name": "Owner"
|
||||
}
|
||||
],
|
||||
"avatar_url": ""
|
||||
"avatar_url": "",
|
||||
"login_type": "password"
|
||||
},
|
||||
{
|
||||
"id": "[second user ID]",
|
||||
|
@ -28,6 +29,7 @@
|
|||
"[first org ID]"
|
||||
],
|
||||
"roles": [],
|
||||
"avatar_url": ""
|
||||
"avatar_url": "",
|
||||
"login_type": "password"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -268,6 +268,20 @@ oidc:
|
|||
# for when OIDC providers only return group IDs.
|
||||
# (default: {}, type: struct[map[string]string])
|
||||
groupMapping: {}
|
||||
# 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.
|
||||
# (default: <unset>, type: string)
|
||||
userRoleField: ""
|
||||
# A map of the OIDC passed in user roles and the groups in Coder it should map to.
|
||||
# This is useful if the group names do not match. If mapped to the empty string,
|
||||
# the role will ignored.
|
||||
# (default: {}, type: struct[map[string][]string])
|
||||
userRoleMapping: {}
|
||||
# If user role sync is enabled, these roles are always included for all
|
||||
# authenticated users. The 'member' role is always assigned.
|
||||
# (default: <unset>, type: string-array)
|
||||
userRoleDefault: []
|
||||
# The text to show on the OpenID Connect sign in button.
|
||||
# (default: OpenID Connect, type: string)
|
||||
signInText: OpenID Connect
|
||||
|
|
|
@ -8393,6 +8393,18 @@ const docTemplate = `{
|
|||
"sign_in_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_role_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_role_mapping": {
|
||||
"type": "object"
|
||||
},
|
||||
"user_roles_default": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"username_field": {
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -9413,6 +9425,9 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"login_type": {
|
||||
"$ref": "#/definitions/codersdk.LoginType"
|
||||
},
|
||||
"organization_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -9866,6 +9881,9 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"login_type": {
|
||||
"$ref": "#/definitions/codersdk.LoginType"
|
||||
},
|
||||
"organization_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -7532,6 +7532,18 @@
|
|||
"sign_in_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_role_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_role_mapping": {
|
||||
"type": "object"
|
||||
},
|
||||
"user_roles_default": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"username_field": {
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -8503,6 +8515,9 @@
|
|||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"login_type": {
|
||||
"$ref": "#/definitions/codersdk.LoginType"
|
||||
},
|
||||
"organization_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -8919,6 +8934,9 @@
|
|||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"login_type": {
|
||||
"$ref": "#/definitions/codersdk.LoginType"
|
||||
},
|
||||
"organization_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -124,6 +124,7 @@ type Options struct {
|
|||
DERPMap *tailcfg.DERPMap
|
||||
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
|
||||
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
|
||||
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
|
||||
|
@ -258,6 +259,14 @@ func New(options *Options) *API {
|
|||
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",
|
||||
slog.F("user_id", userID), slog.F("roles", roles),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if options.TemplateScheduleStore == nil {
|
||||
options.TemplateScheduleStore = &atomic.Pointer[schedule.TemplateScheduleStore]{}
|
||||
}
|
||||
|
|
|
@ -115,6 +115,7 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
|||
OrganizationIDs: organizationIDs,
|
||||
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
|
||||
AvatarURL: user.AvatarURL.String,
|
||||
LoginType: codersdk.LoginType(user.LoginType),
|
||||
}
|
||||
|
||||
for _, roleName := range user.RBACRoles {
|
||||
|
|
|
@ -207,7 +207,7 @@ var (
|
|||
rbac.ResourceWildcard.Type: {rbac.ActionRead},
|
||||
rbac.ResourceAPIKey.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
rbac.ResourceGroup.Type: {rbac.ActionCreate, rbac.ActionUpdate},
|
||||
rbac.ResourceRoleAssignment.Type: {rbac.ActionCreate},
|
||||
rbac.ResourceRoleAssignment.Type: {rbac.ActionCreate, rbac.ActionDelete},
|
||||
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
|
||||
rbac.ResourceOrganization.Type: {rbac.ActionCreate},
|
||||
rbac.ResourceOrganizationMember.Type: {rbac.ActionCreate},
|
||||
|
|
|
@ -283,10 +283,13 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||
// map[actor_role][assign_role]<can_assign>
|
||||
var assignRoles = map[string]map[string]bool{
|
||||
"system": {
|
||||
owner: true,
|
||||
member: true,
|
||||
orgAdmin: true,
|
||||
orgMember: true,
|
||||
owner: true,
|
||||
auditor: true,
|
||||
member: true,
|
||||
orgAdmin: true,
|
||||
orgMember: true,
|
||||
templateAdmin: true,
|
||||
userAdmin: true,
|
||||
},
|
||||
owner: {
|
||||
owner: true,
|
||||
|
|
|
@ -684,12 +684,27 @@ type OIDCConfig struct {
|
|||
// to groups within Coder.
|
||||
// map[oidcGroupName]coderGroupName
|
||||
GroupMapping map[string]string
|
||||
// UserRoleField selects the claim field to be used as the created user's
|
||||
// roles. If the field is the empty string, then no role updates
|
||||
// will ever come from the OIDC provider.
|
||||
UserRoleField string
|
||||
// UserRoleMapping controls how groups returned by the OIDC provider get mapped
|
||||
// to roles within Coder.
|
||||
// map[oidcRoleName][]coderRoleName
|
||||
UserRoleMapping map[string][]string
|
||||
// UserRolesDefault is the default set of roles to assign to a user if role sync
|
||||
// is enabled.
|
||||
UserRolesDefault []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
|
||||
IconURL string
|
||||
}
|
||||
|
||||
func (cfg OIDCConfig) RoleSyncEnabled() bool {
|
||||
return cfg.UserRoleField != ""
|
||||
}
|
||||
|
||||
// @Summary OpenID Connect Callback
|
||||
// @ID openid-connect-callback
|
||||
// @Security CoderSessionToken
|
||||
|
@ -942,6 +957,62 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
roles := api.OIDCConfig.UserRolesDefault
|
||||
if api.OIDCConfig.RoleSyncEnabled() {
|
||||
rolesRow, ok := claims[api.OIDCConfig.UserRoleField]
|
||||
if !ok {
|
||||
// If no claim is provided than we can assume the user is just
|
||||
// a member. This is because there is no way to tell the difference
|
||||
// between []string{} and nil for OIDC claims. IDPs omit claims
|
||||
// if they are empty ([]string{}).
|
||||
rolesRow = []string{}
|
||||
}
|
||||
|
||||
rolesInterface, ok := rolesRow.([]interface{})
|
||||
if !ok {
|
||||
api.Logger.Error(ctx, "oidc claim user roles field was an unknown type",
|
||||
slog.F("type", fmt.Sprintf("%T", rolesRow)),
|
||||
)
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
HideStatus: true,
|
||||
Title: "Login disabled until OIDC config is fixed",
|
||||
Description: fmt.Sprintf("Roles claim must be an array of strings, type found: %T. Disabling role sync will allow login to proceed.", rolesRow),
|
||||
RetryEnabled: false,
|
||||
DashboardURL: "/login",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
api.Logger.Debug(ctx, "roles returned in oidc claims",
|
||||
slog.F("len", len(rolesInterface)),
|
||||
slog.F("roles", rolesInterface),
|
||||
)
|
||||
for _, roleInterface := range rolesInterface {
|
||||
role, ok := roleInterface.(string)
|
||||
if !ok {
|
||||
api.Logger.Error(ctx, "invalid oidc user role type",
|
||||
slog.F("type", fmt.Sprintf("%T", rolesRow)),
|
||||
)
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid user role type. Expected string, got: %T", roleInterface),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if mappedRoles, ok := api.OIDCConfig.UserRoleMapping[role]; ok {
|
||||
if len(mappedRoles) == 0 {
|
||||
continue
|
||||
}
|
||||
// Mapped roles are added to the list of roles
|
||||
roles = append(roles, mappedRoles...)
|
||||
continue
|
||||
}
|
||||
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
|
||||
// If a new user is authenticating for the first time
|
||||
// the audit action is 'register', not 'login'
|
||||
if user.ID == uuid.Nil {
|
||||
|
@ -959,6 +1030,8 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
|||
Username: username,
|
||||
AvatarURL: picture,
|
||||
UsingGroups: usingGroups,
|
||||
UsingRoles: api.OIDCConfig.RoleSyncEnabled(),
|
||||
Roles: roles,
|
||||
Groups: groups,
|
||||
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
|
||||
return audit.InitRequest[database.User](rw, params)
|
||||
|
@ -1045,6 +1118,10 @@ type oauthLoginParams struct {
|
|||
// to the Groups provided.
|
||||
UsingGroups bool
|
||||
Groups []string
|
||||
// Is UsingRoles is true, then the user will be assigned
|
||||
// the roles provided.
|
||||
UsingRoles bool
|
||||
Roles []string
|
||||
|
||||
commitLock sync.Mutex
|
||||
initAuditRequest func(params *audit.RequestParams) *audit.Request[database.User]
|
||||
|
@ -1108,6 +1185,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
|
|||
ctx = r.Context()
|
||||
user database.User
|
||||
cookies []*http.Cookie
|
||||
logger = api.Logger.Named(userAuthLoggerName)
|
||||
)
|
||||
|
||||
var isConvertLoginType bool
|
||||
|
@ -1248,6 +1326,37 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure roles are correct.
|
||||
if params.UsingRoles {
|
||||
ignored := make([]string, 0)
|
||||
filtered := make([]string, 0, len(params.Roles))
|
||||
for _, role := range params.Roles {
|
||||
if _, err := rbac.RoleByName(role); err == nil {
|
||||
filtered = append(filtered, role)
|
||||
} else {
|
||||
ignored = append(ignored, role)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocritic
|
||||
err := api.Options.SetUserSiteRoles(dbauthz.AsSystemRestricted(ctx), tx, user.ID, filtered)
|
||||
if err != nil {
|
||||
return httpError{
|
||||
code: http.StatusBadRequest,
|
||||
msg: "Invalid roles through OIDC claim",
|
||||
detail: fmt.Sprintf("Error from role assignment attempt: %s", err.Error()),
|
||||
renderStaticPage: true,
|
||||
}
|
||||
}
|
||||
if len(ignored) > 0 {
|
||||
logger.Debug(ctx, "OIDC roles ignored in assignment",
|
||||
slog.F("ignored", ignored),
|
||||
slog.F("assigned", filtered),
|
||||
slog.F("user_id", user.ID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
needsUpdate := false
|
||||
if user.AvatarURL.String != params.AvatarURL {
|
||||
user.AvatarURL = sql.NullString{
|
||||
|
|
|
@ -889,6 +889,14 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
|||
defer commitAudit()
|
||||
aReq.Old = user
|
||||
|
||||
if user.LoginType == database.LoginTypeOIDC && api.OIDCConfig.RoleSyncEnabled() {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Cannot modify roles for OIDC users when role sync is enabled.",
|
||||
Detail: "'User Role Field' is set in the OIDC configuration. All role changes must come from the oidc identity provider.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if apiKey.UserID == user.ID {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "You cannot change your own roles.",
|
||||
|
@ -901,7 +909,7 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
updatedUser, err := api.updateSiteUserRoles(ctx, database.UpdateUserRolesParams{
|
||||
updatedUser, err := UpdateSiteUserRoles(ctx, api.Database, database.UpdateUserRolesParams{
|
||||
GrantedRoles: params.Roles,
|
||||
ID: user.ID,
|
||||
})
|
||||
|
@ -929,9 +937,9 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
|||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizationIDs))
|
||||
}
|
||||
|
||||
// updateSiteUserRoles will ensure only site wide roles are passed in as arguments.
|
||||
// UpdateSiteUserRoles will ensure only site wide roles are passed in as arguments.
|
||||
// If an organization role is included, an error is returned.
|
||||
func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) {
|
||||
func UpdateSiteUserRoles(ctx context.Context, db database.Store, args database.UpdateUserRolesParams) (database.User, error) {
|
||||
// Enforce only site wide roles.
|
||||
for _, r := range args.GrantedRoles {
|
||||
if _, ok := rbac.IsOrgRole(r); ok {
|
||||
|
@ -943,7 +951,7 @@ func (api *API) updateSiteUserRoles(ctx context.Context, args database.UpdateUse
|
|||
}
|
||||
}
|
||||
|
||||
updatedUser, err := api.Database.UpdateUserRoles(ctx, args)
|
||||
updatedUser, err := db.UpdateUserRoles(ctx, args)
|
||||
if err != nil {
|
||||
return database.User{}, xerrors.Errorf("update site roles: %w", err)
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ const (
|
|||
FeatureBrowserOnly FeatureName = "browser_only"
|
||||
FeatureSCIM FeatureName = "scim"
|
||||
FeatureTemplateRBAC FeatureName = "template_rbac"
|
||||
FeatureUserRoleManagement FeatureName = "user_role_management"
|
||||
FeatureHighAvailability FeatureName = "high_availability"
|
||||
FeatureMultipleGitAuth FeatureName = "multiple_git_auth"
|
||||
FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons"
|
||||
|
@ -62,6 +63,7 @@ var FeatureNames = []FeatureName{
|
|||
FeatureAppearance,
|
||||
FeatureAdvancedTemplateScheduling,
|
||||
FeatureWorkspaceProxy,
|
||||
FeatureUserRoleManagement,
|
||||
}
|
||||
|
||||
// Humanize returns the feature name in a human-readable format.
|
||||
|
@ -258,21 +260,24 @@ 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.StringArray `json:"email_domain" typescript:",notnull"`
|
||||
IssuerURL clibase.String `json:"issuer_url" typescript:",notnull"`
|
||||
Scopes clibase.StringArray `json:"scopes" typescript:",notnull"`
|
||||
IgnoreEmailVerified clibase.Bool `json:"ignore_email_verified" typescript:",notnull"`
|
||||
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
|
||||
EmailField clibase.String `json:"email_field" typescript:",notnull"`
|
||||
AuthURLParams clibase.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"`
|
||||
IgnoreUserInfo clibase.Bool `json:"ignore_user_info" 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"`
|
||||
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.StringArray `json:"email_domain" typescript:",notnull"`
|
||||
IssuerURL clibase.String `json:"issuer_url" typescript:",notnull"`
|
||||
Scopes clibase.StringArray `json:"scopes" typescript:",notnull"`
|
||||
IgnoreEmailVerified clibase.Bool `json:"ignore_email_verified" typescript:",notnull"`
|
||||
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
|
||||
EmailField clibase.String `json:"email_field" typescript:",notnull"`
|
||||
AuthURLParams clibase.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"`
|
||||
IgnoreUserInfo clibase.Bool `json:"ignore_user_info" typescript:",notnull"`
|
||||
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
|
||||
GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
|
||||
UserRoleField clibase.String `json:"user_role_field" typescript:",notnull"`
|
||||
UserRoleMapping clibase.Struct[map[string][]string] `json:"user_role_mapping" typescript:",notnull"`
|
||||
UserRolesDefault clibase.StringArray `json:"user_roles_default" typescript:",notnull"`
|
||||
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
|
||||
IconURL clibase.URL `json:"icon_url" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type TelemetryConfig struct {
|
||||
|
@ -1043,6 +1048,38 @@ when required by your organization's security policy.`,
|
|||
Group: &deploymentGroupOIDC,
|
||||
YAML: "groupMapping",
|
||||
},
|
||||
{
|
||||
Name: "OIDC User Role Field",
|
||||
Description: "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.",
|
||||
Flag: "oidc-user-role-field",
|
||||
Env: "CODER_OIDC_USER_ROLE_FIELD",
|
||||
// This value is intentionally blank. If this is empty, then OIDC user role
|
||||
// sync behavior is disabled.
|
||||
Default: "",
|
||||
Value: &c.OIDC.UserRoleField,
|
||||
Group: &deploymentGroupOIDC,
|
||||
YAML: "userRoleField",
|
||||
},
|
||||
{
|
||||
Name: "OIDC User Role Mapping",
|
||||
Description: "A map of the OIDC passed in user roles and the groups in Coder it should map to. This is useful if the group names do not match. If mapped to the empty string, the role will ignored.",
|
||||
Flag: "oidc-user-role-mapping",
|
||||
Env: "CODER_OIDC_USER_ROLE_MAPPING",
|
||||
Default: "{}",
|
||||
Value: &c.OIDC.UserRoleMapping,
|
||||
Group: &deploymentGroupOIDC,
|
||||
YAML: "userRoleMapping",
|
||||
},
|
||||
{
|
||||
Name: "OIDC User Role Default",
|
||||
Description: "If user role sync is enabled, these roles are always included for all authenticated users. The 'member' role is always assigned.",
|
||||
Flag: "oidc-user-role-default",
|
||||
Env: "CODER_OIDC_USER_ROLE_DEFAULT",
|
||||
Default: "",
|
||||
Value: &c.OIDC.UserRolesDefault,
|
||||
Group: &deploymentGroupOIDC,
|
||||
YAML: "userRoleDefault",
|
||||
},
|
||||
{
|
||||
Name: "OpenID Connect sign in text",
|
||||
Description: "The text to show on the OpenID Connect sign in button.",
|
||||
|
|
|
@ -45,6 +45,7 @@ type User struct {
|
|||
OrganizationIDs []uuid.UUID `json:"organization_ids" format:"uuid"`
|
||||
Roles []Role `json:"roles"`
|
||||
AvatarURL string `json:"avatar_url" format:"uri"`
|
||||
LoginType LoginType `json:"login_type"`
|
||||
}
|
||||
|
||||
type GetUsersResponse struct {
|
||||
|
|
|
@ -63,6 +63,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?q=string \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
|
|
@ -182,6 +182,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -241,6 +242,7 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -300,6 +302,7 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -434,6 +437,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -473,6 +477,7 @@ Status Code **200**
|
|||
| `»» email` | string(email) | true | | |
|
||||
| `»» id` | string(uuid) | true | | |
|
||||
| `»» last_seen_at` | string(date-time) | false | | |
|
||||
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»» organization_ids` | array | false | | |
|
||||
| `»» roles` | array | false | | |
|
||||
| `»»» display_name` | string | false | | |
|
||||
|
@ -485,10 +490,15 @@ Status Code **200**
|
|||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value |
|
||||
| -------- | ----------- |
|
||||
| `status` | `active` |
|
||||
| `status` | `suspended` |
|
||||
| Property | Value |
|
||||
| ------------ | ----------- |
|
||||
| `login_type` | `password` |
|
||||
| `login_type` | `github` |
|
||||
| `login_type` | `oidc` |
|
||||
| `login_type` | `token` |
|
||||
| `login_type` | `none` |
|
||||
| `status` | `active` |
|
||||
| `status` | `suspended` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
@ -538,6 +548,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -598,6 +609,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -959,6 +971,7 @@ curl -X PATCH http://coder-server:8080/api/v2/scim/v2/Users/{id} \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1010,6 +1023,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"role": "admin",
|
||||
"roles": [
|
||||
|
@ -1042,6 +1056,7 @@ Status Code **200**
|
|||
| `» email` | string(email) | true | | |
|
||||
| `» id` | string(uuid) | true | | |
|
||||
| `» last_seen_at` | string(date-time) | false | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `» organization_ids` | array | false | | |
|
||||
| `» role` | [codersdk.TemplateRole](schemas.md#codersdktemplaterole) | false | | |
|
||||
| `» roles` | array | false | | |
|
||||
|
@ -1052,12 +1067,17 @@ Status Code **200**
|
|||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value |
|
||||
| -------- | ----------- |
|
||||
| `role` | `admin` |
|
||||
| `role` | `use` |
|
||||
| `status` | `active` |
|
||||
| `status` | `suspended` |
|
||||
| Property | Value |
|
||||
| ------------ | ----------- |
|
||||
| `login_type` | `password` |
|
||||
| `login_type` | `github` |
|
||||
| `login_type` | `oidc` |
|
||||
| `login_type` | `token` |
|
||||
| `login_type` | `none` |
|
||||
| `role` | `admin` |
|
||||
| `role` | `use` |
|
||||
| `status` | `active` |
|
||||
| `status` | `suspended` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
|
|
@ -279,6 +279,9 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
|||
"issuer_url": "string",
|
||||
"scopes": ["string"],
|
||||
"sign_in_text": "string",
|
||||
"user_role_field": "string",
|
||||
"user_role_mapping": {},
|
||||
"user_roles_default": ["string"],
|
||||
"username_field": "string"
|
||||
},
|
||||
"pg_connection_url": "string",
|
||||
|
|
|
@ -1000,6 +1000,7 @@
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1076,6 +1077,7 @@
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -2006,6 +2008,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
"issuer_url": "string",
|
||||
"scopes": ["string"],
|
||||
"sign_in_text": "string",
|
||||
"user_role_field": "string",
|
||||
"user_role_mapping": {},
|
||||
"user_roles_default": ["string"],
|
||||
"username_field": "string"
|
||||
},
|
||||
"pg_connection_url": "string",
|
||||
|
@ -2359,6 +2364,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
"issuer_url": "string",
|
||||
"scopes": ["string"],
|
||||
"sign_in_text": "string",
|
||||
"user_role_field": "string",
|
||||
"user_role_mapping": {},
|
||||
"user_roles_default": ["string"],
|
||||
"username_field": "string"
|
||||
},
|
||||
"pg_connection_url": "string",
|
||||
|
@ -2649,6 +2657,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -2865,6 +2874,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -3223,6 +3233,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
"issuer_url": "string",
|
||||
"scopes": ["string"],
|
||||
"sign_in_text": "string",
|
||||
"user_role_field": "string",
|
||||
"user_role_mapping": {},
|
||||
"user_roles_default": ["string"],
|
||||
"username_field": "string"
|
||||
}
|
||||
```
|
||||
|
@ -3245,6 +3258,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| `issuer_url` | string | false | | |
|
||||
| `scopes` | array of string | false | | |
|
||||
| `sign_in_text` | string | false | | |
|
||||
| `user_role_field` | string | false | | |
|
||||
| `user_role_mapping` | object | false | | |
|
||||
| `user_roles_default` | array of string | false | | |
|
||||
| `username_field` | string | false | | |
|
||||
|
||||
## codersdk.Organization
|
||||
|
@ -4304,6 +4320,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"role": "admin",
|
||||
"roles": [
|
||||
|
@ -4326,6 +4343,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `organization_ids` | array of string | false | | |
|
||||
| `role` | [codersdk.TemplateRole](#codersdktemplaterole) | false | | |
|
||||
| `roles` | array of [codersdk.Role](#codersdkrole) | false | | |
|
||||
|
@ -4352,6 +4370,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -4821,6 +4840,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -4842,6 +4862,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `organization_ids` | array of string | false | | |
|
||||
| `roles` | array of [codersdk.Role](#codersdkrole) | false | | |
|
||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||
|
|
|
@ -380,6 +380,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -461,6 +462,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -566,6 +568,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -878,6 +881,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -938,6 +942,7 @@ Status Code **200**
|
|||
| `»» email` | string(email) | true | | |
|
||||
| `»» id` | string(uuid) | true | | |
|
||||
| `»» last_seen_at` | string(date-time) | false | | |
|
||||
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»» organization_ids` | array | false | | |
|
||||
| `»» roles` | array | false | | |
|
||||
| `»»» display_name` | string | false | | |
|
||||
|
@ -972,6 +977,11 @@ Status Code **200**
|
|||
|
||||
| Property | Value |
|
||||
| ------------ | ----------------------------- |
|
||||
| `login_type` | `password` |
|
||||
| `login_type` | `github` |
|
||||
| `login_type` | `oidc` |
|
||||
| `login_type` | `token` |
|
||||
| `login_type` | `none` |
|
||||
| `status` | `active` |
|
||||
| `status` | `suspended` |
|
||||
| `error_code` | `MISSING_TEMPLATE_PARAMETER` |
|
||||
|
@ -1073,6 +1083,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1133,6 +1144,7 @@ Status Code **200**
|
|||
| `»» email` | string(email) | true | | |
|
||||
| `»» id` | string(uuid) | true | | |
|
||||
| `»» last_seen_at` | string(date-time) | false | | |
|
||||
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»» organization_ids` | array | false | | |
|
||||
| `»» roles` | array | false | | |
|
||||
| `»»» display_name` | string | false | | |
|
||||
|
@ -1167,6 +1179,11 @@ Status Code **200**
|
|||
|
||||
| Property | Value |
|
||||
| ------------ | ----------------------------- |
|
||||
| `login_type` | `password` |
|
||||
| `login_type` | `github` |
|
||||
| `login_type` | `oidc` |
|
||||
| `login_type` | `token` |
|
||||
| `login_type` | `none` |
|
||||
| `status` | `active` |
|
||||
| `status` | `suspended` |
|
||||
| `error_code` | `MISSING_TEMPLATE_PARAMETER` |
|
||||
|
@ -1212,6 +1229,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1302,6 +1320,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion}
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
|
|
@ -36,6 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/users \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -101,6 +102,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -359,6 +361,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -409,6 +412,7 @@ curl -X DELETE http://coder-server:8080/api/v2/users/{user} \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1002,6 +1006,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1052,6 +1057,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1112,6 +1118,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1162,6 +1169,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
@ -1212,6 +1220,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \
|
|||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "password",
|
||||
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
|
||||
"roles": [
|
||||
{
|
||||
|
|
|
@ -522,6 +522,37 @@ Issuer URL to use for Login with OIDC.
|
|||
|
||||
Scopes to grant when authenticating with OIDC.
|
||||
|
||||
### --oidc-user-role-default
|
||||
|
||||
| | |
|
||||
| ----------- | ------------------------------------------ |
|
||||
| Type | <code>string-array</code> |
|
||||
| Environment | <code>$CODER_OIDC_USER_ROLE_DEFAULT</code> |
|
||||
| YAML | <code>oidc.userRoleDefault</code> |
|
||||
|
||||
If user role sync is enabled, these roles are always included for all authenticated users. The 'member' role is always assigned.
|
||||
|
||||
### --oidc-user-role-field
|
||||
|
||||
| | |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_OIDC_USER_ROLE_FIELD</code> |
|
||||
| YAML | <code>oidc.userRoleField</code> |
|
||||
|
||||
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.
|
||||
|
||||
### --oidc-user-role-mapping
|
||||
|
||||
| | |
|
||||
| ----------- | ------------------------------------------ |
|
||||
| Type | <code>struct[map[string][]string]</code> |
|
||||
| Environment | <code>$CODER_OIDC_USER_ROLE_MAPPING</code> |
|
||||
| YAML | <code>oidc.userRoleMapping</code> |
|
||||
| Default | <code>{}</code> |
|
||||
|
||||
A map of the OIDC passed in user roles and the groups in Coder it should map to. This is useful if the group names do not match. If mapped to the empty string, the role will ignored.
|
||||
|
||||
### --oidc-username-field
|
||||
|
||||
| | |
|
||||
|
|
|
@ -337,6 +337,20 @@ can safely ignore these settings.
|
|||
--oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email)
|
||||
Scopes to grant when authenticating with OIDC.
|
||||
|
||||
--oidc-user-role-default string-array, $CODER_OIDC_USER_ROLE_DEFAULT
|
||||
If user role sync is enabled, these roles are always included for all
|
||||
authenticated users. The 'member' role is always assigned.
|
||||
|
||||
--oidc-user-role-field string, $CODER_OIDC_USER_ROLE_FIELD
|
||||
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.
|
||||
|
||||
--oidc-user-role-mapping struct[map[string][]string], $CODER_OIDC_USER_ROLE_MAPPING (default: {})
|
||||
A map of the OIDC passed in user roles and the groups in Coder it
|
||||
should map to. This is useful if the group names do not match. If
|
||||
mapped to the empty string, the role will ignored.
|
||||
|
||||
--oidc-username-field string, $CODER_OIDC_USERNAME_FIELD (default: preferred_username)
|
||||
OIDC claim field to use as the username.
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
|||
}()
|
||||
|
||||
api.AGPL.Options.SetUserGroups = api.setUserGroups
|
||||
api.AGPL.Options.SetUserSiteRoles = api.setUserSiteRoles
|
||||
api.AGPL.SiteHandler.AppearanceFetcher = api.fetchAppearanceConfig
|
||||
api.AGPL.SiteHandler.RegionsFetcher = func(ctx context.Context) (any, error) {
|
||||
// If the user can read the workspace proxy resource, return that.
|
||||
|
@ -405,6 +406,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
|||
// FeatureAdvancedTemplateScheduling.
|
||||
codersdk.FeatureTemplateRestartRequirement: api.DefaultQuietHoursSchedule != "",
|
||||
codersdk.FeatureWorkspaceProxy: true,
|
||||
codersdk.FeatureUserRoleManagement: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -56,6 +56,7 @@ func TestEntitlements(t *testing.T) {
|
|||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
codersdk.FeatureWorkspaceProxy: 1,
|
||||
codersdk.FeatureUserRoleManagement: 1,
|
||||
},
|
||||
GraceAt: time.Now().Add(59 * 24 * time.Hour),
|
||||
})
|
||||
|
|
|
@ -416,6 +416,7 @@ func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User
|
|||
OrganizationIDs: organizationIDs,
|
||||
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
|
||||
AvatarURL: user.AvatarURL.String,
|
||||
LoginType: codersdk.LoginType(user.LoginType),
|
||||
}
|
||||
|
||||
for _, roleName := range user.RBACRoles {
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
@ -50,3 +52,29 @@ func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uui
|
|||
return nil
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (api *API) setUserSiteRoles(ctx context.Context, db database.Store, userID uuid.UUID, roles []string) error {
|
||||
api.entitlementsMu.RLock()
|
||||
enabled := api.entitlements.Features[codersdk.FeatureUserRoleManagement].Enabled
|
||||
api.entitlementsMu.RUnlock()
|
||||
|
||||
if !enabled {
|
||||
api.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise entitlement, roles left unchanged",
|
||||
slog.F("user_id", userID), slog.F("roles", roles),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Should this be feature protected?
|
||||
return db.InTx(func(tx database.Store) error {
|
||||
_, err := coderd.UpdateSiteUserRoles(ctx, db, database.UpdateUserRolesParams{
|
||||
GrantedRoles: roles,
|
||||
ID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set user roles(%s): %w", userID.String(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/testutil"
|
||||
|
@ -24,6 +25,99 @@ import (
|
|||
// nolint:bodyclose
|
||||
func TestUserOIDC(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("RoleSync", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NewUserAndRemoveRoles", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
conf := coderdtest.NewOIDCConfig(t, "")
|
||||
|
||||
oidcRoleName := "TemplateAuthor"
|
||||
|
||||
config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
|
||||
cfg.UserRoleMapping = map[string][]string{oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}}
|
||||
})
|
||||
config.AllowSignups = true
|
||||
config.UserRoleField = "roles"
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
OIDCConfig: config,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureUserRoleManagement: 1},
|
||||
},
|
||||
})
|
||||
|
||||
admin, err := client.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, admin.OrganizationIDs, 1)
|
||||
|
||||
resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
|
||||
"email": "alice@coder.com",
|
||||
"roles": []string{"random", oidcRoleName, rbac.RoleOwner()},
|
||||
}))
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
user, err := client.User(ctx, "alice")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, user.Roles, 3)
|
||||
roleNames := []string{user.Roles[0].Name, user.Roles[1].Name, user.Roles[2].Name}
|
||||
require.ElementsMatch(t, roleNames, []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()})
|
||||
|
||||
// Now remove the roles with a new oidc login
|
||||
resp = oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
|
||||
"email": "alice@coder.com",
|
||||
"roles": []string{"random"},
|
||||
}))
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
user, err = client.User(ctx, "alice")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, user.Roles, 0)
|
||||
})
|
||||
t.Run("BlockAssignRoles", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
conf := coderdtest.NewOIDCConfig(t, "")
|
||||
|
||||
config := conf.OIDCConfig(t, jwt.MapClaims{})
|
||||
config.AllowSignups = true
|
||||
config.UserRoleField = "roles"
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
OIDCConfig: config,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureUserRoleManagement: 1},
|
||||
},
|
||||
})
|
||||
|
||||
admin, err := client.User(ctx, "me")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, admin.OrganizationIDs, 1)
|
||||
|
||||
resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
|
||||
"email": "alice@coder.com",
|
||||
"roles": []string{},
|
||||
}))
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
// Try to manually update user roles, even though controlled by oidc
|
||||
// role sync.
|
||||
_, err = client.UpdateUserRoles(ctx, "alice", codersdk.UpdateRoles{
|
||||
Roles: []string{
|
||||
rbac.RoleTemplateAdmin(),
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Cannot modify roles for OIDC users when role sync is enabled.")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Groups", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Assigns", func(t *testing.T) {
|
||||
|
|
|
@ -613,6 +613,12 @@ export interface OIDCConfig {
|
|||
// 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 user_role_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 user_role_mapping: any
|
||||
// This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
|
||||
readonly user_roles_default: string[]
|
||||
readonly sign_in_text: string
|
||||
readonly icon_url: string
|
||||
}
|
||||
|
@ -1162,6 +1168,7 @@ export interface User {
|
|||
readonly organization_ids: string[]
|
||||
readonly roles: Role[]
|
||||
readonly avatar_url: string
|
||||
readonly login_type: LoginType
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
|
@ -1564,6 +1571,7 @@ export type FeatureName =
|
|||
| "template_rbac"
|
||||
| "template_restart_requirement"
|
||||
| "user_limit"
|
||||
| "user_role_management"
|
||||
| "workspace_proxy"
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
"advanced_template_scheduling",
|
||||
|
@ -1577,6 +1585,7 @@ export const FeatureNames: FeatureName[] = [
|
|||
"template_rbac",
|
||||
"template_restart_requirement",
|
||||
"user_limit",
|
||||
"user_role_management",
|
||||
"workspace_proxy",
|
||||
]
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ Loading.args = {
|
|||
isLoading: true,
|
||||
roles: MockSiteRoles,
|
||||
selectedRoles: [MockUserAdminRole, MockOwnerRole],
|
||||
userLoginType: "password",
|
||||
oidcRoleSync: false,
|
||||
}
|
||||
Loading.parameters = {
|
||||
chromatic: { delay: 300 },
|
||||
|
|
|
@ -8,6 +8,12 @@ import { Stack } from "components/Stack/Stack"
|
|||
import Checkbox from "@mui/material/Checkbox"
|
||||
import UserIcon from "@mui/icons-material/PersonOutline"
|
||||
import { Role } from "api/typesGenerated"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "components/Tooltips/HelpTooltip"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
|
||||
const Option: React.FC<{
|
||||
value: string
|
||||
|
@ -46,6 +52,8 @@ export interface EditRolesButtonProps {
|
|||
selectedRoles: Role[]
|
||||
onChange: (roles: Role["name"][]) => void
|
||||
defaultIsOpen?: boolean
|
||||
oidcRoleSync: boolean
|
||||
userLoginType: string
|
||||
}
|
||||
|
||||
export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
||||
|
@ -54,6 +62,8 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
|||
onChange,
|
||||
isLoading,
|
||||
defaultIsOpen = false,
|
||||
userLoginType,
|
||||
oidcRoleSync,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("usersPage")
|
||||
|
@ -71,17 +81,30 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
|||
onChange([...selectedRoleNames, roleName])
|
||||
}
|
||||
|
||||
const canSetRoles =
|
||||
userLoginType !== "oidc" || (userLoginType === "oidc" && !oidcRoleSync)
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
ref={anchorRef}
|
||||
size="small"
|
||||
className={styles.editButton}
|
||||
title={t("editUserRolesTooltip") || ""}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<EditSquare />
|
||||
</IconButton>
|
||||
<Maybe condition={canSetRoles}>
|
||||
<IconButton
|
||||
ref={anchorRef}
|
||||
size="small"
|
||||
className={styles.editButton}
|
||||
title={t("editUserRolesTooltip") || ""}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<EditSquare />
|
||||
</IconButton>
|
||||
</Maybe>
|
||||
<Maybe condition={!canSetRoles}>
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>Externally controlled</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
Roles for this user are controlled by the OIDC identity provider.
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
</Maybe>
|
||||
|
||||
<Popover
|
||||
id={id}
|
||||
|
|
|
@ -36,6 +36,7 @@ export interface UsersTableProps {
|
|||
) => void
|
||||
isNonInitialPage: boolean
|
||||
actorID: string
|
||||
oidcRoleSyncEnabled: boolean
|
||||
}
|
||||
|
||||
export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
||||
|
@ -54,6 +55,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
|||
isLoading,
|
||||
isNonInitialPage,
|
||||
actorID,
|
||||
oidcRoleSyncEnabled,
|
||||
}) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
|
@ -91,6 +93,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
|
|||
onUpdateUserRoles={onUpdateUserRoles}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
actorID={actorID}
|
||||
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
|
@ -48,6 +48,10 @@ interface UsersTableBodyProps {
|
|||
) => void
|
||||
isNonInitialPage: boolean
|
||||
actorID: string
|
||||
// oidcRoleSyncEnabled should be set to false if unknown.
|
||||
// This is used to determine if the oidc roles are synced from the oidc idp and
|
||||
// editing via the UI should be disabled.
|
||||
oidcRoleSyncEnabled: boolean
|
||||
}
|
||||
|
||||
export const UsersTableBody: FC<
|
||||
|
@ -68,6 +72,7 @@ export const UsersTableBody: FC<
|
|||
isLoading,
|
||||
isNonInitialPage,
|
||||
actorID,
|
||||
oidcRoleSyncEnabled,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("usersPage")
|
||||
|
@ -127,6 +132,8 @@ export const UsersTableBody: FC<
|
|||
roles={roles ? sortRoles(roles) : []}
|
||||
selectedRoles={userRoles}
|
||||
isLoading={Boolean(isUpdatingUserRoles)}
|
||||
userLoginType={user.login_type}
|
||||
oidcRoleSync={oidcRoleSyncEnabled}
|
||||
onChange={(roles) => {
|
||||
// Remove the fallback role because it is only for the UI
|
||||
const rolesWithoutFallback = roles.filter(
|
||||
|
|
|
@ -38,6 +38,7 @@ describe("AccountPage", () => {
|
|||
roles: [],
|
||||
avatar_url: "",
|
||||
last_seen_at: new Date().toString(),
|
||||
login_type: "password",
|
||||
...data,
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -20,6 +20,7 @@ import { UsersPageView } from "./UsersPageView"
|
|||
import { useStatusFilterMenu } from "./UsersFilter"
|
||||
import { useFilter } from "components/Filter/filter"
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
||||
import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine"
|
||||
|
||||
export const Language = {
|
||||
suspendDialogTitle: "Suspend user",
|
||||
|
@ -61,7 +62,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
|||
count,
|
||||
} = usersState.context
|
||||
|
||||
const { updateUsers: canEditUsers } = usePermissions()
|
||||
const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions()
|
||||
const [rolesState] = useMachine(siteRolesMachine, {
|
||||
context: {
|
||||
hasPermission: canEditUsers,
|
||||
|
@ -69,6 +70,16 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
|||
})
|
||||
const { roles } = rolesState.context
|
||||
|
||||
// Ideally this only runs if 'canViewDeployment' is true.
|
||||
// TODO: Prevent api call if the user does not have the perms.
|
||||
const [state] = useMachine(deploymentConfigMachine)
|
||||
const { deploymentValues } = state.context
|
||||
// Indicates if oidc roles are synced from the oidc idp.
|
||||
// Assign 'false' if unknown.
|
||||
const oidcRoleSyncEnabled =
|
||||
viewDeploymentValues &&
|
||||
deploymentValues?.config.oidc?.user_role_field !== ""
|
||||
|
||||
// Is loading if
|
||||
// - users are loading or
|
||||
// - the user can edit the users but the roles are loading
|
||||
|
@ -102,6 +113,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
|||
<title>{pageTitle("Users")}</title>
|
||||
</Helmet>
|
||||
<UsersPageView
|
||||
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
|
||||
roles={roles}
|
||||
users={users}
|
||||
count={count}
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface UsersPageViewProps {
|
|||
roles?: TypesGen.AssignableRoles[]
|
||||
isUpdatingUserRoles?: boolean
|
||||
canEditUsers?: boolean
|
||||
oidcRoleSyncEnabled: boolean
|
||||
canViewActivity?: boolean
|
||||
isLoading?: boolean
|
||||
onSuspendUser: (user: TypesGen.User) => void
|
||||
|
@ -47,6 +48,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
|||
onUpdateUserRoles,
|
||||
isUpdatingUserRoles,
|
||||
canEditUsers,
|
||||
oidcRoleSyncEnabled,
|
||||
canViewActivity,
|
||||
isLoading,
|
||||
filterProps,
|
||||
|
@ -77,6 +79,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
|
|||
onUpdateUserRoles={onUpdateUserRoles}
|
||||
isUpdatingUserRoles={isUpdatingUserRoles}
|
||||
canEditUsers={canEditUsers}
|
||||
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
|
||||
canViewActivity={canViewActivity}
|
||||
isLoading={isLoading}
|
||||
isNonInitialPage={isNonInitialPage}
|
||||
|
|
|
@ -268,6 +268,7 @@ export const MockUser: TypesGen.User = {
|
|||
roles: [MockOwnerRole],
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
}
|
||||
|
||||
export const MockUserAdmin: TypesGen.User = {
|
||||
|
@ -280,6 +281,7 @@ export const MockUserAdmin: TypesGen.User = {
|
|||
roles: [MockUserAdminRole],
|
||||
avatar_url: "",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
}
|
||||
|
||||
export const MockUser2: TypesGen.User = {
|
||||
|
@ -292,6 +294,7 @@ export const MockUser2: TypesGen.User = {
|
|||
roles: [],
|
||||
avatar_url: "",
|
||||
last_seen_at: "2022-09-14T19:12:21Z",
|
||||
login_type: "oidc",
|
||||
}
|
||||
|
||||
export const SuspendedMockUser: TypesGen.User = {
|
||||
|
@ -304,6 +307,7 @@ export const SuspendedMockUser: TypesGen.User = {
|
|||
roles: [],
|
||||
avatar_url: "",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
}
|
||||
|
||||
export const MockProvisioner: TypesGen.ProvisionerDaemon = {
|
||||
|
|
Loading…
Reference in New Issue