tight-engine 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 +2 -0
- data/LICENSE +20 -0
- data/README.md +4 -0
- data/Rakefile +15 -0
- data/lib/tight/version.rb +3 -0
- data/lib/tight-auth/access.rb +148 -0
- data/lib/tight-auth/login/controller.rb +20 -0
- data/lib/tight-auth/login/layout.slim +10 -0
- data/lib/tight-auth/login/new.slim +37 -0
- data/lib/tight-auth/login.rb +138 -0
- data/lib/tight-auth/permissions.rb +180 -0
- data/lib/tight-auth.rb +10 -0
- data/test/auth_helper.rb +83 -0
- data/test/test_padrino_access.rb +124 -0
- data/test/test_padrino_auth.rb +38 -0
- data/test/test_padrino_login.rb +76 -0
- data/tight-engine.gemspec +23 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 065ac37dc49f5b5e67ec4271bb92f71eee632e0f
|
4
|
+
data.tar.gz: 72dfcdf5eae3c379407c7c698293515e59822db8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 50b1e579488cad7dc14f878f8e2605e8d1837500de6e980aa175f14839693e7027d5df4dd078ef2d86a085e1714ee0f94988976cb39ba56b547dfad6a8b00ea9
|
7
|
+
data.tar.gz: 0bc29fa1374faeb73b5949bc6f504f613fad53d39eac471350c69dfae7c6a78ae94e5429a6a927f3aba1eb4c6bb6255791c3112cd72c175f05f231448a13d3eb
|
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Igor Bochkariov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
RAKE_ROOT = __FILE__
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'bundler/gem_tasks'
|
7
|
+
require 'minitest/autorun'
|
8
|
+
|
9
|
+
Rake::TestTask.new(:test) do |test|
|
10
|
+
test.libs << 'test'
|
11
|
+
test.test_files = Dir['test/**/test_*.rb']
|
12
|
+
test.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :test
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'tight-auth/permissions'
|
2
|
+
|
3
|
+
module Tight
|
4
|
+
##
|
5
|
+
# Tight authorization module.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Nifty::Application < Tight::Application
|
9
|
+
# # optional settings
|
10
|
+
# set :credentials_reader, :visitor # the name of getter method in helpers
|
11
|
+
# # required statement
|
12
|
+
# register Tight::Access
|
13
|
+
# # example persistance storage
|
14
|
+
# enable :sessions
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # optional helpers
|
18
|
+
# Nifty::Application.helpers do
|
19
|
+
# def visitor
|
20
|
+
# session[:visitor] ||= Visitor.guest_account
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # example visitor model
|
25
|
+
# module Visitor
|
26
|
+
# extend self
|
27
|
+
# def guest_account
|
28
|
+
# OpenStruct.new(:role => :guest, :id => 1)
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # example controllers
|
33
|
+
# Nifty::Application.controller :public_area do
|
34
|
+
# set_access :*
|
35
|
+
# get(:index){ 'public content' }
|
36
|
+
# end
|
37
|
+
# Nifty::Application.controller :members_area do
|
38
|
+
# set_access :member
|
39
|
+
# get(:index){ 'secret content' }
|
40
|
+
# end
|
41
|
+
# Nifty::Application.controller :login do
|
42
|
+
# set_access :*
|
43
|
+
# get(:index){ session[:visitor] = OpenStruct.new(:role => :guest, :id => 1) }
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
module Access
|
47
|
+
class << self
|
48
|
+
def registered(app)
|
49
|
+
included(app)
|
50
|
+
app.default(:credentials_reader, :credentials)
|
51
|
+
app.default(:access_errors, true)
|
52
|
+
app.send :attr_reader, app.credentials_reader unless app.instance_methods.include?(app.credentials_reader)
|
53
|
+
app.set :permissions, Permissions.new
|
54
|
+
app.login_permissions if app.respond_to?(:login_permissions)
|
55
|
+
app.before do
|
56
|
+
authorized? || error(403, '403 Forbidden')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def included(base)
|
61
|
+
base.send(:include, InstanceMethods)
|
62
|
+
base.extend(ClassMethods)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module ClassMethods
|
67
|
+
##
|
68
|
+
# Empties the list of permission.
|
69
|
+
#
|
70
|
+
def reset_access!
|
71
|
+
permissions.clear!
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Allows access to action with objects.
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# # in application
|
79
|
+
# set_access :*, :with => :login # allows everyone to interact with :login controller
|
80
|
+
# # in controller
|
81
|
+
# App.controller :members_area do
|
82
|
+
# set_access :member # allows all members to access :members_area controller
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
def set_access(*args)
|
86
|
+
options = args.extract_options!
|
87
|
+
options[:object] ||= Array(@_controller).first.to_s.singularize.to_sym if @_controller.present?
|
88
|
+
permissions.add(*args, options)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module InstanceMethods
|
93
|
+
##
|
94
|
+
# Checks if current visitor has access to current action with current controller.
|
95
|
+
#
|
96
|
+
def authorized?
|
97
|
+
access_action?
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Returns current visitor.
|
102
|
+
#
|
103
|
+
def access_subject
|
104
|
+
send settings.credentials_reader
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Checks if current visitor is one of the specified roles. Can accept a block.
|
109
|
+
#
|
110
|
+
def access_role?(*roles, &block)
|
111
|
+
settings.permissions.check(access_subject, :have => roles, &block)
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Checks if current visitor is allowed to to the action with object. Can accept a block.
|
116
|
+
#
|
117
|
+
def access_action?(action = nil, object = nil, &block)
|
118
|
+
return true if response.status/100 == 4 && settings.access_errors
|
119
|
+
if respond_to?(:request) && action.nil? && object.nil?
|
120
|
+
object = request.controller
|
121
|
+
action = request.action
|
122
|
+
if object.nil? && action.present? && action.to_s.index('/')
|
123
|
+
object, action = request.env['PATH_INFO'].to_s.scan(/\/([^\/]*)/).map(&:first)
|
124
|
+
end
|
125
|
+
object ||= :''
|
126
|
+
action ||= :index
|
127
|
+
object = object.to_sym
|
128
|
+
action = action.to_sym
|
129
|
+
end
|
130
|
+
settings.permissions.check(access_subject, :allow => action, :with => object, &block)
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Check if current visitor is allowed to interact with object by action. Can accept a block.
|
135
|
+
#
|
136
|
+
def access_object?(object = nil, action = nil, &block)
|
137
|
+
allow_action action, object, &block
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Populates the list of objects the current visitor is allowed to interact with.
|
142
|
+
#
|
143
|
+
def access_objects(subject = access_subject, action = nil)
|
144
|
+
settings.permissions.find_objects(subject, action)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Tight
|
2
|
+
module Login
|
3
|
+
module Controller
|
4
|
+
def self.included(base)
|
5
|
+
base.get :index do
|
6
|
+
render :slim, :"new", :layout => :"layout", :views => File.dirname(__FILE__)
|
7
|
+
end
|
8
|
+
base.post :index do
|
9
|
+
if authenticate
|
10
|
+
restore_location
|
11
|
+
else
|
12
|
+
params.delete 'password'
|
13
|
+
flash.now[:error] = 'Wrong password'
|
14
|
+
render :slim, :"new", :layout => :"layout", :views => File.dirname(__FILE__)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
meta charset="utf-8"
|
5
|
+
meta name="robots" content="noindex"
|
6
|
+
title Padrino::Login
|
7
|
+
link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/2.3.2/css/bootstrap.min.css"
|
8
|
+
body
|
9
|
+
.container.login style='width: 287px'
|
10
|
+
= yield
|
@@ -0,0 +1,37 @@
|
|
1
|
+
h3
|
2
|
+
| Login
|
3
|
+
br
|
4
|
+
small
|
5
|
+
a href=url('/') = request.env['HTTP_HOST']
|
6
|
+
|
7
|
+
form.form-horizontal.well action=''
|
8
|
+
- [:error, :warning, :success, :notice].each do |type|
|
9
|
+
- next if flash[type].blank?
|
10
|
+
.alert.alert-message class=('alert-' + (type == :notice ? :info : type).to_s) data-alert=true
|
11
|
+
= flash[type]
|
12
|
+
|
13
|
+
legend Social
|
14
|
+
.control-group
|
15
|
+
a href=url('/oauth/google')
|
16
|
+
img src='/images/social/google.png'
|
17
|
+
|
18
|
+
legend Obsolete
|
19
|
+
.control-group
|
20
|
+
.input-prepend
|
21
|
+
span.add-on
|
22
|
+
i.icon-envelope
|
23
|
+
input type=:text name=:email value=params[:email] placeholder='email'
|
24
|
+
.control-group
|
25
|
+
.input-prepend
|
26
|
+
span.add-on
|
27
|
+
i.icon-lock
|
28
|
+
input type=:password name=:password value=params[:password] placeholder='password'
|
29
|
+
.control-group
|
30
|
+
input.btn.btn-primary.pull-right type=:submit Log in
|
31
|
+
- if settings.login_bypass
|
32
|
+
label.checkbox
|
33
|
+
| Bypass
|
34
|
+
input type=:checkbox name=:bypass value='Bypass'
|
35
|
+
|
36
|
+
small
|
37
|
+
a href=url('/login/reset_password') Forgot password?
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'tight-auth/login/controller'
|
2
|
+
|
3
|
+
module Tight
|
4
|
+
##
|
5
|
+
# Tight authentication module.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class Nifty::Application < Tight::Application
|
9
|
+
# # optional settings
|
10
|
+
# set :session_key, "visitor_id" # visitor key name in session storage, defaults to "_login_#{app.app_name}")
|
11
|
+
# set :login_model, :visitor # model name for visitor storage, defaults to :account, must be constantizable
|
12
|
+
# set :credentials_accessor, :visitor # the name of setter/getter method in helpers, defaults to :credentials
|
13
|
+
# enable :login_bypass # enables or disables login bypass in development mode, defaults to disable
|
14
|
+
# set :login_url, '/sign/in' # sets the utl to be redirected to if not logged in and in restricted area, defaults to '/login'
|
15
|
+
# disable :login_permissions # sets initial login permissions, defaults to { set_access(:*, :allow => :*, :with => :login) }
|
16
|
+
# disable :login_controller # disables default login controller to show an example of the custom one
|
17
|
+
#
|
18
|
+
# # required statement
|
19
|
+
# register Tight::Login
|
20
|
+
# # example persistance storage
|
21
|
+
# enable :sessions
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# TODO: example controllers
|
25
|
+
#
|
26
|
+
module Login
|
27
|
+
class << self
|
28
|
+
def registered(app)
|
29
|
+
warn 'Tight::Login must be registered before Tight::Access' if app.respond_to?(:set_access)
|
30
|
+
included(app)
|
31
|
+
setup_storage(app)
|
32
|
+
setup_controller(app)
|
33
|
+
app.before do
|
34
|
+
log_in if authorization_required?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def included(base)
|
39
|
+
base.send(:include, InstanceMethods)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def setup_storage(app)
|
45
|
+
app.default(:session_key, "_login_#{app.app_name}")
|
46
|
+
app.default(:login_model, :account)
|
47
|
+
app.default(:credentials_accessor, :credentials)
|
48
|
+
app.send :attr_reader, app.credentials_accessor unless app.instance_methods.include?(app.credentials_accessor)
|
49
|
+
app.send :attr_writer, app.credentials_accessor unless app.instance_methods.include?(:"#{app.credentials_accessor}=")
|
50
|
+
app.default(:login_bypass, false)
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_controller(app)
|
54
|
+
app.default(:login_url, '/login')
|
55
|
+
app.default(:login_permissions) { set_access(:*, :allow => :*, :with => :login) }
|
56
|
+
app.default(:login_controller, true)
|
57
|
+
app.controller(:login) { include Controller } if app.login_controller
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
# Returns the model used to authenticate visitors.
|
63
|
+
def login_model
|
64
|
+
@login_model ||= settings.login_model.to_s.classify.constantize
|
65
|
+
end
|
66
|
+
|
67
|
+
# Authenticates the visitor.
|
68
|
+
def authenticate
|
69
|
+
resource = login_model.authenticate(:email => params[:email], :password => params[:password])
|
70
|
+
resource ||= login_model.authenticate(:bypass => true) if settings.login_bypass && params[:bypass]
|
71
|
+
save_credentials(resource)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Checks if the visitor is authenticated.
|
75
|
+
def logged_in?
|
76
|
+
!!(send(settings.credentials_accessor) || restore_credentials)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Looks for authorization routine and calls it to check if the visitor is authorized.
|
80
|
+
def unauthorized?
|
81
|
+
respond_to?(:authorized?) && !authorized?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Checks if the current location needs the visitor to be authorized.
|
85
|
+
def authorization_required?
|
86
|
+
if logged_in?
|
87
|
+
if unauthorized?
|
88
|
+
# 403 Forbidden, provided credentials were successfully
|
89
|
+
# authenticated but the credentials still do not grant
|
90
|
+
# the client permission to access the resource
|
91
|
+
error 403, '403 Forbidden'
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
else
|
96
|
+
unauthorized?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Logs the visitor in using redirect to login page url.
|
101
|
+
def log_in
|
102
|
+
login_url = settings.login_url
|
103
|
+
if request.env['PATH_INFO'] != login_url
|
104
|
+
save_location
|
105
|
+
# 302 Found
|
106
|
+
redirect url(login_url)
|
107
|
+
# 401 Unauthorized, authentication is required and
|
108
|
+
# has not yet been provided
|
109
|
+
error 401, '401 Unauthorized'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Saves credentials in session.
|
114
|
+
def save_credentials(resource)
|
115
|
+
session[settings.session_key] = resource.respond_to?(:id) ? resource.id : resource
|
116
|
+
send(:"#{settings.credentials_accessor}=", resource)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Restores credentials from session using visitor model.
|
120
|
+
def restore_credentials
|
121
|
+
resource = login_model.authenticate(:id => session[settings.session_key])
|
122
|
+
send(:"#{settings.credentials_accessor}=", resource)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Redirects back to saved location or '/'
|
126
|
+
def restore_location
|
127
|
+
redirect session.delete(:return_to) || url('/')
|
128
|
+
end
|
129
|
+
|
130
|
+
# Saves location to session for following redirect in case of successful authentication.
|
131
|
+
def save_location
|
132
|
+
uri = env['REQUEST_URI'] || url(env['PATH_INFO'])
|
133
|
+
return if uri.blank? || uri.match(/\.css$|\.js$|\.png$/)
|
134
|
+
session[:return_to] = "#{ENV['RACK_BASE_URI']}#{uri}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Tight
|
2
|
+
##
|
3
|
+
# Class to store and check permissions used in Padrino::Access.
|
4
|
+
#
|
5
|
+
class Permissions
|
6
|
+
##
|
7
|
+
# Initializes new permissions storage.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# permissions = Permissions.new
|
11
|
+
#
|
12
|
+
def initialize
|
13
|
+
clear!
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Clears permit records and action cache.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# permissions.clear!
|
21
|
+
#
|
22
|
+
def clear!
|
23
|
+
@permits = {}
|
24
|
+
@actions = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Adds a permission record to storage.
|
29
|
+
#
|
30
|
+
# @param [Symbol || Object] subject
|
31
|
+
# permit subject
|
32
|
+
# @param [Hash] options
|
33
|
+
# permit attributes
|
34
|
+
# @param [Symbol] options[:allow] || options[:action]
|
35
|
+
# what action to allow with objects
|
36
|
+
# @param [Symbol] options[:with] || options[:object]
|
37
|
+
# with what objects allow specified action
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# permissions.add :robots, :allow => :protect, :object => :humans
|
41
|
+
# permissions.add @bender, :allow => :kill, :object => :humans
|
42
|
+
#
|
43
|
+
def add(*args)
|
44
|
+
@actions = {}
|
45
|
+
options = args.extract_options!
|
46
|
+
action, object = action_and_object(options)
|
47
|
+
object_type = detect_type(object)
|
48
|
+
args.each{ |subject| merge(subject, action, object_type) }
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Checks if permission record exists. Returns a boolean or yield a block.
|
53
|
+
#
|
54
|
+
# @param [Object] subject
|
55
|
+
# performer of an action
|
56
|
+
# @param [Hash] options
|
57
|
+
# attributes to check
|
58
|
+
# @param [Symbol] options[:have]
|
59
|
+
# check if the subject has a role
|
60
|
+
# @param [Symbol] options[:allow] || options[:action]
|
61
|
+
# check if the subject is allowed to perform the action
|
62
|
+
# @param [Symbol] options[:with] || options[:object]
|
63
|
+
# check if the subject is allowed to interact with the subject
|
64
|
+
# @param [Proc]
|
65
|
+
# optional block to yield if the action is allowed
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# # check if @bender have role :robots
|
69
|
+
# permissions.check @bender, :have => :robots # => true
|
70
|
+
# # check if @bender is allowed to kill :humans
|
71
|
+
# permissions.check @bender, :allow => :kill, :object => :humans # => true
|
72
|
+
# # check if @bender is allowed to kill :humans and yield a block
|
73
|
+
# permissions.check @bender, :allow => :kill, :object => :humans do
|
74
|
+
# @bender.kill_all! :humans
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
def check(subject, options)
|
78
|
+
case
|
79
|
+
when options[:have]
|
80
|
+
check_role(subject, options[:have])
|
81
|
+
else
|
82
|
+
check_action(subject, *action_and_object(options))
|
83
|
+
end && (block_given? ? yield : true)
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Populates and returns the list of objects available to the subject.
|
88
|
+
#
|
89
|
+
# @param [Object] subject
|
90
|
+
# the subject to be checked for actions
|
91
|
+
#
|
92
|
+
def find_objects(subject, target_action=nil)
|
93
|
+
find_actions(subject).inject([]) do |all,(action,objects)|
|
94
|
+
all |= objects if target_action.nil? || action == target_action || action == :*
|
95
|
+
all
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Merges a list of new permits into permissions storage.
|
102
|
+
def merge(subject, actions, object_type)
|
103
|
+
subject_id = detect_id(subject)
|
104
|
+
@permits[subject_id] ||= {}
|
105
|
+
Array(actions).each do |action|
|
106
|
+
@permits[subject_id][action] ||= []
|
107
|
+
@permits[subject_id][action] |= [object_type]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checks if the subject has the role.
|
112
|
+
def check_role(subject, roles)
|
113
|
+
if subject.respond_to?(:role)
|
114
|
+
Array(roles).include?(subject.role)
|
115
|
+
else
|
116
|
+
false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Checks if the subject is allowed to perform the action with the object.
|
121
|
+
def check_action(subject, action, object)
|
122
|
+
actions = find_actions(subject)
|
123
|
+
objects = actions && (Array(actions[action]) | Array(actions[:*]))
|
124
|
+
objects && (objects & [:*, detect_type(object)]).any?
|
125
|
+
end
|
126
|
+
|
127
|
+
# Finds all permits for the subject. Caches the permits in @actions.
|
128
|
+
# find_actions(@bender) # => { :kill => { :humans }, :drink => { :booze }, :* => { :login } }
|
129
|
+
def find_actions(subject)
|
130
|
+
subject_id = detect_id(subject)
|
131
|
+
return @actions[subject_id] if @actions[subject_id]
|
132
|
+
actions = @permits[subject_id] || {}
|
133
|
+
if subject.respond_to?(:role) && (role_actions = @permits[subject.role.to_sym])
|
134
|
+
actions.merge!(role_actions){ |_,left,right| Array(left)|Array(right) }
|
135
|
+
end
|
136
|
+
if public_actions = @permits[:*]
|
137
|
+
actions.merge!(public_actions){ |_,left,right| Array(left)|Array(right) }
|
138
|
+
end
|
139
|
+
@actions[subject_id] = actions
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns object type.
|
143
|
+
# detect_type :humans # => :human
|
144
|
+
# detect_type 'foobar' # => 'foobar'
|
145
|
+
def detect_type(object)
|
146
|
+
case object
|
147
|
+
when Symbol
|
148
|
+
object.to_s.singularize.to_sym
|
149
|
+
else
|
150
|
+
object
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns parametrized subject.
|
155
|
+
# detect_id :robots # => :robots
|
156
|
+
# detect_id sluggable_ar_resource # => 'Sluggable-resource-slug'
|
157
|
+
# detect_id some_resource_with_id # => '4'
|
158
|
+
# detect_id generic_object # => "<Object:0x00001234>"
|
159
|
+
def detect_id(subject)
|
160
|
+
case
|
161
|
+
when Symbol === subject
|
162
|
+
subject
|
163
|
+
when subject.respond_to?(:to_param)
|
164
|
+
subject.to_param
|
165
|
+
when subject.respond_to?(:id)
|
166
|
+
subject.id.to_s
|
167
|
+
else
|
168
|
+
"#{subject}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Utility function to extract action and object from options. Defaults to [:*, :*]
|
173
|
+
# action_and_object(:allow => :kill, :object => :humans) # => [:kill, :humans]
|
174
|
+
# action_and_object(:action => :romance, :with => :mutants) # => [:romance, :mutants]
|
175
|
+
# action_and_object({}) # => [:*, :*]
|
176
|
+
def action_and_object(options)
|
177
|
+
[options[:allow] || options[:action] || :*, options[:with] || options[:object] || :*]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/lib/tight-auth.rb
ADDED
data/test/auth_helper.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
require 'padrino-core'
|
4
|
+
require 'tight-auth'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
require 'rack/test'
|
7
|
+
|
8
|
+
module TightLogger
|
9
|
+
attr_accessor :io
|
10
|
+
def self.io
|
11
|
+
@io ||= StringIO.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Padrino::Logger::Config[:test] = { :log_level => :devel, :stream => TightLogger.io }
|
16
|
+
|
17
|
+
class Minitest::Spec
|
18
|
+
include Rack::Test::Methods
|
19
|
+
|
20
|
+
def mock_app(base=Padrino::Application, &block)
|
21
|
+
@app = Sinatra.new(base, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def app
|
25
|
+
Rack::Lint.new(@app)
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_access(*args)
|
29
|
+
@app.set_access(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def allow(subject = nil, path = '/')
|
33
|
+
@app.fake_session[:visitor] = nil
|
34
|
+
get "/login/#{subject.id}" if subject
|
35
|
+
get path
|
36
|
+
assert_equal 200, status, caller.first.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def deny(subject = nil, path = '/')
|
40
|
+
@app.fake_session[:visitor] = nil
|
41
|
+
get "/login/#{subject.id}" if subject
|
42
|
+
get path
|
43
|
+
assert_equal 403, status, caller.first.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def status
|
47
|
+
response.status
|
48
|
+
end
|
49
|
+
|
50
|
+
def body
|
51
|
+
response.body
|
52
|
+
end
|
53
|
+
|
54
|
+
def response
|
55
|
+
last_response
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module Character
|
60
|
+
extend self
|
61
|
+
|
62
|
+
def authenticate(credentials)
|
63
|
+
case
|
64
|
+
when credentials[:email] && credentials[:password]
|
65
|
+
target = all.find{ |resource| resource.id.to_s == credentials[:email] }
|
66
|
+
target.name.gsub(/[^A-Z]/,'') == credentials[:password] ? target : nil
|
67
|
+
when credentials.has_key?(:id)
|
68
|
+
all.find{ |resource| resource.id == credentials[:id] }
|
69
|
+
else
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def all
|
75
|
+
@all = [
|
76
|
+
OpenStruct.new(:id => :bender, :name => 'Bender Bending Rodriguez', :role => :robots ),
|
77
|
+
OpenStruct.new(:id => :leela, :name => 'Turanga Leela', :role => :mutants ),
|
78
|
+
OpenStruct.new(:id => :fry, :name => 'Philip J. Fry', :role => :humans ),
|
79
|
+
OpenStruct.new(:id => :ami, :name => 'Amy Wong', :role => :humans ),
|
80
|
+
OpenStruct.new(:id => :zoidberg, :name => 'Dr. John A. Zoidberg', :role => :lobsters),
|
81
|
+
]
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require File.expand_path('../auth_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "Tight::Access" do
|
4
|
+
before do
|
5
|
+
mock_app do
|
6
|
+
set :credentials_reader, :visitor
|
7
|
+
register Tight::Access
|
8
|
+
set_access :*, :allow => :login
|
9
|
+
set :users, Character.all
|
10
|
+
get(:login, :with => :id) do
|
11
|
+
user = settings.users.find{ |user| user.id.to_s == params[:id] }
|
12
|
+
self.send(:"#{settings.credentials_reader}=", user)
|
13
|
+
end
|
14
|
+
get(:index){ 'foo' }
|
15
|
+
get(:bend){ 'bend' }
|
16
|
+
get(:drink){ 'bend' }
|
17
|
+
get(:subject){ self.send(settings.credentials_reader).inspect }
|
18
|
+
get(:stop_partying){ 'stop partying' }
|
19
|
+
controller :surface do
|
20
|
+
get(:live) { 'live on the surface' }
|
21
|
+
end
|
22
|
+
controller :sewers do
|
23
|
+
get(:live) { 'live in the sewers' }
|
24
|
+
get(:visit) { 'visit the sewers' }
|
25
|
+
end
|
26
|
+
set :fake_session, {}
|
27
|
+
helpers do
|
28
|
+
def visitor
|
29
|
+
settings.fake_session[:visitor]
|
30
|
+
end
|
31
|
+
def visitor=(user)
|
32
|
+
settings.fake_session[:visitor] = user
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
Character.all.each do |user|
|
37
|
+
instance_variable_set :"@#{user.id}", user
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should register with authorization module' do
|
42
|
+
assert @app.respond_to? :set_access
|
43
|
+
assert_kind_of Tight::Permissions, @app.permissions
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should properly detect access subject' do
|
47
|
+
set_access :*
|
48
|
+
get '/login/ami'
|
49
|
+
get '/subject'
|
50
|
+
assert_equal @ami.inspect, body
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should reset access properly' do
|
54
|
+
set_access :*
|
55
|
+
allow
|
56
|
+
@app.reset_access!
|
57
|
+
deny
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should set group access' do
|
61
|
+
# only humans should be allowed on TV
|
62
|
+
set_access :humans
|
63
|
+
allow @fry
|
64
|
+
deny @bender
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should set individual access' do
|
68
|
+
# only Fry should be allowed to romance Leela
|
69
|
+
set_access @fry
|
70
|
+
allow @fry
|
71
|
+
deny @ami
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should set mixed individual and group access' do
|
75
|
+
# only humans and Leela should be allowed on the surface
|
76
|
+
set_access :humans
|
77
|
+
set_access @leela
|
78
|
+
allow @fry
|
79
|
+
allow @leela
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should set action-specific access' do
|
83
|
+
# bender should be allowed to bend, and he's denied to stop partying
|
84
|
+
set_access @bender, :allow => :bend
|
85
|
+
set_access @fry, :allow => :stop_partying
|
86
|
+
allow @bender, '/bend'
|
87
|
+
deny @bender, '/stop_partying'
|
88
|
+
allow @fry, '/stop_partying'
|
89
|
+
deny @fry, '/bend'
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should set multiple action' do
|
93
|
+
# bender should be allowed to bend and drink
|
94
|
+
set_access @bender, :allow => [:drink, :bend]
|
95
|
+
allow @bender, '/drink'
|
96
|
+
allow @bender, '/bend'
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should set object-specific access' do
|
100
|
+
# only humans and Leela should be allowed to live on the surface
|
101
|
+
# only mutants should be allowed to live in the sewers though humans can visit
|
102
|
+
set_access :humans, :allow => :live, :with => :surface
|
103
|
+
set_access :mutants, :allow => :live, :with => :sewers
|
104
|
+
set_access @leela, :allow => :live, :with => :surface
|
105
|
+
set_access :humans, :allow => :visit, :with => :sewers
|
106
|
+
allow @fry, '/surface/live'
|
107
|
+
deny @fry, '/sewers/live'
|
108
|
+
allow @fry, '/sewers/visit'
|
109
|
+
allow @leela, '/surface/live'
|
110
|
+
allow @leela, '/sewers/live'
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should detect object when setting access from controller' do
|
114
|
+
# only humans and lobsters should have binocular vision
|
115
|
+
@app.controller :binocular do
|
116
|
+
set_access :humans, :lobsters
|
117
|
+
get(:vision) { 'binocular vision' }
|
118
|
+
end
|
119
|
+
deny @fry, '/'
|
120
|
+
allow @fry, '/binocular/vision'
|
121
|
+
allow @zoidberg, '/binocular/vision'
|
122
|
+
deny @leela, '/binocular/vision'
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.expand_path('../auth_helper', __FILE__)
|
2
|
+
|
3
|
+
Account = Character
|
4
|
+
|
5
|
+
describe "Tight::Auth" do
|
6
|
+
before do
|
7
|
+
mock_app do
|
8
|
+
enable :sessions
|
9
|
+
register Tight::Login
|
10
|
+
register Tight::Access
|
11
|
+
get(:robot_area){ 'robot_area' }
|
12
|
+
set_access :robots, :allow => :robot_area
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should login and access play nicely together' do
|
17
|
+
get '/robot_area'
|
18
|
+
assert_equal 302, status
|
19
|
+
|
20
|
+
post '/login', :email => :bender, :password => 'BBR'
|
21
|
+
get '/robot_area'
|
22
|
+
assert_equal 200, status
|
23
|
+
|
24
|
+
post '/login', :email => :leela, :password => 'TL'
|
25
|
+
get '/robot_area'
|
26
|
+
assert_equal 403, status
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should whine if the order is wrong' do
|
30
|
+
out, err = capture_io do
|
31
|
+
mock_app do
|
32
|
+
register Tight::Access
|
33
|
+
register Tight::Login
|
34
|
+
end
|
35
|
+
end
|
36
|
+
assert_match /must be registered before/, err
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require File.expand_path('../auth_helper', __FILE__)
|
2
|
+
require 'padrino-helpers'
|
3
|
+
|
4
|
+
describe "Tight::Access" do
|
5
|
+
before do
|
6
|
+
mock_app do
|
7
|
+
set :credentials_accessor, :visitor
|
8
|
+
set :login_model, :character
|
9
|
+
enable :sessions
|
10
|
+
register Tight::Login
|
11
|
+
get(:index){ 'index' }
|
12
|
+
get(:restricted){ 'secret' }
|
13
|
+
helpers do
|
14
|
+
def authorized?
|
15
|
+
return !['/restricted'].include?(request.env['PATH_INFO']) unless visitor
|
16
|
+
case
|
17
|
+
when visitor.id == :bender
|
18
|
+
true
|
19
|
+
else
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
Character.all.each do |user|
|
26
|
+
instance_variable_set :"@#{user.id}", user
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should pass unrestricted area' do
|
31
|
+
get '/'
|
32
|
+
assert_equal 200, status
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should be redirected from restricted area to login page' do
|
36
|
+
get '/restricted'
|
37
|
+
assert_equal 302, status
|
38
|
+
get response.location
|
39
|
+
assert_equal 200, status
|
40
|
+
assert_match /<form .*<input .*/, body
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should not be able to authenticate with wrong password' do
|
44
|
+
post '/login', :email => :bender, :password => '123'
|
45
|
+
assert_equal 200, status
|
46
|
+
assert_match 'Wrong password', body
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should be able to authenticate with email and password' do
|
50
|
+
post '/login', :email => :bender, :password => 'BBR'
|
51
|
+
assert_equal 302, status
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should be redirected back' do
|
55
|
+
get '/restricted'
|
56
|
+
post response.location, :email => :bender, :password => 'BBR'
|
57
|
+
assert_match /\/restricted$/, response.location
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should be redirected to root if no location was saved' do
|
61
|
+
post '/login', :email => :bender, :password => 'BBR'
|
62
|
+
assert_match /\/$/, response.location
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should be allowed in restricted area after logging in' do
|
66
|
+
post '/login', :email => :bender, :password => 'BBR'
|
67
|
+
get '/restricted'
|
68
|
+
assert_equal 'secret', body
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should not be allowed in restricted area after logging in an account lacking privileges' do
|
72
|
+
post '/login', :email => :leela, :password => 'TL'
|
73
|
+
get '/restricted'
|
74
|
+
assert_equal 403, status
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path('../lib', __FILE__)
|
2
|
+
require 'tight/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = 'tight-engine'
|
6
|
+
spec.version = Tight::VERSION
|
7
|
+
spec.description = 'Tight engine for Swift CMS'
|
8
|
+
spec.summary = 'A tight engine for a swift content management system'
|
9
|
+
|
10
|
+
spec.authors = ['Igor Bochkariov']
|
11
|
+
spec.email = ['ujifgc@gmail.com']
|
12
|
+
spec.homepage = 'https://github.com/ujifgc/tight-engine'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.require_paths = ['lib']
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.test_files = spec.files.grep(%r{^test/})
|
18
|
+
|
19
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
20
|
+
spec.add_development_dependency 'rake'
|
21
|
+
spec.add_development_dependency 'minitest'
|
22
|
+
spec.add_development_dependency 'padrino-core'
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tight-engine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Igor Bochkariov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: padrino-core
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Tight engine for Swift CMS
|
70
|
+
email:
|
71
|
+
- ujifgc@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- LICENSE
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- lib/tight-auth.rb
|
81
|
+
- lib/tight-auth/access.rb
|
82
|
+
- lib/tight-auth/login.rb
|
83
|
+
- lib/tight-auth/login/controller.rb
|
84
|
+
- lib/tight-auth/login/layout.slim
|
85
|
+
- lib/tight-auth/login/new.slim
|
86
|
+
- lib/tight-auth/permissions.rb
|
87
|
+
- lib/tight/version.rb
|
88
|
+
- test/auth_helper.rb
|
89
|
+
- test/test_padrino_access.rb
|
90
|
+
- test/test_padrino_auth.rb
|
91
|
+
- test/test_padrino_login.rb
|
92
|
+
- tight-engine.gemspec
|
93
|
+
homepage: https://github.com/ujifgc/tight-engine
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 2.2.2
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: A tight engine for a swift content management system
|
117
|
+
test_files:
|
118
|
+
- test/auth_helper.rb
|
119
|
+
- test/test_padrino_access.rb
|
120
|
+
- test/test_padrino_auth.rb
|
121
|
+
- test/test_padrino_login.rb
|
122
|
+
has_rdoc:
|