Support org whitelists for GitHub+GHE remotes

Allow the GitHub and GitHub Enterprise remotes to restrict who can login
based on a user's organisation membership. This can be used as a safe
addition to open registration and also ensures that access is revoked when a
user is subsequently removed from the org. The default is not to restrict at
all.
This commit is contained in:
Dan Carley 2015-01-15 14:39:44 +00:00
parent 789adf90e4
commit 8fa473b07a
7 changed files with 84 additions and 8 deletions

View file

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

View file

@ -37,12 +37,14 @@ datasource="/var/lib/drone/drone.sqlite"
# [github] # [github]
# client="" # client=""
# secret="" # secret=""
# orgs=[]
# [github_enterprise] # [github_enterprise]
# client="" # client=""
# secret="" # secret=""
# api="" # api=""
# url="" # url=""
# orgs=[]
# private_mode=false # private_mode=false
# [bitbucket] # [bitbucket]

View file

@ -27,9 +27,10 @@ type GitHub struct {
Secret string Secret string
Private bool Private bool
SkipVerify 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{ var github = GitHub{
URL: url, URL: url,
API: api, API: api,
@ -37,6 +38,7 @@ func New(url, api, client, secret string, private, skipVerify bool) *GitHub {
Secret: secret, Secret: secret,
Private: private, Private: private,
SkipVerify: skipVerify, SkipVerify: skipVerify,
Orgs: orgs,
} }
// the API must have a trailing slash // the API must have a trailing slash
if !strings.HasSuffix(github.API, "/") { if !strings.HasSuffix(github.API, "/") {
@ -49,8 +51,8 @@ func New(url, api, client, secret string, private, skipVerify bool) *GitHub {
return &github return &github
} }
func NewDefault(client, secret string) *GitHub { func NewDefault(client, secret string, orgs []string) *GitHub {
return New(DefaultURL, DefaultAPI, client, secret, false, false) return New(DefaultURL, DefaultAPI, client, secret, false, false, orgs)
} }
// Authorize handles GitHub API Authorization. // 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) 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) var login = new(model.Login)
login.ID = int64(*useremail.ID) login.ID = int64(*useremail.ID)
login.Access = token.AccessToken login.Access = token.AccessToken

View file

@ -94,7 +94,11 @@ func Test_Github(t *testing.T) {
g.It("Should parse a pull request hook") g.It("Should parse a pull request hook")
g.It("Should authorize a valid user", func() { g.Describe("Authorize", func() {
g.AfterEach(func() {
github.Orgs = []string{}
})
var resp = httptest.NewRecorder() var resp = httptest.NewRecorder()
var state = "validstate" var state = "validstate"
var req, _ = http.NewRequest( var req, _ = http.NewRequest(
@ -104,9 +108,25 @@ func Test_Github(t *testing.T) {
) )
req.AddCookie(&http.Cookie{Name: "github_state", Value: state}) 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) var login, err = github.Authorize(resp, req)
g.Assert(err == nil).IsTrue() g.Assert(err == nil).IsTrue()
g.Assert(login == nil).IsFalse() 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

@ -270,3 +270,25 @@ func GetPayload(req *http.Request) []byte {
} }
return []byte(payload) 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

@ -39,5 +39,20 @@ func Test_Helper(t *testing.T) {
g.It("Should Create or Update a Repo Hook") g.It("Should Create or Update a Repo Hook")
g.It("Should Get a Repo File") 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 // GitHub cloud configuration details
githubClient = config.String("github-client", "") githubClient = config.String("github-client", "")
githubSecret = config.String("github-secret", "") githubSecret = config.String("github-secret", "")
githubOrgs = config.Strings("github-orgs")
// GitHub Enterprise configuration details // GitHub Enterprise configuration details
githubEnterpriseURL = config.String("github-enterprise-url", "") githubEnterpriseURL = config.String("github-enterprise-url", "")
@ -17,6 +18,7 @@ var (
githubEnterpriseSecret = config.String("github-enterprise-secret", "") githubEnterpriseSecret = config.String("github-enterprise-secret", "")
githubEnterprisePrivate = config.Bool("github-enterprise-private-mode", true) githubEnterprisePrivate = config.Bool("github-enterprise-private-mode", true)
githubEnterpriseSkipVerify = config.Bool("github-enterprise-skip-verify", false) githubEnterpriseSkipVerify = config.Bool("github-enterprise-skip-verify", false)
githubEnterpriseOrgs = config.Strings("github-enterprise-orgs")
) )
// Registers the GitHub plugins using the default // Registers the GitHub plugins using the default
@ -33,7 +35,7 @@ func registerGitHub() {
return return
} }
remote.Register( remote.Register(
NewDefault(*githubClient, *githubSecret), NewDefault(*githubClient, *githubSecret, *githubOrgs),
) )
} }
@ -53,6 +55,7 @@ func registerGitHubEnterprise() {
*githubEnterpriseSecret, *githubEnterpriseSecret,
*githubEnterprisePrivate, *githubEnterprisePrivate,
*githubEnterpriseSkipVerify, *githubEnterpriseSkipVerify,
*githubEnterpriseOrgs,
), ),
) )
} }