Browse code

Add support for github teams

Jordan Liggitt authored on 2016/11/16 17:03:32
Showing 9 changed files
... ...
@@ -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.
... ...
@@ -7,5 +7,5 @@ import (
7 7
 )
8 8
 
9 9
 func TestGitHub(t *testing.T) {
10
-	_ = external.Provider(NewProvider("github", "clientid", "clientsecret", nil))
10
+	_ = external.Provider(NewProvider("github", "clientid", "clientsecret", nil, nil))
11 11
 }
... ...
@@ -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, "", "")