Skip to content

feat(coderd): set full name from IDP name claim #13468

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 11 commits into from
Jun 6, 2024
1 change: 1 addition & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*co
EmailDomain: vals.OIDC.EmailDomain,
AllowSignups: vals.OIDC.AllowSignups.Value(),
UsernameField: vals.OIDC.UsernameField.String(),
NameField: vals.OIDC.NameField.String(),
EmailField: vals.OIDC.EmailField.String(),
AuthURLParams: vals.OIDC.AuthURLParams.Value,
IgnoreUserInfo: vals.OIDC.IgnoreUserInfo.Value(),
Expand Down
3 changes: 3 additions & 0 deletions cli/testdata/coder_server_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ OIDC OPTIONS:
--oidc-issuer-url string, $CODER_OIDC_ISSUER_URL
Issuer URL to use for Login with OIDC.

--oidc-name-field string, $CODER_OIDC_NAME_FIELD (default: name)
OIDC claim field to use as the name.

--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
Expand Down
3 changes: 3 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ oidc:
# OIDC claim field to use as the username.
# (default: preferred_username, type: string)
usernameField: preferred_username
# OIDC claim field to use as the name.
# (default: name, type: string)
nameField: name
# OIDC claim field to use as the email.
# (default: email, type: string)
emailField: email
Expand Down
3 changes: 3 additions & 0 deletions coderd/apidoc/docs.go

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

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

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

11 changes: 11 additions & 0 deletions coderd/httpapi/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,14 @@ func UserRealNameValid(str string) error {
}
return nil
}

// NormalizeUserRealName normalizes a user name such that it will pass
// validation by UserRealNameValid. This is done to avoid blocking
// little Bobby Whitespace from using Coder.
func NormalizeRealUsername(str string) string {
s := strings.TrimSpace(str)
if len(s) > 128 {
s = s[:128]
}
return s
}
17 changes: 14 additions & 3 deletions coderd/httpapi/name_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package httpapi_test

import (
"strings"
"testing"

"github.com/moby/moby/pkg/namesgenerator"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/httpapi"
Expand Down Expand Up @@ -217,6 +219,10 @@ func TestUserRealNameValid(t *testing.T) {
Name string
Valid bool
}{
{"", true},
{" a", false},
{"a ", false},
{" a ", false},
{"1", true},
{"A", true},
{"A1", true},
Expand All @@ -229,17 +235,22 @@ func TestUserRealNameValid(t *testing.T) {
{"Małgorzata Kalinowska-Iszkowska", true},
{"成龍", true},
{". .", true},

{"Lord Voldemort ", false},
{" Bellatrix Lestrange", false},
{" ", false},
{strings.Repeat("a", 128), true},
{strings.Repeat("a", 129), false},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
valid := httpapi.UserRealNameValid(testCase.Name)
require.Equal(t, testCase.Valid, valid == nil)
err := httpapi.UserRealNameValid(testCase.Name)
norm := httpapi.NormalizeRealUsername(testCase.Name)
normErr := httpapi.UserRealNameValid(norm)
assert.NoError(t, normErr)
assert.Equal(t, testCase.Valid, err == nil)
assert.Equal(t, testCase.Valid, norm == testCase.Name, "invalid name should be different after normalization")
})
}
}
24 changes: 23 additions & 1 deletion coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,9 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
return
}

ghName := ghUser.GetName()
normName := httpapi.NormalizeRealUsername(ghName)

// If we have a nil GitHub ID, that is a big problem. That would mean we link
// this user and all other users with this bug to the same uuid.
// We should instead throw an error. This should never occur in production.
Expand Down Expand Up @@ -652,6 +655,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
Email: verifiedEmail.GetEmail(),
Username: ghUser.GetLogin(),
AvatarURL: ghUser.GetAvatarURL(),
Name: normName,
DebugContext: OauthDebugContext{},
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
Expand Down Expand Up @@ -701,6 +705,9 @@ type OIDCConfig struct {
// EmailField selects the claim field to be used as the created user's
// email.
EmailField string
// NameField selects the claim field to be used as the created user's
// full / given name.
NameField string
// AuthURLParams are additional parameters to be passed to the OIDC provider
// when requesting an access token.
AuthURLParams map[string]string
Expand Down Expand Up @@ -952,13 +959,22 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
}
}

// The 'name' is an optional property in Coder. If not specified,
// it will be left blank.
var name string
nameRaw, ok := mergedClaims[api.OIDCConfig.NameField]
if ok {
name, _ = nameRaw.(string)
name = httpapi.NormalizeRealUsername(name)
}

var picture string
pictureRaw, ok := mergedClaims["picture"]
if ok {
picture, _ = pictureRaw.(string)
}

ctx = slog.With(ctx, slog.F("email", email), slog.F("username", username))
ctx = slog.With(ctx, slog.F("email", email), slog.F("username", username), slog.F("name", name))
usingGroups, groups, groupErr := api.oidcGroups(ctx, mergedClaims)
if groupErr != nil {
groupErr.Write(rw, r)
Expand Down Expand Up @@ -996,6 +1012,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
AllowSignups: api.OIDCConfig.AllowSignups,
Email: email,
Username: username,
Name: name,
AvatarURL: picture,
UsingRoles: api.OIDCConfig.RoleSyncEnabled(),
Roles: roles,
Expand Down Expand Up @@ -1222,6 +1239,7 @@ type oauthLoginParams struct {
AllowSignups bool
Email string
Username string
Name string
AvatarURL string
// Is UsingGroups is true, then the user will be assigned
// to the Groups provided.
Expand Down Expand Up @@ -1544,6 +1562,10 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
user.AvatarURL = params.AvatarURL
needsUpdate = true
}
if user.Name != params.Name {
user.Name = params.Name
needsUpdate = true
}

// If the upstream email or username has changed we should mirror
// that in Coder. Many enterprises use a user's email/username as
Expand Down
Loading
Loading