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:
Steven Masley
2023-03-09 23:31:38 -06:00
committed by GitHub
parent 11a930e779
commit 7f25d31745
14 changed files with 170 additions and 46 deletions

3
coderd/apidoc/docs.go generated
View File

@ -7078,6 +7078,9 @@ const docTemplate = `{
"type": "string"
}
},
"groups_field": {
"type": "string"
},
"icon_url": {
"$ref": "#/definitions/clibase.URL"
},

View File

@ -6338,6 +6338,9 @@
"type": "string"
}
},
"groups_field": {
"type": "string"
},
"icon_url": {
"$ref": "#/definitions/clibase.URL"
},

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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 {