auth: switch user login to session token and decouple tenant access

This commit is contained in:
2026-03-03 19:45:09 +08:00
parent 67bc6ecae6
commit 3b555df56c
6 changed files with 795 additions and 147 deletions

View File

@@ -91,80 +91,62 @@ func main() {
srv := server.New(cfg)
srv.StartCleanup()
// Admin-only Middleware
// Admin-only Middleware (master token only)
adminMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/auth/login" {
next(w, r)
return
}
authHeader := r.Header.Get("Authorization")
valid := authHeader == fmt.Sprintf("Bearer %d", cfg.Token)
if !valid {
ac, ok := srv.ResolveAccess(r, cfg.Token)
if !ok || ac.Kind != "master" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
return
}
// RBAC: admin only
if srv.Store() != nil {
if u, err := srv.Store().GetUserByToken(server.BearerToken(r)); err == nil && u != nil {
if u.Status != 1 || u.Role != "admin" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, `{"error":403,"message":"forbidden"}`)
return
}
}
}
next(w, r)
}
}
// Tenant or Admin Middleware
// Tenant or Admin Middleware (session/apikey/master)
tenantMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/auth/login" {
next(w, r)
return
}
authHeader := r.Header.Get("Authorization")
if authHeader == fmt.Sprintf("Bearer %d", cfg.Token) {
next(w, r)
ac, ok := srv.ResolveAccess(r, cfg.Token)
if !ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
return
}
// check API key + RBAC
if srv.Store() != nil {
if ten, err := srv.Store().VerifyAPIKey(server.BearerToken(r)); err == nil && ten != nil {
// role check if user exists
if u, err := srv.Store().GetUserByToken(server.BearerToken(r)); err == nil && u != nil {
if u.Status != 1 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, `{"error":403,"message":"forbidden"}`)
return
}
if u.Role == "operator" {
path := r.URL.Path
if path != "/api/v1/nodes" && path != "/api/v1/sdwans" && path != "/api/v1/sdwan/edit" && path != "/api/v1/connect" && path != "/api/v1/nodes/apps" && path != "/api/v1/nodes/kick" && path != "/api/v1/stats" && path != "/api/v1/health" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, `{"error":403,"message":"forbidden"}`)
return
}
}
}
next(w, r)
if ac.Kind == "session" && ac.Role == "operator" {
path := r.URL.Path
if path != "/api/v1/nodes" && path != "/api/v1/sdwans" && path != "/api/v1/sdwan/edit" && path != "/api/v1/connect" && path != "/api/v1/nodes/apps" && path != "/api/v1/nodes/kick" && path != "/api/v1/stats" && path != "/api/v1/health" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, `{"error":403,"message":"forbidden"}`)
return
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
return
next(w, r)
}
}
getTenantID := func(r *http.Request) int64 {
tok := server.BearerToken(r)
if tok == "" {
return 0
}
if ac, ok := srv.ResolveTenantAccessToken(tok); ok && ac.Kind != "master" {
return ac.TenantID
}
return 0
}
mux := http.NewServeMux()
mux.HandleFunc("/ws", srv.HandleWS)
@@ -201,7 +183,7 @@ func main() {
_ = json.Unmarshal(body, &reqTok)
_ = json.Unmarshal(body, &reqUser)
// --- user login ---
// --- user login (session token) ---
if reqUser.TenantID > 0 && reqUser.Username != "" && reqUser.Password != "" {
if srv.Store() == nil {
w.Header().Set("Content-Type", "application/json")
@@ -216,23 +198,24 @@ func main() {
fmt.Fprintf(w, `{"error":1,"message":"invalid credentials"}`)
return
}
// issue API key for this tenant and return subnet
key, err := srv.Store().CreateAPIKey(reqUser.TenantID, "all", 0)
sessionToken, exp, err := srv.Store().CreateSessionToken(u.ID, reqUser.TenantID, u.Role, 24*time.Hour)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"error":1,"message":"create token failed"}`)
fmt.Fprintf(w, `{"error":1,"message":"create session failed"}`)
return
}
ten, _ := srv.Store().GetTenantByID(reqUser.TenantID)
resp := struct {
Error int `json:"error"`
Message string `json:"message"`
Token string `json:"token"`
Role string `json:"role"`
Status int `json:"status"`
Subnet string `json:"subnet"`
}{0, "ok", key, u.Role, u.Status, ""}
Error int `json:"error"`
Message string `json:"message"`
Token string `json:"token"`
TokenType string `json:"token_type"`
ExpiresAt int64 `json:"expires_at"`
Role string `json:"role"`
Status int `json:"status"`
Subnet string `json:"subnet"`
}{0, "ok", sessionToken, "session", exp, u.Role, u.Status, ""}
if ten != nil {
resp.Subnet = ten.Subnet
}
@@ -289,13 +272,8 @@ func main() {
return
}
w.Header().Set("Content-Type", "application/json")
// tenant filter by API key
tenantID := int64(0)
if srv.Store() != nil {
if ten, err := srv.Store().VerifyAPIKey(server.BearerToken(r)); err == nil && ten != nil {
tenantID = ten.ID
}
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 {
nodes := srv.GetOnlineNodesByTenant(tenantID)
_ = json.NewEncoder(w).Encode(map[string]any{"nodes": nodes})
@@ -311,13 +289,8 @@ func main() {
return
}
w.Header().Set("Content-Type", "application/json")
// tenant filter by API key
tenantID := int64(0)
if srv.Store() != nil {
if ten, err := srv.Store().VerifyAPIKey(server.BearerToken(r)); err == nil && ten != nil {
tenantID = ten.ID
}
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 {
_ = json.NewEncoder(w).Encode(srv.GetSDWANTenant(tenantID))
return
@@ -335,13 +308,8 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// tenant filter by API key
tenantID := int64(0)
if srv.Store() != nil {
if ten, err := srv.Store().VerifyAPIKey(server.BearerToken(r)); err == nil && ten != nil {
tenantID = ten.ID
}
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 {
if err := srv.SetSDWANTenant(tenantID, req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -378,13 +346,8 @@ func main() {
http.Error(w, "node not found", http.StatusNotFound)
return
}
// tenant filter by API key
tenantID := int64(0)
if srv.Store() != nil {
if ten, err := srv.Store().VerifyAPIKey(server.BearerToken(r)); err == nil && ten != nil {
tenantID = ten.ID
}
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 && node.TenantID != tenantID {
http.Error(w, "node not found", http.StatusNotFound)
return
@@ -413,13 +376,8 @@ func main() {
http.Error(w, "node not found or offline", http.StatusNotFound)
return
}
// tenant filter by API key
tenantID := int64(0)
if srv.Store() != nil {
if ten, err := srv.Store().VerifyAPIKey(server.BearerToken(r)); err == nil && ten != nil {
tenantID = ten.ID
}
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 && node.TenantID != tenantID {
http.Error(w, "node not found", http.StatusNotFound)
return
@@ -451,13 +409,8 @@ func main() {
http.Error(w, "source node offline", http.StatusNotFound)
return
}
// tenant filter by API key
tenantID := int64(0)
if srv.Store() != nil {
if ten, err := srv.Store().VerifyAPIKey(server.BearerToken(r)); err == nil && ten != nil {
tenantID = ten.ID
}
}
// tenant filter by session/apikey
tenantID := getTenantID(r)
if tenantID > 0 && fromNode.TenantID != tenantID {
http.Error(w, "node not found", http.StatusNotFound)
return
@@ -529,10 +482,10 @@ func main() {
// Enable TCP keepalive at server level
httpSrv := &http.Server{
Handler: mux,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
if err := httpSrv.Serve(ln); err != http.ErrServerClosed {