CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m15s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 3m59s
Introduced a comprehensive firewall blocklist feature, allowing for the management of firewall clients and their associated rules. This includes endpoints for enrolling clients, listing clients and rules, and reporting apply statuses. Enhanced the API to support firewall operations, including the ability to handle block/accept policies. Updated the documentation to reflect these changes and added necessary components in the web UI for better user interaction. Additionally, modified the agent server to support firewall failover and integrated firewall functionality into the existing architecture.
162 lines
4.2 KiB
Go
162 lines
4.2 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"evobgp/internal/authkey"
|
|
)
|
|
|
|
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 {
|
|
if s.firewallResolver != nil {
|
|
if fw, ok := s.firewallResolver.Lookup(raw); ok {
|
|
return Auth{TenantID: fw.tenantID, Role: "firewall", Token: raw, APIKeyID: fw.clientID}, true
|
|
}
|
|
}
|
|
if client, err := s.store.LookupFirewallClientByTokenHash(authkey.HashToken(raw)); err == nil {
|
|
return Auth{TenantID: client.TenantID, Role: "firewall", Token: raw, APIKeyID: client.ID}, true
|
|
}
|
|
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) requireFirewall(w http.ResponseWriter, a Auth) bool {
|
|
if strings.ToLower(a.Role) != "firewall" {
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "firewall client role required")
|
|
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
|
|
}
|