diff --git a/connector/github/github.go b/connector/github/github.go index ef8d418fa8..1ff2cb5f13 100644 --- a/connector/github/github.go +++ b/connector/github/github.go @@ -53,6 +53,13 @@ type Config struct { TeamNameField string `json:"teamNameField"` LoadAllGroups bool `json:"loadAllGroups"` UseLoginAsID bool `json:"useLoginAsID"` + // NoreplyPrivateEmail configures the connector to use + // {id}+{login}@users.noreply.github.com as the user email if user has + // marked their email as private on GitHub. + // See https://docs.github.com/en/enterprise-cloud@latest/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address#setting-your-commit-email-address-on-github. + // Note, this is only valid for public and Enterprise Cloud GitHub (i.e. this only works on github.com domains). + // There is no equivalent for Enterprise Server GitHub / custom hosts. + NoreplyPrivateEmail bool `json:"noreplyPrivateEmail"` } // Org holds org-team filters, in which teams are optional. @@ -153,6 +160,12 @@ 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 + // use {id}+{login}@users.noreply.github.com as the user email if user has + // marked their email as private on GitHub. + // See https://docs.github.com/en/enterprise-cloud@latest/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address#setting-your-commit-email-address-on-github. + // Note, this is only valid for public and Enterprise Cloud GitHub (i.e. this only works on github.com domains). + // There is no equivalent for Enterprise Server GitHub / custom hosts. + noreplyPrivateEmail bool } // groupsRequired returns whether dex requires GitHub's 'read:org' scope. Dex @@ -554,9 +567,16 @@ func (c *githubConnector) user(ctx context.Context, client *http.Client) (user, // Only public user emails are returned by 'GET /user'. u.Email will be empty // if a users' email is private. We must retrieve private emails explicitly. if u.Email == "" { - var err error - if u.Email, err = c.userEmail(ctx, client); err != nil { - return u, err + // If on github.com, GitHub allows for a special noreply email to + // associate users to commits without exposing their private email. + // See https://docs.github.com/en/enterprise-cloud@latest/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address#about-commit-email-addresses + if c.noreplyPrivateEmail && (c.hostName == "" || c.hostName == "github.com") { + u.Email = fmt.Sprintf("%d+%s@users.noreply.github.com", u.ID, u.Login) + } else { + var err error + if u.Email, err = c.userEmail(ctx, client); err != nil { + return u, err + } } } return u, nil diff --git a/connector/github/github_test.go b/connector/github/github_test.go index 76d7463cf6..8153090dcf 100644 --- a/connector/github/github_test.go +++ b/connector/github/github_test.go @@ -236,3 +236,45 @@ func expectEquals(t *testing.T, a interface{}, b interface{}) { t.Errorf("Expected %+v to equal %+v", a, b) } } + +func TestNoreplyUserEmail(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, + }}}, + }) + defer s.Close() + client := newClient() + + for _, tc := range []struct { + host string + want string + }{ + { + want: "12345678+some-login@users.noreply.github.com", + }, + { + host: "github.com", + want: "12345678+some-login@users.noreply.github.com", + }, + { + host: "example.com", + want: "some@email.com", + }, + } { + t.Run(tc.host, func(t *testing.T) { + c := githubConnector{apiURL: s.URL, hostName: tc.host, httpClient: client, noreplyPrivateEmail: true} + u, err := c.user(ctx, client) + if err != nil { + t.Fatal(err) + } + if u.Email != tc.want { + t.Errorf("want %s, got %s", tc.want, u.Email) + } + }) + } +}