mirror of
https://github.com/quay/quay.git
synced 2025-11-16 11:42:27 +03:00
fix(api): superuser panel access without SUPERUSERS_FULL_ACCESS (PROJQUAY-9693) (#4455)
fix(api): implement proper superuser permission model and fix access controls Fixes multiple issues with superuser functionality and implements a comprehensive permission model for FEATURE_SUPERUSERS_FULL_ACCESS: **Permission Model:** - Global Readonly Superusers (auditors): Always have read access to all content, independent of FEATURE_SUPERUSERS_FULL_ACCESS setting - Regular Superusers: Can access /v1/superuser endpoints and their own content. Require FEATURE_SUPERUSERS_FULL_ACCESS=true for cross-namespace read access - Full Access Superusers: Regular superusers with FULL_ACCESS enabled, can perform CRUD on content they don't own - Write operations: Only allowed for full access superusers (global readonly superusers never get write access) **Key Fixes:** 1. Fixed superuser panel endpoints returning 403 when FULL_ACCESS was disabled. Basic panel operations (user list, logs, org list, messages) now work with just FEATURE_SUPER_USERS enabled. 2. Updated decorators to properly differentiate between basic superuser operations and permission bypass operations. 3. Implemented license bypass: Superusers with FULL_ACCESS now bypass license/quota limits when creating or modifying private repositories. 4. Fixed 18 permission checks across 7 files to properly implement cross-namespace access controls for different superuser types. **Changes:** - endpoints/api/__init__.py: Fixed allow_if_superuser(), require_repo_permission, and decorators - endpoints/api/superuser.py: Updated SuperUserAppTokens permission check - endpoints/api/organization.py: Updated 4 GET endpoints to require FULL_ACCESS - endpoints/api/namespacequota.py: Updated 2 GET endpoints to require FULL_ACCESS - endpoints/api/team.py: Updated 2 GET endpoints to require FULL_ACCESS - endpoints/api/prototype.py: Updated 1 GET endpoint to require FULL_ACCESS - endpoints/api/policy.py: Updated auto-prune policy endpoints - endpoints/api/robot.py: Updated robot endpoints - endpoints/api/build.py: Updated repository build logs - endpoints/api/repository.py: Added license bypass for superusers with FULL_ACCESS - endpoints/api/repository_models_pre_oci.py: Updated repository visibility query - endpoints/api/logs.py: Fixed log access to require FULL_ACCESS for permission bypass - endpoints/api/test/test_superuser_full_access.py: Added comprehensive test suite - endpoints/api/test/test_appspecifictoken.py: Updated test mocking and added 403 test - test/test_api_usage.py: Updated test expectations for license bypass behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2412,18 +2412,18 @@ class TestChangeRepoVisibility(ApiTestCase):
|
||||
# Change the subscription of the namespace.
|
||||
self.putJsonResponse(UserPlan, data=dict(plan="personal-2018"))
|
||||
|
||||
# Try to make private.
|
||||
# Try to make private. Superusers with full access bypass license limits.
|
||||
self.postJsonResponse(
|
||||
RepositoryVisibility,
|
||||
params=dict(repository=self.SIMPLE_REPO),
|
||||
data=dict(visibility="private"),
|
||||
expected_code=402,
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
# Verify the visibility.
|
||||
# Verify the visibility changed to private (superuser bypassed license limit).
|
||||
json = self.getJsonResponse(Repository, params=dict(repository=self.SIMPLE_REPO))
|
||||
|
||||
self.assertEqual(True, json["is_public"])
|
||||
self.assertEqual(False, json["is_public"])
|
||||
|
||||
def test_changevisibility(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
@@ -2452,6 +2452,76 @@ class TestChangeRepoVisibility(ApiTestCase):
|
||||
|
||||
self.assertEqual(False, json["is_public"])
|
||||
|
||||
def test_plan_limit_enforcement_for_regular_users(self):
|
||||
"""
|
||||
Test that plan limits are enforced for regular (non-superuser) users.
|
||||
|
||||
This test is identical to test_trychangevisibility but uses a non-superuser
|
||||
to validate that license limits are still enforced for regular users.
|
||||
"""
|
||||
from data import model
|
||||
|
||||
# Use NO_ACCESS_USER (freshuser) who is definitely not a superuser
|
||||
# Set up billing for this user so the UserPlan endpoint works
|
||||
user = model.user.get_user(NO_ACCESS_USER)
|
||||
user.stripe_id = "test_stripe_id_freshuser"
|
||||
user.save()
|
||||
|
||||
self.login(NO_ACCESS_USER)
|
||||
|
||||
# Change the subscription to a limited plan first
|
||||
self.putJsonResponse(UserPlan, data=dict(plan="personal-2018"))
|
||||
|
||||
# Create private repositories until we hit the plan limit
|
||||
# The personal-2018 plan allows a limited number of private repos
|
||||
for i in range(20):
|
||||
try:
|
||||
self.postJsonResponse(
|
||||
RepositoryList,
|
||||
data=dict(
|
||||
namespace=NO_ACCESS_USER,
|
||||
repository=f"private_{i}",
|
||||
description="private repo to exhaust limit",
|
||||
visibility="private",
|
||||
),
|
||||
expected_code=201,
|
||||
)
|
||||
except AssertionError:
|
||||
# Hit the limit, which is expected
|
||||
break
|
||||
|
||||
test_repo = NO_ACCESS_USER + "/simple"
|
||||
|
||||
# Create one more repository as public (should work)
|
||||
self.postJsonResponse(
|
||||
RepositoryList,
|
||||
data=dict(
|
||||
namespace=NO_ACCESS_USER,
|
||||
repository="simple",
|
||||
description="test repository",
|
||||
visibility="public",
|
||||
),
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
# Verify the visibility.
|
||||
json = self.getJsonResponse(Repository, params=dict(repository=test_repo))
|
||||
|
||||
self.assertEqual(True, json["is_public"])
|
||||
|
||||
# Try to make private - should be blocked by plan limit for regular users.
|
||||
self.postJsonResponse(
|
||||
RepositoryVisibility,
|
||||
params=dict(repository=test_repo),
|
||||
data=dict(visibility="private"),
|
||||
expected_code=402,
|
||||
)
|
||||
|
||||
# Verify the visibility stayed public (blocked by plan limit).
|
||||
json = self.getJsonResponse(Repository, params=dict(repository=test_repo))
|
||||
|
||||
self.assertEqual(True, json["is_public"])
|
||||
|
||||
|
||||
class TestDeleteRepository(ApiTestCase):
|
||||
SIMPLE_REPO = ADMIN_ACCESS_USER + "/simple"
|
||||
|
||||
Reference in New Issue
Block a user