-
Notifications
You must be signed in to change notification settings - Fork 25
Restrict access of authenticated users #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,46 +5,59 @@ import ( | |
| "crypto/rand" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| "github.com/gorilla/sessions" | ||
| "github.com/src-d/code-annotation/server/model" | ||
| "golang.org/x/oauth2" | ||
| "golang.org/x/oauth2/github" | ||
| ) | ||
|
|
||
| // ErrNoAccess means user doesn't have necessary permissions for a resource | ||
| var ErrNoAccess = errors.New("access denied") | ||
|
|
||
| // OAuthConfig defines enviroment variables for OAuth | ||
| type OAuthConfig struct { | ||
| ClientID string `envconfig:"CLIENT_ID" required:"true"` | ||
| ClientSecret string `envconfig:"CLIENT_SECRET" required:"true"` | ||
| ClientID string `envconfig:"CLIENT_ID" required:"true"` | ||
| ClientSecret string `envconfig:"CLIENT_SECRET" required:"true"` | ||
| RestrictAccess string `envconfig:"RESTRICT_ACCESS"` | ||
| RestrictRequesterAccess string `envconfig:"RESTRICT_REQUESTER_ACCESS"` | ||
| } | ||
|
|
||
| // OAuth service abstracts OAuth implementation | ||
| type OAuth struct { | ||
| config *oauth2.Config | ||
| store *sessions.CookieStore | ||
| config *oauth2.Config | ||
| store *sessions.CookieStore | ||
| restrictAccess string | ||
| restrictRequesterAccess string | ||
| } | ||
|
|
||
| // NewOAuth return new OAuth service | ||
| func NewOAuth(clientID, clientSecret string) *OAuth { | ||
| func NewOAuth(clientID, clientSecret, restrictAccess, restrictRequesterAccess string) *OAuth { | ||
| config := &oauth2.Config{ | ||
| ClientID: clientID, | ||
| ClientSecret: clientSecret, | ||
| Scopes: []string{"read:user"}, | ||
| Scopes: []string{"read:user", "read:org"}, | ||
| Endpoint: github.Endpoint, | ||
| } | ||
| return &OAuth{ | ||
| config: config, | ||
| store: sessions.NewCookieStore([]byte(clientSecret)), | ||
| config: config, | ||
| store: sessions.NewCookieStore([]byte(clientSecret)), | ||
| restrictAccess: restrictAccess, | ||
| restrictRequesterAccess: restrictRequesterAccess, | ||
| } | ||
| } | ||
|
|
||
| // GithubUser represents the user response returned by the GitHub auth. | ||
| type githubUser struct { | ||
| ID int `json:"id"` | ||
| Login string `json:"login"` | ||
| Username string `json:"name"` | ||
| AvatarURL string `json:"avatar_url"` | ||
| ID int `json:"id"` | ||
| Login string `json:"login"` | ||
| Username string `json:"name"` | ||
| AvatarURL string `json:"avatar_url"` | ||
| Role model.Role `json:"-"` | ||
| } | ||
|
|
||
| // MakeAuthURL returns string for redirect to provider | ||
|
|
@@ -66,9 +79,11 @@ func (o *OAuth) ValidateState(r *http.Request, state string) error { | |
| if err != nil { | ||
| return fmt.Errorf("can't get session: %s", err) | ||
| } | ||
|
|
||
| if state != session.Values["state"] { | ||
| return fmt.Errorf("incorrect state: %s", state) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -78,17 +93,107 @@ func (o *OAuth) GetUser(ctx context.Context, code string) (*githubUser, error) { | |
| if err != nil { | ||
| return nil, fmt.Errorf("oauth exchange error: %s", err) | ||
| } | ||
|
|
||
| client := o.config.Client(ctx, token) | ||
| resp, err := client.Get("https://api.github.com/user") | ||
| if err != nil { | ||
| return nil, fmt.Errorf("can't get user from github: %s", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| var user githubUser | ||
| err = json.NewDecoder(resp.Body).Decode(&user) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("can't parse github response: %s", err) | ||
| return nil, fmt.Errorf("can't parse github user response: %s", err) | ||
| } | ||
|
|
||
| role := model.Requester | ||
| if o.restrictRequesterAccess != "" { | ||
| err := o.checkAccess(client, o.restrictRequesterAccess, user.Login) | ||
| if err != nil && err != ErrNoAccess { | ||
| return nil, err | ||
| } | ||
| if err == ErrNoAccess { | ||
| role = model.Worker | ||
| } | ||
| } | ||
|
|
||
| if o.restrictAccess != "" && role == model.Worker { | ||
| if err := o.checkAccess(client, o.restrictAccess, user.Login); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| user.Role = role | ||
|
|
||
| return &user, nil | ||
| } | ||
|
|
||
| const orgPrefix = "org:" | ||
| const teamPrefix = "team:" | ||
|
|
||
| func (o *OAuth) checkAccess(client *http.Client, restriction, login string) error { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does not need access to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doesn't, but I prefer to group functions as struct methods.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wow... I think that's an ugly thing (and also forces extra steps in unit tests...) :( |
||
| if strings.HasPrefix(restriction, orgPrefix) { | ||
| org := strings.TrimPrefix(restriction, orgPrefix) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. too long. func (o *OAuth) checkAccess(client *http.Client, restriction, login string) error {
if strings.HasPrefix(restriction, orgPrefix) {
org := strings.TrimPrefix(restriction, orgPrefix)
return checkUserInOrganization(client, org, login)
}
if strings.HasPrefix(restriction, teamPrefix) {
team := strings.TrimPrefix(restriction, teamPrefix)
return checkUserInTeam(client, team, login)
}
return return errors.New("invalid restriction")
}
func checkUserInOrganization(client *http.Client, org string, login string) error {}
func checkUserInTeam(client *http.Client, ream string, login string) error {}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doesn't feel too long for me, but ok. |
||
| return o.checkUserInOrg(client, org, login) | ||
| } | ||
|
|
||
| if strings.HasPrefix(restriction, teamPrefix) { | ||
| team := strings.TrimPrefix(restriction, teamPrefix) | ||
| return o.checkUserInTeam(client, team, login) | ||
| } | ||
|
|
||
| return fmt.Errorf("invalid restriction '%s', it must be one of 'org:<organization-name>' or 'team:<team-id>'", restriction) | ||
| } | ||
|
|
||
| func (o *OAuth) checkUserInOrg(client *http.Client, org, login string) error { | ||
| url := fmt.Sprintf("https://api.github.com/orgs/%s/members/%s", org, login) | ||
| resp, err := client.Get(url) | ||
| if err != nil { | ||
| return fmt.Errorf("can't get user organizations from github: %s", err) | ||
| } | ||
|
|
||
| // StatusNoContent means user is a member | ||
| // https://developer.github.com/v3/orgs/members/#check-membership | ||
| if resp.StatusCode != http.StatusNoContent { | ||
| return ErrNoAccess | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (o *OAuth) checkUserInTeam(client *http.Client, team, login string) error { | ||
| url := fmt.Sprintf("https://api.github.com/teams/%s/memberships/%s", team, login) | ||
| r, err := http.NewRequest("GET", url, nil) | ||
| // The Nested Teams API is currently available for developers to preview only | ||
| r.Header.Add("Accept", "application/vnd.github.hellcat-preview+json") | ||
| if err != nil { | ||
| return fmt.Errorf("can't create team request: %s", err) | ||
| } | ||
|
|
||
| resp, err := client.Do(r) | ||
| if err != nil { | ||
| return fmt.Errorf("can't get user organizations from github: %s", err) | ||
| } | ||
|
|
||
| // Only StatusOK means user is a member | ||
| // https://developer.github.com/v3/orgs/teams/#get-team-membership | ||
| if resp.StatusCode != http.StatusOK { | ||
| return ErrNoAccess | ||
| } | ||
|
|
||
| // Don't allow access to pending members | ||
| defer resp.Body.Close() | ||
| var teamResp struct { | ||
| State string `json:"state"` | ||
| } | ||
| if err = json.NewDecoder(resp.Body).Decode(&teamResp); err != nil { | ||
| return fmt.Errorf("can't parse github team response: %s", err) | ||
| } | ||
|
|
||
| if teamResp.State != "active" { | ||
| return ErrNoAccess | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this code would be easier to read if it was:
If I'm not wrong, that way it's easier to understand the logic behind the role assignment depending on GitHub groups and the default value if nothing happens.