diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index fecfe6200888a5c6b4567c91fdefea1eb5611470..21129f22272332a6634bed6c9cbfd34fddecf64d 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -89,6 +89,27 @@ type Config struct { // Configurable key which contains the groups claims GroupsKey string `json:"groups"` // defaults to "groups" } `json:"claimMapping"` + + // ClaimMutations holds all claim mutations options + ClaimMutations struct { + NewGroupFromClaims []NewGroupFromClaims `json:"newGroupFromClaims"` + } `json:"claimModifications"` +} + +// NewGroupFromClaims creates a new group from a list of claims and appends it to the list of existing groups. +type NewGroupFromClaims struct { + // List of claim to join together + Claims []string `json:"claims"` + + // String to separate the claims + Delimiter string `json:"delimiter"` + + // Should Dex remove the Delimiter string from claim values + // This is done to keep resulting claim structure in full control of the Dex operator + ClearDelimiter bool `json:"clearDelimiter"` + + // String to place before the first claim + Prefix string `json:"prefix"` } // Domains that don't support basic auth. golang.org/x/oauth2 has an internal @@ -192,6 +213,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey, emailKey: c.ClaimMapping.EmailKey, groupsKey: c.ClaimMapping.GroupsKey, + newGroupFromClaims: c.ClaimMutations.NewGroupFromClaims, }, nil } @@ -220,6 +242,7 @@ type oidcConnector struct { preferredUsernameKey string emailKey string groupsKey string + newGroupFromClaims []NewGroupFromClaims } func (c *oidcConnector) Close() error { @@ -443,6 +466,30 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I } } + for _, config := range c.newGroupFromClaims { + newGroupSegments := []string{ + config.Prefix, + } + for _, claimName := range config.Claims { + claimValue, ok := claims[claimName].(string) + if !ok { // Non string claim value are ignored, concatenating them doesn't really make any sense + continue + } + + if config.ClearDelimiter { + // Removing the delimiter string from the concatenated claim to ensure resulting claim structure + // is in full control of Dex operator + claimValue = strings.ReplaceAll(claimValue, config.Delimiter, "") + } + + newGroupSegments = append(newGroupSegments, claimValue) + } + + if len(newGroupSegments) > 1 { + groups = append(groups, strings.Join(newGroupSegments, config.Delimiter)) + } + } + cd := connectorData{ RefreshToken: []byte(token.RefreshToken), } diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go index 29e8875ea760daff36d43b2da26a9a86dc1b1a1e..4bb84a40d62f11170a5bafbf6102c992ddfe09dd 100644 --- a/connector/oidc/oidc_test.go +++ b/connector/oidc/oidc_test.go @@ -62,6 +62,7 @@ func TestHandleCallback(t *testing.T) { expectPreferredUsername string expectedEmailField string token map[string]interface{} + newGroupFromClaims []NewGroupFromClaims }{ { name: "simpleCase", @@ -288,6 +289,79 @@ func TestHandleCallback(t *testing.T) { "email_verified": true, }, }, + { + name: "newGroupFromClaims", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "gh::acme::pipeline-one", "clr_delim-acme-foobar", "keep_delim-acme-foo-bar", "bk-emailvalue"}, + expectedEmailField: "emailvalue", + newGroupFromClaims: []NewGroupFromClaims{ + { // The basic functionality, should create "gh::acme::pipeline-one". + Claims: []string{ + "organization", + "pipeline", + }, + Delimiter: "::", + Prefix: "gh", + }, + { // Non existing claims, should not generate any any new group claim. + Claims: []string{ + "non-existing1", + "non-existing2", + }, + Delimiter: "::", + Prefix: "tfe", + }, + { // In this case the delimiter character("-") should be removed removed from "claim-with-delimiter" claim to ensure the resulting + // claim structure is in full control of the Dex operator and not the person creating a new pipeline. + // Should create "clr_delim-acme-foobar" and not "tfe-acme-foo-bar". + Claims: []string{ + "organization", + "claim-with-delimiter", + }, + Delimiter: "-", + ClearDelimiter: true, + Prefix: "clr_delim", + }, + { // In this case the delimiter character("-") should be NOT removed from "claim-with-delimiter" claim. + // Should create "keep_delim-acme-foo-bar". + Claims: []string{ + "organization", + "claim-with-delimiter", + }, + Delimiter: "-", + // ClearDelimiter: false, + Prefix: "keep_delim", + }, + { // Ignore non string claims (like arrays), this should result in "bk-emailvalue". + Claims: []string{ + "non-string-claim", + "non-string-claim2", + "email", + }, + Delimiter: "-", + Prefix: "bk", + }, + }, + + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": "group1", + "organization": "acme", + "pipeline": "pipeline-one", + "email": "emailvalue", + "email_verified": true, + "claim-with-delimiter": "foo-bar", + "non-string-claim": []string{ + "element1", + "element2", + }, + "non-string-claim2": 666, + }, + }, } for _, tc := range tests { @@ -323,6 +397,7 @@ func TestHandleCallback(t *testing.T) { config.ClaimMapping.PreferredUsernameKey = tc.preferredUsernameKey config.ClaimMapping.EmailKey = tc.emailKey config.ClaimMapping.GroupsKey = tc.groupsKey + config.ClaimMutations.NewGroupFromClaims = tc.newGroupFromClaims conn, err := newConnector(config) if err != nil {