Add organization isolation for personal snippets
What does this MR do and why?
Add organization isolation for personal snippets to ensure users can only access snippets within their organization while maintaining backward compatibility for project snippets.
Why this change is needed:
- Personal snippets need to be isolated by organization for data security
- Users should only see and access snippets from their own organization
- Addresses organization isolation requirements for snippets
What changed:
-
SnippetsFinder: Add
by_organizationfilter that restricts personal snippets to user's organization while allowing all project snippets -
PersonalSnippetPolicy: Enforce organization-based access control with exceptions for authors and admins (using
can_admin_all_resources?) -
Snippets::CreateService: Auto-assign
organization_idfrom current user when not explicitly provided -
Snippets::UpdateService: Prevent
organization_idchanges after creation to maintain data integrity -
Snippet model: Add
in_organizationandfor_user_organizationscopes
Key behaviors:
-
✅ Personal snippets are filtered by organization_id -
✅ Project snippets bypass organization filtering (remain accessible) -
✅ Authors can always access their own snippets -
✅ Admins can access all snippets when in admin mode -
✅ Anonymous users can still access public snippets -
✅ Organization_id is immutable after snippet creation
Database constraint ensures exactly one of project_id or organization_id is set (enforced by existing CHECK constraint).
References
- Closes #570399 (Your work - snippets - Organization isolation)
- Related to #565221 (Organization isolation for snippets)
- Related to #460827 (closed) (Organization ID constraint)
Screenshots or screen recordings
No UI changes - this is a backend implementation for organization isolation.
How to set up and validate locally
- Setup test data in Rails console:
# Create two organizations
org1 = Organizations::Organization.create!(name: 'Organization 1', path: 'org1')
org2 = Organizations::Organization.create!(name: 'Organization 2', path: 'org2')
# Create users with proper attributes
user1 = FactoryBot.create(:user, organization: org1)
user2 = FactoryBot.create(:user, organization: org2)
# Create personal snippets
snippet1 = PersonalSnippet.create!(
title: 'Org1 Snippet',
content: 'Content from org1',
file_name: 'test.rb',
author: user1,
organization: org1,
visibility_level: Snippet::PUBLIC
)
snippet2 = PersonalSnippet.create!(
title: 'Org2 Snippet',
content: 'Content from org2',
file_name: 'test.rb',
author: user2,
organization: org2,
visibility_level: Snippet::PUBLIC
)
- Test organization isolation:
# User1 should only see org1 snippets
SnippetsFinder.new(user1, only_personal: true).execute.pluck(:title)
# => ["Org1 Snippet"]
# User2 should only see org2 snippets
SnippetsFinder.new(user2, only_personal: true).execute.pluck(:title)
# => ["Org2 Snippet"]
- Test policy enforcement:
# User1 cannot access org2 snippets
Ability.allowed?(user1, :read_snippet, snippet2)
# => false
# But can access their own org snippets
Ability.allowed?(user1, :read_snippet, snippet1)
# => true
- Test organization_id immutability:
# Try to update organization_id
Snippets::UpdateService.new(
project: nil,
current_user: user1,
params: { organization_id: org2.id }
).execute(snippet1)
snippet1.reload.organization_id
# => Still org1.id (unchanged)
- Test admin access (requires admin user):
admin = User.find_by(admin: true)
# Admin can see all snippets with all_available flag
SnippetsFinder.new(admin, all_available: true).execute.pluck(:title)
# => ["Org1 Snippet", "Org2 Snippet"]
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.
Related to #570399
Edited by Chen Zhang