action_policy 0.0.1 → 0.1.0

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.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +85 -0
  3. data/.travis.yml +25 -2
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +12 -3
  6. data/README.md +71 -12
  7. data/Rakefile +9 -1
  8. data/action_policy.gemspec +11 -5
  9. data/docs/.nojekyll +0 -0
  10. data/docs/CNAME +1 -0
  11. data/docs/README.md +46 -0
  12. data/docs/_sidebar.md +19 -0
  13. data/docs/aliases.md +54 -0
  14. data/docs/assets/docsify.min.js +1 -0
  15. data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
  16. data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
  17. data/docs/assets/images/cache.png +0 -0
  18. data/docs/assets/images/cache.svg +70 -0
  19. data/docs/assets/images/layer.png +0 -0
  20. data/docs/assets/images/layer.svg +92 -0
  21. data/docs/assets/prism-ruby.min.js +1 -0
  22. data/docs/assets/styles.css +317 -0
  23. data/docs/assets/vue.min.css +1 -0
  24. data/docs/authorization_context.md +33 -0
  25. data/docs/caching.md +262 -0
  26. data/docs/custom_lookup_chain.md +48 -0
  27. data/docs/custom_policy.md +51 -0
  28. data/docs/favicon.ico +0 -0
  29. data/docs/i18n.md +3 -0
  30. data/docs/index.html +25 -0
  31. data/docs/instrumentation.md +3 -0
  32. data/docs/lookup_chain.md +16 -0
  33. data/docs/namespaces.md +69 -0
  34. data/docs/non_rails.md +29 -0
  35. data/docs/pre_checks.md +57 -0
  36. data/docs/quick_start.md +102 -0
  37. data/docs/rails.md +110 -0
  38. data/docs/reasons.md +67 -0
  39. data/docs/testing.md +116 -0
  40. data/docs/writing_policies.md +55 -0
  41. data/gemfiles/jruby.gemfile +5 -0
  42. data/gemfiles/rails42.gemfile +5 -0
  43. data/gemfiles/railsmaster.gemfile +6 -0
  44. data/lib/action_policy.rb +34 -2
  45. data/lib/action_policy/authorizer.rb +28 -0
  46. data/lib/action_policy/base.rb +24 -0
  47. data/lib/action_policy/behaviour.rb +94 -0
  48. data/lib/action_policy/behaviours/memoized.rb +56 -0
  49. data/lib/action_policy/behaviours/namespaced.rb +80 -0
  50. data/lib/action_policy/behaviours/policy_for.rb +23 -0
  51. data/lib/action_policy/behaviours/thread_memoized.rb +54 -0
  52. data/lib/action_policy/ext/module_namespace.rb +21 -0
  53. data/lib/action_policy/ext/policy_cache_key.rb +67 -0
  54. data/lib/action_policy/ext/string_constantize.rb +23 -0
  55. data/lib/action_policy/lookup_chain.rb +84 -0
  56. data/lib/action_policy/policy/aliases.rb +69 -0
  57. data/lib/action_policy/policy/authorization.rb +91 -0
  58. data/lib/action_policy/policy/cache.rb +74 -0
  59. data/lib/action_policy/policy/cached_apply.rb +28 -0
  60. data/lib/action_policy/policy/core.rb +64 -0
  61. data/lib/action_policy/policy/defaults.rb +37 -0
  62. data/lib/action_policy/policy/pre_check.rb +210 -0
  63. data/lib/action_policy/policy/reasons.rb +109 -0
  64. data/lib/action_policy/rails/channel.rb +15 -0
  65. data/lib/action_policy/rails/controller.rb +90 -0
  66. data/lib/action_policy/railtie.rb +74 -0
  67. data/lib/action_policy/rspec.rb +3 -0
  68. data/lib/action_policy/rspec/be_authorized_to.rb +93 -0
  69. data/lib/action_policy/rspec/pundit_syntax.rb +48 -0
  70. data/lib/action_policy/test_helper.rb +46 -0
  71. data/lib/action_policy/testing.rb +64 -0
  72. data/lib/action_policy/version.rb +3 -1
  73. metadata +115 -9
