Merge pull request #1446 from maksd/microsoft-groups-uuid-whitelist

microsoft: option for group UUIDs instead of name and group whitelist
This commit is contained in:
Stephan Renatus 2019-07-25 16:21:48 +02:00 committed by GitHub
commit 6e98c04f9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 65 additions and 29 deletions

View file

@ -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, include only security groups. By default all groups (security, Office 365,
mailing lists) are included. 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 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 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 configuration file only the users who are members of at least one of the listed
@ -110,3 +113,6 @@ connectors:
- developers - developers
- devops - devops
``` ```
Also, `useGroupsAsWhitelist` configuration option, can restrict the groups
claims to include only the user's groups that are in the configured `groups`.

View file

@ -19,35 +19,50 @@ import (
"github.com/dexidp/dex/pkg/log" "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 ( const (
apiURL = "https://graph.microsoft.com" apiURL = "https://graph.microsoft.com"
// Microsoft requires this scope to access user's profile // Microsoft requires this scope to access user's profile
scopeUser = "user.read" scopeUser = "user.read"
// Microsoft requires this scope to list groups the user is a member of // 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" scopeGroups = "directory.read.all"
) )
// Config holds configuration options for microsoft logins. // Config holds configuration options for microsoft logins.
type Config struct { type Config struct {
ClientID string `json:"clientID"` ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"` ClientSecret string `json:"clientSecret"`
RedirectURI string `json:"redirectURI"` RedirectURI string `json:"redirectURI"`
Tenant string `json:"tenant"` Tenant string `json:"tenant"`
OnlySecurityGroups bool `json:"onlySecurityGroups"` OnlySecurityGroups bool `json:"onlySecurityGroups"`
Groups []string `json:"groups"` Groups []string `json:"groups"`
GroupNameFormat GroupNameFormat `json:"groupNameFormat"`
UseGroupsAsWhitelist bool `json:"useGroupsAsWhitelist"`
} }
// Open returns a strategy for logging in through Microsoft. // Open returns a strategy for logging in through Microsoft.
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
m := microsoftConnector{ m := microsoftConnector{
redirectURI: c.RedirectURI, redirectURI: c.RedirectURI,
clientID: c.ClientID, clientID: c.ClientID,
clientSecret: c.ClientSecret, clientSecret: c.ClientSecret,
tenant: c.Tenant, tenant: c.Tenant,
onlySecurityGroups: c.OnlySecurityGroups, onlySecurityGroups: c.OnlySecurityGroups,
groups: c.Groups, groups: c.Groups,
logger: logger, groupNameFormat: c.GroupNameFormat,
useGroupsAsWhitelist: c.UseGroupsAsWhitelist,
logger: logger,
} }
// By default allow logins from both personal and business/school // By default allow logins from both personal and business/school
// accounts. // accounts.
@ -55,6 +70,15 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
m.tenant = "common" 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 return &m, nil
} }
@ -70,13 +94,15 @@ var (
) )
type microsoftConnector struct { type microsoftConnector struct {
redirectURI string redirectURI string
clientID string clientID string
clientSecret string clientSecret string
tenant string tenant string
onlySecurityGroups bool onlySecurityGroups bool
groups []string groupNameFormat GroupNameFormat
logger log.Logger groups []string
useGroupsAsWhitelist bool
logger log.Logger
} }
func (c *microsoftConnector) isOrgTenant() bool { func (c *microsoftConnector) isOrgTenant() bool {
@ -300,24 +326,28 @@ type group struct {
Name string `json:"displayName"` Name string `json:"displayName"`
} }
func (c *microsoftConnector) getGroups(ctx context.Context, client *http.Client, userID string) (groups []string, err error) { func (c *microsoftConnector) getGroups(ctx context.Context, client *http.Client, userID string) ([]string, error) {
ids, err := c.getGroupIDs(ctx, client) userGroups, err := c.getGroupIDs(ctx, client)
if err != nil { if err != nil {
return groups, err return nil, err
} }
groups, err = c.getGroupNames(ctx, client, ids) if c.groupNameFormat == GroupName {
if err != nil { userGroups, err = c.getGroupNames(ctx, client, userGroups)
return if err != nil {
return nil, err
}
} }
// ensure that the user is in at least one required group // 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 { if len(c.groups) > 0 && len(filteredGroups) == 0 {
return nil, fmt.Errorf("microsoft: user %v not in any of the required groups", userID) 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) { func (c *microsoftConnector) getGroupIDs(ctx context.Context, client *http.Client) (ids []string, err error) {