woodpecker/server/remote/bitbucket/bitbucket.go

369 lines
11 KiB
Go
Raw Normal View History

// Copyright 2022 Woodpecker Authors
2018-02-19 22:24:10 +00:00
// Copyright 2018 Drone.IO Inc.
2018-03-21 13:02:17 +00:00
//
2018-02-19 22:24:10 +00:00
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
2018-03-21 13:02:17 +00:00
//
2018-02-19 22:24:10 +00:00
// http://www.apache.org/licenses/LICENSE-2.0
2018-03-21 13:02:17 +00:00
//
2018-02-19 22:24:10 +00:00
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitbucket
import (
"context"
"fmt"
"net/http"
"net/url"
"golang.org/x/oauth2"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/bitbucket/internal"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
)
2016-05-01 06:22:30 +00:00
// Bitbucket cloud endpoints.
const (
DefaultAPI = "https://api.bitbucket.org"
DefaultURL = "https://bitbucket.org"
)
// Opts are remote options for bitbucket
type Opts struct {
Client string
Secret string
}
2016-04-29 19:39:56 +00:00
type config struct {
2016-05-01 06:22:30 +00:00
API string
URL string
Client string
Secret string
}
2016-04-29 19:39:56 +00:00
// New returns a new remote Configuration for integrating with the Bitbucket
// repository hosting service at https://bitbucket.org
func New(opts *Opts) (remote.Remote, error) {
2016-04-29 19:39:56 +00:00
return &config{
2016-05-01 06:22:30 +00:00
API: DefaultAPI,
URL: DefaultURL,
Client: opts.Client,
Secret: opts.Secret,
}, nil
// TODO: add checks
}
2022-06-17 18:14:01 +00:00
// Name returns the string name of this driver
func (c *config) Name() string {
return "bitbucket"
}
2016-05-01 06:22:30 +00:00
// Login authenticates an account with Bitbucket using the oauth2 protocol. The
// Bitbucket account details are returned when the user is successfully authenticated.
func (c *config) Login(ctx context.Context, w http.ResponseWriter, req *http.Request) (*model.User, error) {
config := c.newConfig(server.Config.Server.Host)
// get the OAuth errors
if err := req.FormValue("error"); err != "" {
2016-12-19 16:22:11 +00:00
return nil, &remote.AuthError{
Err: err,
Description: req.FormValue("error_description"),
URI: req.FormValue("error_uri"),
}
}
// get the OAuth code
code := req.FormValue("code")
if len(code) == 0 {
http.Redirect(w, req, config.AuthCodeURL("woodpecker"), http.StatusSeeOther)
2016-04-29 19:39:56 +00:00
return nil, nil
}
token, err := config.Exchange(ctx, code)
if err != nil {
2016-04-29 19:39:56 +00:00
return nil, err
}
client := internal.NewClient(ctx, c.API, config.Client(ctx, token))
curr, err := client.FindCurrent()
if err != nil {
2016-04-29 19:39:56 +00:00
return nil, err
}
2016-04-29 19:39:56 +00:00
return convertUser(curr, token), nil
}
2016-05-01 06:22:30 +00:00
// Auth uses the Bitbucket oauth2 access token and refresh token to authenticate
// a session and return the Bitbucket account login.
func (c *config) Auth(ctx context.Context, token, secret string) (string, error) {
client := c.newClientToken(ctx, token, secret)
user, err := client.FindCurrent()
if err != nil {
return "", err
}
return user.Login, nil
}
2016-05-01 06:22:30 +00:00
// Refresh refreshes the Bitbucket oauth2 access token. If the token is
// refreshed the user is updated and a true value is returned.
func (c *config) Refresh(ctx context.Context, user *model.User) (bool, error) {
2016-05-01 06:22:30 +00:00
config := c.newConfig("")
source := config.TokenSource(
ctx, &oauth2.Token{RefreshToken: user.Secret})
token, err := source.Token()
if err != nil || len(token.AccessToken) == 0 {
return false, err
}
user.Token = token.AccessToken
user.Secret = token.RefreshToken
user.Expiry = token.Expiry.UTC().Unix()
return true, nil
}
2016-05-01 06:22:30 +00:00
// Teams returns a list of all team membership for the Bitbucket account.
func (c *config) Teams(ctx context.Context, u *model.User) ([]*model.Team, error) {
opts := &internal.ListWorkspacesOpts{
2016-04-29 19:39:56 +00:00
PageLen: 100,
Role: "member",
}
resp, err := c.newClient(ctx, u).ListWorkspaces(opts)
2016-04-29 19:39:56 +00:00
if err != nil {
return nil, err
}
return convertWorkspaceList(resp.Values), nil
2016-04-29 19:39:56 +00:00
}
2016-05-01 06:22:30 +00:00
// Repo returns the named Bitbucket repository.
func (c *config) Repo(ctx context.Context, u *model.User, id model.RemoteID, owner, name string) (*model.Repo, error) {
if id.IsValid() {
name = string(id)
}
repo, err := c.newClient(ctx, u).FindRepo(owner, name)
if err != nil {
return nil, err
}
return convertRepo(repo), nil
}
2016-05-01 06:22:30 +00:00
// Repos returns a list of all repositories for Bitbucket account, including
// organization repositories.
func (c *config) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error) {
client := c.newClient(ctx, u)
2016-04-29 19:39:56 +00:00
2017-07-14 19:58:38 +00:00
var all []*model.Repo
resp, err := client.ListWorkspaces(&internal.ListWorkspacesOpts{
2016-05-01 06:22:30 +00:00
PageLen: 100,
Role: "member",
})
if err != nil {
2016-05-01 06:22:30 +00:00
return all, err
}
for _, workspace := range resp.Values {
repos, err := client.ListReposAll(workspace.Slug)
if err != nil {
2016-05-01 06:22:30 +00:00
return all, err
}
2016-05-01 06:22:30 +00:00
for _, repo := range repos {
2017-07-14 19:58:38 +00:00
all = append(all, convertRepo(repo))
}
}
2016-05-01 06:22:30 +00:00
return all, nil
}
2016-05-01 06:22:30 +00:00
// Perm returns the user permissions for the named repository. Because Bitbucket
// does not have an endpoint to access user permissions, we attempt to fetch
// the repository hook list, which is restricted to administrators to calculate
// administrative access to a repository.
func (c *config) Perm(ctx context.Context, u *model.User, r *model.Repo) (*model.Perm, error) {
client := c.newClient(ctx, u)
perms := new(model.Perm)
repo, err := client.FindRepo(r.Owner, r.Name)
if err != nil {
return perms, err
}
perm, err := client.GetPermission(repo.FullName)
if err != nil {
return perms, err
}
switch perm.Permission {
case "admin":
perms.Admin = true
fallthrough
case "write":
perms.Push = true
fallthrough
default:
perms.Pull = true
}
return perms, nil
}
// File fetches the file from the Bitbucket repository and returns its contents.
func (c *config) File(ctx context.Context, u *model.User, r *model.Repo, p *model.Pipeline, f string) ([]byte, error) {
config, err := c.newClient(ctx, u).FindSource(r.Owner, r.Name, p.Commit, f)
if err != nil {
return nil, err
}
return []byte(*config), err
}
func (c *config) Dir(ctx context.Context, u *model.User, r *model.Repo, p *model.Pipeline, f string) ([]*remote.FileMeta, error) {
return nil, fmt.Errorf("Not implemented")
}
// Status creates a pipeline status for the Bitbucket commit.
func (c *config) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, proc *model.Proc) error {
status := internal.PipelineStatus{
State: convertStatus(pipeline.Status),
Desc: common.GetPipelineStatusDescription(pipeline.Status),
Key: "Woodpecker",
URL: common.GetPipelineStatusLink(repo, pipeline, nil),
2015-11-21 05:22:28 +00:00
}
return c.newClient(ctx, user).CreateStatus(repo.Owner, repo.Name, pipeline.Commit, &status)
}
2016-05-01 06:22:30 +00:00
// Activate activates the repository by registering repository push hooks with
// the Bitbucket repository. Prior to registering hook, previously created hooks
// are deleted.
func (c *config) Activate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
2016-04-29 19:39:56 +00:00
rawurl, err := url.Parse(link)
if err != nil {
return err
}
_ = c.Deactivate(ctx, u, r, link)
return c.newClient(ctx, u).CreateHook(r.Owner, r.Name, &internal.Hook{
Active: true,
2016-04-29 19:39:56 +00:00
Desc: rawurl.Host,
Events: []string{"repo:push"},
URL: link,
})
}
2016-05-01 06:22:30 +00:00
// Deactivate deactives the repository be removing repository push hooks from
// the Bitbucket repository.
func (c *config) Deactivate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
client := c.newClient(ctx, u)
2016-04-29 19:39:56 +00:00
hooks, err := client.ListHooks(r.Owner, r.Name, &internal.ListOpts{})
if err != nil {
return err
2016-04-29 19:39:56 +00:00
}
2016-05-01 06:22:30 +00:00
hook := matchingHooks(hooks.Values, link)
if hook != nil {
return client.DeleteHook(r.Owner, r.Name, hook.UUID)
}
return nil
}
2016-05-01 06:22:30 +00:00
// Netrc returns a netrc file capable of authenticating Bitbucket requests and
// cloning Bitbucket repositories.
func (c *config) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
return &model.Netrc{
Machine: "bitbucket.org",
Login: "x-token-auth",
Password: u.Token,
}, nil
}
// Branches returns the names of all branches for the named repository.
func (c *config) Branches(ctx context.Context, u *model.User, r *model.Repo) ([]string, error) {
bitbucketBranches, err := c.newClient(ctx, u).ListBranches(r.Owner, r.Name)
if err != nil {
return nil, err
}
branches := make([]string, 0)
for _, branch := range bitbucketBranches {
branches = append(branches, branch.Name)
}
return branches, nil
}
// BranchHead returns the sha of the head (lastest commit) of the specified branch
func (c *config) BranchHead(ctx context.Context, u *model.User, r *model.Repo, branch string) (string, error) {
// TODO(1138): missing implementation
return "", fmt.Errorf("missing implementation")
}
// Hook parses the incoming Bitbucket hook and returns the Repository and
// Pipeline details. If the hook is unsupported nil values are returned.
func (c *config) Hook(ctx context.Context, req *http.Request) (*model.Repo, *model.Pipeline, error) {
return parseHook(req)
}
2016-05-01 06:22:30 +00:00
// OrgMembership returns if user is member of organization and if user
// is admin/owner in this organization.
func (c *config) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) {
perm, err := c.newClient(ctx, u).GetUserWorkspaceMembership(owner, u.Login)
if err != nil {
return nil, err
}
return &model.OrgPerm{Member: perm != "", Admin: perm == "owner"}, nil
}
2016-05-01 06:22:30 +00:00
// helper function to return the bitbucket oauth2 client
func (c *config) newClient(ctx context.Context, u *model.User) *internal.Client {
if u == nil {
return c.newClientToken(ctx, "", "")
}
return c.newClientToken(ctx, u.Token, u.Secret)
2016-05-01 06:22:30 +00:00
}
// helper function to return the bitbucket oauth2 client
func (c *config) newClientToken(ctx context.Context, token, secret string) *internal.Client {
2016-05-01 06:22:30 +00:00
return internal.NewClientToken(
ctx,
2016-05-01 06:22:30 +00:00
c.API,
c.Client,
c.Secret,
&oauth2.Token{
AccessToken: token,
RefreshToken: secret,
},
)
}
// helper function to return the bitbucket oauth2 config
func (c *config) newConfig(redirect string) *oauth2.Config {
return &oauth2.Config{
ClientID: c.Client,
ClientSecret: c.Secret,
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/site/oauth2/authorize", c.URL),
TokenURL: fmt.Sprintf("%s/site/oauth2/access_token", c.URL),
},
RedirectURL: fmt.Sprintf("%s/authorize", redirect),
}
}
// helper function to return matching hooks.
func matchingHooks(hooks []*internal.Hook, rawurl string) *internal.Hook {
link, err := url.Parse(rawurl)
if err != nil {
return nil
}
for _, hook := range hooks {
hookurl, err := url.Parse(hook.URL)
2016-05-01 06:22:30 +00:00
if err == nil && hookurl.Host == link.Host {
return hook
}
}
return nil
}