Skip to content

Commit 35a95e4

Browse files
authored
Merge pull request #51 from vdye/vdye/auth-plugins
Add basic support & documentation for built-in and plugin-based auth
2 parents c076104 + fdb0d44 commit 35a95e4

File tree

24 files changed

+1250
-6
lines changed

24 files changed

+1250
-6
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
/_docs/
77
/_test/
88
node_modules/
9+
10+
*.so

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ func (w *webServerCmd) startServer(ctx context.Context, args []string) error {
7777
parser.Visit(func(f *flag.Flag) {
7878
if webServerFlags.Lookup(f.Name) != nil {
7979
value := f.Value.String()
80-
if f.Name == "cert" || f.Name == "key" || f.Name == "client-ca" {
80+
if f.Name == "cert" ||
81+
f.Name == "key" ||
82+
f.Name == "client-ca" ||
83+
f.Name == "auth-config" {
84+
8185
// Need the absolute value of the path
8286
value, err = filepath.Abs(value)
8387
if err != nil {

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

Lines changed: 23 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)
@@ -172,6 +185,16 @@ func (b *bundleWebServer) StartServerAsync(ctx context.Context) {
172185
}
173186
}(ctx)
174187

188+
// Wait 0.1s before reporting that the server is started in case
189+
// 'listenAndServeFunc' exits immediately.
190+
//
191+
// It's a hack, but a necessary one because 'ListenAndServe[TLS]()' doesn't
192+
// have any mechanism of notifying if it starts successfully, only that it
193+
// fails. We could get around that by copying/reimplementing those functions
194+
// with a print statement inserted at the right place, but that's way more
195+
// cumbersome than just adding a delay here (see:
196+
// https://stackoverflow.com/questions/53332667/how-to-notify-when-http-server-starts-successfully).
197+
time.Sleep(time.Millisecond * 100)
175198
fmt.Println("Server is running at address " + b.server.Addr)
176199
}
177200

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,122 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
6+
"crypto/sha256"
7+
"encoding/hex"
8+
"encoding/json"
59
"flag"
610
"fmt"
11+
"hash"
12+
"io"
713
"os"
14+
"plugin"
15+
"strings"
816

917
"github.com/git-ecosystem/git-bundle-server/cmd/utils"
1018
"github.com/git-ecosystem/git-bundle-server/internal/argparse"
19+
auth_internal "github.com/git-ecosystem/git-bundle-server/internal/auth"
1120
"github.com/git-ecosystem/git-bundle-server/internal/log"
21+
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
1222
)
1323

24+
func getPluginChecksum(pluginPath string) (hash.Hash, error) {
25+
file, err := os.Open(pluginPath)
26+
if err != nil {
27+
return nil, err
28+
}
29+
defer file.Close()
30+
31+
checksum := sha256.New()
32+
if _, err := io.Copy(checksum, file); err != nil {
33+
return nil, err
34+
}
35+
36+
return checksum, nil
37+
}
38+
39+
func parseAuthConfig(configPath string) (auth.AuthMiddleware, error) {
40+
var config authConfig
41+
fileBytes, err := os.ReadFile(configPath)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
err = json.Unmarshal(fileBytes, &config)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
switch strings.ToLower(config.AuthMode) {
52+
case "fixed":
53+
return auth_internal.NewFixedCredentialAuth(config.Parameters)
54+
case "plugin":
55+
if len(config.Path) == 0 {
56+
return nil, fmt.Errorf("plugin .so is empty")
57+
}
58+
if len(config.Initializer) == 0 {
59+
return nil, fmt.Errorf("plugin initializer symbol is empty")
60+
}
61+
if len(config.Checksum) == 0 {
62+
return nil, fmt.Errorf("SHA256 checksum of plugin file is empty")
63+
}
64+
65+
// First, verify plugin checksum matches expected
66+
// Note: time-of-check/time-of-use could be exploited here (anywhere
67+
// between the checksum check and invoking the initializer). There's not
68+
// much we can realistically do about that short of rewriting the plugin
69+
// package, so we advise users to carefully control access to their
70+
// system & limit write permissions on their plugin files as a
71+
// mitigation (see docs/technical/auth-config.md).
72+
expectedChecksum, err := hex.DecodeString(config.Checksum)
73+
if err != nil {
74+
return nil, fmt.Errorf("plugin checksum is invalid: %w", err)
75+
}
76+
checksum, err := getPluginChecksum(config.Path)
77+
if err != nil {
78+
return nil, fmt.Errorf("could not calculate plugin checksum: %w", err)
79+
}
80+
81+
if !bytes.Equal(expectedChecksum, checksum.Sum(nil)) {
82+
return nil, fmt.Errorf("specified hash does not match plugin checksum")
83+
}
84+
85+
// Load the plugin and find the initializer function
86+
p, err := plugin.Open(config.Path)
87+
if err != nil {
88+
return nil, fmt.Errorf("could not load auth plugin: %w", err)
89+
}
90+
91+
rawInit, err := p.Lookup(config.Initializer)
92+
if err != nil {
93+
return nil, fmt.Errorf("failed to load initializer: %w", err)
94+
}
95+
96+
initializer, ok := rawInit.(func(json.RawMessage) (auth.AuthMiddleware, error))
97+
if !ok {
98+
return nil, fmt.Errorf("initializer function has incorrect signature")
99+
}
100+
101+
// Call the initializer
102+
return initializer(config.Parameters)
103+
default:
104+
return nil, fmt.Errorf("unrecognized auth mode '%s'", config.AuthMode)
105+
}
106+
}
107+
108+
type authConfig struct {
109+
AuthMode string `json:"mode"`
110+
111+
// Plugin-specific settings
112+
Path string `json:"path,omitempty"`
113+
Initializer string `json:"initializer,omitempty"`
114+
Checksum string `json:"sha256,omitempty"`
115+
116+
// Per-middleware custom config
117+
Parameters json.RawMessage `json:"parameters,omitempty"`
118+
}
119+
14120
func main() {
15121
log.WithTraceLogger(context.Background(), func(ctx context.Context, logger log.TraceLogger) {
16122
parser := argparse.NewArgParser(logger, "git-bundle-web-server [--port <port>] [--cert <filename> --key <filename>]")
@@ -28,13 +134,35 @@ func main() {
28134
key := utils.GetFlagValue[string](parser, "key")
29135
tlsMinVersion := utils.GetFlagValue[uint16](parser, "tls-version")
30136
clientCA := utils.GetFlagValue[string](parser, "client-ca")
137+
authConfig := utils.GetFlagValue[string](parser, "auth-config")
138+
139+
// Configure auth
140+
var err error
141+
middlewareAuthorize := authFunc(nil)
142+
if authConfig != "" {
143+
middleware, err := parseAuthConfig(authConfig)
144+
if err != nil {
145+
logger.Fatalf(ctx, "Invalid auth config: %w", err)
146+
}
147+
if middleware == nil {
148+
// Up until this point, everything indicates that a user intends
149+
// to use - and has properly configured - custom auth. However,
150+
// despite there being no error from the initializer, the
151+
// middleware was empty. This is almost certainly incorrect, so
152+
// we exit.
153+
logger.Fatalf(ctx, "Middleware is nil, but no error was returned from initializer. "+
154+
"If no middleware is desired, remove the --auth-config option.")
155+
}
156+
middlewareAuthorize = middleware.Authorize
157+
}
31158

32159
// Configure the server
33160
bundleServer, err := NewBundleWebServer(logger,
34161
port,
35162
cert, key,
36163
tlsMinVersion,
37164
clientCA,
165+
middlewareAuthorize,
38166
)
39167
if err != nil {
40168
logger.Fatal(ctx, err)

cmd/utils/common-args.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func WebServerFlags(parser argParser) (*flag.FlagSet, func(context.Context)) {
8686
tlsVersion := tlsVersionValue(tls.VersionTLS12)
8787
f.Var(&tlsVersion, "tls-version", "The minimum TLS version the server will accept")
8888
f.String("client-ca", "", "The path to the client authentication certificate authority PEM")
89+
f.String("auth-config", "", "File containing the configuration for server auth middleware")
8990

9091
// Function to call for additional arg validation (may exit with 'Usage()')
9192
validationFunc := func(ctx context.Context) {

docs/man/git-bundle-web-server.adoc

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,93 @@ web-server* for managing the web server process on their systems.
2727

2828
include::server-options.asc[]
2929

30+
== CONFIGURING AUTH
31+
32+
The *--auth-config* option configures authentication middleware for the server,
33+
either using a built-in mode or with a custom plugin. The auth config specified
34+
by that option is a JSON file that identifies the type of access control
35+
requested and information needed to configure it.
36+
37+
=== Schema
38+
39+
The auth config JSON contains the following fields:
40+
41+
*mode* (string)::
42+
The auth mode to use. Not case-sensitive.
43+
+
44+
Available options:
45+
46+
- _fixed_
47+
48+
*parameters* (object)::
49+
A structure containing mode-specific key-value configuration fields, if
50+
applicable. May be optional, depending on *mode*.
51+
52+
*path* (string) - *plugin*-only::
53+
The absolute path to the auth plugin .so file.
54+
55+
*initializer* (string) - *plugin*-only::
56+
The name of the symbol within the plugin binary that can invoked to create an
57+
'AuthMiddleware' instance. The initializer:
58+
59+
- Must have the signature 'func(json.RawMessage) (AuthMiddleware, error)'.
60+
- Must be exported in its package (i.e., UpperCamelCase name).
61+
62+
*sha256* (string) - *plugin*-only::
63+
The SHA256 checksum of the plugin .so file, rendered as a hex string. If the
64+
checksum does not match the calculated checksum of the plugin file, the web
65+
server will refuse to start.
66+
+
67+
The checksum can be determined using man:shasum[1]:
68+
+
69+
[source,console]
70+
----
71+
$ shasum -a 256 /path/to/your/plugin.so
72+
----
73+
74+
=== Examples
75+
76+
The following examples demonstrate typical usage of built-in and plugin modes.
77+
78+
***
79+
80+
Static, server-wide username & password ("admin" & "bundle_server",
81+
respectively):
82+
83+
[source,json]
84+
----
85+
{
86+
"mode": "fixed",
87+
"parameters": {
88+
"username": "admin",
89+
"passwordHash": "c3c3520adf2f6e25672ba55dc70bcb3dd8b4ef3341bce1a5f38c5eca6571f372"
90+
}
91+
}
92+
----
93+
94+
***
95+
96+
A custom auth plugin implementation:
97+
98+
- The path to the Go plugin file is '/path/to/plugin.so'
99+
- The file contains the symbol
100+
'func NewSimplePluginAuth(rawParams json.RawMessage) (AuthMiddleware, error)'
101+
- The initializer ignores 'rawParams'
102+
- The SHA256 checksum of '/path/to/plugin.so' is
103+
'49db14bb838417a0292e293d0a6e90e82ed26fccb0d78670827c8c8516d2cca6'
104+
105+
[source,json]
106+
----
107+
{
108+
"mode": "plugin",
109+
"path": "/path/to/plugin.so",
110+
"initializer": "NewSimplePluginAuth",
111+
"sha256": "49db14bb838417a0292e293d0a6e90e82ed26fccb0d78670827c8c8516d2cca6"
112+
}
113+
----
114+
115+
***
116+
30117
== SEE ALSO
31118

32119
man:git-bundle-server[1], man:git-bundle[1], man:git-fetch[1]

docs/man/server-options.asc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ configured for TLS, this option is a no-op.
2929
Require that requests to the bundle server include a client certificate that
3030
can be validated by the certificate authority file at the specified _path_.
3131
No-op if *--cert* and *--key* are not configured.
32+
33+
*--auth-config* _path_:::
34+
Use the JSON contents of the specified file to configure
35+
authentication/authorization for requests to the web server.

0 commit comments

Comments
 (0)