action_access 0.0.1

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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/CONTRIBUTING.md +23 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +87 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.md +283 -0
  8. data/Rakefile +28 -0
  9. data/action_access.gemspec +24 -0
  10. data/lib/action_access.rb +14 -0
  11. data/lib/action_access/controller_additions.rb +75 -0
  12. data/lib/action_access/keeper.rb +90 -0
  13. data/lib/action_access/model_additions.rb +14 -0
  14. data/lib/action_access/railtie.rb +19 -0
  15. data/lib/action_access/user_utilities.rb +39 -0
  16. data/lib/action_access/version.rb +3 -0
  17. data/test/action_access_test.rb +21 -0
  18. data/test/controllers/articles_controller_test.rb +41 -0
  19. data/test/controllers/secrets_controller_test.rb +10 -0
  20. data/test/controllers/static_controller_test.rb +15 -0
  21. data/test/dummy/README.rdoc +28 -0
  22. data/test/dummy/Rakefile +6 -0
  23. data/test/dummy/app/assets/images/.keep +0 -0
  24. data/test/dummy/app/assets/javascripts/application.js +13 -0
  25. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  26. data/test/dummy/app/controllers/application_controller.rb +15 -0
  27. data/test/dummy/app/controllers/articles_controller.rb +36 -0
  28. data/test/dummy/app/controllers/concerns/.keep +0 -0
  29. data/test/dummy/app/controllers/secrets_controller.rb +4 -0
  30. data/test/dummy/app/controllers/static_controller.rb +6 -0
  31. data/test/dummy/app/helpers/application_helper.rb +2 -0
  32. data/test/dummy/app/mailers/.keep +0 -0
  33. data/test/dummy/app/models/.keep +0 -0
  34. data/test/dummy/app/models/concerns/.keep +0 -0
  35. data/test/dummy/app/models/user.rb +7 -0
  36. data/test/dummy/app/views/articles/edit.html.erb +1 -0
  37. data/test/dummy/app/views/articles/index.html.erb +1 -0
  38. data/test/dummy/app/views/articles/new.html.erb +1 -0
  39. data/test/dummy/app/views/articles/show.html.erb +3 -0
  40. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  41. data/test/dummy/app/views/static/home.html.erb +1 -0
  42. data/test/dummy/bin/bundle +3 -0
  43. data/test/dummy/bin/rails +4 -0
  44. data/test/dummy/bin/rake +4 -0
  45. data/test/dummy/config.ru +4 -0
  46. data/test/dummy/config/application.rb +23 -0
  47. data/test/dummy/config/boot.rb +5 -0
  48. data/test/dummy/config/database.yml +25 -0
  49. data/test/dummy/config/environment.rb +5 -0
  50. data/test/dummy/config/environments/development.rb +37 -0
  51. data/test/dummy/config/environments/production.rb +78 -0
  52. data/test/dummy/config/environments/test.rb +39 -0
  53. data/test/dummy/config/initializers/assets.rb +8 -0
  54. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  55. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  56. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  57. data/test/dummy/config/initializers/inflections.rb +16 -0
  58. data/test/dummy/config/initializers/mime_types.rb +4 -0
  59. data/test/dummy/config/initializers/session_store.rb +3 -0
  60. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  61. data/test/dummy/config/locales/en.yml +23 -0
  62. data/test/dummy/config/routes.rb +6 -0
  63. data/test/dummy/config/secrets.yml +22 -0
  64. data/test/dummy/db/migrate/20140926071026_create_users.rb +10 -0
  65. data/test/dummy/db/schema.rb +23 -0
  66. data/test/dummy/lib/assets/.keep +0 -0
  67. data/test/dummy/log/.keep +0 -0
  68. data/test/dummy/public/404.html +67 -0
  69. data/test/dummy/public/422.html +67 -0
  70. data/test/dummy/public/500.html +66 -0
  71. data/test/dummy/public/favicon.ico +0 -0
  72. data/test/models/user_test.rb +22 -0
  73. data/test/support/compact_environment.rb +9 -0
  74. data/test/test_helper.rb +8 -0
  75. metadata +203 -0
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ require 'bundler/gem_tasks'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require 'rdoc/task'
9
+ require 'rake/testtask'
10
+
11
+
12
+ RDoc::Task.new(:rdoc) do |rdoc|
13
+ rdoc.rdoc_dir = 'rdoc'
14
+ rdoc.title = 'ActionAccess'
15
+ rdoc.options << '--line-numbers'
16
+ rdoc.rdoc_files.include('README.md')
17
+ rdoc.rdoc_files.include('lib/**/*.rb')
18
+ end
19
+
20
+ Rake::TestTask.new(:test) do |t|
21
+ t.libs << 'lib'
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+
28
+ task default: :test
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'action_access/version'
4
+
5
+ # Gem description and dependencies
6
+ Gem::Specification.new do |s|
7
+ s.name = 'action_access'
8
+ s.version = ActionAccess::VERSION
9
+ s.authors = ['Matías A. Gagliano']
10
+ s.email = ['matias.gagliano@gmail.com']
11
+ s.homepage = 'https://github.com/matiasgagliano/action_access'
12
+ s.summary = 'Access control system for Ruby on Rails.'
13
+ s.description = 'Easy and modular way to secure applications and handle permissions.'
14
+ s.license = 'MIT'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- test/*`.split("\n")
18
+ s.require_paths = ['lib']
19
+
20
+ s.add_dependency 'rails', '~> 4.1'
21
+
22
+ s.add_development_dependency 'sqlite3'
23
+ end
24
+
@@ -0,0 +1,14 @@
1
+ require 'singleton'
2
+ require 'action_access/version'
3
+ require 'action_access/railtie'
4
+
5
+ module ActionAccess
6
+ extend ActiveSupport::Autoload
7
+
8
+ eager_autoload do
9
+ autoload :Keeper
10
+ autoload :ControllerAdditions
11
+ autoload :ModelAdditions
12
+ autoload :UserUtilities
13
+ end
14
+ end
@@ -0,0 +1,75 @@
1
+ module ActionAccess
2
+ module ControllerAdditions
3
+ module ClassMethods
4
+ # Lock actions by default, they won't be accessible unless authorized.
5
+ # It takes the same options as filter callbacks.
6
+ def lock_access(options = {})
7
+ before_action :validate_access!, options
8
+ end
9
+
10
+ # Is this controller locked?
11
+ def access_locked?
12
+ filters = _process_action_callbacks.collect(&:filter)
13
+ :validate_access!.in? filters
14
+ end
15
+
16
+ # Set an access rule for the current controller.
17
+ # It will automatically lock the controller if it wasn't already.
18
+ #
19
+ # == Example:
20
+ # Add the following to ArticlesController to allow admins to edit articles.
21
+ # let :admin, [:edit, :update]
22
+ #
23
+ def let(clearance_level, permissions)
24
+ lock_access unless access_locked?
25
+ keeper = ActionAccess::Keeper.instance
26
+ keeper.let clearance_level, permissions, self
27
+ end
28
+ end
29
+
30
+
31
+ def self.included(base)
32
+ base.extend ClassMethods
33
+ base.helper_method :keeper
34
+ end
35
+
36
+
37
+ private
38
+
39
+ # Helper to access Keeper's instance.
40
+ def keeper
41
+ ActionAccess::Keeper.instance
42
+ end
43
+
44
+ # Clearance level of the current user (override to customize).
45
+ def current_clearance_level
46
+ if defined? current_user and current_user.respond_to?(:clearance_level)
47
+ current_user.clearance_level.to_s.to_sym
48
+ else
49
+ :guest
50
+ end
51
+ end
52
+
53
+ # Default path to redirect any non authorized access (override to customize).
54
+ def unauthorized_access_redirection_path
55
+ root_path
56
+ end
57
+
58
+ # Validate access to the current route.
59
+ def validate_access!
60
+ clearance_level = current_clearance_level
61
+ action = self.action_name
62
+ not_authorized! unless keeper.lets? clearance_level, action, self.class
63
+ end
64
+
65
+ # Redirect if not authorized.
66
+ # May be used inside action methods for finer control.
67
+ def not_authorized!(*args)
68
+ options = args.extract_options!
69
+ message = options[:message] ||
70
+ I18n.t('action_access.redirection_message', default: 'Not authorized.')
71
+ path = options[:path] || unauthorized_access_redirection_path
72
+ redirect_to path, alert: message
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,90 @@
1
+ module ActionAccess
2
+ class Keeper
3
+ include Singleton
4
+
5
+ def initialize
6
+ @rules = {}
7
+ end
8
+
9
+ # Set clearance to perform actions over a resource.
10
+ #
11
+ # Clearance level and resource can be either plural or singular.
12
+ #
13
+ # == Examples:
14
+ # let :user, :show, :profile
15
+ # let :user, :show, @profile
16
+ # let :user, :show, ProfilesController
17
+ # # Any user can can access 'profiles#show'.
18
+ #
19
+ # let :admins, [:edit, :update], :articles, namespace: :admin
20
+ # let :admins, [:edit, :update], @admin_article
21
+ # let :admins, [:edit, :update], Admin::ArticlesController
22
+ # # Admins can access 'admin/articles#edit' and 'admin/articles#update'.
23
+ #
24
+ def let(clearance_level, actions, resource, options = {})
25
+ clearance_level = clearance_level.to_s.singularize.to_sym
26
+ actions = Array(actions).map(&:to_sym)
27
+ controller = get_controller_name(resource, options)
28
+ @rules[controller] ||= {}
29
+ @rules[controller][clearance_level] = actions
30
+ return nil
31
+ end
32
+
33
+ # Check if a given clearance level allows to perform certain action on a resource.
34
+ #
35
+ # Clearance level and resource can be either plural or singular.
36
+ #
37
+ # Examples:
38
+ # lets? :users, :create, :profiles
39
+ # lets? :users, :create, @profile
40
+ # lets? :users, :create, ProfilesController
41
+ # # True if users are allowed to access 'profiles#create'.
42
+ #
43
+ # lets? :admin, :edit, :article, namespace: :admin
44
+ # lets? :admin, :edit, @admin_article
45
+ # lets? :admin, :edit, Admin::ArticlesController
46
+ # # True if any admin is allowed to access 'admin/articles#edit'.
47
+ #
48
+ def lets?(clearance_level, action, resource, options = {})
49
+ clearance_level = clearance_level.to_s.singularize.to_sym
50
+ action = action.to_sym
51
+ controller = get_controller_name(resource, options)
52
+
53
+ # Load the controller to ensure its rules are loaded (lazy loading rules).
54
+ controller.constantize.new
55
+ rules = @rules[controller]
56
+ return false unless rules
57
+
58
+ # Check rules
59
+ Array(rules[:all]).include?(:all) ||
60
+ Array(rules[:all]).include?(action) ||
61
+ Array(rules[clearance_level]).include?(:all) ||
62
+ Array(rules[clearance_level]).include?(action)
63
+ end
64
+
65
+
66
+ private
67
+
68
+ def get_controller_name(resource, options = {})
69
+ # Assume a controller if given a class
70
+ return resource.name if resource.is_a? Class
71
+
72
+ # Assume a model instance if not a string or symbol
73
+ unless resource.is_a?(String) || resource.is_a?(Symbol)
74
+ resource = resource.class.name
75
+ end
76
+
77
+ # Build controller name
78
+ path = options[:namespace].to_s.split(/::|\//).reject(&:blank?).map(&:camelize)
79
+ path << resource.to_s.camelize.pluralize + 'Controller'
80
+ controller = path.join('::')
81
+
82
+ # Make sure that the controller exists.
83
+ # Will throw a NameError exception if resource and/or namespace are wrong.
84
+ controller.constantize
85
+
86
+ # Return controller name
87
+ return controller
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,14 @@
1
+ module ActionAccess
2
+ module ModelAdditions
3
+ module ClassMethods
4
+ # Add Action Access user related utilities to the current model.
5
+ def add_access_utilities
6
+ include ActionAccess::UserUtilities
7
+ end
8
+ end
9
+
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module ActionAccess
2
+ class Railtie < Rails::Railtie
3
+ config.eager_load_namespaces << ActionAccess
4
+
5
+ initializer 'action_access.controller_additions' do
6
+ # Extend ActionController::Base
7
+ ActiveSupport.on_load :action_controller do
8
+ include ControllerAdditions
9
+ end
10
+ end
11
+
12
+ initializer 'action_access.model_additions' do
13
+ # Extend ActiveRecord::Base
14
+ ActiveSupport.on_load :active_record do
15
+ include ModelAdditions
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ module ActionAccess
2
+ module UserUtilities
3
+ # Check if the user is authorized to perform a given action.
4
+ #
5
+ # Resource can be either plural or singular.
6
+ #
7
+ # == Examples:
8
+ # user.can? :show, :articles
9
+ # user.can? :show, @article
10
+ # user.can? :show, ArticlesController
11
+ # # True if the user's clearance level allows to access 'articles#show'
12
+ #
13
+ # user.can? :edit, :articles, namespace: :admin
14
+ # user.can? :edit, @admin_article
15
+ # user.can? :edit, Admin::ArticlesController
16
+ # # True if the user's clearance level allows to access 'admin/articles#edit'
17
+ #
18
+ def can?(action, resource, options = {})
19
+ keeper = ActionAccess::Keeper.instance
20
+ keeper.lets? clearance_level, action, resource, options
21
+ end
22
+
23
+
24
+ private
25
+
26
+ # Accessor for the user's clearance level.
27
+ #
28
+ # Must be overridden to set the proper clearance level.
29
+ #
30
+ # == Example:
31
+ # def clearance_level
32
+ # role.name
33
+ # end
34
+ #
35
+ def clearance_level
36
+ :guest
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module ActionAccess
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,21 @@
1
+ require 'test_helper'
2
+ require 'support/compact_environment'
3
+
4
+ class ActionAccessTest < ActiveSupport::TestCase
5
+ test "unauthorized accesses aren't allowed" do
6
+ assert_equal false, keeper.lets?(:user, :edit, :posts)
7
+ assert_equal false, keeper.lets?(:user, :edit, :posts, namespace: :admin)
8
+ end
9
+
10
+ test "authorized accesses are allowed" do
11
+ keeper.let :editor, [:edit, :update], :posts
12
+ assert_equal true, keeper.lets?(:editor, :edit, :posts)
13
+ assert_equal true, keeper.lets?(:editor, :update, :posts)
14
+ end
15
+
16
+ test "authorized accesses within namespaces are allowed" do
17
+ keeper.let :admin, [:new, :create], :posts, namespace: :admin
18
+ assert_equal true, keeper.lets?(:admin, :new, :posts, namespace: :admin)
19
+ assert_equal true, keeper.lets?(:admin, :create, :posts, namespace: :admin)
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+
3
+ class ArticlesControllerTest < ActionController::TestCase
4
+ test "any access with no clearance level gets redirected" do
5
+ # Undefined role
6
+ get :new
7
+ assert_redirected_to root_url
8
+
9
+ post :create
10
+ assert_redirected_to root_url
11
+ end
12
+
13
+ test "any access with undefined clearance level gets redirected" do
14
+ # The role doesn't exist (undefined)
15
+ get :new, nil, {role: :super}
16
+ assert_redirected_to root_url
17
+
18
+ post :create, nil, {role: :super}
19
+ assert_redirected_to root_url
20
+ end
21
+
22
+ test "any unauthorized access gets redirected" do
23
+ # The roles exist but aren't authorized.
24
+ get :new, nil, {role: :user}
25
+ assert_redirected_to root_url
26
+
27
+ post :create, nil, {role: :editor}
28
+ assert_redirected_to root_url
29
+ end
30
+
31
+ test "authorized accesses aren't redirected" do
32
+ get :new, nil, {role: :admin} # Admins can create articles
33
+ assert_response :success
34
+
35
+ get :edit, {id: 1}, {role: :editor} # Editors can edit articles
36
+ assert_response :success
37
+
38
+ get :show, {id: 1}, {role: :user} # Users can view articles
39
+ assert_response :success
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+
3
+ class SecretsControllerTest < ActionController::TestCase
4
+ test "controllers are locked by default" do
5
+ # Test that the "lock_access" call in ApplicationController works properly.
6
+ # There are no access rules in SecretsController but it should be locked anyway.
7
+ get :index, nil, {role: :admin}
8
+ assert_redirected_to root_url
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class StaticControllerTest < ActionController::TestCase
4
+ # Test that the :all key works properly
5
+ test "anyone can do anything" do
6
+ get :home, nil, {role: :admin}
7
+ assert_response :success
8
+
9
+ get :home, nil, {role: :undefined}
10
+ assert_response :success
11
+
12
+ get :home
13
+ assert_response :success
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.