Class: ForestAdminAgent::Services::Permissions
- Inherits:
-
Object
- Object
- ForestAdminAgent::Services::Permissions
- Includes:
- Http::Exceptions, Utils, ForestAdminDatasourceToolkit::Components::Query::ConditionTree, ForestAdminDatasourceToolkit::Exceptions
- Defined in:
- lib/forest_admin_agent/services/permissions.rb
Instance Attribute Summary collapse
-
#cache ⇒ Object
readonly
Returns the value of attribute cache.
-
#caller ⇒ Object
readonly
Returns the value of attribute caller.
-
#forest_api ⇒ Object
readonly
Returns the value of attribute forest_api.
Class Method Summary collapse
Instance Method Summary collapse
-
#assert_can_read_query_fields(collection, condition_tree: nil, sort: nil, search: nil, search_extended: false) ⇒ Object
Refused rather than redacted: dropping a condition widens the result set and dropping a sort clause silently reorders it, while both leak the value they touch anyway — a
starts_withfilter answers one guess per request without returning a column of its own. - #assert_can_read_usages(root_collection_name, usages) ⇒ Object
- #can?(action, collection, allow_fetch: false) ⇒ Boolean
- #can_chart?(parameters) ⇒ Boolean
- #can_execute_query_segment?(collection, query, connection_name) ⇒ Boolean
- #can_smart_action?(request, collection, filter, allow_fetch: true) ⇒ Boolean
- #get_scope(collection) ⇒ Object
- #get_segments(collection, force_fetch: false) ⇒ Object
- #get_team(rendering_id) ⇒ Object
- #get_user_data(user_id) ⇒ Object
-
#initialize(caller) ⇒ Permissions
constructor
A new instance of Permissions.
-
#read_permissions(root_collection_name, collection_names) ⇒ Object
root_collection_nameis pinned to readable and never looked up:browsealready gates a listing,reada get, and the signed hash a chart. -
#redact_projection(collection, projection, named_by_caller:) ⇒ Object
An unnamed field is dropped rather than refused: the default expansion covers every column of every to-one relation, so refusing would turn an ordinary listing into a 403 for a caller that asked for nothing.
Constructor Details
#initialize(caller) ⇒ Permissions
Returns a new instance of Permissions.
14 15 16 17 18 19 20 21 22 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 14 def initialize(caller) @caller = caller @forest_api = ForestAdminAgent::Http::ForestAdminApiRequester.new @cache = FileCache.new( 'permissions', Facades::Container.config_from_cache[:cache_dir].to_s, Facades::Container.config_from_cache[:permission_expiration] ) end |
Instance Attribute Details
#cache ⇒ Object (readonly)
Returns the value of attribute cache.
12 13 14 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 12 def cache @cache end |
#caller ⇒ Object (readonly)
Returns the value of attribute caller.
12 13 14 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 12 def caller @caller end |
#forest_api ⇒ Object (readonly)
Returns the value of attribute forest_api.
12 13 14 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 12 def forest_api @forest_api end |
Class Method Details
.invalidate_cache(id_cache = nil) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 24 def self.invalidate_cache(id_cache = nil) cache = FileCache.new( 'permissions', Facades::Container.config_from_cache[:cache_dir].to_s, Facades::Container.config_from_cache[:permission_expiration] ) cache.clear if id_cache.nil? cache.delete(id_cache) unless cache.get(id_cache).nil? ForestAdminAgent::Facades::Container.logger.log('Info', "Invalidating #{id_cache} cache..") end |
Instance Method Details
#assert_can_read_query_fields(collection, condition_tree: nil, sort: nil, search: nil, search_extended: false) ⇒ Object
Refused rather than redacted: dropping a condition widens the result set and dropping a sort
clause silently reorders it, while both leak the value they touch anyway — a starts_with
filter answers one guess per request without returning a column of its own.
The route passes the components it will actually apply, already parsed. A component it drops is simply one it does not pass — a count carries no sort, a chart neither sort nor search — so nothing has to be declared alongside the query and then kept in step with it. What is authorised here is what the filter carries, not a second parse of the same parameters.
111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 111 def assert_can_read_query_fields(collection, condition_tree: nil, sort: nil, search: nil, search_extended: false) usages = [] # `projection` collects the leaf fields without touching the tree. The route applies this very # instance next, so a traversal that rebuilt a branch — as `for_each_leaf` does — would leave # the guard deciding what runs. condition_tree&.projection&.each { |path| usages << usage('filter on', collection, path) } sort&.each { |clause| usages << usage('sort on', collection, clause[:field]) } collect_search_usages(collection, search, search_extended, usages) assert_can_read_usages(collection.name, usages) end |
#assert_can_read_usages(root_collection_name, usages) ⇒ Object
125 126 127 128 129 130 131 132 133 134 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 125 def assert_can_read_usages(root_collection_name, usages) allowed = (root_collection_name, usages.flat_map { |usage| usage[:collections] }) denied = usages.find { |usage| !readable_leaves?(usage[:collections], allowed) } return unless denied raise ForbiddenError, "You cannot #{denied[:action]} '#{denied[:path]}': you are not allowed to read " \ "#{leaf_label(denied[:collections])}." end |
#can?(action, collection, allow_fetch: false) ⇒ Boolean
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 38 def can?(action, collection, allow_fetch: false) return true unless user_data = get_user_data(caller.id) collections_data = (force_fetch: allow_fetch) is_allowed = (collections_data, collection, action, user_data) unless is_allowed collections_data = (force_fetch: true) is_allowed = (collections_data, collection, action, user_data) end raise ForbiddenError, "You don't have permission to #{action} this collection." unless is_allowed is_allowed end |
#can_chart?(parameters) ⇒ Boolean
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 136 def can_chart?(parameters) attributes = sanitize_chart_parameters(parameters.deep_symbolize_keys) hash_request = "#{attributes[:type]}:#{array_hash(attributes)}" is_allowed = get_chart_data(caller.rendering_id).include?(hash_request) is_allowed ||= get_chart_data(caller.rendering_id, force_fetch: true).include?(hash_request) unless is_allowed ForestAdminAgent::Facades::Container.logger.log( 'Debug', "User #{caller.id} cannot retrieve chart on rendering #{caller.rendering_id}" ) raise ForbiddenError, "You don't have permission to access this collection." end ForestAdminAgent::Facades::Container.logger.log( 'Debug', "User #{caller.id} can retrieve chart on rendering #{caller.rendering_id}" ) is_allowed end |
#can_execute_query_segment?(collection, query, connection_name) ⇒ Boolean
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 159 def can_execute_query_segment?(collection, query, connection_name) user_data = get_user_data(caller.id) if %w[admin developer editor].include?(user_data&.dig(:permissionLevel)) ForestAdminAgent::Facades::Container.logger.log( 'Debug', "User #{caller.id} can retrieve SQL segment on rendering #{caller.rendering_id}" ) return true end = (collection, force_fetch: false) is_allowed = (, query, connection_name) unless is_allowed = (collection, force_fetch: true) is_allowed = (, query, connection_name) end unless is_allowed ForestAdminAgent::Facades::Container.logger.log( 'Debug', "User #{caller.id} cannot retrieve query segment on rendering #{caller.rendering_id}" ) raise ForbiddenError, "You don't have permission to use this query segment." end ForestAdminAgent::Facades::Container.logger.log( 'Debug', "User #{caller.id} can retrieve query segment on rendering #{caller.rendering_id}" ) is_allowed end |
#can_smart_action?(request, collection, filter, allow_fetch: true) ⇒ Boolean
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 195 def can_smart_action?(request, collection, filter, allow_fetch: true) return true unless user_data = get_user_data(caller.id) collections_data = (force_fetch: allow_fetch) action = find_action_from_endpoint(collection.name, request[:headers]['REQUEST_PATH'], request[:headers]['REQUEST_METHOD']) collection_actions = (collections_data, collection, action) smart_action_approval = SmartActionChecker.new( request[:params], collection, # The schema scope lets the checker skip select-all resolution for global actions. collection_actions[action['name'].to_sym].merge(scope: collection.schema[:actions][action['name']]&.scope), caller, user_data[:roleId], filter ) is_allowed = smart_action_approval.can_execute? ForestAdminAgent::Facades::Container.logger.log( 'Debug', "User #{user_data[:roleId]} is #{"not" unless is_allowed} allowed to perform #{action["name"]}" ) is_allowed end |
#get_scope(collection) ⇒ Object
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 223 def get_scope(collection) = get_rendering_data(caller.rendering_id) scope = [:scopes][collection.name.to_sym] return nil if scope.nil? team = get_team(caller.rendering_id) user = get_user_data(caller.id) if team.nil? || user.nil? raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Unable to resolve the caller's team or user data while computing the permission " \ "scope for '#{collection.name}'." end context_variables = ContextVariables.new(team, user) ContextVariablesInjector.inject_context_in_filter(scope, context_variables) end |
#get_segments(collection, force_fetch: false) ⇒ Object
243 244 245 246 247 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 243 def get_segments(collection, force_fetch: false) = get_rendering_data(caller.rendering_id, force_fetch: force_fetch) [:segments][collection.name.to_sym] end |
#get_team(rendering_id) ⇒ Object
264 265 266 267 268 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 264 def get_team(rendering_id) = get_rendering_data(rendering_id) [:team] end |
#get_user_data(user_id) ⇒ Object
249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 249 def get_user_data(user_id) cache.get_or_set('forest.users') do response = fetch('/liana/v4/permissions/users') users = {} response.each do |user| users[user[:id].to_s] = user end ForestAdminAgent::Facades::Container.logger.log('Debug', 'Refreshing user permissions cache') users end[user_id.to_s] end |
#read_permissions(root_collection_name, collection_names) ⇒ Object
root_collection_name is pinned to readable and never looked up: browse already gates a
listing, read a get, and the signed hash a chart.
Answered once per collection for the whole request: redact_projection and
assert_can_read_query_fields both ask on a listing, and a chart asks three times.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 61 def (root_collection_name, collection_names) to_check = collection_names.uniq.reject { |name| name == root_collection_name } allowed = { root_collection_name => true } return allowed if to_check.empty? # An absent permission system is not a denial: `can?` allows everything there, and answering # anything else would redact every relation on a deployment that granted nothing to check. # `skip_relation_read_permissions` is the operator asking for that same answer on purpose. if || ! return allowed.merge(to_check.to_h { |name| [name, true] }) end @read_permissions ||= {} missing = to_check - @read_permissions.keys @read_permissions.merge!((missing)) unless missing.empty? allowed.merge(@read_permissions.slice(*to_check)) end |
#redact_projection(collection, projection, named_by_caller:) ⇒ Object
An unnamed field is dropped rather than refused: the default expansion covers every column of every to-one relation, so refusing would turn an ordinary listing into a 403 for a caller that asked for nothing.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/forest_admin_agent/services/permissions.rb', line 84 def redact_projection(collection, projection, named_by_caller:) owners = projection.to_h do |path| [path, ForestAdminDatasourceToolkit::Utils::FieldPath.leaf_collection_names(collection, path)] end allowed = (collection.name, owners.values.flatten) readable = ->(path) { readable_leaves?(owners[path], allowed) } if named_by_caller denied = projection.reject { |path| readable.call(path) } unless denied.empty? fields = denied.map { |path| "'#{path}' from #{leaf_label(owners[path])}" } raise ForbiddenError, "You are not allowed to read #{fields.join(", ")}." end end ForestAdminDatasourceToolkit::Components::Query::Projection.new(projection.select { |path| readable.call(path) }) end |