Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit 26d21f5

Browse files
committed
bundle-server: introduce 'authorize' step to 'serve'
Add an 'authorize' function to the bundle web server for managing access to bundle server content. This function returns one of two states: - Deny, which indicates that bundle server content should not be served. This is configured with the desired 4XX response code and (optional) headers. Invoking 'ApplyResult()' with this result will fill in the response information and trigger 'serve' to exit immediately. - Allow, indicating that the requstor may access the requested resource. This does not return an immediate response, but if headers are specified, they will be added to the eventual response. After applying this result with 'ApplyResult()', 'serve' will continue on to add the appropriate content to the response. For now, the 'authorize' function is always nil, so it's never invoked. In later patches, it will be configured via a new '--auth-config' option to the web server. Signed-off-by: Victoria Dye <vdye@github.com>
1 parent c076104 commit 26d21f5

4 files changed

Lines changed: 238 additions & 0 deletions

File tree

cmd/git-bundle-web-server/bundle-server.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,30 @@ import (
2020
"github.com/git-ecosystem/git-bundle-server/internal/core"
2121
"github.com/git-ecosystem/git-bundle-server/internal/git"
2222
"github.com/git-ecosystem/git-bundle-server/internal/log"
23+
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
2324
)
2425

26+
type authFunc func(*http.Request, string, string) auth.AuthResult
27+
2528
type bundleWebServer struct {
2629
logger log.TraceLogger
2730
server *http.Server
2831
serverWaitGroup *sync.WaitGroup
2932
listenAndServeFunc func() error
33+
authorize authFunc
3034
}
3135

