padrino-admin 0.2.9 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
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 "sinatra", ">= 0.9.2"
14
- gem.add_runtime_dependency "padrino-core", ">= 0.1.1"
15
- gem.add_development_dependency "haml", ">= 2.2.1"
16
- gem.add_development_dependency "shoulda", ">= 0"
17
- gem.add_development_dependency "mocha", ">= 0.9.7"
18
- gem.add_development_dependency "rack-test", ">= 0.5.0"
19
- gem.add_development_dependency "webrat", ">= 0.5.1"
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 << 'lib' << 'test'
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.2.9
1
+ 0.4.5
data/lib/padrino-admin.rb CHANGED
@@ -1,2 +1,11 @@
1
- require 'padrino-gen'
2
- Dir[File.dirname(__FILE__) + '/padrino-admin/**/*.rb'].each {|file| require file }
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