Skip to content

Commit

Permalink
add basic http auth, adopt apimiddleware system
Browse files Browse the repository at this point in the history
Co-authored-by: Blake Gentry <[email protected]>
  • Loading branch information
TArch64 and bgentry committed Dec 6, 2024
1 parent 2f09859 commit 02037b6
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add support for basic auth to the riverui executable. Thanks [Taras Turchenko](https://github.com/TArch64)! [PR #241](https://github.com/riverqueue/riverui/pull/241).

## [0.6.0] - 2024-11-26

### Added
Expand Down
31 changes: 31 additions & 0 deletions cmd/riverui/auth_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"crypto/subtle"
"net/http"
)

type authMiddleware struct {
username string
password string
}

func (m *authMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
if isReqAuthorized(req, m.username, m.password) {
next.ServeHTTP(res, req)
return
}

res.Header().Set("WWW-Authenticate", "Basic realm=\"riverui\"")
http.Error(res, "Unauthorized", http.StatusUnauthorized)
})
}

func isReqAuthorized(req *http.Request, username, password string) bool {
reqUsername, reqPassword, ok := req.BasicAuth()

return ok &&
subtle.ConstantTimeCompare([]byte(reqUsername), []byte(username)) == 1 &&
subtle.ConstantTimeCompare([]byte(reqPassword), []byte(password)) == 1
}
39 changes: 24 additions & 15 deletions cmd/riverui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/riverqueue/river/riverdriver/riverpgxv5"

"riverqueue.com/riverui"
"riverqueue.com/riverui/internal/apimiddleware"
)

func main() {
Expand Down Expand Up @@ -48,11 +49,13 @@ func initAndServe(ctx context.Context) int {
pathPrefix = riverui.NormalizePathPrefix(pathPrefix)

var (
corsOrigins = strings.Split(os.Getenv("CORS_ORIGINS"), ",")
dbURL = mustEnv("DATABASE_URL")
host = os.Getenv("RIVER_HOST") // may be left empty to bind to all local interfaces
otelEnabled = os.Getenv("OTEL_ENABLED") == "true"
port = cmp.Or(os.Getenv("PORT"), "8080")
basicAuthUsername = os.Getenv("RIVER_BASIC_AUTH_USER")
basicAuthPassword = os.Getenv("RIVER_BASIC_AUTH_PASS")
corsOrigins = strings.Split(os.Getenv("CORS_ORIGINS"), ",")
dbURL = mustEnv("DATABASE_URL")
host = os.Getenv("RIVER_HOST") // may be left empty to bind to all local interfaces
otelEnabled = os.Getenv("OTEL_ENABLED") == "true"
port = cmp.Or(os.Getenv("PORT"), "8080")
)

dbPool, err := getDBPool(ctx, dbURL)
Expand All @@ -62,11 +65,6 @@ func initAndServe(ctx context.Context) int {
}
defer dbPool.Close()

corsHandler := cors.New(cors.Options{
AllowedMethods: []string{"GET", "HEAD", "POST", "PUT"},
AllowedOrigins: corsOrigins,
})

client, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{})
if err != nil {
logger.ErrorContext(ctx, "error creating river client", slog.String("error", err.Error()))
Expand All @@ -88,21 +86,32 @@ func initAndServe(ctx context.Context) int {
return 1
}

if err := server.Start(ctx); err != nil {
if err = server.Start(ctx); err != nil {
logger.ErrorContext(ctx, "error starting UI server", slog.String("error", err.Error()))
return 1
}

logHandler := sloghttp.Recovery(server)
config := sloghttp.Config{
corsHandler := cors.New(cors.Options{
AllowedMethods: []string{"GET", "HEAD", "POST", "PUT"},
AllowedOrigins: corsOrigins,
})
logHandler := sloghttp.NewWithConfig(logger, sloghttp.Config{
WithSpanID: otelEnabled,
WithTraceID: otelEnabled,
})

middlewareStack := apimiddleware.NewMiddlewareStack(
apimiddleware.MiddlewareFunc(sloghttp.Recovery),
apimiddleware.MiddlewareFunc(corsHandler.Handler),
apimiddleware.MiddlewareFunc(logHandler),
)
if basicAuthUsername != "" && basicAuthPassword != "" {
middlewareStack.Use(&authMiddleware{username: basicAuthUsername, password: basicAuthPassword})
}
wrappedHandler := sloghttp.NewWithConfig(logger, config)(corsHandler.Handler(logHandler))

srv := &http.Server{
Addr: host + ":" + port,
Handler: wrappedHandler,
Handler: middlewareStack.Mount(server),
ReadHeaderTimeout: 5 * time.Second,
}

Expand Down
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ The `riverui` command utilizes the `RIVER_LOG_LEVEL` environment variable to con
* `warn`
* `error`

### Basic HTTP Authentication

The `riverui` supports basic HTTP authentication to protect access to the UI.
To enable it, set the `RIVER_BASIC_AUTH_USER` and `RIVER_BASIC_AUTH_PASS` environment variables.

## Development

See [developing River UI](./development.md).

0 comments on commit 02037b6

Please sign in to comment.