padrino-admin 0.2.9 → 0.4.5
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.
- data/Rakefile +9 -8
- data/VERSION +1 -1
- data/lib/padrino-admin.rb +11 -2
- data/lib/padrino-admin/access_control.rb +353 -0
- data/lib/padrino-admin/access_control/helpers.rb +81 -0
- data/lib/padrino-admin/adapters.rb +82 -0
- data/lib/padrino-admin/adapters/ar.rb +75 -0
- data/lib/padrino-admin/adapters/dm.rb +131 -0
- data/lib/padrino-admin/adapters/mm.rb +49 -0
- data/lib/padrino-admin/ext_js/config.rb +153 -151
- data/lib/padrino-admin/ext_js/controller.rb +167 -0
- data/lib/padrino-admin/generators/backend.rb +1 -1
- data/lib/padrino-admin/locale/en.yml +7 -0
- data/lib/padrino-admin/support.rb +12 -0
- data/lib/padrino-admin/utils/crypt.rb +29 -0
- data/padrino-admin.gemspec +27 -6
- data/test/fixtures/active_record.rb +17 -0
- data/test/fixtures/data_mapper.rb +36 -0
- data/test/fixtures/mongo_mapper.rb +12 -0
- data/test/helper.rb +44 -48
- data/test/test_access_control.rb +98 -0
- data/test/test_active_record.rb +28 -0
- data/test/test_admin_application.rb +38 -0
- data/test/test_controller.rb +28 -0
- data/test/test_data_mapper.rb +32 -0
- data/test/test_mongo_mapper.rb +28 -0
- data/test/test_parsing.rb +12 -12
- metadata +33 -5
- data/test/test_padrino_admin.rb +0 -7
data/Rakefile
CHANGED
@@ -10,13 +10,14 @@ begin
|
|
10
10
|
gem.email = "nesquena@gmail.com"
|
11
11
|
gem.homepage = "http://github.com/padrino/padrino-framework/tree/master/padrino-admin"
|
12
12
|
gem.authors = ["Padrino Team", "Nathan Esquenazi", "Davide D'Agostino", "Arthur Chiu"]
|
13
|
-
gem.add_runtime_dependency "
|
14
|
-
gem.add_runtime_dependency "padrino-core",
|
15
|
-
gem.add_development_dependency "
|
16
|
-
gem.add_development_dependency "
|
17
|
-
gem.add_development_dependency "
|
18
|
-
gem.add_development_dependency "
|
19
|
-
gem.add_development_dependency "
|
13
|
+
gem.add_runtime_dependency "json_pure", ">= 1.2.0"
|
14
|
+
gem.add_runtime_dependency "padrino-core", ">= 0.1.1"
|
15
|
+
gem.add_development_dependency "dm-core", ">= 0.10.2"
|
16
|
+
gem.add_development_dependency "haml", ">= 2.2.1"
|
17
|
+
gem.add_development_dependency "shoulda", ">= 0"
|
18
|
+
gem.add_development_dependency "mocha", ">= 0.9.7"
|
19
|
+
gem.add_development_dependency "rack-test", ">= 0.5.0"
|
20
|
+
gem.add_development_dependency "webrat", ">= 0.5.1"
|
20
21
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
22
|
end
|
22
23
|
Jeweler::GemcutterTasks.new
|
@@ -26,7 +27,7 @@ end
|
|
26
27
|
|
27
28
|
require 'rake/testtask'
|
28
29
|
Rake::TestTask.new(:test) do |test|
|
29
|
-
test.libs << '
|
30
|
+
test.libs << 'test'
|
30
31
|
test.pattern = 'test/**/test_*.rb'
|
31
32
|
test.verbose = true
|
32
33
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.5
|
data/lib/padrino-admin.rb
CHANGED
@@ -1,2 +1,11 @@
|
|
1
|
-
require 'padrino-
|
2
|
-
|
1
|
+
require 'padrino-core'
|
2
|
+
|
3
|
+
Dir[File.dirname(__FILE__) + '/padrino-admin/*.rb'].each {|file| require file }
|
4
|
+
Dir[File.dirname(__FILE__) + '/padrino-admin/{access_control,adapters,ext_js,generators,utils}/*.rb'].each {|file| require file }
|
5
|
+
|
6
|
+
Padrino::Application.send(:cattr_accessor, :access_control)
|
7
|
+
Padrino::Application.send(:access_control=, Class.new(Padrino::AccessControl::Base))
|
8
|
+
String.send(:include, Padrino::Admin::Utils::Crypt)
|
9
|
+
|
10
|
+
# Load our locales
|
11
|
+
I18n.load_path += Dir["#{File.dirname(__FILE__)}/padrino-admin/locale/*.yml"]
|
@@ -0,0 +1,353 @@
|
|
1
|
+
module Padrino
|
2
|
+
|
3
|
+
class AccessControlError < StandardError; end
|
4
|
+
|
5
|
+
# This module give to a padrino application an access control functionality like:
|
6
|
+
#
|
7
|
+
# class EcommerceDemo < Padrino::Application
|
8
|
+
# enable :authentication
|
9
|
+
# set :redirect_back_or_default, "/login" # or your page
|
10
|
+
# set :use_orm, :active_record # or :data_mapper, :mongo_mapper
|
11
|
+
#
|
12
|
+
# access_control.roles_for :any do
|
13
|
+
# role.require_login "/cart"
|
14
|
+
# role.require_login "/account"
|
15
|
+
# role.allow "/account/create"
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# In the EcommerceDemo, we <tt>only</tt> require logins for all paths that start with "/cart" like:
|
20
|
+
#
|
21
|
+
# - "/cart/add"
|
22
|
+
# - "/cart/empty"
|
23
|
+
# - "/cart/checkout"
|
24
|
+
#
|
25
|
+
# same thing for "/account" so we require a login for:
|
26
|
+
#
|
27
|
+
# - "/account"
|
28
|
+
# - "/account/edit"
|
29
|
+
# - "/account/update"
|
30
|
+
#
|
31
|
+
# but if we call "/account/create" we don't need to be logged in our site for do that.
|
32
|
+
# In EcommerceDemo example we set <tt>redirect_back_or_default</tt> so if a <tt>unlogged</tt>
|
33
|
+
# user try to access "/account/edit" will be redirected to "/login" when login is done will be
|
34
|
+
# redirected to "/account/edit".
|
35
|
+
#
|
36
|
+
# If we need something more complex aka roles/permissions we can do that in the same simple way
|
37
|
+
#
|
38
|
+
# class AdminDemo < Padrino::Application
|
39
|
+
# enable :authentication
|
40
|
+
# set :redirect_to_default, "/" # or your page
|
41
|
+
#
|
42
|
+
# access_control.roles_for :any do |role|
|
43
|
+
# role.allow "/sessions"
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# access_control.roles_for :admin do |role, account|
|
47
|
+
# role.allow "/"
|
48
|
+
# role.deny "/posts"
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# access_control.roles_for :editor do |role, account|
|
52
|
+
# role.allow "/posts"
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# If a user logged with role admin can:
|
57
|
+
#
|
58
|
+
# - Access to all paths that start with "/session" like "/sessions/{new,create}"
|
59
|
+
# - Access to any page except those that start with "/posts"
|
60
|
+
#
|
61
|
+
# If a user logged with role editor can:
|
62
|
+
#
|
63
|
+
# - Access to all paths that start with "/session" like "/sessions/{new,create}"
|
64
|
+
# - Access <tt>only</tt> to paths that start with "/posts" like "/post/{new,edit,destroy}"
|
65
|
+
#
|
66
|
+
# Finally we have another good fatures, the possibility in the same time we build role build also <tt>tree</tt>.
|
67
|
+
# Figure this scenario: in my admin every account need their own menu, so an Account with role editor have
|
68
|
+
# a menu different than an Account with role admin.
|
69
|
+
#
|
70
|
+
# So:
|
71
|
+
#
|
72
|
+
# class AdminDemo < Padrino::Application
|
73
|
+
# enable :authentication
|
74
|
+
# set :redirect_to_default, "/" # or your page
|
75
|
+
#
|
76
|
+
# access_control.roles_for :any do |role|
|
77
|
+
# role.allow "/sessions"
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# access_control.roles_for :admin do |role, current_account|
|
81
|
+
#
|
82
|
+
# role.project_module :settings do |project|
|
83
|
+
# project.menu :accounts, "/accounts" do |accounts|
|
84
|
+
# accounts.add :new, "/accounts/new" do |account|
|
85
|
+
# account.add :administrator, "/account/new/?role=administrator"
|
86
|
+
# account.add :editor, "/account/new/?role=editor"
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
# project.menu :spam_rules, "/manage_spam"
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# role.project_module :categories do |project|
|
93
|
+
# current_account.categories.each do |category|
|
94
|
+
# project.menu category.name, "/categories/#{category.id}.js"
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# access_control.roles_for :editor do |role, current_account|
|
100
|
+
#
|
101
|
+
# role.project_module :posts do |posts|
|
102
|
+
# post.menu :list, "/posts"
|
103
|
+
# post.menu :new, "/posts/new"
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# In this example when we build our menu tree we are also defining roles so:
|
108
|
+
#
|
109
|
+
# An Admin Account have access to:
|
110
|
+
#
|
111
|
+
# - All paths that start with "/sessions"
|
112
|
+
# - All paths that start with "/accounts"
|
113
|
+
# - All paths that start with "/manage_spam"
|
114
|
+
#
|
115
|
+
# An Editor Account have access to:
|
116
|
+
#
|
117
|
+
# - All paths that start with "/posts"
|
118
|
+
#
|
119
|
+
# Remember that you always deny a specific actions or allow globally others.
|
120
|
+
#
|
121
|
+
# Remember that when you define role_for :a_role, you have also access to the Model Account.
|
122
|
+
#
|
123
|
+
module AccessControl
|
124
|
+
|
125
|
+
def self.registered(app)
|
126
|
+
app.helpers Padrino::AccessControl::Helpers
|
127
|
+
app.before { login_required }
|
128
|
+
app.set :redirect_to_default, "/"
|
129
|
+
if app.respond_to?(:use_orm)
|
130
|
+
Padrino::Admin::Adapters.register(app.use_orm)
|
131
|
+
else
|
132
|
+
raise Padrino::ApplicationSetupError, "You must define in your app the setting :use_orm!"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Base
|
137
|
+
|
138
|
+
class << self
|
139
|
+
|
140
|
+
def inherited(base) #:nodoc:
|
141
|
+
base.class_eval("@@cache={}; @authorizations=[]; @roles=[]; @mappers=[]")
|
142
|
+
base.send(:cattr_reader, :cache)
|
143
|
+
super
|
144
|
+
end
|
145
|
+
|
146
|
+
# We map project modules for a given role or roles
|
147
|
+
def roles_for(*roles, &block)
|
148
|
+
raise Padrino::AccessControlError, "Role #{role} must be present and must be a symbol!" if roles.any? { |r| !r.kind_of?(Symbol) } || roles.empty?
|
149
|
+
raise Padrino::AccessControlError, "You can't merge :any with other roles" if roles.size > 1 && roles.any? { |r| r == :any }
|
150
|
+
|
151
|
+
if roles == [:any]
|
152
|
+
@authorizations << Authorization.new(&block)
|
153
|
+
else
|
154
|
+
raise Padrino::AccessControlError, "For use custom roles you need to define an Account Class" unless defined?(Account)
|
155
|
+
@roles.concat(roles)
|
156
|
+
@mappers << Proc.new { |account| Mapper.new(account, *roles, &block) }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns (allowed && denied paths).
|
161
|
+
# If an account given we also give allowed & denied paths for their role.
|
162
|
+
def auths(account=nil)
|
163
|
+
if account
|
164
|
+
cache[account.id] ||= Auths.new(@authorizations, @mappers, account)
|
165
|
+
else
|
166
|
+
cache[:any] ||= Auths.new(@authorizations)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class Auths
|
173
|
+
attr_reader :allowed, :denied, :project_modules
|
174
|
+
|
175
|
+
def initialize(authorizations, mappers=nil, account=nil)
|
176
|
+
@allowed, @denied = [], []
|
177
|
+
unless authorizations.empty?
|
178
|
+
@allowed = authorizations.collect(&:allowed).flatten
|
179
|
+
@denied = authorizations.collect(&:denied).flatten
|
180
|
+
end
|
181
|
+
if mappers && !mappers.empty?
|
182
|
+
maps = mappers.collect { |m| m.call(account) }.reject { |m| !m.allowed? }
|
183
|
+
@allowed.concat(maps.collect(&:allowed).flatten)
|
184
|
+
@denied.concat(maps.collect(&:denied).flatten)
|
185
|
+
@project_modules = maps.collect(&:project_modules).flatten.uniq
|
186
|
+
else
|
187
|
+
@project_modules = []
|
188
|
+
end
|
189
|
+
@allowed.uniq!
|
190
|
+
@denied.uniq!
|
191
|
+
end
|
192
|
+
|
193
|
+
def can?(request_path)
|
194
|
+
return true if @allowed.empty?
|
195
|
+
@allowed.any? { |path| request_path =~ /^#{path}/ } && !cannot?(request_path)
|
196
|
+
end
|
197
|
+
|
198
|
+
def cannot?(request_path)
|
199
|
+
return false if @denied.empty?
|
200
|
+
@denied.any? { |path| request_path =~ /^#{path}/ }
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
class Authorization
|
206
|
+
attr_reader :allowed, :denied
|
207
|
+
|
208
|
+
def initialize(&block)
|
209
|
+
@allowed = []
|
210
|
+
@denied = []
|
211
|
+
yield self
|
212
|
+
end
|
213
|
+
|
214
|
+
def allow(path)
|
215
|
+
@allowed << path unless @allowed.include?(path)
|
216
|
+
end
|
217
|
+
|
218
|
+
def require_login(path)
|
219
|
+
@denied << path unless @denied.include?(path)
|
220
|
+
end
|
221
|
+
alias :deny :require_login
|
222
|
+
end
|
223
|
+
|
224
|
+
class Mapper
|
225
|
+
attr_reader :project_modules, :roles, :denied
|
226
|
+
|
227
|
+
def initialize(account, *roles, &block) #:nodoc:
|
228
|
+
@project_modules = []
|
229
|
+
@allowed = []
|
230
|
+
@denied = []
|
231
|
+
@roles = roles
|
232
|
+
@account = account.dup
|
233
|
+
yield(self, @account)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Create a new project module
|
237
|
+
def project_module(name, path=nil, &block)
|
238
|
+
@project_modules << ProjectModule.new(name, path, &block)
|
239
|
+
end
|
240
|
+
|
241
|
+
# Globally allow an paths for the current role
|
242
|
+
def allow(path)
|
243
|
+
@allowed << path unless @allowed.include?(path)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Globally deny an pathsfor the current role
|
247
|
+
def deny(path)
|
248
|
+
@denied << path unless @allowed.include?(path)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Return true if role is included in given roles
|
252
|
+
def allowed?
|
253
|
+
@roles.any? { |r| r == @account.role.to_s.downcase.to_sym }
|
254
|
+
end
|
255
|
+
|
256
|
+
# Return allowed paths
|
257
|
+
def allowed
|
258
|
+
@project_modules.each { |pm| @allowed.concat(pm.allowed) }
|
259
|
+
@allowed.uniq
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
class ProjectModule
|
264
|
+
attr_reader :name, :menus, :path
|
265
|
+
|
266
|
+
def initialize(name, path=nil, options={}, &block)#:nodoc:
|
267
|
+
@name = name
|
268
|
+
@options = options
|
269
|
+
@allowed = []
|
270
|
+
@menus = []
|
271
|
+
@path = path
|
272
|
+
@allowed << path if path
|
273
|
+
yield self
|
274
|
+
end
|
275
|
+
|
276
|
+
# Build a new menu and automaitcally add the action on the allowed actions.
|
277
|
+
def menu(name, path=nil, options={}, &block)
|
278
|
+
@menus << Menu.new(name, path, options, &block)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Return allowed controllers
|
282
|
+
def allowed
|
283
|
+
@menus.each { |m| @allowed.concat(m.allowed) }
|
284
|
+
@allowed.uniq
|
285
|
+
end
|
286
|
+
|
287
|
+
# Return the original name or try to translate or humanize the symbol
|
288
|
+
def human_name
|
289
|
+
@name.is_a?(Symbol) ? I18n.t("admin.menus.#{@name}", :default => @name.to_s.humanize) : @name
|
290
|
+
end
|
291
|
+
|
292
|
+
# Return symbol for the given project module
|
293
|
+
def uid
|
294
|
+
@name.to_s.downcase.gsub(/[^a-z0-9]+/, '').gsub(/-+$/, '').gsub(/^-+$/, '').to_sym
|
295
|
+
end
|
296
|
+
|
297
|
+
# Return ExtJs Config for this project module
|
298
|
+
def config
|
299
|
+
options = @options.merge(:text => human_name)
|
300
|
+
options.merge!(:menu => @menus.collect(&:config)) if @menus.size > 0
|
301
|
+
options.merge!(:handler => ExtJs::Variable.new("function(){ Admin.app.load('#{path}') }")) if @path
|
302
|
+
options
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
class Menu
|
307
|
+
attr_reader :name, :options, :items, :path
|
308
|
+
|
309
|
+
def initialize(name, path=nil, options={}, &block) #:nodoc:
|
310
|
+
@name = name
|
311
|
+
@path = path
|
312
|
+
@options = options
|
313
|
+
@allowed = []
|
314
|
+
@items = []
|
315
|
+
@allowed << path if path
|
316
|
+
yield self if block_given?
|
317
|
+
end
|
318
|
+
|
319
|
+
# Add a new submenu to the menu
|
320
|
+
def add(name, path=nil, options={}, &block)
|
321
|
+
@items << Menu.new(name, path, options, &block)
|
322
|
+
end
|
323
|
+
|
324
|
+
# Return allowed controllers
|
325
|
+
def allowed
|
326
|
+
@items.each { |i| @allowed.concat(i.allowed) }
|
327
|
+
@allowed.uniq
|
328
|
+
end
|
329
|
+
|
330
|
+
# Return the original name or try to translate or humanize the symbol
|
331
|
+
def human_name
|
332
|
+
@name.is_a?(Symbol) ? I18n.t("admin.menus.#{@name}", :default => @name.to_s.humanize) : @name
|
333
|
+
end
|
334
|
+
|
335
|
+
# Return a unique id for the given project module
|
336
|
+
def uid
|
337
|
+
@name.to_s.downcase.gsub(/[^a-z0-9]+/, '').gsub(/-+$/, '').gsub(/^-+$/, '').to_sym
|
338
|
+
end
|
339
|
+
|
340
|
+
# Return ExtJs Config for this menu
|
341
|
+
def config
|
342
|
+
if @path.blank? && @items.empty?
|
343
|
+
options = human_name
|
344
|
+
else
|
345
|
+
options = @options.merge(:text => human_name)
|
346
|
+
options.merge!(:menu => @items.collect(&:config)) if @items.size > 0
|
347
|
+
options.merge!(:handler => ExtJs::Variable.new("function(){ Admin.app.load('#{path}') }")) if @path
|
348
|
+
end
|
349
|
+
options
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Padrino
|
2
|
+
module AccessControl
|
3
|
+
module Helpers
|
4
|
+
|
5
|
+
# Returns true if <tt>current_account</tt> is logged and active.
|
6
|
+
def logged_in?
|
7
|
+
!current_account.nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the current_account, it's an instance of <tt>Account</tt> model
|
11
|
+
def current_account
|
12
|
+
@current_account ||= login_from_session
|
13
|
+
end
|
14
|
+
|
15
|
+
# Ovverride the current_account, you must provide an instance of Account Model
|
16
|
+
#
|
17
|
+
# Examples:
|
18
|
+
#
|
19
|
+
# current_account = Account.last
|
20
|
+
#
|
21
|
+
def set_current_account(account)
|
22
|
+
session[session_name] = account.id rescue nil
|
23
|
+
@current_account = account
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if the <tt>current_account</tt> is allowed to see the requested path
|
27
|
+
#
|
28
|
+
# For configure this role please refer to: <tt>Padrino::AccessControl::Base</tt>
|
29
|
+
def allowed?
|
30
|
+
access_control.auths(current_account).can?(request.path_info)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns a helper to pass in a <tt>before_filter</tt> for check if
|
34
|
+
# an account are: <tt>logged_in?</tt> and <tt>allowed?</tt>
|
35
|
+
#
|
36
|
+
# By default this method is used in BackendController so is not necessary
|
37
|
+
def login_required
|
38
|
+
store_location if options.respond_to?(:redirect_back_or_default)
|
39
|
+
return access_denied unless allowed?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Store in session[:return_to] the request.fullpath
|
43
|
+
def store_location
|
44
|
+
session[:return_to] = request.fullpath
|
45
|
+
end
|
46
|
+
|
47
|
+
# Redirect the account to the page that requested an authentication or
|
48
|
+
# if the account is not allowed/logged return it to a default page
|
49
|
+
def redirect_back_or_default(default)
|
50
|
+
redirect_to(session[:return_to] || default)
|
51
|
+
session[:return_to] = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def access_denied #:nodoc:
|
56
|
+
# If request a javascript we alert the user
|
57
|
+
if request.xhr?
|
58
|
+
"alert('You don\'t have permission for this resource')"
|
59
|
+
# If we request a normal page we redirect and we store request.full_path
|
60
|
+
elsif options.respond_to?(:redirect_back_or_default)
|
61
|
+
redirect_back_or_default(options.redirect_back_or_default)
|
62
|
+
# If we don't want redirect a user to back but always to a path
|
63
|
+
elsif options.respond_to?(:redirect_to_default)
|
64
|
+
redirect(options.redirect_to_default)
|
65
|
+
# If no match we halt with 401
|
66
|
+
else
|
67
|
+
halt 401, "You don't have permission for this resource"
|
68
|
+
end
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def session_name
|
73
|
+
options.app_name.to_sym
|
74
|
+
end
|
75
|
+
|
76
|
+
def login_from_session #:nodoc:
|
77
|
+
Account.first(:conditions => { :id => session[session_name] }) if defined?(Account)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|