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 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