woodpecker/server/remote/bitbucketserver/bitbucketserver.go
qwerty287 52d3652f2e
Fetch repositories with remote ID if possible (#1078)
Use IDs of the forge to fetch repositories instead of their names and owner names. This improves handling of renamed and transferred repos.

TODO

- [ ] try to support as many forges as possible
    - [x] Gogs (no API)
    - [ ] Bitbucket Server
    - [x] Coding (no API?)
- [x] update repo every time it is fetched or received from the forge
- [x] if repo remote IDs are not available, use owner / name to get it
- [x] handle redirections (redirect a renamed repo to its new path)
- [x] ~~pull all repos once during migration to update ID (?)~~ issue fixed by on-demand loading of remote IDs
- [x] handle redirections in web UI
- [ ] improve handling of hooks after a repo was renamed (currently it checks for a redirection to the repo)
- [x] tests
- [x] `UNIQUE` constraint for remote IDs after migration shouldn't work (all repos have an empty string as remote ID)

close #854
close #648 partial
close https://codeberg.org/Codeberg-CI/feedback/issues/46

Possible follow-up PRs
- apply the same scheme on everything fetched from the remote (currently only users)

Co-authored-by: 6543 <6543@obermui.de>
2022-09-05 17:08:51 +02:00

278 lines
8.6 KiB
Go

// Copyright 2018 Drone.IO Inc.
//
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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 bitbucketserver
// WARNING! This is an work-in-progress patch and does not yet conform to the coding,
// quality or security standards expected of this project. Please use with caution.
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"os"
"github.com/mrjones/oauth"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/bitbucketserver/internal"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
)
const (
requestTokenURL = "%s/plugins/servlet/oauth/request-token"
authorizeTokenURL = "%s/plugins/servlet/oauth/authorize"
accessTokenURL = "%s/plugins/servlet/oauth/access-token"
)
// Opts defines configuration options.
type Opts struct {
URL string // Stash server url.
Username string // Git machine account username.
Password string // Git machine account password.
ConsumerKey string // Oauth1 consumer key.
ConsumerRSA string // Oauth1 consumer key file.
ConsumerRSAString string
SkipVerify bool // Skip ssl verification.
}
type Config struct {
URL string
Username string
Password string
SkipVerify bool
Consumer *oauth.Consumer
}
// New returns a Remote implementation that integrates with Bitbucket Server,
// the on-premise edition of Bitbucket Cloud, formerly known as Stash.
func New(opts Opts) (remote.Remote, error) {
config := &Config{
URL: opts.URL,
Username: opts.Username,
Password: opts.Password,
SkipVerify: opts.SkipVerify,
}
switch {
case opts.Username == "":
return nil, fmt.Errorf("Must have a git machine account username")
case opts.Password == "":
return nil, fmt.Errorf("Must have a git machine account password")
case opts.ConsumerKey == "":
return nil, fmt.Errorf("Must have a oauth1 consumer key")
}
if opts.ConsumerRSA == "" && opts.ConsumerRSAString == "" {
return nil, fmt.Errorf("must have CONSUMER_RSA_KEY set to the path of a oauth1 consumer key file or CONSUMER_RSA_KEY_STRING set to the value of a oauth1 consumer key")
}
var keyFileBytes []byte
if opts.ConsumerRSA != "" {
var err error
keyFileBytes, err = os.ReadFile(opts.ConsumerRSA)
if err != nil {
return nil, err
}
} else {
keyFileBytes = []byte(opts.ConsumerRSAString)
}
block, _ := pem.Decode(keyFileBytes)
PrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
config.Consumer = CreateConsumer(opts.URL, opts.ConsumerKey, PrivateKey)
return config, nil
}
// Name returns the string name of this driver
func (c *Config) Name() string {
return "stash"
}
func (c *Config) Login(ctx context.Context, res http.ResponseWriter, req *http.Request) (*model.User, error) {
requestToken, u, err := c.Consumer.GetRequestTokenAndUrl("oob")
if err != nil {
return nil, err
}
code := req.FormValue("oauth_verifier")
if len(code) == 0 {
http.Redirect(res, req, u, http.StatusSeeOther)
return nil, nil
}
requestToken.Token = req.FormValue("oauth_token")
accessToken, err := c.Consumer.AuthorizeToken(requestToken, code)
if err != nil {
return nil, err
}
client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, accessToken.Token)
user, err := client.FindCurrentUser()
if err != nil {
return nil, err
}
return convertUser(user, accessToken), nil
}
// Auth is not supported by the Stash driver.
func (*Config) Auth(ctx context.Context, token, secret string) (string, error) {
return "", fmt.Errorf("Not Implemented")
}
// Teams is not supported by the Stash driver.
func (*Config) Teams(ctx context.Context, u *model.User) ([]*model.Team, error) {
var teams []*model.Team
return teams, nil
}
// TeamPerm is not supported by the Stash driver.
func (*Config) TeamPerm(u *model.User, org string) (*model.Perm, error) {
return nil, nil
}
func (c *Config) Repo(ctx context.Context, u *model.User, _ model.RemoteID, owner, name string) (*model.Repo, error) {
repo, err := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token).FindRepo(owner, name)
if err != nil {
return nil, err
}
return convertRepo(repo), nil
}
func (c *Config) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error) {
repos, err := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token).FindRepos()
if err != nil {
return nil, err
}
var all []*model.Repo
for _, repo := range repos {
all = append(all, convertRepo(repo))
}
return all, nil
}
func (c *Config) Perm(ctx context.Context, u *model.User, repo *model.Repo) (*model.Perm, error) {
client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token)
return client.FindRepoPerms(repo.Owner, repo.Name)
}
func (c *Config) File(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token)
return client.FindFileForRepo(r.Owner, r.Name, f, b.Ref)
}
func (c *Config) Dir(ctx context.Context, u *model.User, r *model.Repo, b *model.Build, f string) ([]*remote.FileMeta, error) {
return nil, fmt.Errorf("Not implemented")
}
// Status is not supported by the bitbucketserver driver.
func (c *Config) Status(ctx context.Context, user *model.User, repo *model.Repo, build *model.Build, proc *model.Proc) error {
status := internal.BuildStatus{
State: convertStatus(build.Status),
Desc: common.GetBuildStatusDescription(build.Status),
Name: fmt.Sprintf("Woodpecker #%d - %s", build.Number, build.Branch),
Key: "Woodpecker",
URL: common.GetBuildStatusLink(repo, build, nil),
}
client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, user.Token)
return client.CreateStatus(build.Commit, &status)
}
func (c *Config) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) {
host, err := common.ExtractHostFromCloneURL(r.Clone)
if err != nil {
return nil, err
}
return &model.Netrc{
Login: c.Username,
Password: c.Password,
Machine: host,
}, nil
}
func (c *Config) Activate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token)
return client.CreateHook(r.Owner, r.Name, link)
}
// 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 := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token).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")
}
func (c *Config) Deactivate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
client := internal.NewClientWithToken(ctx, c.URL, c.Consumer, u.Token)
return client.DeleteHook(r.Owner, r.Name, link)
}
func (c *Config) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
return parseHook(r, c.URL)
}
// 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) {
// TODO: Not implemented currently
return nil, nil
}
func CreateConsumer(URL, ConsumerKey string, PrivateKey *rsa.PrivateKey) *oauth.Consumer {
consumer := oauth.NewRSAConsumer(
ConsumerKey,
PrivateKey,
oauth.ServiceProvider{
RequestTokenUrl: fmt.Sprintf(requestTokenURL, URL),
AuthorizeTokenUrl: fmt.Sprintf(authorizeTokenURL, URL),
AccessTokenUrl: fmt.Sprintf(accessTokenURL, URL),
HttpMethod: "POST",
})
consumer.HttpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
},
}
return consumer
}