strelka 0.0.1.pre148 → 0.0.1.pre177

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.
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
+