Files
EvoBGP/internal/httpapi/auth.go
T
Denozordec 6329a4df27
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
feat(api): implement API key management and authentication enhancements
- 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.
2026-05-21 11:26:17 +07:00

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
}