@@ -0,0 +1,55 @@
1
+ # Writing Policies
2
+
3
+ Policy class contains predicate methods (_rules_) which are used to authorize activities.
4
+
5
+ A Policy is instantiated with the target `record` (authorization object) and the [authorization context](authorization_context.md) (by default equals to `user`):
6
+
7
+ ```ruby
8
+ class PostPolicy < ActionPolicy::Base
9
+ def index?
10
+ # allow everyone to perform "index" activity on posts
11
+ true
12
+ end
13
+
14
+ def update?
15
+ # here we can access our context and record
16
+ user.admin? || (user.id == record.user_id)
17
+ end
18
+ end
19
+ ```
20
+
21
+ ## Initializing policies
22
+
23
+ **NOTE**: it is not recommended to manually initialize policy objects and use them directly (one exclusion–[tests](testing.md)). Use `authorize!` / `allowed_to?` methods instead.
24
+
25
+ To initialize policy object, you should specify target record and context:
26
+
27
+ ```ruby
28
+ policy = PostPolicy.new(post, user: user)
29
+
30
+ # simply call rule method
31
+ policy.update?
32
+ ```
33
+
34
+ You can omit the first argument (in that case `record` would be `nil`).
35
+
36
+ Instead of calling rules directly, it is better to call the `apply` method (which wraps rule method with some useful functionality, such as [caching](caching.md), [pre-checks](pre_checks.md), and [failure reasons tracking](reasons.md)):
37
+
38
+ ```ruby
39
+ policy.apply(:update?)
40
+ ```
41
+
42
+ ## Calling other policies
43
+
44
+ Sometimes it is useful to call other resources policies from within a policy. Action Policy provides the `allowed_to?` method as a part of `ActionPolicy::Base`:
45
+
46
+ ```ruby
47
+ class CommentPolicy < ApplicationPolicy
48
+ def update?
49
+ user.admin? || (user.id == record.id) ||
50
+ allowed_to?(:update?, record.post)
51
+ end
52
+ end
53
+ ```
54
+
55
+ You can also specify all the usual options (such as `with`).
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 5.0"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 4.2"
4
+
5
+ gemspec path: ".."
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "arel", github: "rails/arel"
4
+ gem "rails", github: "rails/rails"
5
+
6
+ gemspec path: ".."
@@ -1,5 +1,37 @@
1
- require "action_policy/version"
1
+ # frozen_string_literal: true
2
2
 
3
+ # ActionPolicy is an authorization framework for Ruby/Rails applications.
4
+ #
5
+ # It provides a way to write access policies and helpers to check these policies
6
+ # in your application.
3
7
  module ActionPolicy
