docs(httpauth-firebase): fix rbac tier reference from 1 to 0
rbac is a Tier 0 module (no micro-lib dependencies). The dependency line incorrectly cited it as Tier 1. The module's own tier (4) is unchanged — it remains the auth layer above the transport infrastructure.
This commit is contained in:
26
.devcontainer/devcontainer.json
Normal file
26
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "Go",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:2-1.25-trixie",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-extra/features/claude-code:1": {}
|
||||||
|
},
|
||||||
|
"forwardPorts": [],
|
||||||
|
"postCreateCommand": "go version",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {
|
||||||
|
"files.autoSave": "afterDelay",
|
||||||
|
"files.autoSaveDelay": 1000,
|
||||||
|
"explorer.compactFolders": false,
|
||||||
|
"explorer.showEmptyFolders": true
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"golang.go",
|
||||||
|
"eamodio.golang-postfix-completion",
|
||||||
|
"quicktype.quicktype",
|
||||||
|
"usernamehw.errorlens"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remoteUser": "vscode"
|
||||||
|
}
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with go test -c
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of go build
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# VCS files
|
||||||
|
COMMIT.md
|
||||||
|
RELEASE.md
|
||||||
27
CHANGELOG.md
Normal file
27
CHANGELOG.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this module will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `TokenVerifier` interface — abstracts `*auth.Client` for unit-test mockability; `*auth.Client` satisfies it directly in production via its `VerifyIDTokenAndCheckRevoked` method
|
||||||
|
- `IdentityEnricher` interface — application-implemented; receives `uid string` and `claims map[string]any`, returns `rbac.Identity`; called by `EnrichmentMiddleware` on every request
|
||||||
|
- `PermissionProvider` interface — application-implemented; receives `uid` and `resource` string, returns `rbac.PermissionMask`; called by `AuthzMiddleware` on every request
|
||||||
|
- `EnrichOpt` functional option type for configuring `EnrichmentMiddleware`
|
||||||
|
- `WithTenantHeader(header string) EnrichOpt` — reads a tenant ID from the named request header and attaches it to the identity via `rbac.Identity.WithTenant`; absent header leaves `TenantID` as an empty string with no error
|
||||||
|
- `AuthMiddleware(verifier TokenVerifier, publicPaths []string) func(http.Handler) http.Handler` — verifies `Authorization: Bearer <token>` via Firebase JWT verification and injects the verified `uid` and raw claims into the request context under unexported typed keys; paths matching any pattern in `publicPaths` bypass token verification (glob patterns via `path.Match`, `*` wildcard supported); returns 401 on missing or invalid tokens
|
||||||
|
- `EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler` — reads the uid and claims stored by `AuthMiddleware`, calls `enricher.Enrich`, and stores the resulting `rbac.Identity` in context via `rbac.SetInContext`; returns 401 if `AuthMiddleware` has not run upstream; returns 500 if the enricher fails
|
||||||
|
- `AuthzMiddleware(provider PermissionProvider, resource string, required rbac.Permission) func(http.Handler) http.Handler` — reads `rbac.Identity` from context via `rbac.FromContext`, resolves the permission mask for the identity's UID on `resource`, and gates the request against the required permission bit; returns 401 if no identity is in context; returns 403 if the permission check fails or the provider returns an error
|
||||||
|
|
||||||
|
### Design Notes
|
||||||
|
|
||||||
|
- The three middleware functions are intentionally separate so they can be applied at different scopes: `AuthMiddleware` at the root router, `EnrichmentMiddleware` on authenticated route groups, and `AuthzMiddleware` per-route or per-group with different resource and permission arguments
|
||||||
|
- The module is named `httpauth-firebase` rather than `httpauth` because it imports the Firebase SDK directly; other providers (`httpauth-auth0`, `httpauth-jwt`, etc.) are separate sibling modules that all converge on the same `rbac.Identity` output contract, which means downstream handlers and business logic never depend on a specific auth provider
|
||||||
|
- No logger parameter is accepted; errors are returned as plain-text HTTP responses, keeping the dependency surface to `rbac` and `firebase.google.com/go/v4` only
|
||||||
|
|
||||||
|
[0.9.0]: https://code.nochebuena.dev/go/httpauth-firebase/releases/tag/v0.9.0
|
||||||
124
CLAUDE.md
Normal file
124
CLAUDE.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# httpauth-firebase
|
||||||
|
|
||||||
|
Firebase-backed HTTP middleware for authentication, identity enrichment, and
|
||||||
|
role-based access control.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
`httpauth-firebase` provides three composable `net/http` middleware functions that
|
||||||
|
implement the full auth stack: JWT verification via Firebase, app-specific identity
|
||||||
|
enrichment via a caller-supplied `IdentityEnricher`, and permission enforcement via
|
||||||
|
`rbac`. The output contract is always `rbac.Identity` — downstream code and business
|
||||||
|
logic are decoupled from Firebase types entirely.
|
||||||
|
|
||||||
|
## Tier & Dependencies
|
||||||
|
|
||||||
|
**Tier:** 4 (transport auth layer; depends on Tier 0 `rbac` and external Firebase SDK)
|
||||||
|
**Module:** `code.nochebuena.dev/go/httpauth-firebase`
|
||||||
|
**Direct imports:** `code.nochebuena.dev/go/rbac`, `firebase.google.com/go/v4/auth`
|
||||||
|
|
||||||
|
`httpauth-firebase` does not import `logz`, `httpmw`, or `httputil`. It has no
|
||||||
|
logger parameter — errors are returned as HTTP responses, not logged here.
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
- **Provider-specific naming** (ADR-001): The module is named `httpauth-firebase`
|
||||||
|
because it imports Firebase directly. Other providers live in sibling modules
|
||||||
|
(`httpauth-auth0`, etc.). All converge on `rbac.Identity`. The `TokenVerifier`
|
||||||
|
interface is retained for unit-test mockability only.
|
||||||
|
- **rbac.Identity as output contract** (ADR-002): `EnrichmentMiddleware` stores the
|
||||||
|
final identity with `rbac.SetInContext`; `AuthzMiddleware` reads it with
|
||||||
|
`rbac.FromContext`. Downstream handlers only need to import `rbac`. Global ADR-003
|
||||||
|
applies: `rbac` owns its context helpers.
|
||||||
|
- **Composable three-middleware stack** (ADR-003): `AuthMiddleware`,
|
||||||
|
`EnrichmentMiddleware`, and `AuthzMiddleware` are separate functions. Each can be
|
||||||
|
applied independently; each returns 401 if its upstream dependency is missing from
|
||||||
|
context.
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
**Full stack (most routes):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, []string{"/health", "/metrics/*"}))
|
||||||
|
r.Use(httpauth.EnrichmentMiddleware(userEnricher, httpauth.WithTenantHeader("X-Tenant-ID")))
|
||||||
|
|
||||||
|
// Per-route RBAC:
|
||||||
|
r.With(httpauth.AuthzMiddleware(permProvider, "orders", rbac.Write)).
|
||||||
|
Post("/orders", httputil.Handle(v, svc.CreateOrder))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token-only (webhook verification):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, nil))
|
||||||
|
// No enrichment, no authz — just verify the token is valid
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reading identity in a handler:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
identity, ok := rbac.FromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
// should not happen if EnrichmentMiddleware is in the chain
|
||||||
|
}
|
||||||
|
fmt.Println(identity.UID, identity.Role, identity.TenantID)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Public paths (bypass token check):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// path.Match patterns — supports * wildcard
|
||||||
|
publicPaths := []string{"/health", "/ready", "/metrics/*"}
|
||||||
|
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, publicPaths))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Interfaces the application must implement:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// IdentityEnricher: called by EnrichmentMiddleware
|
||||||
|
type MyEnricher struct{ db *sql.DB }
|
||||||
|
func (e *MyEnricher) Enrich(ctx context.Context, uid string, claims map[string]any) (rbac.Identity, error) {
|
||||||
|
user, err := e.db.LookupUser(ctx, uid)
|
||||||
|
if err != nil { return rbac.Identity{}, err }
|
||||||
|
return rbac.NewIdentity(uid, user.DisplayName, user.Email).WithRole(user.Role), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermissionProvider: called by AuthzMiddleware
|
||||||
|
type MyPermProvider struct{}
|
||||||
|
func (p *MyPermProvider) ResolveMask(ctx context.Context, uid, resource string) (rbac.PermissionMask, error) {
|
||||||
|
return rbac.ReadWrite, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## What to Avoid
|
||||||
|
|
||||||
|
- Do not use `AuthMiddleware` alone and assume the request is fully authorised.
|
||||||
|
Token verification only confirms the token is valid; it does not enforce
|
||||||
|
application-level roles or permissions.
|
||||||
|
- Do not put `AuthzMiddleware` before `EnrichmentMiddleware` in the chain.
|
||||||
|
`AuthzMiddleware` reads `rbac.Identity` from context; if enrichment has not run,
|
||||||
|
it will return 401.
|
||||||
|
- Do not read `firebase.Token` fields directly in business logic. The token UID and
|
||||||
|
claims are stored under unexported context keys and are not part of the public API.
|
||||||
|
Use `rbac.FromContext` to read the enriched identity.
|
||||||
|
- Do not store application-specific data in Firebase custom claims and bypass the
|
||||||
|
`IdentityEnricher`. Claims are read by `EnrichmentMiddleware` and passed to
|
||||||
|
`Enrich`, but the enricher is the correct place to resolve application identity —
|
||||||
|
not raw Firebase claims spread across handlers.
|
||||||
|
- Do not import `httpauth-firebase` from service or domain layers. It is a transport
|
||||||
|
package. Dependency should flow inward: handler → service, never service →
|
||||||
|
handler/middleware.
|
||||||
|
|
||||||
|
## Testing Notes
|
||||||
|
|
||||||
|
- `compliance_test.go` verifies at compile time that `*mockVerifier` satisfies
|
||||||
|
`TokenVerifier`, `*mockEnricher` satisfies `IdentityEnricher`, and `*mockProvider`
|
||||||
|
satisfies `PermissionProvider`.
|
||||||
|
- `AuthMiddleware` can be tested by injecting a `mockVerifier` that returns a
|
||||||
|
controlled `*auth.Token`. No real Firebase project is needed.
|
||||||
|
- `EnrichmentMiddleware` can be tested by pre-populating the request context with
|
||||||
|
`uid` via an upstream `AuthMiddleware` using a mock verifier.
|
||||||
|
- `AuthzMiddleware` can be tested by pre-populating the request context with an
|
||||||
|
`rbac.Identity` via `rbac.SetInContext` — no auth or enrichment middleware needed.
|
||||||
|
- Use `httptest.NewRecorder()` and `httptest.NewRequest()` for all HTTP-level tests.
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 NOCHEBUENADEV
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
82
README.md
Normal file
82
README.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# httpauth-firebase
|
||||||
|
|
||||||
|
Firebase-backed HTTP middleware for authentication, identity enrichment, and RBAC authorization.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Three composable `func(http.Handler) http.Handler` middleware functions:
|
||||||
|
|
||||||
|
| Middleware | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `AuthMiddleware` | Verifies Firebase Bearer token; injects uid + claims into context |
|
||||||
|
| `EnrichmentMiddleware` | Calls app-provided `IdentityEnricher`; stores `rbac.Identity` in context |
|
||||||
|
| `AuthzMiddleware` | Resolves permission mask; gates request |
|
||||||
|
|
||||||
|
All functions accept interfaces — testable without a live Firebase connection.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
require code.nochebuena.dev/go/httpauth-firebase v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.Use(httpauth.AuthMiddleware(firebaseAuthClient, []string{"/health", "/public/*"}))
|
||||||
|
r.Use(httpauth.EnrichmentMiddleware(myUserEnricher, httpauth.WithTenantHeader("X-Tenant-ID")))
|
||||||
|
r.Use(httpauth.AuthzMiddleware(myPermProvider, "orders", rbac.Read))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
### TokenVerifier
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TokenVerifier interface {
|
||||||
|
VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken string) (*auth.Token, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`*firebase/auth.Client` satisfies this directly. Swap in a mock for tests.
|
||||||
|
|
||||||
|
### IdentityEnricher
|
||||||
|
|
||||||
|
```go
|
||||||
|
type IdentityEnricher interface {
|
||||||
|
Enrich(ctx context.Context, uid string, claims map[string]any) (rbac.Identity, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement this in your application to load user data from your store and return an `rbac.Identity`.
|
||||||
|
|
||||||
|
### PermissionProvider
|
||||||
|
|
||||||
|
```go
|
||||||
|
type PermissionProvider interface {
|
||||||
|
ResolveMask(ctx context.Context, uid, resource string) (rbac.PermissionMask, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the permission bitmask for the user on a given resource.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|---|---|
|
||||||
|
| `WithTenantHeader(header)` | Reads `TenantID` from the named request header. If absent, `TenantID` remains `""`. |
|
||||||
|
|
||||||
|
## Public paths
|
||||||
|
|
||||||
|
`AuthMiddleware` skips token verification for requests matching any pattern in `publicPaths`. Patterns use `path.Match` semantics (e.g. `"/public/*"`).
|
||||||
|
|
||||||
|
## HTTP status codes
|
||||||
|
|
||||||
|
| Condition | Status |
|
||||||
|
|---|---|
|
||||||
|
| Missing or malformed `Authorization` header | 401 |
|
||||||
|
| Token verification failure | 401 |
|
||||||
|
| No `rbac.Identity` in context (AuthzMiddleware) | 401 |
|
||||||
|
| Missing uid in context (EnrichmentMiddleware) | 401 |
|
||||||
|
| Enricher error | 500 |
|
||||||
|
| Permission denied or provider error | 403 |
|
||||||
70
auth.go
Normal file
70
auth.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package httpauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"firebase.google.com/go/v4/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenVerifier abstracts Firebase JWT verification.
|
||||||
|
// *auth.Client satisfies this interface directly — no adapter needed in production.
|
||||||
|
// Retained solely for unit-test mockability.
|
||||||
|
type TokenVerifier interface {
|
||||||
|
VerifyIDTokenAndCheckRevoked(ctx context.Context, idToken string) (*auth.Token, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctxUIDKey and ctxClaimsKey are unexported typed context keys.
|
||||||
|
// Using distinct types prevents collisions with keys from other packages.
|
||||||
|
type ctxUIDKey struct{}
|
||||||
|
type ctxClaimsKey struct{}
|
||||||
|
|
||||||
|
func setTokenData(ctx context.Context, uid string, claims map[string]any) context.Context {
|
||||||
|
ctx = context.WithValue(ctx, ctxUIDKey{}, uid)
|
||||||
|
ctx = context.WithValue(ctx, ctxClaimsKey{}, claims)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUID(ctx context.Context) (string, bool) {
|
||||||
|
uid, ok := ctx.Value(ctxUIDKey{}).(string)
|
||||||
|
return uid, ok && uid != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClaims(ctx context.Context) (map[string]any, bool) {
|
||||||
|
claims, ok := ctx.Value(ctxClaimsKey{}).(map[string]any)
|
||||||
|
return claims, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMiddleware verifies the Bearer token and injects uid + claims into the
|
||||||
|
// request context. Requests to publicPaths are skipped without token verification
|
||||||
|
// (wildcards supported via path.Match). Returns 401 on missing or invalid tokens.
|
||||||
|
func AuthMiddleware(verifier TokenVerifier, publicPaths []string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for _, pattern := range publicPaths {
|
||||||
|
if matched, _ := path.Match(pattern, r.URL.Path); matched {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
|
||||||
|
decoded, err := verifier.VerifyIDTokenAndCheckRevoked(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := setTokenData(r.Context(), decoded.UID, decoded.Claims)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
37
authz.go
Normal file
37
authz.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package httpauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.nochebuena.dev/go/rbac"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PermissionProvider resolves the permission mask for a given uid on a resource.
|
||||||
|
type PermissionProvider interface {
|
||||||
|
ResolveMask(ctx context.Context, uid, resource string) (rbac.PermissionMask, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthzMiddleware reads the rbac.Identity from context (set by EnrichmentMiddleware)
|
||||||
|
// and gates the request against the required permission on resource.
|
||||||
|
// Returns 401 if no identity is in the context.
|
||||||
|
// Returns 403 if the identity lacks the required permission or if the provider errors.
|
||||||
|
func AuthzMiddleware(provider PermissionProvider, resource string, required rbac.Permission) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
identity, ok := rbac.FromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mask, err := provider.ResolveMask(r.Context(), identity.UID, resource)
|
||||||
|
if err != nil || !mask.Has(required) {
|
||||||
|
http.Error(w, "forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
33
compliance_test.go
Normal file
33
compliance_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package httpauth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"firebase.google.com/go/v4/auth"
|
||||||
|
|
||||||
|
httpauth "code.nochebuena.dev/go/httpauth-firebase"
|
||||||
|
"code.nochebuena.dev/go/rbac"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockVerifier struct{}
|
||||||
|
|
||||||
|
func (m *mockVerifier) VerifyIDTokenAndCheckRevoked(_ context.Context, _ string) (*auth.Token, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockEnricher struct{}
|
||||||
|
|
||||||
|
func (m *mockEnricher) Enrich(_ context.Context, _ string, _ map[string]any) (rbac.Identity, error) {
|
||||||
|
return rbac.Identity{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockProvider struct{}
|
||||||
|
|
||||||
|
func (m *mockProvider) ResolveMask(_ context.Context, _, _ string) (rbac.PermissionMask, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time interface satisfaction checks.
|
||||||
|
var _ httpauth.TokenVerifier = (*mockVerifier)(nil)
|
||||||
|
var _ httpauth.IdentityEnricher = (*mockEnricher)(nil)
|
||||||
|
var _ httpauth.PermissionProvider = (*mockProvider)(nil)
|
||||||
17
doc.go
Normal file
17
doc.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Package httpauth provides Firebase-backed HTTP middleware for authentication,
|
||||||
|
// identity enrichment, and role-based access control.
|
||||||
|
//
|
||||||
|
// Typical middleware chain:
|
||||||
|
//
|
||||||
|
// r.Use(httpauth.AuthMiddleware(firebaseClient, publicPaths))
|
||||||
|
// r.Use(httpauth.EnrichmentMiddleware(userEnricher, httpauth.WithTenantHeader("X-Tenant-ID")))
|
||||||
|
// r.Use(httpauth.AuthzMiddleware(permProvider, "orders", rbac.Read))
|
||||||
|
//
|
||||||
|
// AuthMiddleware verifies Firebase Bearer tokens and injects uid + claims into
|
||||||
|
// the request context. EnrichmentMiddleware reads those values, calls the
|
||||||
|
// app-provided IdentityEnricher, and stores the full rbac.Identity. AuthzMiddleware
|
||||||
|
// resolves the permission mask and gates the request.
|
||||||
|
//
|
||||||
|
// All three middleware functions accept interfaces, so they can be tested without
|
||||||
|
// a live Firebase connection.
|
||||||
|
package httpauth
|
||||||
49
docs/adr/ADR-001-provider-specific-naming.md
Normal file
49
docs/adr/ADR-001-provider-specific-naming.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ADR-001: Provider-Specific Module Naming (httpauth-firebase)
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-03-18
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
An auth middleware module originally named `httpauth` was designed to be
|
||||||
|
provider-agnostic: it defined a `TokenVerifier` interface and duck-typed
|
||||||
|
`firebase.Client` so the module never imported Firebase directly. The intent was
|
||||||
|
that any JWT provider could be supported by implementing `TokenVerifier`.
|
||||||
|
|
||||||
|
Under global ADR-001, framework modules import their named dependencies directly
|
||||||
|
rather than duck-typing them away. An auth module for Firebase should import
|
||||||
|
`firebase.google.com/go/v4/auth` directly, since that is its explicit purpose.
|
||||||
|
This raises the naming question: if the module imports Firebase, should it still be
|
||||||
|
called `httpauth`?
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The module is named **`httpauth-firebase`**:
|
||||||
|
|
||||||
|
- Go module path: `code.nochebuena.dev/go/httpauth-firebase`
|
||||||
|
- Directory: `micro-lib/httpauth-firebase/`
|
||||||
|
- Package name: `httpauth` (short import alias; the module path carries the provider name)
|
||||||
|
|
||||||
|
A different auth provider would live in `httpauth-auth0`, `httpauth-jwks`, etc.
|
||||||
|
All `httpauth-*` modules converge on the same output contract: `rbac.Identity` stored
|
||||||
|
in context via `rbac.SetInContext`.
|
||||||
|
|
||||||
|
The `TokenVerifier` interface is retained **solely for unit-test mockability** —
|
||||||
|
not for provider abstraction. In production, `*auth.Client` from
|
||||||
|
`firebase.google.com/go/v4/auth` satisfies `TokenVerifier` directly without any
|
||||||
|
adapter.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The module name is honest: scanning the module list, a developer immediately knows
|
||||||
|
this is Firebase-specific and not a generic auth abstraction.
|
||||||
|
- Other auth providers are accommodated by creating sibling modules
|
||||||
|
(`httpauth-auth0`, etc.) with identical output contracts, not by implementing an
|
||||||
|
interface in this module.
|
||||||
|
- The `TokenVerifier` interface remains exported so test code outside the package can
|
||||||
|
implement it (`mockVerifier` in `compliance_test.go`). Its presence does not imply
|
||||||
|
that swapping providers at runtime is a supported pattern.
|
||||||
|
- Applications that switch from Firebase to another provider replace the module
|
||||||
|
import; the rest of the auth stack (`EnrichmentMiddleware`, `AuthzMiddleware`) and
|
||||||
|
all business logic remain unchanged because they depend on `rbac.Identity`, not on
|
||||||
|
Firebase types.
|
||||||
55
docs/adr/ADR-002-rbac-identity-output-contract.md
Normal file
55
docs/adr/ADR-002-rbac-identity-output-contract.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# ADR-002: rbac.Identity as the Output Contract
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-03-18
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`AuthMiddleware` verifies a Firebase JWT and extracts a UID and claims map from the
|
||||||
|
decoded token. Downstream code needs a richer identity: application-specific role,
|
||||||
|
tenant, display name, email. Several design options exist:
|
||||||
|
|
||||||
|
1. Expose Firebase token fields (`auth.Token`) directly in context — ties all
|
||||||
|
downstream code to Firebase types.
|
||||||
|
2. Define a custom identity struct in `httpauth-firebase` — decouples from Firebase
|
||||||
|
but creates a module-specific contract that other `httpauth-*` modules cannot share.
|
||||||
|
3. Use `rbac.Identity` as the shared identity type — all `httpauth-*` modules
|
||||||
|
produce the same type; downstream code and `AuthzMiddleware` depend on `rbac`
|
||||||
|
only, not on any Firebase types.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
`EnrichmentMiddleware` calls `rbac.SetInContext(ctx, identity)` to store the
|
||||||
|
enriched identity. `AuthzMiddleware` reads it with `rbac.FromContext(ctx)`.
|
||||||
|
Downstream business logic and service handlers use `rbac.FromContext` directly —
|
||||||
|
they never import `httpauth-firebase`.
|
||||||
|
|
||||||
|
The flow is:
|
||||||
|
|
||||||
|
1. `AuthMiddleware` (this module): verifies Firebase JWT → stores `uid` + `claims`
|
||||||
|
in context under unexported keys local to this package.
|
||||||
|
2. `EnrichmentMiddleware` (this module): reads `uid` + `claims` → calls
|
||||||
|
`IdentityEnricher.Enrich` → stores `rbac.Identity` via `rbac.SetInContext`.
|
||||||
|
3. `AuthzMiddleware` (this module): reads `rbac.Identity` via `rbac.FromContext` →
|
||||||
|
calls `PermissionProvider.ResolveMask` → allows or rejects.
|
||||||
|
|
||||||
|
The intermediate `uid` + `claims` context values are stored under unexported typed
|
||||||
|
keys (`ctxUIDKey{}`, `ctxClaimsKey{}`). They are internal to `httpauth-firebase`
|
||||||
|
and not part of the public API.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Business logic and service layers only need to import `rbac` to read the caller's
|
||||||
|
identity. They have no knowledge of Firebase, JWTs, or token claims.
|
||||||
|
- Switching from Firebase to another provider (e.g. Auth0) requires replacing the
|
||||||
|
`AuthMiddleware` module. `EnrichmentMiddleware`, `AuthzMiddleware`, and all
|
||||||
|
downstream code remain unchanged because they operate on `rbac.Identity`.
|
||||||
|
- `IdentityEnricher` is the application's extension point: it receives the Firebase
|
||||||
|
UID and raw claims and returns a fully populated `rbac.Identity` with role and
|
||||||
|
tenant. This is the only place where app-specific user store queries should occur.
|
||||||
|
- `rbac.Identity.WithTenant(tenantID)` is called in `EnrichmentMiddleware` if a
|
||||||
|
tenant header is configured. The base identity from `Enrich` is immutable; a new
|
||||||
|
value is returned.
|
||||||
|
- Global ADR-003 (context helpers live with data owners) is applied here: `rbac`
|
||||||
|
owns `SetInContext` and `FromContext` because `rbac.Identity` is a RBAC concern,
|
||||||
|
not an auth transport concern.
|
||||||
60
docs/adr/ADR-003-composable-middleware-stack.md
Normal file
60
docs/adr/ADR-003-composable-middleware-stack.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# ADR-003: Composable Three-Middleware Auth Stack
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-03-18
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
HTTP authentication and authorisation involve at least three distinct concerns:
|
||||||
|
|
||||||
|
1. **Token verification**: Is this JWT valid and non-revoked? Who issued it?
|
||||||
|
2. **Identity enrichment**: Given the verified UID, what role and tenant does this
|
||||||
|
user have in the application?
|
||||||
|
3. **Permission enforcement**: Does this identity have the required permission on
|
||||||
|
this resource?
|
||||||
|
|
||||||
|
A single "auth middleware" that handles all three concerns is a common pattern, but
|
||||||
|
it creates problems:
|
||||||
|
|
||||||
|
- Enrichment requires a user-store lookup. Not every route needs enrichment (e.g.
|
||||||
|
public endpoints that only verify the token is structurally valid).
|
||||||
|
- Permission enforcement is per-resource and per-route. A single monolithic
|
||||||
|
middleware cannot vary by route without routing logic inside it.
|
||||||
|
- Testing each concern in isolation becomes difficult when they are combined.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Three separate middleware functions are provided:
|
||||||
|
|
||||||
|
- **`AuthMiddleware(verifier, publicPaths)`**: Verifies the Bearer token using
|
||||||
|
Firebase. Stores `uid` and `claims` in context. Returns 401 on missing or invalid
|
||||||
|
token. Routes matching `publicPaths` (glob patterns) are bypassed entirely.
|
||||||
|
- **`EnrichmentMiddleware(enricher, opts...)`**: Reads `uid` + `claims` from context.
|
||||||
|
Calls `IdentityEnricher.Enrich` (app-provided). Stores `rbac.Identity` in context.
|
||||||
|
Returns 401 if no UID is present (i.e. `AuthMiddleware` was not in the chain).
|
||||||
|
Optionally reads a tenant ID from a configurable request header.
|
||||||
|
- **`AuthzMiddleware(provider, resource, required)`**: Reads `rbac.Identity` from
|
||||||
|
context. Calls `PermissionProvider.ResolveMask` to get the permission mask for the
|
||||||
|
identity's UID on the named resource. Returns 401 if no identity is present, 403
|
||||||
|
if the mask does not include the required permission.
|
||||||
|
|
||||||
|
Each function returns a `func(http.Handler) http.Handler`, composable with any
|
||||||
|
standard middleware stack.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Each middleware can be tested in isolation. `AuthMiddleware` tests do not need a
|
||||||
|
real user store; `EnrichmentMiddleware` tests do not need a Firebase client;
|
||||||
|
`AuthzMiddleware` tests do not need either.
|
||||||
|
- Routes can apply only the subset they need. A webhook endpoint that only verifies
|
||||||
|
token validity can use `AuthMiddleware` alone. A read-only endpoint can use
|
||||||
|
`AuthMiddleware` + `EnrichmentMiddleware` without `AuthzMiddleware`.
|
||||||
|
- The order constraint is enforced by dependency: `EnrichmentMiddleware` depends on
|
||||||
|
values written by `AuthMiddleware`; `AuthzMiddleware` depends on values written by
|
||||||
|
`EnrichmentMiddleware`. Incorrect ordering produces 401 (missing UID or identity)
|
||||||
|
rather than a silent bug.
|
||||||
|
- `publicPaths` in `AuthMiddleware` uses `path.Match` for glob matching. Patterns
|
||||||
|
like `/health`, `/metrics/*` can be listed to skip token verification entirely.
|
||||||
|
- `IdentityEnricher` and `PermissionProvider` are interfaces injected at
|
||||||
|
construction time, making the middleware fully testable without a real Firebase
|
||||||
|
connection or database.
|
||||||
66
enrichment.go
Normal file
66
enrichment.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package httpauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.nochebuena.dev/go/rbac"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IdentityEnricher builds an rbac.Identity from verified token claims.
|
||||||
|
// The application provides the implementation — typically reads from a user store.
|
||||||
|
type IdentityEnricher interface {
|
||||||
|
Enrich(ctx context.Context, uid string, claims map[string]any) (rbac.Identity, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnrichOpt configures EnrichmentMiddleware.
|
||||||
|
type EnrichOpt func(*enrichConfig)
|
||||||
|
|
||||||
|
type enrichConfig struct {
|
||||||
|
tenantHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTenantHeader configures the request header from which TenantID is read.
|
||||||
|
// If the header is absent on a request, TenantID remains "" — no error is returned.
|
||||||
|
func WithTenantHeader(header string) EnrichOpt {
|
||||||
|
return func(c *enrichConfig) {
|
||||||
|
c.tenantHeader = header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnrichmentMiddleware reads uid + claims injected by AuthMiddleware, calls
|
||||||
|
// enricher.Enrich to build a full rbac.Identity, and stores it in the context.
|
||||||
|
// Returns 401 if no uid is present (AuthMiddleware was not in the chain).
|
||||||
|
// Returns 500 if the enricher fails.
|
||||||
|
func EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler {
|
||||||
|
cfg := &enrichConfig{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(cfg)
|
||||||
|
}
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid, ok := getUID(r.Context())
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, _ := getClaims(r.Context())
|
||||||
|
|
||||||
|
identity, err := enricher.Enrich(r.Context(), uid, claims)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.tenantHeader != "" {
|
||||||
|
if tenantID := r.Header.Get(cfg.tenantHeader); tenantID != "" {
|
||||||
|
identity = identity.WithTenant(tenantID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := rbac.SetInContext(r.Context(), identity)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
39
go.mod
Normal file
39
go.mod
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
module code.nochebuena.dev/go/httpauth-firebase
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
code.nochebuena.dev/go/rbac v0.9.0
|
||||||
|
firebase.google.com/go/v4 v4.15.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/compute v1.24.0 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||||
|
golang.org/x/crypto v0.21.0 // indirect
|
||||||
|
golang.org/x/net v0.23.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.18.0 // indirect
|
||||||
|
golang.org/x/sync v0.6.0 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
google.golang.org/api v0.170.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/appengine/v2 v2.0.2 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect
|
||||||
|
google.golang.org/grpc v1.62.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
|
)
|
||||||
183
go.sum
Normal file
183
go.sum
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
|
||||||
|
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
|
code.nochebuena.dev/go/rbac v0.9.0 h1:2fQngWIOeluIaMmo+H2ajT0NVw8GjNFJVi6pbdB3f/o=
|
||||||
|
code.nochebuena.dev/go/rbac v0.9.0/go.mod h1:LzW8tTJmdbu6HHN26NZZ3HzzdlZAd1sp6aml25Cfz5c=
|
||||||
|
firebase.google.com/go/v4 v4.15.0 h1:k27M+cHbyN1YpBI2Cf4NSjeHnnYRB9ldXwpqA5KikN0=
|
||||||
|
firebase.google.com/go/v4 v4.15.0/go.mod h1:S/4MJqVZn1robtXkHhpRUbwOC4gdYtgsiMMJQ4x+xmQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||||
|
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||||
|
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||||
|
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||||
|
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||||
|
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||||
|
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
|
||||||
|
google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
|
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||||
|
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
|
||||||
|
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
|
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||||
|
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
261
httpauth_test.go
Normal file
261
httpauth_test.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package httpauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"firebase.google.com/go/v4/auth"
|
||||||
|
|
||||||
|
"code.nochebuena.dev/go/rbac"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- mocks ---
|
||||||
|
|
||||||
|
type mockVerifier struct {
|
||||||
|
token *auth.Token
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockVerifier) VerifyIDTokenAndCheckRevoked(_ context.Context, _ string) (*auth.Token, error) {
|
||||||
|
return m.token, m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockEnricher struct {
|
||||||
|
identity rbac.Identity
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockEnricher) Enrich(_ context.Context, _ string, _ map[string]any) (rbac.Identity, error) {
|
||||||
|
return m.identity, m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockProvider struct {
|
||||||
|
mask rbac.PermissionMask
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockProvider) ResolveMask(_ context.Context, _, _ string) (rbac.PermissionMask, error) {
|
||||||
|
return m.mask, m.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// testRead is permission bit 0, used in authz tests.
|
||||||
|
const testRead rbac.Permission = 0
|
||||||
|
|
||||||
|
func chain(mw func(http.Handler) http.Handler, h http.HandlerFunc) http.Handler {
|
||||||
|
return mw(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// injectUID bypasses AuthMiddleware for EnrichmentMiddleware tests.
|
||||||
|
func injectUID(uid string, claims map[string]any, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := setTokenData(r.Context(), uid, claims)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AuthMiddleware ---
|
||||||
|
|
||||||
|
func TestAuthMiddleware_ValidToken(t *testing.T) {
|
||||||
|
mv := &mockVerifier{token: &auth.Token{UID: "uid123", Claims: map[string]any{"name": "Alice"}}}
|
||||||
|
var capturedUID string
|
||||||
|
h := chain(AuthMiddleware(mv, nil), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
capturedUID, _ = getUID(r.Context())
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer valid-token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("want 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if capturedUID != "uid123" {
|
||||||
|
t.Errorf("want uid123, got %q", capturedUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_InvalidToken(t *testing.T) {
|
||||||
|
mv := &mockVerifier{err: errors.New("token invalid")}
|
||||||
|
h := chain(AuthMiddleware(mv, nil), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer bad-token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Errorf("want 401, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_MissingHeader(t *testing.T) {
|
||||||
|
mv := &mockVerifier{}
|
||||||
|
h := chain(AuthMiddleware(mv, nil), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api", nil))
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Errorf("want 401, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_PublicPath(t *testing.T) {
|
||||||
|
mv := &mockVerifier{err: errors.New("should not be called")}
|
||||||
|
h := chain(AuthMiddleware(mv, []string{"/health"}), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/health", nil))
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("want 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_PublicPathWildcard(t *testing.T) {
|
||||||
|
mv := &mockVerifier{err: errors.New("should not be called")}
|
||||||
|
h := chain(AuthMiddleware(mv, []string{"/public/*"}), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/public/resource", nil))
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("want 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- EnrichmentMiddleware ---
|
||||||
|
|
||||||
|
func TestEnrichmentMiddleware_Success(t *testing.T) {
|
||||||
|
me := &mockEnricher{identity: rbac.NewIdentity("uid123", "Alice", "alice@example.com")}
|
||||||
|
var capturedIdentity rbac.Identity
|
||||||
|
inner := EnrichmentMiddleware(me)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
capturedIdentity, _ = rbac.FromContext(r.Context())
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
h := injectUID("uid123", nil, inner)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("want 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if capturedIdentity.UID != "uid123" {
|
||||||
|
t.Errorf("want uid123, got %q", capturedIdentity.UID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnrichmentMiddleware_NoUID(t *testing.T) {
|
||||||
|
me := &mockEnricher{}
|
||||||
|
h := chain(EnrichmentMiddleware(me), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Errorf("want 401, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnrichmentMiddleware_EnricherError(t *testing.T) {
|
||||||
|
me := &mockEnricher{err: errors.New("db error")}
|
||||||
|
inner := EnrichmentMiddleware(me)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
h := injectUID("uid123", nil, inner)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
if rec.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("want 500, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnrichmentMiddleware_WithTenant(t *testing.T) {
|
||||||
|
me := &mockEnricher{identity: rbac.NewIdentity("uid123", "", "")}
|
||||||
|
var capturedIdentity rbac.Identity
|
||||||
|
inner := EnrichmentMiddleware(me, WithTenantHeader("X-Tenant-ID"))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
capturedIdentity, _ = rbac.FromContext(r.Context())
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
h := injectUID("uid123", nil, inner)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("X-Tenant-ID", "tenant-abc")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if capturedIdentity.TenantID != "tenant-abc" {
|
||||||
|
t.Errorf("want tenant-abc, got %q", capturedIdentity.TenantID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnrichmentMiddleware_NoTenantHeader(t *testing.T) {
|
||||||
|
me := &mockEnricher{identity: rbac.NewIdentity("uid123", "", "")}
|
||||||
|
var capturedIdentity rbac.Identity
|
||||||
|
inner := EnrichmentMiddleware(me, WithTenantHeader("X-Tenant-ID"))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
capturedIdentity, _ = rbac.FromContext(r.Context())
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
h := injectUID("uid123", nil, inner)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
if capturedIdentity.TenantID != "" {
|
||||||
|
t.Errorf("want empty TenantID, got %q", capturedIdentity.TenantID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AuthzMiddleware ---
|
||||||
|
|
||||||
|
func TestAuthzMiddleware_Allowed(t *testing.T) {
|
||||||
|
mp := &mockProvider{mask: rbac.PermissionMask(0).Grant(testRead)}
|
||||||
|
inner := AuthzMiddleware(mp, "orders", testRead)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
ctx := rbac.SetInContext(req.Context(), rbac.NewIdentity("uid123", "", ""))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
inner.ServeHTTP(rec, req.WithContext(ctx))
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("want 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthzMiddleware_Denied(t *testing.T) {
|
||||||
|
mp := &mockProvider{mask: rbac.PermissionMask(0)} // no permissions granted
|
||||||
|
inner := AuthzMiddleware(mp, "orders", testRead)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
ctx := rbac.SetInContext(req.Context(), rbac.NewIdentity("uid123", "", ""))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
inner.ServeHTTP(rec, req.WithContext(ctx))
|
||||||
|
if rec.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("want 403, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthzMiddleware_NoIdentity(t *testing.T) {
|
||||||
|
mp := &mockProvider{}
|
||||||
|
h := chain(AuthzMiddleware(mp, "orders", testRead), func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Errorf("want 401, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthzMiddleware_ProviderError(t *testing.T) {
|
||||||
|
mp := &mockProvider{err: errors.New("db error")}
|
||||||
|
inner := AuthzMiddleware(mp, "orders", testRead)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
ctx := rbac.SetInContext(req.Context(), rbac.NewIdentity("uid123", "", ""))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
inner.ServeHTTP(rec, req.WithContext(ctx))
|
||||||
|
if rec.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("want 403, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user