fix(backend): enforce private-by-default visibility and pool-op ACL
Listings returned every row regardless of ownership: GET /files, /tags, /pools and /categories exposed other users' private items (while the single-item GET correctly returned 403), and the pool file operations (GET /pools/:id, /pools/:id/files, add/remove/reorder) skipped ACL entirely, so any authenticated user could read and rewrite anyone's private pool. - List queries now filter to rows the caller may see (public, owned, or granted can_view) via a shared SQL condition; admins bypass. The viewer identity is taken from the request context by the service and passed to the repository in the list params. - Tag/Category/Pool single-item Get now enforce CanView (File already did). - Pool Get/ListFiles require pool view; AddFiles/RemoveFiles/Reorder require pool edit. Adds regression tests for private-by-default listing (hidden / public / granted / admin) and for pool operations rejecting a non-owner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,14 +49,27 @@ func NewCategoryService(
|
||||
// CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// List returns a paginated, optionally filtered list of categories.
|
||||
// List returns a paginated list of categories the caller may see.
|
||||
func (s *CategoryService) List(ctx context.Context, params port.OffsetParams) (*domain.CategoryOffsetPage, error) {
|
||||
params.ViewerID, params.ViewerIsAdmin, _ = domain.UserFromContext(ctx)
|
||||
return s.categories.List(ctx, params)
|
||||
}
|
||||
|
||||
// Get returns a category by ID.
|
||||
// Get returns a category by ID, enforcing view ACL.
|
||||
func (s *CategoryService) Get(ctx context.Context, id uuid.UUID) (*domain.Category, error) {
|
||||
return s.categories.GetByID(ctx, id)
|
||||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||||
c, err := s.categories.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok, err := s.acl.CanView(ctx, userID, isAdmin, c.CreatorID, c.IsPublic, categoryObjectTypeID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, domain.ErrForbidden
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Create inserts a new category record.
|
||||
@@ -158,7 +171,9 @@ func (s *CategoryService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
// Tags in category
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListTags returns a paginated list of tags belonging to this category.
|
||||
// ListTags returns a paginated list of tags in this category that the caller
|
||||
// may see.
|
||||
func (s *CategoryService) ListTags(ctx context.Context, categoryID uuid.UUID, params port.OffsetParams) (*domain.TagOffsetPage, error) {
|
||||
params.ViewerID, params.ViewerIsAdmin, _ = domain.UserFromContext(ctx)
|
||||
return s.tags.ListByCategory(ctx, categoryID, params)
|
||||
}
|
||||
Reference in New Issue
Block a user