Skip to content

Commit

Permalink
Reverse proxy authentication to Woodpeck and Gitea
Browse files Browse the repository at this point in the history
This mod adds option to authenticate user using HTTP header set by
reverse proxy. It forwards specified HTTP header with authenticated
username in requests to Gitea.

Requirements:

* Gitea must be configured for reverse proxy authentication and
  must accept HTTP header auth in API calls
  (go-gitea/gitea#15119).

To enable set the following variables in woodpecker server
environment (woodpecker running behind reverse proxy):

* internal woodpecker server URL, i.e.:

  WOODPECKER_HOST_INTERNAL=http://192.168.1.100:8000

* enable reverse proxy auth in woodpecker and forwarding auth
  header to gitea:

  WOODPECKER_GITEA_REV_PROXY_AUTH=true

* set name of header with authenticated username (set by
  reverse proxy), i.e.:

  WOODPECKER_GITEA_REV_PROXY_AUTH_HEADER=X-Forward-Username

Related: go-gitea/gitea#15119
Author-Change-Id: IB#1107569
  • Loading branch information
pboguslawski committed Mar 8, 2022
1 parent 7a4c6d3 commit f3e08a2
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 28 deletions.
18 changes: 18 additions & 0 deletions cmd/server/flags.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2019 Laszlo Fogas
// Copyright 2022 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.
Expand All @@ -11,6 +12,8 @@
// 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 main

Expand Down Expand Up @@ -44,6 +47,11 @@ var flags = []cli.Flag{
Name: "server-host",
Usage: "server fully qualified url (<scheme>://<host>)",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_HOST_INTERNAL"},
Name: "server-host-internal",
Usage: "server internal fully qualified url (<scheme>://<host>)",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_SERVER_ADDR"},
Name: "server-addr",
Expand Down Expand Up @@ -146,6 +154,16 @@ var flags = []cli.Flag{
Name: "keepalive-min-time",
Usage: "server-side enforcement policy on the minimum amount of time a client should wait before sending a keepalive ping.",
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_GITEA_REV_PROXY_AUTH"},
Name: "gitea-rev-proxy-auth",
Usage: "enable gitea authentication using HTTP header specified in WOODPECKER_GITEA_REV_PROXY_AUTH_HEADER",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_GITEA_REV_PROXY_AUTH_HEADER"},
Name: "gitea-rev-proxy-auth-header",
Usage: "HTTP header with gitea authenticated user login",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_SECRET_ENDPOINT"},
Name: "secret-service",
Expand Down
18 changes: 18 additions & 0 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2018 Drone.IO Inc.
// Copyright 2022 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.
Expand All @@ -11,6 +12,8 @@
// 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 main

Expand Down Expand Up @@ -99,6 +102,18 @@ func run(c *cli.Context) error {
)
}

if strings.Contains(c.String("server-host-internal"), "://localhost") {
log.Warn().Msg(
"WOODPECKER_HOST_INTERNAL should probably be publicly accessible (not localhost)",
)
}

if strings.HasSuffix(c.String("server-host-internal"), "/") {
log.Fatal().Msg(
"WOODPECKER_HOST_INTERNAL must not have trailing slash",
)
}

_remote, err := setupRemote(c)
if err != nil {
log.Fatal().Err(err).Msg("")
Expand Down Expand Up @@ -290,6 +305,9 @@ func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) {
server.Config.Server.Key = c.String("server-key")
server.Config.Server.Pass = c.String("agent-secret")
server.Config.Server.Host = c.String("server-host")
server.Config.Server.HostInternal = c.String("server-host-internal")
server.Config.Server.RevProxyAuth = c.Bool("gitea-rev-proxy-auth")

if c.IsSet("server-dev-oauth-host") {
server.Config.Server.OAuthHost = c.String("server-dev-oauth-host")
} else {
Expand Down
13 changes: 9 additions & 4 deletions cmd/server/setup.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2018 Drone.IO Inc.
// Copyright 2022 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.
Expand All @@ -11,6 +12,8 @@
// 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 main

Expand Down Expand Up @@ -228,10 +231,12 @@ func setupGitea(c *cli.Context) (remote.Remote, error) {
return nil, err
}
opts := gitea.Opts{
URL: strings.TrimRight(server.String(), "/"),
Client: c.String("gitea-client"),
Secret: c.String("gitea-secret"),
SkipVerify: c.Bool("gitea-skip-verify"),
URL: strings.TrimRight(server.String(), "/"),
Client: c.String("gitea-client"),
Secret: c.String("gitea-secret"),
SkipVerify: c.Bool("gitea-skip-verify"),
RevProxyAuth: c.Bool("gitea-rev-proxy-auth"),
RevProxyAuthHeader: c.String("gitea-rev-proxy-auth-header"),
}
if len(opts.URL) == 0 {
log.Fatal().Msg("WOODPECKER_GITEA_URL must be set")
Expand Down
22 changes: 20 additions & 2 deletions server/api/repo.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2018 Drone.IO Inc.
// Copyright 2022 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.
Expand All @@ -11,6 +12,8 @@
// 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 api

Expand Down Expand Up @@ -79,9 +82,14 @@ func PostRepo(c *gin.Context) {
return
}

host := server.Config.Server.Host
if server.Config.Server.HostInternal != "" {
host = server.Config.Server.HostInternal
}

link := fmt.Sprintf(
"%s/hook?access_token=%s",
server.Config.Server.Host,
host,
sig,
)

Expand Down Expand Up @@ -218,7 +226,11 @@ func DeleteRepo(c *gin.Context) {
}
}

if err := server.Config.Services.Remote.Deactivate(c, user, repo, server.Config.Server.Host); err != nil {
host := server.Config.Server.Host
if server.Config.Server.HostInternal != "" {
host = server.Config.Server.HostInternal
}
if err := server.Config.Services.Remote.Deactivate(c, user, repo, host); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
Expand All @@ -241,6 +253,9 @@ func RepairRepo(c *gin.Context) {

// reconstruct the link
host := server.Config.Server.Host
if server.Config.Server.HostInternal != "" {
host = server.Config.Server.HostInternal
}
link := fmt.Sprintf(
"%s/hook?access_token=%s",
host,
Expand Down Expand Up @@ -335,6 +350,9 @@ func MoveRepo(c *gin.Context) {

// reconstruct the link
host := server.Config.Server.Host
if server.Config.Server.HostInternal != "" {
host = server.Config.Server.HostInternal
}
link := fmt.Sprintf(
"%s/hook?access_token=%s",
host,
Expand Down
4 changes: 3 additions & 1 deletion server/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2018 Drone.IO Inc.
// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
// Copyright 2022 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.
Expand Down Expand Up @@ -53,6 +53,8 @@ var Config = struct {
Cert string
OAuthHost string
Host string
HostInternal string
RevProxyAuth bool
Port string
Pass string
Docs string
Expand Down
131 changes: 114 additions & 17 deletions server/remote/gitea/gitea.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2018 Drone.IO Inc.
// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
// Copyright 2022 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.
Expand Down Expand Up @@ -43,19 +43,24 @@ const (
)

type Gitea struct {
URL string
Machine string
ClientID string
ClientSecret string
SkipVerify bool
URL string
Machine string
ClientID string
ClientSecret string
SkipVerify bool
RevProxyAuth bool
RevProxyAuthHeader string
RevProxyAuthHeaderValue string
}

// Opts defines configuration options.
type Opts struct {
URL string // Gitea server url.
Client string // OAuth2 Client ID
Secret string // OAuth2 Client Secret
SkipVerify bool // Skip ssl verification.
URL string // Gitea server url.
Client string // OAuth2 Client ID
Secret string // OAuth2 Client Secret
SkipVerify bool // Skip ssl verification.
RevProxyAuth bool // Enable reverse proxy authentication using RevProxyAuthHeader.
RevProxyAuthHeader string // Name of HTTP header with username for reverse proxy authentication.
}

// New returns a Remote implementation that integrates with Gitea,
Expand All @@ -70,17 +75,64 @@ func New(opts Opts) (remote.Remote, error) {
u.Host = host
}
return &Gitea{
URL: opts.URL,
Machine: u.Host,
ClientID: opts.Client,
ClientSecret: opts.Secret,
SkipVerify: opts.SkipVerify,
URL: opts.URL,
Machine: u.Host,
ClientID: opts.Client,
ClientSecret: opts.Secret,
SkipVerify: opts.SkipVerify,
RevProxyAuth: opts.RevProxyAuth,
RevProxyAuthHeader: opts.RevProxyAuthHeader,
}, nil
}

// Login authenticates an account with Gitea using basic authentication. The
// Gitea account details are returned when the user is successfully authenticated.
// Login authenticates an account with Gitea. The Gitea account details
// are returned when the user is successfully authenticated.
func (c *Gitea) Login(ctx context.Context, w http.ResponseWriter, req *http.Request) (*model.User, error) {
// Authenticate using reverse proxy header if enabled.
if c.RevProxyAuth {
c.ClientID = req.Header.Get(c.RevProxyAuthHeader)
c.RevProxyAuthHeaderValue = c.ClientID
c.ClientSecret = ""

client, err := c.newClientBasicAuth(ctx, c.ClientID, "")
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(ctx, accessToken)
if err != nil {
return nil, err
}

account, _, err := client.GetMyUserInfo()
if err != nil {
return nil, err
}

return &model.User{
Token: accessToken,
Secret: "",
Expiry: 0,
Login: account.UserName,
Email: account.Email,
Avatar: expandAvatar(c.URL, account.AvatarURL),
}, nil
}

// oAuth2 authentication.
config := &oauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Expand Down Expand Up @@ -443,6 +495,22 @@ func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.
return parseHook(r)
}

// authTransport forwards authentication HTTP header to gitea.
type authTransport struct {
headerName string
headerValue string
underlyingTransport http.RoundTripper
}

func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Add(t.headerName, t.headerValue)
if t.underlyingTransport != nil {
return t.underlyingTransport.RoundTrip(req)
} else {
return http.DefaultTransport.RoundTrip(req)
}
}

// helper function to return the Gitea client with Token
func (c *Gitea) newClientToken(ctx context.Context, token string) (*gitea.Client, error) {
httpClient := &http.Client{}
Expand All @@ -451,9 +519,38 @@ func (c *Gitea) newClientToken(ctx context.Context, token string) (*gitea.Client
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
// Forward authentication header in every HTTP request to Gitea
// in reverse proxy authentication mode.
if c.RevProxyAuth {
httpClient.Transport = &authTransport{
headerName: c.RevProxyAuthHeader,
headerValue: c.RevProxyAuthHeaderValue,
underlyingTransport: httpClient.Transport,
}
}
return gitea.NewClient(c.URL, gitea.SetToken(token), gitea.SetHTTPClient(httpClient), gitea.SetContext(ctx))
}

// helper function to return the Gitea client with Basic Auth
func (c *Gitea) newClientBasicAuth(ctx context.Context, username, password string) (*gitea.Client, error) {
httpClient := &http.Client{}
if c.SkipVerify {
httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
// Forward authentication header in every HTTP request to Gitea
// in reverse proxy authentication mode.
if c.RevProxyAuth {
httpClient.Transport = &authTransport{
headerName: c.RevProxyAuthHeader,
headerValue: c.RevProxyAuthHeaderValue,
underlyingTransport: httpClient.Transport,
}
}
return gitea.NewClient(c.URL, gitea.SetBasicAuth(username, password), gitea.SetHTTPClient(httpClient), gitea.SetContext(ctx))
}

// getStatus is a helper function that converts a Woodpecker
// status to a Gitea status.
func getStatus(status model.StatusValue) gitea.StatusState {
Expand Down
Loading

0 comments on commit f3e08a2

Please sign in to comment.