recurso 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b76502a3d8dcabffaece8ca8e0c1e180119a6f2abafe095e41aacf6bd6d916ce
4
- data.tar.gz: c9f3e6241010e82e24cefa1529a23b5fbc8567e98f3820cbd9aab83de2b89816
3
+ metadata.gz: a46321da0a26c046aa4fbfebe14061760fe414a8f82c50ec16bb018ce99b39e1
4
+ data.tar.gz: e8663c77f4aebdcee48b8582f218cfb3adacbe92a65cc0c8cc33552cce08d8a4
5
5
  SHA512:
6
- metadata.gz: 73c28571558f188c57a9e17f972d64d8a4abd757ef77981a045125c13742d739a8fea35dd99993c5db319b8f24ac04318630779c4f8efe1835c4d115f9f06b2c
7
- data.tar.gz: 4d10393228eed9cba9aba3e5d34143343600d7ca55eff105d4c6e6b074adbf18d84298ea119e48e84ccad8fb7487db8fc3a26417f51eb999022de80445718185
6
+ metadata.gz: '0842b588a11c858e9f08099d6333447ee16863f11e20a68319f8f33e0644a796e5366e87fdb15f88fae1133fd93a854831d440d80316cdff4b539d539c7a37c6'
7
+ data.tar.gz: 20aa209fa69e2a2037f84f16c34a35e70492b15ea301c1c7aca541332aa08dd5584a225f6f33b2358ac481c71d75292f5b102d23609cd8878e9009ed6ab87c89
data/README.md CHANGED
@@ -81,6 +81,15 @@ The second, which resources a user can access of a given relation:
81
81
  # ^^ which squad can I view within this team?
82
82
  ```
83
83
 
84
+ Calling `resources_with_permission` directly on an identity will return all records in the database which that identity has access to.
85
+
86
+ _(NB that these classes must be whitelisted via the `global_relations` config parameter, documented below)_
87
+
88
+ ```ruby
89
+ @user.resources_with_permission(:teams)
90
+ # ^^ which teams in the the database can I view?
91
+ ```
92
+
84
93
  #### 4. Authorizing resources
85
94
 
86
95
  A common use case for these permissions is to authorize actions on a certain controller action.
@@ -272,6 +281,22 @@ Recurso::Config.instance.identity_foreign_key = lambda do |model|
272
281
  end
273
282
  ```
274
283
 
284
+ **global_relations**:
285
+
286
+ The names of relations you're interested in accessing globally. This expects an array of symbols, which will be constantized in order to find a class name
287
+
288
+ ```ruby
289
+ Recurso::Config.instance.global_relations = [:organizations, :teams, :squads]
290
+ ```
291
+
292
+ This enables querying the `Recurso::Global` for all models of that class.
293
+
294
+ e.g.:
295
+ ```ruby
296
+ # return all teams in the database which this user can view
297
+ user.resources_with_permission(:teams)
298
+ ```
299
+
275
300
  ## Development
276
301
 
277
302
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -11,6 +11,15 @@ module Recurso
11
11
  (resource&.policy_class || Recurso::NilClassPolicy).new(self, resource)
12
12
  end
13
13
 
14
+ def resources_with_permission(relation_name, action: :view, all_columns: true, include_actions: [])
15
+ policy(Recurso::Global.instance).resources_with_permission(
16
+ relation_name,
17
+ action: action,
18
+ all_columns: all_columns,
19
+ include_actions: include_actions
20
+ )
21
+ end
22
+
14
23
  def policy_class
15
24
  Recurso::BasePolicy
16
25
  end
@@ -12,7 +12,8 @@ module Recurso
12
12
  :default_level,
13
13
  :identity_foreign_key,
14
14
  :permission_class_name,
15
- :default_permission_class_name
15
+ :default_permission_class_name,
16
+ :global_relations
16
17
 
17
18
  def initialize
