mirror of
https://github.com/quay/quay.git
synced 2026-01-26 06:21:37 +03:00
When FEATURE_SUPERUSERS_FULL_ACCESS=false, regular superusers could create/update/delete quotas for other users' organizations (returning 201/200), but couldn't view them (returning 403). This was a security bug - both read and write operations should require FULL_ACCESS permission to access other organizations' quotas. Root cause: Organization quota write endpoints used SuperUserPermission().can() instead of allow_if_superuser_with_full_access(), allowing any superuser to modify other orgs' quotas regardless of the FULL_ACCESS setting. Changes: - endpoints/api/namespacequota.py: Replace SuperUserPermission().can() with allow_if_superuser_with_full_access() in all quota write operations: * OrganizationQuotaList.post() - create quota * OrganizationQuota.put() - update quota * OrganizationQuota.delete() - delete quota * OrganizationQuotaLimitList.post() - create quota limit * OrganizationQuotaLimit.put() - update quota limit * OrganizationQuotaLimit.delete() - delete quota limit - endpoints/api/test/test_superuser_full_access.py: Add comprehensive tests for quota operations with and without FULL_ACCESS enabled (6 new tests) Note: Superuser panel endpoints (/v1/superuser/users/<namespace>/quota) were intentionally NOT changed - these are admin panel functions that should work with basic superuser permission, consistent with other panel operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
395 lines
16 KiB
Python
395 lines
16 KiB
Python
"""
|
|
Tests for superuser functionality with and without FEATURE_SUPERUSERS_FULL_ACCESS.
|
|
|
|
This tests the fix for the bug where superuser panel endpoints return 403
|
|
when FEATURE_SUPERUSERS_FULL_ACCESS is disabled, even though they should
|
|
work with just FEATURE_SUPER_USERS enabled.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from endpoints.api.globalmessages import GlobalUserMessages
|
|
from endpoints.api.namespacequota import (
|
|
OrganizationQuota,
|
|
OrganizationQuotaLimit,
|
|
OrganizationQuotaLimitList,
|
|
OrganizationQuotaList,
|
|
)
|
|
from endpoints.api.organization import Organization
|
|
from endpoints.api.repository import RepositoryList
|
|
from endpoints.api.superuser import (
|
|
SuperUserAggregateLogs,
|
|
SuperUserList,
|
|
SuperUserLogs,
|
|
SuperUserManagement,
|
|
SuperUserOrganizationList,
|
|
)
|
|
from endpoints.api.team import OrganizationTeam
|
|
from endpoints.api.test.shared import conduct_api_call
|
|
from endpoints.test.shared import client_with_identity
|
|
from test.fixtures import *
|
|
|
|
|
|
class TestSuperuserBasicAccessWithoutFullAccess:
|
|
"""
|
|
Tests that basic superuser panel endpoints work with FEATURE_SUPER_USERS=true
|
|
and FEATURE_SUPERUSERS_FULL_ACCESS=false.
|
|
|
|
These are core superuser functions and should NOT require full access.
|
|
"""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, app):
|
|
"""Disable SUPERUSERS_FULL_ACCESS for these tests."""
|
|
import features
|
|
|
|
# Ensure SUPER_USERS is enabled but FULL_ACCESS is disabled
|
|
features.import_features(
|
|
{
|
|
"FEATURE_SUPER_USERS": True,
|
|
"FEATURE_SUPERUSERS_FULL_ACCESS": False,
|
|
}
|
|
)
|
|
yield
|
|
# Reset to default test config
|
|
features.import_features(
|
|
{
|
|
"FEATURE_SUPER_USERS": True,
|
|
"FEATURE_SUPERUSERS_FULL_ACCESS": True,
|
|
}
|
|
)
|
|
|
|
def test_superuser_can_list_users_without_full_access(self, app):
|
|
"""
|
|
Test that superusers can access /v1/superuser/users/ without FULL_ACCESS.
|
|
|
|
This is a core superuser panel function and should work with just
|
|
FEATURE_SUPER_USERS enabled.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
result = conduct_api_call(cl, SuperUserList, "GET", None, None, 200)
|
|
assert result.json is not None
|
|
assert "users" in result.json
|
|
assert len(result.json["users"]) > 0
|
|
|
|
def test_superuser_can_get_user_details_without_full_access(self, app):
|
|
"""
|
|
Test that superusers can access /v1/superuser/users/<username> without FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"username": "randomuser"}
|
|
result = conduct_api_call(cl, SuperUserManagement, "GET", params, None, 200)
|
|
assert result.json is not None
|
|
assert result.json["username"] == "randomuser"
|
|
|
|
def test_superuser_can_list_organizations_without_full_access(self, app):
|
|
"""
|
|
Test that superusers can access /v1/superuser/organizations/ without FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
result = conduct_api_call(cl, SuperUserOrganizationList, "GET", None, None, 200)
|
|
assert result.json is not None
|
|
assert "organizations" in result.json
|
|
assert len(result.json["organizations"]) > 0
|
|
|
|
def test_superuser_can_view_logs_without_full_access(self, app):
|
|
"""
|
|
Test that superusers can access /v1/superuser/logs without FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
result = conduct_api_call(cl, SuperUserLogs, "GET", None, None, 200)
|
|
assert result.json is not None
|
|
assert "logs" in result.json
|
|
|
|
def test_superuser_can_view_aggregate_logs_without_full_access(self, app):
|
|
"""
|
|
Test that superusers can access /v1/superuser/aggregatelogs without FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"starttime": "01/01/2024 UTC", "endtime": "12/31/2024 UTC"}
|
|
result = conduct_api_call(cl, SuperUserAggregateLogs, "GET", params, None, 200)
|
|
assert result.json is not None
|
|
assert "aggregated" in result.json
|
|
|
|
def test_superuser_can_manage_global_messages_without_full_access(self, app):
|
|
"""
|
|
Test that superusers can create/delete global messages without FULL_ACCESS.
|
|
|
|
Managing global messages is a core superuser function.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Create a global message
|
|
body = {
|
|
"message": {
|
|
"severity": "info",
|
|
"media_type": "text/plain",
|
|
"content": "Test message",
|
|
}
|
|
}
|
|
result = conduct_api_call(cl, GlobalUserMessages, "POST", None, body, 201)
|
|
assert result.status_code == 201
|
|
|
|
|
|
class TestSuperuserFullAccessRequired:
|
|
"""
|
|
Tests that operations requiring FEATURE_SUPERUSERS_FULL_ACCESS are properly
|
|
blocked when the feature is disabled.
|
|
|
|
These operations bypass normal permission checks to access/modify resources
|
|
owned by other users/organizations.
|
|
"""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, app):
|
|
"""Disable SUPERUSERS_FULL_ACCESS for these tests."""
|
|
import features
|
|
from data import model
|
|
|
|
features.import_features(
|
|
{
|
|
"FEATURE_SUPER_USERS": True,
|
|
"FEATURE_SUPERUSERS_FULL_ACCESS": False,
|
|
"FEATURE_QUOTA_MANAGEMENT": True,
|
|
"FEATURE_EDIT_QUOTA": True,
|
|
}
|
|
)
|
|
|
|
# Create a test organization owned by randomuser (not devtable)
|
|
# This is needed to test that devtable (superuser) cannot modify it without FULL_ACCESS
|
|
randomuser = model.user.get_user("randomuser")
|
|
try:
|
|
model.organization.get_organization("testorg")
|
|
except model.InvalidOrganizationException:
|
|
model.organization.create_organization("testorg", "testorg@test.com", randomuser)
|
|
|
|
yield
|
|
# Note: We don't clean up the organization because it has foreign key constraints
|
|
# and the test database is reset between test runs anyway
|
|
|
|
features.import_features(
|
|
{
|
|
"FEATURE_SUPER_USERS": True,
|
|
"FEATURE_SUPERUSERS_FULL_ACCESS": True,
|
|
}
|
|
)
|
|
|
|
def test_superuser_cannot_create_repo_in_other_namespace_without_full_access(self, app):
|
|
"""
|
|
Test that superusers CANNOT create repos in other namespaces without FULL_ACCESS.
|
|
|
|
This is a permission bypass operation that requires FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Try to create a repo in another user's namespace
|
|
body = {
|
|
"namespace": "randomuser", # Not devtable's namespace
|
|
"repository": "test-repo",
|
|
"visibility": "private",
|
|
"description": "test",
|
|
}
|
|
# Should be blocked without FULL_ACCESS
|
|
conduct_api_call(cl, RepositoryList, "POST", None, body, 403)
|
|
|
|
def test_superuser_cannot_modify_other_org_team_without_full_access(self, app):
|
|
"""
|
|
Test that superusers CANNOT modify teams in other orgs without FULL_ACCESS.
|
|
|
|
This is a permission bypass operation that requires FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Try to create a team in testorg (owned by randomuser, not devtable)
|
|
params = {"orgname": "testorg", "teamname": "testteam"}
|
|
body = {"role": "member"}
|
|
# Should be blocked without FULL_ACCESS
|
|
conduct_api_call(cl, OrganizationTeam, "PUT", params, body, 403)
|
|
|
|
def test_superuser_cannot_create_quota_in_other_org_without_full_access(self, app):
|
|
"""
|
|
Test that superusers CANNOT create quotas in other orgs without FULL_ACCESS.
|
|
|
|
This reproduces the bug reported in PROJQUAY-9833.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Try to create a quota for testorg (owned by randomuser, not devtable)
|
|
params = {"orgname": "testorg"}
|
|
body = {"limit_bytes": 1073741824} # 1GB
|
|
# Should be blocked without FULL_ACCESS
|
|
conduct_api_call(cl, OrganizationQuotaList, "POST", params, body, 403)
|
|
|
|
def test_superuser_cannot_update_quota_in_other_org_without_full_access(self, app):
|
|
"""
|
|
Test that superusers CANNOT update quotas in other orgs without FULL_ACCESS.
|
|
|
|
This reproduces the bug reported in PROJQUAY-9833.
|
|
"""
|
|
from data import model
|
|
|
|
# Create a quota directly via the data model (only superusers can create quotas via API)
|
|
org = model.organization.get_organization("testorg")
|
|
quota = model.namespacequota.create_namespace_quota(org, 1073741824) # 1GB
|
|
quota_id = quota.id
|
|
|
|
# Try to update it as superuser without FULL_ACCESS
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "testorg", "quota_id": quota_id}
|
|
body = {"limit_bytes": 5368709120} # 5GB
|
|
# Should be blocked without FULL_ACCESS
|
|
conduct_api_call(cl, OrganizationQuota, "PUT", params, body, 403)
|
|
|
|
def test_superuser_cannot_delete_quota_in_other_org_without_full_access(self, app):
|
|
"""
|
|
Test that superusers CANNOT delete quotas in other orgs without FULL_ACCESS.
|
|
|
|
This reproduces the bug reported in PROJQUAY-9833.
|
|
"""
|
|
from data import model
|
|
|
|
# Create a quota directly via the data model
|
|
org = model.organization.get_organization("testorg")
|
|
quotas = model.namespacequota.get_namespace_quota_list("testorg")
|
|
if not quotas:
|
|
quota = model.namespacequota.create_namespace_quota(org, 1073741824) # 1GB
|
|
quota_id = quota.id
|
|
else:
|
|
quota_id = quotas[0].id
|
|
|
|
# Try to delete it as superuser without FULL_ACCESS
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "testorg", "quota_id": quota_id}
|
|
# Should be blocked without FULL_ACCESS
|
|
conduct_api_call(cl, OrganizationQuota, "DELETE", params, None, 403)
|
|
|
|
|
|
class TestSuperuserFullAccessEnabled:
|
|
"""
|
|
Tests that operations work correctly when FEATURE_SUPERUSERS_FULL_ACCESS is enabled.
|
|
|
|
With full access, superusers can bypass permission checks.
|
|
"""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, app):
|
|
"""Enable SUPERUSERS_FULL_ACCESS for these tests."""
|
|
import features
|
|
from data import model
|
|
|
|
features.import_features(
|
|
{
|
|
"FEATURE_SUPER_USERS": True,
|
|
"FEATURE_SUPERUSERS_FULL_ACCESS": True,
|
|
"FEATURE_QUOTA_MANAGEMENT": True,
|
|
"FEATURE_EDIT_QUOTA": True,
|
|
}
|
|
)
|
|
|
|
# Create a test organization owned by randomuser if it doesn't exist
|
|
randomuser = model.user.get_user("randomuser")
|
|
try:
|
|
model.organization.get_organization("testorg2")
|
|
except model.InvalidOrganizationException:
|
|
model.organization.create_organization("testorg2", "testorg2@test.com", randomuser)
|
|
|
|
yield
|
|
|
|
def test_superuser_can_view_other_org_details_with_full_access(self, app):
|
|
"""
|
|
Test that superusers CAN view other organizations' details with FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Can view org that devtable doesn't own
|
|
params = {"orgname": "buynlarge"}
|
|
result = conduct_api_call(cl, Organization, "GET", params, None, 200)
|
|
assert result.json is not None
|
|
assert result.json["name"] == "buynlarge"
|
|
|
|
def test_superuser_can_modify_other_org_team_with_full_access(self, app):
|
|
"""
|
|
Test that superusers CAN modify teams in other orgs with FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Can create team in org that devtable doesn't own
|
|
params = {"orgname": "buynlarge", "teamname": "testteam"}
|
|
body = {"role": "member"}
|
|
result = conduct_api_call(cl, OrganizationTeam, "PUT", params, body, 200)
|
|
assert result.json is not None
|
|
|
|
def test_superuser_can_create_repo_in_other_namespace_with_full_access(self, app):
|
|
"""
|
|
Test that superusers CAN create repos in other namespaces with FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Can create repo in another user's namespace
|
|
body = {
|
|
"namespace": "randomuser",
|
|
"repository": "test-repo-full-access",
|
|
"visibility": "public", # Use public to avoid license limit issues
|
|
"description": "test with full access",
|
|
}
|
|
result = conduct_api_call(cl, RepositoryList, "POST", None, body, 201)
|
|
assert result.json is not None
|
|
|
|
def test_superuser_can_create_quota_in_other_org_with_full_access(self, app):
|
|
"""
|
|
Test that superusers CAN create quotas in other orgs with FULL_ACCESS.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
# Can create quota in org that devtable doesn't own
|
|
params = {"orgname": "testorg2"}
|
|
body = {"limit_bytes": 1073741824} # 1GB
|
|
result = conduct_api_call(cl, OrganizationQuotaList, "POST", params, body, 201)
|
|
assert result.status_code == 201
|
|
|
|
def test_superuser_can_update_quota_in_other_org_with_full_access(self, app):
|
|
"""
|
|
Test that superusers CAN update quotas in other orgs with FULL_ACCESS.
|
|
"""
|
|
from data import model
|
|
|
|
# Get or create a quota
|
|
quotas = model.namespacequota.get_namespace_quota_list("testorg2")
|
|
if not quotas:
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "testorg2"}
|
|
body = {"limit_bytes": 1073741824}
|
|
conduct_api_call(cl, OrganizationQuotaList, "POST", params, body, 201)
|
|
quotas = model.namespacequota.get_namespace_quota_list("testorg2")
|
|
|
|
quota_id = quotas[0].id
|
|
|
|
# Update the quota as superuser with FULL_ACCESS
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "testorg2", "quota_id": quota_id}
|
|
body = {"limit_bytes": 5368709120} # 5GB
|
|
result = conduct_api_call(cl, OrganizationQuota, "PUT", params, body, 200)
|
|
assert result.json is not None
|
|
assert result.json["limit_bytes"] == 5368709120
|
|
|
|
def test_superuser_can_delete_quota_in_other_org_with_full_access(self, app):
|
|
"""
|
|
Test that superusers CAN delete quotas in other orgs with FULL_ACCESS.
|
|
"""
|
|
from data import model
|
|
|
|
# Create a quota to delete
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "testorg2"}
|
|
body = {"limit_bytes": 2147483648} # 2GB
|
|
result = conduct_api_call(cl, OrganizationQuotaList, "POST", params, body, 201)
|
|
|
|
# Get the quota ID
|
|
quotas = model.namespacequota.get_namespace_quota_list("testorg2")
|
|
# Find the quota we just created (2GB)
|
|
quota_id = None
|
|
for q in quotas:
|
|
if q.limit_bytes == 2147483648:
|
|
quota_id = q.id
|
|
break
|
|
|
|
assert quota_id is not None
|
|
|
|
# Delete the quota as superuser with FULL_ACCESS
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "testorg2", "quota_id": quota_id}
|
|
result = conduct_api_call(cl, OrganizationQuota, "DELETE", params, None, 204)
|
|
assert result.status_code == 204
|