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.
- checksums.yaml +5 -5
- data/.rubocop.yml +85 -0
- data/.travis.yml +25 -2
- data/CHANGELOG.md +7 -0
- data/Gemfile +12 -3
- data/README.md +71 -12
- data/Rakefile +9 -1
- data/action_policy.gemspec +11 -5
- data/docs/.nojekyll +0 -0
- data/docs/CNAME +1 -0
- data/docs/README.md +46 -0
- data/docs/_sidebar.md +19 -0
- data/docs/aliases.md +54 -0
- data/docs/assets/docsify.min.js +1 -0
- data/docs/assets/fonts/FiraCode-Medium.woff +0 -0
- data/docs/assets/fonts/FiraCode-Regular.woff +0 -0
- data/docs/assets/images/cache.png +0 -0
- data/docs/assets/images/cache.svg +70 -0
- data/docs/assets/images/layer.png +0 -0
- data/docs/assets/images/layer.svg +92 -0
- data/docs/assets/prism-ruby.min.js +1 -0
- data/docs/assets/styles.css +317 -0
- data/docs/assets/vue.min.css +1 -0
- data/docs/authorization_context.md +33 -0
- data/docs/caching.md +262 -0
- data/docs/custom_lookup_chain.md +48 -0
- data/docs/custom_policy.md +51 -0
- data/docs/favicon.ico +0 -0
- data/docs/i18n.md +3 -0
- data/docs/index.html +25 -0
- data/docs/instrumentation.md +3 -0
- data/docs/lookup_chain.md +16 -0
- data/docs/namespaces.md +69 -0
- data/docs/non_rails.md +29 -0
- data/docs/pre_checks.md +57 -0
- data/docs/quick_start.md +102 -0
- data/docs/rails.md +110 -0
- data/docs/reasons.md +67 -0
- data/docs/testing.md +116 -0
- data/docs/writing_policies.md +55 -0
- data/gemfiles/jruby.gemfile +5 -0
- data/gemfiles/rails42.gemfile +5 -0
- data/gemfiles/railsmaster.gemfile +6 -0
- data/lib/action_policy.rb +34 -2
- data/lib/action_policy/authorizer.rb +28 -0
- data/lib/action_policy/base.rb +24 -0
- data/lib/action_policy/behaviour.rb +94 -0
- data/lib/action_policy/behaviours/memoized.rb +56 -0
- data/lib/action_policy/behaviours/namespaced.rb +80 -0
- data/lib/action_policy/behaviours/policy_for.rb +23 -0
- data/lib/action_policy/behaviours/thread_memoized.rb +54 -0
- data/lib/action_policy/ext/module_namespace.rb +21 -0
- data/lib/action_policy/ext/policy_cache_key.rb +67 -0
- data/lib/action_policy/ext/string_constantize.rb +23 -0
- data/lib/action_policy/lookup_chain.rb +84 -0
- data/lib/action_policy/policy/aliases.rb +69 -0
- data/lib/action_policy/policy/authorization.rb +91 -0
- data/lib/action_policy/policy/cache.rb +74 -0
- data/lib/action_policy/policy/cached_apply.rb +28 -0
- data/lib/action_policy/policy/core.rb +64 -0
- data/lib/action_policy/policy/defaults.rb +37 -0
- data/lib/action_policy/policy/pre_check.rb +210 -0
- data/lib/action_policy/policy/reasons.rb +109 -0
- data/lib/action_policy/rails/channel.rb +15 -0
- data/lib/action_policy/rails/controller.rb +90 -0
- data/lib/action_policy/railtie.rb +74 -0
- data/lib/action_policy/rspec.rb +3 -0
- data/lib/action_policy/rspec/be_authorized_to.rb +93 -0
- data/lib/action_policy/rspec/pundit_syntax.rb +48 -0
- data/lib/action_policy/test_helper.rb +46 -0
- data/lib/action_policy/testing.rb +64 -0
- data/lib/action_policy/version.rb +3 -1
- 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`).
|
data/lib/action_policy.rb
CHANGED
@@ -1,5 +1,37 @@
|
|
1
|
-
|
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
|
-
|
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
|