-
Notifications
You must be signed in to change notification settings - Fork 165
refactor(BA-3687): consolidate admin_repository into repository for image domain #7860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the image domain architecture by consolidating the AdminImageRepository into the ImageRepository, moving ownership validation logic from the repository layer to the service layer. This creates a cleaner separation where repositories handle pure data access and services handle business logic including authorization.
Key Changes
- Removed the
AdminImageRepositoryclass entirely, eliminating code duplication - Refactored
ImageServiceto perform role-based branching, with SUPERADMIN users bypassing ownership checks and regular users validated viavalidate_image_ownership - Simplified repository methods by removing
user_idparameters from data modification methods while keepingvalidate_image_ownershipexposed
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ai/backend/manager/repositories/image/admin_repository.py | Entire file deleted - consolidated functionality into ImageRepository |
| src/ai/backend/manager/repositories/image/repository.py | Simplified methods to pure data access; removed user_id parameters; exposed validate_image_ownership; changed return type of untag_image_from_registry from Optional[ImageData] to ImageData |
| src/ai/backend/manager/repositories/image/repositories.py | Removed admin_repository field and its instantiation |
| src/ai/backend/manager/repositories/image/db_source/db_source.py | Removed user_id parameters from mark_user_image_deleted, mark_image_deleted_by_id, and removed combined validation+operation methods |
| src/ai/backend/manager/services/image/service.py | Added role-based branching logic; SUPERADMIN users skip validation; regular users call validate_image_ownership before operations; added ImageRef import for type annotations |
| src/ai/backend/manager/services/processors.py | Updated ImageService instantiation to remove admin_repository parameter |
| tests/unit/manager/services/image/conftest.py | Removed mock_admin_image_repository fixture and its usage in image_service fixture |
| tests/unit/manager/services/image/test_image_service.py | Updated all SUPERADMIN tests to use mock_image_repository instead of mock_admin_image_repository; updated forbidden access tests to mock validate_image_ownership and resolve_image calls |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if action.client_role != UserRole.SUPERADMIN: | ||
| image_data = await self._image_repository.resolve_image(identifiers) | ||
| await self._image_repository.validate_image_ownership(image_data.id, action.user_id) | ||
| data = await self._image_repository.soft_delete_image(identifiers) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation makes three database queries for non-SUPERADMIN users: one in resolve_image to get the image data, one in validate_image_ownership to validate ownership, and a third in soft_delete_image which internally calls _resolve_image again. Consider optimizing to reduce to two queries by removing the explicit resolve_image call since validate_image_ownership already fetches the image and validates ownership, or by having soft_delete_image accept an optional pre-resolved image.
| data = await self._image_repository.soft_delete_image(identifiers) | |
| data = await self._image_repository.soft_delete_image_by_id(image_data.id) | |
| else: | |
| data = await self._image_repository.soft_delete_image(identifiers) |
| if action.client_role != UserRole.SUPERADMIN: | ||
| await self._image_repository.validate_image_ownership( | ||
| action.image_id, action.user_id, load_aliases=True | ||
| ) | ||
| image_data = await self._image_repository.delete_image_with_aliases(action.image_id) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The result of validate_image_ownership (which returns ImageData) is being discarded, then the image is fetched again by delete_image_with_aliases. This makes two database queries when only one is needed. Consider using the ImageData returned from validate_image_ownership if the delete operation doesn't need to refetch the latest state, or have the delete method accept pre-fetched image data to avoid the redundant query.
| if action.client_role != UserRole.SUPERADMIN: | ||
| await self._image_repository.validate_image_ownership( | ||
| action.image_id, action.user_id, load_aliases=True | ||
| ) | ||
| image_data = await self._image_repository.untag_image_from_registry(action.image_id) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The result of validate_image_ownership (which returns ImageData) is being discarded, then the image is fetched again by untag_image_from_registry. This makes two database queries when only one is needed. Consider using the ImageData returned from validate_image_ownership if the untag operation doesn't need to refetch the latest state, or have the untag method accept pre-fetched image data to avoid the redundant query.
|
I think we need to add some service-level test cases to verify behavior changes based on |
|
Could we also add test cases that cover exception scenarios, not just the success cases? |
|
It doesn’t make sense to raise a ForgetImageForbiddenError when the user lacks the required permissions. |
|
The conventions used in However, this would significantly increase the scope of this PR. |
src/ai/backend/manager/repositories/image/db_source/db_source.py
Outdated
Show resolved
Hide resolved
- Remove AdminImageRepository class entirely - Move ownership validation from Repository to Service layer - Repository becomes pure data access layer (read/write only) - Service layer handles SUPERADMIN vs regular user branching - SUPERADMIN bypasses ownership check, regular users validate ownership 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add tests for regular users successfully operating on images they own: - test_forget_image_as_user_success - test_forget_image_by_id_as_user_success - test_purge_image_by_id_as_user_success - test_untag_image_as_user_success These tests verify that client_role-based behavior works correctly when ownership validation passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
…tion ForgetImageForbiddenError was being raised in validate_image_ownership, but this method is used by multiple services (forget, purge, untag). Created ImageAccessForbiddenError for generic image access violations in the repository layer to avoid misleading exception names. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Change error_title to "Access to this image is forbidden." - Change operation from READ to ACCESS for clearer semantics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
…rate methods - db_source.py: Split validate_and_fetch_image_ownership() into: - validate_image_ownership() -> None (validation only) - fetch_image_by_id_with_ownership() -> ImageData (data retrieval) - repository.py: Update validate_image_ownership() to return None This follows the principle that validate_* methods should only validate (returning None or raising exceptions), while fetch_* methods should return data. Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add explanation that image ownership validation stays in repository layer because it's a simple user_id comparison (binary ownership) unlike model_serving domain which requires complex RBAC with role-based access. Co-Authored-By: Claude Opus 4.5 <[email protected]>
… service layer - Remove validation logic from ImageDBSource and ImageRepository - Rename fetch_image_by_id_with_ownership to fetch_image_by_id - Add owner_id field to ImageData for service-level validation - Add _get_owner_id() helper to ImageRow for extracting owner from labels - Add _validate_image_ownership() helper to ImageService - Update tests to use new validation pattern with replace() Co-Authored-By: Claude Opus 4.5 <[email protected]>
Follow internal convention of using UUID directly. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1d5ea7c to
fd66997
Compare

Summary
AdminImageRepositoryclass entirely from the image domainImageAccessForbiddenErrorfor ownership validation failuresowner_idfield toImageDatafor service-level validationRelated Issue
Architecture Change
Before:
After:
Changed Files
types.pyowner_id: Optional[uuid.UUID]field toImageDatarow.py_get_owner_id()helper to extract owner from labelsdb_source.py_validate_image_ownership()andvalidate_image_ownership(), renamefetch_image_by_id_with_ownership()→fetch_image_by_id()repository.pyvalidate_image_ownership(), keepfetch_image_by_id()service.py_validate_image_ownership()helper for service-level ownership checkrepositories.pyadmin_repositoryfielderrors/image.pyImageAccessForbiddenErrorfor ownership validation failuresadmin_repository.pyDesign Decisions
Validation in Service Layer
Per reviewer feedback, validation logic should not be in the repository layer. Repository is now pure data access:
owner_idField in ImageDataAdded to support service-level validation. Extracted from image labels (
ai.backend.customized-image.owner) inImageRow._get_owner_id().Security
Test plan
pants lintpassespants check(mypy) passes🤖 Generated with Claude Code