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