strelka 0.0.1.pre148 → 0.0.1.pre177
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +1294 -0
- data/IDEAS.rdoc +6 -0
- data/Manifest.txt +20 -0
- data/README.rdoc +8 -2
- data/Rakefile +9 -7
- data/examples/auth-demo.rb +32 -0
- data/examples/auth-demo2.rb +37 -0
- data/examples/auth-form.tmpl +10 -0
- data/examples/auth-success.tmpl +3 -0
- data/examples/config.yml +12 -0
- data/examples/examples.css +4 -0
- data/examples/examples.html +31 -0
- data/examples/gen-config.rb +5 -2
- data/examples/layout.tmpl +31 -0
- data/lib/strelka/app/auth.rb +480 -0
- data/lib/strelka/app/sessions.rb +8 -6
- data/lib/strelka/app/templating.rb +78 -17
- data/lib/strelka/app.rb +13 -5
- data/lib/strelka/authprovider/basic.rb +134 -0
- data/lib/strelka/authprovider/hostaccess.rb +91 -0
- data/lib/strelka/authprovider.rb +122 -0
- data/lib/strelka/cookie.rb +1 -1
- data/lib/strelka/cookieset.rb +1 -1
- data/lib/strelka/httprequest/auth.rb +31 -0
- data/lib/strelka/logging.rb +69 -14
- data/lib/strelka/mixins.rb +35 -65
- data/lib/strelka/session/db.rb +115 -0
- data/lib/strelka/session/default.rb +38 -49
- data/lib/strelka/session.rb +1 -1
- data/lib/strelka.rb +4 -1
- data/spec/lib/helpers.rb +8 -3
- data/spec/strelka/app/auth_spec.rb +367 -0
- data/spec/strelka/authprovider/basic_spec.rb +192 -0
- data/spec/strelka/authprovider/hostaccess_spec.rb +70 -0
- data/spec/strelka/authprovider_spec.rb +99 -0
- data/spec/strelka/cookie_spec.rb +1 -1
- data/spec/strelka/httprequest/auth_spec.rb +55 -0
- data/spec/strelka/httprequest/session_spec.rb +63 -3
- data/spec/strelka/session/db_spec.rb +85 -0
- data/spec/strelka/session/default_spec.rb +5 -51
- data.tar.gz.sig +0 -0
- metadata +88 -57
- metadata.gz.sig +0 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# vim: set nosta noet ts=4 sw=4:
|
3
|
+
|
4
|
+
require 'securerandom'
|
5
|
+
require 'forwardable'
|
6
|
+
require 'sequel'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
require 'strelka' unless defined?( Strelka )
|
10
|
+
require 'strelka/app' unless defined?( Strelka::App )
|
11
|
+
require 'strelka/session/default'
|
12
|
+
|
13
|
+
# Database session class -- this class offers persistent session data using
|
14
|
+
# any database that Sequel supports. It defaults to non-persistent, in memory Sqlite.
|
15
|
+
#
|
16
|
+
class Strelka::Session::Db < Strelka::Session::Default
|
17
|
+
include Strelka::Loggable
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
# Class-instance variables
|
21
|
+
@table_name = :sessions
|
22
|
+
@db = nil
|
23
|
+
@dataset = nil
|
24
|
+
@cookie_options = {
|
25
|
+
:name => 'strelka-session'
|
26
|
+
}
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# The Sequel dataset connection
|
30
|
+
attr_reader :db
|
31
|
+
|
32
|
+
# The Sequel dataset for the sessions table
|
33
|
+
attr_reader :dataset
|
34
|
+
|
35
|
+
# The configured session cookie parameters
|
36
|
+
attr_accessor :cookie_options
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
########################################################################
|
41
|
+
### C L A S S M E T H O D S
|
42
|
+
########################################################################
|
43
|
+
|
44
|
+
### Configure the session class with the given +options+, which should be a
|
45
|
+
### Hash or an object that has a Hash-like interface.
|
46
|
+
###
|
47
|
+
### Valid options:
|
48
|
+
### cookie -> A hash that contains valid Strelka::Cookie options
|
49
|
+
### connect -> The Sequel connection string
|
50
|
+
### table_name -> The name of the sessions table
|
51
|
+
def self::configure( options={} )
|
52
|
+
options ||= {}
|
53
|
+
|
54
|
+
self.cookie_options.merge!( options[:cookie] ) if options[:cookie]
|
55
|
+
@table_name = options[:table_name] || :sessions
|
56
|
+
|
57
|
+
@db = options[ :connect ].nil? ?
|
58
|
+
Mongrel2::Config.in_memory_db :
|
59
|
+
Sequel.connect( options[:connect] )
|
60
|
+
@db.logger = Strelka.logger
|
61
|
+
|
62
|
+
self.initialize_sessions_table
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
### Create the initial DB sessions schema if needed, setting the +sessions+
|
67
|
+
### attribute to a Sequel dataset on the configured DB table.
|
68
|
+
###
|
69
|
+
def self::initialize_sessions_table
|
70
|
+
if self.db.table_exists?( @table_name )
|
71
|
+
Strelka.log.debug "Using existing sessions table for %p" % [ db ]
|
72
|
+
|
73
|
+
else
|
74
|
+
Strelka.log.debug "Creating new sessions table for %p" % [ db ]
|
75
|
+
self.db.create_table( @table_name ) do
|
76
|
+
text :session_id, :index => true
|
77
|
+
text :session
|
78
|
+
timestamp :created
|
79
|
+
|
80
|
+
primary_key :session_id
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
@dataset = self.db[ @table_name ]
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
### Load a session instance from storage using the given +session_id+.
|
89
|
+
def self::load( session_id )
|
90
|
+
session_row = self.dataset.filter( :session_id => session_id ).first
|
91
|
+
session = session_row.nil? ? {} : YAML.load( session_row[:session] )
|
92
|
+
return new( session_id, session )
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
### Save the given +data+ associated with the +session_id+ to the DB.
|
97
|
+
def self::save_session_data( session_id, data )
|
98
|
+
self.db.transaction do
|
99
|
+
self.delete_session_data( session_id.to_s )
|
100
|
+
self.dataset.insert(
|
101
|
+
:session_id => session_id,
|
102
|
+
:session => data.to_yaml,
|
103
|
+
:created => Time.now.utc.to_s
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
### Delete the data associated with the given +session_id+ from the DB.
|
110
|
+
def self::delete_session_data( session_id )
|
111
|
+
self.dataset.filter( :session_id => session_id ).delete
|
112
|
+
end
|
113
|
+
|
114
|
+
end # class Strelka::Session::Db
|
115
|
+
|
@@ -6,6 +6,7 @@ require 'forwardable'
|
|
6
6
|
|
7
7
|
require 'strelka' unless defined?( Strelka )
|
8
8
|
require 'strelka/app' unless defined?( Strelka::App )
|
9
|
+
require 'strelka/cookie'
|
9
10
|
require 'strelka/session'
|
10
11
|
require 'strelka/mixins'
|
11
12
|
|
@@ -14,53 +15,40 @@ require 'strelka/mixins'
|
|
14
15
|
# == Hash Interface
|
15
16
|
#
|
16
17
|
# The following methods are delegated to the inner Hash via the #namespaced_hash method:
|
17
|
-
#
|
18
|
-
# #compare_by_identity?, #default, # #default=, #default_proc, #default_proc=,
|
19
|
-
# #delete, #delete_if, #each, #each_key, #each_pair, #each_value,
|
20
|
-
# #empty?, #eql?, #fetch, #flatten, #has_key?, #has_value?, #hash, #include?,
|
21
|
-
# #index, #inspect, #invert, #keep_if, #key, #key?, #keys, #length, #member?,
|
22
|
-
# #merge, #merge!, #pretty_print, #pretty_print_cycle, #rassoc, #rehash, #reject,
|
23
|
-
# #reject!, #replace, #select, #select!, #shift, #size, #sql_expr, #sql_negate,
|
24
|
-
# #sql_or, #store, #to_a, #to_hash, #to_s, #update, #value?, #values, #values_at,
|
25
|
-
# #|, #~
|
18
|
+
# #[], #[]=, #delete, #key?
|
26
19
|
class Strelka::Session::Default < Strelka::Session
|
27
20
|
extend Forwardable,
|
28
21
|
Strelka::Delegation
|
29
|
-
include
|
30
|
-
Strelka::Loggable,
|
22
|
+
include Strelka::Loggable,
|
31
23
|
Strelka::DataUtilities
|
32
24
|
|
33
|
-
|
34
|
-
# The default name of the cookie that stores the session ID
|
35
|
-
DEFAULT_COOKIE_NAME = 'strelka-sessionid'
|
36
|
-
|
37
25
|
# Class-instance variables
|
38
26
|
@sessions = {}
|
39
|
-
@
|
27
|
+
@cookie_options = {
|
28
|
+
:name => 'strelka-session'
|
29
|
+
}
|
40
30
|
|
41
31
|
class << self
|
42
32
|
# Persist sessions in memory
|
43
33
|
attr_reader :sessions
|
44
34
|
|
45
|
-
# The
|
46
|
-
attr_accessor :
|
35
|
+
# The configured session cookie parameters
|
36
|
+
attr_accessor :cookie_options
|
47
37
|
end
|
48
38
|
|
49
39
|
|
50
40
|
### Configure the session class with the given +options+, which should be a
|
51
|
-
### Hash or an object that has a Hash-like interface. Sets
|
52
|
-
### the session
|
53
|
-
### is set.
|
41
|
+
### Hash or an object that has a Hash-like interface. Sets cookie options
|
42
|
+
### for the session if the +:cookie+ key is set.
|
54
43
|
def self::configure( options )
|
55
44
|
if options
|
56
|
-
self.
|
45
|
+
self.cookie_options.merge!( options[:cookie] ) if options[:cookie]
|
57
46
|
end
|
58
47
|
end
|
59
48
|
|
60
49
|
|
61
50
|
### Load a session instance from storage using the given +session_id+ and return
|
62
|
-
### it. Returns +nil+ if no session could be loaded.
|
63
|
-
### this, as the default just returns +nil+.
|
51
|
+
### it. Returns +nil+ if no session could be loaded.
|
64
52
|
def self::load_session_data( session_id )
|
65
53
|
return Strelka::DataUtilities.deep_copy( self.sessions[session_id] )
|
66
54
|
end
|
@@ -79,16 +67,13 @@ class Strelka::Session::Default < Strelka::Session
|
|
79
67
|
|
80
68
|
|
81
69
|
### Fetch the session ID from the given +request+, or create a new one if the
|
82
|
-
### request doesn't have the necessary attributes.
|
83
|
-
### as the default implementation just returns +nil+.
|
70
|
+
### request doesn't have the necessary attributes.
|
84
71
|
def self::get_session_id( request=nil )
|
85
72
|
id = nil
|
86
73
|
|
87
74
|
# Fetch and untaint the existing ID if it exists and looks valid
|
88
|
-
if request && (cookie = request.cookies[ self.
|
89
|
-
Strelka.log.debug "Cookie value: %p" % [ cookie.value ]
|
75
|
+
if request && (cookie = request.cookies[ self.cookie_options[:name] ])
|
90
76
|
id = $1.untaint if cookie.value =~ /^([[:xdigit:]]+)$/i
|
91
|
-
Strelka.log.debug "Hmm? %p" % [ $1 ]
|
92
77
|
end
|
93
78
|
|
94
79
|
return id || SecureRandom.random_bytes.unpack( 'H*' ).join
|
@@ -99,8 +84,7 @@ class Strelka::Session::Default < Strelka::Session
|
|
99
84
|
### I N S T A N C E M E T H O D S
|
100
85
|
#################################################################
|
101
86
|
|
102
|
-
### Create a new
|
103
|
-
### values.
|
87
|
+
### Create a new session store using the given +hash+ for initial values.
|
104
88
|
def initialize( session_id=nil, initial_values={} )
|
105
89
|
session_id ||= self.class.get_session_id
|
106
90
|
|
@@ -134,7 +118,7 @@ class Strelka::Session::Default < Strelka::Session
|
|
134
118
|
attr_reader :namespace
|
135
119
|
|
136
120
|
# Delegate Hash methods to whichever Hash is the current namespace
|
137
|
-
def_method_delegators :namespaced_hash,
|
121
|
+
def_method_delegators :namespaced_hash, :[], :[]=, :delete, :key?
|
138
122
|
|
139
123
|
|
140
124
|
### Set the current namespace for session values to +namespace+. Setting
|
@@ -150,7 +134,28 @@ class Strelka::Session::Default < Strelka::Session
|
|
150
134
|
self.log.debug "Saving session %s" % [ self.session_id ]
|
151
135
|
self.class.save_session_data( self.session_id, @hash )
|
152
136
|
self.log.debug "Adding session cookie to the request."
|
153
|
-
|
137
|
+
|
138
|
+
session_cookie = Strelka::Cookie.new(
|
139
|
+
self.class.cookie_options[ :name ],
|
140
|
+
self.session_id,
|
141
|
+
self.class.cookie_options
|
142
|
+
)
|
143
|
+
|
144
|
+
response.cookies << session_cookie
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
### Return the Hash that corresponds with the current namespace's storage. If no
|
149
|
+
### namespace is currently set, returns the entire session store as a Hash of Hashes
|
150
|
+
### keyed by namespaces as Symbols.
|
151
|
+
def namespaced_hash
|
152
|
+
if @namespace
|
153
|
+
self.log.debug "Returning namespaced hash: %p" % [ @namespace ]
|
154
|
+
return @hash[ @namespace ]
|
155
|
+
else
|
156
|
+
self.log.debug "Returning toplevel namespace"
|
157
|
+
return @hash
|
158
|
+
end
|
154
159
|
end
|
155
160
|
|
156
161
|
|
@@ -169,10 +174,8 @@ class Strelka::Session::Default < Strelka::Session
|
|
169
174
|
|
170
175
|
method_body = nil
|
171
176
|
if assignment
|
172
|
-
self.log.debug "Makin' a setter for %p" % [ key ]
|
173
177
|
method_body = self.make_setter( key )
|
174
178
|
else
|
175
|
-
self.log.debug "Makin' a getter for %p" % [ key ]
|
176
179
|
method_body = self.make_getter( key )
|
177
180
|
end
|
178
181
|
|
@@ -192,18 +195,4 @@ class Strelka::Session::Default < Strelka::Session
|
|
192
195
|
return Proc.new { self.namespaced_hash[key.to_sym] }
|
193
196
|
end
|
194
197
|
|
195
|
-
|
196
|
-
### Return the Hash that corresponds with the current namespace's storage. If no
|
197
|
-
### namespace is currently set, returns the entire session store as a Hash of Hashes
|
198
|
-
### keyed by namespaces as Symbols.
|
199
|
-
def namespaced_hash
|
200
|
-
if @namespace
|
201
|
-
self.log.debug "Returning namespaced hash: %p" % [ @namespace ]
|
202
|
-
return @hash[ @namespace ]
|
203
|
-
else
|
204
|
-
self.log.debug "Returning toplevel namespace"
|
205
|
-
return @hash
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
198
|
end # class Strelka::Session::Default
|
data/lib/strelka/session.rb
CHANGED
data/lib/strelka.rb
CHANGED
@@ -12,13 +12,16 @@ require 'configurability/config'
|
|
12
12
|
#
|
13
13
|
# * Michael Granger <ged@FaerieMUD.org>
|
14
14
|
#
|
15
|
+
# :title: Strelka Web Application Framework
|
16
|
+
# :main: README.rdoc
|
17
|
+
#
|
15
18
|
module Strelka
|
16
19
|
|
17
20
|
# Library version constant
|
18
21
|
VERSION = '0.0.1'
|
19
22
|
|
20
23
|
# Version-control revision constant
|
21
|
-
REVISION = %q$Revision:
|
24
|
+
REVISION = %q$Revision: 28465ed81190 $
|
22
25
|
|
23
26
|
|
24
27
|
require 'strelka/logging'
|
data/spec/lib/helpers.rb
CHANGED
@@ -113,9 +113,9 @@ module Strelka::SpecHelpers
|
|
113
113
|
end
|
114
114
|
|
115
115
|
### Set up a Mongrel2 configuration database according to the specified +dbspec+.
|
116
|
-
|
117
|
-
|
118
|
-
|
116
|
+
### Set up a Mongrel2 configuration database in memory.
|
117
|
+
def setup_config_db
|
118
|
+
Mongrel2::Config.db ||= Mongrel2::Config.in_memory_db
|
119
119
|
Mongrel2::Config.init_database
|
120
120
|
Mongrel2::Config.db.tables.collect {|t| Mongrel2::Config.db[t] }.each( &:truncate )
|
121
121
|
end
|
@@ -126,6 +126,11 @@ module Strelka::SpecHelpers
|
|
126
126
|
return {:action => name.to_sym}
|
127
127
|
end
|
128
128
|
|
129
|
+
|
130
|
+
#
|
131
|
+
# Matchers
|
132
|
+
#
|
133
|
+
|
129
134
|
# Route matcher
|
130
135
|
RSpec::Matchers.define( :match_route ) do |routename|
|
131
136
|
match do |route|
|
@@ -0,0 +1,367 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
BEGIN {
|
4
|
+
require 'pathname'
|
5
|
+
basedir = Pathname.new( __FILE__ ).dirname.parent.parent.parent
|
6
|
+
$LOAD_PATH.unshift( basedir ) unless $LOAD_PATH.include?( basedir )
|
7
|
+
}
|
8
|
+
|
9
|
+
require 'rspec'
|
10
|
+
require 'rspec/mocks'
|
11
|
+
|
12
|
+
require 'spec/lib/helpers'
|
13
|
+
|
14
|
+
require 'strelka'
|
15
|
+
require 'strelka/app/plugins'
|
16
|
+
require 'strelka/app/auth'
|
17
|
+
require 'strelka/authprovider'
|
18
|
+
|
19
|
+
require 'strelka/behavior/plugin'
|
20
|
+
|
21
|
+
|
22
|
+
#####################################################################
|
23
|
+
### C O N T E X T S
|
24
|
+
#####################################################################
|
25
|
+
|
26
|
+
describe Strelka::App::Auth do
|
27
|
+
|
28
|
+
before( :all ) do
|
29
|
+
setup_logging( :fatal )
|
30
|
+
@request_factory = Mongrel2::RequestFactory.new( route: '/api/v1' )
|
31
|
+
end
|
32
|
+
|
33
|
+
after( :all ) do
|
34
|
+
reset_logging()
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
it_should_behave_like( "A Strelka::App Plugin" )
|
39
|
+
|
40
|
+
|
41
|
+
it "gives including apps a default authprovider" do
|
42
|
+
app = Class.new( Strelka::App ) do
|
43
|
+
plugins :auth
|
44
|
+
end
|
45
|
+
|
46
|
+
app.auth_provider.should be_a( Class )
|
47
|
+
app.auth_provider.should < Strelka::AuthProvider
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
it "adds the Auth mixin to the request class" do
|
52
|
+
app = Class.new( Strelka::App ) do
|
53
|
+
plugins :auth
|
54
|
+
end
|
55
|
+
|
56
|
+
@request_factory.get( '/api/v1/verify' ).should respond_to( :authenticated? )
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
describe "an including App" do
|
61
|
+
|
62
|
+
before( :each ) do
|
63
|
+
@app = Class.new( Strelka::App ) do
|
64
|
+
plugins :auth
|
65
|
+
|
66
|
+
# Stand in for a real AuthProvider
|
67
|
+
@auth_provider = RSpec::Mocks::Mock
|
68
|
+
|
69
|
+
def initialize( appid='auth-test', sspec=TEST_SEND_SPEC, rspec=TEST_RECV_SPEC )
|
70
|
+
super
|
71
|
+
end
|
72
|
+
|
73
|
+
def handle_request( req )
|
74
|
+
super do
|
75
|
+
res = req.response
|
76
|
+
res.status = HTTP::OK
|
77
|
+
res.content_type = 'text/plain'
|
78
|
+
res.puts "Ran successfully."
|
79
|
+
|
80
|
+
res
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
it "applies auth to every request by default" do
|
88
|
+
app = @app.new
|
89
|
+
req = @request_factory.get( '/api/v1' )
|
90
|
+
|
91
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
92
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
93
|
+
|
94
|
+
res = app.handle( req )
|
95
|
+
|
96
|
+
res.status.should == HTTP::OK
|
97
|
+
end
|
98
|
+
|
99
|
+
it "doesn't have any auth criteria by default" do
|
100
|
+
@app.should_not have_auth_criteria()
|
101
|
+
end
|
102
|
+
|
103
|
+
it "sets the authenticated_user attribute of the request to the credentials of the authenticating user" do
|
104
|
+
app = @app.new
|
105
|
+
req = @request_factory.get( '/api/v1' )
|
106
|
+
|
107
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
108
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
109
|
+
|
110
|
+
app.handle( req )
|
111
|
+
req.authenticated_user.should == 'anonymous'
|
112
|
+
end
|
113
|
+
|
114
|
+
context "that has negative auth criteria" do
|
115
|
+
|
116
|
+
before( :each ) do
|
117
|
+
@app.no_auth_for( '/login' )
|
118
|
+
end
|
119
|
+
|
120
|
+
it "knows that it has auth criteria" do
|
121
|
+
@app.should have_auth_criteria()
|
122
|
+
end
|
123
|
+
|
124
|
+
it "doesn't pass a request that matches through auth" do
|
125
|
+
req = @request_factory.get( '/api/v1/login' )
|
126
|
+
|
127
|
+
app = @app.new
|
128
|
+
app.auth_provider.should_not_receive( :authenticate )
|
129
|
+
app.auth_provider.should_not_receive( :authorize )
|
130
|
+
|
131
|
+
app.handle( req )
|
132
|
+
end
|
133
|
+
|
134
|
+
it "passes a request that doesn't match through auth" do
|
135
|
+
req = @request_factory.get( '/api/v1/console' )
|
136
|
+
|
137
|
+
app = @app.new
|
138
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
139
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
140
|
+
|
141
|
+
app.handle( req )
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
context "that has a negative auth criteria block" do
|
147
|
+
|
148
|
+
before( :each ) do
|
149
|
+
@app.no_auth_for do |req|
|
150
|
+
req.notes[:skip_auth]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it "knows that it has auth criteria" do
|
155
|
+
@app.should have_auth_criteria()
|
156
|
+
end
|
157
|
+
|
158
|
+
it "doesn't pass a request for which the block returns true through auth" do
|
159
|
+
req = @request_factory.get( '/api/v1/login' )
|
160
|
+
req.notes[:skip_auth] = true
|
161
|
+
|
162
|
+
app = @app.new
|
163
|
+
app.auth_provider.should_not_receive( :authenticate )
|
164
|
+
app.auth_provider.should_not_receive( :authorize )
|
165
|
+
|
166
|
+
app.handle( req )
|
167
|
+
end
|
168
|
+
|
169
|
+
it "passes a request for which the block returns false through auth" do
|
170
|
+
req = @request_factory.get( '/api/v1/login' )
|
171
|
+
req.notes[:skip_auth] = false
|
172
|
+
|
173
|
+
app = @app.new
|
174
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
175
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
176
|
+
|
177
|
+
app.handle( req )
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
context "that has a negative auth criteria with both a pattern and a block" do
|
184
|
+
|
185
|
+
before( :each ) do
|
186
|
+
@app.no_auth_for( %r{^/login/(?<username>\w+)} ) do |req, match|
|
187
|
+
match[:username] != 'validuser'
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
it "knows that it has auth criteria" do
|
192
|
+
@app.should have_auth_criteria()
|
193
|
+
end
|
194
|
+
|
195
|
+
it "doesn't pass a request through auth if the path matches and the block returns true" do
|
196
|
+
req = @request_factory.get( '/api/v1/login/lyssa' )
|
197
|
+
|
198
|
+
app = @app.new
|
199
|
+
app.auth_provider.should_not_receive( :authenticate )
|
200
|
+
app.auth_provider.should_not_receive( :authorize )
|
201
|
+
|
202
|
+
app.handle( req )
|
203
|
+
end
|
204
|
+
|
205
|
+
it "passes a request through auth if the path doesn't match" do
|
206
|
+
req = @request_factory.get( '/api/v1/console' )
|
207
|
+
|
208
|
+
app = @app.new
|
209
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
210
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
211
|
+
|
212
|
+
app.handle( req )
|
213
|
+
end
|
214
|
+
|
215
|
+
it "passes a request through auth if the path matches, but the the block returns false" do
|
216
|
+
req = @request_factory.get( '/api/v1/login/validuser' )
|
217
|
+
|
218
|
+
app = @app.new
|
219
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
220
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
221
|
+
|
222
|
+
app.handle( req )
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
context "that has positive auth criteria" do
|
229
|
+
|
230
|
+
before( :each ) do
|
231
|
+
@app.require_auth_for( '/login' )
|
232
|
+
end
|
233
|
+
|
234
|
+
it "knows that it has auth criteria" do
|
235
|
+
@app.should have_auth_criteria()
|
236
|
+
end
|
237
|
+
|
238
|
+
it "passes requests that match through auth" do
|
239
|
+
req = @request_factory.get( '/api/v1/login' )
|
240
|
+
|
241
|
+
app = @app.new
|
242
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
243
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
244
|
+
|
245
|
+
app.handle( req )
|
246
|
+
end
|
247
|
+
|
248
|
+
it "doesn't pass requests that don't match through auth" do
|
249
|
+
req = @request_factory.get( '/api/v1/console' )
|
250
|
+
|
251
|
+
app = @app.new
|
252
|
+
app.auth_provider.should_not_receive( :authenticate )
|
253
|
+
app.auth_provider.should_not_receive( :authorize )
|
254
|
+
|
255
|
+
app.handle( req )
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
|
260
|
+
context "that has a positive auth criteria block" do
|
261
|
+
|
262
|
+
before( :each ) do
|
263
|
+
@app.require_auth_for do |req|
|
264
|
+
req.notes[:require_auth]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
it "knows that it has auth criteria" do
|
269
|
+
@app.should have_auth_criteria()
|
270
|
+
end
|
271
|
+
|
272
|
+
it "passes requests for which the block returns true through auth" do
|
273
|
+
req = @request_factory.get( '/api/v1/login' )
|
274
|
+
req.notes[:require_auth] = true
|
275
|
+
|
276
|
+
app = @app.new
|
277
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'anonymous' )
|
278
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
279
|
+
|
280
|
+
app.handle( req )
|
281
|
+
end
|
282
|
+
|
283
|
+
it "doesn't pass requests for which the block returns false through auth" do
|
284
|
+
req = @request_factory.get( '/api/v1/console' )
|
285
|
+
req.notes[:require_auth] = false
|
286
|
+
|
287
|
+
app = @app.new
|
288
|
+
app.auth_provider.should_not_receive( :authenticate )
|
289
|
+
app.auth_provider.should_not_receive( :authorize )
|
290
|
+
|
291
|
+
app.handle( req )
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
|
296
|
+
context "that has a positive auth criteria with both a pattern and a block" do
|
297
|
+
|
298
|
+
before( :each ) do
|
299
|
+
@app.require_auth_for( %r{^/login/(?<username>\w+)} ) do |req, match|
|
300
|
+
match[:username] != 'guest'
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
it "knows that it has auth criteria" do
|
305
|
+
@app.should have_auth_criteria()
|
306
|
+
end
|
307
|
+
|
308
|
+
it "passes a request through auth if the path matches and the block returns true" do
|
309
|
+
req = @request_factory.get( '/api/v1/login/lyssa' )
|
310
|
+
|
311
|
+
app = @app.new
|
312
|
+
app.auth_provider.should_receive( :authenticate ).and_return( 'lyssa' )
|
313
|
+
app.auth_provider.should_receive( :authorize ).and_return( true )
|
314
|
+
|
315
|
+
app.handle( req )
|
316
|
+
end
|
317
|
+
|
318
|
+
it "doesn't pass a request through auth if the path doesn't match" do
|
319
|
+
req = @request_factory.get( '/api/v1/console' )
|
320
|
+
|
321
|
+
app = @app.new
|
322
|
+
app.auth_provider.should_not_receive( :authenticate )
|
323
|
+
app.auth_provider.should_not_receive( :authorize )
|
324
|
+
|
325
|
+
app.handle( req )
|
326
|
+
end
|
327
|
+
|
328
|
+
it "doesn't pass a request through auth if the path matches, but the the block returns false" do
|
329
|
+
req = @request_factory.get( '/api/v1/login/guest' )
|
330
|
+
|
331
|
+
app = @app.new
|
332
|
+
app.auth_provider.should_not_receive( :authenticate )
|
333
|
+
app.auth_provider.should_not_receive( :authorize )
|
334
|
+
|
335
|
+
app.handle( req )
|
336
|
+
end
|
337
|
+
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
it "can register an authorization callback with a block" do
|
342
|
+
@app.authz_callback { :authz }
|
343
|
+
@app.authz_callback.should be_a( Proc )
|
344
|
+
end
|
345
|
+
|
346
|
+
it "can register an authorization callback with a callable object" do
|
347
|
+
callback = Proc.new { :authz }
|
348
|
+
@app.authz_callback( callback )
|
349
|
+
@app.authz_callback.should == callback
|
350
|
+
end
|
351
|
+
|
352
|
+
|
353
|
+
context "that has an authz callback" do
|
354
|
+
|
355
|
+
before( :each ) do
|
356
|
+
@app.authz_callback { }
|
357
|
+
end
|
358
|
+
|
359
|
+
it "yields authorization to the callback if authentication succeeds"
|
360
|
+
it "responds with a 403 Forbidden response if the block doesn't return true"
|
361
|
+
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
367
|
+
|