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:
commit
6e98c04f9b
2 changed files with 65 additions and 29 deletions
|
@ -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`.
|
|
@ -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) {
|
||||
|
|
Reference in a new issue