Class: Collavre::Creatives::PermissionFilter
- Inherits:
-
Object
- Object
- Collavre::Creatives::PermissionFilter
- Defined in:
- app/services/collavre/creatives/permission_filter.rb
Overview
Batch "which of these creative ids can the user read?" using the O(1) CreativeSharesCache table. Extracted from FilterPipeline so the picker's has_children presence check can apply the exact same permission posture as the browse endpoint (children_with_permission) without re-deriving it.
A user-specific cache entry is authoritative over a public share: it grants only when its own rank meets the requested threshold and otherwise suppresses the public share (so a no_access — or any below-threshold user entry — wins over a more permissive public share). Owned creatives are always readable.
Constant Summary collapse
- OWNER_RANK =
An owner always resolves to admin, the top rank, so it satisfies any min_permission threshold. This is the canonical owner rank the read-path callers (TreeBuilder, SlideViewable) inherit via #ranks_for / #readable_ids.
CreativeShare.[:admin]
Instance Method Summary collapse
-
#initialize(user:) ⇒ PermissionFilter
constructor
A new instance of PermissionFilter.
-
#ranks_for(ids) ⇒ Object
Returns { input_id => effective_permission_rank } for the ids the user has ANY relationship to, mirroring single-item PermissionChecker: owner wins (admin rank), else the user's own cache entry (INCLUDING a no_access deny, which is rank 0 and beats a public share), else the public entry.
-
#readable_ids(ids, min_permission: :read) ⇒ Object
Returns the subset of
idsthe user may access atmin_permissionor higher, as an Array.
Constructor Details
#initialize(user:) ⇒ PermissionFilter
Returns a new instance of PermissionFilter.
18 19 20 |
# File 'app/services/collavre/creatives/permission_filter.rb', line 18 def initialize(user:) @user = user end |
Instance Method Details
#ranks_for(ids) ⇒ Object
Returns { input_id => effective_permission_rank } for the ids the user has ANY relationship to, mirroring single-item PermissionChecker: owner wins (admin rank), else the user's own cache entry (INCLUDING a no_access deny, which is rank 0 and beats a public share), else the public entry. Ids with no owner/entry/public share are ABSENT from the hash (distinct from rank 0).
The rank is the raw CreativeShare.permissions integer, so callers compare
rank >= rank_for(:write) themselves. Unlike readable_ids this does NOT
apply the shell-ownership anti-leak gate — it is for tree_builder /
slide_viewable which rank creatives already inside the viewer's own tree,
exactly as PermissionChecker#allowed? would resolve them one by one.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'app/services/collavre/creatives/permission_filter.rb', line 77 def ranks_for(ids) ids = ids.to_a.filter_map { |id| Integer(id, exception: false) }.uniq return {} if ids.empty? effective_by_id = EffectiveCreativeResolution.effective_creative_ids(ids) effective_ids = effective_by_id.values.uniq owner_by_effective = Creative.where(id: effective_ids).pluck(:id, :user_id).to_h user_entries = if user CreativeSharesCache.where(creative_id: effective_ids, user_id: user.id) .pluck(:creative_id, :permission).to_h else {} end needs_public = effective_ids - user_entries.keys public_entries = if needs_public.any? CreativeSharesCache.where(creative_id: needs_public, user_id: nil) .pluck(:creative_id, :permission).to_h else {} end ranks = {} ids.each do |id| effective = effective_by_id[id] if user && owner_by_effective[effective] == user.id ranks[id] = OWNER_RANK else = user_entries[effective] || public_entries[effective] ranks[id] = CreativeShare.[] if end end ranks end |
#readable_ids(ids, min_permission: :read) ⇒ Object
Returns the subset of ids the user may access at min_permission or
higher, as an Array. min_permission: defaults to :read, so the no-arg
form is unchanged; pass :write (etc.) to share the one batch filter with
write-posture sites instead of re-deriving the rank comparison.
Each id is resolved to its effective (origin) creative via the SAME logic PermissionChecker uses, so a linked creative yields identical permission whether checked one-by-one or filtered in a batch.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'app/services/collavre/creatives/permission_filter.rb', line 30 def readable_ids(ids, min_permission: :read) # Normalize to Integer so the final select (and owned-shell lookup) compare # against pluck-derived integer ids rather than string inputs; keeps the # canonical batch filter robust to param-sourced ids. Non-numeric ids drop. ids = ids.to_a.filter_map { |id| Integer(id, exception: false) }.uniq return [] if ids.empty? min_rank = rank_for() effective_by_id = EffectiveCreativeResolution.effective_creative_ids(ids) readable_effective = readable_effective_ids(effective_by_id.values.uniq, min_rank) # A linked creative borrows its ORIGIN's permission, but the shell row # itself represents a specific PLACEMENT of that link inside a tree. # Returning a shell solely because its origin is readable would leak other # users' private shell placements into search/filter results — a batch can # be fed foreign shells (e.g. FilterPipeline#resolve_ancestors pulls every # Creative.where(origin_id: ...) regardless of owner). So a shell is gated # on the viewer being able to SEE its placement at the requested rank, in # ADDITION to origin readability: either the viewer owns the shell, or the # shell sits in a subtree shared with the viewer AT >= min_permission (a # propagated CreativeSharesCache entry on the shell row itself — e.g. a # public help doc's linked child). The placement is checked at min_rank # (not always :read) because higher-privilege callers share this batch — # children_with_permission(user, :admin), used by DestroyService's # recursive delete, must not reach a shell the viewer only has read on. A # shell in a foreign PRIVATE tree (no viewer entry) stays hidden. Non-shell # ids keep origin-readability-only behaviour. visible_shells = visible_shell_ids(effective_by_id, min_rank) ids.select do |id| next false unless readable_effective.include?(effective_by_id[id]) effective_by_id[id] == id || visible_shells.include?(id) end end |