BastionSSO/user_manage_api/tests/test_api_integration.py

252 lines
10 KiB
Python

from fastapi.testclient import TestClient
from typing import Dict, List, Optional
import app.container as container
from app.core.audit import AuditLogger
from app.core.config import Settings
from app.core.errors import ApiError
from app.factory import create_app
from app.core.models import GroupSummary, UserSummary
from app.providers.base import SystemProvider
from app.services.user_group_service import UserGroupService
from app.state import AppState
class MockProvider(SystemProvider):
def __init__(self):
self.users: Dict[str, UserSummary] = {}
self.groups: Dict[str, GroupSummary] = {}
self.user_group_map: Dict[str, List[str]] = {}
def create_user(self, username: str, password_hash: str, home_dir: Optional[str], linked_home_dir: Optional[str], shell: str, primary_group: Optional[str], groups: List[str]) -> None:
if username in self.users:
raise ApiError(409, "resource_conflict", "user exists")
self.users[username] = UserSummary(username=username, uid=1000, gid=1000, home_dir=home_dir or f"/home/{username}", shell=shell)
self.user_group_map[username] = groups
def delete_user(self, username: str) -> None:
if username not in self.users:
raise ApiError(404, "not_found", "user not found")
del self.users[username]
self.user_group_map.pop(username, None)
def change_user_password(self, username: str, password_hash: str) -> None:
if username not in self.users:
raise ApiError(404, "not_found", "user not found")
def list_users(self) -> List[UserSummary]:
return list(self.users.values())
def get_user(self, username: str) -> UserSummary:
if username not in self.users:
raise ApiError(404, "not_found", "user not found")
return self.users[username]
def create_group(self, groupname: str) -> None:
if groupname in self.groups:
raise ApiError(409, "resource_conflict", "group exists")
self.groups[groupname] = GroupSummary(groupname=groupname, gid=1000, members=[])
def delete_group(self, groupname: str) -> None:
if groupname not in self.groups:
raise ApiError(404, "not_found", "group not found")
del self.groups[groupname]
def list_groups(self) -> List[GroupSummary]:
return list(self.groups.values())
def get_group(self, groupname: str) -> GroupSummary:
if groupname not in self.groups:
raise ApiError(404, "not_found", "group not found")
return self.groups[groupname]
def add_user_groups(self, username: str, groups: List[str], replace: bool) -> None:
if username not in self.users:
raise ApiError(404, "not_found", "user not found")
if replace:
self.user_group_map[username] = groups
else:
self.user_group_map[username] = list(dict.fromkeys(self.user_group_map.get(username, []) + groups))
def remove_user_groups(self, username: str, groups: List[str]) -> None:
if username not in self.users:
raise ApiError(404, "not_found", "user not found")
self.user_group_map[username] = [item for item in self.user_group_map.get(username, []) if item not in set(groups)]
def get_user_groups(self, username: str) -> List[str]:
if username not in self.users:
raise ApiError(404, "not_found", "user not found")
return self.user_group_map.get(username, [])
def build_client(
link_home_dir: Optional[str] = None,
hidden_users: Optional[List[str]] = None,
hidden_groups: Optional[List[str]] = None,
whitelist_users: Optional[List[str]] = None,
whitelist_groups: Optional[List[str]] = None,
locked_users: Optional[List[str]] = None,
user_uid_min: Optional[int] = None,
user_uid_max: Optional[int] = None,
group_gid_min: Optional[int] = None,
group_gid_max: Optional[int] = None,
) -> TestClient:
settings = Settings(TOKEN="test-token")
service = UserGroupService(
provider=MockProvider(),
home_base_dir="/home",
link_home_dir=link_home_dir,
hidden_users=hidden_users,
hidden_groups=hidden_groups,
whitelist_users=whitelist_users,
whitelist_groups=whitelist_groups,
locked_users=locked_users,
user_uid_min=user_uid_min,
user_uid_max=user_uid_max,
group_gid_min=group_gid_min,
group_gid_max=group_gid_max,
)
audit = AuditLogger("./logs/test_audit.log")
container.app_state = AppState(settings=settings, service=service, audit=audit)
return TestClient(create_app())
def test_user_group_happy_path() -> None:
client = build_client()
headers = {"Authorization": "Bearer test-token"}
response = client.post("/groups", json={"groupname": "dev"}, headers=headers)
assert response.status_code == 200
response = client.post("/users", json={"username": "alice", "password_hash": "$6$rounds=5000$abcdefghij", "groups": ["dev"]}, headers=headers)
assert response.status_code == 200
response = client.post("/users/alice/groups", json={"groups": ["dev"], "mode": "append"}, headers=headers)
assert response.status_code == 200
response = client.get("/users/alice/groups", headers=headers)
assert response.status_code == 200
assert "dev" in response.json()["groups"]
def test_health_returns_server_name_and_online_status() -> None:
client = build_client()
headers = {"Authorization": "Bearer test-token"}
response = client.get("/health", headers=headers)
assert response.status_code == 200
assert response.json() == {"server_name": "user-manage-api", "status": "online"}
def test_change_user_password() -> None:
client = build_client()
headers = {"Authorization": "Bearer test-token"}
client.post("/users", json={"username": "alice", "password_hash": "$6$rounds=5000$abcdefghij", "groups": []}, headers=headers)
response = client.patch("/users/alice/password", json={"password_hash": "$6$rounds=5000$newhashvalue"}, headers=headers)
assert response.status_code == 200
assert response.json()["message"] == "User password updated."
def test_locked_user_can_be_read_but_not_modified() -> None:
client = build_client(locked_users=["alice"])
headers = {"Authorization": "Bearer test-token"}
provider = container.app_state.service.provider
provider.users["alice"] = UserSummary(username="alice", uid=1000, gid=1000, home_dir="/home/alice", shell="/bin/bash")
response = client.get("/users/alice", headers=headers)
assert response.status_code == 200
response = client.patch("/users/alice/password", json={"password_hash": "$6$rounds=5000$newhashvalue"}, headers=headers)
assert response.status_code == 423
response = client.delete("/users/alice", headers=headers)
assert response.status_code == 423
response = client.post("/users/alice/groups", json={"groups": ["dev"], "mode": "append"}, headers=headers)
assert response.status_code == 423
def test_conflict_and_not_found_codes() -> None:
client = build_client()
headers = {"Authorization": "Bearer test-token"}
payload = {"username": "alice", "password_hash": "$6$rounds=5000$abcdefghij", "groups": []}
response = client.post("/users", json=payload, headers=headers)
assert response.status_code == 200
response = client.post("/users", json=payload, headers=headers)
assert response.status_code == 409
response = client.delete("/users/bob", headers=headers)
assert response.status_code == 404
def test_hidden_users_and_groups_are_filtered_and_blocked() -> None:
client = build_client(hidden_users=["root"], hidden_groups=["sudo"])
headers = {"Authorization": "Bearer test-token"}
client.post("/users", json={"username": "alice", "password_hash": "$6$rounds=5000$abcdefghij", "groups": []}, headers=headers)
client.post("/groups", json={"groupname": "dev"}, headers=headers)
provider = container.app_state.service.provider
provider.users["root"] = UserSummary(username="root", uid=0, gid=0, home_dir="/root", shell="/bin/bash")
provider.groups["sudo"] = GroupSummary(groupname="sudo", gid=27, members=["root"])
provider.user_group_map["alice"] = ["dev", "sudo"]
response = client.get("/users", headers=headers)
assert response.status_code == 200
assert [user["username"] for user in response.json()] == ["alice"]
response = client.get("/groups", headers=headers)
assert response.status_code == 200
assert [group["groupname"] for group in response.json()] == ["dev"]
response = client.get("/users/root", headers=headers)
assert response.status_code == 404
response = client.delete("/groups/sudo", headers=headers)
assert response.status_code == 404
response = client.get("/users/alice/groups", headers=headers)
assert response.status_code == 200
assert response.json()["groups"] == ["dev"]
def test_whitelist_overrides_hidden_and_id_ranges() -> None:
client = build_client(
hidden_users=["root"],
hidden_groups=["sudo"],
whitelist_users=["root"],
whitelist_groups=["sudo"],
user_uid_min=1000,
user_uid_max=2000,
group_gid_min=1000,
group_gid_max=2000,
)
headers = {"Authorization": "Bearer test-token"}
provider = container.app_state.service.provider
provider.users["root"] = UserSummary(username="root", uid=0, gid=0, home_dir="/root", shell="/bin/bash")
provider.users["alice"] = UserSummary(username="alice", uid=1000, gid=1000, home_dir="/home/alice", shell="/bin/bash")
provider.users["system"] = UserSummary(username="system", uid=500, gid=500, home_dir="/home/system", shell="/bin/bash")
provider.groups["sudo"] = GroupSummary(groupname="sudo", gid=27, members=["root"])
provider.groups["dev"] = GroupSummary(groupname="dev", gid=1000, members=[])
provider.groups["system"] = GroupSummary(groupname="system", gid=500, members=[])
response = client.get("/users", headers=headers)
assert response.status_code == 200
assert [user["username"] for user in response.json()] == ["root", "alice"]
response = client.get("/groups", headers=headers)
assert response.status_code == 200
assert [group["groupname"] for group in response.json()] == ["sudo", "dev"]
response = client.get("/users/root", headers=headers)
assert response.status_code == 200
response = client.get("/users/system", headers=headers)
assert response.status_code == 404