woodpecker/plugin/remote/github/github.go
Matt Bostock 307aed12bc Move open registration setting into remote plugins
...so that it's possible to enable or disable open registration on a
per-remote basis.

For example, the `DRONE_REGISTRATION_OPEN` environment variable now
becomes `DRONE_GITHUB_OPEN` when using GitHub as a remote.

The default for open registration in this commit is `false` (disabled),
which matches the existing behaviour.

This is useful if you need to support both public and private remotes,
e.g. GitHub.com and GitHub Enterprise, where you trust all of the
private users and want to allow open registration for those but would
not want all GitHub.com users to run builds on your server.

Tested with GitHub and GitLab.
2015-01-16 22:04:24 +00:00

313 lines
8.3 KiB
Go

package github
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/drone/drone/plugin/remote/github/oauth"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/model"
"github.com/drone/go-github/github"
)
const (
DefaultAPI = "https://api.github.com/"
DefaultURL = "https://github.com"
DefaultScope = "repo,repo:status,user:email"
)
type GitHub struct {
URL string
API string
Client string
Secret string
Private bool
SkipVerify bool
Orgs []string
Open bool
}
func New(url, api, client, secret string, private, skipVerify bool, orgs []string, open bool) *GitHub {
var github = GitHub{
URL: url,
API: api,
Client: client,
Secret: secret,
Private: private,
SkipVerify: skipVerify,
Orgs: orgs,
Open: open,
}
// the API must have a trailing slash
if !strings.HasSuffix(github.API, "/") {
github.API += "/"
}
// the URL must NOT have a trailing slash
if strings.HasSuffix(github.URL, "/") {
github.URL = github.URL[:len(github.URL)-1]
}
return &github
}
func NewDefault(client, secret string, orgs []string, open bool) *GitHub {
return New(DefaultURL, DefaultAPI, client, secret, false, false, orgs, open)
}
// Authorize handles GitHub API Authorization.
func (r *GitHub) Authorize(res http.ResponseWriter, req *http.Request) (*model.Login, error) {
var config = &oauth.Config{
ClientId: r.Client,
ClientSecret: r.Secret,
Scope: DefaultScope,
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", r.URL),
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", r.URL),
RedirectURL: fmt.Sprintf("%s/api/auth/%s", httputil.GetURL(req), r.GetKind()),
}
// get the OAuth code
var code = req.FormValue("code")
var state = req.FormValue("state")
if len(code) == 0 {
var random = GetRandom()
httputil.SetCookie(res, req, "github_state", random)
http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther)
return nil, nil
}
cookieState := httputil.GetCookie(req, "github_state")
httputil.DelCookie(res, req, "github_state")
if cookieState != state {
return nil, fmt.Errorf("Error matching state in OAuth2 redirect")
}
var trans = &oauth.Transport{Config: config}
var token, err = trans.Exchange(code)
if err != nil {
return nil, fmt.Errorf("Error exchanging token. %s", err)
}
var client = NewClient(r.API, token.AccessToken, r.SkipVerify)
var useremail, errr = GetUserEmail(client)
if errr != nil {
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
login.Login = *useremail.Login
login.Email = *useremail.Email
if useremail.Name != nil {
login.Name = *useremail.Name
}
return login, nil
}
// GetKind returns the internal identifier of this remote GitHub instane.
func (r *GitHub) GetKind() string {
if r.IsEnterprise() {
return model.RemoteGithubEnterprise
} else {
return model.RemoteGithub
}
}
// GetHost returns the hostname of this remote GitHub instance.
func (r *GitHub) GetHost() string {
uri, _ := url.Parse(r.URL)
return uri.Host
}
// IsEnterprise returns true if the remote system is an
// instance of GitHub Enterprise Edition.
func (r *GitHub) IsEnterprise() bool {
return r.URL != DefaultURL
}
// GetRepos fetches all repositories that the specified
// user has access to in the remote system.
func (r *GitHub) GetRepos(user *model.User) ([]*model.Repo, error) {
var repos []*model.Repo
var client = NewClient(r.API, user.Access, r.SkipVerify)
var list, err = GetAllRepos(client)
if err != nil {
return nil, err
}
var remote = r.GetKind()
var hostname = r.GetHost()
for _, item := range list {
var repo = model.Repo{
UserID: user.ID,
Remote: remote,
Host: hostname,
Owner: *item.Owner.Login,
Name: *item.Name,
Private: *item.Private,
URL: *item.HTMLURL,
CloneURL: *item.GitURL,
GitURL: *item.GitURL,
SSHURL: *item.SSHURL,
Role: &model.Perm{},
}
if r.Private || repo.Private {
repo.CloneURL = *item.SSHURL
}
// if no permissions we should skip the repository
// entirely, since this should never happen
if item.Permissions == nil {
continue
}
repo.Role.Admin = (*item.Permissions)["admin"]
repo.Role.Write = (*item.Permissions)["push"]
repo.Role.Read = (*item.Permissions)["pull"]
repos = append(repos, &repo)
}
return repos, err
}
// GetScript fetches the build script (.drone.yml) from the remote
// repository and returns in string format.
func (r *GitHub) GetScript(user *model.User, repo *model.Repo, hook *model.Hook) ([]byte, error) {
var client = NewClient(r.API, user.Access, r.SkipVerify)
return GetFile(client, repo.Owner, repo.Name, ".drone.yml", hook.Sha)
}
// Activate activates a repository by adding a Post-commit hook and
// a Public Deploy key, if applicable.
func (r *GitHub) Activate(user *model.User, repo *model.Repo, link string) error {
var client = NewClient(r.API, user.Access, r.SkipVerify)
var title, err = GetKeyTitle(link)
if err != nil {
return err
}
// if the CloneURL is using the SSHURL then we know that
// we need to add an SSH key to GitHub.
if repo.SSHURL == repo.CloneURL {
_, err = CreateUpdateKey(client, repo.Owner, repo.Name, title, repo.PublicKey)
if err != nil {
return err
}
}
_, err = CreateUpdateHook(client, repo.Owner, repo.Name, link)
return err
}
// ParseHook parses the post-commit hook from the Request body
// and returns the required data in a standard format.
func (r *GitHub) ParseHook(req *http.Request) (*model.Hook, error) {
// handle github ping
if req.Header.Get("X-Github-Event") == "ping" {
return nil, nil
}
// handle github pull request hook differently
if req.Header.Get("X-Github-Event") == "pull_request" {
return r.ParsePullRequestHook(req)
}
// parse the github Hook payload
var payload = GetPayload(req)
var data, err = github.ParseHook(payload)
if err != nil {
return nil, nil
}
// make sure this is being triggered because of a commit
// and not something like a tag deletion or whatever
if data.IsTag() ||
data.IsGithubPages() ||
data.IsHead() == false ||
data.IsDeleted() {
return nil, nil
}
var hook = new(model.Hook)
hook.Repo = data.Repo.Name
hook.Owner = data.Repo.Owner.Login
hook.Sha = data.Head.Id
hook.Branch = data.Branch()
if len(hook.Owner) == 0 {
hook.Owner = data.Repo.Owner.Name
}
// extract the author and message from the commit
// this is kind of experimental, since I don't know
// what I'm doing here.
if data.Head != nil && data.Head.Author != nil {
hook.Message = data.Head.Message
hook.Timestamp = data.Head.Timestamp
hook.Author = data.Head.Author.Email
} else if data.Commits != nil && len(data.Commits) > 0 && data.Commits[0].Author != nil {
hook.Message = data.Commits[0].Message
hook.Timestamp = data.Commits[0].Timestamp
hook.Author = data.Commits[0].Author.Email
}
return hook, nil
}
// ParsePullRequestHook parses the pull request hook from the Request body
// and returns the required data in a standard format.
func (r *GitHub) ParsePullRequestHook(req *http.Request) (*model.Hook, error) {
// parse the payload to retrieve the pull-request
// hook meta-data.
var payload = GetPayload(req)
var data, err = github.ParsePullRequestHook(payload)
if err != nil {
return nil, err
}
// ignore these
if data.Action != "opened" && data.Action != "synchronize" {
return nil, nil
}
// TODO we should also store the pull request branch (ie from x to y)
// we can find it here: data.PullRequest.Head.Ref
var hook = model.Hook{
Owner: data.Repo.Owner.Login,
Repo: data.Repo.Name,
Sha: data.PullRequest.Head.Sha,
Branch: data.PullRequest.Head.Ref,
Author: data.PullRequest.User.Login,
Gravatar: data.PullRequest.User.GravatarId,
Timestamp: time.Now().UTC().String(),
Message: data.PullRequest.Title,
PullRequest: strconv.Itoa(data.Number),
}
if len(hook.Owner) == 0 {
hook.Owner = data.Repo.Owner.Name
}
return &hook, nil
}
func (r *GitHub) OpenRegistration() bool {
return r.Open
}