PegaProx_project-pegaprox/tests/test_integration_push.py
MrMasterbay cf8b6f16e2 security(authz): fix 3 CodeAnt re-scan criticals (global-perm priv-esc, push fail-open, VMware BOLA)
Second CodeAnt exploitation scan (on the current tip) surfaced 3 critical auth findings, each
verified real and fixed:

- static_files.set_user_perms: the GLOBAL-permissions branch (no tenant_id) only required
  admin.users — which a tenant-scoped admin can hold — so a tenant admin could set a user's
  GLOBAL permissions and grant themselves/anyone global-admin-equivalent perms (priv-esc). The
  per-tenant branch already gated non-global-admins; the global branch now does too (requires
  session role == ROLE_ADMIN).
- push._alert_handler: the cross-tenant scope I added last cycle FAILED OPEN when a subscriber's
  user record was missing/deleted (still in push_subscriptions) — get_user_clusters({}) => None
  => treated as all-cluster admin => received every tenant's alerts. Now fails CLOSED on a
  missing record (and on lookup error). Good catch by the re-scan on my own fix.
- rbac.user_can_access_vmware_vm: the role-permission fallback had NO tenant isolation (the
  Proxmox user_can_access_vm has the equivalent guard) — any vmware.vm.* holder could reach
  every VMware server's VMs cross-tenant. Now gated by the server's linked_clusters
  (admin/unlinked open, else require get_user_clusters overlap), mirroring check_pbs_access.

Regression tests: tenant-admin global-perm PUT => 403 (global admin => 200); ghost/deleted
subscriber gets no cluster alert; VMware cross-tenant => denied, admin/unlinked => allowed.
274 passing. (The ~40 locked re-scan findings are under a parallel adversarial hunt.)
2026-07-13 21:41:37 +02:00

81 lines
4.0 KiB
Python

# Regression: cluster-scoped alert push must not fan out cross-tenant.
#
# CodeAnt exploitation finding (2026-07-13): push._alert_handler wrote every alert (cluster/node/
# VM names + live metric %) into EVERY push subscriber's inbox and _wake_all()'d them, with zero
# tenant scoping -> a tenant-A user learned tenant-B cluster events. Fixed: recipients are scoped
# to subscribers who can reach alert_data['cluster_id'] via get_user_clusters (None => admin/
# default-tenant get all; cluster-less system alerts go to all; fail closed on lookup error).
#
# Driven at the handler level (it's a background hook, not an HTTP route): real DB subscriptions,
# real get_user_clusters + seeded tenants; load_users + the delivery sinks are stubbed so we can
# observe exactly who would receive each alert.
import pegaprox.api.push as push
import pegaprox.utils.auth as auth
def _subscribe(db, username):
db.execute(
"INSERT INTO push_subscriptions (username, endpoint, p256dh, auth, created_at) "
"VALUES (?, ?, ?, ?, ?)",
(username, f'https://push.example/{username}', 'p256', 'authsecret', '2026-01-01T00:00:00'),
)
def _setup(api, seed, monkeypatch):
# tenant_a owns cluster_1, tenant_b owns cluster_2, root is a default-tenant admin (sees all)
seed.tenant('tenant_a', clusters=['cluster_1'])
seed.tenant('tenant_b', clusters=['cluster_2'])
for u in ('alice', 'bob', 'root'):
_subscribe(seed.db, u)
users_map = {
'alice': {'tenant_id': 'tenant_a', 'role': 'user'},
'bob': {'tenant_id': 'tenant_b', 'role': 'user'},
'root': {'tenant_id': 'default', 'role': 'admin'},
}
monkeypatch.setattr(auth, 'load_users', lambda: users_map)
delivered = []
monkeypatch.setattr(push, '_push_to_inbox', lambda u, *a, **k: delivered.append(u))
monkeypatch.setattr(push, '_wake_user', lambda u: None)
monkeypatch.setattr(push, '_wake_all', lambda: delivered.append('__ALL__')) # trip if the old path runs
return delivered
def test_cluster_alert_scoped_to_reachable_subscribers(api, seed, monkeypatch):
delivered = _setup(api, seed, monkeypatch)
push._alert_handler({'alert_name': 'CPU', 'message': '95%', 'severity': 'warning',
'cluster_id': 'cluster_1'})
assert 'alice' in delivered # tenant_a owns cluster_1
assert 'root' in delivered # admin sees all clusters
assert 'bob' not in delivered # tenant_b must NOT learn about cluster_1
assert '__ALL__' not in delivered # the unscoped broadcast path must be gone
def test_symmetric_other_tenant(api, seed, monkeypatch):
delivered = _setup(api, seed, monkeypatch)
push._alert_handler({'alert_name': 'DISK', 'message': 'full', 'severity': 'critical',
'cluster_id': 'cluster_2'})
assert 'bob' in delivered # tenant_b owns cluster_2
assert 'root' in delivered
assert 'alice' not in delivered # tenant_a must NOT learn about cluster_2
def test_clusterless_system_alert_reaches_all(api, seed, monkeypatch):
delivered = _setup(api, seed, monkeypatch)
push._alert_handler({'alert_name': 'System', 'message': 'update available',
'severity': 'info', 'cluster_id': ''})
for u in ('alice', 'bob', 'root'):
assert u in delivered # no cluster => not tenant-scoped
def test_ghost_subscriber_fails_closed(api, seed, monkeypatch):
# CodeAnt re-scan: a subscription whose user record is MISSING/DELETED (still in
# push_subscriptions) must NOT be treated as an all-cluster admin and receive every
# tenant's alerts — it must fail CLOSED.
delivered = _setup(api, seed, monkeypatch)
_subscribe(seed.db, 'ghost') # 'ghost' is NOT in the load_users map (deleted user)
push._alert_handler({'alert_name': 'CPU', 'message': '95%', 'severity': 'warning',
'cluster_id': 'cluster_1'})
assert 'ghost' not in delivered # missing record => no cluster-scoped alert (fail closed)
assert 'alice' in delivered # a real tenant-a user still gets it