fix: enroll revoke cross-tenant isolation (check token ownership before status update)
This commit is contained in:
+184
-29
@@ -243,36 +243,29 @@ func (s *Store) migrate() error {
|
||||
}
|
||||
|
||||
func (s *Store) ensureSubnetPool() error {
|
||||
// pool: 10.10.1.0/24 .. 10.10.254.0/24
|
||||
// reserve: 10.10.0.0/24 and 10.10.255.0/24
|
||||
rows, err := s.DB.Query(`SELECT COUNT(1) FROM subnet_pool;`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var count int
|
||||
if rows.Next() {
|
||||
_ = rows.Scan(&count)
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
// scheme-1 pool: 10.200.0.0/24 .. 10.200.255.0/24 (256 tenants)
|
||||
// keep legacy rows if they already exist; new allocations prefer 10.200.x.0/24
|
||||
now := time.Now().Unix()
|
||||
insert := `INSERT INTO subnet_pool(subnet,status,reserved,tenant_id,updated_at) VALUES(?,?,?,?,?)`
|
||||
// reserved
|
||||
_, _ = s.DB.Exec(insert, "10.10.0.0/24", 0, 1, nil, now)
|
||||
_, _ = s.DB.Exec(insert, "10.10.255.0/24", 0, 1, nil, now)
|
||||
for i := 1; i <= 254; i++ {
|
||||
sn := fmt.Sprintf("10.10.%d.0/24", i)
|
||||
_, _ = s.DB.Exec(insert, sn, 0, 0, nil, now)
|
||||
insert := `INSERT OR IGNORE INTO subnet_pool(subnet,status,reserved,tenant_id,updated_at) VALUES(?,?,?,?,?)`
|
||||
for i := 0; i <= 255; i++ {
|
||||
sn := fmt.Sprintf("10.200.%d.0/24", i)
|
||||
if _, err := s.DB.Exec(insert, sn, 0, 0, nil, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) AllocateSubnet() (string, error) {
|
||||
// find first available subnet
|
||||
row := s.DB.QueryRow(`SELECT subnet FROM subnet_pool WHERE status=0 AND reserved=0 ORDER BY subnet LIMIT 1`)
|
||||
// prefer scheme-1 range
|
||||
row := s.DB.QueryRow(`SELECT subnet FROM subnet_pool WHERE status=0 AND reserved=0 AND subnet LIKE '10.200.%/24' ORDER BY subnet LIMIT 1`)
|
||||
var subnet string
|
||||
if err := row.Scan(&subnet); err == nil && subnet != "" {
|
||||
return subnet, nil
|
||||
}
|
||||
|
||||
// fallback: any available subnet (legacy compatibility)
|
||||
row = s.DB.QueryRow(`SELECT subnet FROM subnet_pool WHERE status=0 AND reserved=0 ORDER BY subnet LIMIT 1`)
|
||||
if err := row.Scan(&subnet); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -395,6 +388,50 @@ func (s *Store) GetNodeCredentialByName(tenantID int64, nodeName string) (*NodeC
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListNodeCredentialsByTenant(tenantID int64) ([]NodeCredential, error) {
|
||||
rows, err := s.DB.Query(`SELECT id,node_uuid,node_name,alias,virtual_ip,tenant_id,status,last_seen,created_at FROM nodes WHERE tenant_id=? ORDER BY id DESC`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]NodeCredential, 0)
|
||||
for rows.Next() {
|
||||
var n NodeCredential
|
||||
var seen sql.NullInt64
|
||||
if err := rows.Scan(&n.NodeID, &n.NodeUUID, &n.NodeName, &n.Alias, &n.VirtualIP, &n.TenantID, &n.Status, &seen, &n.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seen.Valid {
|
||||
v := seen.Int64
|
||||
n.LastSeen = &v
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListNodeCredentialsAll() ([]NodeCredential, error) {
|
||||
rows, err := s.DB.Query(`SELECT id,node_uuid,node_name,alias,virtual_ip,tenant_id,status,last_seen,created_at FROM nodes ORDER BY id DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]NodeCredential, 0)
|
||||
for rows.Next() {
|
||||
var n NodeCredential
|
||||
var seen sql.NullInt64
|
||||
if err := rows.Scan(&n.NodeID, &n.NodeUUID, &n.NodeName, &n.Alias, &n.VirtualIP, &n.TenantID, &n.Status, &seen, &n.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seen.Valid {
|
||||
v := seen.Int64
|
||||
n.LastSeen = &v
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetNodeAlias(tenantID int64, nodeUUID, alias string) error {
|
||||
alias = strings.TrimSpace(alias)
|
||||
res, err := s.DB.Exec(`UPDATE nodes SET alias=? WHERE tenant_id=? AND node_uuid=?`, alias, tenantID, nodeUUID)
|
||||
@@ -519,9 +556,13 @@ func (s *Store) IncEnrollAttempt(code string) {
|
||||
|
||||
func (s *Store) ensureSettings() error {
|
||||
defaults := map[string]string{
|
||||
"advanced_impersonate": "0",
|
||||
"advanced_force_network": "0",
|
||||
"advanced_cross_tenant": "0",
|
||||
"advanced_impersonate": "0",
|
||||
"advanced_force_network": "0",
|
||||
"advanced_cross_tenant": "0",
|
||||
"advanced_override_enabled": "0",
|
||||
"allow_apikey_mutate_sdwan": "0",
|
||||
"allow_operator_kick_cross_node": "0",
|
||||
"allow_connect_cross_tenant": "0",
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
for k, v := range defaults {
|
||||
@@ -567,14 +608,44 @@ func (s *Store) AddAuditLog(actorType, actorID, action, targetType, targetID, de
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) ListAuditLogs(tenantID int64, limit, offset int) ([]AuditLog, error) {
|
||||
q := `SELECT id,actor_type,actor_id,action,target_type,target_id,detail,ip,created_at FROM audit_logs`
|
||||
func (s *Store) ListAuditLogs(tenantID int64, limit, offset int, action, actorType, actorID, targetType, targetID string, fromTs, toTs int64) ([]AuditLog, error) {
|
||||
q := `SELECT id,actor_type,actor_id,action,target_type,target_id,detail,ip,created_at FROM audit_logs WHERE 1=1`
|
||||
args := []any{}
|
||||
|
||||
if tenantID > 0 {
|
||||
// limit to logs related to this tenant
|
||||
q += ` WHERE (target_type='tenant' AND target_id=?)`
|
||||
q += ` AND (target_type='tenant' AND target_id=?)`
|
||||
args = append(args, fmt.Sprintf("%d", tenantID))
|
||||
}
|
||||
if action != "" {
|
||||
q += ` AND action=?`
|
||||
args = append(args, action)
|
||||
}
|
||||
if actorType != "" {
|
||||
q += ` AND actor_type=?`
|
||||
args = append(args, actorType)
|
||||
}
|
||||
if actorID != "" {
|
||||
q += ` AND actor_id=?`
|
||||
args = append(args, actorID)
|
||||
}
|
||||
if targetType != "" {
|
||||
q += ` AND target_type=?`
|
||||
args = append(args, targetType)
|
||||
}
|
||||
if targetID != "" {
|
||||
q += ` AND target_id=?`
|
||||
args = append(args, targetID)
|
||||
}
|
||||
if fromTs > 0 {
|
||||
q += ` AND created_at>=?`
|
||||
args = append(args, fromTs)
|
||||
}
|
||||
if toTs > 0 {
|
||||
q += ` AND created_at<=?`
|
||||
args = append(args, toTs)
|
||||
}
|
||||
|
||||
q += ` ORDER BY id DESC`
|
||||
if limit > 0 {
|
||||
q += ` LIMIT ?`
|
||||
@@ -621,6 +692,75 @@ func (s *Store) UpdateTenantStatus(id int64, status int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ReassignTenantSubnet reassigns tenant subnet with conflict checks.
|
||||
// Allowed range (scheme-1): 10.200.0.0/16, /24 only.
|
||||
func (s *Store) ReassignTenantSubnet(tenantID int64, newSubnet string) (string, error) {
|
||||
newSubnet = strings.TrimSpace(newSubnet)
|
||||
if newSubnet == "" {
|
||||
return "", errors.New("subnet required")
|
||||
}
|
||||
ip, ipnet, err := net.ParseCIDR(newSubnet)
|
||||
if err != nil || ipnet == nil {
|
||||
return "", errors.New("invalid subnet")
|
||||
}
|
||||
ones, bits := ipnet.Mask.Size()
|
||||
if bits != 32 || ones != 24 {
|
||||
return "", errors.New("only /24 ipv4 subnet supported")
|
||||
}
|
||||
if ip4 := ip.To4(); ip4 == nil || ip4[0] != 10 || ip4[1] != 200 {
|
||||
return "", errors.New("subnet must be in 10.200.0.0/16")
|
||||
}
|
||||
if ip.String() != ipnet.IP.String() {
|
||||
return "", errors.New("subnet must be network address, e.g. 10.200.x.0/24")
|
||||
}
|
||||
|
||||
tx, err := s.DB.Begin()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var oldSubnet string
|
||||
row := tx.QueryRow(`SELECT subnet FROM tenants WHERE id=?`, tenantID)
|
||||
if err := row.Scan(&oldSubnet); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if oldSubnet == newSubnet {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return oldSubnet, nil
|
||||
}
|
||||
|
||||
// conflict check: target subnet must not belong to another tenant
|
||||
var occupiedBy sql.NullInt64
|
||||
if err := tx.QueryRow(`SELECT id FROM tenants WHERE subnet=?`, newSubnet).Scan(&occupiedBy); err == nil {
|
||||
if occupiedBy.Valid && occupiedBy.Int64 != tenantID {
|
||||
return "", errors.New("subnet already occupied")
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
// release old pool mark
|
||||
_, _ = tx.Exec(`UPDATE subnet_pool SET status=0, tenant_id=NULL, updated_at=? WHERE subnet=? AND tenant_id=?`, now, oldSubnet, tenantID)
|
||||
// assign tenant
|
||||
if _, err := tx.Exec(`UPDATE tenants SET subnet=? WHERE id=?`, newSubnet, tenantID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// ensure pool row exists and mark occupied
|
||||
if _, err := tx.Exec(`INSERT OR IGNORE INTO subnet_pool(subnet,status,reserved,tenant_id,updated_at) VALUES(?,?,?,?,?)`, newSubnet, 1, 0, tenantID, now); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := tx.Exec(`UPDATE subnet_pool SET status=1, tenant_id=?, updated_at=? WHERE subnet=?`, tenantID, now, newSubnet); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return oldSubnet, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteTenant(id int64) error {
|
||||
_, err := s.DB.Exec(`DELETE FROM tenants WHERE id=?`, id)
|
||||
return err
|
||||
@@ -680,6 +820,21 @@ func (s *Store) ListEnrollTokens(tenantID int64) ([]EnrollToken, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetEnrollTokenByID returns a single enroll token (used for ownership checks)
|
||||
func (s *Store) GetEnrollTokenByID(id int64) (*EnrollToken, error) {
|
||||
row := s.DB.QueryRow(`SELECT id,tenant_id,token_hash,expires_at,used_at,max_attempt,attempts,status,created_at FROM enroll_tokens WHERE id=?`, id)
|
||||
var et EnrollToken
|
||||
var used sql.NullInt64
|
||||
if err := row.Scan(&et.ID, &et.TenantID, &et.Hash, &et.ExpiresAt, &used, &et.MaxAttempt, &et.Attempts, &et.Status, &et.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if used.Valid {
|
||||
v := used.Int64
|
||||
et.UsedAt = &v
|
||||
}
|
||||
return &et, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateEnrollStatus(id int64, status int) error {
|
||||
_, err := s.DB.Exec(`UPDATE enroll_tokens SET status=? WHERE id=?`, status, id)
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user