action_policy 0.0.1 → 0.1.0

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