diff --git a/Makefile b/Makefile index a6592238a6..c6be682694 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ clean: rm -f debian/drone/usr/local/bin/droned rm -f debian/drone.deb rm -f server/server - rm -f client/client + rm -f cmd/cmd lessc: lessc --clean-css server/app/styles/drone.less server/app/styles/drone.css diff --git a/plugin/remote/bitbucket/bitbucket.go b/plugin/remote/bitbucket/bitbucket.go index 1c49e0f9c5..21f7fb8f01 100644 --- a/plugin/remote/bitbucket/bitbucket.go +++ b/plugin/remote/bitbucket/bitbucket.go @@ -6,78 +6,50 @@ import ( "net/url" "time" - "github.com/drone/drone/plugin/remote" "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/model" "github.com/drone/go-bitbucket/bitbucket" "github.com/drone/go-bitbucket/oauth1" ) +const ( + DefaultAPI = "https://api.bitbucket.org/1.0" + DefaultURL = "https://bitbucket.org" +) + type Bitbucket struct { - URL string `json:"url"` // https://bitbucket.org - API string `json:"api"` // https://api.bitbucket.org - Client string `json:"client"` - Secret string `json:"secret"` - Enabled bool `json:"enabled"` + URL string + API string + Client string + Secret string } -// GetName returns the name of this remote system. -func (b *Bitbucket) GetName() string { - return "bitbucket.org" -} - -// GetHost returns the url.Host of this remote system. -func (b *Bitbucket) GetHost() (host string) { - u, err := url.Parse(b.URL) - if err != nil { - return +func New(url, api, client, secret string) *Bitbucket { + return &Bitbucket{ + URL: url, + API: api, + Client: client, + Secret: secret, } - return u.Host } -// GetHook parses the post-commit hook from the Request body -// and returns the required data in a standard format. -func (b *Bitbucket) GetHook(r *http.Request) (*remote.Hook, error) { - // get the payload from the request - payload := r.FormValue("payload") - - // parse the post-commit hook - hook, err := bitbucket.ParseHook([]byte(payload)) - if err != nil { - return nil, err - } - - // verify the payload has the minimum amount of required data. - if hook.Repo == nil || hook.Commits == nil || len(hook.Commits) == 0 { - return nil, fmt.Errorf("Invalid Bitbucket post-commit Hook. Missing Repo or Commit data.") - } - - return &remote.Hook{ - Owner: hook.Repo.Owner, - Repo: hook.Repo.Name, - Sha: hook.Commits[len(hook.Commits)-1].Hash, - Branch: hook.Commits[len(hook.Commits)-1].Branch, - Author: hook.Commits[len(hook.Commits)-1].Author, - Timestamp: time.Now().UTC().String(), - Message: hook.Commits[len(hook.Commits)-1].Message, - }, nil +func NewDefault(client, secret string) *Bitbucket { + return New(DefaultURL, DefaultAPI, client, secret) } -// GetLogin handles authentication to third party, remote services -// and returns the required user data in a standard format. -func (b *Bitbucket) GetLogin(w http.ResponseWriter, r *http.Request) (*remote.Login, error) { - - // bitbucket oauth1 consumer +// Authorize handles Bitbucket API Authorization +func (r *Bitbucket) Authorize(res http.ResponseWriter, req *http.Request) (*model.Login, error) { consumer := oauth1.Consumer{ RequestTokenURL: "https://bitbucket.org/api/1.0/oauth/request_token/", AuthorizationURL: "https://bitbucket.org/!api/1.0/oauth/authenticate", AccessTokenURL: "https://bitbucket.org/api/1.0/oauth/access_token/", - CallbackURL: httputil.GetScheme(r) + "://" + httputil.GetHost(r) + "/login/bitbucket.org", - ConsumerKey: b.Client, - ConsumerSecret: b.Secret, + CallbackURL: httputil.GetScheme(req) + "://" + httputil.GetHost(req) + "/login/bitbucket.org", + ConsumerKey: r.Client, + ConsumerSecret: r.Secret, } // get the oauth verifier - verifier := r.FormValue("oauth_verifier") + verifier := req.FormValue("oauth_verifier") if len(verifier) == 0 { // Generate a Request Token requestToken, err := consumer.RequestToken() @@ -86,19 +58,19 @@ func (b *Bitbucket) GetLogin(w http.ResponseWriter, r *http.Request) (*remote.Lo } // add the request token as a signed cookie - httputil.SetCookie(w, r, "bitbucket_token", requestToken.Encode()) + httputil.SetCookie(res, req, "bitbucket_token", requestToken.Encode()) url, _ := consumer.AuthorizeRedirect(requestToken) - http.Redirect(w, r, url, http.StatusSeeOther) + http.Redirect(res, req, url, http.StatusSeeOther) return nil, nil } // remove bitbucket token data once before redirecting // back to the application. - defer httputil.DelCookie(w, r, "bitbucket_token") + defer httputil.DelCookie(res, req, "bitbucket_token") // get the tokens from the request - requestTokenStr := httputil.GetCookie(r, "bitbucket_token") + requestTokenStr := httputil.GetCookie(req, "bitbucket_token") requestToken, err := oauth1.ParseRequestTokenStr(requestTokenStr) if err != nil { return nil, err @@ -112,8 +84,8 @@ func (b *Bitbucket) GetLogin(w http.ResponseWriter, r *http.Request) (*remote.Lo // create the Bitbucket client client := bitbucket.New( - b.Client, - b.Secret, + r.Client, + r.Secret, accessToken.Token(), accessToken.Secret(), ) @@ -125,7 +97,7 @@ func (b *Bitbucket) GetLogin(w http.ResponseWriter, r *http.Request) (*remote.Lo } // put the user data in the common format - login := remote.Login{ + login := model.Login{ Login: user.User.Username, Access: accessToken.Token(), Secret: accessToken.Secret(), @@ -140,13 +112,147 @@ func (b *Bitbucket) GetLogin(w http.ResponseWriter, r *http.Request) (*remote.Lo return &login, nil } -// GetClient returns a new Bitbucket remote client. -func (b *Bitbucket) GetClient(access, secret string) remote.Client { - return &Client{b, access, secret} +// GetKind returns the internal identifier of this remote Bitbucket instane. +func (r *Bitbucket) GetKind() string { + return model.RemoteBitbucket } -// IsMatch returns true if the hostname matches the -// hostname of this remote client. -func (b *Bitbucket) IsMatch(hostname string) bool { - return hostname == "bitbucket.org" +// GetHost returns the hostname of this remote Bitbucket instance. +func (r *Bitbucket) GetHost() string { + uri, _ := url.Parse(r.URL) + return uri.Host +} + +// GetRepos fetches all repositories that the specified +// user has access to in the remote system. +func (r *Bitbucket) GetRepos(user *model.User) ([]*model.Repo, error) { + var repos []*model.Repo + var client = bitbucket.New( + r.Client, + r.Secret, + user.Access, + user.Secret, + ) + var list, err = client.Repos.List() + if err != nil { + return nil, err + } + + var remote = r.GetKind() + var hostname = r.GetHost() + + for _, item := range list { + // for now we only support git repos + if item.Scm != "git" { + continue + } + + // these are the urls required to clone the repository + // TODO use the bitbucketurl.Host and bitbucketurl.Scheme instead of hardcoding + // so that we can support Stash. + var clone = fmt.Sprintf("https://bitbucket.org/%s/%s.git", item.Owner, item.Name) + var ssh = fmt.Sprintf("git@bitbucket.org:%s/%s.git", item.Owner, item.Name) + + var repo = model.Repo{ + UserID: user.ID, + Remote: remote, + Host: hostname, + Owner: item.Owner, + Name: item.Name, + Private: item.Private, + CloneURL: clone, + GitURL: clone, + SSHURL: ssh, + Role: &model.Perm{ + Admin: true, + Write: true, + Read: true, + }, + } + + if repo.Private { + repo.CloneURL = repo.SSHURL + } + + 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 *Bitbucket) GetScript(user *model.User, repo *model.Repo, hook *model.Hook) ([]byte, error) { + var client = bitbucket.New( + r.Client, + r.Secret, + user.Access, + user.Secret, + ) + + // get the yaml from the database + var raw, err = client.Sources.Find(repo.Owner, repo.Name, hook.Sha, ".drone.yml") + if err != nil { + return nil, err + } + + return []byte(raw.Data), nil +} + +// Activate activates a repository by adding a Post-commit hook and +// a Public Deploy key, if applicable. +func (r *Bitbucket) Activate(user *model.User, repo *model.Repo, link string) error { + var client = bitbucket.New( + r.Client, + r.Secret, + user.Access, + user.Secret, + ) + + // parse the hostname from the hook, and use this + // to name the ssh key + var hookurl, err = url.Parse(link) + if err != nil { + return err + } + + // if the repository is private we'll need + // to upload a github key to the repository + if repo.Private { + // name the key + var keyname = "drone@" + hookurl.Host + var _, err = client.RepoKeys.CreateUpdate(repo.Owner, repo.Name, repo.PublicKey, keyname) + if err != nil { + return err + } + } + + // add the hook + _, err = client.Brokers.CreateUpdate(repo.Owner, repo.Name, link, bitbucket.BrokerTypePost) + return err +} + +// ParseHook parses the post-commit hook from the Request body +// and returns the required data in a standard format. +func (r *Bitbucket) ParseHook(req *http.Request) (*model.Hook, error) { + var payload = req.FormValue("payload") + var hook, err = bitbucket.ParseHook([]byte(payload)) + if err != nil { + return nil, err + } + + // verify the payload has the minimum amount of required data. + if hook.Repo == nil || hook.Commits == nil || len(hook.Commits) == 0 { + return nil, fmt.Errorf("Invalid Bitbucket post-commit Hook. Missing Repo or Commit data.") + } + + return &model.Hook{ + Owner: hook.Repo.Owner, + Repo: hook.Repo.Name, + Sha: hook.Commits[len(hook.Commits)-1].Hash, + Branch: hook.Commits[len(hook.Commits)-1].Branch, + Author: hook.Commits[len(hook.Commits)-1].Author, + Timestamp: time.Now().UTC().String(), + Message: hook.Commits[len(hook.Commits)-1].Message, + }, nil } diff --git a/plugin/remote/bitbucket/client.go b/plugin/remote/bitbucket/client.go deleted file mode 100644 index ea73b4127e..0000000000 --- a/plugin/remote/bitbucket/client.go +++ /dev/null @@ -1,146 +0,0 @@ -package bitbucket - -import ( - "fmt" - "github.com/drone/drone/plugin/remote" - "github.com/drone/go-bitbucket/bitbucket" - "net/url" -) - -type Client struct { - config *Bitbucket - access string // user access token - secret string // user access token secret -} - -// GetUser fetches the user by ID (login name). -func (c *Client) GetUser(login string) (*remote.User, error) { - return nil, nil -} - -// GetRepos fetches all repositories that the specified -// user has access to in the remote system. -func (c *Client) GetRepos(owner string) ([]*remote.Repo, error) { - // create the Bitbucket client - client := bitbucket.New( - c.config.Client, - c.config.Secret, - c.access, - c.secret, - ) - - // parse the hostname from the bitbucket url - bitbucketurl, err := url.Parse(c.config.URL) - if err != nil { - return nil, err - } - - repos, err := client.Repos.List() - if err != nil { - return nil, err - } - - // store results in common format - result := []*remote.Repo{} - - // loop throught the list and convert to the standard repo format - for _, repo := range repos { - // for now we only support git repos - if repo.Scm != "git" { - continue - } - - // these are the urls required to clone the repository - // TODO use the bitbucketurl.Host and bitbucketurl.Scheme instead of hardcoding - // so that we can support Stash. - clone := fmt.Sprintf("https://bitbucket.org/%s/%s.git", repo.Owner, repo.Name) - ssh := fmt.Sprintf("git@bitbucket.org:%s/%s.git", repo.Owner, repo.Name) - - result = append(result, &remote.Repo{ - Host: bitbucketurl.Host, - Owner: repo.Owner, - Name: repo.Name, - Kind: repo.Scm, - Private: repo.Private, - Clone: clone, - SSH: ssh, - // Bitbucket doesn't return permissions with repository - // lists, so we're going to grant full access. - // - // TODO we need to verify this API call only returns - // repositories that we can access (ie not repos we just follow). - // otherwise this would cause a security flaw. - Push: true, - Pull: true, - Admin: true, - }) - } - - return result, nil -} - -// GetScript fetches the build script (.drone.yml) from the remote -// repository and returns in string format. -func (c *Client) GetScript(hook *remote.Hook) (out string, err error) { - // create the Bitbucket client - client := bitbucket.New( - c.config.Client, - c.config.Secret, - c.access, - c.secret, - ) - - // get the yaml from the database - raw, err := client.Sources.Find(hook.Owner, hook.Repo, hook.Sha, ".drone.yml") - if err != nil { - return - } - - return raw.Data, nil -} - -// SetStatus -func (c *Client) SetStatus(owner, name, sha, status string) error { - // not implemented for Bitbucket - return nil -} - -// SetActive -func (c *Client) SetActive(owner, name, hook, key string) error { - // create the Bitbucket client - client := bitbucket.New( - c.config.Client, - c.config.Secret, - c.access, - c.secret, - ) - - // parse the hostname from the hook, and use this - // to name the ssh key - hookurl, err := url.Parse(hook) - if err != nil { - return err - } - - // fetch the repository so that we can see if it - // is public or private. - repo, err := client.Repos.Find(owner, name) - if err != nil { - return err - } - - // if the repository is private we'll need - // to upload a github key to the repository - if repo.Private { - // name the key - keyname := "drone@" + hookurl.Host - _, err := client.RepoKeys.CreateUpdate(owner, name, key, keyname) - if err != nil { - return err - } - } - - // add the hook - _, err = client.Brokers.CreateUpdate(owner, name, hook, bitbucket.BrokerTypePost) - return err -} diff --git a/plugin/remote/bitbucket/init.go b/plugin/remote/bitbucket/init.go deleted file mode 100644 index ec9464e079..0000000000 --- a/plugin/remote/bitbucket/init.go +++ /dev/null @@ -1,20 +0,0 @@ -package bitbucket - -import ( - "github.com/drone/drone/plugin/remote" - "github.com/drone/drone/shared/model" -) - -func init() { - remote.Register(model.RemoteBitbucket, plugin) -} - -func plugin(remote *model.Remote) remote.Remote { - return &Bitbucket{ - URL: remote.URL, - API: remote.API, - Client: remote.Client, - Secret: remote.Secret, - Enabled: remote.Open, - } -} diff --git a/plugin/remote/bitbucket/register.go b/plugin/remote/bitbucket/register.go new file mode 100644 index 0000000000..b59b81d9b0 --- /dev/null +++ b/plugin/remote/bitbucket/register.go @@ -0,0 +1,16 @@ +package bitbucket + +import ( + "os" + + "github.com/drone/drone/plugin/remote" +) + +func init() { + var cli = os.Getenv("BITBUCKET_CLIENT") + var sec = os.Getenv("BITBUCKET_SECRET") + if len(cli) == 0 || len(sec) == 0 { + return + } + remote.Register(NewDefault(cli, sec)) +} diff --git a/plugin/remote/github/client.go b/plugin/remote/github/client.go deleted file mode 100644 index ac11455a9a..0000000000 --- a/plugin/remote/github/client.go +++ /dev/null @@ -1,166 +0,0 @@ -package github - -import ( - "fmt" - "net/url" - - "github.com/drone/drone/plugin/remote" - "github.com/drone/go-github/github" -) - -type Client struct { - config *Github - access string // user access token -} - -// GetUser fetches the user by ID (login name). -func (c *Client) GetUser(login string) (*remote.User, error) { - return nil, nil -} - -// GetRepos fetches all repositories that the specified -// user has access to in the remote system. -func (c *Client) GetRepos(owner string) ([]*remote.Repo, error) { - // create the github client - client := github.New(c.access) - client.ApiUrl = c.config.API - - // retrieve a list of all github repositories - repos, err := client.Repos.ListAll() - if err != nil { - return nil, err - } - - // store results in common format - result := []*remote.Repo{} - - // parse the hostname from the github url - githuburl, err := url.Parse(c.config.URL) - if err != nil { - return nil, err - } - - // loop throught the list and convert to the standard repo format - for _, repo := range repos { - // if the repository is private we should use the ssh - // url to clone, else we should use the git url - if repo.Private { - repo.CloneUrl = repo.SshUrl - } - - result = append(result, &remote.Repo{ - ID: repo.ID, - Host: githuburl.Host, - Owner: repo.Owner.Login, - Name: repo.Name, - Kind: "git", - Clone: repo.CloneUrl, - Git: repo.GitUrl, - SSH: repo.SshUrl, - Private: repo.Private, - Push: repo.Permissions.Push, - Pull: repo.Permissions.Pull, - Admin: repo.Permissions.Admin, - }) - } - - return result, nil -} - -// GetScript fetches the build script (.drone.yml) from the remote -// repository using the GitHub API and returns the raw file in string format. -func (c *Client) GetScript(hook *remote.Hook) (out string, err error) { - // create the github client - client := github.New(c.access) - client.ApiUrl = c.config.API - - // retrieve the .drone.yml file from GitHub - content, err := client.Contents.FindRef(hook.Owner, hook.Repo, ".drone.yml", hook.Sha) - if err != nil { - return - } - - // decode the content - raw, err := content.DecodeContent() - if err != nil { - return - } - - return string(raw), nil -} - -// SetStatus -func (c *Client) SetStatus(owner, name, sha, status string) error { - // create the github client - client := github.New(c.access) - client.ApiUrl = c.config.API - - // convert from drone status to github status - var message string - switch status { - case "Success": - status = "success" - message = "The build succeeded on drone.io" - case "Failure": - status = "failure" - message = "The build failed on drone.io" - case "Started", "Pending": - status = "pending" - message = "The build is pending on drone.io" - default: - status = "error" - message = "The build errored on drone.io" - } - - // format the build URL - // TODO we really need the branch here - // TODO we really need the drone.io hostname as well - url := fmt.Sprintf("http://beta.drone.io/%s/%s/%s/%s", owner, name, "master", sha) - - // update the status - return client.Repos.CreateStatus(owner, name, status, url, message, sha) -} - -// SetActive will configure a post-commit and pull-request hook -// with the remote GitHub repository using the GitHub API. -// -// It will also, optionally, add a public RSA key. This is primarily -// used for private repositories, which typically must use the Git+SSH -// protocol to clone the repository. -func (c *Client) SetActive(owner, name, hook, key string) error { - // create the github client - client := github.New(c.access) - client.ApiUrl = c.config.API - - // parse the hostname from the hook, and use this - // to name the ssh key - hookurl, err := url.Parse(hook) - if err != nil { - return err - } - - // fetch the repository so that we can see if it - // is public or private. - repo, err := client.Repos.Find(owner, name) - if err != nil { - return err - } - - // if the repository is private we'll need - // to upload a github key to the repository - if repo.Private { - // name the key - keyname := "drone@" + hookurl.Host - _, err := client.RepoKeys.CreateUpdate(owner, name, key, keyname) - if err != nil { - return err - } - } - - // add the hook - if _, err := client.Hooks.CreateUpdate(owner, name, hook); err != nil { - return err - } - - return nil -} diff --git a/plugin/remote/github/github.go b/plugin/remote/github/github.go index 17b45d06ba..944bda2ccd 100644 --- a/plugin/remote/github/github.go +++ b/plugin/remote/github/github.go @@ -1,7 +1,6 @@ package github import ( - "encoding/base32" "fmt" "net/http" "net/url" @@ -9,74 +8,225 @@ import ( "strings" "time" - "github.com/drone/drone/plugin/remote" + "code.google.com/p/goauth2/oauth" "github.com/drone/drone/shared/httputil" + "github.com/drone/drone/shared/model" "github.com/drone/go-github/github" - "github.com/drone/go-github/oauth2" - "github.com/gorilla/securecookie" ) -var ( - scope = "repo,repo:status,user:email" +const ( + DefaultAPI = "https://api.github.com/" + DefaultURL = "https://github.com" + DefaultScope = "repo,repo:status,user:email" ) -type Github struct { - URL string `json:"url"` // https://github.com - API string `json:"api"` // https://api.github.com - Client string `json:"client"` - Secret string `json:"secret"` - Enabled bool `json:"enabled"` +type GitHub struct { + URL string + API string + Client string + Secret string } -// GetName returns the name of this remote system. -func (g *Github) GetName() string { - switch g.URL { - case "https://github.com": - return "github.com" - default: - return "enterprise.github.com" +func New(url, api, client, secret string) *GitHub { + var github = GitHub{ + URL: url, + API: api, + Client: client, + Secret: secret, } + // 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 } -// GetHost returns the url.Host of this remote system. -func (g *Github) GetHost() (host string) { - u, err := url.Parse(g.URL) +func NewDefault(client, secret string) *GitHub { + return New(DefaultURL, DefaultAPI, client, secret) +} + +// 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/login/%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 + return nil, err } - return u.Host + + var client = NewClient(r.API, token.AccessToken) + var useremail, errr = GetUserEmail(client) + if errr != nil { + return nil, errr + } + + var login = new(model.Login) + login.ID = int64(*useremail.ID) + login.Access = token.AccessToken + login.Login = *useremail.Login + login.Name = *useremail.Name + login.Email = *useremail.Email + return login, nil } -// GetHook parses the post-commit hook from the Request body +// 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) + var list, err = GetAllRepos(client) + if err != nil { + return nil, err + } + + var remote = r.GetKind() + var hostname = r.GetHost() + var enterprise = r.IsEnterprise() + + 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.URL, + CloneURL: *item.GitURL, + GitURL: *item.GitURL, + SSHURL: *item.SSHURL, + Role: &model.Perm{}, + } + + if enterprise || 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) + 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) + 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 (g *Github) GetHook(r *http.Request) (*remote.Hook, error) { +func (r *GitHub) ParseHook(req *http.Request) (*model.Hook, error) { // handle github ping - if r.Header.Get("X-Github-Event") == "ping" { + if req.Header.Get("X-Github-Event") == "ping" { return nil, nil } // handle github pull request hook differently - if r.Header.Get("X-Github-Event") == "pull_request" { - return g.GetPullRequestHook(r) + if req.Header.Get("X-Github-Event") == "pull_request" { + return r.ParsePullRequestHook(req) } // get the payload of the message - payload := r.FormValue("payload") + var payload = req.FormValue("payload") // parse the github Hook payload - data, err := github.ParseHook([]byte(payload)) + var data, err = github.ParseHook([]byte(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() { + if data.IsTag() || + data.IsGithubPages() || + data.IsHead() == false || + data.IsDeleted() { return nil, nil } - hook := remote.Hook{} + var hook = new(model.Hook) hook.Repo = data.Repo.Name hook.Owner = data.Repo.Owner.Login hook.Sha = data.Head.Id @@ -99,15 +249,17 @@ func (g *Github) GetHook(r *http.Request) (*remote.Hook, error) { hook.Author = data.Commits[0].Author.Email } - return &hook, nil + return hook, nil } -func (g *Github) GetPullRequestHook(r *http.Request) (*remote.Hook, error) { - payload := r.FormValue("payload") +// 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) { + var payload = req.FormValue("payload") // parse the payload to retrieve the pull-request // hook meta-data. - data, err := github.ParsePullRequestHook([]byte(payload)) + var data, err = github.ParsePullRequestHook([]byte(payload)) if err != nil { return nil, err } @@ -119,7 +271,7 @@ func (g *Github) GetPullRequestHook(r *http.Request) (*remote.Hook, error) { // TODO we should also store the pull request branch (ie from x to y) // we can find it here: data.PullRequest.Head.Ref - hook := remote.Hook{ + var hook = model.Hook{ Owner: data.Repo.Owner.Login, Repo: data.Repo.Name, Sha: data.PullRequest.Head.Sha, @@ -137,78 +289,3 @@ func (g *Github) GetPullRequestHook(r *http.Request) (*remote.Hook, error) { return &hook, nil } - -// GetLogin handles authentication to third party, remote services -// and returns the required user data in a standard format. -func (g *Github) GetLogin(w http.ResponseWriter, r *http.Request) (*remote.Login, error) { - // create the oauth2 client - oauth := oauth2.Client{ - RedirectURL: fmt.Sprintf("%s://%s/login/%s", httputil.GetScheme(r), httputil.GetHost(r), g.GetName()), - AccessTokenURL: fmt.Sprintf("%s/login/oauth/access_token", g.URL), - AuthorizationURL: fmt.Sprintf("%s/login/oauth/authorize", g.URL), - ClientId: g.Client, - ClientSecret: g.Secret, - } - - // get the OAuth code - code := r.FormValue("code") - state := r.FormValue("state") - if len(code) == 0 { - var random = base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)) - httputil.SetCookie(w, r, "github_state", string(random)) - - // redirect the user to login - redirect := oauth.AuthorizeRedirect(scope, random) - http.Redirect(w, r, redirect, http.StatusSeeOther) - return nil, nil - } - - cookieState := httputil.GetCookie(r, "github_state") - httputil.DelCookie(w, r, "github_state") - if cookieState != state { - return nil, fmt.Errorf("Error matching state in OAuth2 redirect") - } - - // exchange code for an auth token - token, err := oauth.GrantToken(code) - if err != nil { - return nil, fmt.Errorf("Error granting GitHub authorization token. %s", err) - } - - // create the client - client := github.New(token.AccessToken) - client.ApiUrl = g.API - - // get the user information - user, err := client.Users.Current() - if err != nil { - return nil, fmt.Errorf("Error retrieving currently authenticated GitHub user. %s", err) - } - - // put the user data in the common format - login := remote.Login{ - ID: user.ID, - Login: user.Login, - Access: token.AccessToken, - Name: user.Name, - } - - // get the users primary email address - email, err := client.Emails.FindPrimary() - if err == nil { - login.Email = email.Email - } - - return &login, nil -} - -// GetClient returns a new Github remote client. -func (g *Github) GetClient(access, secret string) remote.Client { - return &Client{g, access} -} - -// IsMatch returns true if the hostname matches the -// hostname of this remote client. -func (g *Github) IsMatch(hostname string) bool { - return strings.HasSuffix(hostname, g.URL) -} diff --git a/plugin/remote/github/github_test.go b/plugin/remote/github/github_test.go new file mode 100644 index 0000000000..071951b97f --- /dev/null +++ b/plugin/remote/github/github_test.go @@ -0,0 +1,94 @@ +package github + +import ( + "testing" + + "github.com/drone/drone/plugin/remote/github/testdata" + "github.com/drone/drone/shared/model" + "github.com/franela/goblin" +) + +func Test_Github(t *testing.T) { + // setup a dummy github server + var server = testdata.NewServer() + defer server.Close() + + var github = GitHub{ + URL: server.URL, + API: server.URL, + } + var user = model.User{ + Access: "e3b0c44298fc1c149afbf4c8996fb", + } + var repo = model.Repo{ + Owner: "octocat", + Name: "Hello-World", + } + var hook = model.Hook{ + Sha: "6dcb09b5b57875f334f61aebed695e2e4193db5e", + } + + g := goblin.Goblin(t) + g.Describe("GitHub Plugin", func() { + + g.It("Should identify github vs github enterprise", func() { + var ghc = &GitHub{URL: "https://github.com"} + var ghe = &GitHub{URL: "https://github.drone.io"} + g.Assert(ghc.IsEnterprise()).IsFalse() + g.Assert(ghe.IsEnterprise()).IsTrue() + g.Assert(ghc.GetKind()).Equal(model.RemoteGithub) + g.Assert(ghe.GetKind()).Equal(model.RemoteGithubEnterprise) + }) + + g.It("Should parse the hostname", func() { + var ghc = &GitHub{URL: "https://github.com"} + var ghe = &GitHub{URL: "https://github.drone.io:80"} + g.Assert(ghc.GetHost()).Equal("github.com") + g.Assert(ghe.GetHost()).Equal("github.drone.io:80") + }) + + g.It("Should get the repo list", func() { + var repos, err = github.GetRepos(&user) + g.Assert(err == nil).IsTrue() + g.Assert(len(repos)).Equal(4) + g.Assert(repos[0].Name).Equal("Hello-World") + g.Assert(repos[0].Owner).Equal("octocat") + g.Assert(repos[0].Host).Equal(github.GetHost()) + g.Assert(repos[0].Remote).Equal(github.GetKind()) + g.Assert(repos[0].Private).Equal(true) + g.Assert(repos[0].CloneURL).Equal("git@github.com:octocat/Hello-World.git") + g.Assert(repos[0].SSHURL).Equal("git@github.com:octocat/Hello-World.git") + g.Assert(repos[0].GitURL).Equal("git://github.com/octocat/Hello-World.git") + g.Assert(repos[0].Role.Admin).Equal(true) + g.Assert(repos[0].Role.Read).Equal(true) + g.Assert(repos[0].Role.Write).Equal(true) + }) + + g.It("Should get the build script", func() { + var script, err = github.GetScript(&user, &repo, &hook) + g.Assert(err == nil).IsTrue() + g.Assert(string(script)).Equal("image: go") + }) + + g.It("Should activate a public repo", func() { + repo.Private = false + repo.CloneURL = "git://github.com/octocat/Hello-World.git" + repo.SSHURL = "git@github.com:octocat/Hello-World.git" + var err = github.Activate(&user, &repo, "http://example.com") + g.Assert(err == nil).IsTrue() + }) + + g.It("Should activate a private repo", func() { + repo.Name = "Hola-Mundo" + repo.Private = true + repo.CloneURL = "git@github.com:octocat/Hola-Mundo.git" + repo.SSHURL = "git@github.com:octocat/Hola-Mundo.git" + var err = github.Activate(&user, &repo, "http://example.com") + g.Assert(err == nil).IsTrue() + }) + + g.It("Should parse a commit hook") + + g.It("Should parse a pull request hook") + }) +} diff --git a/plugin/remote/github/helper.go b/plugin/remote/github/helper.go new file mode 100644 index 0000000000..6cd21d8175 --- /dev/null +++ b/plugin/remote/github/helper.go @@ -0,0 +1,238 @@ +package github + +import ( + "encoding/base32" + "fmt" + "net/url" + + "code.google.com/p/goauth2/oauth" + "github.com/google/go-github/github" + "github.com/gorilla/securecookie" +) + +// NewClient is a helper function that returns a new GitHub +// client using the provided OAuth token. +func NewClient(uri, token string) *github.Client { + t := &oauth.Transport{ + Token: &oauth.Token{AccessToken: token}, + } + c := github.NewClient(t.Client()) + c.BaseURL, _ = url.Parse(uri) + return c +} + +// GetUserEmail is a heper function that retrieves the currently +// authenticated user from GitHub + Email address. +func GetUserEmail(client *github.Client) (*github.User, error) { + user, _, err := client.Users.Get("") + if err != nil { + return nil, err + } + + emails, _, err := client.Users.ListEmails(nil) + if err != nil { + return nil, err + } + for _, email := range emails { + if *email.Primary && *email.Verified { + user.Email = email.Email + return user, nil + } + } + return nil, fmt.Errorf("No verified Email address for GitHub account") +} + +// GetAllRepos is a helper function that returns an aggregated list +// of all user and organization repositories. +func GetAllRepos(client *github.Client) ([]github.Repository, error) { + orgs, err := GetOrgs(client) + if err != nil { + return nil, err + } + + repos, err := GetUserRepos(client) + if err != nil { + return nil, err + } + + for _, org := range orgs { + list, err := GetOrgRepos(client, *org.Login) + if err != nil { + return nil, err + } + repos = append(repos, list...) + } + + return repos, nil +} + +// GetUserRepos is a helper function that returns a list of +// all user repositories. Paginated results are aggregated into +// a single list. +func GetUserRepos(client *github.Client) ([]github.Repository, error) { + var repos []github.Repository + var opts = github.RepositoryListOptions{} + opts.PerPage = 100 + opts.Page = 1 + + // loop through user repository list + for opts.Page > 0 { + list, resp, err := client.Repositories.List("", &opts) + if err != nil { + return nil, err + } + repos = append(repos, list...) + + // increment the next page to retrieve + opts.Page = resp.NextPage + } + + return repos, nil +} + +// GetOrgRepos is a helper function that returns a list of +// all org repositories. Paginated results are aggregated into +// a single list. +func GetOrgRepos(client *github.Client, org string) ([]github.Repository, error) { + var repos []github.Repository + var opts = github.RepositoryListByOrgOptions{} + opts.PerPage = 100 + opts.Page = 1 + + // loop through user repository list + for opts.Page > 0 { + list, resp, err := client.Repositories.ListByOrg(org, &opts) + if err != nil { + return nil, err + } + repos = append(repos, list...) + + // increment the next page to retrieve + opts.Page = resp.NextPage + } + + return repos, nil +} + +// GetOrgs is a helper function that returns a list of +// all org repositories. +func GetOrgs(client *github.Client) ([]github.Organization, error) { + orgs, _, err := client.Organizations.List("", nil) + return orgs, err +} + +// GetHook is a heper function that retrieves a hook by +// hostname. To do this, it will retrieve a list of all hooks +// and iterate through the list. +func GetHook(client *github.Client, owner, name, url string) (*github.Hook, error) { + hooks, _, err := client.Repositories.ListHooks(owner, name, nil) + if err != nil { + return nil, err + } + for _, hook := range hooks { + if hook.Config["url"] == url { + return &hook, nil + } + } + return nil, nil +} + +// CreateHook is a heper function that creates a post-commit hook +// for the specified repository. +func CreateHook(client *github.Client, owner, name, url string) (*github.Hook, error) { + var hook = new(github.Hook) + hook.Name = github.String("web") + hook.Events = []string{"push", "pull_request"} + hook.Config = map[string]interface{}{} + hook.Config["url"] = url + hook.Config["content_type"] = "json" + created, _, err := client.Repositories.CreateHook(owner, name, hook) + return created, err +} + +// CreateUpdateHook is a heper function that creates a post-commit hook +// for the specified repository if it does not already exist, otherwise +// it updates the existing hook +func CreateUpdateHook(client *github.Client, owner, name, url string) (*github.Hook, error) { + var hook, _ = GetHook(client, owner, name, url) + if hook != nil { + hook.Name = github.String("web") + hook.Events = []string{"push", "pull_request"} + hook.Config = map[string]interface{}{} + hook.Config["url"] = url + hook.Config["content_type"] = "json" + var updated, _, err = client.Repositories.EditHook(owner, name, *hook.ID, hook) + return updated, err + } + + return CreateHook(client, owner, name, url) +} + +// GetKey is a heper function that retrieves a public Key by +// title. To do this, it will retrieve a list of all keys +// and iterate through the list. +func GetKey(client *github.Client, owner, name, title string) (*github.Key, error) { + keys, _, err := client.Repositories.ListKeys(owner, name, nil) + if err != nil { + return nil, err + } + for _, key := range keys { + if *key.Title == title { + return &key, nil + } + } + return nil, nil +} + +// GetKeyTitle is a helper function that generates a title for the +// RSA public key based on the username and domain name. +func GetKeyTitle(rawurl string) (string, error) { + var uri, err = url.Parse(rawurl) + if err != nil { + return "", err + } + return fmt.Sprintf("drone@%s", uri.Host), nil +} + +// CreateKey is a heper function that creates a deploy key +// for the specified repository. +func CreateKey(client *github.Client, owner, name, title, key string) (*github.Key, error) { + var k = new(github.Key) + k.Title = github.String(title) + k.Key = github.String(key) + created, _, err := client.Repositories.CreateKey(owner, name, k) + return created, err +} + +// CreateUpdateKey is a heper function that creates a deployment key +// for the specified repository if it does not already exist, otherwise +// it updates the existing key +func CreateUpdateKey(client *github.Client, owner, name, title, key string) (*github.Key, error) { + var k, _ = GetKey(client, owner, name, title) + if k != nil { + k.Title = github.String(title) + k.Key = github.String(key) + var updated, _, err = client.Repositories.EditKey(owner, name, *k.ID, k) + return updated, err + } + + return CreateKey(client, owner, name, title, key) +} + +// GetFile is a heper function that retrieves a file from +// GitHub and returns its contents in byte array format. +func GetFile(client *github.Client, owner, name, path, ref string) ([]byte, error) { + var opts = new(github.RepositoryContentGetOptions) + opts.Ref = ref + content, _, _, err := client.Repositories.GetContents(owner, name, path, opts) + if err != nil { + return nil, err + } + return content.Decode() +} + +// GetRandom is a helper function that generates a 32-bit random +// key, base32 encoded as a string value. +func GetRandom() string { + return base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)) +} diff --git a/plugin/remote/github/helper_test.go b/plugin/remote/github/helper_test.go new file mode 100644 index 0000000000..8bb0608133 --- /dev/null +++ b/plugin/remote/github/helper_test.go @@ -0,0 +1,34 @@ +package github + +import ( + "testing" + + "github.com/drone/drone/plugin/remote/github/testdata" + "github.com/franela/goblin" +) + +func Test_Helper(t *testing.T) { + // setup a dummy github server + var server = testdata.NewServer() + defer server.Close() + + 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 User Repos") + g.It("Should Get a list of Org Repos") + g.It("Should Get a list of All Repos") + g.It("Should Get a Repo Key") + g.It("Should Get a Repo Hook") + g.It("Should Create a Repo Key") + g.It("Should Create a Repo Hook") + g.It("Should Create or Update a Repo Key") + g.It("Should Create or Update a Repo Hook") + g.It("Should Get a Repo File") + + }) +} diff --git a/plugin/remote/github/init.go b/plugin/remote/github/init.go deleted file mode 100644 index 35e3a0b777..0000000000 --- a/plugin/remote/github/init.go +++ /dev/null @@ -1,21 +0,0 @@ -package github - -import ( - "github.com/drone/drone/plugin/remote" - "github.com/drone/drone/shared/model" -) - -func init() { - remote.Register(model.RemoteGithub, plugin) - remote.Register(model.RemoteGithubEnterprise, plugin) -} - -func plugin(remote *model.Remote) remote.Remote { - return &Github{ - URL: remote.URL, - API: remote.API, - Client: remote.Client, - Secret: remote.Secret, - Enabled: remote.Open, - } -} diff --git a/plugin/remote/github/register.go b/plugin/remote/github/register.go new file mode 100644 index 0000000000..bce7df93ff --- /dev/null +++ b/plugin/remote/github/register.go @@ -0,0 +1,41 @@ +package github + +import ( + "os" + + "github.com/drone/drone/plugin/remote" +) + +func init() { + init_github() + init_github_enterprise() +} + +// registers the GitHub (github.com) plugin +func init_github() { + var cli = os.Getenv("GITHUB_CLIENT") + var sec = os.Getenv("GITHUB_SECRET") + if len(cli) == 0 || + len(sec) == 0 { + return + } + var github = NewDefault(cli, sec) + remote.Register(github) +} + +// registers the GitHub Enterprise plugin +func init_github_enterprise() { + var url = os.Getenv("GITHUB_ENTERPRISE_URL") + var api = os.Getenv("GITHUB_ENTERPRISE_API") + var cli = os.Getenv("GITHUB_ENTERPRISE_CLIENT") + var sec = os.Getenv("GITHUB_ENTERPRISE_SECRET") + + if len(url) == 0 || + len(api) == 0 || + len(cli) == 0 || + len(sec) == 0 { + return + } + var github = New(url, api, cli, sec) + remote.Register(github) +} diff --git a/plugin/remote/github/testdata/testdata.go b/plugin/remote/github/testdata/testdata.go new file mode 100644 index 0000000000..6845aa0b57 --- /dev/null +++ b/plugin/remote/github/testdata/testdata.go @@ -0,0 +1,148 @@ +package testdata + +import ( + "net/http" + "net/http/httptest" +) + +// setup a mock server for testing purposes. +func NewServer() *httptest.Server { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + // handle requests and serve mock data + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + + // evaluate the path to serve a dummy data file + switch r.URL.Path { + case "/user/repos": + w.Write(userReposPayload) + return + case "/user/orgs": + w.Write(userOrgsPayload) + return + case "/orgs/github/repos": + w.Write(userReposPayload) + return + case "/repos/octocat/Hello-World/contents/.drone.yml": + w.Write(droneYamlPayload) + return + case "/repos/octocat/Hello-World/hooks": + switch r.Method { + case "POST": + w.Write(createHookPayload) + return + } + case "/repos/octocat/Hola-Mundo/hooks": + switch r.Method { + case "POST": + w.Write(createHookPayload) + return + } + case "/repos/octocat/Hola-Mundo/keys": + switch r.Method { + case "POST": + w.Write(createKeyPayload) + return + } + } + + // else return a 404 + http.NotFound(w, r) + }) + + // return the server to the client which + // will need to know the base URL path + return server +} + +// sample repository list +var userReposPayload = []byte(` +[ + { + "owner": { + "login": "octocat", + "id": 1 + }, + "id": 1296269, + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "private": true, + "url": "https://api.github.com/repos/octocat/Hello-World", + "html_url": "https://github.com/octocat/Hello-World", + "clone_url": "https://github.com/octocat/Hello-World.git", + "git_url": "git://github.com/octocat/Hello-World.git", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "permissions": { + "admin": true, + "push": true, + "pull": true + } + }, + { + "owner": { + "login": "octocat", + "id": 1 + }, + "id": 9626921, + "name": "Hola-Mundo", + "full_name": "octocat/Hola-Mundo", + "private": false, + "url": "https://api.github.com/repos/octocat/Hola-Mundo", + "html_url": "https://github.com/octocat/Hola-Mundo", + "clone_url": "https://github.com/octocat/Hola-Mundo.git", + "git_url": "git://github.com/octocat/Hola-Mundo.git", + "ssh_url": "git@github.com:octocat/Hola-Mundo.git", + "permissions": { + "admin": false, + "push": false, + "pull": true + } + } +] +`) + +var emptySetPayload = []byte(`[]`) +var emptyObjPayload = []byte(`{}`) + +// sample org list response +var userOrgsPayload = []byte(` +[ + { "login": "github", "id": 1 } +] +`) + +// sample content response for .drone.yml request +var droneYamlPayload = []byte(` +{ + "type": "file", + "encoding": "base64", + "name": ".drone.yml", + "path": ".drone.yml", + "content": "aW1hZ2U6IGdv" +} +`) + +// sample create hook response +var createHookPayload = []byte(` +{ + "id": 1, + "name": "web", + "events": [ "push", "pull_request" ], + "active": true, + "config": { + "url": "http://example.com", + "content_type": "json" + } +} +`) + +// sample create hook response +var createKeyPayload = []byte(` +{ + "id": 1, + "key": "ssh-rsa AAA...", + "url": "https://api.github.com/user/keys/1", + "title": "octocat@octomac" +} +`) diff --git a/plugin/remote/gitlab/client.go b/plugin/remote/gitlab/client.go deleted file mode 100644 index 1795cf33da..0000000000 --- a/plugin/remote/gitlab/client.go +++ /dev/null @@ -1,37 +0,0 @@ -package gitlab - -import ( - "github.com/drone/drone/plugin/remote" -) - -type Client struct { - config *Gitlab - access string // user access token -} - -// GetUser fetches the user by ID (login name). -func (c *Client) GetUser(login string) (*remote.User, error) { - return nil, nil -} - -// GetRepos fetches all repositories that the specified -// user has access to in the remote system. -func (c *Client) GetRepos(owner string) ([]*remote.Repo, error) { - return nil, nil -} - -// GetScript fetches the build script (.drone.yml) from the remote -// repository and returns in string format. -func (c *Client) GetScript(*remote.Hook) (string, error) { - return "", nil -} - -// SetStatus -func (c *Client) SetStatus(owner, repo, sha, status string) error { - return nil -} - -// SetActive -func (c *Client) SetActive(owner, repo, hook, key string) error { - return nil -} diff --git a/plugin/remote/gitlab/gitlab.go b/plugin/remote/gitlab/gitlab.go index b7a5336825..120735e159 100644 --- a/plugin/remote/gitlab/gitlab.go +++ b/plugin/remote/gitlab/gitlab.go @@ -1,51 +1,174 @@ package gitlab import ( + "io/ioutil" "net/http" "net/url" - "strings" - "github.com/drone/drone/plugin/remote" + "github.com/Bugagazavr/go-gitlab-client" + "github.com/drone/drone/shared/model" ) type Gitlab struct { - URL string `json:"url"` // https://github.com - Enabled bool `json:"enabled"` + url string } -// GetName returns the name of this remote system. -func (g *Gitlab) GetName() string { - return "gitlab.com" +func New(url string) *Gitlab { + return &Gitlab{url: url} } -// GetHost returns the url.Host of this remote system. -func (g *Gitlab) GetHost() (host string) { - u, err := url.Parse(g.URL) +// Authorize handles authentication with thrid party remote systems, +// such as github or bitbucket, and returns user data. +func (r *Gitlab) Authorize(res http.ResponseWriter, req *http.Request) (*model.Login, error) { + var username = req.FormValue("username") + var password = req.FormValue("password") + + var client = NewClient(r.url, "") + var session, err = client.GetSession(username, password) if err != nil { - return + return nil, err } - return u.Host + + var login = new(model.Login) + login.ID = int64(session.Id) + login.Access = session.PrivateToken + login.Login = session.UserName + login.Name = session.Name + login.Email = session.Email + return login, nil } -// GetHook parses the post-commit hook from the Request body +// GetKind returns the identifier of this remote GitHub instane. +func (r *Gitlab) GetKind() string { + return model.RemoteGitlab +} + +// GetHost returns the hostname of this remote GitHub instance. +func (r *Gitlab) GetHost() string { + uri, _ := url.Parse(r.url) + return uri.Host +} + +// GetRepos fetches all repositories that the specified +// user has access to in the remote system. +func (r *Gitlab) GetRepos(user *model.User) ([]*model.Repo, error) { + + var repos []*model.Repo + var client = NewClient(r.url, user.Access) + var list, err = client.AllProjects() + 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.Namespace.Path, + Name: item.Path, + Private: !item.Public, + CloneURL: item.HttpRepoUrl, + GitURL: item.HttpRepoUrl, + SSHURL: item.SshRepoUrl, + Role: &model.Perm{}, + } + + if repo.Private { + repo.CloneURL = repo.SSHURL + } + + // if no permissions we should skip the repository + // entirely, since this should never happen + if item.Permissions == nil { + continue + } + + repo.Role.Admin = IsAdmin(item) + repo.Role.Write = IsWrite(item) + repo.Role.Read = IsRead(item) + 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 *Gitlab) GetScript(user *model.User, repo *model.Repo, hook *model.Hook) ([]byte, error) { + var client = NewClient(r.url, user.Access) + var path = ns(repo.Owner, repo.Name) + return client.RepoRawFile(path, hook.Sha, ".drone.yml") +} + +// Activate activates a repository by adding a Post-commit hook and +// a Public Deploy key, if applicable. +func (r *Gitlab) Activate(user *model.User, repo *model.Repo, link string) error { + var client = NewClient(r.url, user.Access) + var path = ns(repo.Owner, repo.Name) + var title, err = GetKeyTitle(link) + if err != nil { + return err + } + + // if the repository is private we'll need + // to upload a github key to the repository + if repo.Private { + var err = client.AddProjectDeployKey(path, title, repo.PublicKey) + if err != nil { + return err + } + } + + // append the repo owner / name to the hook url since gitlab + // doesn't send this detail in the post-commit hook + link += "?owner=" + repo.Owner + "&name=" + repo.Name + + // add the hook + return client.AddProjectHook(path, link, true, false, true) +} + +// ParseHook parses the post-commit hook from the Request body // and returns the required data in a standard format. -func (g *Gitlab) GetHook(*http.Request) (*remote.Hook, error) { - return nil, nil -} +func (r *Gitlab) ParseHook(req *http.Request) (*model.Hook, error) { -// GetLogin handles authentication to third party, remote services -// and returns the required user data in a standard format. -func (g *Gitlab) GetLogin(http.ResponseWriter, *http.Request) (*remote.Login, error) { - return nil, nil -} + defer req.Body.Close() + var payload, _ = ioutil.ReadAll(req.Body) + var parsed, err = gogitlab.ParseHook(payload) + if err != nil { + return nil, err + } -// GetClient returns a new Gitlab remote client. -func (g *Gitlab) GetClient(access, secret string) remote.Client { - return &Client{g, access} -} + if parsed.ObjectKind == "merge_request" { + // TODO (bradrydzewski) figure out how to handle merge requests + return nil, nil + } -// IsMatch returns true if the hostname matches the -// hostname of this remote client. -func (g *Gitlab) IsMatch(hostname string) bool { - return strings.HasSuffix(hostname, g.URL) + if len(parsed.After) == 0 { + return nil, nil + } + + var hook = new(model.Hook) + hook.Owner = req.FormValue("owner") + hook.Repo = req.FormValue("name") + hook.Sha = parsed.After + hook.Branch = parsed.Branch() + + var head = parsed.Head() + hook.Message = head.Message + hook.Timestamp = head.Timestamp + + // extracts the commit author (ideally email) + // from the post-commit hook + switch { + case head.Author != nil: + hook.Author = head.Author.Email + case head.Author == nil: + hook.Author = parsed.UserName + } + + return hook, nil } diff --git a/plugin/remote/gitlab/gitlab_test.go b/plugin/remote/gitlab/gitlab_test.go new file mode 100644 index 0000000000..ca7e2ba0f8 --- /dev/null +++ b/plugin/remote/gitlab/gitlab_test.go @@ -0,0 +1,86 @@ +package gitlab + +import ( + "net/http" + "testing" + + "github.com/drone/drone/plugin/remote/gitlab/testdata" + "github.com/drone/drone/shared/model" + "github.com/franela/goblin" +) + +func Test_Github(t *testing.T) { + // setup a dummy github server + var server = testdata.NewServer() + defer server.Close() + + var gitlab = New(server.URL) + var user = model.User{ + Access: "e3b0c44298fc1c149afbf4c8996fb", + } + /* + var repo = model.Repo{ + Owner: "gitlab", + Name: "Hello-World", + } + var commit = model.Commit{ + Sha: "6dcb09b5b57875f334f61aebed695e2e4193db5e", + } + */ + + g := goblin.Goblin(t) + g.Describe("Gitlab Plugin", func() { + + g.It("Should authorize user", func() { + var req, _ = http.NewRequest("GET", "/login/gitlab", nil) + var login, err = gitlab.Authorize(nil, req) + g.Assert(err == nil).IsTrue() + g.Assert(login.Email).Equal("john@example.com") + g.Assert(login.Name).Equal("John Smith") + g.Assert(login.Login).Equal("john_smith") + g.Assert(login.Access).Equal("dd34asd13as") + g.Assert(login.ID).Equal(int64(1)) + }) + + g.It("Should get the repo list", func() { + var repos, err = gitlab.GetRepos(&user) + g.Assert(err == nil).IsTrue() + g.Assert(len(repos)).Equal(2) + g.Assert(repos[0].Name).Equal("diaspora-client") + g.Assert(repos[0].Owner).Equal("diaspora") + g.Assert(repos[0].Host).Equal(gitlab.GetHost()) + g.Assert(repos[0].Remote).Equal(gitlab.GetKind()) + g.Assert(repos[0].Private).Equal(true) + g.Assert(repos[0].Role.Admin).Equal(true) + g.Assert(repos[0].Role.Read).Equal(true) + g.Assert(repos[0].Role.Write).Equal(true) + }) + /* + g.It("Should get the build script", func() { + var script, err = github.GetScript(&user, &repo, &commit) + g.Assert(err == nil).IsTrue() + g.Assert(string(script)).Equal("image: go") + }) + + g.It("Should activate a public repo", func() { + repo.Private = false + repo.CloneURL = "git://github.com/octocat/Hello-World.git" + repo.SSHURL = "git@github.com:octocat/Hello-World.git" + var err = github.Activate(&user, &repo, "http://example.com") + g.Assert(err == nil).IsTrue() + }) + + g.It("Should activate a private repo", func() { + repo.Name = "Hola-Mundo" + repo.Private = true + repo.CloneURL = "git@github.com:octocat/Hola-Mundo.git" + repo.SSHURL = "git@github.com:octocat/Hola-Mundo.git" + var err = github.Activate(&user, &repo, "http://example.com") + g.Assert(err == nil).IsTrue() + }) + */ + g.It("Should parse a commit hook") + + g.It("Should ignore a pull request hook") + }) +} diff --git a/plugin/remote/gitlab/helper.go b/plugin/remote/gitlab/helper.go new file mode 100644 index 0000000000..ea2fb25f16 --- /dev/null +++ b/plugin/remote/gitlab/helper.go @@ -0,0 +1,78 @@ +package gitlab + +import ( + "fmt" + "net/url" + + "github.com/Bugagazavr/go-gitlab-client" +) + +// NewClient is a helper function that returns a new GitHub +// client using the provided OAuth token. +func NewClient(uri, token string) *gogitlab.Gitlab { + return gogitlab.NewGitlab(uri, "/api/v3", token) +} + +// IsRead is a helper function that returns true if the +// user has Read-only access to the repository. +func IsRead(proj *gogitlab.Project) bool { + var user = proj.Permissions.ProjectAccess + var group = proj.Permissions.GroupAccess + + switch { + case proj.Public: + return true + case user != nil && user.AccessLevel >= 20: + return true + case group != nil && group.AccessLevel >= 20: + return true + default: + return false + } +} + +// IsWrite is a helper function that returns true if the +// user has Read-Write access to the repository. +func IsWrite(proj *gogitlab.Project) bool { + var user = proj.Permissions.ProjectAccess + var group = proj.Permissions.GroupAccess + + switch { + case user != nil && user.AccessLevel >= 30: + return true + case group != nil && group.AccessLevel >= 30: + return true + default: + return false + } +} + +// IsAdmin is a helper function that returns true if the +// user has Admin access to the repository. +func IsAdmin(proj *gogitlab.Project) bool { + var user = proj.Permissions.ProjectAccess + var group = proj.Permissions.GroupAccess + + switch { + case user != nil && user.AccessLevel >= 40: + return true + case group != nil && group.AccessLevel >= 40: + return true + default: + return false + } +} + +// GetKeyTitle is a helper function that generates a title for the +// RSA public key based on the username and domain name. +func GetKeyTitle(rawurl string) (string, error) { + var uri, err = url.Parse(rawurl) + if err != nil { + return "", err + } + return fmt.Sprintf("drone@%s", uri.Host), nil +} + +func ns(owner, name string) string { + return fmt.Sprintf("%s%%2F%s", owner, name) +} diff --git a/plugin/remote/gitlab/helper_test.go b/plugin/remote/gitlab/helper_test.go new file mode 100644 index 0000000000..4d4b35afe5 --- /dev/null +++ b/plugin/remote/gitlab/helper_test.go @@ -0,0 +1 @@ +package gitlab diff --git a/plugin/remote/gitlab/init.go b/plugin/remote/gitlab/init.go deleted file mode 100644 index 336991ee84..0000000000 --- a/plugin/remote/gitlab/init.go +++ /dev/null @@ -1,17 +0,0 @@ -package gitlab - -import ( - "github.com/drone/drone/plugin/remote" - "github.com/drone/drone/shared/model" -) - -func init() { - remote.Register(model.RemoteGitlab, plugin) -} - -func plugin(remote *model.Remote) remote.Remote { - return &Gitlab{ - URL: remote.URL, - Enabled: remote.Open, - } -} diff --git a/plugin/remote/gitlab/register.go b/plugin/remote/gitlab/register.go new file mode 100644 index 0000000000..4e2e36a847 --- /dev/null +++ b/plugin/remote/gitlab/register.go @@ -0,0 +1,16 @@ +package gitlab + +import ( + "os" + + "github.com/drone/drone/plugin/remote" +) + +// registers the Gitlab plugin +func init() { + var url = os.Getenv("GITLAB_URL") + if len(url) == 0 { + return + } + remote.Register(New(url)) +} diff --git a/plugin/remote/gitlab/testdata/testdata.go b/plugin/remote/gitlab/testdata/testdata.go new file mode 100644 index 0000000000..01d58b0aac --- /dev/null +++ b/plugin/remote/gitlab/testdata/testdata.go @@ -0,0 +1,151 @@ +package testdata + +import ( + "net/http" + "net/http/httptest" +) + +// setup a mock server for testing purposes. +func NewServer() *httptest.Server { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + // handle requests and serve mock data + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + //println(r.URL.Path + " " + r.Method) + // evaluate the path to serve a dummy data file + switch r.URL.Path { + case "/api/v3/projects": + w.Write(projectsPayload) + return + case "/api/v3/session": + w.Write(sessionPayload) + return + } + + // else return a 404 + http.NotFound(w, r) + }) + + // return the server to the client which + // will need to know the base URL path + return server +} + +// sample repository list +var projectsPayload = []byte(` +[ + { + "id": 4, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", + "web_url": "http://example.com/diaspora/diaspora-client", + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Client", + "name_with_namespace": "Diaspora / Diaspora Client", + "path": "diaspora-client", + "path_with_namespace": "diaspora/diaspora-client", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "archived": false, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } + }, + { + "id": 6, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:brightbox/puppet.git", + "http_url_to_repo": "http://example.com/brightbox/puppet.git", + "web_url": "http://example.com/brightbox/puppet", + "owner": { + "id": 4, + "name": "Brightbox", + "created_at": "2013-09-30T13:46:02Z" + }, + "name": "Puppet", + "name_with_namespace": "Brightbox / Puppet", + "path": "puppet", + "path_with_namespace": "brightbox/puppet", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", + "namespace": { + "created_at": "2013-09-30T13:46:02Z", + "description": "", + "id": 4, + "name": "Brightbox", + "owner_id": 1, + "path": "brightbox", + "updated_at": "2013-09-30T13:46:02Z" + }, + "archived": false, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } + } +] +`) + +// sample org list response +var sessionPayload = []byte(` +{ + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "private_token": "dd34asd13as" +} +`) + +// sample content response for .drone.yml request +var droneYamlPayload = []byte(` +{ + "type": "file", + "encoding": "base64", + "name": ".drone.yml", + "path": ".drone.yml", + "content": "aW1hZ2U6IGdv" +} +`) diff --git a/plugin/remote/remote.go b/plugin/remote/remote.go index 041ae1fe13..ad6c3f162d 100644 --- a/plugin/remote/remote.go +++ b/plugin/remote/remote.go @@ -6,113 +6,58 @@ import ( "github.com/drone/drone/shared/model" ) -// Defines a model for integrating (or pluggin in) remote version -// control systems, such as GitHub and Bitbucket. -type Plugin func(*model.Remote) Remote - -var plugins = map[string]Plugin{} - -// Register registers a new plugin. -func Register(name string, plugin Plugin) { - plugins[name] = plugin -} - -// Lookup retrieves the plugin for the remote. -func Lookup(name string) (Plugin, bool) { - plugin, ok := plugins[name] - return plugin, ok -} - type Remote interface { - // GetName returns the name of this remote system. - GetName() string + // Authorize handles authentication with thrid party remote systems, + // such as github or bitbucket, and returns user data. + Authorize(w http.ResponseWriter, r *http.Request) (*model.Login, error) - // GetHost returns the URL hostname of this remote system. - GetHost() (host string) + // GetKind returns the kind of plugin + GetKind() string - // GetHook parses the post-commit hook from the Request body - // and returns the required data in a standard format. - GetHook(*http.Request) (*Hook, error) - - // GetLogin handles authentication to third party, remote services - // and returns the required user data in a standard format. - GetLogin(http.ResponseWriter, *http.Request) (*Login, error) - - // NewClient returns a new Bitbucket remote client. - GetClient(access, secret string) Client - - // Match returns true if the hostname matches the - // hostname of this remote client. - IsMatch(hostname string) bool -} - -type Client interface { - // GetUser fetches the user by ID (login name). - GetUser(login string) (*User, error) + // GetHost returns the hostname of the remote service. + GetHost() string // GetRepos fetches all repositories that the specified // user has access to in the remote system. - GetRepos(owner string) ([]*Repo, error) + GetRepos(user *model.User) ([]*model.Repo, error) // GetScript fetches the build script (.drone.yml) from the remote // repository and returns in string format. - GetScript(*Hook) (string, error) + GetScript(user *model.User, repo *model.Repo, hook *model.Hook) ([]byte, error) - // SetStatus - SetStatus(owner, repo, sha, status string) error + // Activate activates a repository by creating the post-commit hook and + // adding the SSH deploy key, if applicable. + Activate(user *model.User, repo *model.Repo, link string) error - // SetActive - SetActive(owner, repo, hook, key string) error + // ParseHook parses the post-commit hook from the Request body + // and returns the required data in a standard format. + ParseHook(r *http.Request) (*model.Hook, error) } -// Hook represents a subset of commit meta-data provided -// by post-commit and pull request hooks. -type Hook struct { - Owner string - Repo string - Sha string - Branch string - PullRequest string - Author string - Gravatar string - Timestamp string - Message string +// List of registered plugins. +var remotes []Remote + +// Register registers a plugin by name. +// +// All plugins must be registered when the application +// initializes. This should not be invoked while the application +// is running, and is not thread safe. +func Register(remote Remote) { + remotes = append(remotes, remote) } -// Login represents a standard subset of user meta-data -// provided by OAuth login services. -type Login struct { - ID int64 - Login string - Access string - Secret string - Name string - Email string +// List Registered remote plugins +func Registered() []Remote { + return remotes } -// User represents a standard subset of user meta-data -// returned by REST API user endpoints (ie github user api). -type User struct { - ID int64 - Login string - Name string - Gravatar string -} - -// Repo represents a standard subset of repository meta-data -// returned by REST API endpoints (ie github repo api). -type Repo struct { - ID int64 - Host string - Owner string - Name string - Kind string - Clone string - Git string - SSH string - URL string - Private bool - Pull bool - Push bool - Admin bool +// Lookup gets a plugin by name. +func Lookup(name string) Remote { + for _, remote := range remotes { + if remote.GetKind() == name || + remote.GetHost() == name { + return remote + } + } + return nil } diff --git a/plugin/remote/stash/client.go b/plugin/remote/stash/client.go deleted file mode 100644 index b7e9ecc9ff..0000000000 --- a/plugin/remote/stash/client.go +++ /dev/null @@ -1,38 +0,0 @@ -package stash - -import ( - "github.com/drone/drone/plugin/remote" -) - -type Client struct { - config *Stash - access string // user access token - secret string // user access token secret -} - -// GetUser fetches the user by ID (login name). -func (c *Client) GetUser(login string) (*remote.User, error) { - return nil, nil -} - -// GetRepos fetches all repositories that the specified -// user has access to in the remote system. -func (c *Client) GetRepos(owner string) ([]*remote.Repo, error) { - return nil, nil -} - -// GetScript fetches the build script (.drone.yml) from the remote -// repository and returns in string format. -func (c *Client) GetScript(*remote.Hook) (string, error) { - return "", nil -} - -// SetStatus -func (c *Client) SetStatus(owner, repo, sha, status string) error { - return nil -} - -// SetActive -func (c *Client) SetActive(owner, repo, hook, key string) error { - return nil -} diff --git a/plugin/remote/stash/stash.go b/plugin/remote/stash/stash.go deleted file mode 100644 index f4cf69c658..0000000000 --- a/plugin/remote/stash/stash.go +++ /dev/null @@ -1,54 +0,0 @@ -package stash - -import ( - "net/http" - "net/url" - "strings" - - "github.com/drone/drone/plugin/remote" -) - -type Stash struct { - URL string `json:"url"` // https://bitbucket.org - API string `json:"api"` // https://api.bitbucket.org - Client string `json:"client"` - Secret string `json:"secret"` - Enabled bool `json:"enabled"` -} - -// GetName returns the name of this remote system. -func (s *Stash) GetName() string { - return "stash.atlassian.com" -} - -// GetHost returns the url.Host of this remote system. -func (s *Stash) GetHost() (host string) { - u, err := url.Parse(s.URL) - if err != nil { - return - } - return u.Host -} - -// GetHook parses the post-commit hook from the Request body -// and returns the required data in a standard format. -func (s *Stash) GetHook(*http.Request) (*remote.Hook, error) { - return nil, nil -} - -// GetLogin handles authentication to third party, remote services -// and returns the required user data in a standard format. -func (s *Stash) GetLogin(http.ResponseWriter, *http.Request) (*remote.Login, error) { - return nil, nil -} - -// GetClient returns a new Stash remote client. -func (s *Stash) GetClient(access, secret string) remote.Client { - return &Client{s, access, secret} -} - -// IsMatch returns true if the hostname matches the -// hostname of this remote client. -func (s *Stash) IsMatch(hostname string) bool { - return strings.HasSuffix(hostname, s.URL) -} diff --git a/server/handler/hook.go b/server/handler/hook.go index aec90bf279..a813a6f9d3 100644 --- a/server/handler/hook.go +++ b/server/handler/hook.go @@ -1,7 +1,6 @@ package handler import ( - "log" "net/http" "strings" @@ -28,24 +27,14 @@ func NewHookHandler(users database.UserManager, repos database.RepoManager, comm // PostHook receives a post-commit hook from GitHub, Bitbucket, etc // GET /hook/:host func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { - host := r.FormValue(":host") - log.Println("received post-commit hook.") - - remoteServer, err := h.remotes.FindType(host) - if err != nil { - return notFound{err} - } - - remotePlugin, ok := remote.Lookup(remoteServer.Type) - if !ok { + var host = r.FormValue(":host") + var remote = remote.Lookup(host) + if remote == nil { return notFound{} } - // get the remote system's client. - plugin := remotePlugin(remoteServer) - // parse the hook payload - hook, err := plugin.GetHook(r) + hook, err := remote.ParseHook(r) if err != nil { return badRequest{err} } @@ -59,7 +48,7 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { } // fetch the repository from the database - repo, err := h.repos.FindName(plugin.GetHost(), hook.Owner, hook.Repo) + repo, err := h.repos.FindName(remote.GetHost(), hook.Owner, hook.Repo) if err != nil { return notFound{} } @@ -78,8 +67,7 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { } // featch the .drone.yml file from the database - client := plugin.GetClient(user.Access, user.Secret) - yml, err := client.GetScript(hook) + yml, err := remote.GetScript(user, repo, hook) if err != nil { return badRequest{err} } @@ -87,7 +75,7 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { // verify the commit hooks branch matches the list of approved // branches (unless it is a pull request). Note that we don't really // care if parsing the yaml fails here. - s, _ := script.ParseBuild(yml, map[string]string{}) + s, _ := script.ParseBuild(string(yml), map[string]string{}) if len(hook.PullRequest) == 0 && !s.MatchBranch(hook.Branch) { w.WriteHeader(http.StatusOK) return nil @@ -101,7 +89,7 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { PullRequest: hook.PullRequest, Timestamp: hook.Timestamp, Message: hook.Message, - Config: yml} + Config: string(yml)} c.SetAuthor(hook.Author) // inser the commit into the database if err := h.commits.Insert(&c); err != nil { diff --git a/server/handler/login.go b/server/handler/login.go index 8628560817..6004f5cd7f 100644 --- a/server/handler/login.go +++ b/server/handler/login.go @@ -28,24 +28,15 @@ func NewLoginHandler(users database.UserManager, repos database.RepoManager, per // GetLogin gets the login to the 3rd party remote system. // GET /login/:host func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { - host := r.FormValue(":host") - redirect := "/" - - remoteServer, err := h.remotes.FindType(host) - if err != nil { - return notFound{err} - } - - remotePlugin, ok := remote.Lookup(remoteServer.Type) - if !ok { + var host = r.FormValue(":host") + var redirect = "/" + var remote = remote.Lookup(host) + if remote == nil { return notFound{} } - // get the remote system's client. - plugin := remotePlugin(remoteServer) - // authenticate the user - login, err := plugin.GetLogin(w, r) + login, err := remote.Authorize(w, r) if err != nil { return badRequest{err} } else if login == nil { @@ -60,12 +51,12 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { // if self-registration is disabled we should // return a notAuthorized error. the only exception // is if no users exist yet in the system we'll proceed. - if remoteServer.Open == false && h.users.Exist() { + if h.users.Exist() { return notAuthorized{} } // create the user account - u = model.NewUser(plugin.GetName(), login.Login, login.Email) + u = model.NewUser(remote.GetKind(), login.Login, login.Email) u.Name = login.Name u.SetEmail(login.Email) @@ -110,41 +101,32 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { // sync inside a goroutine. This should eventually be moved to // its own package / sync utility. go func() { - // list all repositories - client := plugin.GetClient(u.Access, u.Secret) - repos, err := client.GetRepos("") + repos, err := remote.GetRepos(u) if err != nil { log.Println("Error syncing user account, listing repositories", u.Login, err) return } // insert all repositories - for _, remoteRepo := range repos { - repo, _ := model.NewRepo(plugin.GetName(), remoteRepo.Owner, remoteRepo.Name) - repo.Private = remoteRepo.Private - repo.Host = remoteRepo.Host - repo.CloneURL = remoteRepo.Clone - repo.GitURL = remoteRepo.Git - repo.SSHURL = remoteRepo.SSH - repo.URL = remoteRepo.URL - + for _, repo := range repos { + var role = repo.Role if err := h.repos.Insert(repo); err != nil { // typically we see a failure because the repository already exists // in which case, we can retrieve the existing record to get the ID. repo, err = h.repos.FindName(repo.Host, repo.Owner, repo.Name) if err != nil { - log.Println("Error adding repo.", u.Login, remoteRepo.Name, err) + log.Println("Error adding repo.", u.Login, repo.Name, err) continue } } // add user permissions - if err := h.perms.Grant(u, repo, remoteRepo.Pull, remoteRepo.Push, remoteRepo.Admin); err != nil { - log.Println("Error adding permissions.", u.Login, remoteRepo.Name, err) + if err := h.perms.Grant(u, repo, role.Read, role.Write, role.Admin); err != nil { + log.Println("Error adding permissions.", u.Login, repo.Name, err) continue } - log.Println("Successfully syced repo.", u.Login+"/"+remoteRepo.Name) + log.Println("Successfully syced repo.", u.Login+"/"+repo.Name) } u.Synced = time.Now().UTC().Unix() diff --git a/server/handler/remote.go b/server/handler/remote.go index 57280d6571..c0004389b6 100644 --- a/server/handler/remote.go +++ b/server/handler/remote.go @@ -3,11 +3,10 @@ package handler import ( "encoding/json" "net/http" - "net/url" + "github.com/drone/drone/plugin/remote" "github.com/drone/drone/server/database" "github.com/drone/drone/server/session" - "github.com/drone/drone/shared/model" "github.com/gorilla/pat" ) @@ -21,130 +20,20 @@ func NewRemoteHandler(users database.UserManager, remotes database.RemoteManager return &RemoteHandler{users, remotes, sess} } -// GetRemotes gets all remotes. -// GET /api/remotes -func (h *RemoteHandler) GetRemotes(w http.ResponseWriter, r *http.Request) error { - // get the user form the session - user := h.sess.User(r) - switch { - case user == nil: - return notAuthorized{} - case user.Admin == false: - return forbidden{} - } - // get all remotes - remotes, err := h.remotes.List() - if err != nil { - return internalServerError{err} - } - - return json.NewEncoder(w).Encode(remotes) -} - // GetRemoteLogins gets all remote logins. // GET /api/remotes/logins func (h *RemoteHandler) GetRemoteLogins(w http.ResponseWriter, r *http.Request) error { - remotes, err := h.remotes.List() - if err != nil { - return internalServerError{err} - } + var list = remote.Registered() var logins []interface{} - for _, remote := range remotes { + for _, item := range list { logins = append(logins, struct { Type string `json:"type"` Host string `json:"host"` - }{remote.Type, remote.Host}) + }{item.GetKind(), item.GetHost()}) } return json.NewEncoder(w).Encode(&logins) } -// PostRemote creates a new remote. -// POST /api/remotes -func (h *RemoteHandler) PostRemote(w http.ResponseWriter, r *http.Request) error { - // get the user form the session - user := h.sess.User(r) - if user == nil || !user.Admin { - // if no users exist, this request is part of - // the system installation process and can proceed. - // else we should reject. - if h.users.Exist() { - return notAuthorized{} - } - } - // unmarshal the remote from the payload - defer r.Body.Close() - in := model.Remote{} - if err := json.NewDecoder(r.Body).Decode(&in); err != nil { - return badRequest{err} - } - uri, err := url.Parse(in.URL) - if err != nil { - return badRequest{err} - } - in.Host = uri.Host - - // there is an edge case where, during installation, a user could attempt - // to add the same result multiple times. In this case we will delete - // the old remote prior to adding the new one. - if remote, err := h.remotes.FindHost(in.Host); err == nil && h.users.Exist() { - h.remotes.Delete(remote) - } - - // insert the remote in the database - if err := h.remotes.Insert(&in); err != nil { - return internalServerError{err} - } - - return json.NewEncoder(w).Encode(&in) -} - -// PutRemote updates an existing remote. -// PUT /api/remotes -func (h *RemoteHandler) PutRemote(w http.ResponseWriter, r *http.Request) error { - // get the user form the session - user := h.sess.User(r) - switch { - case user == nil: - return notAuthorized{} - case user.Admin == false: - return forbidden{} - } - // unmarshal the remote from the payload - defer r.Body.Close() - in := model.Remote{} - if err := json.NewDecoder(r.Body).Decode(&in); err != nil { - return badRequest{err} - } - uri, err := url.Parse(in.URL) - if err != nil { - return badRequest{err} - } - in.Host = uri.Host - - // retrieve the remote and return an error if not exists - remote, err := h.remotes.FindHost(in.Host) - if err != nil { - return notFound{err} - } - - // update the remote details - remote.API = in.API - remote.URL = in.URL - remote.Host = in.Host - remote.Client = in.Client - remote.Secret = in.Secret - - // insert the remote in the database - if err := h.remotes.Update(remote); err != nil { - return internalServerError{err} - } - - return json.NewEncoder(w).Encode(remote) -} - func (h *RemoteHandler) Register(r *pat.Router) { r.Get("/v1/logins", errorHandler(h.GetRemoteLogins)) - r.Get("/v1/remotes", errorHandler(h.GetRemotes)) - r.Post("/v1/remotes", errorHandler(h.PostRemote)) - r.Put("/v1/remotes", errorHandler(h.PutRemote)) } diff --git a/server/handler/repo.go b/server/handler/repo.go index 37cd07a728..c1b969261d 100644 --- a/server/handler/repo.go +++ b/server/handler/repo.go @@ -46,20 +46,17 @@ func (h *RepoHandler) GetRepo(w http.ResponseWriter, r *http.Request) error { } // user must have read access to the repository. - role := h.perms.Find(user, repo) + repo.Role = h.perms.Find(user, repo) switch { - case role.Read == false && user == nil: + case repo.Role.Read == false && user == nil: return notAuthorized{} - case role.Read == false && user != nil: + case repo.Role.Read == false && user != nil: return notFound{} } // if the user is not requesting admin data we can // return exactly what we have. if len(admin) == 0 { - return json.NewEncoder(w).Encode(struct { - *model.Repo - Role *model.Perm `json:"role"` - }{repo, role}) + return json.NewEncoder(w).Encode(repo) } // ammend the response to include data that otherwise @@ -71,10 +68,9 @@ func (h *RepoHandler) GetRepo(w http.ResponseWriter, r *http.Request) error { return json.NewEncoder(w).Encode(struct { *model.Repo - Role *model.Perm `json:"role"` - PublicKey string `json:"public_key"` - Params string `json:"params"` - }{repo, role, repo.PublicKey, repo.Params}) + PublicKey string `json:"public_key"` + Params string `json:"params"` + }{repo, repo.PublicKey, repo.Params}) } // PostRepo activates the named repository. @@ -118,26 +114,16 @@ func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error { repo.PublicKey = sshutil.MarshalPublicKey(&key.PublicKey) repo.PrivateKey = sshutil.MarshalPrivateKey(key) - // get the remote and client - remoteServer, err := h.remotes.FindType(repo.Remote) - if err != nil { - return notFound{err} - } - - remotePlugin, ok := remote.Lookup(remoteServer.Type) - if !ok { + var remote = remote.Lookup(host) + if remote == nil { return notFound{} } - // get the remote system's client. - plugin := remotePlugin(remoteServer) - // post commit hook url - hook := fmt.Sprintf("%s://%s/v1/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), plugin.GetName()) + hook := fmt.Sprintf("%s://%s/v1/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), remote.GetKind()) // activate the repository in the remote system - client := plugin.GetClient(user.Access, user.Secret) - if err := client.SetActive(owner, name, hook, repo.PublicKey); err != nil { + if err := remote.Activate(user, repo, hook); err != nil { return badRequest{err} } diff --git a/shared/model/hook.go b/shared/model/hook.go new file mode 100644 index 0000000000..ef5a585e5f --- /dev/null +++ b/shared/model/hook.go @@ -0,0 +1,15 @@ +package model + +// Hook represents a subset of commit meta-data provided +// by post-commit and pull request hooks. +type Hook struct { + Owner string + Repo string + Sha string + Branch string + PullRequest string + Author string + Gravatar string + Timestamp string + Message string +} diff --git a/shared/model/login.go b/shared/model/login.go new file mode 100644 index 0000000000..a7ff2c048f --- /dev/null +++ b/shared/model/login.go @@ -0,0 +1,12 @@ +package model + +// Login represents a standard subset of user meta-data +// provided by OAuth login services. +type Login struct { + ID int64 + Login string + Access string + Secret string + Name string + Email string +} diff --git a/shared/model/repo.go b/shared/model/repo.go index ad1a07181d..df31e2c1a2 100644 --- a/shared/model/repo.go +++ b/shared/model/repo.go @@ -39,6 +39,11 @@ type Repo struct { Timeout int64 `meddler:"repo_timeout" json:"timeout"` Created int64 `meddler:"repo_created" json:"created_at"` Updated int64 `meddler:"repo_updated" json:"updated_at"` + + // Role defines the user's role relative to this repository. + // Note that this data is stored separately in the datastore, + // and must be joined to populate. + Role *Perm `meddler:"-" json:"role,omitempty"` } func NewRepo(remote, owner, name string) (*Repo, error) {