Skip to content

Commit f3e08a2

Browse files
committed
Reverse proxy authentication to Woodpeck and Gitea
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
1 parent 7a4c6d3 commit f3e08a2

8 files changed

Lines changed: 197 additions & 28 deletions

File tree

cmd/server/flags.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2019 Laszlo Fogas
2+
// Copyright 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
45
// you may not use this file except in compliance with the License.
@@ -11,6 +12,8 @@
1112
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1213
// See the License for the specific language governing permissions and
1314
// limitations under the License.
15+
//
16+
// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k.
1417

1518
package main
1619

@@ -44,6 +47,11 @@ var flags = []cli.Flag{
4447
Name: "server-host",
4548
Usage: "server fully qualified url (<scheme>://<host>)",
4649
},
50+
&cli.StringFlag{
51+
EnvVars: []string{"WOODPECKER_HOST_INTERNAL"},
52+
Name: "server-host-internal",
53+
Usage: "server internal fully qualified url (<scheme>://<host>)",
54+
},
4755
&cli.StringFlag{
4856
EnvVars: []string{"WOODPECKER_SERVER_ADDR"},
4957
Name: "server-addr",
@@ -146,6 +154,16 @@ var flags = []cli.Flag{
146154
Name: "keepalive-min-time",
147155
Usage: "server-side enforcement policy on the minimum amount of time a client should wait before sending a keepalive ping.",
148156
},
157+
&cli.BoolFlag{
158+
EnvVars: []string{"WOODPECKER_GITEA_REV_PROXY_AUTH"},
159+
Name: "gitea-rev-proxy-auth",
160+
Usage: "enable gitea authentication using HTTP header specified in WOODPECKER_GITEA_REV_PROXY_AUTH_HEADER",
161+
},
162+
&cli.StringFlag{
163+
EnvVars: []string{"WOODPECKER_GITEA_REV_PROXY_AUTH_HEADER"},
164+
Name: "gitea-rev-proxy-auth-header",
165+
Usage: "HTTP header with gitea authenticated user login",
166+
},
149167
&cli.StringFlag{
150168
EnvVars: []string{"WOODPECKER_SECRET_ENDPOINT"},
151169
Name: "secret-service",

cmd/server/server.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2018 Drone.IO Inc.
2+
// Copyright 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
45
// you may not use this file except in compliance with the License.
@@ -11,6 +12,8 @@
1112
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1213
// See the License for the specific language governing permissions and
1314
// limitations under the License.
15+
//
16+
// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k.
1417

1518
package main
1619

@@ -99,6 +102,18 @@ func run(c *cli.Context) error {
99102
)
100103
}
101104

105+
if strings.Contains(c.String("server-host-internal"), "://localhost") {
106+
log.Warn().Msg(
107+
"WOODPECKER_HOST_INTERNAL should probably be publicly accessible (not localhost)",
108+
)
109+
}
110+
111+
if strings.HasSuffix(c.String("server-host-internal"), "/") {
112+
log.Fatal().Msg(
113+
"WOODPECKER_HOST_INTERNAL must not have trailing slash",
114+
)
115+
}
116+
102117
_remote, err := setupRemote(c)
103118
if err != nil {
104119
log.Fatal().Err(err).Msg("")
@@ -290,6 +305,9 @@ func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) {
290305
server.Config.Server.Key = c.String("server-key")
291306
server.Config.Server.Pass = c.String("agent-secret")
292307
server.Config.Server.Host = c.String("server-host")
308+
server.Config.Server.HostInternal = c.String("server-host-internal")
309+
server.Config.Server.RevProxyAuth = c.Bool("gitea-rev-proxy-auth")
310+
293311
if c.IsSet("server-dev-oauth-host") {
294312
server.Config.Server.OAuthHost = c.String("server-dev-oauth-host")
295313
} else {

cmd/server/setup.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2018 Drone.IO Inc.
2+
// Copyright 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
45
// you may not use this file except in compliance with the License.
@@ -11,6 +12,8 @@
1112
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1213
// See the License for the specific language governing permissions and
1314
// limitations under the License.
15+
//
16+
// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k.
1417

1518
package main
1619

@@ -228,10 +231,12 @@ func setupGitea(c *cli.Context) (remote.Remote, error) {
228231
return nil, err
229232
}
230233
opts := gitea.Opts{
231-
URL: strings.TrimRight(server.String(), "/"),
232-
Client: c.String("gitea-client"),
233-
Secret: c.String("gitea-secret"),
234-
SkipVerify: c.Bool("gitea-skip-verify"),
234+
URL: strings.TrimRight(server.String(), "/"),
235+
Client: c.String("gitea-client"),
236+
Secret: c.String("gitea-secret"),
237+
SkipVerify: c.Bool("gitea-skip-verify"),
238+
RevProxyAuth: c.Bool("gitea-rev-proxy-auth"),
239+
RevProxyAuthHeader: c.String("gitea-rev-proxy-auth-header"),
235240
}
236241
if len(opts.URL) == 0 {
237242
log.Fatal().Msg("WOODPECKER_GITEA_URL must be set")

server/api/repo.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2018 Drone.IO Inc.
2+
// Copyright 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
45
// you may not use this file except in compliance with the License.
@@ -11,6 +12,8 @@
1112
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1213
// See the License for the specific language governing permissions and
1314
// limitations under the License.
15+
//
16+
// This file has been modified by Informatyka Boguslawski sp. z o.o. sp.k.
1417

1518
package api
1619

@@ -79,9 +82,14 @@ func PostRepo(c *gin.Context) {
7982
return
8083
}
8184

85+
host := server.Config.Server.Host
86+
if server.Config.Server.HostInternal != "" {
87+
host = server.Config.Server.HostInternal
88+
}
89+
8290
link := fmt.Sprintf(
8391
"%s/hook?access_token=%s",
84-
server.Config.Server.Host,
92+
host,
8593
sig,
8694
)
8795

@@ -218,7 +226,11 @@ func DeleteRepo(c *gin.Context) {
218226
}
219227
}
220228

221-
if err := server.Config.Services.Remote.Deactivate(c, user, repo, server.Config.Server.Host); err != nil {
229+
host := server.Config.Server.Host
230+
if server.Config.Server.HostInternal != "" {
231+
host = server.Config.Server.HostInternal
232+
}
233+
if err := server.Config.Services.Remote.Deactivate(c, user, repo, host); err != nil {
222234
_ = c.AbortWithError(http.StatusInternalServerError, err)
223235
return
224236
}
@@ -241,6 +253,9 @@ func RepairRepo(c *gin.Context) {
241253

242254
// reconstruct the link
243255
host := server.Config.Server.Host
256+
if server.Config.Server.HostInternal != "" {
257+
host = server.Config.Server.HostInternal
258+
}
244259
link := fmt.Sprintf(
245260
"%s/hook?access_token=%s",
246261
host,
@@ -335,6 +350,9 @@ func MoveRepo(c *gin.Context) {
335350

336351
// reconstruct the link
337352
host := server.Config.Server.Host
353+
if server.Config.Server.HostInternal != "" {
354+
host = server.Config.Server.HostInternal
355+
}
338356
link := fmt.Sprintf(
339357
"%s/hook?access_token=%s",
340358
host,

server/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright 2018 Drone.IO Inc.
2-
// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
2+
// Copyright 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -53,6 +53,8 @@ var Config = struct {
5353
Cert string
5454
OAuthHost string
5555
Host string
56+
HostInternal string
57+
RevProxyAuth bool
5658
Port string
5759
Pass string
5860
Docs string

server/remote/gitea/gitea.go

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright 2018 Drone.IO Inc.
2-
// Copyright 2021 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
2+
// Copyright 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -43,19 +43,24 @@ const (
4343
)
4444

4545
type Gitea struct {
46-
URL string
47-
Machine string
48-
ClientID string
49-
ClientSecret string
50-
SkipVerify bool
46+
URL string
47+
Machine string
48+
ClientID string
49+
ClientSecret string
50+
SkipVerify bool
51+
RevProxyAuth bool
52+
RevProxyAuthHeader string
53+
RevProxyAuthHeaderValue string
5154
}
5255

5356
// Opts defines configuration options.
5457
type Opts struct {
55-
URL string // Gitea server url.
56-
Client string // OAuth2 Client ID
57-
Secret string // OAuth2 Client Secret
58-
SkipVerify bool // Skip ssl verification.
58+
URL string // Gitea server url.
59+
Client string // OAuth2 Client ID
60+
Secret string // OAuth2 Client Secret
61+
SkipVerify bool // Skip ssl verification.
62+
RevProxyAuth bool // Enable reverse proxy authentication using RevProxyAuthHeader.
63+
RevProxyAuthHeader string // Name of HTTP header with username for reverse proxy authentication.
5964
}
6065

6166
// New returns a Remote implementation that integrates with Gitea,
@@ -70,17 +75,64 @@ func New(opts Opts) (remote.Remote, error) {
7075
u.Host = host
7176
}
7277
return &Gitea{
73-
URL: opts.URL,
74-
Machine: u.Host,
75-
ClientID: opts.Client,
76-
ClientSecret: opts.Secret,
77-
SkipVerify: opts.SkipVerify,
78+
URL: opts.URL,
79+
Machine: u.Host,
80+
ClientID: opts.Client,
81+
ClientSecret: opts.Secret,
82+
SkipVerify: opts.SkipVerify,
83+
RevProxyAuth: opts.RevProxyAuth,
84+
RevProxyAuthHeader: opts.RevProxyAuthHeader,
7885
}, nil
7986
}
8087

81-
// Login authenticates an account with Gitea using basic authentication. The
82-
// Gitea account details are returned when the user is successfully authenticated.
88+
// Login authenticates an account with Gitea. The Gitea account details
89+
// are returned when the user is successfully authenticated.
8390
func (c *Gitea) Login(ctx context.Context, w http.ResponseWriter, req *http.Request) (*model.User, error) {
91+
// Authenticate using reverse proxy header if enabled.
92+
if c.RevProxyAuth {
93+
c.ClientID = req.Header.Get(c.RevProxyAuthHeader)
94+
c.RevProxyAuthHeaderValue = c.ClientID
95+
c.ClientSecret = ""
96+
97+
client, err := c.newClientBasicAuth(ctx, c.ClientID, "")
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
// Since api does not return token secret, if drone token exists create new one.
103+
resp, err := client.DeleteAccessToken("drone")
104+
if err != nil && !(resp != nil && resp.StatusCode == 404) {
105+
return nil, err
106+
}
107+
token, _, terr := client.CreateAccessToken(
108+
gitea.CreateAccessTokenOption{Name: "drone"},
109+
)
110+
if terr != nil {
111+
return nil, terr
112+
}
113+
accessToken := token.Token
114+
115+
client, err = c.newClientToken(ctx, accessToken)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
account, _, err := client.GetMyUserInfo()
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
return &model.User{
126+
Token: accessToken,
127+
Secret: "",
128+
Expiry: 0,
129+
Login: account.UserName,
130+
Email: account.Email,
131+
Avatar: expandAvatar(c.URL, account.AvatarURL),
132+
}, nil
133+
}
134+
135+
// oAuth2 authentication.
84136
config := &oauth2.Config{
85137
ClientID: c.ClientID,
86138
ClientSecret: c.ClientSecret,
@@ -443,6 +495,22 @@ func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.
443495
return parseHook(r)
444496
}
445497

498+
// authTransport forwards authentication HTTP header to gitea.
499+
type authTransport struct {
500+
headerName string
501+
headerValue string
502+
underlyingTransport http.RoundTripper
503+
}
504+
505+
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
506+
req.Header.Add(t.headerName, t.headerValue)
507+
if t.underlyingTransport != nil {
508+
return t.underlyingTransport.RoundTrip(req)
509+
} else {
510+
return http.DefaultTransport.RoundTrip(req)
511+
}
512+
}
513+
446514
// helper function to return the Gitea client with Token
447515
func (c *Gitea) newClientToken(ctx context.Context, token string) (*gitea.Client, error) {
448516
httpClient := &http.Client{}
@@ -451,9 +519,38 @@ func (c *Gitea) newClientToken(ctx context.Context, token string) (*gitea.Client
451519
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
452520
}
453521
}
522+
// Forward authentication header in every HTTP request to Gitea
523+
// in reverse proxy authentication mode.
524+
if c.RevProxyAuth {
525+
httpClient.Transport = &authTransport{
526+
headerName: c.RevProxyAuthHeader,
527+
headerValue: c.RevProxyAuthHeaderValue,
528+
underlyingTransport: httpClient.Transport,
529+
}
530+
}
454531
return gitea.NewClient(c.URL, gitea.SetToken(token), gitea.SetHTTPClient(httpClient), gitea.SetContext(ctx))
455532
}
456533

534+
// helper function to return the Gitea client with Basic Auth
535+
func (c *Gitea) newClientBasicAuth(ctx context.Context, username, password string) (*gitea.Client, error) {
536+
httpClient := &http.Client{}
537+
if c.SkipVerify {
538+
httpClient.Transport = &http.Transport{
539+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
540+
}
541+
}
542+
// Forward authentication header in every HTTP request to Gitea
543+
// in reverse proxy authentication mode.
544+
if c.RevProxyAuth {
545+
httpClient.Transport = &authTransport{
546+
headerName: c.RevProxyAuthHeader,
547+
headerValue: c.RevProxyAuthHeaderValue,
548+
underlyingTransport: httpClient.Transport,
549+
}
550+
}
551+
return gitea.NewClient(c.URL, gitea.SetBasicAuth(username, password), gitea.SetHTTPClient(httpClient), gitea.SetContext(ctx))
552+
}
553+
457554
// getStatus is a helper function that converts a Woodpecker
458555
// status to a Gitea status.
459556
func getStatus(status model.StatusValue) gitea.StatusState {

0 commit comments

Comments
 (0)