18
19
  @levels_for_action = {
@@ -34,6 +35,8 @@ module Recurso
34
35
  @identity_foreign_key = DEFAULT_IDENTITY_FOREIGN_KEY
35
36
 
36
37
  @permission_class_name = DEFAULT_PERMISSION_CLASS_NAME
38
+
39
+ @global_relations = []
37
40
  end
38
41
 
39
42
  def model_specific(value, model)
@@ -0,0 +1,21 @@
1
+ module Recurso
2
+ class Global
3
+ # no-op methods to support being a Recurso::Resource
4
+ define_singleton_method :has_one, ->(*args) {}
5
+ define_singleton_method :has_many, ->(*args) {}
6
+ define_singleton_method :belongs_to, ->(*args) {}
7
+
8
+ include Singleton
9
+ include Recurso::Resource
10
+
11
+ def permission_policy
12
+ OpenStruct.new(policy_type: :open)
13
+ end
14
+
15
+ def method_missing(method)
16
+ super unless Recurso::Config.instance.global_relations.include?(method)
17
+
18
+ method.to_s.classify.constantize.all
19
+ end
20
+ end
21
+ end
@@ -7,23 +7,14 @@ module Recurso
7
7
  Recurso::Queries::Single.new(identity, resource, action).permission?
8
8
  end
9
9
 
10
- def resources_with_permission(relation_name, all_columns: true, include_actions: [:modify, :administer])
11
- include_actions.reduce(resource_query_for(relation_name, :view, all_columns: all_columns)) do |resources, action|
12
- resources
13
- .joins("LEFT OUTER JOIN(#{resource_query_for(relation_name, action).to_sql}) AS #{action} ON #{action}.id = #{relation_name}.id")
14
- .select("#{action}.id IS NOT NULL AS can_#{action}")
15
- end
16
- end
17
-
18
- private
19
-
20
- def resource_query_for(relation_name, action, all_columns: false)
10
+ def resources_with_permission(relation_name, action: :view, all_columns: true, include_actions: [:modify, :administer])
21
11
  Recurso::Queries::Relation.new(
22
12
  identity,
23
13
  resource,
24
14
  relation_name,
25
15
  all_columns: all_columns,
26
- action: action
16
+ action: action,
17
+ include_actions: include_actions,
27
18
  ).resources
28
19
  end
29
20
  end
@@ -2,32 +2,56 @@ module Recurso
2
2
  module Queries
3
3
  class Relation
4
4
 
5
- def initialize(identity, resource, relation_name, all_columns: true, action: :view)
5
+ def initialize(identity, resource, relation_name, all_columns: true, action: :view, include_actions: [])
6
6
  @identity = identity
7
7
  @resource = resource
8
8
  @relation = resource.send(relation_name)
9
9
  @all_columns = all_columns
10
+ @include_actions = (include_actions + Array(action)).uniq
10
11
  @action = action
11
12
  end
12
13
 
13
14
  def resources
14
- @resources ||= join_permissions
15
- .select("#{@relation.table_name}.#{@all_columns ? :* : :id}")
16
- .where(coalesce(@action))
17
- .distinct
15
+ @relation
16
+ .select("#{@relation.table_name}.#{@all_columns ? :"*" : :id}")
17
+ .select(*@include_actions.map { |action| "included.can_#{action}"})
18
+ .joins("
19
+ INNER JOIN (#{included_permissions}) included
20
+ ON #{@relation.table_name}.id = included.id
21
+ AND included.can_#{@action} = 1
22
+ ")
18
23
  end
19
24
 
20
25
  private
21
26
 
22
- def join_permissions
23
- @relation.relevant_associations.reduce(@relation) do |result, assoc|
24
- result.joins(through_join_for(assoc)).joins("
25
- LEFT OUTER JOIN #{permission_class.table_name} #{assoc.name}_permissions
26
- ON #{assoc.name}_permissions.resource_type = '#{assoc.class_name}'
27
- AND #{assoc.name}_permissions.resource_id = #{resource_id_for(assoc)}
28
- AND #{assoc.name}_permissions.#{identity_foreign_key} = #{@identity.id.to_i}
29
- ")
30
- end
27
+ def included_permissions
28
+ "
29
+ SELECT cascaded.id, #{included_permission_select}
30
+ FROM (#{cascaded_permissions}) cascaded
31
+ GROUP BY cascaded.id
32
+ "
33
+ end
34
+
35
+ def cascaded_permissions
36
+ @relation.relevant_associations.map do |assoc|
37
+ @relation
38
+ .select(:id, :level)
39
+ .joins(through_join_for(assoc))
40
+ .joins("
41
+ INNER join #{permission_class.table_name}
42
+ ON #{permission_class.table_name}.resource_type = '#{assoc.class_name}'
43
+ AND #{permission_class.table_name}.resource_id = #{resource_id_for(assoc)}
44
+ AND #{permission_class.table_name}.#{identity_foreign_key} = #{@identity.id.to_i}
45
+ ").to_sql
46
+ end.join(" UNION ")
47
+ end
48
+
49
+ def included_permission_select
50
+ @include_actions.map do |action|
51
+ level_values = @resource.relevant_levels_for(action).map { |level| permission_class.levels[level] }.join(',')
52
+
53
+ "cascaded.level IN (#{level_values}) as can_#{action}"
54
+ end.join(", ")
31
55
  end
32
56
 
33
57
  def resource_id_for(assoc)
@@ -42,18 +66,6 @@ module Recurso
42
66
  "
43
67
  end
44
68
 
45
- def coalesce(action)
46
- "coalesce(#{level_columns}) IN (#{level_values(action)})"
47
- end
48
-
49
- def level_columns
50
- @relation.relevant_associations.map { |assoc| "#{assoc.name}_permissions.level" }.join(',')
51
- end
52
-
53
- def level_values(action)
54
- @resource.relevant_levels_for(action).map { |level| permission_class.levels[level] }.join(',')
55
- end
56
-
57
69
  def permission_class
58
70
  @permission_class ||= Recurso::Config.instance.permission_class_name_for(@identity.class).constantize
59
71
  end
@@ -1,3 +1,3 @@
1
1
  module Recurso
2
- VERSION = "0.5.3"
2
+ VERSION = "0.6.1"
3
3
  end
data/lib/recurso.rb CHANGED
@@ -4,6 +4,7 @@ require 'recurso/concerns/identity'
4
4
  require 'recurso/concerns/resource'
5
5
  require 'recurso/concerns/permission'
6
6
  require 'recurso/concerns/controller'
7
+ require 'recurso/models/global'
7
8
  require 'recurso/models/permission'
8
9
  require 'recurso/models/permission_policy'
9
10
  require 'recurso/policies/base_policy'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recurso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Kiesel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-14 00:00:00.000000000 Z
11
+ date: 2021-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -152,6 +152,7 @@ files:
152
152
  - lib/recurso/concerns/permission.rb
153
153
  - lib/recurso/concerns/resource.rb
154
154
  - lib/recurso/config.rb
155
+ - lib/recurso/models/global.rb
155
156
  - lib/recurso/models/permission.rb
156
157
  - lib/recurso/models/permission_policy.rb
157
158
  - lib/recurso/policies/base_policy.rb