Skip to content

Commit

Permalink
Merge pull request #51 from vdye/vdye/auth-plugins
Browse files Browse the repository at this point in the history
Add basic support & documentation for built-in and plugin-based auth
  • Loading branch information
vdye authored May 17, 2023
2 parents c076104 + fdb0d44 commit 35a95e4
Show file tree
Hide file tree
Showing 24 changed files with 1,250 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
/_docs/
/_test/
node_modules/

*.so
6 changes: 5 additions & 1 deletion cmd/git-bundle-server/web-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ func (w *webServerCmd) startServer(ctx context.Context, args []string) error {
parser.Visit(func(f *flag.Flag) {
if webServerFlags.Lookup(f.Name) != nil {
value := f.Value.String()
if f.Name == "cert" || f.Name == "key" || f.Name == "client-ca" {
if f.Name == "cert" ||
f.Name == "key" ||
f.Name == "client-ca" ||
f.Name == "auth-config" {

// Need the absolute value of the path
value, err = filepath.Abs(value)
if err != nil {
Expand Down
23 changes: 23 additions & 0 deletions cmd/git-bundle-web-server/bundle-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,30 @@ import (
"github.com/git-ecosystem/git-bundle-server/internal/core"
"github.com/git-ecosystem/git-bundle-server/internal/git"
"github.com/git-ecosystem/git-bundle-server/internal/log"
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
)

type authFunc func(*http.Request, string, string) auth.AuthResult

type bundleWebServer struct {
logger log.TraceLogger
server *http.Server
serverWaitGroup *sync.WaitGroup
listenAndServeFunc func() error
authorize authFunc
}

func NewBundleWebServer(logger log.TraceLogger,
port string,
certFile string, keyFile string,
tlsMinVersion uint16,
clientCAFile string,
middlewareAuthorize authFunc,
) (*bundleWebServer, error) {
bundleServer := &bundleWebServer{
logger: logger,
serverWaitGroup: &sync.WaitGroup{},
authorize: middlewareAuthorize,
}

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

route := owner + "/" + repo

if b.authorize != nil {
authResult := b.authorize(r, owner, repo)
if authResult.ApplyResult(w) {
return
}
}

userProvider := common.NewUserProvider()
fileSystem := common.NewFileSystem()
commandExecutor := cmd.NewCommandExecutor(b.logger)
Expand Down Expand Up @@ -172,6 +185,16 @@ func (b *bundleWebServer) StartServerAsync(ctx context.Context) {
}
}(ctx)

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

Expand Down
128 changes: 128 additions & 0 deletions cmd/git-bundle-web-server/main.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,122 @@
package main

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"hash"
"io"
"os"
"plugin"
"strings"

"github.com/git-ecosystem/git-bundle-server/cmd/utils"
"github.com/git-ecosystem/git-bundle-server/internal/argparse"
auth_internal "github.com/git-ecosystem/git-bundle-server/internal/auth"
"github.com/git-ecosystem/git-bundle-server/internal/log"
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
)

func getPluginChecksum(pluginPath string) (hash.Hash, error) {
file, err := os.Open(pluginPath)
if err != nil {
return nil, err
}
defer file.Close()

checksum := sha256.New()
if _, err := io.Copy(checksum, file); err != nil {
return nil, err
}

return checksum, nil
}

func parseAuthConfig(configPath string) (auth.AuthMiddleware, error) {
var config authConfig
fileBytes, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}

err = json.Unmarshal(fileBytes, &config)
if err != nil {
return nil, err
}

switch strings.ToLower(config.AuthMode) {
case "fixed":
return auth_internal.NewFixedCredentialAuth(config.Parameters)
case "plugin":
if len(config.Path) == 0 {
return nil, fmt.Errorf("plugin .so is empty")
}
if len(config.Initializer) == 0 {
return nil, fmt.Errorf("plugin initializer symbol is empty")
}
if len(config.Checksum) == 0 {
return nil, fmt.Errorf("SHA256 checksum of plugin file is empty")
}

// First, verify plugin checksum matches expected
// Note: time-of-check/time-of-use could be exploited here (anywhere
// between the checksum check and invoking the initializer). There's not
// much we can realistically do about that short of rewriting the plugin
// package, so we advise users to carefully control access to their
// system & limit write permissions on their plugin files as a
// mitigation (see docs/technical/auth-config.md).
expectedChecksum, err := hex.DecodeString(config.Checksum)
if err != nil {
return nil, fmt.Errorf("plugin checksum is invalid: %w", err)
}
checksum, err := getPluginChecksum(config.Path)
if err != nil {
return nil, fmt.Errorf("could not calculate plugin checksum: %w", err)
}

if !bytes.Equal(expectedChecksum, checksum.Sum(nil)) {
return nil, fmt.Errorf("specified hash does not match plugin checksum")
}

// Load the plugin and find the initializer function
p, err := plugin.Open(config.Path)
if err != nil {
return nil, fmt.Errorf("could not load auth plugin: %w", err)
}

rawInit, err := p.Lookup(config.Initializer)
if err != nil {
return nil, fmt.Errorf("failed to load initializer: %w", err)
}

initializer, ok := rawInit.(func(json.RawMessage) (auth.AuthMiddleware, error))
if !ok {
return nil, fmt.Errorf("initializer function has incorrect signature")
}

// Call the initializer
return initializer(config.Parameters)
default:
return nil, fmt.Errorf("unrecognized auth mode '%s'", config.AuthMode)
}
}

type authConfig struct {
AuthMode string `json:"mode"`

// Plugin-specific settings
Path string `json:"path,omitempty"`
Initializer string `json:"initializer,omitempty"`
Checksum string `json:"sha256,omitempty"`

// Per-middleware custom config
Parameters json.RawMessage `json:"parameters,omitempty"`
}

func main() {
log.WithTraceLogger(context.Background(), func(ctx context.Context, logger log.TraceLogger) {
parser := argparse.NewArgParser(logger, "git-bundle-web-server [--port <port>] [--cert <filename> --key <filename>]")
Expand All @@ -28,13 +134,35 @@ func main() {
key := utils.GetFlagValue[string](parser, "key")
tlsMinVersion := utils.GetFlagValue[uint16](parser, "tls-version")
clientCA := utils.GetFlagValue[string](parser, "client-ca")
authConfig := utils.GetFlagValue[string](parser, "auth-config")

// Configure auth
var err error
middlewareAuthorize := authFunc(nil)
if authConfig != "" {
middleware, err := parseAuthConfig(authConfig)
if err != nil {
logger.Fatalf(ctx, "Invalid auth config: %w", err)
}
if middleware == nil {
// Up until this point, everything indicates that a user intends
// to use - and has properly configured - custom auth. However,
// despite there being no error from the initializer, the
// middleware was empty. This is almost certainly incorrect, so
// we exit.
logger.Fatalf(ctx, "Middleware is nil, but no error was returned from initializer. "+
"If no middleware is desired, remove the --auth-config option.")
}
middlewareAuthorize = middleware.Authorize
}

// Configure the server
bundleServer, err := NewBundleWebServer(logger,
port,
cert, key,
tlsMinVersion,
clientCA,
middlewareAuthorize,
)
if err != nil {
logger.Fatal(ctx, err)
Expand Down
1 change: 1 addition & 0 deletions cmd/utils/common-args.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func WebServerFlags(parser argParser) (*flag.FlagSet, func(context.Context)) {
tlsVersion := tlsVersionValue(tls.VersionTLS12)
f.Var(&tlsVersion, "tls-version", "The minimum TLS version the server will accept")
f.String("client-ca", "", "The path to the client authentication certificate authority PEM")
f.String("auth-config", "", "File containing the configuration for server auth middleware")

// Function to call for additional arg validation (may exit with 'Usage()')
validationFunc := func(ctx context.Context) {
Expand Down
87 changes: 87 additions & 0 deletions docs/man/git-bundle-web-server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,93 @@ web-server* for managing the web server process on their systems.

include::server-options.asc[]

== CONFIGURING AUTH

The *--auth-config* option configures authentication middleware for the server,
either using a built-in mode or with a custom plugin. The auth config specified
by that option is a JSON file that identifies the type of access control
requested and information needed to configure it.

=== Schema

The auth config JSON contains the following fields:

*mode* (string)::
The auth mode to use. Not case-sensitive.
+
Available options:

- _fixed_

*parameters* (object)::
A structure containing mode-specific key-value configuration fields, if
applicable. May be optional, depending on *mode*.

*path* (string) - *plugin*-only::
The absolute path to the auth plugin .so file.

*initializer* (string) - *plugin*-only::
The name of the symbol within the plugin binary that can invoked to create an
'AuthMiddleware' instance. The initializer:

- Must have the signature 'func(json.RawMessage) (AuthMiddleware, error)'.
- Must be exported in its package (i.e., UpperCamelCase name).

*sha256* (string) - *plugin*-only::
The SHA256 checksum of the plugin .so file, rendered as a hex string. If the
checksum does not match the calculated checksum of the plugin file, the web
server will refuse to start.
+
The checksum can be determined using man:shasum[1]:
+
[source,console]
----
$ shasum -a 256 /path/to/your/plugin.so
----

=== Examples

The following examples demonstrate typical usage of built-in and plugin modes.

***

Static, server-wide username & password ("admin" & "bundle_server",
respectively):

[source,json]
----
{
"mode": "fixed",
"parameters": {
"username": "admin",
"passwordHash": "c3c3520adf2f6e25672ba55dc70bcb3dd8b4ef3341bce1a5f38c5eca6571f372"
}
}
----

***

A custom auth plugin implementation:

- The path to the Go plugin file is '/path/to/plugin.so'
- The file contains the symbol
'func NewSimplePluginAuth(rawParams json.RawMessage) (AuthMiddleware, error)'
- The initializer ignores 'rawParams'
- The SHA256 checksum of '/path/to/plugin.so' is
'49db14bb838417a0292e293d0a6e90e82ed26fccb0d78670827c8c8516d2cca6'

[source,json]
----
{
"mode": "plugin",
"path": "/path/to/plugin.so",
"initializer": "NewSimplePluginAuth",
"sha256": "49db14bb838417a0292e293d0a6e90e82ed26fccb0d78670827c8c8516d2cca6"
}
----

***

== SEE ALSO

man:git-bundle-server[1], man:git-bundle[1], man:git-fetch[1]
4 changes: 4 additions & 0 deletions docs/man/server-options.asc
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ configured for TLS, this option is a no-op.
Require that requests to the bundle server include a client certificate that
can be validated by the certificate authority file at the specified _path_.
No-op if *--cert* and *--key* are not configured.

*--auth-config* _path_:::
Use the JSON contents of the specified file to configure
authentication/authorization for requests to the web server.
Loading

0 comments on commit 35a95e4

Please sign in to comment.