mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 16:09:41 +00:00
389 lines
13 KiB
Python
389 lines
13 KiB
Python
"""
|
|
Sourcemap RBAC E2E Tests
|
|
|
|
Tests for Role-Based Access Control on sourcemap APIs.
|
|
|
|
Test Coverage:
|
|
- Viewer role permissions (read-only: can list, cannot upload/delete)
|
|
- Editor role permissions (write access: can upload, list, delete)
|
|
- Admin role permissions (full access: can upload, list, delete)
|
|
|
|
Expected RBAC Behavior (Role-Based):
|
|
- Viewer: Read-only access (list, translate stacktrace)
|
|
- Editor: Full write access (upload, list, delete)
|
|
- Admin/Root: Full access (upload, list, delete)
|
|
|
|
Prerequisites:
|
|
- OpenObserve ENTERPRISE build running on ZO_BASE_URL
|
|
- Root user credentials for user creation
|
|
- OpenFGA enabled for RBAC enforcement
|
|
|
|
Note:
|
|
- Skipped at runtime by `_require_custom_roles` when the build has no custom
|
|
roles (GET /roles answers 403 on OSS); they run on enterprise
|
|
- Tests must run serially due to shared module-scoped fixture
|
|
- Uses session-based authentication (matches enterprise RBAC test pattern)
|
|
"""
|
|
|
|
import pytest
|
|
import logging
|
|
import requests
|
|
|
|
pytestmark = [
|
|
pytest.mark.order(2),
|
|
]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Test user credentials (created during test run)
|
|
test_users = {}
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
def _require_custom_roles(create_session, base_url):
|
|
"""Skip on builds without custom roles, rather than skipping everywhere.
|
|
|
|
These cases need viewer/editor/admin accounts. An OSS build has no custom
|
|
roles, so the users cannot exist and every case errors at login.
|
|
|
|
This replaces an unconditional `pytest.mark.skip`, which skipped the module
|
|
on EVERY build -- enterprise included -- so these cases had never run
|
|
anywhere. The capability is probed rather than assumed from the edition, in
|
|
the same way `test_anomaly_conditions.py` probes its endpoint.
|
|
|
|
The probe is deliberately READ-ONLY. Creating and deleting a probe user
|
|
would mutate the shared `default` org from an autouse fixture, in parallel
|
|
(xdist) with the RBAC suite that tests user permissions -- `GET /roles`
|
|
answers 403 "Not Supported" on OSS and lists roles on enterprise, which is
|
|
the same signal without touching anything.
|
|
"""
|
|
probe = create_session.get(f"{base_url}api/default/roles")
|
|
if probe.status_code != 200:
|
|
pytest.skip(
|
|
f"custom roles are not available on this build "
|
|
f"(GET /roles -> {probe.status_code})"
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
def setup_rbac_users(create_session, base_url, org_id):
|
|
"""
|
|
Create test users with different roles for RBAC testing.
|
|
|
|
Creates:
|
|
- viewer@sourcemap-test.local (role: viewer)
|
|
- editor@sourcemap-test.local (role: editor)
|
|
- admin@sourcemap-test.local (role: admin)
|
|
"""
|
|
global test_users
|
|
|
|
session = create_session
|
|
url = base_url
|
|
|
|
logger.info("========== SETUP: Creating RBAC test users ==========")
|
|
|
|
roles = ['viewer', 'editor', 'admin']
|
|
|
|
for role in roles:
|
|
email = f"{role}@sourcemap-test.local"
|
|
# The enterprise password policy requires at least 15 characters; the
|
|
# previous 12-character value was rejected with HTTP 400, so every case
|
|
# then failed at login.
|
|
password = "TestPass123!Secure"
|
|
|
|
logger.info(f"Creating user: {email} with role: {role}")
|
|
|
|
try:
|
|
response = session.post(
|
|
f"{url}api/{org_id}/users",
|
|
json={
|
|
'email': email,
|
|
'password': password,
|
|
'first_name': 'Test',
|
|
'last_name': role.capitalize(),
|
|
'role': role
|
|
}
|
|
)
|
|
|
|
if response.status_code in [200, 201]:
|
|
logger.info(f"✅ User created: {email}")
|
|
else:
|
|
logger.warning(f"⚠️ User creation returned {response.status_code}: {response.text}")
|
|
|
|
# Create authenticated session via login endpoint
|
|
user_session = create_user_session(email, password, url)
|
|
if not user_session:
|
|
# Fail loudly rather than skip. The module gate already
|
|
# established that this build supports custom roles, so failing
|
|
# to provision one here is a real defect -- skipping would hide
|
|
# it, which is how the 15-character password policy went
|
|
# unnoticed. The create response is included so the cause is
|
|
# visible without re-running.
|
|
logger.error(f"Failed to create session for {email}")
|
|
raise AssertionError(
|
|
f"could not provision the {role} user ({email}) on a build that "
|
|
f"reports custom-role support; create returned "
|
|
f"{response.status_code}: {response.text[:200]}"
|
|
)
|
|
|
|
# Store user info
|
|
test_users[role] = {
|
|
'email': email,
|
|
'password': password,
|
|
'role': role,
|
|
'session': user_session
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to create user {email}: {e}")
|
|
raise
|
|
|
|
logger.info(f"========== SETUP COMPLETE: Created {len(test_users)} test users ==========")
|
|
|
|
yield test_users
|
|
|
|
# Cleanup
|
|
logger.info("========== TEARDOWN: Deleting RBAC test users ==========")
|
|
|
|
for role, user_info in test_users.items():
|
|
try:
|
|
response = session.delete(
|
|
f"{url}api/{org_id}/users/{user_info['email']}"
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
logger.info(f"✅ User deleted: {user_info['email']}")
|
|
else:
|
|
logger.warning(f"⚠️ User deletion returned {response.status_code}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete user {user_info['email']}: {e}")
|
|
|
|
logger.info("========== TEARDOWN COMPLETE ==========")
|
|
|
|
|
|
def create_user_session(email, password, base_url):
|
|
"""
|
|
Create authenticated session via /auth/login endpoint.
|
|
|
|
This matches the enterprise RBAC test pattern, ensuring proper
|
|
OpenFGA permission setup through the login flow.
|
|
"""
|
|
user_session = requests.Session()
|
|
login_url = f"{base_url}auth/login"
|
|
login_data = {"name": email, "password": password}
|
|
|
|
try:
|
|
login_resp = user_session.post(login_url, json=login_data, timeout=30)
|
|
|
|
if login_resp.status_code != 200:
|
|
logger.error(f"Login failed for {email}: {login_resp.status_code} - {login_resp.text}")
|
|
return None
|
|
|
|
logger.info(f"Successfully logged in as {email}")
|
|
return user_session # Returns session with cookies from login
|
|
|
|
except requests.RequestException as e:
|
|
logger.error(f"Network error during login for {email}: {str(e)}")
|
|
return None
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def test_sourcemaps_zip():
|
|
"""
|
|
Path to static sourcemaps ZIP fixture.
|
|
Uses pre-built sourcemaps from fixtures directory.
|
|
"""
|
|
from pathlib import Path
|
|
# 3 parents from tests/api-testing/tests/rum/test_sourcemap_rbac.py -> tests/api-testing/ where fixtures/ lives
|
|
zip_path = Path(__file__).parent.parent.parent / "fixtures" / "sourcemaps" / "sourcemaps.zip"
|
|
|
|
if not zip_path.exists():
|
|
pytest.skip(f"Sourcemaps ZIP not found at {zip_path}. Static fixture missing.")
|
|
|
|
return str(zip_path)
|
|
|
|
|
|
# ==============================================================================
|
|
# P1 - VIEWER PERMISSIONS
|
|
# ==============================================================================
|
|
|
|
def test_p1_viewer_cannot_upload_sourcemaps(base_url, org_id, test_sourcemaps_zip):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Viewer role CANNOT upload sourcemaps (read-only access).
|
|
|
|
Expected: 401 or 403 (Unauthorized/Forbidden)
|
|
"""
|
|
assert 'viewer' in test_users, "Viewer user was not created - RBAC setup failed"
|
|
|
|
viewer_session = test_users['viewer']['session']
|
|
|
|
with open(test_sourcemaps_zip, 'rb') as f:
|
|
files = {'file': ('sourcemaps.zip', f, 'application/zip')}
|
|
data = {
|
|
'service': 'o2-sourcemap-rbac-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-rbac'
|
|
}
|
|
|
|
response = viewer_session.post(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
files=files,
|
|
data=data
|
|
)
|
|
|
|
assert response.status_code in [401, 403], \
|
|
f"Viewer should NOT be able to upload sourcemaps. Got {response.status_code}: {response.text}"
|
|
logger.info("✅ Viewer correctly denied upload access")
|
|
|
|
|
|
def test_p1_viewer_can_list_sourcemaps(base_url, org_id):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Viewer role CAN list sourcemaps (read-only access).
|
|
|
|
Expected: 200 OK with list of sourcemaps
|
|
"""
|
|
assert 'viewer' in test_users, "Viewer user was not created - RBAC setup failed"
|
|
|
|
viewer_session = test_users['viewer']['session']
|
|
|
|
response = viewer_session.get(
|
|
f"{base_url}api/{org_id}/sourcemaps"
|
|
)
|
|
|
|
assert response.status_code == 200, \
|
|
f"Viewer should be able to list sourcemaps. Got {response.status_code}: {response.text}"
|
|
|
|
maps = response.json()
|
|
assert isinstance(maps, list), "Response should be a list"
|
|
logger.info(f"✅ Viewer correctly allowed list access ({len(maps)} sourcemaps)")
|
|
|
|
|
|
def test_p1_viewer_cannot_delete_sourcemaps(base_url, org_id):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Viewer role CANNOT delete sourcemaps (read-only access).
|
|
|
|
Expected: 401 or 403 (Unauthorized/Forbidden)
|
|
"""
|
|
assert 'viewer' in test_users, "Viewer user was not created - RBAC setup failed"
|
|
|
|
viewer_session = test_users['viewer']['session']
|
|
|
|
# Use isolated service/version to avoid affecting shared test data
|
|
params = {
|
|
'service': 'o2-sourcemap-rbac-viewer-delete-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-rbac-delete'
|
|
}
|
|
|
|
response = viewer_session.delete(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params=params
|
|
)
|
|
|
|
assert response.status_code in [401, 403], \
|
|
f"Viewer should NOT be able to delete sourcemaps. Got {response.status_code}: {response.text}"
|
|
logger.info("✅ Viewer correctly denied delete access")
|
|
|
|
|
|
# ==============================================================================
|
|
# P1 - EDITOR PERMISSIONS
|
|
# ==============================================================================
|
|
|
|
def test_p1_editor_can_upload_sourcemaps(base_url, org_id, test_sourcemaps_zip):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Editor role CAN upload sourcemaps (write access).
|
|
|
|
Expected: 200 or 201 (Success/Created)
|
|
"""
|
|
assert 'editor' in test_users, "Editor user was not created - RBAC setup failed"
|
|
|
|
editor_session = test_users['editor']['session']
|
|
|
|
with open(test_sourcemaps_zip, 'rb') as f:
|
|
files = {'file': ('sourcemaps.zip', f, 'application/zip')}
|
|
data = {
|
|
'service': 'o2-sourcemap-editor-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-editor'
|
|
}
|
|
|
|
response = editor_session.post(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
files=files,
|
|
data=data
|
|
)
|
|
|
|
assert response.status_code in [200, 201], \
|
|
f"Editor should be able to upload sourcemaps. Got {response.status_code}: {response.text}"
|
|
logger.info("✅ Editor correctly allowed upload access")
|
|
|
|
# Cleanup: Delete what we just uploaded (editors have full write access)
|
|
delete_response = editor_session.delete(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params={'service': data['service'], 'env': data['env'], 'version': data['version']}
|
|
)
|
|
|
|
if delete_response.status_code == 200:
|
|
logger.info("✅ Editor cleanup successful")
|
|
else:
|
|
logger.warning(f"⚠️ Editor cleanup returned {delete_response.status_code}: {delete_response.text}")
|
|
|
|
|
|
# ==============================================================================
|
|
# P1 - ADMIN PERMISSIONS
|
|
# ==============================================================================
|
|
|
|
def test_p1_admin_can_delete_sourcemaps(create_session, base_url, org_id, test_sourcemaps_zip):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Admin role can delete sourcemaps.
|
|
"""
|
|
assert 'admin' in test_users, "Admin user was not created - RBAC setup failed"
|
|
|
|
# First, upload sourcemaps as root
|
|
root_session = create_session
|
|
|
|
with open(test_sourcemaps_zip, 'rb') as f:
|
|
files = {'file': ('sourcemaps.zip', f, 'application/zip')}
|
|
data = {
|
|
'service': 'o2-sourcemap-admin-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-admin'
|
|
}
|
|
|
|
upload_response = root_session.post(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
files=files,
|
|
data=data
|
|
)
|
|
|
|
assert upload_response.status_code in [200, 201], \
|
|
f"Root upload failed: {upload_response.status_code}"
|
|
|
|
# Try to delete as admin
|
|
admin_session = test_users['admin']['session']
|
|
|
|
delete_response = admin_session.delete(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params={'service': data['service'], 'env': data['env'], 'version': data['version']}
|
|
)
|
|
|
|
assert delete_response.status_code == 200, \
|
|
f"Admin delete failed: {delete_response.status_code} - {delete_response.text}"
|
|
|
|
# Verify deletion
|
|
list_response = root_session.get(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params={'service': data['service'], 'env': data['env'], 'version': data['version']}
|
|
)
|
|
|
|
maps = list_response.json()
|
|
assert len(maps) == 0, f"Expected empty list after delete, got {len(maps)}"
|
|
|
|
logger.info("✅ Admin can delete sourcemaps")
|