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.
Files changed (43) hide show
  1. data/ChangeLog +1294 -0
  2. data/IDEAS.rdoc +6 -0
  3. data/Manifest.txt +20 -0
  4. data/README.rdoc +8 -2
  5. data/Rakefile +9 -7
  6. data/examples/auth-demo.rb +32 -0
  7. data/examples/auth-demo2.rb +37 -0
  8. data/examples/auth-form.tmpl +10 -0
  9. data/examples/auth-success.tmpl +3 -0
  10. data/examples/config.yml +12 -0
  11. data/examples/examples.css +4 -0
  12. data/examples/examples.html +31 -0
  13. data/examples/gen-config.rb +5 -2
  14. data/examples/layout.tmpl +31 -0
  15. data/lib/strelka/app/auth.rb +480 -0
  16. data/lib/strelka/app/sessions.rb +8 -6
  17. data/lib/strelka/app/templating.rb +78 -17
  18. data/lib/strelka/app.rb +13 -5
  19. data/lib/strelka/authprovider/basic.rb +134 -0
  20. data/lib/strelka/authprovider/hostaccess.rb +91 -0
  21. data/lib/strelka/authprovider.rb +122 -0
  22. data/lib/strelka/cookie.rb +1 -1
  23. data/lib/strelka/cookieset.rb +1 -1
  24. data/lib/strelka/httprequest/auth.rb +31 -0
  25. data/lib/strelka/logging.rb +69 -14
  26. data/lib/strelka/mixins.rb +35 -65
  27. data/lib/strelka/session/db.rb +115 -0
  28. data/lib/strelka/session/default.rb +38 -49
  29. data/lib/strelka/session.rb +1 -1
  30. data/lib/strelka.rb +4 -1
  31. data/spec/lib/helpers.rb +8 -3
  32. data/spec/strelka/app/auth_spec.rb +367 -0
  33. data/spec/strelka/authprovider/basic_spec.rb +192 -0
  34. data/spec/strelka/authprovider/hostaccess_spec.rb +70 -0
  35. data/spec/strelka/authprovider_spec.rb +99 -0
  36. data/spec/strelka/cookie_spec.rb +1 -1
  37. data/spec/strelka/httprequest/auth_spec.rb +55 -0
  38. data/spec/strelka/httprequest/session_spec.rb +63 -3
  39. data/spec/strelka/session/db_spec.rb +85 -0
  40. data/spec/strelka/session/default_spec.rb +5 -51
  41. data.tar.gz.sig +0 -0
  42. metadata +88 -57
  43. 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
- # #&, #==, #[], #[]=, #assoc, #case, #clear, #compare_by_identity,
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 Enumerable,
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
- @cookie_name = DEFAULT_COOKIE_NAME
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 name of the cookie that stores the session ID
46
- attr_accessor :cookie_name
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 the cookie name
52
- ### the session ID is stored in in responses if the +:cookie_name+ key
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.cookie_name = options[:cookie_name] if options[:cookie_name]
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. You should probably override
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. You should override this,
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.cookie_name ])
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 Mongrel2::Table using the given +hash+ for initial
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, *Hash.instance_methods( false )
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
- response.cookies[ self.class.cookie_name ] = self.session_id
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
@@ -1,4 +1,4 @@
1
- #! -*- ruby -*-
1
+ # -*- ruby -*-
2
2
  # vim: set nosta noet ts=4 sw=4:
3
3
 
4
4
  require 'digest/sha1'
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: fa6133ac407d $
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
- def setup_config_db( dbspec=':memory:' )
117
- Mongrel2::Config.configure( :configdb => dbspec ) unless
118
- Mongrel2::Config.db.uri[ %r{sqlite:/(.*)}, 1 ] == dbspec
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
+