fix: enroll revoke cross-tenant isolation (check token ownership before status update)

This commit is contained in:
2026-09-13 08:41:09 +08:00
parent 90ada35d86
commit 84c709bbcc
2 changed files with 252 additions and 42 deletions
+68 -13
View File
@@ -50,23 +50,50 @@ func (s *Server) HandleAdminCreateTenant(w http.ResponseWriter, r *http.Request)
return
}
// update tenant status via /api/v1/admin/tenants/{id}?status=0|1
// reassign subnet via /api/v1/admin/tenants/{id}/subnet {"subnet":"10.200.x.0/24"}
if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/admin/tenants/") {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) >= 4 {
if len(parts) >= 5 {
var id int64
_, _ = fmt.Sscanf(parts[len(parts)-1], "%d", &id)
st := r.URL.Query().Get("status")
if id > 0 && st != "" {
status := 0
if st == "1" {
status = 1
_, _ = fmt.Sscanf(parts[4], "%d", &id)
if id > 0 {
// /api/v1/admin/tenants/{id}/subnet
if len(parts) >= 6 && parts[5] == "subnet" {
var req struct {
Subnet string `json:"subnet"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Subnet) == "" {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
return
}
oldSubnet, err := s.store.ReassignTenantSubnet(id, req.Subnet)
if err != nil {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"`+err.Error()+`"}`)
return
}
if ac := GetAccessContext(r); ac != nil {
detail := fmt.Sprintf("subnet_old=%s subnet_new=%s", oldSubnet, strings.TrimSpace(req.Subnet))
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "tenant_subnet_reassign", "tenant", fmt.Sprintf("%d", id), detail, r.RemoteAddr)
}
// auto rewrite tenant SDWAN routes+gateway and push to online nodes
s.ReconcileTenantSDWAN(id)
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
_ = s.store.UpdateTenantStatus(id, status)
if ac := GetAccessContext(r); ac != nil {
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "tenant_status", "tenant", fmt.Sprintf("%d", id), fmt.Sprintf("status=%d", status), r.RemoteAddr)
st := r.URL.Query().Get("status")
if st != "" {
status := 0
if st == "1" {
status = 1
}
_ = s.store.UpdateTenantStatus(id, status)
if ac := GetAccessContext(r); ac != nil {
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "tenant_status", "tenant", fmt.Sprintf("%d", id), fmt.Sprintf("status=%d", status), r.RemoteAddr)
}
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
}
}
@@ -116,6 +143,10 @@ func (s *Server) HandleAdminCreateTenant(w http.ResponseWriter, r *http.Request)
}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
if ac := GetAccessContext(r); ac != nil {
detail := fmt.Sprintf("name=%s subnet=%s", req.Name, ten.Subnet)
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "tenant_create", "tenant", fmt.Sprintf("%d", ten.ID), detail, r.RemoteAddr)
}
}
func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request) {
@@ -198,7 +229,8 @@ func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request)
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
if ac := GetAccessContext(r); ac != nil {
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "apikey_create", "tenant", fmt.Sprintf("%d", tenantID), req.Scope, r.RemoteAddr)
detail := fmt.Sprintf("tenant=%d ttl=%d scope=%s", tenantID, req.TTL, req.Scope)
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "apikey_create", "tenant", fmt.Sprintf("%d", tenantID), detail, r.RemoteAddr)
}
}
@@ -251,6 +283,10 @@ func (s *Server) HandleTenantEnroll(w http.ResponseWriter, r *http.Request) {
}{0, "ok", code, ten.ID}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
if ac != nil {
detail := fmt.Sprintf("tenant=%d ttl=600 max_attempt=5", ten.ID)
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "enroll_create", "tenant", fmt.Sprintf("%d", ten.ID), detail, r.RemoteAddr)
}
}
func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
@@ -260,6 +296,11 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
}
// revoke support: /api/v1/enroll/consume/{id}?status=0
if strings.Contains(r.URL.Path, "/enroll/consume/") {
ac, ok := s.ResolveTenantAccessToken(BearerToken(r))
if !ok || ac.TenantID <= 0 || !TenantCan(ac, "enroll_manage") {
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"unauthorized"}`)
return
}
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) >= 4 {
var id int64
@@ -270,7 +311,14 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
if st == "1" {
status = 1
}
// tenant isolation: enroll token must belong to the caller's tenant
et, terr := s.store.GetEnrollTokenByID(id)
if terr != nil || et == nil || et.TenantID != ac.TenantID {
writeJSON(w, http.StatusNotFound, `{"error":1,"message":"not found"}`)
return
}
_ = s.store.UpdateEnrollStatus(id, status)
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "enroll_status", "enroll", fmt.Sprintf("%d", id), fmt.Sprintf("status=%d", status), r.RemoteAddr)
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
@@ -284,6 +332,11 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
return
}
actorType, actorID := "enroll", "0"
if ac, ok := s.ResolveTenantAccessToken(BearerToken(r)); ok && ac != nil {
actorType = ac.Kind
actorID = fmt.Sprintf("%d", ac.UserID)
}
if s.store == nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
return
@@ -291,6 +344,7 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
et, err := s.store.ConsumeEnrollToken(req.Code)
if err != nil {
s.store.IncEnrollAttempt(req.Code)
_ = s.store.AddAuditLog(actorType, actorID, "enroll_consume_denied", "enroll", req.Code, "invalid_or_expired", r.RemoteAddr)
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"invalid enroll"}`)
return
}
@@ -313,6 +367,7 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
}{0, "ok", cred.NodeID, cred.NodeUUID, cred.NodeName, cred.Alias, cred.VirtualIP, cred.Secret, cred.TenantID, cred.CreatedAt}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
_ = s.store.AddAuditLog(actorType, actorID, "enroll_consume", "node", cred.NodeUUID, fmt.Sprintf("tenant=%d node=%s", cred.TenantID, cred.NodeName), r.RemoteAddr)
}
// placeholder to avoid unused import