| ... | ... |
@@ -22,6 +22,7 @@ const ( |
| 22 | 22 |
githubTokenURL = "https://github.com/login/oauth/access_token" |
| 23 | 23 |
githubUserApiURL = "https://api.github.com/user" |
| 24 | 24 |
githubUserOrgURL = "https://api.github.com/user/orgs" |
| 25 |
+ githubUserTeamURL = "https://api.github.com/user/teams" |
|
| 25 | 26 |
githubOAuthScope = "user:email" |
| 26 | 27 |
githubOrgScope = "read:org" |
| 27 | 28 |
|
| ... | ... |
@@ -35,6 +36,7 @@ type provider struct {
|
| 35 | 35 |
clientID string |
| 36 | 36 |
clientSecret string |
| 37 | 37 |
allowedOrganizations sets.String |
| 38 |
+ allowedTeams sets.String |
|
| 38 | 39 |
} |
| 39 | 40 |
|
| 40 | 41 |
// https://developer.github.com/v3/users/#response |
| ... | ... |
@@ -51,7 +53,14 @@ type githubOrg struct {
|
| 51 | 51 |
Login string |
| 52 | 52 |
} |
| 53 | 53 |
|
| 54 |
-func NewProvider(providerName, clientID, clientSecret string, organizations []string) external.Provider {
|
|
| 54 |
+// https://developer.github.com/v3/orgs/teams/#response-12 |
|
| 55 |
+type githubTeam struct {
|
|
| 56 |
+ ID uint64 |
|
| 57 |
+ Slug string |
|
| 58 |
+ Organization githubOrg |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func NewProvider(providerName, clientID, clientSecret string, organizations, teams []string) external.Provider {
|
|
| 55 | 62 |
allowedOrganizations := sets.NewString() |
| 56 | 63 |
for _, org := range organizations {
|
| 57 | 64 |
if len(org) > 0 {
|
| ... | ... |
@@ -59,11 +68,19 @@ func NewProvider(providerName, clientID, clientSecret string, organizations []st |
| 59 | 59 |
} |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 |
+ allowedTeams := sets.NewString() |
|
| 63 |
+ for _, team := range teams {
|
|
| 64 |
+ if len(team) > 0 {
|
|
| 65 |
+ allowedTeams.Insert(strings.ToLower(team)) |
|
| 66 |
+ } |
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 62 | 69 |
return &provider{
|
| 63 | 70 |
providerName: providerName, |
| 64 | 71 |
clientID: clientID, |
| 65 | 72 |
clientSecret: clientSecret, |
| 66 | 73 |
allowedOrganizations: allowedOrganizations, |
| 74 |
+ allowedTeams: allowedTeams, |
|
| 67 | 75 |
} |
| 68 | 76 |
} |
| 69 | 77 |
|
| ... | ... |
@@ -74,8 +91,8 @@ func (p *provider) GetTransport() (http.RoundTripper, error) {
|
| 74 | 74 |
// NewConfig implements external/interfaces/Provider.NewConfig |
| 75 | 75 |
func (p *provider) NewConfig() (*osincli.ClientConfig, error) {
|
| 76 | 76 |
scopes := []string{githubOAuthScope}
|
| 77 |
- // if we're limiting to specific organizations, we also need to read their org membership |
|
| 78 |
- if len(p.allowedOrganizations) > 0 {
|
|
| 77 |
+ // if we're limiting to specific organizations or teams, we also need to read their org membership |
|
| 78 |
+ if len(p.allowedOrganizations) > 0 || len(p.allowedTeams) > 0 {
|
|
| 79 | 79 |
scopes = append(scopes, githubOrgScope) |
| 80 | 80 |
} |
| 81 | 81 |
|
| ... | ... |
@@ -114,6 +131,18 @@ func (p *provider) GetUserIdentity(data *osincli.AccessData) (authapi.UserIdenti |
| 114 | 114 |
if !userOrgs.HasAny(p.allowedOrganizations.List()...) {
|
| 115 | 115 |
return nil, false, fmt.Errorf("User %s is not a member of any allowed organizations %v (user is a member of %v)", userdata.Login, p.allowedOrganizations.List(), userOrgs.List())
|
| 116 | 116 |
} |
| 117 |
+ glog.V(4).Infof("User %s is a member of organizations %v)", userdata.Login, userOrgs.List())
|
|
| 118 |
+ } |
|
| 119 |
+ if len(p.allowedTeams) > 0 {
|
|
| 120 |
+ userTeams, err := getUserTeams(data.AccessToken) |
|
| 121 |
+ if err != nil {
|
|
| 122 |
+ return nil, false, err |
|
| 123 |
+ } |
|
| 124 |
+ |
|
| 125 |
+ if !userTeams.HasAny(p.allowedTeams.List()...) {
|
|
| 126 |
+ return nil, false, fmt.Errorf("User %s is not a member of any allowed teams %v (user is a member of %v)", userdata.Login, p.allowedTeams.List(), userTeams.List())
|
|
| 127 |
+ } |
|
| 128 |
+ glog.V(4).Infof("User %s is a member of teams %v)", userdata.Login, userTeams.List())
|
|
| 117 | 129 |
} |
| 118 | 130 |
|
| 119 | 131 |
identity := authapi.NewDefaultUserIdentityInfo(p.providerName, fmt.Sprintf("%d", userdata.ID))
|
| ... | ... |
@@ -133,41 +162,73 @@ func (p *provider) GetUserIdentity(data *osincli.AccessData) (authapi.UserIdenti |
| 133 | 133 |
|
| 134 | 134 |
// getUserOrgs retrieves the organization membership for the user with the given access token. |
| 135 | 135 |
func getUserOrgs(token string) (sets.String, error) {
|
| 136 |
- // start with the empty set, and the initial org url |
|
| 137 | 136 |
userOrgs := sets.NewString() |
| 138 |
- orgURL := githubUserOrgURL |
|
| 137 |
+ err := page(githubUserOrgURL, token, |
|
| 138 |
+ func() interface{} {
|
|
| 139 |
+ return &[]githubOrg{}
|
|
| 140 |
+ }, |
|
| 141 |
+ func(obj interface{}) error {
|
|
| 142 |
+ for _, org := range *(obj.(*[]githubOrg)) {
|
|
| 143 |
+ if len(org.Login) > 0 {
|
|
| 144 |
+ userOrgs.Insert(strings.ToLower(org.Login)) |
|
| 145 |
+ } |
|
| 146 |
+ } |
|
| 147 |
+ return nil |
|
| 148 |
+ }, |
|
| 149 |
+ ) |
|
| 150 |
+ return userOrgs, err |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+// getUserTeams retrieves the team memberships for the user with the given access token. |
|
| 154 |
+func getUserTeams(token string) (sets.String, error) {
|
|
| 155 |
+ userTeams := sets.NewString() |
|
| 156 |
+ err := page(githubUserTeamURL, token, |
|
| 157 |
+ func() interface{} {
|
|
| 158 |
+ return &[]githubTeam{}
|
|
| 159 |
+ }, |
|
| 160 |
+ func(obj interface{}) error {
|
|
| 161 |
+ for _, team := range *(obj.(*[]githubTeam)) {
|
|
| 162 |
+ if len(team.Slug) > 0 && len(team.Organization.Login) > 0 {
|
|
| 163 |
+ userTeams.Insert(strings.ToLower(team.Organization.Login + "/" + team.Slug)) |
|
| 164 |
+ } |
|
| 165 |
+ } |
|
| 166 |
+ return nil |
|
| 167 |
+ }, |
|
| 168 |
+ ) |
|
| 169 |
+ return userTeams, err |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 172 |
+// page fetches the intialURL, and follows "next" links |
|
| 173 |
+func page(initialURL, token string, newObj func() interface{}, processObj func(interface{}) error) error {
|
|
| 139 | 174 |
// track urls we've fetched to avoid cycles |
| 140 |
- fetchedURLs := sets.NewString(orgURL) |
|
| 175 |
+ url := initialURL |
|
| 176 |
+ fetchedURLs := sets.NewString(url) |
|
| 141 | 177 |
for {
|
| 142 |
- // fetch organizations |
|
| 143 |
- organizations := []githubOrg{}
|
|
| 144 |
- links, err := getJSON(orgURL, token, &organizations) |
|
| 178 |
+ // fetch and process |
|
| 179 |
+ obj := newObj() |
|
| 180 |
+ links, err := getJSON(url, token, obj) |
|
| 145 | 181 |
if err != nil {
|
| 146 |
- return nil, err |
|
| 182 |
+ return err |
|
| 147 | 183 |
} |
| 148 |
- for _, org := range organizations {
|
|
| 149 |
- if len(org.Login) > 0 {
|
|
| 150 |
- userOrgs.Insert(strings.ToLower(org.Login)) |
|
| 151 |
- } |
|
| 184 |
+ if err := processObj(obj); err != nil {
|
|
| 185 |
+ return err |
|
| 152 | 186 |
} |
| 153 | 187 |
|
| 154 | 188 |
// see if we need to page |
| 155 | 189 |
// https://developer.github.com/v3/#link-header |
| 156 |
- nextURL := links["next"] |
|
| 157 |
- if len(nextURL) == 0 {
|
|
| 190 |
+ url = links["next"] |
|
| 191 |
+ if len(url) == 0 {
|
|
| 158 | 192 |
// no next URL, we're done paging |
| 159 | 193 |
break |
| 160 | 194 |
} |
| 161 |
- if fetchedURLs.Has(nextURL) {
|
|
| 195 |
+ if fetchedURLs.Has(url) {
|
|
| 162 | 196 |
// break to avoid a loop |
| 163 | 197 |
break |
| 164 | 198 |
} |
| 165 | 199 |
// remember to avoid a loop |
| 166 |
- fetchedURLs.Insert(nextURL) |
|
| 167 |
- orgURL = nextURL |
|
| 200 |
+ fetchedURLs.Insert(url) |
|
| 168 | 201 |
} |
| 169 |
- |
|
| 170 |
- return userOrgs, nil |
|
| 202 |
+ return nil |
|
| 171 | 203 |
} |
| 172 | 204 |
|
| 173 | 205 |
// getJSON fetches and deserializes JSON into the given object. |
| ... | ... |
@@ -864,6 +864,8 @@ type GitHubIdentityProvider struct {
|
| 864 | 864 |
ClientSecret StringSource |
| 865 | 865 |
// Organizations optionally restricts which organizations are allowed to log in |
| 866 | 866 |
Organizations []string |
| 867 |
+ // Teams optionally restricts which teams are allowed to log in. Format is <org>/<team>. |
|
| 868 |
+ Teams []string |
|
| 867 | 869 |
} |
| 868 | 870 |
|
| 869 | 871 |
type GitLabIdentityProvider struct {
|
| ... | ... |
@@ -135,7 +135,7 @@ func TestStringSourceMarshaling(t *testing.T) {
|
| 135 | 135 |
} |
| 136 | 136 |
|
| 137 | 137 |
// Wrap in a dummy JSON from the surrounding object |
| 138 |
- input := fmt.Sprintf(`{"kind":"GitHubIdentityProvider","apiVersion":"v1","clientID":"","clientSecret":%s,"organizations":null}`, tc.ExpectedJSON)
|
|
| 138 |
+ input := fmt.Sprintf(`{"kind":"GitHubIdentityProvider","apiVersion":"v1","clientID":"","clientSecret":%s,"organizations":null,"teams":null}`, tc.ExpectedJSON)
|
|
| 139 | 139 |
if strings.TrimSpace(string(json)) != input {
|
| 140 | 140 |
t.Log(len(input), len(json)) |
| 141 | 141 |
t.Errorf("%s: expected\n%s\ngot\n%s", k, input, string(json))
|
| ... | ... |
@@ -217,6 +217,7 @@ var map_GitHubIdentityProvider = map[string]string{
|
| 217 | 217 |
"clientID": "ClientID is the oauth client ID", |
| 218 | 218 |
"clientSecret": "ClientSecret is the oauth client secret", |
| 219 | 219 |
"organizations": "Organizations optionally restricts which organizations are allowed to log in", |
| 220 |
+ "teams": "Teams optionally restricts which teams are allowed to log in. Format is <org>/<team>.", |
|
| 220 | 221 |
} |
| 221 | 222 |
|
| 222 | 223 |
func (GitHubIdentityProvider) SwaggerDoc() map[string]string {
|
| ... | ... |
@@ -842,6 +842,8 @@ type GitHubIdentityProvider struct {
|
| 842 | 842 |
ClientSecret StringSource `json:"clientSecret"` |
| 843 | 843 |
// Organizations optionally restricts which organizations are allowed to log in |
| 844 | 844 |
Organizations []string `json:"organizations"` |
| 845 |
+ // Teams optionally restricts which teams are allowed to log in. Format is <org>/<team>. |
|
| 846 |
+ Teams []string `json:"teams"` |
|
| 845 | 847 |
} |
| 846 | 848 |
|
| 847 | 849 |
// GitLabIdentityProvider provides identities for users authenticating using GitLab credentials |
| ... | ... |
@@ -323,6 +323,7 @@ oauthConfig: |
| 323 | 323 |
clientSecret: "" |
| 324 | 324 |
kind: GitHubIdentityProvider |
| 325 | 325 |
organizations: null |
| 326 |
+ teams: null |
|
| 326 | 327 |
- challenge: false |
| 327 | 328 |
login: false |
| 328 | 329 |
mappingMethod: "" |
| ... | ... |
@@ -337,6 +338,7 @@ oauthConfig: |
| 337 | 337 |
value: "" |
| 338 | 338 |
kind: GitHubIdentityProvider |
| 339 | 339 |
organizations: null |
| 340 |
+ teams: null |
|
| 340 | 341 |
- challenge: false |
| 341 | 342 |
login: false |
| 342 | 343 |
mappingMethod: "" |
| ... | ... |
@@ -183,13 +183,13 @@ func ValidateIdentityProvider(identityProvider api.IdentityProvider, fldPath *fi |
| 183 | 183 |
validationResults.Append(ValidateKeystoneIdentityProvider(provider, identityProvider, providerPath)) |
| 184 | 184 |
|
| 185 | 185 |
case (*api.GitHubIdentityProvider): |
| 186 |
- validationResults.AddErrors(ValidateGitHubIdentityProvider(provider, identityProvider.UseAsChallenger, fldPath)...) |
|
| 186 |
+ validationResults.Append(ValidateGitHubIdentityProvider(provider, identityProvider.UseAsChallenger, identityProvider.MappingMethod, fldPath)) |
|
| 187 | 187 |
|
| 188 | 188 |
case (*api.GitLabIdentityProvider): |
| 189 | 189 |
validationResults.AddErrors(ValidateGitLabIdentityProvider(provider, fldPath)...) |
| 190 | 190 |
|
| 191 | 191 |
case (*api.GoogleIdentityProvider): |
| 192 |
- validationResults.AddErrors(ValidateGoogleIdentityProvider(provider, identityProvider.UseAsChallenger, fldPath)...) |
|
| 192 |
+ validationResults.Append(ValidateGoogleIdentityProvider(provider, identityProvider.UseAsChallenger, identityProvider.MappingMethod, fldPath)) |
|
| 193 | 193 |
|
| 194 | 194 |
case (*api.OpenIDIdentityProvider): |
| 195 | 195 |
validationResults.AddErrors(ValidateOpenIDIdentityProvider(provider, identityProvider, fldPath)...) |
| ... | ... |
@@ -309,28 +309,45 @@ func ValidateOAuthIdentityProvider(clientID string, clientSecret api.StringSourc |
| 309 | 309 |
return allErrs |
| 310 | 310 |
} |
| 311 | 311 |
|
| 312 |
-func ValidateGitHubIdentityProvider(provider *api.GitHubIdentityProvider, challenge bool, fieldPath *field.Path) field.ErrorList {
|
|
| 313 |
- allErrs := field.ErrorList{}
|
|
| 312 |
+func ValidateGitHubIdentityProvider(provider *api.GitHubIdentityProvider, challenge bool, mappingMethod string, fieldPath *field.Path) ValidationResults {
|
|
| 313 |
+ validationResults := ValidationResults{}
|
|
| 314 | 314 |
|
| 315 |
- allErrs = append(allErrs, ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, fieldPath)...) |
|
| 315 |
+ validationResults.AddErrors(ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, fieldPath)...) |
|
| 316 | 316 |
|
| 317 | 317 |
if challenge {
|
| 318 |
- allErrs = append(allErrs, field.Invalid(fieldPath.Child("challenge"), challenge, "A GitHub identity provider cannot be used for challenges"))
|
|
| 318 |
+ validationResults.AddErrors(field.Invalid(fieldPath.Child("challenge"), challenge, "A GitHub identity provider cannot be used for challenges"))
|
|
| 319 | 319 |
} |
| 320 | 320 |
|
| 321 |
- return allErrs |
|
| 321 |
+ if len(provider.Teams) > 0 && len(provider.Organizations) > 0 {
|
|
| 322 |
+ validationResults.AddErrors(field.Invalid(fieldPath.Child("organizations"), provider.Organizations, "specify organizations or teams, not both"))
|
|
| 323 |
+ validationResults.AddErrors(field.Invalid(fieldPath.Child("teams"), provider.Teams, "specify organizations or teams, not both"))
|
|
| 324 |
+ } |
|
| 325 |
+ if len(provider.Teams) == 0 && len(provider.Organizations) == 0 && mappingMethod != string(identitymapper.MappingMethodLookup) {
|
|
| 326 |
+ validationResults.AddWarnings(field.Invalid(fieldPath, nil, "no organizations or teams specified, any GitHub user will be allowed to authenticate")) |
|
| 327 |
+ } |
|
| 328 |
+ for i, team := range provider.Teams {
|
|
| 329 |
+ if len(strings.Split(team, "/")) != 2 {
|
|
| 330 |
+ validationResults.AddErrors(field.Invalid(fieldPath.Child("teams").Index(i), team, "must be in the format <org>/<team>"))
|
|
| 331 |
+ } |
|
| 332 |
+ } |
|
| 333 |
+ |
|
| 334 |
+ return validationResults |
|
| 322 | 335 |
} |
| 323 | 336 |
|
| 324 |
-func ValidateGoogleIdentityProvider(provider *api.GoogleIdentityProvider, challenge bool, fieldPath *field.Path) field.ErrorList {
|
|
| 325 |
- allErrs := field.ErrorList{}
|
|
| 337 |
+func ValidateGoogleIdentityProvider(provider *api.GoogleIdentityProvider, challenge bool, mappingMethod string, fieldPath *field.Path) ValidationResults {
|
|
| 338 |
+ validationResults := ValidationResults{}
|
|
| 326 | 339 |
|
| 327 |
- allErrs = append(allErrs, ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, fieldPath)...) |
|
| 340 |
+ validationResults.AddErrors(ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, fieldPath)...) |
|
| 328 | 341 |
|
| 329 | 342 |
if challenge {
|
| 330 |
- allErrs = append(allErrs, field.Invalid(fieldPath.Child("challenge"), challenge, "A Google identity provider cannot be used for challenges"))
|
|
| 343 |
+ validationResults.AddErrors(field.Invalid(fieldPath.Child("challenge"), challenge, "A Google identity provider cannot be used for challenges"))
|
|
| 331 | 344 |
} |
| 332 | 345 |
|
| 333 |
- return allErrs |
|
| 346 |
+ if len(provider.HostedDomain) == 0 && mappingMethod != string(identitymapper.MappingMethodLookup) {
|
|
| 347 |
+ validationResults.AddWarnings(field.Invalid(fieldPath, nil, "no hostedDomain specified, any Google user will be allowed to authenticate")) |
|
| 348 |
+ } |
|
| 349 |
+ |
|
| 350 |
+ return validationResults |
|
| 334 | 351 |
} |
| 335 | 352 |
|
| 336 | 353 |
func ValidateGitLabIdentityProvider(provider *api.GitLabIdentityProvider, fieldPath *field.Path) field.ErrorList {
|
| ... | ... |
@@ -534,7 +534,7 @@ func (c *AuthConfig) getOAuthProvider(identityProvider configapi.IdentityProvide |
| 534 | 534 |
if err != nil {
|
| 535 | 535 |
return nil, err |
| 536 | 536 |
} |
| 537 |
- return github.NewProvider(identityProvider.Name, provider.ClientID, clientSecret, provider.Organizations), nil |
|
| 537 |
+ return github.NewProvider(identityProvider.Name, provider.ClientID, clientSecret, provider.Organizations, provider.Teams), nil |
|
| 538 | 538 |
|
| 539 | 539 |
case (*configapi.GitLabIdentityProvider): |
| 540 | 540 |
transport, err := cmdutil.TransportFor(provider.CA, "", "") |