4
- # Your code goes here...
8
+ class Error < StandardError; end
9
+
10
+ # Raised when Action Policy fails to find a policy class for a record.
11
+ class NotFound < Error
12
+ attr_reader :target, :message
13
+
14
+ def initialize(target)
15
+ @target = target
16
+ @message = "Couldn't find policy class for #{target.inspect}"
17
+ end
18
+ end
19
+
20
+ require "action_policy/version"
21
+ require "action_policy/base"
22
+ require "action_policy/lookup_chain"
23
+ require "action_policy/authorizer"
24
+ require "action_policy/behaviour"
25
+
26
+ class << self
27
+ attr_accessor :cache_store
28
+
29
+ # Find a policy class for a target
30
+ def lookup(target, **options)
31
+ LookupChain.call(target, **options) ||
32
+ raise(NotFound, target)
33
+ end
34
+ end
35
+
36
+ require "action_policy/railtie" if defined?(::Rails)
5
37
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ # Raised when `authorize!` check fails
5
+ class Unauthorized < Error
6
+ attr_reader :policy, :rule, :reasons
7
+
8
+ def initialize(policy, rule)
9
+ @policy = policy.class
10
+ @rule = rule
11
+ # Reasons module could be not included
12
+ @reasons = policy.reasons if policy.respond_to?(:reasons)
13
+ end
14
+ end
15
+
16
+ # Performs authorization, raises an exception when check failed.
17
+ #
18
+ # The main purpose of this module is to extact authorize action
19
+ # from everything else to make it easily testable.
20
+ module Authorizer
21
+ class << self
22
+ def call(policy, rule)
23
+ policy.apply(rule) ||
24
+ raise(::ActionPolicy::Unauthorized.new(policy, rule))
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ # Base class for application policies.
5
+ class Base
6
+ require "action_policy/policy/core"
7
+ require "action_policy/policy/defaults"
8
+ require "action_policy/policy/authorization"
9
+ require "action_policy/policy/reasons"
10
+ require "action_policy/policy/pre_check"
11
+ require "action_policy/policy/aliases"
12
+ require "action_policy/policy/cache"
13
+ require "action_policy/policy/cached_apply"
14
+
15
+ include ActionPolicy::Policy::Core
16
+ include ActionPolicy::Policy::Authorization
17
+ include ActionPolicy::Policy::Reasons
18
+ include ActionPolicy::Policy::PreCheck
19
+ include ActionPolicy::Policy::Aliases
20
+ include ActionPolicy::Policy::Cache
21
+ include ActionPolicy::Policy::CachedApply
22
+ include ActionPolicy::Policy::Defaults
23
+ end
24
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy/behaviours/policy_for"
4
+ require "action_policy/behaviours/memoized"
5
+ require "action_policy/behaviours/thread_memoized"
6
+ require "action_policy/behaviours/namespaced"
7
+
8
+ module ActionPolicy
9
+ # Provides `authorize!` and `allowed_to?` methods and
10
+ # `authorize` class method to define authorization context.
11
+ #
12
+ # Could be included anywhere to perform authorization.
13
+ module Behaviour
14
+ include ActionPolicy::Behaviours::PolicyFor
15
+
16
+ def self.included(base)
17
+ # Handle ActiveSupport::Concern differently
18
+ if base.respond_to?(:class_methods)
19
+ base.class_methods do
20
+ include ClassMethods
21
+ end
22
+ else
23
+ base.extend ClassMethods
24
+ end
25
+ end
26
+
27
+ # Authorize action against a policy.
28
+ #
29
+ # Policy is inferred from record
30
+ # (unless explicitly specified through `with` option).
31
+ #
32
+ # Raises `ActionPolicy::Unauthorized` if check failed.
33
+ def authorize!(record, to:, **options)
34
+ policy = policy_for(record: record, **options)
35
+
36
+ Authorizer.call(policy, authorization_rule_for(policy, to))
37
+ end
38
+
39
+ # Checks that an activity is allowed for the current context (e.g. user).
40
+ #
41
+ # Returns true of false.
42
+ def allowed_to?(rule, record, **options)
43
+ policy = policy_for(record: record, **options)
44
+ policy.apply(authorization_rule_for(policy, rule))
45
+ end
46
+
47
+ def authorization_context
48
+ return @__authorization_context if
49
+ instance_variable_defined?(:@__authorization_context)
50
+
51
+ @__authorization_context = self.class.authorization_targets
52
+ .each_with_object({}) do |(key, meth), obj|
53
+ obj[key] = public_send(meth)
54
+ end
55
+ end
56
+
57
+ # Check that rule is defined for policy,
58
+ # otherwise fallback to :manage? rule.
59
+ def authorization_rule_for(policy, rule)
60
+ policy.resolve_rule(rule)
61
+ end
62
+
63
+ module ClassMethods # :nodoc:
64
+ # Configure authorization context.
65
+ #
66
+ # For example:
67
+ #
68
+ # class ApplicationController < ActionController::Base
69
+ # # Pass the value of `current_user` to authorization as `user`
70
+ # authorize :user, through: :current_user
71
+ # end
72
+ #
73
+ # # Assuming that in your ApplicationPolicy
74
+ # class ApplicationPolicy < ActionPolicy::Base
75
+ # authorize :user
76
+ # end
77
+ def authorize(key, through: nil)
78
+ meth = through || key
79
+ authorization_targets[key] = meth
80
+ end
81
+
82
+ def authorization_targets
83
+ return @authorization_targets if instance_variable_defined?(:@authorization_targets)
84
+
85
+ @authorization_targets =
86
+ if superclass.respond_to?(:authorization_targets)
87
+ superclass.authorization_targets.dup
88
+ else
89
+ {}
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Behaviours
5
+ # Per-instance memoization for policies.
6
+ #
7
+ # Used by `policy_for` to re-use policy object for records.
8
+ #
9
+ # Example:
10
+ #
11
+ # include ActionPolicy::Behaviour
12
+ # include ActionPolicy::Memoized
13
+ #
14
+ # record = User.first
15
+ # policy = policy_for(record)
16
+ # policy2 = policy_for(record)
17
+ #
18
+ # policy.equal?(policy) #=> true
19
+ #
20
+ # policy.equal?(policy_for(record, with: CustomPolicy)) #=> false
21
+ module Memoized
22
+ require "action_policy/ext/policy_cache_key"
23
+ using ActionPolicy::Ext::PolicyCacheKey
24
+
25
+ class << self
26
+ def prepended(base)
27
+ base.prepend InstanceMethods
28
+ end
29
+
30
+ alias included prepended
31
+ end
32
+
33
+ module InstanceMethods # :nodoc:
34
+ def policy_for(record:, **opts)
35
+ __policy_memoize__(record, **opts) { super(record: record, **opts) }
36
+ end
37
+ end
38
+
39
+ def __policy_memoize__(record, with: nil, namespace: nil)
40
+ record_key = record._policy_cache_key(use_object_id: true)
41
+ cache_key = "#{namespace}/#{with}/#{record_key}"
42
+
43
+ return __policies_cache__[cache_key] if
44
+ __policies_cache__.key?(cache_key)
45
+
46
+ policy = yield
47
+
48
+ __policies_cache__[cache_key] = policy
49
+ end
50
+
51
+ def __policies_cache__
52
+ @__policies_cache__ ||= {}
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Behaviours
5
+ # Adds an ability to lookup policies from current _context_ (namespace):
6
+ #
7
+ # module Admin
8
+ # class UsersController < ApplictionController
9
+ # def index
10
+ # # uses Admin::UserPolicy if any, otherwise fallbacks to UserPolicy
11
+ # authorize!
12
+ # end
13
+ # end
14
+ # end
15
+ #
16
+ # Modules nesting is also supported:
17
+ #
18
+ # module Admin
19
+ # module Client
20
+ # class UsersController < ApplictionController
21
+ # def index
22
+ # # lookup for Admin::Client::UserPolicy -> Admin::UserPolicy -> UserPolicy
23
+ # authorize!
24
+ # end
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ # NOTE: in order to support namespaced lookup for non-inferrable resources,
30
+ # you should specify `policy_name` at a class level
31
+ # (instead of `policy_class`, which doesn't take into account namespaces):
32
+ #
33
+ # class Guest < User
34
+ # def self.policy_name
35
+ # "UserPolicy"
36
+ # end
37
+ # end
38
+ #
39
+ # NOTE: by default, we use class's name as a policy name; so, for namespaced
40
+ # resources the namespace part is also included:
41
+ #
42
+ # class Admin
43
+ # class User
44
+ # end
45
+ # end
46
+ #
47
+ # # search for Admin::UserPolicy, but not for UserPolicy
48
+ # authorize! Admin::User.new
49
+ #
50
+ # You can access the current authorization namespace through `authorization_namespace` method.
51
+ #
52
+ # You can also define your own namespacing logic by overriding `authorization_namespace`:
53
+ #
54
+ # def authorization_namespace
55
+ # return ::Admin if current_user.admin?
56
+ # return ::Staff if current_user.staff?
57
+ # # fallback to current namespace
58
+ # super
59
+ # end
60
+ module Namespaced
61
+ require "action_policy/ext/module_namespace"
62
+ using ActionPolicy::Ext::ModuleNamespace
63
+
64
+ class << self
65
+ def prepended(base)
66
+ base.prepend InstanceMethods
67
+ end
68
+
69
+ alias included prepended
70
+ end
71
+
72
+ module InstanceMethods # :nodoc:
73
+ def authorization_namespace
74
+ return @authorization_namespace if instance_variable_defined?(:@authorization_namespace)
75
+ @authorization_namespace = self.class.namespace
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module Behaviours
5
+ # Adds `policy_for` method
6
+ module PolicyFor
7
+ # Returns policy instance for the record.
8
+ def policy_for(record:, with: nil, namespace: nil)
9
+ namespace ||= authorization_namespace
10
+ policy_class = with || ::ActionPolicy.lookup(record, namespace: namespace)
11
+ policy_class.new(record, **authorization_context)
12
+ end
13
+
14
+ def authorization_context
15
+ raise NotImplementedError, "Please, define `authorization_context` method!"
16
+ end
17
+
18
+ def authorization_namespace
19
+ # override to provide specific authorization namespace
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module PerThreadCache # :nodoc:
5
+ CACHE_KEY = "action_policy.per_thread_cache"
6
+
7
+ class << self
8
+ def fetch(key)
9
+ store = (Thread.current[CACHE_KEY] ||= {})
10
+
11
+ return store[key] if store.key?(key)
12
+
13
+ store[key] = yield
14
+ end
15
+
16
+ def clear_all
17
+ Thread.current[CACHE_KEY] = {}
18
+ end
19
+ end
20
+ end
21
+
22
+ module Behaviours
23
+ # Per-thread memoization for policies.
24
+ #
25
+ # Used by `policy_for` to re-use policy object for records.
26
+ #
27
+ # NOTE: don't forget to clear thread cache with ActionPolicy::PerThreadCache.clear_all
28
+ module ThreadMemoized
29
+ require "action_policy/ext/policy_cache_key"
30
+ using ActionPolicy::Ext::PolicyCacheKey
31
+
32
+ class << self
33
+ def prepended(base)
34
+ base.prepend InstanceMethods
35
+ end
36
+
37
+ alias included prepended
38
+ end
39
+
40
+ module InstanceMethods # :nodoc:
41
+ def policy_for(record:, **opts)
42
+ __policy_thread_memoize__(record, **opts) { super(record: record, **opts) }
43
+ end
44
+ end
45
+
46
+ def __policy_thread_memoize__(record, with: nil, namespace: nil)
47
+ record_key = record._policy_cache_key(use_object_id: true)
48
+ cache_key = "#{namespace}/#{with}/#{record_key}"
49
+
50
+ ActionPolicy::PerThreadCache.fetch(cache_key) { yield }
51
+ end
52
+ end
53
+ end
54
+ end