padrino-auth 0.0.12
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 +7 -0
- data/.gitignore +34 -0
- data/EXAMPLES.md +249 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +118 -0
- data/Rakefile +12 -0
- data/lib/padrino-auth.rb +2 -0
- data/lib/padrino-auth/access.rb +148 -0
- data/lib/padrino-auth/login.rb +138 -0
- data/lib/padrino-auth/login/controller.rb +20 -0
- data/lib/padrino-auth/login/layouts/layout.slim +10 -0
- data/lib/padrino-auth/login/new.slim +35 -0
- data/lib/padrino-auth/permissions.rb +180 -0
- data/lib/padrino-auth/version.rb +5 -0
- data/padrino-auth.gemspec +29 -0
- data/test/helper.rb +68 -0
- data/test/test_padrino_access.rb +124 -0
- data/test/test_padrino_auth.rb +38 -0
- data/test/test_padrino_login.rb +81 -0
- metadata +154 -0
data/lib/padrino-auth.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'padrino-auth/permissions'
|
2
|
+
|
3
|
+
module Padrino
|
4
|
+
##
|
5
|
+
# Padrino authorization module.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Nifty::Application < Padrino::Application
|
9
|
+
# # optional settings
|
10
|
+
# set :credentials_reader, :visitor # the name of getter method in helpers
|
11
|
+
# # required statement
|
12
|
+
# register Padrino::Access
|
13
|
+
# # example persistance storage
|
14
|
+
# enable :sessions
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # optional helpers
|
18
|
+
# Nifty::Application.helpers do
|
19
|
+
# def visitor
|
20
|
+
# session[:visitor] ||= Visitor.guest_account
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # example visitor model
|
25
|
+
# module Visitor
|
26
|
+
# extend self
|
27
|
+
# def guest_account
|
28
|
+
# OpenStruct.new(:role => :guest, :id => 1)
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # example controllers
|
33
|
+
# Nifty::Application.controller :public_area do
|
34
|
+
# set_access :*
|
35
|
+
# get(:index){ 'public content' }
|
36
|
+
# end
|
37
|
+
# Nifty::Application.controller :members_area do
|
38
|
+
# set_access :member
|
39
|
+
# get(:index){ 'secret content' }
|
40
|
+
# end
|
41
|
+
# Nifty::Application.controller :login do
|
42
|
+
# set_access :*
|
43
|
+
# get(:index){ session[:visitor] = OpenStruct.new(:role => :guest, :id => 1) }
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
module Access
|
47
|
+
class << self
|
48
|
+
def registered(app)
|
49
|
+
included(app)
|
50
|
+
app.default(:credentials_reader, :credentials)
|
51
|
+
app.default(:access_errors, true)
|
52
|
+
app.send :attr_reader, app.credentials_reader unless app.instance_methods.include?(app.credentials_reader)
|
53
|
+
app.set :permissions, Permissions.new
|
54
|
+
app.login_permissions if app.respond_to?(:login_permissions)
|
55
|
+
app.before do
|
56
|
+
authorized? || error(403, '403 Forbidden')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def included(base)
|
61
|
+
base.send(:include, InstanceMethods)
|
62
|
+
base.extend(ClassMethods)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module ClassMethods
|
67
|
+
##
|
68
|
+
# Empties the list of permission.
|
69
|
+
#
|
70
|
+
def reset_access!
|
71
|
+
permissions.clear!
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Allows access to action with objects.
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# # in application
|
79
|
+
# set_access :*, :with => :login # allows everyone to interact with :login controller
|
80
|
+
# # in controller
|
81
|
+
# App.controller :members_area do
|
82
|
+
# set_access :member # allows all members to access :members_area controller
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
def set_access(*args)
|
86
|
+
options = args.extract_options!
|
87
|
+
options[:object] ||= Array(@_controller).first.to_s.singularize.to_sym if @_controller.present?
|
88
|
+
permissions.add(*args, options)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module InstanceMethods
|
93
|
+
##
|
94
|
+
# Checks if current visitor has access to current action with current controller.
|
95
|
+
#
|
96
|
+
def authorized?
|
97
|
+
access_action?
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Returns current visitor.
|
102
|
+
#
|
103
|
+
def access_subject
|
104
|
+
send settings.credentials_reader
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Checks if current visitor is one of the specified roles. Can accept a block.
|
109
|
+
#
|
110
|
+
def access_role?(*roles, &block)
|
111
|
+
settings.permissions.check(access_subject, :have => roles, &block)
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Checks if current visitor is allowed to to the action with object. Can accept a block.
|
116
|
+
#
|
117
|
+
def access_action?(action = nil, object = nil, &block)
|
118
|
+
return true if response.status/100 == 4 && settings.access_errors
|
119
|
+
if respond_to?(:request) && action.nil? && object.nil?
|
120
|
+
object = request.controller
|
121
|
+
action = request.action
|
122
|
+
if object.nil? && action.present? && action.to_s.index('/')
|
123
|
+
object, action = request.env['PATH_INFO'].to_s.scan(/\/([^\/]*)/).map(&:first)
|
124
|
+
end
|
125
|
+
object ||= :''
|
126
|
+
action ||= :index
|
127
|
+
object = object.to_sym
|
128
|
+
action = action.to_sym
|
129
|
+
end
|
130
|
+
settings.permissions.check(access_subject, :allow => action, :with => object, &block)
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Check if current visitor is allowed to interact with object by action. Can accept a block.
|
135
|
+
#
|
136
|
+
def access_object?(object = nil, action = nil, &block)
|
137
|
+
allow_action action, object, &block
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Populates the list of objects the current visitor is allowed to interact with.
|
142
|
+
#
|
143
|
+
def access_objects(subject = access_subject, action = nil)
|
144
|
+
settings.permissions.find_objects(subject, action)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'padrino-auth/login/controller'
|
2
|
+
|
3
|
+
module Padrino
|
4
|
+
##
|
5
|
+
# Padrino authentication module.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Nifty::Application < Padrino::Application
|
9
|
+
# # optional settings
|
10
|
+
# set :session_key, "visitor_id" # visitor key name in session storage, defaults to "_login_#{app.app_name}")
|
11
|
+
# set :login_model, :visitor # model name for visitor storage, defaults to :account, must be constantizable
|
12
|
+
# set :credentials_accessor, :visitor # the name of setter/getter method in helpers, defaults to :credentials
|
13
|
+
# enable :login_bypass # enables or disables login bypass in development mode, defaults to disable
|
14
|
+
# set :login_url, '/sign/in' # sets the utl to be redirected to if not logged in and in restricted area, defaults to '/login'
|
15
|
+
# disable :login_permissions # sets initial login permissions, defaults to { set_access(:*, :allow => :*, :with => :login) }
|
16
|
+
# disable :login_controller # disables default login controller to show an example of the custom one
|
17
|
+
#
|
18
|
+
# # required statement
|
19
|
+
# register Padrino::Login
|
20
|
+
# # example persistance storage
|
21
|
+
# enable :sessions
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# TODO: example controllers
|
25
|
+
#
|
26
|
+
module Login
|
27
|
+
class << self
|
28
|
+
def registered(app)
|
29
|
+
warn 'Padrino::Login must be registered before Padrino::Access' if app.respond_to?(:set_access)
|
30
|
+
included(app)
|
31
|
+
setup_storage(app)
|
32
|
+
setup_controller(app)
|
33
|
+
app.before do
|
34
|
+
log_in if authorization_required?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def included(base)
|
39
|
+
base.send(:include, InstanceMethods)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def setup_storage(app)
|
45
|
+
app.default(:session_key, "_login_#{app.app_name}")
|
46
|
+
app.default(:login_model, :account)
|
47
|
+
app.default(:credentials_accessor, :credentials)
|
48
|
+
app.send :attr_reader, app.credentials_accessor unless app.instance_methods.include?(app.credentials_accessor)
|
49
|
+
app.send :attr_writer, app.credentials_accessor unless app.instance_methods.include?(:"#{app.credentials_accessor}=")
|
50
|
+
app.default(:login_bypass, false)
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_controller(app)
|
54
|
+
app.default(:login_url, '/login')
|
55
|
+
app.default(:login_permissions) { set_access(:*, :allow => :*, :with => :login) }
|
56
|
+
app.default(:login_controller, true)
|
57
|
+
app.controller(:login) { include Controller } if app.login_controller
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
# Returns the model used to authenticate visitors.
|
63
|
+
def login_model
|
64
|
+
@login_model ||= settings.login_model.to_s.classify.constantize
|
65
|
+
end
|
66
|
+
|
67
|
+
# Authenticates the visitor.
|
68
|
+
def authenticate
|
69
|
+
resource = login_model.authenticate(:email => params[:email], :password => params[:password])
|
70
|
+
resource ||= login_model.authenticate(:bypass => true) if settings.login_bypass && params[:bypass]
|
71
|
+
save_credentials(resource)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Checks if the visitor is authenticated.
|
75
|
+
def logged_in?
|
76
|
+
!!(send(settings.credentials_accessor) || restore_credentials)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Looks for authorization routine and calls it to check if the visitor is authorized.
|
80
|
+
def unauthorized?
|
81
|
+
respond_to?(:authorized?) && !authorized?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Checks if the current location needs the visitor to be authorized.
|
85
|
+
def authorization_required?
|
86
|
+
if logged_in?
|
87
|
+
if unauthorized?
|
88
|
+
# 403 Forbidden, provided credentials were successfully
|
89
|
+
# authenticated but the credentials still do not grant
|
90
|
+
# the client permission to access the resource
|
91
|
+
error 403, '403 Forbidden'
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
else
|
96
|
+
unauthorized?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Logs the visitor in using redirect to login page url.
|
101
|
+
def log_in
|
102
|
+
login_url = settings.login_url
|
103
|
+
if request.env['PATH_INFO'] != login_url
|
104
|
+
save_location
|
105
|
+
# 302 Found
|
106
|
+
redirect url(login_url)
|
107
|
+
# 401 Unauthorized, authentication is required and
|
108
|
+
# has not yet been provided
|
109
|
+
error 401, '401 Unauthorized'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Saves credentials in session.
|
114
|
+
def save_credentials(resource)
|
115
|
+
session[settings.session_key] = resource.respond_to?(:id) ? resource.id : resource
|
116
|
+
send(:"#{settings.credentials_accessor}=", resource)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Restores credentials from session using visitor model.
|
120
|
+
def restore_credentials
|
121
|
+
resource = login_model.authenticate(:id => session[settings.session_key])
|
122
|
+
send(:"#{settings.credentials_accessor}=", resource)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Redirects back to saved location or '/'
|
126
|
+
def restore_location
|
127
|
+
redirect session.delete(:return_to) || url('/')
|
128
|
+
end
|
129
|
+
|
130
|
+
# Saves location to session for following redirect in case of successful authentication.
|
131
|
+
def save_location
|
132
|
+
uri = env['REQUEST_URI'] || url(env['PATH_INFO'])
|
133
|
+
return if uri.blank? || uri.match(/\.css$|\.js$|\.png$/)
|
134
|
+
session[:return_to] = "#{ENV['RACK_BASE_URI']}#{uri}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Padrino
|
2
|
+
module Login
|
3
|
+
module Controller
|
4
|
+
def self.included(base)
|
5
|
+
base.get :index do
|
6
|
+
render :slim, :"new", :layout => "layout", :views => File.dirname(__FILE__)
|
7
|
+
end
|
8
|
+
base.post :index do
|
9
|
+
if authenticate
|
10
|
+
restore_location
|
11
|
+
else
|
12
|
+
params.delete 'password'
|
13
|
+
flash.now[:error] = 'Wrong password'
|
14
|
+
render :slim, :"new", :layout => "layout", :views => File.dirname(__FILE__)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
meta charset="utf-8"
|
5
|
+
meta name="robots" content="noindex"
|
6
|
+
title Padrino::Login
|
7
|
+
link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/2.3.2/css/bootstrap.min.css"
|
8
|
+
body
|
9
|
+
.container.login style='width: 287px'
|
10
|
+
= yield
|
@@ -0,0 +1,35 @@
|
|
1
|
+
h3
|
2
|
+
| Login
|
3
|
+
br
|
4
|
+
small= link_to request.env['HTTP_HOST'], url('/')
|
5
|
+
|
6
|
+
= form_tag( '', :class => 'form-horizontal well' ) do
|
7
|
+
- [:error, :warning, :success, :notice].each do |type|
|
8
|
+
- next if flash[type].blank?
|
9
|
+
.alert.alert-message class=('alert-' + (type == :notice ? :info : type).to_s) data-alert=true
|
10
|
+
= flash[type]
|
11
|
+
|
12
|
+
legend Social
|
13
|
+
.control-group
|
14
|
+
= link_to image_tag('/images/social/google.png'), url('/oauth/google')
|
15
|
+
|
16
|
+
legend Obsolete
|
17
|
+
.control-group
|
18
|
+
.input-prepend
|
19
|
+
span.add-on
|
20
|
+
i.icon-envelope
|
21
|
+
= text_field_tag :email, :value => params[:email], :placeholder => "email"
|
22
|
+
.control-group
|
23
|
+
.input-prepend
|
24
|
+
span.add-on
|
25
|
+
i.icon-lock
|
26
|
+
= password_field_tag :password, :value => params[:password], :placeholder => "password"
|
27
|
+
.control-group
|
28
|
+
= submit_tag('Log in', :class => 'btn btn-primary pull-right')
|
29
|
+
- if settings.login_bypass
|
30
|
+
label.checkbox
|
31
|
+
| Bypass
|
32
|
+
= check_box_tag :bypass, :value => 'Bypass'
|
33
|
+
|
34
|
+
small
|
35
|
+
= link_to 'Forgot password?', url('/login/reset_password')
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Padrino
|
2
|
+
##
|
3
|
+
# Class to store and check permissions used in Padrino::Access.
|
4
|
+
#
|
5
|
+
class Permissions
|
6
|
+
##
|
7
|
+
# Initializes new permissions storage.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# permissions = Permissions.new
|
11
|
+
#
|
12
|
+
def initialize
|
13
|
+
clear!
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Clears permit records and action cache.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# permissions.clear!
|
21
|
+
#
|
22
|
+
def clear!
|
23
|
+
@permits = {}
|
24
|
+
@actions = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Adds a permission record to storage.
|
29
|
+
#
|
30
|
+
# @param [Symbol || Object] subject
|
31
|
+
# permit subject
|
32
|
+
# @param [Hash] options
|
33
|
+
# permit attributes
|
34
|
+
# @param [Symbol] options[:allow] || options[:action]
|
35
|
+
# what action to allow with objects
|
36
|
+
# @param [Symbol] options[:with] || options[:object]
|
37
|
+
# with what objects allow specified action
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# permissions.add :robots, :allow => :protect, :object => :humans
|
41
|
+
# permissions.add @bender, :allow => :kill, :object => :humans
|
42
|
+
#
|
43
|
+
def add(*args)
|
44
|
+
@actions = {}
|
45
|
+
options = args.extract_options!
|
46
|
+
action, object = action_and_object(options)
|
47
|
+
object_type = detect_type(object)
|
48
|
+
args.each{ |subject| merge(subject, action, object_type) }
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Checks if permission record exists. Returns a boolean or yield a block.
|
53
|
+
#
|
54
|
+
# @param [Object] subject
|
55
|
+
# performer of an action
|
56
|
+
# @param [Hash] options
|
57
|
+
# attributes to check
|
58
|
+
# @param [Symbol] options[:have]
|
59
|
+
# check if the subject has a role
|
60
|
+
# @param [Symbol] options[:allow] || options[:action]
|
61
|
+
# check if the subject is allowed to perform the action
|
62
|
+
# @param [Symbol] options[:with] || options[:object]
|
63
|
+
# check if the subject is allowed to interact with the subject
|
64
|
+
# @param [Proc]
|
65
|
+
# optional block to yield if the action is allowed
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# # check if @bender have role :robots
|
69
|
+
# permissions.check @bender, :have => :robots # => true
|
70
|
+
# # check if @bender is allowed to kill :humans
|
71
|
+
# permissions.check @bender, :allow => :kill, :object => :humans # => true
|
72
|
+
# # check if @bender is allowed to kill :humans and yield a block
|
73
|
+
# permissions.check @bender, :allow => :kill, :object => :humans do
|
74
|
+
# @bender.kill_all! :humans
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
def check(subject, options)
|
78
|
+
case
|
79
|
+
when options[:have]
|
80
|
+
check_role(subject, options[:have])
|
81
|
+
else
|
82
|
+
check_action(subject, *action_and_object(options))
|
83
|
+
end && (block_given? ? yield : true)
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Populates and returns the list of objects available to the subject.
|
88
|
+
#
|
89
|
+
# @param [Object] subject
|
90
|
+
# the subject to be checked for actions
|
91
|
+
#
|
92
|
+
def find_objects(subject, target_action=nil)
|
93
|
+
find_actions(subject).inject([]) do |all,(action,objects)|
|
94
|
+
all |= objects if target_action.nil? || action == target_action || action == :*
|
95
|
+
all
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Merges a list of new permits into permissions storage.
|
102
|
+
def merge(subject, actions, object_type)
|
103
|
+
subject_id = detect_id(subject)
|
104
|
+
@permits[subject_id] ||= {}
|
105
|
+
Array(actions).each do |action|
|
106
|
+
@permits[subject_id][action] ||= []
|
107
|
+
@permits[subject_id][action] |= [object_type]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checks if the subject has the role.
|
112
|
+
def check_role(subject, roles)
|
113
|
+
if subject.respond_to?(:role)
|
114
|
+
Array(roles).include?(subject.role)
|
115
|
+
else
|
116
|
+
false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Checks if the subject is allowed to perform the action with the object.
|
121
|
+
def check_action(subject, action, object)
|
122
|
+
actions = find_actions(subject)
|
123
|
+
objects = actions && (Array(actions[action]) | Array(actions[:*]))
|
124
|
+
objects && (objects & [:*, detect_type(object)]).any?
|
125
|
+
end
|
126
|
+
|
127
|
+
# Finds all permits for the subject. Caches the permits in @actions.
|
128
|
+
# find_actions(@bender) # => { :kill => { :humans }, :drink => { :booze }, :* => { :login } }
|
129
|
+
def find_actions(subject)
|
130
|
+
subject_id = detect_id(subject)
|
131
|
+
return @actions[subject_id] if @actions[subject_id]
|
132
|
+
actions = @permits[subject_id] || {}
|
133
|
+
if subject.respond_to?(:role) && (role_actions = @permits[subject.role.to_sym])
|
134
|
+
actions.merge!(role_actions){ |_,left,right| Array(left)|Array(right) }
|
135
|
+
end
|
136
|
+
if public_actions = @permits[:*]
|
137
|
+
actions.merge!(public_actions){ |_,left,right| Array(left)|Array(right) }
|
138
|
+
end
|
139
|
+
@actions[subject_id] = actions
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns object type.
|
143
|
+
# detect_type :humans # => :human
|
144
|
+
# detect_type 'foobar' # => 'foobar'
|
145
|
+
def detect_type(object)
|
146
|
+
case object
|
147
|
+
when Symbol
|
148
|
+
object.to_s.singularize.to_sym
|
149
|
+
else
|
150
|
+
object
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns parametrized subject.
|
155
|
+
# detect_id :robots # => :robots
|
156
|
+
# detect_id sluggable_ar_resource # => 'Sluggable-resource-slug'
|
157
|
+
# detect_id some_resource_with_id # => '4'
|
158
|
+
# detect_id generic_object # => "<Object:0x00001234>"
|
159
|
+
def detect_id(subject)
|
160
|
+
case
|
161
|
+
when Symbol === subject
|
162
|
+
subject
|
163
|
+
when subject.respond_to?(:to_param)
|
164
|
+
subject.to_param
|
165
|
+
when subject.respond_to?(:id)
|
166
|
+
subject.id.to_s
|
167
|
+
else
|
168
|
+
"#{subject}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Utility function to extract action and object from options. Defaults to [:*, :*]
|
173
|
+
# action_and_object(:allow => :kill, :object => :humans) # => [:kill, :humans]
|
174
|
+
# action_and_object(:action => :romance, :with => :mutants) # => [:romance, :mutants]
|
175
|
+
# action_and_object({}) # => [:*, :*]
|
176
|
+
def action_and_object(options)
|
177
|
+
[options[:allow] || options[:action] || :*, options[:with] || options[:object] || :*]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|