Files
EvoBGP/internal/httpapi/auth.go
T
Denozordec db79820df0
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 28s
CI / web (push) Successful in 57s
CI / go (push) Successful in 1m9s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 3m57s
feat(auth): introduce demo token support and enhance API token handling
Added a local demo token for development purposes and improved the API token management by normalizing input tokens. Updated the authentication flow to utilize the new token handling, allowing for better session management and user experience. Enhanced the settings component to support the demo token and provide clear instructions for its use in local development.
2026-07-06 22:55:30 +07:00

143 lines
3.4 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))
a, ok := s.resolveAuth(raw)
if !ok {
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "unknown api key")
return
}
if a.APIKeyID != "" {
go func(id string) { _ = s.store.TouchAPIKeyLastUsed(id) }(a.APIKeyID)
}
r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a))
next.ServeHTTP(w, r)
})
}
func authFromKeyRecord(raw string, rec apiKeyRecord) Auth {
return Auth{TenantID: rec.tenantID, Role: rec.role, Token: raw, APIKeyID: rec.keyID}
}
// resolveAuth maps a bearer token to tenant identity.
// For the literal token "dev", env/DB keys take precedence over the demo shortcut (devAuth).
func (s *Server) resolveAuth(raw string) (Auth, bool) {
if raw == "dev" {
if rec, ok := s.keyResolver.Lookup(raw); ok {
return authFromKeyRecord(raw, rec), true
}
if a, ok := s.devAuth(); ok {
return a, true
}
return Auth{}, false
}
rec, ok := s.keyResolver.Lookup(raw)
if !ok {
return Auth{}, false
}
return authFromKeyRecord(raw, rec), true
}
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
}