CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 28s
CI / go (push) Failing after 24s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
- Added endpoints for managing API keys, including creation, retrieval, updating, and revocation. - Introduced a new Auth session endpoint to retrieve current tenant and role information. - Updated the authentication middleware to support API key-based authentication and track last used timestamps. - Enhanced documentation to reflect new API key functionalities and usage guidelines. - Improved logging for demo authentication scenarios.
128 lines
3.1 KiB
Go
128 lines
3.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type ctxKey int
|
|
|
|
const authCtxKey ctxKey = 1
|
|
|
|
// Auth holds resolved API identity for a request.
|
|
type Auth struct {
|
|
TenantID string
|
|
Role string // viewer, editor, operator, node
|
|
Token string
|
|
APIKeyID string // non-empty for DB-managed keys
|
|
}
|
|
|
|
func authFromContext(ctx context.Context) (Auth, bool) {
|
|
a, ok := ctx.Value(authCtxKey).(Auth)
|
|
return a, ok
|
|
}
|
|
|
|
type apiKeyRecord struct {
|
|
token string
|
|
tenantID string
|
|
role string
|
|
keyID string // set for DB-managed keys (last_used_at)
|
|
}
|
|
|
|
func parseAPIKeysSpec(spec string) []apiKeyRecord {
|
|
spec = strings.TrimSpace(spec)
|
|
if spec == "" {
|
|
return nil
|
|
}
|
|
var out []apiKeyRecord
|
|
for _, part := range strings.Split(spec, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
fields := strings.Split(part, "|")
|
|
if len(fields) != 3 {
|
|
continue
|
|
}
|
|
out = append(out, apiKeyRecord{
|
|
token: strings.TrimSpace(fields[0]),
|
|
tenantID: strings.TrimSpace(fields[1]),
|
|
role: strings.TrimSpace(fields[2]),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
h := r.Header.Get("Authorization")
|
|
const p = "Bearer "
|
|
if !strings.HasPrefix(h, p) {
|
|
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing or invalid bearer token")
|
|
return
|
|
}
|
|
raw := strings.TrimSpace(strings.TrimPrefix(h, p))
|
|
if raw == "dev" {
|
|
if a, ok := s.devAuth(); ok {
|
|
r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a))
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
matched, ok := s.keyResolver.Lookup(raw)
|
|
if !ok {
|
|
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "unknown api key")
|
|
return
|
|
}
|
|
a := Auth{TenantID: matched.tenantID, Role: matched.role, Token: raw, APIKeyID: matched.keyID}
|
|
if matched.keyID != "" {
|
|
go func(id string) { _ = s.store.TouchAPIKeyLastUsed(id) }(matched.keyID)
|
|
}
|
|
r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a))
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (s *Server) devAuth() (Auth, bool) {
|
|
tid, _, _, _, _ := s.store.DemoIDs()
|
|
if tid == "" {
|
|
return Auth{}, false
|
|
}
|
|
return Auth{TenantID: tid, Role: "operator", Token: "dev"}, true
|
|
}
|
|
|
|
func roleLevel(role string) int {
|
|
switch strings.ToLower(role) {
|
|
case "viewer":
|
|
return 1
|
|
case "editor":
|
|
return 2
|
|
case "operator":
|
|
return 3
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// requireAtLeast rejects node role and enforces viewer/editor/operator ladder.
|
|
func (s *Server) requireAtLeast(w http.ResponseWriter, a Auth, need string) bool {
|
|
if strings.ToLower(a.Role) == "node" {
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "node role cannot access this resource")
|
|
return false
|
|
}
|
|
if roleLevel(a.Role) < roleLevel(need) {
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "insufficient role")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Server) requireNode(w http.ResponseWriter, a Auth) bool {
|
|
if strings.ToLower(a.Role) != "node" {
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "node role required")
|
|
return false
|
|
}
|
|
return true
|
|
}
|