Skip to content

Commit a987111

Browse files
committed
feat(wrtagweb): add web-auth flag to control interface authentication
closes #172 closes #170
1 parent d3eab80 commit a987111

File tree

4 files changed

+62
-23
lines changed

4 files changed

+62
-23
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ flowchart LR
169169

170170
Jobs are added to the queue with an HTTP request like `POST <wrtag.host>/op/<copy|move|reflink>` with form value `path=<absolute path to directory>`. Optional form value `mbid=<musicbrainz release URL>` can be supplied if you know your release. Both of the form values can be `application/x-www-form-urlencoded` form bodies, or URL query parameters.
171171

172-
Authentication is done via a HTTP Basic authentication password **without a username**. The password is configured with the `web-api-key` config option.
172+
The external API requires HTTP Basic authentication with `-web-api-key` as the password (no username). The web UI authentication is controlled by `-web-auth`: either `disabled`, or `basic-auth-from-api-key` (the default) which uses the same API key.
173173

174174
> [!WARNING]
175175
> HTTP Basic Authentication is only as secure as the transport layer it runs on. Make sure `wrtagweb` is secured using TLS behind your reverse proxy.
@@ -271,13 +271,14 @@ Configuration for `wrtagweb` works the same as [Global configuration](#global-co
271271

272272
<!-- gen with ```go run ./cmd/wrtagweb -h 2>&1 | ./gen-docs | wl-copy``` -->
273273

274-
| CLI argument | Environment variable | Config file key | Description |
275-
| ---------------- | --------------------- | --------------- | ------------------------------------------------------------- |
276-
| -web-api-key | WRTAG_WEB_API_KEY | web-api-key | API key for web interface |
277-
| -web-db-path | WRTAG_WEB_DB_PATH | web-db-path | Path to persistent database path for web interface (optional) |
278-
| -web-listen-addr | WRTAG_WEB_LISTEN_ADDR | web-listen-addr | Listen address for web interface (optional) (default ":7373") |
279-
| -web-num-workers | WRTAG_WEB_NUM_WORKERS | web-num-workers | Number of directories to process concurrently |
280-
| -web-public-url | WRTAG_WEB_PUBLIC_URL | web-public-url | Public URL for web interface (optional) |
274+
| CLI argument | Environment variable | Config file key | Description |
275+
| ---------------- | --------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------- |
276+
| -web-api-key | WRTAG_WEB_API_KEY | web-api-key | Key for external API endpoints |
277+
| -web-auth | WRTAG_WEB_AUTH | web-auth | Authentication mode, one of "disabled", "basic-auth-from-api-key" (optional) (default "basic-auth-from-api-key") |
278+
| -web-db-path | WRTAG_WEB_DB_PATH | web-db-path | Path to persistent database path for web interface (optional) |
279+
| -web-listen-addr | WRTAG_WEB_LISTEN_ADDR | web-listen-addr | Listen address for web interface (optional) (default ":7373") |
280+
| -web-num-workers | WRTAG_WEB_NUM_WORKERS | web-num-workers | Number of directories to process concurrently (default 4) |
281+
| -web-public-url | WRTAG_WEB_PUBLIC_URL | web-public-url | Public URL for web interface (optional) |
281282

282283
## Tool `metadata`
283284

cmd/wrtagweb/main.go

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ func main() {
6565
cfg = wrtagflag.Config()
6666
notifs = wrtagflag.Notifications()
6767
researchLinkQuerier = wrtagflag.ResearchLinks()
68-
apiKey = flag.String("web-api-key", "", "API key for web interface")
68+
apiKey = flag.String("web-api-key", "", "Key for external API endpoints")
69+
auth = flag.String("web-auth", string(authBasicFromAPIKey), "Authentication mode, one of \"disabled\", \"basic-auth-from-api-key\" (optional)")
6970
listenAddr = flag.String("web-listen-addr", ":7373", "Listen address for web interface (optional)")
7071
dbPath = flag.String("web-db-path", "", "Path to persistent database path for web interface (optional)")
7172
publicURL = flag.String("web-public-url", "", "Public URL for web interface (optional)")
@@ -87,6 +88,13 @@ func main() {
8788
return
8889
}
8990

91+
switch ath := authMode(*auth); ath {
92+
case authDisabled, authBasicFromAPIKey:
93+
default:
94+
slog.Error("unknown auth mode", "value", ath)
95+
return
96+
}
97+
9098
if *dbPath == "" {
9199
tmpf, err := os.CreateTemp("", "wrtagweb*.db")
92100
if err != nil {
@@ -292,8 +300,17 @@ func main() {
292300

293301
mux.Handle("/", http.FileServer(http.FS(ui)))
294302

303+
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
304+
mux.HandleFunc("GET /debug/pprof/*", pprof.Index)
305+
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
306+
mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
307+
mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
308+
mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
309+
295310
// external API
296-
mux.HandleFunc("POST /op/{operation}", func(w http.ResponseWriter, r *http.Request) {
311+
muxExternal := http.NewServeMux()
312+
313+
muxExternal.HandleFunc("POST /op/{operation}", func(w http.ResponseWriter, r *http.Request) {
297314
operationStr := r.PathValue("operation")
298315
if _, err := wrtagflag.OperationByName(operationStr, false); err != nil {
299316
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -326,23 +343,19 @@ func main() {
326343
jobQueue <- job.ID
327344
})
328345

329-
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
330-
mux.HandleFunc("GET /debug/pprof/*", pprof.Index)
331-
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
332-
mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
333-
mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
334-
mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
335-
336346
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
337347
defer cancel()
338348
errgrp, ctx := errgroup.WithContext(ctx)
339349

340350
errgrp.Go(func() error {
341351
defer logJob("http", "addr", *listenAddr)()
342352

353+
m := http.NewServeMux()
354+
m.Handle("/", authMiddleware(mux, authMode(*auth), *apiKey))
355+
m.Handle("/op/", apiKeyMiddleware(muxExternal, *apiKey))
356+
343357
var h http.Handler
344-
h = mux
345-
h = authMiddleware(h, *apiKey)
358+
h = m
346359
h = logMiddleware(h)
347360

348361
server := &http.Server{
@@ -599,9 +612,24 @@ func logJob(jobName string, args ...any) func() {
599612
return func() { slog.Info("stopping job", "job", jobName) }
600613
}
601614

615+
type authMode string
616+
617+
const (
618+
authDisabled authMode = "disabled"
619+
authBasicFromAPIKey authMode = "basic-auth-from-api-key" //nolint:gosec
620+
)
621+
602622
const cookieKey = "api-key"
603623

604-
func authMiddleware(next http.Handler, apiKey string) http.Handler {
624+
func authMiddleware(next http.Handler, mode authMode, apiKey string) http.Handler {
625+
switch mode {
626+
case authDisabled:
627+
return next
628+
case authBasicFromAPIKey:
629+
default:
630+
panic("invalid mode")
631+
}
632+
605633
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
606634
// exchange a valid basic auth request for a cookie that lasts 30 days
607635
if cookie, _ := r.Cookie(cookieKey); cookie != nil && subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(apiKey)) == 1 {
@@ -618,6 +646,16 @@ func authMiddleware(next http.Handler, apiKey string) http.Handler {
618646
})
619647
}
620648

649+
func apiKeyMiddleware(next http.Handler, apiKey string) http.Handler {
650+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
651+
if _, key, _ := r.BasicAuth(); subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1 {
652+
next.ServeHTTP(w, r)
653+
return
654+
}
655+
http.Error(w, "unauthorised", http.StatusUnauthorized)
656+
})
657+
}
658+
621659
func logMiddleware(next http.Handler) http.Handler {
622660
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
623661
slog.InfoContext(r.Context(), "request", "url", r.URL)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/rogpeppe/go-internal v1.14.1
1717
github.com/sergi/go-diff v1.4.0
1818
github.com/stretchr/testify v1.11.1
19-
go.senan.xyz/flagconf v0.1.10
19+
go.senan.xyz/flagconf v0.1.11
2020
go.senan.xyz/natcmp v0.1.2
2121
go.senan.xyz/sqlb v0.3.11
2222
go.senan.xyz/table v0.0.0-20251023151529-96acc7f0ad6c

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
7171
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
7272
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
7373
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
74-
go.senan.xyz/flagconf v0.1.10 h1:IGWaX9z4uh03xopYJSwpw1u0KrwRWs8vDmFjaULFH3U=
75-
go.senan.xyz/flagconf v0.1.10/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
74+
go.senan.xyz/flagconf v0.1.11 h1:ApA9DpoSrfVQlLt8spKJC3fOT7oTEZ2/MmBGMnb9mt4=
75+
go.senan.xyz/flagconf v0.1.11/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
7676
go.senan.xyz/natcmp v0.1.2 h1:ko7umA435ZyrNtNZeAvBOWzxct9RaXdie7XC1rWhD84=
7777
go.senan.xyz/natcmp v0.1.2/go.mod h1:OuIIjIsyj2PwVqCtfzqTeplsZhSXGjm4okZei78Bck4=
7878
go.senan.xyz/sqlb v0.3.11 h1:uSfFZmtiUUyPddAYf/1rJqcgZIMeONagIxBWlmYyO9w=

0 commit comments

Comments
 (0)