CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 28s
CI / web (push) Successful in 1m0s
CI / go (push) Successful in 1m11s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 4m5s
Updated the settings query options to eliminate the tenant ID parameter, streamlining the settings retrieval process. Adjusted the TenantSettingsComponent to reflect this change, ensuring it now queries settings without relying on tenant-specific data. This refactor enhances code clarity and reduces complexity in the settings management flow.
144 lines
3.5 KiB
Go
144 lines
3.5 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", the demo shortcut (devAuth) takes precedence when demo-seed
|
|
// is available; env/DB mapping is used only when demo tenant is absent.
|
|
func (s *Server) resolveAuth(raw string) (Auth, bool) {
|
|
if raw == "dev" {
|
|
if a, ok := s.devAuth(); ok {
|
|
return a, true
|
|
}
|
|
if rec, ok := s.keyResolver.Lookup(raw); ok {
|
|
return authFromKeyRecord(raw, rec), 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
|
|
}
|