diff --git a/README.md b/README.md index fef19f00a..3cd213cb8 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,14 @@ open=true [github] client="" secret="" +orgs=[] [github_enterprise] client="" secret="" api="" url="" +orgs=[] private_mode=false [bitbucket] diff --git a/packaging/root/etc/drone/drone.toml b/packaging/root/etc/drone/drone.toml index 29dfbe42e..3f74c6bcf 100644 --- a/packaging/root/etc/drone/drone.toml +++ b/packaging/root/etc/drone/drone.toml @@ -37,12 +37,14 @@ datasource="/var/lib/drone/drone.sqlite" # [github] # client="" # secret="" +# orgs=[] # [github_enterprise] # client="" # secret="" # api="" # url="" +# orgs=[] # private_mode=false # [bitbucket] diff --git a/plugin/remote/github/github.go b/plugin/remote/github/github.go index 31ff84e53..840d493f1 100644 --- a/plugin/remote/github/github.go +++ b/plugin/remote/github/github.go @@ -27,9 +27,10 @@ type GitHub struct { Secret string Private bool SkipVerify bool + Orgs []string } -func New(url, api, client, secret string, private, skipVerify bool) *GitHub { +func New(url, api, client, secret string, private, skipVerify bool, orgs []string) *GitHub { var github = GitHub{ URL: url, API: api, @@ -37,6 +38,7 @@ func New(url, api, client, secret string, private, skipVerify bool) *GitHub { Secret: secret, Private: private, SkipVerify: skipVerify, + Orgs: orgs, } // the API must have a trailing slash if !strings.HasSuffix(github.API, "/") { @@ -49,8 +51,8 @@ func New(url, api, client, secret string, private, skipVerify bool) *GitHub { return &github } -func NewDefault(client, secret string) *GitHub { - return New(DefaultURL, DefaultAPI, client, secret, false, false) +func NewDefault(client, secret string, orgs []string) *GitHub { + return New(DefaultURL, DefaultAPI, client, secret, false, false, orgs) } // Authorize handles GitHub API Authorization. @@ -92,6 +94,16 @@ func (r *GitHub) Authorize(res http.ResponseWriter, req *http.Request) (*model.L return nil, fmt.Errorf("Error retrieving user or verified email. %s", errr) } + if len(r.Orgs) > 0 { + allowedOrg, err := UserBelongsToOrg(client, r.Orgs) + if err != nil { + return nil, fmt.Errorf("Could not check org membership. %s", err) + } + if !allowedOrg { + return nil, fmt.Errorf("User does not belong to correct org") + } + } + var login = new(model.Login) login.ID = int64(*useremail.ID) login.Access = token.AccessToken diff --git a/plugin/remote/github/github_test.go b/plugin/remote/github/github_test.go index 071951b97..c46aede92 100644 --- a/plugin/remote/github/github_test.go +++ b/plugin/remote/github/github_test.go @@ -1,6 +1,9 @@ package github import ( + "fmt" + "net/http" + "net/http/httptest" "testing" "github.com/drone/drone/plugin/remote/github/testdata" @@ -90,5 +93,40 @@ func Test_Github(t *testing.T) { g.It("Should parse a commit hook") g.It("Should parse a pull request hook") + + g.Describe("Authorize", func() { + g.AfterEach(func() { + github.Orgs = []string{} + }) + + var resp = httptest.NewRecorder() + var state = "validstate" + var req, _ = http.NewRequest( + "GET", + fmt.Sprintf("%s/?code=sekret&state=%s", server.URL, state), + nil, + ) + req.AddCookie(&http.Cookie{Name: "github_state", Value: state}) + + g.It("Should authorize a valid user with no org restrictions", func() { + var login, err = github.Authorize(resp, req) + g.Assert(err == nil).IsTrue() + g.Assert(login == nil).IsFalse() + }) + + g.It("Should authorize a valid user in the correct org", func() { + github.Orgs = []string{"octocats-inc"} + var login, err = github.Authorize(resp, req) + g.Assert(err == nil).IsTrue() + g.Assert(login == nil).IsFalse() + }) + + g.It("Should not authorize a valid user in the wrong org", func() { + github.Orgs = []string{"acme"} + var login, err = github.Authorize(resp, req) + g.Assert(err != nil).IsTrue() + g.Assert(login == nil).IsTrue() + }) + }) }) } diff --git a/plugin/remote/github/helper.go b/plugin/remote/github/helper.go index b55895a89..16dd1f02d 100644 --- a/plugin/remote/github/helper.go +++ b/plugin/remote/github/helper.go @@ -138,7 +138,7 @@ func GetOrgRepos(client *github.Client, org string) ([]github.Repository, error) } // GetOrgs is a helper function that returns a list of -// all org repositories. +// all orgs that a user belongs to. func GetOrgs(client *github.Client) ([]github.Organization, error) { orgs, _, err := client.Organizations.List("", nil) return orgs, err @@ -270,3 +270,25 @@ func GetPayload(req *http.Request) []byte { } return []byte(payload) } + +// UserBelongsToOrg returns true if the currently authenticated user is a +// member of any of the organizations provided. +func UserBelongsToOrg(client *github.Client, permittedOrgs []string) (bool, error) { + userOrgs, err := GetOrgs(client) + if err != nil { + return false, err + } + + userOrgSet := make(map[string]struct{}, len(userOrgs)) + for _, org := range userOrgs { + userOrgSet[*org.Login] = struct{}{} + } + + for _, org := range permittedOrgs { + if _, ok := userOrgSet[org]; ok { + return true, nil + } + } + + return false, nil +} diff --git a/plugin/remote/github/helper_test.go b/plugin/remote/github/helper_test.go index 8bb060813..3155e61e6 100644 --- a/plugin/remote/github/helper_test.go +++ b/plugin/remote/github/helper_test.go @@ -12,13 +12,22 @@ func Test_Helper(t *testing.T) { var server = testdata.NewServer() defer server.Close() + var client = NewClient(server.URL, "sekret", false) + g := goblin.Goblin(t) g.Describe("GitHub Helper Functions", func() { g.It("Should Get a User") g.It("Should Get a User Primary Email") g.It("Should Get a User + Primary Email") - g.It("Should Get a list of Orgs") + + g.It("Should Get a list of Orgs", func() { + var orgs, err = GetOrgs(client) + g.Assert(err == nil).IsTrue() + g.Assert(len(orgs)).Equal(1) + g.Assert(*orgs[0].Login).Equal("octocats-inc") + }) + g.It("Should Get a list of User Repos") g.It("Should Get a list of Org Repos") g.It("Should Get a list of All Repos") @@ -30,5 +39,20 @@ func Test_Helper(t *testing.T) { g.It("Should Create or Update a Repo Hook") g.It("Should Get a Repo File") + g.Describe("UserBelongsToOrg", func() { + g.It("Should confirm user does belong to 'octocats-inc' org", func() { + var requiredOrgs = []string{"one", "octocats-inc", "two"} + var member, err = UserBelongsToOrg(client, requiredOrgs) + g.Assert(err == nil).IsTrue() + g.Assert(member).IsTrue() + }) + + g.It("Should confirm user not does belong to 'octocats-inc' org", func() { + var requiredOrgs = []string{"one", "two"} + var member, err = UserBelongsToOrg(client, requiredOrgs) + g.Assert(err == nil).IsTrue() + g.Assert(member).IsFalse() + }) + }) }) } diff --git a/plugin/remote/github/register.go b/plugin/remote/github/register.go index eb71403f2..52c72838c 100644 --- a/plugin/remote/github/register.go +++ b/plugin/remote/github/register.go @@ -9,6 +9,7 @@ var ( // GitHub cloud configuration details githubClient = config.String("github-client", "") githubSecret = config.String("github-secret", "") + githubOrgs = config.Strings("github-orgs") // GitHub Enterprise configuration details githubEnterpriseURL = config.String("github-enterprise-url", "") @@ -17,6 +18,7 @@ var ( githubEnterpriseSecret = config.String("github-enterprise-secret", "") githubEnterprisePrivate = config.Bool("github-enterprise-private-mode", true) githubEnterpriseSkipVerify = config.Bool("github-enterprise-skip-verify", false) + githubEnterpriseOrgs = config.Strings("github-enterprise-orgs") ) // Registers the GitHub plugins using the default @@ -33,7 +35,7 @@ func registerGitHub() { return } remote.Register( - NewDefault(*githubClient, *githubSecret), + NewDefault(*githubClient, *githubSecret, *githubOrgs), ) } @@ -53,6 +55,7 @@ func registerGitHubEnterprise() { *githubEnterpriseSecret, *githubEnterprisePrivate, *githubEnterpriseSkipVerify, + *githubEnterpriseOrgs, ), ) } diff --git a/plugin/remote/github/testdata/testdata.go b/plugin/remote/github/testdata/testdata.go index 6845aa0b5..d84262c5b 100644 --- a/plugin/remote/github/testdata/testdata.go +++ b/plugin/remote/github/testdata/testdata.go @@ -15,13 +15,22 @@ func NewServer() *httptest.Server { // evaluate the path to serve a dummy data file switch r.URL.Path { + case "/login/oauth/access_token": + w.Write(accessTokenPayload) + return + case "/user": + w.Write(userPayload) + return + case "/user/emails": + w.Write(userEmailsPayload) + return case "/user/repos": w.Write(userReposPayload) return case "/user/orgs": w.Write(userOrgsPayload) return - case "/orgs/github/repos": + case "/orgs/octocats-inc/repos": w.Write(userReposPayload) return case "/repos/octocat/Hello-World/contents/.drone.yml": @@ -56,6 +65,64 @@ func NewServer() *httptest.Server { return server } +var accessTokenPayload = []byte(`access_token=sekret&scope=repo%2Cuser%3Aemail&token_type=bearer`) + +var userPayload = []byte(` +{ + "login": "octocat", + "id": 1, + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false, + "name": "monalisa octocat", + "company": "GitHub", + "blog": "https://github.com/blog", + "location": "San Francisco", + "email": "octocat@github.com", + "hireable": false, + "bio": "There once was...", + "public_repos": 2, + "public_gists": 1, + "followers": 20, + "following": 0, + "created_at": "2008-01-14T04:33:35Z", + "updated_at": "2008-01-14T04:33:35Z", + "total_private_repos": 100, + "owned_private_repos": 100, + "private_gists": 81, + "disk_usage": 10000, + "collaborators": 8, + "plan": { + "name": "Medium", + "space": 400, + "private_repos": 20, + "collaborators": 0 + } +} +`) + +var userEmailsPayload = []byte(` +[ + { + "email": "octocat@github.com", + "verified": true, + "primary": true + } +] +`) + // sample repository list var userReposPayload = []byte(` [ @@ -108,7 +175,7 @@ var emptyObjPayload = []byte(`{}`) // sample org list response var userOrgsPayload = []byte(` [ - { "login": "github", "id": 1 } + { "login": "octocats-inc", "id": 1 } ] `)