3236
func NewBundleWebServer(logger log.TraceLogger,
3337
port string,
3438
certFile string, keyFile string,
3539
tlsMinVersion uint16,
3640
clientCAFile string,
41+
middlewareAuthorize authFunc,
3742
) (*bundleWebServer, error) {
3843
bundleServer := &bundleWebServer{
3944
logger: logger,
4045
serverWaitGroup: &sync.WaitGroup{},
46+
authorize: middlewareAuthorize,
4147
}
4248

4349
// Configure the http.Server
@@ -107,6 +113,13 @@ func (b *bundleWebServer) serve(w http.ResponseWriter, r *http.Request) {
107113

108114
route := owner + "/" + repo
109115

116+
if b.authorize != nil {
117+
authResult := b.authorize(r, owner, repo)
118+
if authResult.ApplyResult(w) {
119+
return
120+
}
121+
}
122+
110123
userProvider := common.NewUserProvider()
111124
fileSystem := common.NewFileSystem()
112125
commandExecutor := cmd.NewCommandExecutor(b.logger)

cmd/git-bundle-web-server/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ func main() {
2929
tlsMinVersion := utils.GetFlagValue[uint16](parser, "tls-version")
3030
clientCA := utils.GetFlagValue[string](parser, "client-ca")
3131

32+
// Configure auth
33+
middlewareAuthorize := authFunc(nil)
34+
3235
// Configure the server
3336
bundleServer, err := NewBundleWebServer(logger,
3437
port,
3538
cert, key,
3639
tlsMinVersion,
3740
clientCA,
41+
middlewareAuthorize,
3842
)
3943
if err != nil {
4044
logger.Fatal(ctx, err)

pkg/auth/auth-result.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package auth
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
)
7+
8+
// The Header type captures HTTP response header information.
9+
type Header struct {
10+
Key string
11+
Value string
12+
}
13+
14+
// The AuthResult represents the result of authenticating/authorizing via an
15+
// AuthMiddleware's Authorize function.
16+
type AuthResult struct {
17+
applyResultFunc func(http.ResponseWriter) bool
18+
}
19+
20+
// ApplyResult applies the AuthResult's configuration to the provided
21+
// http.ResponseWriter w and returns whether the web server should immediately
22+
// send the response (for an AuthResult created with Deny()) or continue on to
23+
// get and serve bundle server content (for an AuthResult created with
24+
// Accept()). If the AuthResult is invalid (e.g., created with AuthResult{}),
25+
// ApplyResult will indicate an immediate 500 response.
26+
func (a *AuthResult) ApplyResult(w http.ResponseWriter) bool {
27+
if a.applyResultFunc == nil {
28+
// AuthResult was initialized incorrectly - throw an ISE & exit
29+
w.WriteHeader(http.StatusInternalServerError)
30+
return true
31+
} else {
32+
return a.applyResultFunc(w)
33+
}
34+
}
35+
36+
func writeCustomHeaders(w http.ResponseWriter, headers []Header) {
37+
for _, h := range headers {
38+
w.Header().Add(h.Key, h.Value)
39+
}
40+
}
41+
42+
// Deny creates an AuthResult instance indicating that the bundle web server
43+
// should not serve the requested content and instead return an error response.
44+
// The response will have the status indicated by code (*must* be 4XX) and
45+
// include HTTP headers specified by the headers arg(s). Repeated headers (e.g.
46+
// multiple WWW-Authenticate headers) will be added to the response in the order
47+
// they are provided to this function.
48+
func Deny(code int, headers ...Header) AuthResult {
49+
// Make sure the code is a 4XX
50+
if code < 400 || code > 499 {
51+
panic(fmt.Sprintf("invalid auth middleware response code (must be 4XX, got %d)", code))
52+
}
53+
54+
// Configure ApplyResult to write the response & exit
55+
return AuthResult{
56+
applyResultFunc: func(w http.ResponseWriter) bool {
57+
writeCustomHeaders(w, headers)
58+
w.WriteHeader(code)
59+
return true
60+
},
61+
}
62+
}
63+
64+
// Allow creates an AuthResult instance indicating that the bundle web server
65+
// should serve the requested content. If headers are specified, they will be
66+
// applied to the http.ResponseWriter and applied to the response. Repeated
67+
// headers are applied in the order they are provided to this function.
68+
func Allow(headers ...Header) AuthResult {
69+
return AuthResult{
70+
applyResultFunc: func(w http.ResponseWriter) bool {
71+
writeCustomHeaders(w, headers)
72+
return false
73+
},
74+
}
75+
}

pkg/auth/auth-result_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package auth_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
var denyTests = []struct {
13+
title string
14+
15+
code int
16+
headers []auth.Header
17+
expectedInitPanic bool
18+
19+
expectedHeaders http.Header
20+
}{
21+
{
22+
"Invalid code causes panic",
23+
500,
24+
[]auth.Header{},
25+
true,
26+
nil,
27+
},
28+
{
29+
"Valid code with no headers",
30+
404,
31+
[]auth.Header{},
32+
false,
33+
map[string][]string{},
34+
},
35+
{
36+
"Valid code with unique headers",
37+
401,
38+
[]auth.Header{{"WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`}},
39+
false,
40+
map[string][]string{"Www-Authenticate": {`Basic realm="restricted", charset="UTF-8"`}},
41+
},
42+
{
43+
"Valid code with repeated headers",
44+
401,
45+
[]auth.Header{
46+
{"www-authenticate", `Basic realm="example.com"`},
47+
{"WWW-Authenticate", `Bearer authorize="idp.example.com/oauth"`},
48+
},
49+
false,
50+
map[string][]string{"Www-Authenticate": {
51+
`Basic realm="example.com"`,
52+
`Bearer authorize="idp.example.com/oauth"`,
53+
}},
54+
},
55+
}
56+
57+
func Test_Deny(t *testing.T) {
58+
for _, tt := range denyTests {
59+
t.Run(tt.title, func(t *testing.T) {
60+
w := httptest.NewRecorder()
61+
62+
// Create the AuthResult, call WriteResponse
63+
if tt.expectedInitPanic {
64+
assert.Panics(t, func() { auth.Deny(tt.code, tt.headers...) })
65+
return
66+
}
67+
result := auth.Deny(tt.code, tt.headers...)
68+
wroteResponse := result.ApplyResult(w)
69+
70+
// Response has been written; should exit
71+
assert.True(t, wroteResponse)
72+
73+
// Check code and content
74+
assert.Equal(t, tt.code, w.Code)
75+
assert.Equal(t, tt.expectedHeaders, w.Header())
76+
assert.Empty(t, w.Body)
77+
})
78+
}
79+
}
80+
81+
var allowTests = []struct {
82+
title string
83+
84+
headers []auth.Header
85+
expectedHeaders http.Header
86+
}{
87+
{
88+
"Allow with no headers",
89+
[]auth.Header{},
90+
map[string][]string{},
91+
},
92+
{
93+
"Allow with headers",
94+
[]auth.Header{{"Cache-Control", "no-store"}},
95+
map[string][]string{"Cache-Control": {"no-store"}},
96+
},
97+
{
98+
"Allow with repeated headers",
99+
[]auth.Header{
100+
{"FAKE-HEADER", "first value"},
101+
{"fake-header", "second value"},
102+
},
103+
map[string][]string{"Fake-Header": {
104+
"first value",
105+
"second value",
106+
}},
107+
},
108+
}
109+
110+
func Test_Allow(t *testing.T) {
111+
for _, tt := range allowTests {
112+
t.Run(tt.title, func(t *testing.T) {
113+
w := httptest.NewRecorder()
114+
115+
// Create the AuthResult, call WriteResponse
116+
result := auth.Allow(tt.headers...)
117+
wroteResponse := result.ApplyResult(w)
118+
119+
// Make sure we aren't exiting
120+
assert.False(t, wroteResponse)
121+
122+
// Make sure no code or body was written, but headers are
123+
assert.Equal(t, 200, w.Code) // default code
124+
assert.Equal(t, tt.expectedHeaders, w.Header())
125+
assert.Empty(t, w.Body)
126+
})
127+
}
128+
}
129+
130+
func Test_AuthResult(t *testing.T) {
131+
t.Run("Default AuthResult writes 500 response", func(t *testing.T) {
132+
w := httptest.NewRecorder()
133+
134+
// Create the AuthResult, call WriteResponse
135+
result := auth.AuthResult{}
136+
wroteResponse := result.ApplyResult(w)
137+
138+
// Response has been written; should exit
139+
assert.True(t, wroteResponse)
140+
141+
// Check code and content
142+
assert.Equal(t, 500, w.Code)
143+
assert.Empty(t, w.Header())
144+
assert.Empty(t, w.Body)
145+
})
146+
}

0 commit comments

Comments
 (0)