tight-engine 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 +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:
|