From 458585008b04109ce3462519ca0562a8027eedb1 Mon Sep 17 00:00:00 2001 From: Maxime Desrosiers Date: Wed, 8 May 2019 16:06:19 -0400 Subject: [PATCH] microsoft: option for group UUIDs instead of name and group whitelist --- Documentation/connectors/microsoft.md | 6 ++ connector/microsoft/microsoft.go | 88 ++++++++++++++++++--------- 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Documentation/connectors/microsoft.md b/Documentation/connectors/microsoft.md index 11024a6d..4d5dafc8 100644 --- a/Documentation/connectors/microsoft.md +++ b/Documentation/connectors/microsoft.md @@ -88,6 +88,9 @@ a member of. `onlySecurityGroups` configuration option restricts the list to include only security groups. By default all groups (security, Office 365, mailing lists) are included. +By default, dex resolve groups ids to groups names, to keep groups ids, you can +specify the configuration option `groupNameFormat: id`. + It is possible to require a user to be a member of a particular group in order to be successfully authenticated in dex. For example, with the following configuration file only the users who are members of at least one of the listed @@ -110,3 +113,6 @@ connectors: - developers - devops ``` + +Also, `useGroupsAsWhitelist` configuration option, can restrict the groups +claims to include only the user's groups that are in the configured `groups`. \ No newline at end of file diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go index b31cfa55..d4ce2e67 100644 --- a/connector/microsoft/microsoft.go +++ b/connector/microsoft/microsoft.go @@ -19,35 +19,50 @@ import ( "github.com/dexidp/dex/pkg/log" ) +// GroupNameFormat represents the format of the group identifier +// we use type of string instead of int because it's easier to +// marshall/unmarshall +type GroupNameFormat string + +// Possible values for GroupNameFormat +const ( + GroupID GroupNameFormat = "id" + GroupName GroupNameFormat = "name" +) + const ( apiURL = "https://graph.microsoft.com" // Microsoft requires this scope to access user's profile scopeUser = "user.read" // Microsoft requires this scope to list groups the user is a member of - // and resolve their UUIDs to groups names. + // and resolve their ids to groups names. scopeGroups = "directory.read.all" ) // Config holds configuration options for microsoft logins. type Config struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - Tenant string `json:"tenant"` - OnlySecurityGroups bool `json:"onlySecurityGroups"` - Groups []string `json:"groups"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + Tenant string `json:"tenant"` + OnlySecurityGroups bool `json:"onlySecurityGroups"` + Groups []string `json:"groups"` + GroupNameFormat GroupNameFormat `json:"groupNameFormat"` + UseGroupsAsWhitelist bool `json:"useGroupsAsWhitelist"` } // Open returns a strategy for logging in through Microsoft. func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { m := microsoftConnector{ - redirectURI: c.RedirectURI, - clientID: c.ClientID, - clientSecret: c.ClientSecret, - tenant: c.Tenant, - onlySecurityGroups: c.OnlySecurityGroups, - groups: c.Groups, - logger: logger, + redirectURI: c.RedirectURI, + clientID: c.ClientID, + clientSecret: c.ClientSecret, + tenant: c.Tenant, + onlySecurityGroups: c.OnlySecurityGroups, + groups: c.Groups, + groupNameFormat: c.GroupNameFormat, + useGroupsAsWhitelist: c.UseGroupsAsWhitelist, + logger: logger, } // By default allow logins from both personal and business/school // accounts. @@ -55,6 +70,15 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) m.tenant = "common" } + // By default, use group names + switch m.groupNameFormat { + case "": + m.groupNameFormat = GroupName + case GroupID, GroupName: + default: + return nil, fmt.Errorf("invalid groupNameFormat: %s", m.groupNameFormat) + } + return &m, nil } @@ -70,13 +94,15 @@ var ( ) type microsoftConnector struct { - redirectURI string - clientID string - clientSecret string - tenant string - onlySecurityGroups bool - groups []string - logger log.Logger + redirectURI string + clientID string + clientSecret string + tenant string + onlySecurityGroups bool + groupNameFormat GroupNameFormat + groups []string + useGroupsAsWhitelist bool + logger log.Logger } func (c *microsoftConnector) isOrgTenant() bool { @@ -300,24 +326,28 @@ type group struct { Name string `json:"displayName"` } -func (c *microsoftConnector) getGroups(ctx context.Context, client *http.Client, userID string) (groups []string, err error) { - ids, err := c.getGroupIDs(ctx, client) +func (c *microsoftConnector) getGroups(ctx context.Context, client *http.Client, userID string) ([]string, error) { + userGroups, err := c.getGroupIDs(ctx, client) if err != nil { - return groups, err + return nil, err } - groups, err = c.getGroupNames(ctx, client, ids) - if err != nil { - return + if c.groupNameFormat == GroupName { + userGroups, err = c.getGroupNames(ctx, client, userGroups) + if err != nil { + return nil, err + } } // ensure that the user is in at least one required group - filteredGroups := groups_pkg.Filter(groups, c.groups) + filteredGroups := groups_pkg.Filter(userGroups, c.groups) if len(c.groups) > 0 && len(filteredGroups) == 0 { return nil, fmt.Errorf("microsoft: user %v not in any of the required groups", userID) + } else if c.useGroupsAsWhitelist { + return filteredGroups, nil } - return + return userGroups, nil } func (c *microsoftConnector) getGroupIDs(ctx context.Context, client *http.Client) (ids []string, err error) {