From 8b3174e6a55e8e0e08221d94a72dc60f6a6c3c6d Mon Sep 17 00:00:00 2001 From: Maxim Sukharev Date: Thu, 8 Feb 2018 15:57:32 +0100 Subject: [PATCH] Restrict access of authenticated users Signed-off-by: Maxim Sukharev --- .env.tpl | 2 + README.md | 18 ++++++ cli/server/server.go | 5 +- server/handler/auth.go | 7 ++- server/service/oauth.go | 131 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 148 insertions(+), 15 deletions(-) diff --git a/.env.tpl b/.env.tpl index 8e8d770..2dd40e5 100644 --- a/.env.tpl +++ b/.env.tpl @@ -2,3 +2,5 @@ OAUTH_CLIENT_ID= OAUTH_CLIENT_SECRET= JWT_SIGNING_KEY=testing DB_CONNECTION=sqlite:///path/to/db.db +OAUTH_RESTRICT_ACCESS= +OAUTH_RESTRICT_REQUESTER_ACCESS= diff --git a/README.md b/README.md index a6df612..11097fa 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,24 @@ In this case, origin will be the internal database, and destination the new data The annotations made by the users will be stored in the **`assignments`** table. +## Access control + +It is possible to restrict access and choose each user's role by adding their GitHub accounts to a specific [organization](https://help.github.com/articles/collaborating-with-groups-in-organizations/) or [team](https://help.github.com/articles/organizing-members-into-teams/). + +This is optional, if you don't set any restrictions all users with a valid GitHub account will be able to login as a Requester. You may also set a restriction only for Requester users, and leave open access to anyone as Workers. + +To do so, set the following variables in your `.env` file: + +* `OAUTH_RESTRICT_ACCESS` +* `OAUTH_RESTRICT_REQUESTER_ACCESS` + +Both variables accept a string with either `org:` or `team:`. For example: + +```bash +OAUTH_RESTRICT_ACCESS=org:my-organization +OAUTH_RESTRICT_REQUESTER_ACCESS=team:123456 +``` + ## Contributing [Contributions](https://github.com/src-d/code-annotation/issues) are more than welcome, if you are interested please take a look to diff --git a/cli/server/server.go b/cli/server/server.go index 292accb..4033bb8 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -44,7 +44,10 @@ func main() { // create services var oauthConfig service.OAuthConfig envconfig.MustProcess("oauth", &oauthConfig) - oauth := service.NewOAuth(oauthConfig.ClientID, oauthConfig.ClientSecret) + oauth := service.NewOAuth( + oauthConfig.ClientID, oauthConfig.ClientSecret, + oauthConfig.RestrictAccess, oauthConfig.RestrictRequesterAccess, + ) var jwtConfig service.JWTConfig envconfig.MustProcess("jwt", &jwtConfig) diff --git a/server/handler/auth.go b/server/handler/auth.go index 495f4b6..2b3b171 100644 --- a/server/handler/auth.go +++ b/server/handler/auth.go @@ -36,6 +36,10 @@ func OAuthCallback( code := r.FormValue("code") ghUser, err := oAuth.GetUser(r.Context(), code) + if err == service.ErrNoAccess { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } if err != nil { logger.Errorf("oauth get user error: %s", err) // FIXME can it be not server error? for wrong code @@ -55,7 +59,8 @@ func OAuthCallback( Login: ghUser.Login, Username: ghUser.Username, AvatarURL: ghUser.AvatarURL, - Role: model.Requester} + Role: ghUser.Role, + } err = userRepo.Create(user) if err != nil { diff --git a/server/service/oauth.go b/server/service/oauth.go index b1746e8..3353953 100644 --- a/server/service/oauth.go +++ b/server/service/oauth.go @@ -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 { + if strings.HasPrefix(restriction, orgPrefix) { + org := strings.TrimPrefix(restriction, orgPrefix) + 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:' or 'team:'", 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 +}