@@ -15,9 +15,21 @@ module PlaceOS::Api
1515 # GroupUser entry wins). A Manage grant on a group implicitly covers
1616 # that group's descendants.
1717 module Utils::GroupPermissions
18+ # Per-request memo caches. Controllers are per-request `ActionController::Base`
19+ # instances, so these are request-scoped and cannot serve stale authz (gates
20+ # run in before_actions, before any mutation). Never cache across requests.
21+ @group_memberships_cache : Hash (String , Hash (UUID , ::PlaceOS ::Model ::Permissions ))?
22+ @subsystem_perms_cache : Hash (String , Hash (String , ::PlaceOS ::Model ::Permissions ))?
23+
1824 # Effective per-group Permissions for the user, keyed by group id.
1925 # Groups where the user has no transitive membership are absent.
26+ # Memoised per request (called repeatedly across gates/reads).
2027 def group_memberships (user : ::PlaceOS ::Model ::User ) : Hash (UUID , ::PlaceOS ::Model ::Permissions )
28+ cache = (@group_memberships_cache ||= {} of String => Hash (UUID , ::PlaceOS ::Model ::Permissions ))
29+ cache[user.id.as(String )] ||= compute_group_memberships(user)
30+ end
31+
32+ private def compute_group_memberships (user : ::PlaceOS ::Model ::User ) : Hash (UUID , ::PlaceOS ::Model ::Permissions )
2133 user_id = user.id.as(String )
2234
2335 # Direct GroupUser rows, scoped to the user's authority.
@@ -186,16 +198,25 @@ module PlaceOS::Api
186198 # `subsystems`) include `required`, or Manage (a superset). The model
187199 # resolver already ANDs the user's group perms with the GroupZone
188200 # grants, so a non-zero result means both sides agree.
201+ # Memoised `{zone_id => Permissions}` map for the current user within
202+ # `subsystem`, resolved once per request (then reused by every gate /
203+ # scoping query for that subsystem).
204+ def subsystem_zone_permissions (subsystem : String ) : Hash (String , ::PlaceOS ::Model ::Permissions )
205+ cache = (@subsystem_perms_cache ||= {} of String => Hash (String , ::PlaceOS ::Model ::Permissions ))
206+ cache[subsystem] ||= ::PlaceOS ::Model ::Group .resolve_subsystem_permissions(
207+ current_user.authority_id.as(String ), subsystem, current_user.id.as(String ),
208+ )
209+ end
210+
189211 def subsystem_grants_on_zones ?(
190212 subsystems : Array (String ),
191213 zones : Array (String ),
192214 required : ::PlaceOS ::Model ::Permissions ,
193215 ) : Bool
194216 return false if zones.empty?
195- authority_id = current_user.authority_id.as(String )
196- user_id = current_user.id.as(String )
197217 subsystems.any? do |subsystem |
198- perms = ::PlaceOS ::Model ::Group .effective_permissions(authority_id, subsystem, user_id, zones)
218+ perms_map = subsystem_zone_permissions(subsystem)
219+ perms = zones.reduce(::PlaceOS ::Model ::Permissions ::None ) { |acc , z | acc | (perms_map[z]? || ::PlaceOS ::Model ::Permissions ::None ) }
199220 perms.manage? || (perms & required) != ::PlaceOS ::Model ::Permissions ::None
200221 end
201222 end
@@ -268,11 +289,11 @@ module PlaceOS::Api
268289 end
269290
270291 # Zone ids reachable by the current user via the "support" subsystem.
271- # Useful for scoping index/list queries.
292+ # Useful for scoping index/list queries. Served from the per-request memo.
272293 def support_accessible_zone_ids : Array (String )
273- authority_id = current_user.authority_id.as( String )
274- user_id = current_user.id.as( String )
275- :: PlaceOS :: Model :: Group .accessible_zone_ids(authority_id, SUPPORT_SUBSYSTEM , user_id)
294+ subsystem_zone_permissions( SUPPORT_SUBSYSTEM ).compact_map do | zid , perms |
295+ zid if perms != :: PlaceOS :: Model :: Permissions :: None
296+ end
276297 end
277298 end
278299end
0 commit comments