PegaProx_project-pegaprox/tests/test_integration_groups.py
MrMasterbay 96ac523abd security(authz): fix 3 confirmed auth-bypass findings (deleted-user, alert leak, cluster hijack)
Three critical/high auth-bypass findings from a CodeAnt AI-exploitation scan, each
independently verified as REAL (adversarially cross-checked + reproduced) and fixed:

- auth.py / users.py — off-boarding bypass: require_auth did get_user() or {}, so a
  DELETED user resolved to {} -> {}.get('enabled', True)==True passed the disabled
  check and the role fell back to the stale session role; get_user_clusters({})==None
  = all-cluster read. delete_user's inline session purge was also DEAD (operated on a
  stale active_sessions binding after load_sessions rebind) and never revoked pgx_ API
  tokens. Fix: require_auth fails CLOSED when the record is gone (ACCOUNT_DELETED,
  covers session AND token auth); delete_user uses invalidate_all_user_sessions() +
  revokes api_tokens.

- push.py — cross-tenant alert leak: _alert_handler wrote every cluster alert (cluster/
  node/VM names + live metric %) into EVERY push subscriber's inbox and woke them, with
  zero tenant scoping. Fix: scope recipients to subscribers who can reach
  alert_data['cluster_id'] via get_user_clusters (admin/default-tenant still get all;
  cluster-less system alerts go to all; fail closed on lookup error).

- groups.py — cross-tenant cluster hijack: assign_cluster_to_group / rename_cluster only
  gated the source cluster with check_cluster_access, whose ACL/pool fallbacks pass on
  mere VM-level reach; a non-admin with admin.groups + one foreign VM-ACL grant could
  move that cluster into their own group and gain cluster-wide tenant membership. Fix:
  require real tenant ownership (get_user_clusters(include_pools=False)) of the source
  cluster before re-grouping/renaming.

Regression tests (integration harness): test_integration_auth_deleted / _groups / _push
drive each through the real app. 260 passing (was 249).
2026-07-13 17:53:26 +02:00

58 lines
3.0 KiB
Python

# Regression: cluster (re-)grouping / rename must require real TENANT ownership of the cluster,
# not mere additive VM-ACL/pool reach into it.
#
# CodeAnt exploitation finding (2026-07-13): assign_cluster_to_group / rename_cluster only gated
# the SOURCE cluster with check_cluster_access, whose #248/#555 ACL+pool fallbacks return True for
# a cluster the actor merely has a VM-level grant on. A non-admin with admin.groups + one foreign
# VM-ACL grant could move that cluster into their own group and gain cluster-wide tenant membership
# over it (cross-tenant hijack). Fixed with a get_user_clusters(include_pools=False) ownership gate.
CLUSTER = 'cluster_1' # the foreign cluster the actor only has VM-ACL reach into
def _acl_reach_actor(api, seed):
# alice: non-admin in a tenant that does NOT own cluster_1, holds admin.groups, and has a
# single VM-ACL grant on VM 100 in cluster_1 -> check_cluster_access passes via the ACL
# fallback, but she does not TENANT-own cluster_1.
seed.tenant('tenant_a', clusters=['cluster_home'])
alice = seed.user('alice', role='user', tenant_id='tenant_a', permissions=['admin.groups'])
seed.vm_acl(CLUSTER, 100, users=['alice'], inherit_role=True)
api.set_manager(CLUSTER, api.make_fake_manager())
return alice
def test_acl_reach_actor_cannot_move_foreign_cluster_into_group(api, seed):
alice = _acl_reach_actor(api, seed)
r = api.as_user(alice).put(f'/api/clusters/{CLUSTER}/group', json={'group_id': 'g-alice'})
assert r.status_code == 403, r.get_data(as_text=True)
def test_acl_reach_actor_cannot_ungroup_foreign_cluster(api, seed):
# the ungroup variant ({group_id: null}) skips the destination check entirely — the source
# ownership gate must still deny it (integrity: can't rip a cluster out of its owner's group).
alice = _acl_reach_actor(api, seed)
r = api.as_user(alice).put(f'/api/clusters/{CLUSTER}/group', json={'group_id': None})
assert r.status_code == 403, r.get_data(as_text=True)
def test_acl_reach_actor_cannot_rename_foreign_cluster(api, seed):
alice = _acl_reach_actor(api, seed)
r = api.as_user(alice).put(f'/api/clusters/{CLUSTER}/rename', json={'display_name': 'pwned'})
assert r.status_code == 403, r.get_data(as_text=True)
def test_tenant_owner_can_regroup_own_cluster(api, seed):
# positive control: the actual tenant owner (with admin.groups) is NOT over-restricted.
seed.tenant('tenant_b', clusters=[CLUSTER])
bob = seed.user('bob', role='user', tenant_id='tenant_b', permissions=['admin.groups'])
api.set_manager(CLUSTER, api.make_fake_manager())
r = api.as_user(bob).put(f'/api/clusters/{CLUSTER}/group', json={'group_id': None})
assert r.status_code == 200, r.get_data(as_text=True)
def test_admin_can_regroup_any_cluster(api, seed):
root = seed.user('root', role='admin', tenant_id='default')
api.set_manager(CLUSTER, api.make_fake_manager())
r = api.as_user(root).put(f'/api/clusters/{CLUSTER}/group', json={'group_id': None})
assert r.status_code == 200, r.get_data(as_text=True)