git.iko.soy/typeable/oauth2-proxy
on 71dc702 help sign in
464 lines 12 kB history raw
1package providers
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "net/url"
9 "path"
10 "regexp"
11 "strconv"
12 "strings"
14 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
16 "github.com/oauth2-proxy/oauth2-proxy/pkg/requests"
19// GitHubProvider represents an GitHub based Identity Provider
20type GitHubProvider struct {
21 *ProviderData
22 Org string
23 Team string
24 Repo string
25 Token string
26 Users []string
29var _ Provider = (*GitHubProvider)(nil)
31// NewGitHubProvider initiates a new GitHubProvider
32func NewGitHubProvider(p *ProviderData) *GitHubProvider {
33 p.ProviderName = "GitHub"
34 if p.LoginURL == nil || p.LoginURL.String() == "" {
35 p.LoginURL = &url.URL{
36 Scheme: "https",
37 Host: "github.com",
38 Path: "/login/oauth/authorize",
39 }
40 }
41 if p.RedeemURL == nil || p.RedeemURL.String() == "" {
42 p.RedeemURL = &url.URL{
43 Scheme: "https",
44 Host: "github.com",
45 Path: "/login/oauth/access_token",
46 }
47 }
48 // ValidationURL is the API Base URL
49 if p.ValidateURL == nil || p.ValidateURL.String() == "" {
50 p.ValidateURL = &url.URL{
51 Scheme: "https",
52 Host: "api.github.com",
53 Path: "/",
54 }
55 }
56 if p.Scope == "" {
57 p.Scope = "user:email"
58 }
59 return &GitHubProvider{ProviderData: p}
62func getGitHubHeader(accessToken string) http.Header {
63 header := make(http.Header)
64 header.Set("Accept", "application/vnd.github.v3+json")
65 header.Set("Authorization", fmt.Sprintf("token %s", accessToken))
66 return header
69// SetOrgTeam adds GitHub org reading parameters to the OAuth2 scope
70func (p *GitHubProvider) SetOrgTeam(org, team string) {
71 p.Org = org
72 p.Team = team
73 if org != "" || team != "" {
74 p.Scope += " read:org"
75 }
78// SetRepo configures the target repository and optional token to use
79func (p *GitHubProvider) SetRepo(repo, token string) {
80 p.Repo = repo
81 p.Token = token
84// SetUsers configures allowed usernames
85func (p *GitHubProvider) SetUsers(users []string) {
86 p.Users = users
89func (p *GitHubProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) {
90 // https://developer.github.com/v3/orgs/#list-your-organizations
92 var orgs []struct {
93 Login string `json:"login"`
94 }
96 type orgsPage []struct {
97 Login string `json:"login"`
98 }
100 pn := 1
101 for {
102 params := url.Values{
103 "per_page": {"100"},
104 "page": {strconv.Itoa(pn)},
105 }
107 endpoint := &url.URL{
108 Scheme: p.ValidateURL.Scheme,
109 Host: p.ValidateURL.Host,
110 Path: path.Join(p.ValidateURL.Path, "/user/orgs"),
111 RawQuery: params.Encode(),
112 }
114 var op orgsPage
115 err := requests.New(endpoint.String()).
116 WithContext(ctx).
117 WithHeaders(getGitHubHeader(accessToken)).
118 Do().
119 UnmarshalInto(&op)
120 if err != nil {
121 return false, err
122 }
124 if len(op) == 0 {
125 break
126 }
128 orgs = append(orgs, op...)
129 pn++
130 }
132 presentOrgs := make([]string, 0, len(orgs))
133 for _, org := range orgs {
134 if p.Org == org.Login {
135 logger.Printf("Found Github Organization: %q", org.Login)
136 return true, nil
137 }
138 presentOrgs = append(presentOrgs, org.Login)
139 }
141 logger.Printf("Missing Organization:%q in %v", p.Org, presentOrgs)
142 return false, nil
145func (p *GitHubProvider) hasOrgAndTeam(ctx context.Context, accessToken string) (bool, error) {
146 // https://developer.github.com/v3/orgs/teams/#list-user-teams
148 var teams []struct {
149 Name string `json:"name"`
150 Slug string `json:"slug"`
151 Org struct {
152 Login string `json:"login"`
153 } `json:"organization"`
154 }
156 type teamsPage []struct {
157 Name string `json:"name"`
158 Slug string `json:"slug"`
159 Org struct {
160 Login string `json:"login"`
161 } `json:"organization"`
162 }
164 pn := 1
165 last := 0
166 for {
167 params := url.Values{
168 "per_page": {"100"},
169 "page": {strconv.Itoa(pn)},
170 }
172 endpoint := &url.URL{
173 Scheme: p.ValidateURL.Scheme,
174 Host: p.ValidateURL.Host,
175 Path: path.Join(p.ValidateURL.Path, "/user/teams"),
176 RawQuery: params.Encode(),
177 }
179 // bodyclose cannot detect that the body is being closed later in requests.Into,
180 // so have to skip the linting for the next line.
181 // nolint:bodyclose
182 result := requests.New(endpoint.String()).
183 WithContext(ctx).
184 WithHeaders(getGitHubHeader(accessToken)).
185 Do()
186 if result.Error() != nil {
187 return false, result.Error()
188 }
190 if last == 0 {
191 // link header may not be obtained
192 // When paging is not required and all data can be retrieved with a single call
194 // Conditions for obtaining the link header.
195 // 1. When paging is required (Example: When the data size is 100 and the page size is 99 or less)
196 // 2. When it exceeds the paging frame (Example: When there is only 10 records but the second page is called with a page size of 100)
198 // link header at not last page
199 // <https://api.github.com/user/teams?page=1&per_page=100>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=100>; rel="last", <https://api.github.com/user/teams?page=1&per_page=100>; rel="first"
200 // link header at last page (doesn't exist last info)
201 // <https://api.github.com/user/teams?page=3&per_page=10>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=10>; rel="first"
203 link := result.Headers().Get("Link")
204 rep1 := regexp.MustCompile(`(?s).*\<https://api.github.com/user/teams\?page=(.)&per_page=[0-9]+\>; rel="last".*`)
205 i, converr := strconv.Atoi(rep1.ReplaceAllString(link, "$1"))
207 // If the last page cannot be taken from the link in the http header, the last variable remains zero
208 if converr == nil {
209 last = i
210 }
211 }
213 var tp teamsPage
214 if err := result.UnmarshalInto(&tp); err != nil {
215 return false, err
216 }
217 if len(tp) == 0 {
218 break
219 }
221 teams = append(teams, tp...)
223 if pn == last {
224 break
225 }
226 if last == 0 {
227 break
228 }
230 pn++
231 }
233 var hasOrg bool
234 presentOrgs := make(map[string]bool)
235 var presentTeams []string
236 for _, team := range teams {
237 presentOrgs[team.Org.Login] = true
238 if p.Org == team.Org.Login {
239 hasOrg = true
240 ts := strings.Split(p.Team, ",")
241 for _, t := range ts {
242 if t == team.Slug {
243 logger.Printf("Found Github Organization:%q Team:%q (Name:%q)", team.Org.Login, team.Slug, team.Name)
244 return true, nil
245 }
246 }
247 presentTeams = append(presentTeams, team.Slug)
248 }
249 }
250 if hasOrg {
251 logger.Printf("Missing Team:%q from Org:%q in teams: %v", p.Team, p.Org, presentTeams)
252 } else {
253 var allOrgs []string
254 for org := range presentOrgs {
255 allOrgs = append(allOrgs, org)
256 }
257 logger.Printf("Missing Organization:%q in %#v", p.Org, allOrgs)
258 }
259 return false, nil
262func (p *GitHubProvider) hasRepo(ctx context.Context, accessToken string) (bool, error) {
263 // https://developer.github.com/v3/repos/#get-a-repository
265 type permissions struct {
266 Pull bool `json:"pull"`
267 Push bool `json:"push"`
268 }
270 type repository struct {
271 Permissions permissions `json:"permissions"`
272 Private bool `json:"private"`
273 }
275 endpoint := &url.URL{
276 Scheme: p.ValidateURL.Scheme,
277 Host: p.ValidateURL.Host,
278 Path: path.Join(p.ValidateURL.Path, "/repo/", p.Repo),
279 }
281 var repo repository
282 err := requests.New(endpoint.String()).
283 WithContext(ctx).
284 WithHeaders(getGitHubHeader(accessToken)).
285 Do().
286 UnmarshalInto(&repo)
287 if err != nil {
288 return false, err
289 }
291 // Every user can implicitly pull from a public repo, so only grant access
292 // if they have push access or the repo is private and they can pull
293 return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil
296func (p *GitHubProvider) hasUser(ctx context.Context, accessToken string) (bool, error) {
297 // https://developer.github.com/v3/users/#get-the-authenticated-user
299 var user struct {
300 Login string `json:"login"`
301 Email string `json:"email"`
302 }
304 endpoint := &url.URL{
305 Scheme: p.ValidateURL.Scheme,
306 Host: p.ValidateURL.Host,
307 Path: path.Join(p.ValidateURL.Path, "/user"),
308 }
310 err := requests.New(endpoint.String()).
311 WithContext(ctx).
312 WithHeaders(getGitHubHeader(accessToken)).
313 Do().
314 UnmarshalInto(&user)
315 if err != nil {
316 return false, err
317 }
319 if p.isVerifiedUser(user.Login) {
320 return true, nil
321 }
322 return false, nil
325func (p *GitHubProvider) isCollaborator(ctx context.Context, username, accessToken string) (bool, error) {
326 //https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
328 endpoint := &url.URL{
329 Scheme: p.ValidateURL.Scheme,
330 Host: p.ValidateURL.Host,
331 Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "/collaborators/", username),
332 }
333 result := requests.New(endpoint.String()).
334 WithContext(ctx).
335 WithHeaders(getGitHubHeader(accessToken)).
336 Do()
337 if result.Error() != nil {
338 return false, result.Error()
339 }
341 if result.StatusCode() != 204 {
342 return false, fmt.Errorf("got %d from %q %s",
343 result.StatusCode(), endpoint.String(), result.Body())
344 }
346 logger.Printf("got %d from %q %s", result.StatusCode(), endpoint.String(), result.Body())
348 return true, nil
351// GetEmailAddress returns the Account email address
352func (p *GitHubProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
354 var emails []struct {
355 Email string `json:"email"`
356 Primary bool `json:"primary"`
357 Verified bool `json:"verified"`
358 }
360 // If usernames are set, check that first
361 verifiedUser := false
362 if len(p.Users) > 0 {
363 var err error
364 verifiedUser, err = p.hasUser(ctx, s.AccessToken)
365 if err != nil {
366 return "", err
367 }
368 // org and repository options are not configured
369 if !verifiedUser && p.Org == "" && p.Repo == "" {
370 return "", errors.New("missing github user")
371 }
372 }
373 // If a user is verified by username options, skip the following restrictions
374 if !verifiedUser {
375 if p.Org != "" {
376 if p.Team != "" {
377 if ok, err := p.hasOrgAndTeam(ctx, s.AccessToken); err != nil || !ok {
378 return "", err
379 }
380 } else {
381 if ok, err := p.hasOrg(ctx, s.AccessToken); err != nil || !ok {
382 return "", err
383 }
384 }
385 } else if p.Repo != "" && p.Token == "" { // If we have a token we'll do the collaborator check in GetUserName
386 if ok, err := p.hasRepo(ctx, s.AccessToken); err != nil || !ok {
387 return "", err
388 }
389 }
390 }
392 endpoint := &url.URL{
393 Scheme: p.ValidateURL.Scheme,
394 Host: p.ValidateURL.Host,
395 Path: path.Join(p.ValidateURL.Path, "/user/emails"),
396 }
397 err := requests.New(endpoint.String()).
398 WithContext(ctx).
399 WithHeaders(getGitHubHeader(s.AccessToken)).
400 Do().
401 UnmarshalInto(&emails)
402 if err != nil {
403 return "", err
404 }
406 returnEmail := ""
407 for _, email := range emails {
408 if email.Verified {
409 returnEmail = email.Email
410 if email.Primary {
411 return returnEmail, nil
412 }
413 }
414 }
416 return returnEmail, nil
419// GetUserName returns the Account user name
420func (p *GitHubProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) {
421 var user struct {
422 Login string `json:"login"`
423 Email string `json:"email"`
424 }
426 endpoint := &url.URL{
427 Scheme: p.ValidateURL.Scheme,
428 Host: p.ValidateURL.Host,
429 Path: path.Join(p.ValidateURL.Path, "/user"),
430 }
432 err := requests.New(endpoint.String()).
433 WithContext(ctx).
434 WithHeaders(getGitHubHeader(s.AccessToken)).
435 Do().
436 UnmarshalInto(&user)
437 if err != nil {
438 return "", err
439 }
441 // Now that we have the username we can check collaborator status
442 if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
443 if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
444 return "", err
445 }
446 }
448 return user.Login, nil
451// ValidateSessionState validates the AccessToken
452func (p *GitHubProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
453 return validateToken(ctx, p, s.AccessToken, getGitHubHeader(s.AccessToken))
456// isVerifiedUser
457func (p *GitHubProvider) isVerifiedUser(username string) bool {
458 for _, u := range p.Users {
459 if username == u {
460 return true
461 }
462 }
463 return false