1
0
mirror of https://github.com/quay/quay.git synced 2026-01-26 06:21:37 +03:00
Files
quay/data/model/__init__.py
jbpratt 2e3dd39ae8 feat(data): add tag immutability enforcement layer (PROJQUAY-10158) (#4822)
This commit implements the core enforcement layer for tag immutability,
which prevents immutable tags from being deleted, overwritten, or
permanently removed from the time machine.

Changes:
- Add ImmutableTagException class with tag_name, operation, and
  repository_id fields for detailed error reporting
- Enforce immutability in delete_tag() - raises exception for immutable tags
- Enforce immutability in retarget_tag() - prevents overwriting immutable
  tags, respects raise_on_error parameter
- Enforce immutability in remove_tag_from_timemachine() - blocks permanent
  deletion for both alive and expired immutable tags
- Add is_tag_immutable() - returns True/False/None for tag lookup
- Add set_tag_immutable() - updates immutability with optimistic locking

The immutable column, indexes, and log entry kind were previously added
in migration 5b8dc452f5c3. This commit adds the enforcement logic and
utility functions that use those database structures.

Signed-off-by: Brady Pratt <bpratt@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 08:37:05 -06:00

267 lines
4.9 KiB
Python

from data.database import db, db_transaction
class DataModelException(Exception):
pass
class InvalidLabelKeyException(DataModelException):
pass
class InvalidMediaTypeException(DataModelException):
pass
class BlobDoesNotExist(DataModelException):
pass
class InvalidBlobUpload(DataModelException):
pass
class InvalidEmailAddressException(DataModelException):
pass
class InvalidOrganizationException(DataModelException):
pass
class InvalidProxyCacheConfigException(DataModelException):
pass
class InvalidPasswordException(DataModelException):
pass
class InvalidRobotException(DataModelException):
pass
class DeactivatedRobotOwnerException(InvalidRobotException):
pass
class InvalidRobotCredentialException(InvalidRobotException):
pass
class InvalidRobotOwnerException(InvalidRobotException):
pass
class InvalidUsernameException(DataModelException):
pass
class RepositoryDoesNotExist(DataModelException):
pass
class InvalidRepositoryBuildException(DataModelException):
pass
class InvalidBuildTriggerException(DataModelException):
pass
class InvalidTokenException(DataModelException):
pass
class InvalidNotificationException(DataModelException):
pass
class InvalidImageException(DataModelException):
pass
class UserAlreadyInTeam(DataModelException):
pass
class InvalidTeamException(DataModelException):
pass
class InvalidTeamMemberException(DataModelException):
pass
class InvalidManifestException(DataModelException):
pass
class ManifestDoesNotExist(DataModelException):
pass
class ServiceKeyDoesNotExist(DataModelException):
pass
class ServiceKeyAlreadyApproved(DataModelException):
pass
class ServiceNameInvalid(DataModelException):
pass
class TagDoesNotExist(DataModelException):
pass
class TagAlreadyCreatedException(DataModelException):
pass
class StaleTagException(DataModelException):
pass
class ImmutableTagException(DataModelException):
"""Exception raised when an operation is attempted on an immutable tag."""
def __init__(self, tag_name: str, operation: str, repository_id: int | None = None) -> None:
self.tag_name = tag_name
self.operation = operation
self.repository_id = repository_id
msg = f"Cannot {operation} immutable tag '{tag_name}'"
super().__init__(msg)
class InvalidSystemQuotaConfig(Exception):
pass
class QuotaExceededException(DataModelException):
pass
class InvalidNamespaceQuota(DataModelException):
pass
class InvalidNamespaceQuotaLimit(DataModelException):
pass
class InvalidNamespaceQuotaType(DataModelException):
pass
class UnsupportedQuotaSize(DataModelException):
pass
class OrgSubscriptionBindingAlreadyExists(DataModelException):
pass
class NamespaceAutoPrunePolicyAlreadyExists(DataModelException):
pass
class NamespaceAutoPrunePolicyDoesNotExist(DataModelException):
pass
class InvalidNamespaceAutoPrunePolicy(DataModelException):
pass
class InvalidNamespaceAutoPruneMethod(DataModelException):
pass
class InvalidNamespaceException(DataModelException):
pass
class RepositoryAutoPrunePolicyAlreadyExists(DataModelException):
pass
class RepositoryAutoPrunePolicyDoesNotExist(DataModelException):
pass
class InvalidRepositoryAutoPrunePolicy(DataModelException):
pass
class InvalidRepositoryAutoPruneMethod(DataModelException):
pass
class InvalidRepositoryException(DataModelException):
pass
class PushesDisabledException(Exception):
pass
class TooManyLoginAttemptsException(Exception):
def __init__(self, message, retry_after):
super(TooManyLoginAttemptsException, self).__init__(message)
self.retry_after = retry_after
class Config(object):
def __init__(self):
self.app_config = None
self.store = None
self.image_cleanup_callbacks = []
self.repo_cleanup_callbacks = []
def register_image_cleanup_callback(self, callback):
self.image_cleanup_callbacks.append(callback)
return lambda: self.image_cleanup_callbacks.remove(callback)
def register_repo_cleanup_callback(self, callback):
self.repo_cleanup_callbacks.append(callback)
return lambda: self.repo_cleanup_callbacks.remove(callback)
config = Config()
# There MUST NOT be any circular dependencies between these subsections. If there are fix it by
# moving the minimal number of things to _basequery
from data.model import (
appspecifictoken,
autoprune,
blob,
build,
entitlements,
gc,
label,
log,
message,
modelutil,
namespacequota,
notification,
oauth,
organization,
organization_skus,
permission,
proxy_cache,
pull_statistics,
release,
repo_mirror,
repository,
repositoryactioncount,
service_keys,
storage,
team,
token,
user,
)