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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/CONTRIBUTING.md +23 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +87 -0
- data/LICENSE.txt +20 -0
- data/README.md +283 -0
- data/Rakefile +28 -0
- data/action_access.gemspec +24 -0
- data/lib/action_access.rb +14 -0
- data/lib/action_access/controller_additions.rb +75 -0
- data/lib/action_access/keeper.rb +90 -0
- data/lib/action_access/model_additions.rb +14 -0
- data/lib/action_access/railtie.rb +19 -0
- data/lib/action_access/user_utilities.rb +39 -0
- data/lib/action_access/version.rb +3 -0
- data/test/action_access_test.rb +21 -0
- data/test/controllers/articles_controller_test.rb +41 -0
- data/test/controllers/secrets_controller_test.rb +10 -0
- data/test/controllers/static_controller_test.rb +15 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +15 -0
- data/test/dummy/app/controllers/articles_controller.rb +36 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/secrets_controller.rb +4 -0
- data/test/dummy/app/controllers/static_controller.rb +6 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/mailers/.keep +0 -0
- data/test/dummy/app/models/.keep +0 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/user.rb +7 -0
- data/test/dummy/app/views/articles/edit.html.erb +1 -0
- data/test/dummy/app/views/articles/index.html.erb +1 -0
- data/test/dummy/app/views/articles/new.html.erb +1 -0
- data/test/dummy/app/views/articles/show.html.erb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/static/home.html.erb +1 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +78 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/assets.rb +8 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/migrate/20140926071026_create_users.rb +10 -0
- data/test/dummy/db/schema.rb +23 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/models/user_test.rb +22 -0
- data/test/support/compact_environment.rb +9 -0
- data/test/test_helper.rb +8 -0
- 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,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>.
|