diff --git a/cmd/inp2ps/main.go b/cmd/inp2ps/main.go index 2523f0b..2676d22 100644 --- a/cmd/inp2ps/main.go +++ b/cmd/inp2ps/main.go @@ -106,6 +106,17 @@ func main() { 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) } } @@ -122,9 +133,27 @@ func main() { next(w, r) return } - // check API key + // 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) return } @@ -146,36 +175,88 @@ func main() { // Tenant APIs (API key auth inside handlers) mux.HandleFunc("/api/v1/admin/tenants", adminMiddleware(srv.HandleAdminCreateTenant)) mux.HandleFunc("/api/v1/admin/tenants/", adminMiddleware(srv.HandleAdminCreateAPIKey)) + mux.HandleFunc("/api/v1/admin/users", adminMiddleware(srv.HandleAdminUsers)) + mux.HandleFunc("/api/v1/admin/users/", adminMiddleware(srv.HandleAdminUsers)) mux.HandleFunc("/api/v1/tenants/enroll", srv.HandleTenantEnroll) mux.HandleFunc("/api/v1/enroll/consume", srv.HandleEnrollConsume) + mux.HandleFunc("/api/v1/enroll/consume/", srv.HandleEnrollConsume) mux.HandleFunc("/api/v1/auth/login", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } - var req struct { - Token uint64 `json:"token,string"` // support string from frontend + // Support two modes: + // 1) token login: {"token":"xxxx"} (admin/master only, backward compatible) + // 2) user login: {"tenant":1,"username":"admin","password":"pass"} + var reqTok struct { + Token string `json:"token"` + } + var reqUser struct { + TenantID int64 `json:"tenant"` + Username string `json:"username"` + Password string `json:"password"` } - // Try string first then uint64 body, _ := io.ReadAll(r.Body) - if err := json.Unmarshal(body, &req); err != nil { - var req2 struct { - Token uint64 `json:"token"` - } - if err := json.Unmarshal(body, &req2); err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + _ = json.Unmarshal(body, &reqTok) + _ = json.Unmarshal(body, &reqUser) + + // --- user login --- + if reqUser.TenantID > 0 && reqUser.Username != "" && reqUser.Password != "" { + if srv.Store() == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{"error":1,"message":"store not ready"}`) return } - req.Token = req2.Token + u, err := srv.Store().VerifyUserPassword(reqUser.TenantID, reqUser.Username, reqUser.Password) + if err != nil || u == nil || u.Status != 1 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + 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) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{"error":1,"message":"create token 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, ""} + if ten != nil { + resp.Subnet = ten.Subnet + } + b, _ := json.Marshal(resp) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(b) + return } - valid := req.Token == cfg.Token - if !valid { - for _, t := range cfg.Tokens { - if req.Token == t { - valid = true - break + // --- token login (legacy/admin) --- + valid := false + role := "admin" + status := 1 + if reqTok.Token != "" { + // support numeric token as string + if reqTok.Token == fmt.Sprintf("%d", cfg.Token) { + valid = true + } else { + for _, t := range cfg.Tokens { + if reqTok.Token == fmt.Sprintf("%d", t) { + valid = true + break + } } } } @@ -185,8 +266,16 @@ func main() { fmt.Fprintf(w, `{"error":1,"message":"invalid token"}`) return } + if srv.Store() != nil { + if u, err := srv.Store().GetUserByTenant(0); err == nil && u != nil { + if u.Role != "" { + role = u.Role + } + status = u.Status + } + } w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"error":0,"token":"%d"}`, cfg.Token) + fmt.Fprintf(w, `{"error":0,"token":"%d","role":"%s","status":%d}`, cfg.Token, role, status) }) mux.HandleFunc("/api/v1/health", tenantMiddleware(func(w http.ResponseWriter, r *http.Request) { diff --git a/web/index.html b/web/index.html index 05bd3e7..6984f14 100644 --- a/web/index.html +++ b/web/index.html @@ -1,457 +1,557 @@
- - + +输入 Master Token
+登录后可管理节点、SDWAN、连接与租户
+