mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat: Allow changing the 'group' oidc claim field (#6546)
* feat: Allow changing the 'group' oidc claim field * Enable empty groups support * fix: Delete was wiping all groups, not just the single user's groups * Update docs * fix: Dbfake delete group member fixed
This commit is contained in:
3
coderd/apidoc/docs.go
generated
3
coderd/apidoc/docs.go
generated
@ -7078,6 +7078,9 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"groups_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon_url": {
|
||||
"$ref": "#/definitions/clibase.URL"
|
||||
},
|
||||
|
3
coderd/apidoc/swagger.json
generated
3
coderd/apidoc/swagger.json
generated
@ -6338,6 +6338,9 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"groups_field": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon_url": {
|
||||
"$ref": "#/definitions/clibase.URL"
|
||||
},
|
||||
|
@ -939,7 +939,7 @@ func (o *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(signed))
|
||||
}
|
||||
|
||||
func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims) *coderd.OIDCConfig {
|
||||
func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig {
|
||||
// By default, the provider can be empty.
|
||||
// This means it won't support any endpoints!
|
||||
provider := &oidc.Provider{}
|
||||
@ -956,7 +956,7 @@ func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims) *cod
|
||||
}
|
||||
provider = cfg.NewProvider(context.Background())
|
||||
}
|
||||
return &coderd.OIDCConfig{
|
||||
cfg := &coderd.OIDCConfig{
|
||||
OAuth2Config: o,
|
||||
Verifier: oidc.NewVerifier(o.issuer, &oidc.StaticKeySet{
|
||||
PublicKeys: []crypto.PublicKey{o.key.Public()},
|
||||
@ -965,7 +965,12 @@ func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims) *cod
|
||||
}),
|
||||
Provider: provider,
|
||||
UsernameField: "preferred_username",
|
||||
GroupField: "groups",
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// NewAzureInstanceIdentity returns a metadata client and ID token validator for faking
|
||||
|
@ -3905,13 +3905,22 @@ func (q *fakeQuerier) DeleteGroupMembersByOrgAndUser(_ context.Context, arg data
|
||||
|
||||
newMembers := q.groupMembers[:0]
|
||||
for _, member := range q.groupMembers {
|
||||
if member.UserID == arg.UserID {
|
||||
if member.UserID != arg.UserID {
|
||||
// Do not delete the other members
|
||||
newMembers = append(newMembers, member)
|
||||
} else if member.UserID == arg.UserID {
|
||||
// We only want to delete from groups in the organization in the args.
|
||||
for _, group := range q.groups {
|
||||
if group.ID == member.GroupID && group.OrganizationID == arg.OrganizationID {
|
||||
continue
|
||||
// Find the group that the member is apartof.
|
||||
if group.ID == member.GroupID {
|
||||
// Only add back the member if the organization ID does not match
|
||||
// the arg organization ID. Since the arg is saying which
|
||||
// org to delete.
|
||||
if group.OrganizationID != arg.OrganizationID {
|
||||
newMembers = append(newMembers, member)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
newMembers = append(newMembers, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -960,25 +960,19 @@ func (q *sqlQuerier) DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteG
|
||||
|
||||
const deleteGroupMembersByOrgAndUser = `-- name: DeleteGroupMembersByOrgAndUser :exec
|
||||
DELETE FROM
|
||||
group_members
|
||||
USING
|
||||
group_members AS gm
|
||||
LEFT JOIN
|
||||
groups
|
||||
ON
|
||||
groups.id = gm.group_id
|
||||
group_members
|
||||
WHERE
|
||||
groups.organization_id = $1 AND
|
||||
gm.user_id = $2
|
||||
group_members.user_id = $1
|
||||
AND group_id = ANY(SELECT id FROM groups WHERE organization_id = $2)
|
||||
`
|
||||
|
||||
type DeleteGroupMembersByOrgAndUserParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) DeleteGroupMembersByOrgAndUser(ctx context.Context, arg DeleteGroupMembersByOrgAndUserParams) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteGroupMembersByOrgAndUser, arg.OrganizationID, arg.UserID)
|
||||
_, err := q.db.ExecContext(ctx, deleteGroupMembersByOrgAndUser, arg.UserID, arg.OrganizationID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -35,16 +35,10 @@ FROM
|
||||
|
||||
-- name: DeleteGroupMembersByOrgAndUser :exec
|
||||
DELETE FROM
|
||||
group_members
|
||||
USING
|
||||
group_members AS gm
|
||||
LEFT JOIN
|
||||
groups
|
||||
ON
|
||||
groups.id = gm.group_id
|
||||
group_members
|
||||
WHERE
|
||||
groups.organization_id = @organization_id AND
|
||||
gm.user_id = @user_id;
|
||||
group_members.user_id = @user_id
|
||||
AND group_id = ANY(SELECT id FROM groups WHERE organization_id = @organization_id);
|
||||
|
||||
-- name: InsertGroupMember :exec
|
||||
INSERT INTO
|
||||
|
@ -477,6 +477,10 @@ type OIDCConfig struct {
|
||||
// UsernameField selects the claim field to be used as the created user's
|
||||
// username.
|
||||
UsernameField string
|
||||
// GroupField selects the claim field to be used as the created user's
|
||||
// groups. If the group field is the empty string, then no group updates
|
||||
// will ever come from the OIDC provider.
|
||||
GroupField 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
|
||||
@ -609,21 +613,27 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var usingGroups bool
|
||||
var groups []string
|
||||
groupsRaw, ok := claims["groups"]
|
||||
if ok {
|
||||
// Convert the []interface{} we get to a []string.
|
||||
groupsInterface, ok := groupsRaw.([]interface{})
|
||||
if ok {
|
||||
for _, groupInterface := range groupsInterface {
|
||||
group, ok := groupInterface.(string)
|
||||
if !ok {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid group type. Expected string, got: %t", emailRaw),
|
||||
})
|
||||
return
|
||||
// If the GroupField is the empty string, then groups from OIDC are not used.
|
||||
// This is so we can support manual group assignment.
|
||||
if api.OIDCConfig.GroupField != "" {
|
||||
usingGroups = true
|
||||
groupsRaw, ok := claims[api.OIDCConfig.GroupField]
|
||||
if ok && api.OIDCConfig.GroupField != "" {
|
||||
// Convert the []interface{} we get to a []string.
|
||||
groupsInterface, ok := groupsRaw.([]interface{})
|
||||
if ok {
|
||||
for _, groupInterface := range groupsInterface {
|
||||
group, ok := groupInterface.(string)
|
||||
if !ok {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid group type. Expected string, got: %t", emailRaw),
|
||||
})
|
||||
return
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -684,6 +694,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
||||
Email: email,
|
||||
Username: username,
|
||||
AvatarURL: picture,
|
||||
UsingGroups: usingGroups,
|
||||
Groups: groups,
|
||||
})
|
||||
var httpErr httpError
|
||||
@ -725,7 +736,10 @@ type oauthLoginParams struct {
|
||||
Email string
|
||||
Username string
|
||||
AvatarURL string
|
||||
Groups []string
|
||||
// Is UsingGroups is true, then the user will be assigned
|
||||
// to the Groups provided.
|
||||
UsingGroups bool
|
||||
Groups []string
|
||||
}
|
||||
|
||||
type httpError struct {
|
||||
@ -865,7 +879,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
|
||||
}
|
||||
|
||||
// Ensure groups are correct.
|
||||
if len(params.Groups) > 0 {
|
||||
if params.UsingGroups {
|
||||
//nolint:gocritic
|
||||
err := api.Options.SetUserGroups(dbauthz.AsSystemRestricted(ctx), tx, user.ID, params.Groups)
|
||||
if err != nil {
|
||||
|
Reference in New Issue
Block a user