fix: enroll revoke cross-tenant isolation (check token ownership before status update)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user