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:
@@ -350,11 +350,24 @@ def require_repo_permission(permission_class, scope, allow_public=False):
|
|||||||
):
|
):
|
||||||
return func(self, namespace, repository, *args, **kwargs)
|
return func(self, namespace, repository, *args, **kwargs)
|
||||||
|
|
||||||
if features.SUPERUSERS_FULL_ACCESS and allow_for_superuser:
|
if allow_for_superuser:
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
|
|
||||||
if user is not None and allow_if_superuser():
|
if user is not None:
|
||||||
return func(self, namespace, repository, *args, **kwargs)
|
# For read operations that also allow global readonly superusers,
|
||||||
|
# allow any superuser with FULL_ACCESS
|
||||||
|
if (
|
||||||
|
allow_for_global_readonly_superuser
|
||||||
|
and features.SUPERUSERS_FULL_ACCESS
|
||||||
|
and allow_if_any_superuser()
|
||||||
|
):
|
||||||
|
return func(self, namespace, repository, *args, **kwargs)
|
||||||
|
# For write operations, only allow regular superusers with FULL_ACCESS
|
||||||
|
elif (
|
||||||
|
not allow_for_global_readonly_superuser
|
||||||
|
and allow_if_superuser_with_full_access()
|
||||||
|
):
|
||||||
|
return func(self, namespace, repository, *args, **kwargs)
|
||||||
|
|
||||||
if allow_for_global_readonly_superuser and allow_if_global_readonly_superuser():
|
if allow_for_global_readonly_superuser and allow_if_global_readonly_superuser():
|
||||||
return func(self, namespace, repository, *args, **kwargs)
|
return func(self, namespace, repository, *args, **kwargs)
|
||||||
@@ -395,7 +408,7 @@ def require_user_permission(permission_class, scope=None):
|
|||||||
if permission.can():
|
if permission.can():
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
if features.SUPERUSERS_FULL_ACCESS and allow_for_superuser and allow_if_superuser():
|
if allow_for_superuser and allow_if_superuser_with_full_access():
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
@@ -493,18 +506,47 @@ log_unauthorized_delete = log_unauthorized("delete_tag_failed")
|
|||||||
|
|
||||||
|
|
||||||
def allow_if_superuser():
|
def allow_if_superuser():
|
||||||
|
"""
|
||||||
|
Returns True if the user is a regular superuser (not global readonly).
|
||||||
|
|
||||||
|
This is for basic superuser panel access and should work when FEATURE_SUPER_USERS
|
||||||
|
is enabled, regardless of SUPERUSERS_FULL_ACCESS. This does NOT grant permission
|
||||||
|
to bypass normal access controls on other users' resources.
|
||||||
|
|
||||||
|
For operations that need to bypass normal permission checks (like accessing other
|
||||||
|
organizations or creating repos in other namespaces), use allow_if_superuser_with_full_access().
|
||||||
|
"""
|
||||||
|
return SuperUserPermission().can()
|
||||||
|
|
||||||
|
|
||||||
|
def allow_if_superuser_with_full_access():
|
||||||
|
"""
|
||||||
|
Returns True if the user is a superuser with full access enabled.
|
||||||
|
|
||||||
|
This is for operations that bypass normal permission checks to access or modify
|
||||||
|
resources owned by other users/organizations. Examples include:
|
||||||
|
- Creating repositories in other namespaces
|
||||||
|
- Modifying teams/robots in other organizations
|
||||||
|
- Accessing private data of other organizations
|
||||||
|
|
||||||
|
Requires both:
|
||||||
|
- User is a superuser (FEATURE_SUPER_USERS enabled and user in SUPER_USERS list)
|
||||||
|
- FEATURE_SUPERUSERS_FULL_ACCESS is enabled
|
||||||
|
|
||||||
|
Note: Global readonly superusers are explicitly excluded from this permission.
|
||||||
|
"""
|
||||||
return bool(features.SUPERUSERS_FULL_ACCESS and SuperUserPermission().can())
|
return bool(features.SUPERUSERS_FULL_ACCESS and SuperUserPermission().can())
|
||||||
|
|
||||||
|
|
||||||
def allow_if_any_superuser():
|
def allow_if_any_superuser():
|
||||||
"""
|
"""
|
||||||
Returns True if the user is either a regular superuser (with SUPERUSERS_FULL_ACCESS enabled)
|
Returns True if the user is either a regular superuser or a global readonly superuser.
|
||||||
or a global readonly superuser.
|
|
||||||
|
|
||||||
Since these two types are mutually exclusive, this is a convenience helper for read-only
|
Since these two types are mutually exclusive, this is a convenience helper for read-only
|
||||||
endpoints that should be accessible to both types of superusers.
|
endpoints that should be accessible to both types of superusers (like viewing user lists,
|
||||||
|
logs, organizations in the superuser panel).
|
||||||
|
|
||||||
Note: Regular superusers require SUPERUSERS_FULL_ACCESS to be enabled, but global readonly
|
Note: Regular superusers work with just FEATURE_SUPER_USERS enabled. Global readonly
|
||||||
superusers are always allowed (when the feature is enabled) since they're read-only by design.
|
superusers are always allowed (when the feature is enabled) since they're read-only by design.
|
||||||
"""
|
"""
|
||||||
return allow_if_superuser() or allow_if_global_readonly_superuser()
|
return allow_if_superuser() or allow_if_global_readonly_superuser()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from endpoints.api import (
|
|||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
api,
|
api,
|
||||||
disallow_for_app_repositories,
|
disallow_for_app_repositories,
|
||||||
disallow_for_non_normal_repositories,
|
disallow_for_non_normal_repositories,
|
||||||
@@ -310,7 +311,7 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||||||
(robot_namespace, _) = result
|
(robot_namespace, _) = result
|
||||||
if (
|
if (
|
||||||
not AdministerOrganizationPermission(robot_namespace).can()
|
not AdministerOrganizationPermission(robot_namespace).can()
|
||||||
and not allow_if_superuser()
|
and not allow_if_superuser_with_full_access()
|
||||||
):
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
else:
|
else:
|
||||||
@@ -326,7 +327,7 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||||||
not ModifyRepositoryPermission(
|
not ModifyRepositoryPermission(
|
||||||
associated_repository.namespace_user.username, associated_repository.name
|
associated_repository.namespace_user.username, associated_repository.name
|
||||||
)
|
)
|
||||||
and not allow_if_superuser()
|
and not allow_if_superuser_with_full_access()
|
||||||
):
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
@@ -516,7 +517,13 @@ class RepositoryBuildLogs(RepositoryParamResource):
|
|||||||
Return the build logs for the build specified by the build uuid.
|
Return the build logs for the build specified by the build uuid.
|
||||||
"""
|
"""
|
||||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||||
if not features.READER_BUILD_LOGS and not can_write and not allow_if_any_superuser():
|
# Global readonly superusers can always view build logs, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not features.READER_BUILD_LOGS
|
||||||
|
and not can_write
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
build = model.build.get_repository_build(build_uuid)
|
build = model.build.get_repository_build(build_uuid)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from endpoints.api import (
|
|||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
format_date,
|
format_date,
|
||||||
log_action,
|
log_action,
|
||||||
nickname,
|
nickname,
|
||||||
@@ -204,7 +205,7 @@ class OrgLogs(ApiResource):
|
|||||||
List the logs for the specified organization.
|
List the logs for the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
performer_name = parsed_args["performer"]
|
performer_name = parsed_args["performer"]
|
||||||
start_time = parsed_args["starttime"]
|
start_time = parsed_args["starttime"]
|
||||||
end_time = parsed_args["endtime"]
|
end_time = parsed_args["endtime"]
|
||||||
@@ -296,7 +297,7 @@ class OrgAggregateLogs(ApiResource):
|
|||||||
Gets the aggregated logs for the specified organization.
|
Gets the aggregated logs for the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
performer_name = parsed_args["performer"]
|
performer_name = parsed_args["performer"]
|
||||||
start_time = parsed_args["starttime"]
|
start_time = parsed_args["starttime"]
|
||||||
end_time = parsed_args["endtime"]
|
end_time = parsed_args["endtime"]
|
||||||
@@ -492,7 +493,7 @@ class ExportOrgLogs(ApiResource):
|
|||||||
Exports the logs for the specified organization.
|
Exports the logs for the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
start_time = parsed_args["starttime"]
|
start_time = parsed_args["starttime"]
|
||||||
end_time = parsed_args["endtime"]
|
end_time = parsed_args["endtime"]
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,12 @@ class OrganizationQuotaList(ApiResource):
|
|||||||
@nickname("listOrganizationQuota")
|
@nickname("listOrganizationQuota")
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
orgperm = OrganizationMemberPermission(orgname)
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
if not orgperm.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not orgperm.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -201,7 +206,12 @@ class OrganizationQuota(ApiResource):
|
|||||||
@nickname("getOrganizationQuota")
|
@nickname("getOrganizationQuota")
|
||||||
def get(self, orgname, quota_id):
|
def get(self, orgname, quota_id):
|
||||||
orgperm = OrganizationMemberPermission(orgname)
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
if not orgperm.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not orgperm.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
quota = get_quota(orgname, quota_id)
|
quota = get_quota(orgname, quota_id)
|
||||||
@@ -277,7 +287,7 @@ class OrganizationQuotaLimitList(ApiResource):
|
|||||||
@nickname("listOrganizationQuotaLimit")
|
@nickname("listOrganizationQuotaLimit")
|
||||||
def get(self, orgname, quota_id):
|
def get(self, orgname, quota_id):
|
||||||
orgperm = OrganizationMemberPermission(orgname)
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
if not orgperm.can() and not allow_if_any_superuser():
|
if not orgperm.can() and not (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
quota = get_quota(orgname, quota_id)
|
quota = get_quota(orgname, quota_id)
|
||||||
@@ -347,7 +357,7 @@ class OrganizationQuotaLimit(ApiResource):
|
|||||||
@nickname("getOrganizationQuotaLimit")
|
@nickname("getOrganizationQuotaLimit")
|
||||||
def get(self, orgname, quota_id, limit_id):
|
def get(self, orgname, quota_id, limit_id):
|
||||||
orgperm = OrganizationMemberPermission(orgname)
|
orgperm = OrganizationMemberPermission(orgname)
|
||||||
if not orgperm.can() and not allow_if_any_superuser():
|
if not orgperm.can() and not (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
quota = get_quota(orgname, quota_id)
|
quota = get_quota(orgname, quota_id)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from endpoints.api import (
|
|||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
internal_only,
|
internal_only,
|
||||||
log_action,
|
log_action,
|
||||||
nickname,
|
nickname,
|
||||||
@@ -88,11 +89,14 @@ def team_view(orgname, team):
|
|||||||
def org_view(o, teams):
|
def org_view(o, teams):
|
||||||
is_admin = AdministerOrganizationPermission(o.username).can()
|
is_admin = AdministerOrganizationPermission(o.username).can()
|
||||||
is_member = OrganizationMemberPermission(o.username).can()
|
is_member = OrganizationMemberPermission(o.username).can()
|
||||||
is_any_superuser = allow_if_any_superuser()
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
can_view_as_superuser = allow_if_global_readonly_superuser() or (
|
||||||
|
features.SUPERUSERS_FULL_ACCESS and allow_if_superuser()
|
||||||
|
)
|
||||||
|
|
||||||
view = {
|
view = {
|
||||||
"name": o.username,
|
"name": o.username,
|
||||||
"email": o.email if is_admin or is_any_superuser else "",
|
"email": o.email if is_admin or can_view_as_superuser else "",
|
||||||
"avatar": avatar.get_data_for_user(o),
|
"avatar": avatar.get_data_for_user(o),
|
||||||
"is_admin": is_admin,
|
"is_admin": is_admin,
|
||||||
"is_member": is_member,
|
"is_member": is_member,
|
||||||
@@ -258,7 +262,12 @@ class Organization(ApiResource):
|
|||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
teams = None
|
teams = None
|
||||||
if OrganizationMemberPermission(orgname).can() or allow_if_any_superuser():
|
# Global readonly superusers can always view teams, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
OrganizationMemberPermission(orgname).can()
|
||||||
|
or allow_if_global_readonly_superuser()
|
||||||
|
or (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
has_syncing = features.TEAM_SYNCING and bool(authentication.federated_service)
|
has_syncing = features.TEAM_SYNCING and bool(authentication.federated_service)
|
||||||
teams = model.team.get_teams_within_org(org, has_syncing)
|
teams = model.team.get_teams_within_org(org, has_syncing)
|
||||||
|
|
||||||
@@ -272,7 +281,7 @@ class Organization(ApiResource):
|
|||||||
Change the details for the specified organization.
|
Change the details for the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -339,7 +348,7 @@ class Organization(ApiResource):
|
|||||||
Deletes the specified organization.
|
Deletes the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -436,7 +445,12 @@ class OrganizationCollaboratorList(ApiResource):
|
|||||||
List outside collaborators of the specified organization.
|
List outside collaborators of the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not permission.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -484,7 +498,7 @@ class OrganizationMemberList(ApiResource):
|
|||||||
List the human members of the specified organization.
|
List the human members of the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -545,7 +559,7 @@ class OrganizationMember(ApiResource):
|
|||||||
Retrieves the details of a member of the organization.
|
Retrieves the details of a member of the organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
# Lookup the user.
|
# Lookup the user.
|
||||||
member = model.user.get_user(membername)
|
member = model.user.get_user(membername)
|
||||||
if not member:
|
if not member:
|
||||||
@@ -595,7 +609,7 @@ class OrganizationMember(ApiResource):
|
|||||||
it from all teams in the organization.
|
it from all teams in the organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
# Lookup the user.
|
# Lookup the user.
|
||||||
user = model.user.get_nonrobot_user(membername)
|
user = model.user.get_nonrobot_user(membername)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -706,7 +720,7 @@ class OrganizationApplications(ApiResource):
|
|||||||
List the applications for the specified organization.
|
List the applications for the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -725,7 +739,7 @@ class OrganizationApplications(ApiResource):
|
|||||||
Creates a new application under this organization.
|
Creates a new application under this organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -796,7 +810,7 @@ class OrganizationApplicationResource(ApiResource):
|
|||||||
Retrieves the application with the specified client_id under the specified organization.
|
Retrieves the application with the specified client_id under the specified organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -818,7 +832,7 @@ class OrganizationApplicationResource(ApiResource):
|
|||||||
Updates an application under this organization.
|
Updates an application under this organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -852,7 +866,7 @@ class OrganizationApplicationResource(ApiResource):
|
|||||||
Deletes the application under this organization.
|
Deletes the application under this organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -887,7 +901,7 @@ class OrganizationApplicationResetClientSecret(ApiResource):
|
|||||||
Resets the client secret of the application.
|
Resets the client secret of the application.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -944,7 +958,12 @@ class OrganizationProxyCacheConfig(ApiResource):
|
|||||||
Retrieves the proxy cache configuration of the organization.
|
Retrieves the proxy cache configuration of the organization.
|
||||||
"""
|
"""
|
||||||
permission = OrganizationMemberPermission(orgname)
|
permission = OrganizationMemberPermission(orgname)
|
||||||
if not permission.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not permission.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -961,7 +980,7 @@ class OrganizationProxyCacheConfig(ApiResource):
|
|||||||
Creates proxy cache configuration for the organization.
|
Creates proxy cache configuration for the organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -998,7 +1017,7 @@ class OrganizationProxyCacheConfig(ApiResource):
|
|||||||
Delete proxy cache configuration for the organization.
|
Delete proxy cache configuration for the organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1041,7 +1060,7 @@ class ProxyCacheConfigValidation(ApiResource):
|
|||||||
@validate_json_request("NewProxyCacheConfig")
|
@validate_json_request("NewProxyCacheConfig")
|
||||||
def post(self, orgname):
|
def post(self, orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from endpoints.api import (
|
|||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
log_action,
|
log_action,
|
||||||
nickname,
|
nickname,
|
||||||
path_param,
|
path_param,
|
||||||
@@ -74,7 +75,12 @@ class OrgAutoPrunePolicies(ApiResource):
|
|||||||
Lists the auto-prune policies for the organization
|
Lists the auto-prune policies for the organization
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not permission.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
policies = model.autoprune.get_namespace_autoprune_policies_by_orgname(orgname)
|
policies = model.autoprune.get_namespace_autoprune_policies_by_orgname(orgname)
|
||||||
@@ -89,7 +95,7 @@ class OrgAutoPrunePolicies(ApiResource):
|
|||||||
Creates an auto-prune policy for the organization
|
Creates an auto-prune policy for the organization
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
app_data = request.get_json()
|
app_data = request.get_json()
|
||||||
@@ -178,7 +184,12 @@ class OrgAutoPrunePolicy(ApiResource):
|
|||||||
Fetches the auto-prune policy for the organization
|
Fetches the auto-prune policy for the organization
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not permission.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
policy = model.autoprune.get_namespace_autoprune_policy(orgname, policy_uuid)
|
policy = model.autoprune.get_namespace_autoprune_policy(orgname, policy_uuid)
|
||||||
@@ -195,7 +206,7 @@ class OrgAutoPrunePolicy(ApiResource):
|
|||||||
Updates the auto-prune policy for the organization
|
Updates the auto-prune policy for the organization
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
app_data = request.get_json()
|
app_data = request.get_json()
|
||||||
@@ -250,7 +261,7 @@ class OrgAutoPrunePolicy(ApiResource):
|
|||||||
Deletes the auto-prune policy for the organization
|
Deletes the auto-prune policy for the organization
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -312,7 +323,12 @@ class RepositoryAutoPrunePolicies(RepositoryParamResource):
|
|||||||
Lists the auto-prune policies for the repository
|
Lists the auto-prune policies for the repository
|
||||||
"""
|
"""
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if not permission.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not permission.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
if registry_model.lookup_repository(namespace, repository) is None:
|
if registry_model.lookup_repository(namespace, repository) is None:
|
||||||
@@ -332,7 +348,7 @@ class RepositoryAutoPrunePolicies(RepositoryParamResource):
|
|||||||
Creates an auto-prune policy for the repository
|
Creates an auto-prune policy for the repository
|
||||||
"""
|
"""
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
if registry_model.lookup_repository(namespace, repository) is None:
|
if registry_model.lookup_repository(namespace, repository) is None:
|
||||||
@@ -428,7 +444,12 @@ class RepositoryAutoPrunePolicy(RepositoryParamResource):
|
|||||||
Fetches the auto-prune policy for the repository
|
Fetches the auto-prune policy for the repository
|
||||||
"""
|
"""
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if not permission.can() and not allow_if_any_superuser():
|
# Global readonly superusers can always view, regular superusers need FULL_ACCESS
|
||||||
|
if (
|
||||||
|
not permission.can()
|
||||||
|
and not allow_if_global_readonly_superuser()
|
||||||
|
and not (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
policy = model.autoprune.get_repository_autoprune_policy_by_uuid(repository, policy_uuid)
|
policy = model.autoprune.get_repository_autoprune_policy_by_uuid(repository, policy_uuid)
|
||||||
@@ -445,7 +466,7 @@ class RepositoryAutoPrunePolicy(RepositoryParamResource):
|
|||||||
Updates the auto-prune policy for the repository
|
Updates the auto-prune policy for the repository
|
||||||
"""
|
"""
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
app_data = request.get_json()
|
app_data = request.get_json()
|
||||||
@@ -504,7 +525,7 @@ class RepositoryAutoPrunePolicy(RepositoryParamResource):
|
|||||||
Deletes the auto-prune policy for the repository
|
Deletes the auto-prune policy for the repository
|
||||||
"""
|
"""
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if not permission.can() and not allow_if_superuser():
|
if not permission.can() and not allow_if_superuser_with_full_access():
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Manage default permissions added to repositories.
|
|||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
|
import features
|
||||||
from app import avatar
|
from app import avatar
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
@@ -14,6 +15,7 @@ from endpoints.api import (
|
|||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
log_action,
|
log_action,
|
||||||
nickname,
|
nickname,
|
||||||
path_param,
|
path_param,
|
||||||
@@ -147,7 +149,7 @@ class PermissionPrototypeList(ApiResource):
|
|||||||
List the existing prototypes for this organization.
|
List the existing prototypes for this organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -173,7 +175,7 @@ class PermissionPrototypeList(ApiResource):
|
|||||||
Create a new permission prototype.
|
Create a new permission prototype.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -264,7 +266,7 @@ class PermissionPrototype(ApiResource):
|
|||||||
Delete an existing permission prototype.
|
Delete an existing permission prototype.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
@@ -288,7 +290,7 @@ class PermissionPrototype(ApiResource):
|
|||||||
Update the role of an existing permission prototype.
|
Update the role of an existing permission prototype.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
try:
|
try:
|
||||||
org = model.organization.get_organization(orgname)
|
org = model.organization.get_organization(orgname)
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from endpoints.api import (
|
|||||||
ApiResource,
|
ApiResource,
|
||||||
RepositoryParamResource,
|
RepositoryParamResource,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
format_date,
|
format_date,
|
||||||
log_action,
|
log_action,
|
||||||
nickname,
|
nickname,
|
||||||
@@ -69,6 +70,10 @@ def check_allowed_private_repos(namespace):
|
|||||||
|
|
||||||
If so, raises a ExceedsLicenseException.
|
If so, raises a ExceedsLicenseException.
|
||||||
"""
|
"""
|
||||||
|
# Superusers with full access bypass license limits
|
||||||
|
if allow_if_superuser_with_full_access():
|
||||||
|
return
|
||||||
|
|
||||||
# Not enabled if billing is disabled.
|
# Not enabled if billing is disabled.
|
||||||
if not features.BILLING:
|
if not features.BILLING:
|
||||||
return
|
return
|
||||||
@@ -142,7 +147,7 @@ class RepositoryList(ApiResource):
|
|||||||
|
|
||||||
permission = CreateRepositoryPermission(namespace_name)
|
permission = CreateRepositoryPermission(namespace_name)
|
||||||
|
|
||||||
if (permission.can() or allow_if_superuser()) and not (
|
if (permission.can() or allow_if_superuser_with_full_access()) and not (
|
||||||
features.RESTRICTED_USERS
|
features.RESTRICTED_USERS
|
||||||
and usermanager.is_restricted_user(owner.username)
|
and usermanager.is_restricted_user(owner.username)
|
||||||
and owner.username == namespace_name
|
and owner.username == namespace_name
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from data.database import Repository as RepositoryTable
|
|||||||
from data.database import RepositoryState
|
from data.database import RepositoryState
|
||||||
from data.registry_model import registry_model
|
from data.registry_model import registry_model
|
||||||
from data.registry_model.datatypes import RepositoryReference
|
from data.registry_model.datatypes import RepositoryReference
|
||||||
from endpoints.api import allow_if_any_superuser
|
from endpoints.api import allow_if_global_readonly_superuser, allow_if_superuser
|
||||||
from endpoints.api.repository_models_interface import (
|
from endpoints.api.repository_models_interface import (
|
||||||
ApplicationRepository,
|
ApplicationRepository,
|
||||||
Channel,
|
Channel,
|
||||||
@@ -129,6 +129,7 @@ class PreOCIModel(RepositoryDataInterface):
|
|||||||
# Also note the +1 on the limit, as paginate_query uses the extra result to determine whether
|
# Also note the +1 on the limit, as paginate_query uses the extra result to determine whether
|
||||||
# there is a next page.
|
# there is a next page.
|
||||||
start_id = model.modelutil.pagination_start(page_token)
|
start_id = model.modelutil.pagination_start(page_token)
|
||||||
|
# Global readonly superusers can always see all repos, regular superusers need FULL_ACCESS
|
||||||
repo_query = model.repository.get_visible_repositories(
|
repo_query = model.repository.get_visible_repositories(
|
||||||
username=username,
|
username=username,
|
||||||
include_public=public,
|
include_public=public,
|
||||||
@@ -136,7 +137,10 @@ class PreOCIModel(RepositoryDataInterface):
|
|||||||
limit=REPOS_PER_PAGE + 1,
|
limit=REPOS_PER_PAGE + 1,
|
||||||
kind_filter=repo_kind,
|
kind_filter=repo_kind,
|
||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
is_superuser=allow_if_any_superuser(),
|
is_superuser=(
|
||||||
|
allow_if_global_readonly_superuser()
|
||||||
|
or (features.SUPERUSERS_FULL_ACCESS and allow_if_superuser())
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
repos, next_page_token = model.modelutil.paginate_query(
|
repos, next_page_token = model.modelutil.paginate_query(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import logging
|
|||||||
|
|
||||||
from flask import abort, request
|
from flask import abort, request
|
||||||
|
|
||||||
|
import features
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth.permissions import (
|
from auth.permissions import (
|
||||||
@@ -27,6 +28,7 @@ from endpoints.api import (
|
|||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
log_action,
|
log_action,
|
||||||
max_json_size,
|
max_json_size,
|
||||||
nickname,
|
nickname,
|
||||||
@@ -221,7 +223,12 @@ class OrgRobotList(ApiResource):
|
|||||||
List the organization's robots.
|
List the organization's robots.
|
||||||
"""
|
"""
|
||||||
permission = OrganizationMemberPermission(orgname)
|
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 = (
|
include_token = (
|
||||||
AdministerOrganizationPermission(orgname).can()
|
AdministerOrganizationPermission(orgname).can()
|
||||||
or allow_if_global_readonly_superuser()
|
or allow_if_global_readonly_superuser()
|
||||||
@@ -261,7 +268,12 @@ class OrgRobot(ApiResource):
|
|||||||
Returns the organization's robot with the specified name.
|
Returns the organization's robot with the specified name.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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)
|
robot = model.get_org_robot(robot_shortname, orgname)
|
||||||
return robot.to_dict(include_metadata=True, include_token=True)
|
return robot.to_dict(include_metadata=True, include_token=True)
|
||||||
|
|
||||||
@@ -276,7 +288,7 @@ class OrgRobot(ApiResource):
|
|||||||
Create a new robot in the organization.
|
Create a new robot in the organization.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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 {}
|
create_data = request.get_json(silent=True) or {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -308,7 +320,7 @@ class OrgRobot(ApiResource):
|
|||||||
Delete an existing organization robot.
|
Delete an existing organization robot.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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_username = format_robot_username(orgname, robot_shortname)
|
||||||
if not model.robot_has_mirror(robot_username):
|
if not model.robot_has_mirror(robot_username):
|
||||||
model.delete_robot(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.
|
Returns the list of repository permissions for the org's robot.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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)
|
robot = model.get_org_robot(robot_shortname, orgname)
|
||||||
permissions = model.list_robot_permissions(robot.name)
|
permissions = model.list_robot_permissions(robot.name)
|
||||||
|
|
||||||
@@ -408,7 +425,7 @@ class RegenerateOrgRobot(ApiResource):
|
|||||||
Regenerates the token for an organization robot.
|
Regenerates the token for an organization robot.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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)
|
robot = model.regenerate_org_robot_token(robot_shortname, orgname)
|
||||||
log_action("regenerate_robot_token", orgname, {"robot": robot_shortname})
|
log_action("regenerate_robot_token", orgname, {"robot": robot_shortname})
|
||||||
return robot.to_dict(include_token=True)
|
return robot.to_dict(include_token=True)
|
||||||
@@ -431,7 +448,12 @@ class OrgRobotFederation(ApiResource):
|
|||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
def get(self, orgname, robot_shortname):
|
def get(self, orgname, robot_shortname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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_username = format_robot_username(orgname, robot_shortname)
|
||||||
robot = lookup_robot(robot_username)
|
robot = lookup_robot(robot_username)
|
||||||
return get_robot_federation_config(robot)
|
return get_robot_federation_config(robot)
|
||||||
@@ -442,7 +464,7 @@ class OrgRobotFederation(ApiResource):
|
|||||||
@validate_json_request("CreateRobotFederation", optional=False)
|
@validate_json_request("CreateRobotFederation", optional=False)
|
||||||
def post(self, orgname, robot_shortname):
|
def post(self, orgname, robot_shortname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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)
|
fed_config = self._parse_federation_config(request)
|
||||||
|
|
||||||
robot_username = format_robot_username(orgname, robot_shortname)
|
robot_username = format_robot_username(orgname, robot_shortname)
|
||||||
@@ -460,7 +482,7 @@ class OrgRobotFederation(ApiResource):
|
|||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
def delete(self, orgname, robot_shortname):
|
def delete(self, orgname, robot_shortname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
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_username = format_robot_username(orgname, robot_shortname)
|
||||||
robot = lookup_robot(robot_username)
|
robot = lookup_robot(robot_username)
|
||||||
delete_robot_federation_config(robot)
|
delete_robot_federation_config(robot)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from endpoints.api import (
|
|||||||
Unauthorized,
|
Unauthorized,
|
||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
format_date,
|
format_date,
|
||||||
internal_only,
|
internal_only,
|
||||||
log_action,
|
log_action,
|
||||||
@@ -1250,9 +1251,11 @@ class SuperUserAppTokens(ApiResource):
|
|||||||
"""
|
"""
|
||||||
Returns a list of all app specific tokens in the system.
|
Returns a list of all app specific tokens in the system.
|
||||||
|
|
||||||
This endpoint is for system-wide auditing by superusers and global read-only superusers.
|
This endpoint is for system-wide auditing by global read-only superusers and
|
||||||
|
full access superusers only. Regular superusers without full access are denied.
|
||||||
"""
|
"""
|
||||||
if allow_if_any_superuser():
|
# Global readonly superusers can always audit, regular superusers need FULL_ACCESS
|
||||||
|
if allow_if_global_readonly_superuser() or allow_if_superuser_with_full_access():
|
||||||
expiring = parsed_args["expiring"]
|
expiring = parsed_args["expiring"]
|
||||||
|
|
||||||
if expiring:
|
if expiring:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from endpoints.api import (
|
|||||||
allow_if_any_superuser,
|
allow_if_any_superuser,
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
format_date,
|
format_date,
|
||||||
internal_only,
|
internal_only,
|
||||||
log_action,
|
log_action,
|
||||||
@@ -212,7 +213,7 @@ class OrganizationTeam(ApiResource):
|
|||||||
Update the org-wide permission for the specified team.
|
Update the org-wide permission for the specified team.
|
||||||
"""
|
"""
|
||||||
edit_permission = AdministerOrganizationPermission(orgname)
|
edit_permission = AdministerOrganizationPermission(orgname)
|
||||||
if edit_permission.can() or allow_if_superuser():
|
if edit_permission.can() or allow_if_superuser_with_full_access():
|
||||||
team = None
|
team = None
|
||||||
|
|
||||||
details = request.get_json()
|
details = request.get_json()
|
||||||
@@ -262,7 +263,7 @@ class OrganizationTeam(ApiResource):
|
|||||||
Delete the specified team.
|
Delete the specified team.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
model.team.remove_team(orgname, teamname, get_authenticated_user().username)
|
model.team.remove_team(orgname, teamname, get_authenticated_user().username)
|
||||||
log_action("org_delete_team", orgname, {"team": teamname})
|
log_action("org_delete_team", orgname, {"team": teamname})
|
||||||
return "", 204
|
return "", 204
|
||||||
@@ -360,7 +361,7 @@ class TeamMemberList(ApiResource):
|
|||||||
view_permission = ViewTeamPermission(orgname, teamname)
|
view_permission = ViewTeamPermission(orgname, teamname)
|
||||||
edit_permission = AdministerOrganizationPermission(orgname)
|
edit_permission = AdministerOrganizationPermission(orgname)
|
||||||
|
|
||||||
if view_permission.can() or allow_if_any_superuser():
|
if view_permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
team = None
|
team = None
|
||||||
try:
|
try:
|
||||||
team = model.team.get_organization_team(orgname, teamname)
|
team = model.team.get_organization_team(orgname, teamname)
|
||||||
@@ -423,7 +424,7 @@ class TeamMember(ApiResource):
|
|||||||
Adds or invites a member to an existing team.
|
Adds or invites a member to an existing team.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
team = None
|
team = None
|
||||||
user = None
|
user = None
|
||||||
|
|
||||||
@@ -465,7 +466,7 @@ class TeamMember(ApiResource):
|
|||||||
If the user is merely invited to join the team, then the invite is removed instead.
|
If the user is merely invited to join the team, then the invite is removed instead.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
# Remote the user from the team.
|
# Remote the user from the team.
|
||||||
invoking_user = get_authenticated_user().username
|
invoking_user = get_authenticated_user().username
|
||||||
|
|
||||||
@@ -515,7 +516,7 @@ class InviteTeamMember(ApiResource):
|
|||||||
Invites an email address to an existing team.
|
Invites an email address to an existing team.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
team = None
|
team = None
|
||||||
|
|
||||||
# Find the team.
|
# Find the team.
|
||||||
@@ -543,7 +544,7 @@ class InviteTeamMember(ApiResource):
|
|||||||
Delete an invite of an email address to join a team.
|
Delete an invite of an email address to join a team.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_superuser():
|
if permission.can() or allow_if_superuser_with_full_access():
|
||||||
team = None
|
team = None
|
||||||
|
|
||||||
# Find the team.
|
# Find the team.
|
||||||
@@ -580,7 +581,7 @@ class TeamPermissions(ApiResource):
|
|||||||
Returns the list of repository permissions for the org's team.
|
Returns the list of repository permissions for the org's team.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can() or allow_if_any_superuser():
|
if permission.can() or (features.SUPERUSERS_FULL_ACCESS and allow_if_any_superuser()):
|
||||||
try:
|
try:
|
||||||
team = model.team.get_organization_team(orgname, teamname)
|
team = model.team.get_organization_team(orgname, teamname)
|
||||||
except model.InvalidTeamException:
|
except model.InvalidTeamException:
|
||||||
|
|||||||
@@ -337,16 +337,10 @@ def test_superuser_endpoint_sees_all_tokens(app):
|
|||||||
assert "token_code" not in token
|
assert "token_code" not in token
|
||||||
|
|
||||||
# Test global readonly superuser
|
# Test global readonly superuser
|
||||||
# Mock global readonly superuser by mocking the permission classes
|
# Mock global readonly superuser by mocking the permission functions
|
||||||
with patch("endpoints.api.SuperUserPermission") as mock_super_perm, patch(
|
with patch(
|
||||||
"endpoints.api.GlobalReadOnlySuperUserPermission"
|
"endpoints.api.superuser.allow_if_global_readonly_superuser", return_value=True
|
||||||
) as mock_global_ro_perm, patch(
|
), patch("endpoints.api.superuser.allow_if_superuser_with_full_access", return_value=False):
|
||||||
"endpoints.api.superuser.allow_if_any_superuser", return_value=True
|
|
||||||
):
|
|
||||||
# Not a regular superuser, but is a global readonly superuser
|
|
||||||
mock_super_perm.return_value.can.return_value = False
|
|
||||||
mock_global_ro_perm.return_value.can.return_value = True
|
|
||||||
|
|
||||||
with client_with_identity("reader", app) as cl:
|
with client_with_identity("reader", app) as cl:
|
||||||
# On /v1/superuser/apptokens, global readonly superuser should see all tokens
|
# On /v1/superuser/apptokens, global readonly superuser should see all tokens
|
||||||
resp = conduct_api_call(cl, SuperUserAppTokens, "GET", None, None, 200).json
|
resp = conduct_api_call(cl, SuperUserAppTokens, "GET", None, None, 200).json
|
||||||
@@ -363,6 +357,17 @@ def test_superuser_endpoint_sees_all_tokens(app):
|
|||||||
reader_token.delete_instance()
|
reader_token.delete_instance()
|
||||||
|
|
||||||
|
|
||||||
|
def test_superuser_endpoint_requires_full_access(app):
|
||||||
|
"""Test that regular superusers without FULL_ACCESS get 403 on /v1/superuser/apptokens"""
|
||||||
|
# Mock a regular superuser (not global readonly) without FULL_ACCESS
|
||||||
|
with patch(
|
||||||
|
"endpoints.api.superuser.allow_if_global_readonly_superuser", return_value=False
|
||||||
|
), patch("endpoints.api.superuser.allow_if_superuser_with_full_access", return_value=False):
|
||||||
|
with client_with_identity("devtable", app) as cl:
|
||||||
|
# Regular superuser without FULL_ACCESS should get 403
|
||||||
|
conduct_api_call(cl, SuperUserAppTokens, "GET", None, None, 403)
|
||||||
|
|
||||||
|
|
||||||
def test_superuser_endpoint_expiring_tokens(app):
|
def test_superuser_endpoint_expiring_tokens(app):
|
||||||
"""Test expiring token filtering on /v1/superuser/apptokens"""
|
"""Test expiring token filtering on /v1/superuser/apptokens"""
|
||||||
devtable_user = model.user.get_user("devtable")
|
devtable_user = model.user.get_user("devtable")
|
||||||
|
|||||||
254
endpoints/api/test/test_superuser_full_access.py
Normal file
254
endpoints/api/test/test_superuser_full_access.py
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
"""
|
||||||
|
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.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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
features.import_features(
|
||||||
|
{
|
||||||
|
"FEATURE_SUPER_USERS": True,
|
||||||
|
"FEATURE_SUPERUSERS_FULL_ACCESS": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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
|
||||||
@@ -23,6 +23,7 @@ from endpoints.api import (
|
|||||||
RepositoryParamResource,
|
RepositoryParamResource,
|
||||||
abort,
|
abort,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
api,
|
api,
|
||||||
disallow_for_app_repositories,
|
disallow_for_app_repositories,
|
||||||
disallow_for_non_normal_repositories,
|
disallow_for_non_normal_repositories,
|
||||||
@@ -281,7 +282,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||||||
raise InvalidRequest("Trigger config is not sufficient for activation.")
|
raise InvalidRequest("Trigger config is not sufficient for activation.")
|
||||||
|
|
||||||
user_permission = UserAdminPermission(trigger.connected_user.username)
|
user_permission = UserAdminPermission(trigger.connected_user.username)
|
||||||
if user_permission.can() or allow_if_superuser():
|
if user_permission.can() or allow_if_superuser_with_full_access():
|
||||||
# Update the pull robot (if any).
|
# Update the pull robot (if any).
|
||||||
pull_robot_name = request.get_json().get("pull_robot", None)
|
pull_robot_name = request.get_json().get("pull_robot", None)
|
||||||
if pull_robot_name:
|
if pull_robot_name:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from data.registry_model.datatypes import RepositoryReference
|
|||||||
from endpoints.api import (
|
from endpoints.api import (
|
||||||
allow_if_global_readonly_superuser,
|
allow_if_global_readonly_superuser,
|
||||||
allow_if_superuser,
|
allow_if_superuser,
|
||||||
|
allow_if_superuser_with_full_access,
|
||||||
log_action,
|
log_action,
|
||||||
)
|
)
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
@@ -264,7 +265,10 @@ def _authorize_or_downscope_request(scope_param, has_valid_auth_context):
|
|||||||
# Lookup the repository. If it exists, make sure the entity has modify
|
# Lookup the repository. If it exists, make sure the entity has modify
|
||||||
# permission. Otherwise, make sure the entity has create permission.
|
# permission. Otherwise, make sure the entity has create permission.
|
||||||
if repository_ref:
|
if repository_ref:
|
||||||
if ModifyRepositoryPermission(namespace, reponame).can() or allow_if_superuser():
|
if (
|
||||||
|
ModifyRepositoryPermission(namespace, reponame).can()
|
||||||
|
or allow_if_superuser_with_full_access()
|
||||||
|
):
|
||||||
if repository_ref is not None and repository_ref.kind != "image":
|
if repository_ref is not None and repository_ref.kind != "image":
|
||||||
raise Unsupported(message=invalid_repo_message)
|
raise Unsupported(message=invalid_repo_message)
|
||||||
|
|
||||||
@@ -383,7 +387,10 @@ def _authorize_or_downscope_request(scope_param, has_valid_auth_context):
|
|||||||
|
|
||||||
if "*" in requested_actions:
|
if "*" in requested_actions:
|
||||||
# Grant * user is admin
|
# Grant * user is admin
|
||||||
if AdministerRepositoryPermission(namespace, reponame).can() or allow_if_superuser():
|
if (
|
||||||
|
AdministerRepositoryPermission(namespace, reponame).can()
|
||||||
|
or allow_if_superuser_with_full_access()
|
||||||
|
):
|
||||||
if repository_ref is not None and repository_ref.kind != "image":
|
if repository_ref is not None and repository_ref.kind != "image":
|
||||||
raise Unsupported(message=invalid_repo_message)
|
raise Unsupported(message=invalid_repo_message)
|
||||||
|
|
||||||
|
|||||||
@@ -2412,18 +2412,18 @@ class TestChangeRepoVisibility(ApiTestCase):
|
|||||||
# Change the subscription of the namespace.
|
# Change the subscription of the namespace.
|
||||||
self.putJsonResponse(UserPlan, data=dict(plan="personal-2018"))
|
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(
|
self.postJsonResponse(
|
||||||
RepositoryVisibility,
|
RepositoryVisibility,
|
||||||
params=dict(repository=self.SIMPLE_REPO),
|
params=dict(repository=self.SIMPLE_REPO),
|
||||||
data=dict(visibility="private"),
|
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))
|
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):
|
def test_changevisibility(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
@@ -2452,6 +2452,76 @@ class TestChangeRepoVisibility(ApiTestCase):
|
|||||||
|
|
||||||
self.assertEqual(False, json["is_public"])
|
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):
|
class TestDeleteRepository(ApiTestCase):
|
||||||
SIMPLE_REPO = ADMIN_ACCESS_USER + "/simple"
|
SIMPLE_REPO = ADMIN_ACCESS_USER + "/simple"
|
||||||
|
|||||||
Reference in New Issue
Block a user