Browse Source

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>
pull/2583/head
Nobuo Takizawa 3 years ago committed by GitHub
parent
commit
c91b87faf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 95
      connector/github/github.go
  2. 291
      connector/github/github_test.go

95
connector/github/github.go

@ -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.

291
connector/github/github_test.go

@ -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)

Loading…
Cancel
Save