"""Authorization and access control integration tests. Covers RBAC enforcement, cross-user isolation, and soft-delete behavior. """ import pytest from tests.integration.client.base import ApiError from gatehouse_app.utils.constants import OrganizationRole def assert_error(exc: ApiError, expected_status: int, expected_error_type: str | None = None): assert exc.status_code == expected_status if expected_error_type: assert exc.error_type == expected_error_type class TestAuthorization: """Test access control across endpoints.""" def test_access_protected_without_auth_negative(self, integration_client): """TEST: AUTHZ-01 — Access protected endpoint without auth. WHAT: Call GET /auth/me with no token. WHY: All protected endpoints must require authentication. EXPECTED: 401 Unauthorized. """ integration_client.clear_token() with pytest.raises(ApiError) as exc_info: integration_client.auth.me() assert exc_info.value.status_code == 401 def test_member_attempts_admin_operation_negative(self, integration_client, create_test_user, create_test_org, create_test_membership): """TEST: AUTHZ-02 — Member attempts admin operation. WHAT: Member tries to delete an organization. WHY: Role-based access must be enforced. EXPECTED: 403 Forbidden. """ member = create_test_user(password="MemberPass123!") org = create_test_org() create_test_membership(member["id"], org["id"], OrganizationRole.MEMBER) integration_client.auth.login(email=member["email"], password="MemberPass123!") with pytest.raises(ApiError) as exc_info: integration_client.orgs.delete(org["id"], confirm=True) assert exc_info.value.status_code == 403 def test_admin_attempts_owner_operation_negative(self, integration_client, create_test_user, create_test_org, create_test_membership): """TEST: AUTHZ-03 — Admin attempts owner-only operation. WHAT: Admin tries to transfer ownership. WHY: Ownership transfer is owner-only. EXPECTED: 403 Forbidden. """ owner = create_test_user(password="OwnerPass123!") admin = create_test_user(password="AdminPass123!") org = create_test_org() create_test_membership(owner["id"], org["id"], OrganizationRole.OWNER) create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN) integration_client.auth.login(email=admin["email"], password="AdminPass123!") with pytest.raises(ApiError) as exc_info: integration_client.orgs.transfer_ownership(org["id"], owner["id"]) assert exc_info.value.status_code == 403 def test_non_member_attempts_org_operation_negative(self, integration_client, create_test_user, create_test_org): """TEST: AUTHZ-04 — Non-member attempts org operation. WHAT: Unrelated user tries to GET an organization. WHY: Org data must not leak to outsiders. EXPECTED: 403 Forbidden. """ org = create_test_org() user = create_test_user(password="MyPassword123!") integration_client.auth.login(email=user["email"], password="MyPassword123!") with pytest.raises(ApiError) as exc_info: integration_client.orgs.get(org["id"]) assert exc_info.value.status_code == 403 def test_user_a_accesses_user_b_ssh_keys_negative(self, integration_app, integration_client, create_test_user): """TEST: AUTHZ-05 — User A accesses User B's SSH keys. WHAT: User A tries to GET User B's SSH key. WHY: Cross-user data isolation. EXPECTED: 403 Forbidden. """ from tests.integration.fixtures.ssh_keys import TEST_PUBLIC_KEY user_a = create_test_user(password="PassA123!") user_b = create_test_user(password="PassB123!") integration_client.auth.login(email=user_b["email"], password="PassB123!") add_result = integration_client.ssh.add_key(TEST_PUBLIC_KEY, "User B Key") key_id = add_result["data"]["id"] integration_client.auth.logout() integration_client.auth.login(email=user_a["email"], password="PassA123!") with pytest.raises(ApiError) as exc_info: integration_client.ssh.get_key(key_id) assert exc_info.value.status_code == 403 def test_user_a_accesses_user_b_sessions_negative(self, integration_app, integration_client, create_test_user): """TEST: AUTHZ-07 — User A accesses User B's sessions. WHAT: User A tries to list User B's sessions. WHY: Session data is private. EXPECTED: 403 Forbidden or only own sessions returned. """ user_a = create_test_user(password="PassA123!") user_b = create_test_user(password="PassB123!") integration_client.auth.login(email=user_b["email"], password="PassB123!") sessions_b = integration_client.auth.list_sessions() session_id_b = sessions_b["data"]["sessions"][0]["id"] integration_client.auth.logout() integration_client.auth.login(email=user_a["email"], password="PassA123!") # User A should not be able to revoke User B's session with pytest.raises(ApiError) as exc_info: integration_client.auth.revoke_session(session_id_b) assert exc_info.value.status_code == 404 def test_soft_deleted_user_cannot_login_negative(self, integration_app, integration_client, create_test_user): """TEST: AUTHZ-08 — Soft-deleted user cannot login. WHAT: Create a user, soft-delete them, attempt login. WHY: Soft delete must block access. EXPECTED: 401 or 404. """ from gatehouse_app.extensions import db from gatehouse_app.models.user.user import User user = create_test_user(password="MyPassword123!") with integration_app.app_context(): db_user = User.query.get(user["id"]) db_user.deleted_at = db.func.now() db.session.commit() with pytest.raises(ApiError) as exc_info: integration_client.auth.login(email=user["email"], password="MyPassword123!") assert exc_info.value.status_code in (400, 401, 404) def test_soft_deleted_org_not_listable_negative(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership): """TEST: AUTHZ-09 — Soft-deleted org not listable. WHAT: Create an org, soft-delete it, then GET /users/me/organizations. WHY: Soft-deleted orgs should not appear. EXPECTED: Org not in the list. """ from gatehouse_app.extensions import db from gatehouse_app.models.organization.organization import Organization user = create_test_user(password="MyPassword123!") org = create_test_org() create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER) with integration_app.app_context(): db_org = Organization.query.get(org["id"]) db_org.deleted_at = db.func.now() db.session.commit() integration_client.auth.login(email=user["email"], password="MyPassword123!") result = integration_client.users.get_my_organizations() orgs = result.get("data", {}).get("organizations", []) assert not any(o.get("id") == org["id"] for o in orgs)