1
0
mirror of https://github.com/quay/quay.git synced 2025-11-17 23:02:34 +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:
Dave O'Connor
2025-11-13 09:38:11 -05:00
committed by GitHub
parent 573a03e98b
commit 2511b45e89
17 changed files with 564 additions and 90 deletions

View File

@@ -6,6 +6,7 @@ import logging
from flask import abort, request
import features
from auth import scopes
from auth.auth_context import get_authenticated_user
from auth.permissions import (
@@ -27,6 +28,7 @@ from endpoints.api import (
allow_if_any_superuser,
allow_if_global_readonly_superuser,
allow_if_superuser,
allow_if_superuser_with_full_access,
log_action,
max_json_size,
nickname,
@@ -221,7 +223,12 @@ class OrgRobotList(ApiResource):
List the organization's robots.
"""
permission = OrganizationMemberPermission(orgname)
if permission.can() or allow_if_any_superuser():
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
if (
permission.can()
or allow_if_global_readonly_superuser()
or (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
):
include_token = (
AdministerOrganizationPermission(orgname).can()
or allow_if_global_readonly_superuser()
@@ -261,7 +268,12 @@ class OrgRobot(ApiResource):
Returns the organization's robot with the specified name.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_any_superuser():
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
if (
permission.can()
or allow_if_global_readonly_superuser()
or (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
):
robot = model.get_org_robot(robot_shortname, orgname)
return robot.to_dict(include_metadata=True, include_token=True)
@@ -276,7 +288,7 @@ class OrgRobot(ApiResource):
Create a new robot in the organization.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_superuser():
if permission.can() or allow_if_superuser_with_full_access():
create_data = request.get_json(silent=True) or {}
try:
@@ -308,7 +320,7 @@ class OrgRobot(ApiResource):
Delete an existing organization robot.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_superuser():
if permission.can() or allow_if_superuser_with_full_access():
robot_username = format_robot_username(orgname, robot_shortname)
if not model.robot_has_mirror(robot_username):
model.delete_robot(robot_username)
@@ -360,7 +372,12 @@ class OrgRobotPermissions(ApiResource):
Returns the list of repository permissions for the org's robot.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_any_superuser():
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
if (
permission.can()
or allow_if_global_readonly_superuser()
or (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
):
robot = model.get_org_robot(robot_shortname, orgname)
permissions = model.list_robot_permissions(robot.name)
@@ -408,7 +425,7 @@ class RegenerateOrgRobot(ApiResource):
Regenerates the token for an organization robot.
"""
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_superuser():
if permission.can() or allow_if_superuser_with_full_access():
robot = model.regenerate_org_robot_token(robot_shortname, orgname)
log_action("regenerate_robot_token", orgname, {"robot": robot_shortname})
return robot.to_dict(include_token=True)
@@ -431,7 +448,12 @@ class OrgRobotFederation(ApiResource):
@require_scope(scopes.ORG_ADMIN)
def get(self, orgname, robot_shortname):
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_any_superuser():
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
if (
permission.can()
or allow_if_global_readonly_superuser()
or (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
):
robot_username = format_robot_username(orgname, robot_shortname)
robot = lookup_robot(robot_username)
return get_robot_federation_config(robot)
@@ -442,7 +464,7 @@ class OrgRobotFederation(ApiResource):
@validate_json_request("CreateRobotFederation", optional=False)
def post(self, orgname, robot_shortname):
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_superuser():
if permission.can() or allow_if_superuser_with_full_access():
fed_config = self._parse_federation_config(request)
robot_username = format_robot_username(orgname, robot_shortname)
@@ -460,7 +482,7 @@ class OrgRobotFederation(ApiResource):
@require_scope(scopes.ORG_ADMIN)
def delete(self, orgname, robot_shortname):
permission = AdministerOrganizationPermission(orgname)
if permission.can() or allow_if_superuser():
if permission.can() or allow_if_superuser_with_full_access():
robot_username = format_robot_username(orgname, robot_shortname)
robot = lookup_robot(robot_username)
delete_robot_federation_config(robot)