@@ -22,9 +22,10 @@ module PlaceOS::Api
2222 before_action :can_write , only: [:create , :update , :destroy , :remove ]
2323
2424 before_action :check_admin , except: [:index , :create , :update , :destroy , :state , :show , :ping , :start , :stop ]
25- # start/stop are gated by the support-subsystem Operate check in-action
26- # (see `start`/`stop`), so they're intentionally absent here.
27- before_action :check_support , only: [:state , :show , :ping , :show_error ]
25+ # start/stop are gated by the support-subsystem Operate check in-action;
26+ # show is gated by `check_show_permissions` (support-subsystem Read on the
27+ # module's zones) — so both are intentionally absent here.
28+ before_action :check_support , only: [:state , :ping , :show_error ]
2829
2930 # ##############################################################################################
3031
@@ -63,29 +64,56 @@ module PlaceOS::Api
6364 zones.uniq
6465 end
6566
67+ # Reading a single module: admin/support JWT and the legacy org_zone path
68+ # bypass; otherwise a "support" subsystem user needs Read on the module's
69+ # zones. A module with no derivable zones stays admin/support only.
70+ @[AC ::Route ::Filter (:before_action , only: [:show ])]
71+ def check_show_permissions
72+ ensure_support_access!(zones_for_module(current_module), ::PlaceOS ::Model ::Permissions ::Read )
73+ end
74+
6675 @[AC ::Route ::Filter (:before_action , only: [:index ])]
6776 def check_view_permissions (
6877 @[AC ::Param ::Info (description: " only return modules running in this system (query params are ignored if this is provided)" , example: " sys-1234" )]
6978 control_system_id : String ? = nil ,
7079 )
80+ # admin/support JWT see everything (no scoping)
7181 return if user_support?
7282
73- # find the org zone
74- authority = current_authority.as(::PlaceOS ::Model ::Authority )
75- @org_zone_id = org_zone_id = authority.config[" org_zone" ]?.try(& .as_s?)
76- raise Error ::Forbidden .new unless org_zone_id
83+ org_zone = support_org_zone_id
7784
7885 if control_system_id
7986 zones = ::PlaceOS ::Model ::ControlSystem .find!(control_system_id).zones
80- raise Error ::Forbidden .new unless zones.includes?(org_zone_id)
81- raise Error ::Forbidden .new unless check_access(current_user.groups, zones).can_manage?
82- else
83- access = check_access(current_user.groups, [org_zone_id])
84- raise Error ::Forbidden .new unless access.can_manage?
87+ # "support" subsystem: Read on the system's zones.
88+ return if support_subsystem_grants?(zones, ::PlaceOS ::Model ::Permissions ::Read )
89+ # legacy org_zone path: the system must include the org_zone and the
90+ # user must be able to manage it.
91+ return if org_zone && zones.includes?(org_zone) && check_access(current_user.groups, zones).can_manage?
92+ raise Error ::Forbidden .new
93+ end
94+
95+ # Whole-list view: instead of a flat allow/deny, resolve the zone set the
96+ # caller may see and scope the result set to it (see `index`).
97+ # legacy org_zone manager -> systems containing the org_zone.
98+ if org_zone && check_access(current_user.groups, [org_zone]).can_manage?
99+ @module_scope_zones = [org_zone]
100+ return
101+ end
102+
103+ # "support" subsystem -> systems whose zones the caller can reach.
104+ accessible = support_accessible_zone_ids
105+ unless accessible.empty?
106+ @module_scope_zones = accessible
107+ return
85108 end
109+
110+ raise Error ::Forbidden .new
86111 end
87112
88- getter org_zone_id : String ? = nil
113+ # Zone ids used to scope the module list for non-admin/non-support callers
114+ # (legacy org_zone, or the caller's reachable "support" zones). Left nil for
115+ # admin/support callers, who see everything.
116+ getter module_scope_zones : Array (String )? = nil
89117
90118 # Response helpers
91119 # ##############################################################################################
@@ -147,18 +175,19 @@ module PlaceOS::Api
147175
148176 # TODO:: we can remove this once there is a tenant_id field on modules
149177 # which will make this much simpler to filter
150- if filter_zone_id = org_zone_id
151- # we only want to show modules in use by systems that include this zone
178+ if scope_zones = module_scope_zones
179+ # we only want to show modules in use by systems within these zones
152180 no_logic = true
153181
154182 # find all the non-logic modules that this user can access
155- # 1. grabs all the module ids in the systems of the provided org zone
156- # 2. select distinct modules ids which are not logic modules (99)
183+ # 1. grab all the module ids in systems whose zones overlap scope_zones
184+ # 2. select distinct module ids which are not logic modules (99)
185+ placeholders = (1 ..scope_zones.size).map { |i | " $#{ i } " }.join(" , " )
157186 sql_query = %[
158187 WITH matching_rows AS (
159188 SELECT unnest(modules) AS module_id
160189 FROM sys
161- WHERE $1 = ANY(zones)
190+ WHERE zones && ARRAY[#{placeholders}]::text[]
162191 )
163192
164193 SELECT ARRAY_AGG(DISTINCT m.module_id)
@@ -168,7 +197,7 @@ module PlaceOS::Api
168197 ]
169198
170199 module_ids = PgORM ::Database .connection do |conn |
171- conn.query_one(sql_query, args: [filter_zone_id] , & .read(Array (String )?))
200+ conn.query_one(sql_query, args: scope_zones.map( & .as( PgORM :: Value )) , & .read(Array (String )?))
172201 end || [] of String
173202
174203 query.must({
0 commit comments