action_access 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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>.
|