connector/saml: Adding group filtering
- 4 new tests - Doc changes to use the group filtering
This commit is contained in:
parent
8427f0f15c
commit
285c1f162e
@ -14,6 +14,10 @@ __The connector doesn't support refresh tokens__ since the SAML 2.0 protocol doe
|
|||||||
|
|
||||||
The connector doesn't support signed AuthnRequests or encrypted attributes.
|
The connector doesn't support signed AuthnRequests or encrypted attributes.
|
||||||
|
|
||||||
|
## Group Filtering
|
||||||
|
|
||||||
|
The SAML Connector supports providing a whitelist of SAML Groups to filter access based on, and when the `groupsattr` is set with a scope including groups, Dex will check for membership based on configured groups in the `allowedGroups` config setting for the SAML connector.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@ -44,6 +48,10 @@ connectors:
|
|||||||
emailAttr: email
|
emailAttr: email
|
||||||
groupsAttr: groups # optional
|
groupsAttr: groups # optional
|
||||||
|
|
||||||
|
# List of groups to filter access based on membership
|
||||||
|
# allowedGroups
|
||||||
|
# - Admins
|
||||||
|
|
||||||
# CA's can also be provided inline as a base64'd blob.
|
# CA's can also be provided inline as a base64'd blob.
|
||||||
#
|
#
|
||||||
# caData: ( RAW base64'd PEM encoded CA )
|
# caData: ( RAW base64'd PEM encoded CA )
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
"github.com/dexidp/dex/connector"
|
"github.com/dexidp/dex/connector"
|
||||||
|
"github.com/dexidp/dex/pkg/groups"
|
||||||
"github.com/dexidp/dex/pkg/log"
|
"github.com/dexidp/dex/pkg/log"
|
||||||
dsig "github.com/russellhaering/goxmldsig"
|
dsig "github.com/russellhaering/goxmldsig"
|
||||||
"github.com/russellhaering/goxmldsig/etreeutils"
|
"github.com/russellhaering/goxmldsig/etreeutils"
|
||||||
@ -97,9 +98,9 @@ type Config struct {
|
|||||||
// If GroupsDelim is supplied the connector assumes groups are returned as a
|
// If GroupsDelim is supplied the connector assumes groups are returned as a
|
||||||
// single string instead of multiple attribute values. This delimiter will be
|
// single string instead of multiple attribute values. This delimiter will be
|
||||||
// used split the groups string.
|
// used split the groups string.
|
||||||
GroupsDelim string `json:"groupsDelim"`
|
GroupsDelim string `json:"groupsDelim"`
|
||||||
|
AllowedGroups []string `json:"allowedGroups"`
|
||||||
RedirectURI string `json:"redirectURI"`
|
RedirectURI string `json:"redirectURI"`
|
||||||
|
|
||||||
// Requested format of the NameID. The NameID value is is mapped to the ID Token
|
// Requested format of the NameID. The NameID value is is mapped to the ID Token
|
||||||
// 'sub' claim.
|
// 'sub' claim.
|
||||||
@ -154,16 +155,17 @@ func (c *Config) openConnector(logger log.Logger) (*provider, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p := &provider{
|
p := &provider{
|
||||||
entityIssuer: c.EntityIssuer,
|
entityIssuer: c.EntityIssuer,
|
||||||
ssoIssuer: c.SSOIssuer,
|
ssoIssuer: c.SSOIssuer,
|
||||||
ssoURL: c.SSOURL,
|
ssoURL: c.SSOURL,
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
usernameAttr: c.UsernameAttr,
|
usernameAttr: c.UsernameAttr,
|
||||||
emailAttr: c.EmailAttr,
|
emailAttr: c.EmailAttr,
|
||||||
groupsAttr: c.GroupsAttr,
|
groupsAttr: c.GroupsAttr,
|
||||||
groupsDelim: c.GroupsDelim,
|
groupsDelim: c.GroupsDelim,
|
||||||
redirectURI: c.RedirectURI,
|
allowedGroups: c.AllowedGroups,
|
||||||
logger: logger,
|
redirectURI: c.RedirectURI,
|
||||||
|
logger: logger,
|
||||||
|
|
||||||
nameIDPolicyFormat: c.NameIDPolicyFormat,
|
nameIDPolicyFormat: c.NameIDPolicyFormat,
|
||||||
}
|
}
|
||||||
@ -232,10 +234,11 @@ type provider struct {
|
|||||||
validator *dsig.ValidationContext
|
validator *dsig.ValidationContext
|
||||||
|
|
||||||
// Attribute mappings
|
// Attribute mappings
|
||||||
usernameAttr string
|
usernameAttr string
|
||||||
emailAttr string
|
emailAttr string
|
||||||
groupsAttr string
|
groupsAttr string
|
||||||
groupsDelim string
|
groupsDelim string
|
||||||
|
allowedGroups []string
|
||||||
|
|
||||||
redirectURI string
|
redirectURI string
|
||||||
|
|
||||||
@ -388,11 +391,16 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
|||||||
return ident, fmt.Errorf("no attribute with name %q: %s", p.usernameAttr, attributes.names())
|
return ident, fmt.Errorf("no attribute with name %q: %s", p.usernameAttr, attributes.names())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.Groups || p.groupsAttr == "" {
|
if len(p.allowedGroups) == 0 && (!s.Groups || p.groupsAttr == "") {
|
||||||
// Groups not requested or not configured. We're done.
|
// Groups not requested or not configured. We're done.
|
||||||
return ident, nil
|
return ident, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(p.allowedGroups) > 0 && (!s.Groups || p.groupsAttr == "") {
|
||||||
|
// allowedGroups set but no groups or groupsAttr. Disallowing.
|
||||||
|
return ident, fmt.Errorf("User not a member of allowed groups")
|
||||||
|
}
|
||||||
|
|
||||||
// Grab the groups.
|
// Grab the groups.
|
||||||
if p.groupsDelim != "" {
|
if p.groupsDelim != "" {
|
||||||
groupsStr, ok := attributes.get(p.groupsAttr)
|
groupsStr, ok := attributes.get(p.groupsAttr)
|
||||||
@ -408,6 +416,21 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
|||||||
}
|
}
|
||||||
ident.Groups = groups
|
ident.Groups = groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(p.allowedGroups) == 0 {
|
||||||
|
// No allowed groups set, just return the ident
|
||||||
|
return ident, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for membership in one of the allowed groups
|
||||||
|
groupMatches := groups.Filter(ident.Groups, p.allowedGroups)
|
||||||
|
|
||||||
|
if len(groupMatches) == 0 {
|
||||||
|
// No group membership matches found, disallowing
|
||||||
|
return ident, fmt.Errorf("User not a member of allowed groups")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we're good
|
||||||
return ident, nil
|
return ident, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,9 +49,10 @@ type responseTest struct {
|
|||||||
entityIssuer string
|
entityIssuer string
|
||||||
|
|
||||||
// Attribute customization.
|
// Attribute customization.
|
||||||
usernameAttr string
|
usernameAttr string
|
||||||
emailAttr string
|
emailAttr string
|
||||||
groupsAttr string
|
groupsAttr string
|
||||||
|
allowedGroups []string
|
||||||
|
|
||||||
// Expected outcome of the test.
|
// Expected outcome of the test.
|
||||||
wantErr bool
|
wantErr bool
|
||||||
@ -98,6 +99,96 @@ func TestGroups(t *testing.T) {
|
|||||||
test.run(t)
|
test.run(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupsWhitelist(t *testing.T) {
|
||||||
|
test := responseTest{
|
||||||
|
caFile: "testdata/ca.crt",
|
||||||
|
respFile: "testdata/good-resp.xml",
|
||||||
|
now: "2017-04-04T04:34:59.330Z",
|
||||||
|
usernameAttr: "Name",
|
||||||
|
emailAttr: "email",
|
||||||
|
groupsAttr: "groups",
|
||||||
|
allowedGroups: []string{"Admins"},
|
||||||
|
inResponseTo: "6zmm5mguyebwvajyf2sdwwcw6m",
|
||||||
|
redirectURI: "http://127.0.0.1:5556/dex/callback",
|
||||||
|
wantIdent: connector.Identity{
|
||||||
|
UserID: "eric.chiang+okta@coreos.com",
|
||||||
|
Username: "Eric",
|
||||||
|
Email: "eric.chiang+okta@coreos.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: []string{"Admins", "Everyone"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupsWhitelistEmpty(t *testing.T) {
|
||||||
|
test := responseTest{
|
||||||
|
caFile: "testdata/ca.crt",
|
||||||
|
respFile: "testdata/good-resp.xml",
|
||||||
|
now: "2017-04-04T04:34:59.330Z",
|
||||||
|
usernameAttr: "Name",
|
||||||
|
emailAttr: "email",
|
||||||
|
groupsAttr: "groups",
|
||||||
|
allowedGroups: []string{},
|
||||||
|
inResponseTo: "6zmm5mguyebwvajyf2sdwwcw6m",
|
||||||
|
redirectURI: "http://127.0.0.1:5556/dex/callback",
|
||||||
|
wantIdent: connector.Identity{
|
||||||
|
UserID: "eric.chiang+okta@coreos.com",
|
||||||
|
Username: "Eric",
|
||||||
|
Email: "eric.chiang+okta@coreos.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: []string{"Admins", "Everyone"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupsWhitelistDisallowed(t *testing.T) {
|
||||||
|
test := responseTest{
|
||||||
|
wantErr: true,
|
||||||
|
caFile: "testdata/ca.crt",
|
||||||
|
respFile: "testdata/good-resp.xml",
|
||||||
|
now: "2017-04-04T04:34:59.330Z",
|
||||||
|
usernameAttr: "Name",
|
||||||
|
emailAttr: "email",
|
||||||
|
groupsAttr: "groups",
|
||||||
|
allowedGroups: []string{"Nope"},
|
||||||
|
inResponseTo: "6zmm5mguyebwvajyf2sdwwcw6m",
|
||||||
|
redirectURI: "http://127.0.0.1:5556/dex/callback",
|
||||||
|
wantIdent: connector.Identity{
|
||||||
|
UserID: "eric.chiang+okta@coreos.com",
|
||||||
|
Username: "Eric",
|
||||||
|
Email: "eric.chiang+okta@coreos.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: []string{"Admins", "Everyone"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupsWhitelistDisallowedNoGroupsOnIdent(t *testing.T) {
|
||||||
|
test := responseTest{
|
||||||
|
wantErr: true,
|
||||||
|
caFile: "testdata/ca.crt",
|
||||||
|
respFile: "testdata/good-resp.xml",
|
||||||
|
now: "2017-04-04T04:34:59.330Z",
|
||||||
|
usernameAttr: "Name",
|
||||||
|
emailAttr: "email",
|
||||||
|
groupsAttr: "groups",
|
||||||
|
allowedGroups: []string{"Nope"},
|
||||||
|
inResponseTo: "6zmm5mguyebwvajyf2sdwwcw6m",
|
||||||
|
redirectURI: "http://127.0.0.1:5556/dex/callback",
|
||||||
|
wantIdent: connector.Identity{
|
||||||
|
UserID: "eric.chiang+okta@coreos.com",
|
||||||
|
Username: "Eric",
|
||||||
|
Email: "eric.chiang+okta@coreos.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
// TestOkta tests against an actual response from Okta.
|
// TestOkta tests against an actual response from Okta.
|
||||||
func TestOkta(t *testing.T) {
|
func TestOkta(t *testing.T) {
|
||||||
test := responseTest{
|
test := responseTest{
|
||||||
@ -290,12 +381,13 @@ func loadCert(ca string) (*x509.Certificate, error) {
|
|||||||
|
|
||||||
func (r responseTest) run(t *testing.T) {
|
func (r responseTest) run(t *testing.T) {
|
||||||
c := Config{
|
c := Config{
|
||||||
CA: r.caFile,
|
CA: r.caFile,
|
||||||
UsernameAttr: r.usernameAttr,
|
UsernameAttr: r.usernameAttr,
|
||||||
EmailAttr: r.emailAttr,
|
EmailAttr: r.emailAttr,
|
||||||
GroupsAttr: r.groupsAttr,
|
GroupsAttr: r.groupsAttr,
|
||||||
RedirectURI: r.redirectURI,
|
RedirectURI: r.redirectURI,
|
||||||
EntityIssuer: r.entityIssuer,
|
EntityIssuer: r.entityIssuer,
|
||||||
|
AllowedGroups: r.allowedGroups,
|
||||||
// Never logging in, don't need this.
|
// Never logging in, don't need this.
|
||||||
SSOURL: "http://foo.bar/",
|
SSOURL: "http://foo.bar/",
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user