recurso 0.5.3 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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