mirror of
https://github.com/netbirdio/dex.git
synced 2026-05-22 18:43:53 -07:00
Add preferredEmailDomain config option for GitHub connector (#2740)
Signed-off-by: nobuyo <longzechangsheng@gmail.com> Signed-off-by: Nobuo Takizawa <nobuyo@users.noreply.github.com> Co-authored-by: Maksim Nabokikh <max.nabokih@gmail.com>
This commit is contained in:
co-authored by
Maksim Nabokikh
parent
263526a317
commit
c91b87faf1
+75
-20
@@ -39,16 +39,17 @@ var (
|
||||
|
||||
// Config holds configuration options for github logins.
|
||||
type Config struct {
|
||||
ClientID string `json:"clientID"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
RedirectURI string `json:"redirectURI"`
|
||||
Org string `json:"org"`
|
||||
Orgs []Org `json:"orgs"`
|
||||
HostName string `json:"hostName"`
|
||||
RootCA string `json:"rootCA"`
|
||||
TeamNameField string `json:"teamNameField"`
|
||||
LoadAllGroups bool `json:"loadAllGroups"`
|
||||
UseLoginAsID bool `json:"useLoginAsID"`
|
||||
ClientID string `json:"clientID"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
RedirectURI string `json:"redirectURI"`
|
||||
Org string `json:"org"`
|
||||
Orgs []Org `json:"orgs"`
|
||||
HostName string `json:"hostName"`
|
||||
RootCA string `json:"rootCA"`
|
||||
TeamNameField string `json:"teamNameField"`
|
||||
LoadAllGroups bool `json:"loadAllGroups"`
|
||||
UseLoginAsID bool `json:"useLoginAsID"`
|
||||
PreferredEmailDomain string `json:"preferredEmailDomain"`
|
||||
}
|
||||
|
||||
// Org holds org-team filters, in which teams are optional.
|
||||
@@ -75,14 +76,15 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
|
||||
}
|
||||
|
||||
g := githubConnector{
|
||||
redirectURI: c.RedirectURI,
|
||||
org: c.Org,
|
||||
orgs: c.Orgs,
|
||||
clientID: c.ClientID,
|
||||
clientSecret: c.ClientSecret,
|
||||
apiURL: apiURL,
|
||||
logger: logger,
|
||||
useLoginAsID: c.UseLoginAsID,
|
||||
redirectURI: c.RedirectURI,
|
||||
org: c.Org,
|
||||
orgs: c.Orgs,
|
||||
clientID: c.ClientID,
|
||||
clientSecret: c.ClientSecret,
|
||||
apiURL: apiURL,
|
||||
logger: logger,
|
||||
useLoginAsID: c.UseLoginAsID,
|
||||
preferredEmailDomain: c.PreferredEmailDomain,
|
||||
}
|
||||
|
||||
if c.HostName != "" {
|
||||
@@ -115,6 +117,12 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
|
||||
return nil, fmt.Errorf("invalid connector config: unsupported team name field value `%s`", c.TeamNameField)
|
||||
}
|
||||
|
||||
if c.PreferredEmailDomain != "" {
|
||||
if strings.HasSuffix(c.PreferredEmailDomain, "*") {
|
||||
return nil, errors.New("invalid PreferredEmailDomain: glob pattern cannot end with \"*\"")
|
||||
}
|
||||
}
|
||||
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
@@ -149,6 +157,8 @@ type githubConnector struct {
|
||||
loadAllGroups bool
|
||||
// if set to true will use the user's handle rather than their numeric id as the ID
|
||||
useLoginAsID bool
|
||||
// the domain to be preferred among the user's emails. e.g. "github.com"
|
||||
preferredEmailDomain string
|
||||
}
|
||||
|
||||
// groupsRequired returns whether dex requires GitHub's 'read:org' scope. Dex
|
||||
@@ -548,7 +558,13 @@ type userEmail struct {
|
||||
// The HTTP client is expected to be constructed by the golang.org/x/oauth2 package,
|
||||
// which inserts a bearer token as part of the request.
|
||||
func (c *githubConnector) userEmail(ctx context.Context, client *http.Client) (string, error) {
|
||||
var (
|
||||
primaryEmail userEmail
|
||||
preferredEmails []userEmail
|
||||
)
|
||||
|
||||
apiURL := c.apiURL + "/user/emails"
|
||||
|
||||
for {
|
||||
// https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user
|
||||
var (
|
||||
@@ -575,7 +591,17 @@ func (c *githubConnector) userEmail(ctx context.Context, client *http.Client) (s
|
||||
}
|
||||
|
||||
if email.Verified && email.Primary {
|
||||
return email.Email, nil
|
||||
primaryEmail = email
|
||||
}
|
||||
|
||||
if c.preferredEmailDomain != "" {
|
||||
_, domainPart, ok := strings.Cut(email.Email, "@")
|
||||
if !ok {
|
||||
return "", errors.New("github: invalid format email is detected")
|
||||
}
|
||||
if email.Verified && c.isPreferredEmailDomain(domainPart) {
|
||||
preferredEmails = append(preferredEmails, email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,7 +610,36 @@ func (c *githubConnector) userEmail(ctx context.Context, client *http.Client) (s
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("github: user has no verified, primary email")
|
||||
if len(preferredEmails) > 0 {
|
||||
return preferredEmails[0].Email, nil
|
||||
}
|
||||
|
||||
if primaryEmail.Email != "" {
|
||||
return primaryEmail.Email, nil
|
||||
}
|
||||
|
||||
return "", errors.New("github: user has no verified, primary email or preferred-domain email")
|
||||
}
|
||||
|
||||
// isPreferredEmailDomain checks the domain is matching with preferredEmailDomain.
|
||||
func (c *githubConnector) isPreferredEmailDomain(domain string) bool {
|
||||
if domain == c.preferredEmailDomain {
|
||||
return true
|
||||
}
|
||||
|
||||
preferredDomainParts := strings.Split(c.preferredEmailDomain, ".")
|
||||
domainParts := strings.Split(domain, ".")
|
||||
|
||||
if len(preferredDomainParts) != len(domainParts) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, v := range preferredDomainParts {
|
||||
if domainParts[i] != v && v != "*" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// userInOrg queries the GitHub API for a users' org membership.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -198,6 +199,290 @@ func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
|
||||
expectEquals(t, identity.Username, "Joe Bloggs")
|
||||
}
|
||||
|
||||
func TestPreferredEmailDomainConfigured(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestServer(map[string]testResponse{
|
||||
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
|
||||
"/user/emails": {
|
||||
data: []userEmail{
|
||||
{
|
||||
Email: "some@email.com",
|
||||
Verified: true,
|
||||
Primary: true,
|
||||
},
|
||||
{
|
||||
Email: "another@email.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
{
|
||||
Email: "some@preferred-domain.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
{
|
||||
Email: "another@preferred-domain.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
defer s.Close()
|
||||
|
||||
hostURL, err := url.Parse(s.URL)
|
||||
expectNil(t, err)
|
||||
|
||||
client := newClient()
|
||||
c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "preferred-domain.com"}
|
||||
|
||||
u, err := c.user(ctx, client)
|
||||
expectNil(t, err)
|
||||
expectEquals(t, u.Email, "some@preferred-domain.com")
|
||||
}
|
||||
|
||||
func TestPreferredEmailDomainConfiguredWithGlob(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestServer(map[string]testResponse{
|
||||
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
|
||||
"/user/emails": {
|
||||
data: []userEmail{
|
||||
{
|
||||
Email: "some@email.com",
|
||||
Verified: true,
|
||||
Primary: true,
|
||||
},
|
||||
{
|
||||
Email: "another@email.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
{
|
||||
Email: "some@another.preferred-domain.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
{
|
||||
Email: "some@sub-domain.preferred-domain.co",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
defer s.Close()
|
||||
|
||||
hostURL, err := url.Parse(s.URL)
|
||||
expectNil(t, err)
|
||||
|
||||
client := newClient()
|
||||
c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "*.preferred-domain.co"}
|
||||
|
||||
u, err := c.user(ctx, client)
|
||||
expectNil(t, err)
|
||||
expectEquals(t, u.Email, "some@sub-domain.preferred-domain.co")
|
||||
}
|
||||
|
||||
func TestPreferredEmailDomainConfigured_UserHasNoPreferredDomainEmail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestServer(map[string]testResponse{
|
||||
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
|
||||
"/user/emails": {
|
||||
data: []userEmail{
|
||||
{
|
||||
Email: "some@email.com",
|
||||
Verified: true,
|
||||
Primary: true,
|
||||
},
|
||||
{
|
||||
Email: "another@email.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
defer s.Close()
|
||||
|
||||
hostURL, err := url.Parse(s.URL)
|
||||
expectNil(t, err)
|
||||
|
||||
client := newClient()
|
||||
c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "preferred-domain.com"}
|
||||
|
||||
u, err := c.user(ctx, client)
|
||||
expectNil(t, err)
|
||||
expectEquals(t, u.Email, "some@email.com")
|
||||
}
|
||||
|
||||
func TestPreferredEmailDomainNotConfigured(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestServer(map[string]testResponse{
|
||||
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
|
||||
"/user/emails": {
|
||||
data: []userEmail{
|
||||
{
|
||||
Email: "some@email.com",
|
||||
Verified: true,
|
||||
Primary: true,
|
||||
},
|
||||
{
|
||||
Email: "another@email.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
{
|
||||
Email: "some@preferred-domain.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
defer s.Close()
|
||||
|
||||
hostURL, err := url.Parse(s.URL)
|
||||
expectNil(t, err)
|
||||
|
||||
client := newClient()
|
||||
c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client}
|
||||
|
||||
u, err := c.user(ctx, client)
|
||||
expectNil(t, err)
|
||||
expectEquals(t, u.Email, "some@email.com")
|
||||
}
|
||||
|
||||
func TestPreferredEmailDomainConfigured_Error_BothPrimaryAndPreferredDomainEmailNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestServer(map[string]testResponse{
|
||||
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
|
||||
"/user/emails": {
|
||||
data: []userEmail{
|
||||
{
|
||||
Email: "some@email.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
{
|
||||
Email: "another@email.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
{
|
||||
Email: "some@preferred-domain.com",
|
||||
Verified: true,
|
||||
Primary: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
defer s.Close()
|
||||
|
||||
hostURL, err := url.Parse(s.URL)
|
||||
expectNil(t, err)
|
||||
|
||||
client := newClient()
|
||||
c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "foo.bar"}
|
||||
|
||||
_, err = c.user(ctx, client)
|
||||
expectNotNil(t, err, "Email not found error")
|
||||
expectEquals(t, err.Error(), "github: user has no verified, primary email or preferred-domain email")
|
||||
}
|
||||
|
||||
func Test_isPreferredEmailDomain(t *testing.T) {
|
||||
client := newClient()
|
||||
tests := []struct {
|
||||
preferredEmailDomain string
|
||||
email string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
preferredEmailDomain: "example.com",
|
||||
email: "test@example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "example.com",
|
||||
email: "test@another.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.example.com",
|
||||
email: "test@my.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.example.com",
|
||||
email: "test@my.another.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.example.com",
|
||||
email: "test@my.domain.example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.example.com",
|
||||
email: "test@sub.domain.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.*.example.com",
|
||||
email: "test@sub.my.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.*.example.com",
|
||||
email: "test@a.my.google.com",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.preferredEmailDomain, func(t *testing.T) {
|
||||
c := githubConnector{apiURL: "apiURL", hostName: "github.com", httpClient: client, preferredEmailDomain: test.preferredEmailDomain}
|
||||
_, domainPart, _ := strings.Cut(test.email, "@")
|
||||
res := c.isPreferredEmailDomain(domainPart)
|
||||
|
||||
expectEquals(t, res, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Open_PreferredDomainConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
preferredEmailDomain string
|
||||
email string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
preferredEmailDomain: "example.com",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.example.com",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "*.*.example.com",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
preferredEmailDomain: "example.*",
|
||||
expected: errors.New("invalid PreferredEmailDomain: glob pattern cannot end with \"*\""),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.preferredEmailDomain, func(t *testing.T) {
|
||||
c := Config{
|
||||
PreferredEmailDomain: test.preferredEmailDomain,
|
||||
}
|
||||
_, err := c.Open("id", nil)
|
||||
|
||||
expectEquals(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newTestServer(responses map[string]testResponse) *httptest.Server {
|
||||
var s *httptest.Server
|
||||
s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -231,6 +516,12 @@ func expectNil(t *testing.T, a interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func expectNotNil(t *testing.T, a interface{}, msg string) {
|
||||
if a == nil {
|
||||
t.Errorf("Expected %+v to not to be nil", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func expectEquals(t *testing.T, a interface{}, b interface{}) {
|
||||
if !reflect.DeepEqual(a, b) {
|
||||
t.Errorf("Expected %+v to equal %+v", a, b)
|
||||
|
||||
Reference in New Issue
Block a user