Merge pull request #820 from dcarley/github_whitelist_orgs

Support org whitelists for GitHub+GHE remotes
This commit is contained in:
Brad Rydzewski 2015-01-15 21:50:01 -08:00
commit f79762177c
8 changed files with 178 additions and 8 deletions

View file

@ -100,12 +100,14 @@ open=true
[github]
client=""
secret=""
orgs=[]
[github_enterprise]
client=""
secret=""
api=""
url=""
orgs=[]
private_mode=false
[bitbucket]

View file

@ -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]

View file

@ -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

View file

@ -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()
})
})
})
}

View file

@ -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
}

View file

@ -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()
})
})
})
}

View file

@ -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,
),
)
}

View file

@ -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 }
]
`)