Repository: dexidp/dex
Pull request #1684 (2020-04-04 23:59:25)

Allow preferred_username claim to be set for Crowd connector
**What this is**
In the current implementation, the `preferred_username` claim remains empty. Some clients might require that claim and Dex supports it, if set from the connector's side.

This PR allows the connector to **optionally** support the `preferred_username` claim. It achieves this by allowing a user to choose to fill the `preferred_username` claim by one of the available Crowd fields. Specifically: `key`, `name` or `email`. Invalid options cause the claim to remain empty.

**Breaking change:** No. Existing configuration should not notice this change.
**Optional:** Yes.

**Notes:**
- Added test for `identityFromCrowdUser` function which was not extensively tested before
- Added entry for the Crowd connector in the readme
- Updated documentation for the connector
- I'm fairly new to Go, so I welcome constructive feedback

mvdkleijn: Hi @srenatus @bonifaido @sagikazarmark - I was wondering if I could get some feedback on what this still needs or possibly just have this approved... It's for the recently added Crowd connector. :smiley:

Passes the checks and latest changes from master were merged in.
nabokihms: @mvdkleijn thanks a lot for your work. This is wonderful! It would be great to get it merged.
mvdkleijn: The build / checks failed due to stability issues with Github. Can they be re-triggered manually or do I need to push a dummy commit?
mvdkleijn: Thanks for the retrigger @bonifaido Sadly Github is still having issues. I'll check back later. This PR should be good to merge anyway. I don't believe this needs anything else and the change is backwards compatible.

Patch:
diff --git a/Documentation/connectors/atlassian-crowd.md b/Documentation/connectors/atlassian-crowd.md
index 3a5f0712..8dbaef80 100644
--- a/Documentation/connectors/atlassian-crowd.md
+++ b/Documentation/connectors/atlassian-crowd.md
@@ -36,4 +36,9 @@ connectors:
     - my-group
     # Prompt for username field.
     usernamePrompt: Login
+    # Optionally set preferred_username claim.
+    # If `preferredUsernameField` is omitted or contains an invalid option, the `preferred_username` claim will be empty.
+    # If `preferredUsernameField` is set, the `preferred_username` claim will be set to the chosen Crowd user attribute value.
+    # Possible choices are: "key", "name", "email"
+    preferredUsernameField: name
 ```
diff --git a/README.md b/README.md
index 65384cda..bbfb4f29 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@ Dex implements the following connectors:
 | [AuthProxy](Documentation/connectors/authproxy.md) | no | no | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. |
 | [Bitbucket Cloud](Documentation/connectors/bitbucketcloud.md) | yes | yes | no | alpha | |
 | [OpenShift](Documentation/connectors/openshift.md) | no | yes | no | stable | |
+| [Atlassian Crowd](Documentation/connectors/atlassiancrowd.md) | yes | yes | yes *) | beta | preferred_username claim must be configured through config |
 
 Stable, beta, and alpha are defined as:
 
diff --git a/connector/atlassiancrowd/atlassiancrowd.go b/connector/atlassiancrowd/atlassiancrowd.go
index d835d4cf..928e69a6 100644
--- a/connector/atlassiancrowd/atlassiancrowd.go
+++ b/connector/atlassiancrowd/atlassiancrowd.go
@@ -35,6 +35,7 @@ import (
 //       - admin
 //       # Prompt for username field
 //       usernamePrompt: Login
+//		 preferredUsernameField: name
 //
 type Config struct {
 	BaseURL      string   `json:"baseURL"`
@@ -42,6 +43,11 @@ type Config struct {
 	ClientSecret string   `json:"clientSecret"`
 	Groups       []string `json:"groups"`
 
+	// PreferredUsernameField allows users to set the field to any of the
+	// following values: "key", "name" or "email".
+	// If unset, the preferred_username field will remain empty.
+	PreferredUsernameField string `json:"preferredUsernameField"`
+
 	// UsernamePrompt allows users to override the username attribute (displayed
 	// in the username/password prompt). If unset, the handler will use.
 	// "Username".
@@ -368,6 +374,19 @@ func (c *crowdConnector) identityFromCrowdUser(user crowdUser) (connector.Identi
 		EmailVerified: true,
 	}
 
+	switch c.PreferredUsernameField {
+	case "key":
+		identity.PreferredUsername = user.Key
+	case "name":
+		identity.PreferredUsername = user.Name
+	case "email":
+		identity.PreferredUsername = user.Email
+	default:
+		if c.PreferredUsernameField != "" {
+			c.logger.Warnf("preferred_username left empty. Invalid crowd field mapped to preferred_username: %s", c.PreferredUsernameField)
+		}
+	}
+
 	return identity, nil
 }
 


Test patch:
diff --git a/connector/atlassiancrowd/atlassiancrowd_test.go b/connector/atlassiancrowd/atlassiancrowd_test.go
index 966f5b6f..213c1829 100644
--- a/connector/atlassiancrowd/atlassiancrowd_test.go
+++ b/connector/atlassiancrowd/atlassiancrowd_test.go
@@ -104,6 +104,53 @@ func TestUserPassword(t *testing.T) {
 	expectNil(t, err)
 }
 
+func TestIdentityFromCrowdUser(t *testing.T) {
+	user := crowdUser{
+		Key:    "12345",
+		Name:   "testuser",
+		Active: true,
+		Email:  "testuser@example.com",
+	}
+
+	c := newTestCrowdConnector("/")
+
+	// Sanity checks
+	expectEquals(t, user.Name, "testuser")
+	expectEquals(t, user.Email, "testuser@example.com")
+
+	// Test unconfigured behaviour
+	i, err := c.identityFromCrowdUser(user)
+	expectNil(t, err)
+	expectEquals(t, i.UserID, "12345")
+	expectEquals(t, i.Username, "testuser")
+	expectEquals(t, i.Email, "testuser@example.com")
+	expectEquals(t, i.EmailVerified, true)
+
+	// Test for various PreferredUsernameField settings
+	// unset
+	expectEquals(t, i.PreferredUsername, "")
+
+	c.Config.PreferredUsernameField = "key"
+	i, err = c.identityFromCrowdUser(user)
+	expectNil(t, err)
+	expectEquals(t, i.PreferredUsername, "12345")
+
+	c.Config.PreferredUsernameField = "name"
+	i, err = c.identityFromCrowdUser(user)
+	expectNil(t, err)
+	expectEquals(t, i.PreferredUsername, "testuser")
+
+	c.Config.PreferredUsernameField = "email"
+	i, err = c.identityFromCrowdUser(user)
+	expectNil(t, err)
+	expectEquals(t, i.PreferredUsername, "testuser@example.com")
+
+	c.Config.PreferredUsernameField = "invalidstring"
+	i, err = c.identityFromCrowdUser(user)
+	expectNil(t, err)
+	expectEquals(t, i.PreferredUsername, "")
+}
+
 type TestServerResponse struct {
 	Body interface{}
 	Code int