feat(api): add authenticated containers http api
GET /api/v1/containers behind bearer token auth, JSON errors for 401/404/405/500, one audit row per authenticated request, graceful shutdown; unit and e2e tests for auth, routing, auditing, request logging and server lifecycle.
This commit is contained in:
1 parent
9a85e9acc8
commit
b792ad256e
11 files changed
+2028
No files matched your search
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Authentication result classes; these fixed strings are the only authentication detail ever logged
|
||||
const (
|
||||
authMissingHeader = "missing_header"
|
||||
authEmptyHeader = "empty_header"
|
||||
authMultipleHeaders = "multiple_headers"
|
||||
authBadScheme = "bad_scheme"
|
||||
authEmptyToken = "empty_token"
|
||||
authUnknownToken = "unknown_token"
|
||||
authInactiveToken = "inactive_token"
|
||||
authLookupError = "lookup_error"
|
||||
authOK = "ok"
|
||||
)
|
||||
|
||||
// TokenLookup is the token-store dependency of the HTTP layer
|
||||
type TokenLookup interface {
|
||||
GetToken(ctx context.Context, value string) (Token, error)
|
||||
}
|
||||
|
||||
// parseAuthorization extracts the bearer token from the request headers
|
||||
// It returns the token and an empty class on success, or an empty token and the failure class
|
||||
// Accepted form: exactly one Authorization header, scheme "Bearer" (ASCII case-insensitive), one space, token
|
||||
// The token is taken byte-exact: no trimming or normalization is applied
|
||||
func parseAuthorization(h http.Header) (string, string) {
|
||||
values := h.Values("Authorization")
|
||||
switch {
|
||||
case len(values) == 0:
|
||||
return "", authMissingHeader
|
||||
case len(values) > 1:
|
||||
return "", authMultipleHeaders
|
||||
case values[0] == "":
|
||||
return "", authEmptyHeader
|
||||
}
|
||||
|
||||
// Split "<scheme> <token>" at the first space
|
||||
scheme, token, found := strings.Cut(values[0], " ")
|
||||
if !asciiEqualFold(scheme, "Bearer") {
|
||||
return "", authBadScheme
|
||||
}
|
||||
if !found || token == "" {
|
||||
return "", authEmptyToken
|
||||
}
|
||||
return token, ""
|
||||
}
|
||||
|
||||
// authenticate validates the request credentials against the token store
|
||||
// It returns the authenticated token, the result class, and a non-nil error only when the lookup itself failed
|
||||
func authenticate(ctx context.Context, log *slog.Logger, tokens TokenLookup, h http.Header) (Token, string, error) {
|
||||
defer traceFunc(ctx, log, "authenticate")()
|
||||
|
||||
// Parse the header
|
||||
value, class := parseAuthorization(h)
|
||||
if class != "" {
|
||||
return Token{}, class, nil
|
||||
}
|
||||
|
||||
// Look the token up
|
||||
tok, err := tokens.GetToken(ctx, value)
|
||||
if errors.Is(err, ErrTokenNotFound) {
|
||||
return Token{}, authUnknownToken, nil
|
||||
}
|
||||
if err != nil {
|
||||
return Token{}, authLookupError, err
|
||||
}
|
||||
if !tok.Active {
|
||||
return Token{}, authInactiveToken, nil
|
||||
}
|
||||
return tok, authOK, nil
|
||||
}
|
||||
Reference in new issue
Block a user