← repoman internals

internal/server/middleware

import "git.griefed.de/griefed/repoman/internal/server/middleware"
package middleware // import "git.griefed.de/griefed/repoman/internal/server/middleware"

Package middleware contains repoman's HTTP middleware:

  - CSRF — cross-origin (CSRF) protection on state-changing requests
  - Auth — session lookup and user-context injection
  - RequireRole — fine-grained authorization checks per handler
  - RequestLog — structured access logging via slog

CONSTANTS

const SessionCookieName = "repoman_session"
    SessionCookieName is the name of the cookie holding the session ID.
    Hard-coded so the value is grep-friendly and never accidentally reconfigured
    to something a forgotten test still expects.


FUNCTIONS

func Auth(opts AuthOptions) func(http.Handler) http.Handler
    Auth returns middleware that:

     1. Looks up the session cookie
     2. Loads the matching session and user from the repositories
     3. Stores both in the request context for downstream handlers
     4. Redirects to LoginPath when no valid session exists, EXCEPT for the
        configured AllowedAnonymousPaths
     5. Redirects to PendingTwoFactorPath when the session has pending_2fa=1
        (so the user cannot reach normal pages until they complete the second
        factor)

    Handlers retrieve the user via UserFromContext.

func CSRF(opts CSRFOptions) func(http.Handler) http.Handler
    CSRF returns cross-origin request protection built on the standard library's
    net/http.CrossOriginProtection (Go 1.25+). Wrap any router that handles
    non-idempotent methods (POST, PUT, PATCH, DELETE).

    It rejects unsafe requests the browser reports as cross-site via the
    Sec-Fetch-Site header (falling back to an Origin/Host comparison when that
    header is absent), answering with 403 Forbidden. GET/HEAD/OPTIONS are always
    allowed, so the SSE stream and static assets are unaffected.

    Unlike a synchronizer-token scheme this needs no per-form hidden field,
    no signing key, and no cookie — which is why repoman dropped gorilla/csrf,
    whose host-only TrustedOrigins matching ignored the scheme (CVE-2025-47909,
    for which no upstream fix exists), in favour of the stdlib mechanism.

func RequireRole(perm auth.Permission) func(http.Handler) http.Handler
    RequireRole returns middleware that lets only users with the given
    permission proceed. Unauthorised access returns 403; this is distinct from
    the 302-to-login path of Auth — by the time RequireRole runs, we KNOW the
    user is authenticated.

func SessionFromContext(ctx context.Context) *auth.Session
    SessionFromContext retrieves the session from the request context.

func UserFromContext(ctx context.Context) *auth.User
    UserFromContext retrieves the authenticated user from the request context.
    Returns nil if no user is attached (anonymous request or middleware was not
    wired).

func WithSession(ctx context.Context, session *auth.Session) context.Context
    WithSession returns a copy of ctx carrying session as the active session.
    Test counterpart to WithUser; SessionFromContext will return session.

func WithUser(ctx context.Context, user *auth.User) context.Context
    WithUser returns a copy of ctx carrying user as the authenticated user. The
    context keys are unexported, so handler/middleware tests in other packages
    use this to construct a request as if Auth had already run; UserFromContext
    will then return user.

func NewActorContext(ctx context.Context) (context.Context, *requestActor)
    NewActorContext installs an empty actor holder on ctx and returns the
    augmented context together with the holder. The access logger calls this
    at the top of the chain, then reads Username() once the request has been
    served.


TYPES

type AuthOptions struct {
	Sessions *auth.SessionManager
	Users    auth.UserRepository

	// LoginPath is where unauthenticated requests are redirected.
	// Default "/auth/login".
	LoginPath string

	// PendingTwoFactorPath is where requests with a session that has
	// not yet completed the 2FA step are redirected. Default
	// "/auth/2fa".
	PendingTwoFactorPath string

	// AllowedAnonymousPaths are URL prefixes that bypass the auth check.
	// Used for /auth/* endpoints, static assets, and the health probe.
	AllowedAnonymousPaths []string
}
    AuthOptions wires the middleware to the auth package. Sessions and Users are
    interfaces so tests can stub them.

type CSRFOptions struct {
	// TrustedOrigins lists full scheme://host[:port] origins (e.g.
	// "https://repoman.example.com") from which cross-origin state-changing
	// requests are accepted. This is only consulted when the browser omits
	// the Sec-Fetch-Site header and the operator-visible origin differs from
	// the host repoman sees behind a reverse proxy; every browser since 2023
	// sends Sec-Fetch-Site, so same-origin form posts are allowed without any
	// entry here.
	TrustedOrigins []string
}
    CSRFOptions controls the cross-origin protection middleware.