015c622016
Add 162 integration tests covering authentication flows, TOTP MFA, SSH key/certificate management, organization workflows, multi-org access, self-service features, admin operations, authorization, security edge cases, department/principal management, CA management, policy compliance, WebAuthn passkeys, and ZeroTier network access. Includes: - Reusable API client library with session management - Test fixtures for users, organizations, memberships, and CAs - Helper functions for SSH key generation and verification - Documentation for running and writing tests Also update test configuration to disable conflicting maas plugins and configure WebAuthn/session settings for localhost testing.
169 lines
7.3 KiB
Python
169 lines
7.3 KiB
Python
"""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)
|