diff --git a/cmd/server/setup.go b/cmd/server/setup.go index 4c801d369..adaa325d3 100644 --- a/cmd/server/setup.go +++ b/cmd/server/setup.go @@ -74,7 +74,7 @@ func setupEnvironService(c *cli.Context, s store.Store) model.EnvironService { return environments.Filesystem(c.StringSlice("environment")) } -// helper function to setup the remote from the CLI arguments. +// SetupRemote helper function to setup the remote from the CLI arguments. func SetupRemote(c *cli.Context) (remote.Remote, error) { switch { case c.Bool("github"): @@ -117,17 +117,7 @@ func setupGogs(c *cli.Context) (remote.Remote, error) { // helper function to setup the Gitea remote from the CLI arguments. func setupGitea(c *cli.Context) (remote.Remote, error) { - if !c.IsSet("gitea-client") { - return gitea.New(gitea.Opts{ - URL: c.String("gitea-server"), - Context: c.String("gitea-context"), - Username: c.String("gitea-git-username"), - Password: c.String("gitea-git-password"), - PrivateMode: c.Bool("gitea-private-mode"), - SkipVerify: c.Bool("gitea-skip-verify"), - }) - } - return gitea.NewOauth(gitea.Opts{ + return gitea.New(gitea.Opts{ URL: c.String("gitea-server"), Context: c.String("gitea-context"), Username: c.String("gitea-git-username"), diff --git a/server/remote/gitea/gitea.go b/server/remote/gitea/gitea.go index 1054a8dd5..85685e33b 100644 --- a/server/remote/gitea/gitea.go +++ b/server/remote/gitea/gitea.go @@ -18,6 +18,7 @@ package gitea import ( + "context" "crypto/tls" "fmt" "net" @@ -28,9 +29,30 @@ import ( "code.gitea.io/sdk/gitea" "github.com/woodpecker-ci/woodpecker/model" + "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/remote" + + "golang.org/x/oauth2" ) +const ( + authorizeTokenURL = "%s/login/oauth/authorize" + accessTokenURL = "%s/login/oauth/access_token" + perPage = 50 +) + +type Gitea struct { + URL string + Context string + Machine string + ClientID string + ClientSecret string + Username string + Password string + PrivateMode bool + SkipVerify bool +} + // Opts defines configuration options. type Opts struct { URL string // Gitea server url. @@ -43,14 +65,380 @@ type Opts struct { SkipVerify bool // Skip ssl verification. } -type client struct { - URL string - Context string - Machine string - Username string - Password string - PrivateMode bool - SkipVerify bool +// New returns a Remote implementation that integrates with Gitea, +// an open source Git service written in Go. See https://gitea.io/ +func New(opts Opts) (remote.Remote, error) { + u, err := url.Parse(opts.URL) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(u.Host) + if err == nil { + u.Host = host + } + return &Gitea{ + URL: opts.URL, + Context: opts.Context, + Machine: u.Host, + ClientID: opts.Client, + ClientSecret: opts.Secret, + Username: opts.Username, + Password: opts.Password, + PrivateMode: opts.PrivateMode, + SkipVerify: opts.SkipVerify, + }, nil +} + +// Login authenticates an account with Gitea using basic authentication. The +// Gitea account details are returned when the user is successfully authenticated. +func (c *Gitea) Login(w http.ResponseWriter, req *http.Request) (*model.User, error) { + config := &oauth2.Config{ + ClientID: c.ClientID, + ClientSecret: c.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf(authorizeTokenURL, c.URL), + TokenURL: fmt.Sprintf(accessTokenURL, c.URL), + }, + RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.Host), + } + + // get the OAuth errors + if err := req.FormValue("error"); err != "" { + 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("drone"), http.StatusSeeOther) + return nil, nil + } + + token, err := config.Exchange(oauth2.NoContext, code) + if err != nil { + return nil, err + } + + client, err := c.newClientToken(token.AccessToken) + if err != nil { + return nil, err + } + account, _, err := client.GetMyUserInfo() + if err != nil { + return nil, err + } + + return &model.User{ + Token: token.AccessToken, + Secret: token.RefreshToken, + Expiry: token.Expiry.UTC().Unix(), + Login: account.UserName, + Email: account.Email, + Avatar: expandAvatar(c.URL, account.AvatarURL), + }, nil +} + +// Auth uses the Gitea oauth2 access token and refresh token to authenticate +// a session and return the Gitea account login. +func (c *Gitea) Auth(token, _ string) (string, error) { + client, err := c.newClientToken(token) + if err != nil { + return "", err + } + user, _, err := client.GetMyUserInfo() + if err != nil { + return "", err + } + return user.UserName, nil +} + +// Refresh refreshes the Gitea oauth2 access token. If the token is +// refreshed the user is updated and a true value is returned. +func (c *Gitea) Refresh(user *model.User) (bool, error) { + config := &oauth2.Config{ + ClientID: c.ClientID, + ClientSecret: c.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf(authorizeTokenURL, c.URL), + TokenURL: fmt.Sprintf(accessTokenURL, c.URL), + }, + } + source := config.TokenSource(context.TODO(), &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 +} + +// Teams is supported by the Gitea driver. +func (c *Gitea) Teams(u *model.User) ([]*model.Team, error) { + client, err := c.newClientToken(u.Token) + if err != nil { + return nil, err + } + + teams := make([]*model.Team, 0, perPage) + + page := 1 + for { + orgs, _, err := client.ListMyOrgs( + gitea.ListOrgsOptions{ + ListOptions: gitea.ListOptions{ + Page: page, + PageSize: perPage, + }, + }, + ) + if err != nil { + return nil, err + } + + for _, org := range orgs { + teams = append(teams, toTeam(org, c.URL)) + } + + if len(orgs) < perPage { + break + } + page++ + } + + return teams, nil +} + +// TeamPerm is not supported by the Gitea driver. +func (c *Gitea) TeamPerm(u *model.User, org string) (*model.Perm, error) { + return nil, nil +} + +// Repo returns the named Gitea repository. +func (c *Gitea) Repo(u *model.User, owner, name string) (*model.Repo, error) { + client, err := c.newClientToken(u.Token) + if err != nil { + return nil, err + } + + repo, _, err := client.GetRepo(owner, name) + if err != nil { + return nil, err + } + if c.PrivateMode { + repo.Private = true + } + return toRepo(repo, c.PrivateMode), nil +} + +// Repos returns a list of all repositories for the Gitea account, including +// organization repositories. +func (c *Gitea) Repos(u *model.User) ([]*model.Repo, error) { + repos := make([]*model.Repo, 0, perPage) + + client, err := c.newClientToken(u.Token) + if err != nil { + return nil, err + } + + // Gitea SDK forces us to read repo list paginated. + var page = 1 + for { + all, _, err := client.ListMyRepos( + gitea.ListReposOptions{ + ListOptions: gitea.ListOptions{ + Page: page, + PageSize: perPage, + }, + }, + ) + if err != nil { + return nil, err + } + + for _, repo := range all { + repos = append(repos, toRepo(repo, c.PrivateMode)) + } + + if len(all) < perPage { + break + } + // Last page was not empty so more repos may be available - continue loop. + page++ + } + + return repos, nil +} + +// Perm returns the user permissions for the named Gitea repository. +func (c *Gitea) Perm(u *model.User, owner, name string) (*model.Perm, error) { + client, err := c.newClientToken(u.Token) + if err != nil { + return nil, err + } + + repo, _, err := client.GetRepo(owner, name) + if err != nil { + return nil, err + } + return toPerm(repo.Permissions), nil +} + +// File fetches the file from the Gitea repository and returns its contents. +func (c *Gitea) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { + client, err := c.newClientToken(u.Token) + if err != nil { + return nil, err + } + + cfg, _, err := client.GetFile(r.Owner, r.Name, b.Commit, f) + return cfg, err +} + +func (c *Gitea) Dir(u *model.User, r *model.Repo, b *model.Build, f string) ([]*remote.FileMeta, error) { + var configs []*remote.FileMeta + + client, err := c.newClientToken(u.Token) + if err != nil { + return nil, err + } + + // List files in repository. Path from root + tree, _, err := client.GetTrees(r.Owner, r.Name, b.Commit, true) + if err != nil { + return nil, err + } + + f = path.Clean(f) // We clean path and remove trailing slash + f += "/" + "*" // construct pattern for match i.e. file in subdir + for _, e := range tree.Entries { + // Filter path matching pattern and type file (blob) + if m, _ := filepath.Match(f, e.Path); m && e.Type == "blob" { + data, err := c.File(u, r, b, e.Path) + if err != nil { + return nil, fmt.Errorf("multi-pipeline cannot get %s: %s", e.Path, err) + } + + configs = append(configs, &remote.FileMeta{ + Name: e.Path, + Data: data, + }) + } + } + + return configs, nil +} + +// Status is supported by the Gitea driver. +func (c *Gitea) Status(u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { + client, err := c.newClientToken(u.Token) + if err != nil { + return err + } + + status := getStatus(b.Status) + desc := getDesc(b.Status) + + _, _, err = client.CreateStatus( + r.Owner, + r.Name, + b.Commit, + gitea.CreateStatusOption{ + State: status, + TargetURL: link, + Description: desc, + Context: c.Context, + }, + ) + + return err +} + +// Netrc returns a netrc file capable of authenticating Gitea requests and +// cloning Gitea repositories. The netrc will use the global machine account +// when configured. +func (c *Gitea) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { + if c.Password != "" { + return &model.Netrc{ + Login: c.Username, + Password: c.Password, + Machine: c.Machine, + }, nil + } + return &model.Netrc{ + Login: u.Login, + Password: u.Token, + Machine: c.Machine, + }, nil +} + +// Activate activates the repository by registering post-commit hooks with +// the Gitea repository. +func (c *Gitea) Activate(u *model.User, r *model.Repo, link string) error { + config := map[string]string{ + "url": link, + "secret": r.Hash, + "content_type": "json", + } + hook := gitea.CreateHookOption{ + Type: gitea.HookTypeGitea, + Config: config, + Events: []string{"push", "create", "pull_request"}, + Active: true, + } + + client, err := c.newClientToken(u.Token) + if err != nil { + return err + } + _, _, err = client.CreateRepoHook(r.Owner, r.Name, hook) + return err +} + +// Deactivate deactives the repository be removing repository push hooks from +// the Gitea repository. +func (c *Gitea) Deactivate(u *model.User, r *model.Repo, link string) error { + client, err := c.newClientToken(u.Token) + if err != nil { + return err + } + + hooks, _, err := client.ListRepoHooks(r.Owner, r.Name, gitea.ListHooksOptions{}) + if err != nil { + return err + } + + hook := matchingHooks(hooks, link) + if hook != nil { + _, err := client.DeleteRepoHook(r.Owner, r.Name, hook.ID) + return err + } + + return nil +} + +// Hook parses the incoming Gitea hook and returns the Repository and Build +// details. If the hook is unsupported nil values are returned. +func (c *Gitea) Hook(r *http.Request) (*model.Repo, *model.Build, error) { + return parseHook(r) +} + +// helper function to return the Gitea client with Token +func (c *Gitea) newClientToken(token string) (*gitea.Client, error) { + httpClient := &http.Client{} + if c.SkipVerify { + httpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + return gitea.NewClient(c.URL, gitea.SetToken(token), gitea.SetHTTPClient(httpClient)) } const ( @@ -106,361 +494,3 @@ func getDesc(status string) string { return DescFailure } } - -// New returns a Remote implementation that integrates with Gitea, an open -// source Git service written in Go. See https://gitea.io/ -func New(opts Opts) (remote.Remote, error) { - u, err := url.Parse(opts.URL) - if err != nil { - return nil, err - } - host, _, err := net.SplitHostPort(u.Host) - if err == nil { - u.Host = host - } - return &client{ - URL: opts.URL, - Context: opts.Context, - Machine: u.Host, - Username: opts.Username, - Password: opts.Password, - PrivateMode: opts.PrivateMode, - SkipVerify: opts.SkipVerify, - }, nil -} - -// TODO: dont create a new client for each func - -// Login authenticates an account with Gitea using basic authentication. The -// Gitea account details are returned when the user is successfully authenticated. -func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { - var ( - username = req.FormValue("username") - password = req.FormValue("password") - ) - - // if the username or password is empty we re-direct to the login screen. - if len(username) == 0 || len(password) == 0 { - http.Redirect(res, req, "/login/form", http.StatusSeeOther) - return nil, nil - } - - // Create Client with Basic Auth - client, err := c.newClientBasicAuth(username, password) - if err != nil { - return nil, err - } - - // since api does not return token secret, if drone token exists create new one - resp, err := client.DeleteAccessToken("drone") - if err != nil && !(resp != nil && resp.StatusCode == 404) { - return nil, err - } - - token, _, terr := client.CreateAccessToken( - gitea.CreateAccessTokenOption{Name: "drone"}, - ) - if terr != nil { - return nil, terr - } - accessToken := token.Token - - client, err = c.newClientToken(accessToken) - if err != nil { - return nil, err - } - account, _, err := client.GetUserInfo(username) - if err != nil { - return nil, err - } - - return &model.User{ - Token: accessToken, - Login: account.UserName, - Email: account.Email, - Avatar: expandAvatar(c.URL, account.AvatarURL), - }, nil -} - -// Auth is not supported by the Gitea driver. -func (c *client) Auth(token, secret string) (string, error) { - return "", fmt.Errorf("Not Implemented") -} - -// Teams is supported by the Gitea driver. -func (c *client) Teams(u *model.User) ([]*model.Team, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - orgs, _, err := client.ListMyOrgs(gitea.ListOrgsOptions{}) - if err != nil { - return nil, err - } - - var teams []*model.Team - for _, org := range orgs { - teams = append(teams, toTeam(org, c.URL)) - } - return teams, nil -} - -// TeamPerm is not supported by the Gitea driver. -func (c *client) TeamPerm(u *model.User, org string) (*model.Perm, error) { - return nil, nil -} - -// Repo returns the named Gitea repository. -func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - repo, _, err := client.GetRepo(owner, name) - if err != nil { - return nil, err - } - if c.PrivateMode { - repo.Private = true - } - return toRepo(repo, c.PrivateMode), nil -} - -// Repos returns a list of all repositories for the Gitea account, including -// organization repositories. -func (c *client) Repos(u *model.User) (repos []*model.Repo, err error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - // Gitea SDK forces us to read repo list paginated. - var page = 1 - for { - all, _, err := client.ListMyRepos( - gitea.ListReposOptions{ - ListOptions: gitea.ListOptions{ - Page: page, - PageSize: 50, // Gitea SDK limit per page. - }, - }, - ) - - // Gitea SDK does not return error when asking for - // non existing repos page (empty list is returned) - // so this should be safe. - if err != nil { - return repos, err - } - - for _, repo := range all { - repos = append(repos, toRepo(repo, c.PrivateMode)) - } - - // Check if no more repos are available; we don't test len(all) < 50 - // because of Gitea SDK bug https://gitea.com/gitea/go-sdk/issues/507. - if len(all) == 0 { - // Empty page returned - finish loop. - break - } else { - // Last page was not empty so more repos may be available - continue loop. - page = page + 1 - } - } - - return repos, nil -} - -// Perm returns the user permissions for the named Gitea repository. -func (c *client) Perm(u *model.User, owner, name string) (*model.Perm, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - repo, _, err := client.GetRepo(owner, name) - if err != nil { - return nil, err - } - return toPerm(repo.Permissions), nil -} - -// File fetches the file from the Gitea repository and returns its contents. -func (c *client) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - cfg, _, err := client.GetFile(r.Owner, r.Name, b.Commit, f) - return cfg, err -} - -func (c *client) Dir(u *model.User, r *model.Repo, b *model.Build, f string) ([]*remote.FileMeta, error) { - var configs []*remote.FileMeta - - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - // List files in repository. Path from root - tree, _, err := client.GetTrees(r.Owner, r.Name, b.Commit, true) - if err != nil { - return nil, err - } - - f = path.Clean(f) // We clean path and remove trailing slash - f += "/" + "*" // construct pattern for match i.e. file in subdir - for _, e := range tree.Entries { - // Filter path matching pattern and type file (blob) - if m, _ := filepath.Match(f, e.Path); m && e.Type == "blob" { - data, err := c.File(u, r, b, e.Path) - if err != nil { - return nil, fmt.Errorf("multi-pipeline cannot get %s: %s", e.Path, err) - } - - configs = append(configs, &remote.FileMeta{ - Name: e.Path, - Data: data, - }) - } - } - - return configs, nil -} - -// Status is supported by the Gitea driver. -func (c *client) Status(u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { - client, err := c.newClientToken(u.Token) - if err != nil { - return err - } - - status := getStatus(b.Status) - desc := getDesc(b.Status) - - _, _, err = client.CreateStatus( - r.Owner, - r.Name, - b.Commit, - gitea.CreateStatusOption{ - State: status, - TargetURL: link, - Description: desc, - Context: c.Context, - }, - ) - - return err -} - -// Netrc returns a netrc file capable of authenticating Gitea requests and -// cloning Gitea repositories. The netrc will use the global machine account -// when configured. -func (c *client) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - if c.Password != "" { - return &model.Netrc{ - Login: c.Username, - Password: c.Password, - Machine: c.Machine, - }, nil - } - return &model.Netrc{ - Login: u.Login, - Password: u.Token, - Machine: c.Machine, - }, nil -} - -// Activate activates the repository by registering post-commit hooks with -// the Gitea repository. -func (c *client) Activate(u *model.User, r *model.Repo, link string) error { - config := map[string]string{ - "url": link, - "secret": r.Hash, - "content_type": "json", - } - hook := gitea.CreateHookOption{ - Type: "gitea", - Config: config, - Events: []string{"push", "create", "pull_request"}, - Active: true, - } - - client, err := c.newClientToken(u.Token) - if err != nil { - return err - } - _, _, err = client.CreateRepoHook(r.Owner, r.Name, hook) - return err -} - -// Deactivate deactives the repository be removing repository push hooks from -// the Gitea repository. -func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error { - client, err := c.newClientToken(u.Token) - if err != nil { - return err - } - - hooks, _, err := client.ListRepoHooks(r.Owner, r.Name, gitea.ListHooksOptions{}) - if err != nil { - return err - } - - hook := matchingHooks(hooks, link) - if hook != nil { - _, err := client.DeleteRepoHook(r.Owner, r.Name, hook.ID) - return err - } - - return nil -} - -// Hook parses the incoming Gitea hook and returns the Repository and Build -// details. If the hook is unsupported nil values are returned. -func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - return parseHook(r) -} - -// helper function to return the Gitea client with Token -func (c *client) newClientToken(token string) (*gitea.Client, error) { - httpClient := &http.Client{} - if c.SkipVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - return gitea.NewClient(c.URL, gitea.SetToken(token), gitea.SetHTTPClient(httpClient)) -} - -// helper function to return the Gitea client with Basic Auth -func (c *client) newClientBasicAuth(username, password string) (*gitea.Client, error) { - httpClient := &http.Client{} - if c.SkipVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - return gitea.NewClient(c.URL, gitea.SetBasicAuth(username, password), gitea.SetHTTPClient(httpClient)) -} - -// helper function to return matching hooks. -func matchingHooks(hooks []*gitea.Hook, rawurl string) *gitea.Hook { - link, err := url.Parse(rawurl) - if err != nil { - return nil - } - for _, hook := range hooks { - if val, ok := hook.Config["url"]; ok { - hookurl, err := url.Parse(val) - if err == nil && hookurl.Host == link.Host { - return hook - } - } - } - return nil -} diff --git a/server/remote/gitea/gitea_oauth.go b/server/remote/gitea/gitea_oauth.go deleted file mode 100644 index f8ccf6c1d..000000000 --- a/server/remote/gitea/gitea_oauth.go +++ /dev/null @@ -1,422 +0,0 @@ -// Copyright 2018 Drone.IO Inc. -// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ -// -// 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. -// -// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k. - -package gitea - -import ( - "crypto/tls" - "fmt" - "net" - "net/http" - "net/url" - "path" - "path/filepath" - - "code.gitea.io/sdk/gitea" - "github.com/woodpecker-ci/woodpecker/model" - "github.com/woodpecker-ci/woodpecker/server" - "github.com/woodpecker-ci/woodpecker/server/remote" - - "golang.org/x/oauth2" -) - -const ( - authorizeTokenURL = "%s/login/oauth/authorize" - accessTokenURL = "%s/login/oauth/access_token" -) - -type oauthclient struct { - URL string - Context string - Machine string - Client string - Secret string - Username string - Password string - PrivateMode bool - SkipVerify bool -} - -// TODO: merge with gitea.go (or drop basic auth) - -// NewOauth returns a Remote implementation that integrates with Gitea, an open -// source Git service written in Go. See https://gitea.io/ -func NewOauth(opts Opts) (remote.Remote, error) { - u, err := url.Parse(opts.URL) - if err != nil { - return nil, err - } - host, _, err := net.SplitHostPort(u.Host) - if err == nil { - u.Host = host - } - return &oauthclient{ - URL: opts.URL, - Context: opts.Context, - Machine: u.Host, - Client: opts.Client, - Secret: opts.Secret, - Username: opts.Username, - Password: opts.Password, - PrivateMode: opts.PrivateMode, - SkipVerify: opts.SkipVerify, - }, nil -} - -// Login authenticates an account with Gitea using basic authentication. The -// Gitea account details are returned when the user is successfully authenticated. -func (c *oauthclient) Login(w http.ResponseWriter, req *http.Request) (*model.User, error) { - config := &oauth2.Config{ - ClientID: c.Client, - ClientSecret: c.Secret, - Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf(authorizeTokenURL, c.URL), - TokenURL: fmt.Sprintf(accessTokenURL, c.URL), - }, - RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.Host), - } - - // get the OAuth errors - if err := req.FormValue("error"); err != "" { - 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("drone"), http.StatusSeeOther) - return nil, nil - } - - token, err := config.Exchange(oauth2.NoContext, code) - if err != nil { - return nil, err - } - - client, err := c.newClientToken(token.AccessToken) - if err != nil { - return nil, err - } - account, _, err := client.GetMyUserInfo() - if err != nil { - return nil, err - } - - return &model.User{ - Token: token.AccessToken, - Secret: token.RefreshToken, - Expiry: token.Expiry.UTC().Unix(), - Login: account.UserName, - Email: account.Email, - Avatar: expandAvatar(c.URL, account.AvatarURL), - }, nil -} - -// Auth uses the Gitea oauth2 access token and refresh token to authenticate -// a session and return the Gitea account login. -func (c *oauthclient) Auth(token, secret string) (string, error) { - client, err := c.newClientToken(token) - if err != nil { - return "", err - } - user, _, err := client.GetMyUserInfo() - if err != nil { - return "", err - } - return user.UserName, nil -} - -// Refresh refreshes the Gitea oauth2 access token. If the token is -// refreshed the user is updated and a true value is returned. -func (c *oauthclient) Refresh(user *model.User) (bool, error) { - config := &oauth2.Config{ - ClientID: c.Client, - ClientSecret: c.Secret, - Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf(authorizeTokenURL, c.URL), - TokenURL: fmt.Sprintf(accessTokenURL, c.URL), - }, - } - source := config.TokenSource( - oauth2.NoContext, &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 -} - -// Teams is supported by the Gitea driver. -func (c *oauthclient) Teams(u *model.User) ([]*model.Team, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - orgs, _, err := client.ListMyOrgs(gitea.ListOrgsOptions{}) - if err != nil { - return nil, err - } - - var teams []*model.Team - for _, org := range orgs { - teams = append(teams, toTeam(org, c.URL)) - } - return teams, nil -} - -// TeamPerm is not supported by the Gitea driver. -func (c *oauthclient) TeamPerm(u *model.User, org string) (*model.Perm, error) { - return nil, nil -} - -// Repo returns the named Gitea repository. -func (c *oauthclient) Repo(u *model.User, owner, name string) (*model.Repo, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - repo, _, err := client.GetRepo(owner, name) - if err != nil { - return nil, err - } - if c.PrivateMode { - repo.Private = true - } - return toRepo(repo, c.PrivateMode), nil -} - -// Repos returns a list of all repositories for the Gitea account, including -// organization repositories. -func (c *oauthclient) Repos(u *model.User) ([]*model.Repo, error) { - repos := []*model.Repo{} - - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - // Gitea SDK forces us to read repo list paginated. - var page = 1 - for { - all, _, err := client.ListMyRepos( - gitea.ListReposOptions{ - ListOptions: gitea.ListOptions{ - Page: page, - PageSize: 50, // Gitea SDK limit per page. - }, - }, - ) - - // Gitea SDK does not return error when asking for - // non existing repos page (empty list is returned) - // so this should be safe. - if err != nil { - return repos, err - } - - for _, repo := range all { - repos = append(repos, toRepo(repo, c.PrivateMode)) - } - - // Check if no more repos are available; we don't test len(all) < 50 - // because of Gitea SDK bug https://gitea.com/gitea/go-sdk/issues/507. - if len(all) == 0 { - // Empty page returned - finish loop. - break - } else { - // Last page was not empty so more repos may be available - continue loop. - page = page + 1 - } - } - - return repos, nil -} - -// Perm returns the user permissions for the named Gitea repository. -func (c *oauthclient) Perm(u *model.User, owner, name string) (*model.Perm, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - repo, _, err := client.GetRepo(owner, name) - if err != nil { - return nil, err - } - return toPerm(repo.Permissions), nil -} - -// File fetches the file from the Gitea repository and returns its contents. -func (c *oauthclient) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - cfg, _, err := client.GetFile(r.Owner, r.Name, b.Commit, f) - return cfg, err -} - -func (c *oauthclient) Dir(u *model.User, r *model.Repo, b *model.Build, f string) ([]*remote.FileMeta, error) { - var configs []*remote.FileMeta - - client, err := c.newClientToken(u.Token) - if err != nil { - return nil, err - } - - // List files in repository. Path from root - tree, _, err := client.GetTrees(r.Owner, r.Name, b.Commit, true) - if err != nil { - return nil, err - } - - f = path.Clean(f) // We clean path and remove trailing slash - f += "/" + "*" // construct pattern for match i.e. file in subdir - for _, e := range tree.Entries { - // Filter path matching pattern and type file (blob) - if m, _ := filepath.Match(f, e.Path); m && e.Type == "blob" { - data, err := c.File(u, r, b, e.Path) - if err != nil { - return nil, fmt.Errorf("multi-pipeline cannot get %s: %s", e.Path, err) - } - - configs = append(configs, &remote.FileMeta{ - Name: e.Path, - Data: data, - }) - } - } - - return configs, nil -} - -// Status is supported by the Gitea driver. -func (c *oauthclient) Status(u *model.User, r *model.Repo, b *model.Build, link string, proc *model.Proc) error { - client, err := c.newClientToken(u.Token) - if err != nil { - return err - } - - status := getStatus(b.Status) - desc := getDesc(b.Status) - - _, _, err = client.CreateStatus( - r.Owner, - r.Name, - b.Commit, - gitea.CreateStatusOption{ - State: status, - TargetURL: link, - Description: desc, - Context: c.Context, - }, - ) - - return err -} - -// Netrc returns a netrc file capable of authenticating Gitea requests and -// cloning Gitea repositories. The netrc will use the global machine account -// when configured. -func (c *oauthclient) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - if c.Password != "" { - return &model.Netrc{ - Login: c.Username, - Password: c.Password, - Machine: c.Machine, - }, nil - } - return &model.Netrc{ - Login: u.Login, - Password: u.Token, - Machine: c.Machine, - }, nil -} - -// Activate activates the repository by registering post-commit hooks with -// the Gitea repository. -func (c *oauthclient) Activate(u *model.User, r *model.Repo, link string) error { - config := map[string]string{ - "url": link, - "secret": r.Hash, - "content_type": "json", - } - hook := gitea.CreateHookOption{ - Type: "gitea", - Config: config, - Events: []string{"push", "create", "pull_request"}, - Active: true, - } - - client, err := c.newClientToken(u.Token) - if err != nil { - return err - } - _, _, err = client.CreateRepoHook(r.Owner, r.Name, hook) - return err -} - -// Deactivate deactives the repository be removing repository push hooks from -// the Gitea repository. -func (c *oauthclient) Deactivate(u *model.User, r *model.Repo, link string) error { - client, err := c.newClientToken(u.Token) - if err != nil { - return err - } - - hooks, _, err := client.ListRepoHooks(r.Owner, r.Name, gitea.ListHooksOptions{}) - if err != nil { - return err - } - - hook := matchingHooks(hooks, link) - if hook != nil { - _, err := client.DeleteRepoHook(r.Owner, r.Name, hook.ID) - return err - } - - return nil -} - -// Hook parses the incoming Gitea hook and returns the Repository and Build -// details. If the hook is unsupported nil values are returned. -func (c *oauthclient) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - return parseHook(r) -} - -// helper function to return the Gitea client with Token -func (c *oauthclient) newClientToken(token string) (*gitea.Client, error) { - httpClient := &http.Client{} - if c.SkipVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - return gitea.NewClient(c.URL, gitea.SetToken(token), gitea.SetHTTPClient(httpClient)) -} diff --git a/server/remote/gitea/gitea_test.go b/server/remote/gitea/gitea_test.go index a5a453d66..77917e162 100644 --- a/server/remote/gitea/gitea_test.go +++ b/server/remote/gitea/gitea_test.go @@ -50,13 +50,13 @@ func Test_gitea(t *testing.T) { SkipVerify: true, PrivateMode: true, }) - g.Assert(remote.(*client).URL).Equal("http://localhost:8080") - g.Assert(remote.(*client).Context).Equal("continuous-integration/test") - g.Assert(remote.(*client).Machine).Equal("localhost") - g.Assert(remote.(*client).Username).Equal("someuser") - g.Assert(remote.(*client).Password).Equal("password") - g.Assert(remote.(*client).SkipVerify).Equal(true) - g.Assert(remote.(*client).PrivateMode).Equal(true) + g.Assert(remote.(*Gitea).URL).Equal("http://localhost:8080") + g.Assert(remote.(*Gitea).Context).Equal("continuous-integration/test") + g.Assert(remote.(*Gitea).Machine).Equal("localhost") + g.Assert(remote.(*Gitea).Username).Equal("someuser") + g.Assert(remote.(*Gitea).Password).Equal("password") + g.Assert(remote.(*Gitea).SkipVerify).Equal(true) + g.Assert(remote.(*Gitea).PrivateMode).Equal(true) }) g.It("Should handle malformed url", func() { _, err := New(Opts{URL: "%gh&%ij"}) diff --git a/server/remote/gitea/helper.go b/server/remote/gitea/helper.go index 3932c66fc..7927bead6 100644 --- a/server/remote/gitea/helper.go +++ b/server/remote/gitea/helper.go @@ -243,3 +243,20 @@ func expandAvatar(repo, rawurl string) string { return aurl.String() } + +// helper function to return matching hooks. +func matchingHooks(hooks []*gitea.Hook, rawurl string) *gitea.Hook { + link, err := url.Parse(rawurl) + if err != nil { + return nil + } + for _, hook := range hooks { + if val, ok := hook.Config["url"]; ok { + hookurl, err := url.Parse(val) + if err == nil && hookurl.Host == link.Host { + return hook + } + } + } + return nil +}