pasaporte 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.
Files changed (41) hide show
  1. data/History.txt +2 -0
  2. data/Manifest.txt +41 -0
  3. data/README.txt +72 -0
  4. data/Rakefile +111 -0
  5. data/TODO.txt +2 -0
  6. data/bin/pasaporte-fcgi.rb +17 -0
  7. data/lib/pasaporte/.DS_Store +0 -0
  8. data/lib/pasaporte/assets/.DS_Store +0 -0
  9. data/lib/pasaporte/assets/bgbar.png +0 -0
  10. data/lib/pasaporte/assets/lock.png +0 -0
  11. data/lib/pasaporte/assets/mainbg_green.gif +0 -0
  12. data/lib/pasaporte/assets/mainbg_red.gif +0 -0
  13. data/lib/pasaporte/assets/openid.png +0 -0
  14. data/lib/pasaporte/assets/pasaporte.css +192 -0
  15. data/lib/pasaporte/assets/pasaporte.js +10 -0
  16. data/lib/pasaporte/assets/user.png +0 -0
  17. data/lib/pasaporte/auth/cascade.rb +16 -0
  18. data/lib/pasaporte/auth/remote_web_workplace.rb +61 -0
  19. data/lib/pasaporte/auth/yaml_digest_table.rb +23 -0
  20. data/lib/pasaporte/auth/yaml_table.rb +43 -0
  21. data/lib/pasaporte/faster_openid.rb +39 -0
  22. data/lib/pasaporte/iso_countries.yml +247 -0
  23. data/lib/pasaporte/julik_state.rb +42 -0
  24. data/lib/pasaporte/markaby_ext.rb +8 -0
  25. data/lib/pasaporte/pasaporte_store.rb +60 -0
  26. data/lib/pasaporte/timezones.yml +797 -0
  27. data/lib/pasaporte.rb +1214 -0
  28. data/test/fixtures/pasaporte_approvals.yml +12 -0
  29. data/test/fixtures/pasaporte_profiles.yml +45 -0
  30. data/test/fixtures/pasaporte_throttles.yml +4 -0
  31. data/test/helper.rb +66 -0
  32. data/test/mosquito.rb +596 -0
  33. data/test/test_approval.rb +33 -0
  34. data/test/test_auth_backends.rb +59 -0
  35. data/test/test_openid.rb +363 -0
  36. data/test/test_pasaporte.rb +326 -0
  37. data/test/test_profile.rb +165 -0
  38. data/test/test_settings.rb +27 -0
  39. data/test/test_throttle.rb +70 -0
  40. data/test/testable_openid_fetcher.rb +82 -0
  41. metadata +151 -0
data/lib/pasaporte.rb ADDED
@@ -0,0 +1,1214 @@
1
+ require 'rubygems'
2
+ gem 'ruby-openid', '>=2.1.0'
3
+
4
+ $: << File.dirname(__FILE__)
5
+
6
+ %w(
7
+ camping
8
+ camping/session
9
+ openid
10
+ openid/extensions/sreg
11
+ pasaporte/faster_openid
12
+ pasaporte/julik_state
13
+ pasaporte/markaby_ext
14
+ ).each {|r| require r }
15
+
16
+ Camping.goes :Pasaporte
17
+
18
+ Markaby::Builder.set(:output_xml_instruction, false)
19
+
20
+ module Pasaporte
21
+ module Auth; end # Acts as a container for auth classes
22
+
23
+ MAX_FAILED_LOGIN_ATTEMPTS = 3
24
+ THROTTLE_FOR = 2.minutes
25
+ DEFAULT_COUNTRY = 'nl'
26
+ DEFAULT_TZ = 'Europe/Amsterdam'
27
+ ALLOW_DELEGATION = true
28
+ VERSION = '0.0.1'
29
+ SESSION_LIFETIME = 10.hours
30
+
31
+ LOGGER = Logger.new(STDERR) #:nodoc:
32
+ PATH = File.expand_path(__FILE__) #:nodoc:
33
+
34
+ # Stick your super auth HERE. Should be a proc accepting login, pass and domain
35
+ my_little_auth = lambda do | login, pass, domain |
36
+ allowd = {"julian" => "useless"}
37
+ return (allowd[login] && (allowd[login] == pass))
38
+ end
39
+
40
+ AUTH = my_little_auth
41
+
42
+ COUNTRIES = YAML::load(File.read(File.dirname(PATH) + '/pasaporte/iso_countries.yml')) #:nodoc:
43
+ TIMEZONES = YAML::load(File.read(File.dirname(PATH) + '/pasaporte/timezones.yml')).sort{|e1, e2| e1[1] <=> e2[1]} #:nodoc:
44
+
45
+ # Reads and applies pasaporte/config.yml to the constants
46
+ def self.apply_config!
47
+ silence_warnings do
48
+ paths_to_analyze = [ENV['HOME'] + '/pasaporte-config.yml', File.dirname(PATH) + '/pasaporte/config.yml']
49
+ paths_to_analyze.each do | config_path |
50
+ begin
51
+ fc = File.read(config_path)
52
+ YAML::load(fc).each_pair do |k, v|
53
+ # Cause us to fail if this constant does not exist
54
+ norm = k.to_s.upcase.to_sym
55
+ const_get(norm); const_set(norm, v)
56
+ end
57
+ rescue Errno::ENOENT # silence
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # Adds a magicvar that gets cleansed akin to Rails Flash
64
+ module CampingFlash
65
+
66
+ def show_error(message)
67
+ @err = message
68
+ end
69
+
70
+ def show_message(message)
71
+ @msg = message
72
+ end
73
+
74
+ def service(*a)
75
+ @msg, @err = [:msg, :err].map{|e| @state.delete(e) },
76
+ super(*a)
77
+ end
78
+
79
+ def redirect(*argz)
80
+ @state.msg, @state.err = @msg, @err
81
+ super(*argz)
82
+ end
83
+ end
84
+
85
+ module LoggerHijack
86
+ #def service(*the_rest)
87
+ # _hijacked, OpenID::Util.logger = OpenID::Util.logger, LOGGER
88
+ # returning(super(*the_rest)) { OpenID::Util.logger = _hijacked }
89
+ #end
90
+ end
91
+
92
+ # Or a semblance thereof
93
+ module Secure
94
+ class PleaseLogin < RuntimeError; end
95
+ class Throttled < RuntimeError; end
96
+ class FullStop < RuntimeError; end
97
+
98
+ module CheckMethods
99
+ def _redir_to_login_page!(persistables)
100
+ @state.merge!(persistables)
101
+ # Prevent Camping from munging our URI with the mountpoint
102
+ @state.msg ||= "First you will need to login"
103
+ LOGGER.info "Suspending #{@nickname} until he is logged in"
104
+ raise PleaseLogin
105
+ end
106
+
107
+ # Allow the user to log in. Suspend any variables passed in the session.
108
+ def require_login(persistables = {})
109
+ deny_throttled!
110
+ raise "No nickname" unless @nickname
111
+ _redir_to_login_page!(persistables) unless is_logged_in?
112
+ @profile = profile_by_nickname(@nickname)
113
+ @title = "%s's pasaporte" % @nickname
114
+ end
115
+
116
+ # Deny throttled users any action
117
+ def deny_throttled!
118
+ raise Throttled if Models::Throttle.throttled?(env)
119
+ end
120
+
121
+ def profile_by_nickname(n)
122
+ ::Pasaporte::Models::Profile.find_or_create_by_nickname_and_domain_name(n, my_domain)
123
+ end
124
+ end
125
+
126
+ def expire_old_sessions!
127
+ day_zero = (Time.now - SESSION_LIFETIME).to_s(:db)
128
+ # JulikState.expire_old(SESSION_LIFETIME)
129
+ # Camping::Models::Session.delete_all("created_at < '%s'" % day_zero)
130
+ end
131
+
132
+ def service(*a)
133
+ begin
134
+ expire_old_sessions!
135
+ @ctr = self.class.to_s.split('::').pop
136
+ super(*a)
137
+ rescue FullStop
138
+ return self
139
+ rescue PleaseLogin
140
+ LOGGER.info "#{env['REMOTE_ADDR']} - Redirecting to signon #{@cookies.inspect}"
141
+ redirect R(Pasaporte::Controllers::Signon, @nickname)
142
+ return self
143
+ rescue Throttled
144
+ LOGGER.info "#{env['REMOTE_ADDR']} - Throttled user tried again"
145
+ redirect R(Pasaporte::Controllers::ThrottledPage)
146
+ return self
147
+ end
148
+ self
149
+ end
150
+ end
151
+
152
+ # Camping bug workaround - on redirect the cookie header is not set
153
+ module CookiePreservingRedirect
154
+ def redirect(*args)
155
+ @headers['Set-Cookie'] = @cookies.map { |k,v| "#{k}=#{C.escape(v)}; path=#{self/"/"}" if v != @k[k] } - [nil]
156
+ super(*args)
157
+ end
158
+ end
159
+
160
+ # The order here is important. Camping::Session has to come LAST (innermost)
161
+ # otherwise you risk losing the session if one of the services upstream
162
+ # redirects.
163
+ [CampingFlash, CookiePreservingRedirect, Secure, JulikState, LoggerHijack].map{|m| include m }
164
+
165
+
166
+ module Models
167
+ MAX = :limit # Thank you rails core, it was MAX before
168
+ class CreatePasaporte < V 1.0
169
+ def self.up
170
+ create_table :pasaporte_profiles, :force => true do |t|
171
+ # http://openid.net/specs/openid-simple-registration-extension-1_0.html
172
+ t.column :nickname, :string, MAX => 20
173
+ t.column :email, :string, MAX => 70
174
+ t.column :fullname, :string, MAX => 50
175
+ t.column :dob, :date, :null => true
176
+ t.column :gender, :string, MAX => 1
177
+ t.column :postcode, :string, MAX => 10
178
+ t.column :country, :string, MAX => 2
179
+ t.column :language, :string, MAX => 5
180
+ t.column :timezone, :string, MAX => 50
181
+
182
+ # And our extensions
183
+ # is the profile shared (visible to others)
184
+ t.column :shared, :boolean, :default => false
185
+
186
+ # his bio
187
+ t.column :info, :text
188
+
189
+ # when he last used Pasaporte
190
+ t.column :last_login, :datetime
191
+
192
+ # the encryption part that we generate for every user, the other is the pass
193
+ # the total encryption key for private data will be stored in the session only when
194
+ # the user is logged in
195
+ t.column :secret_salt, :integer
196
+
197
+ # Good servers delegate
198
+ t.column :openid_server, :string
199
+ t.column :openid_delegate, :string
200
+
201
+ # We shard by domain
202
+ t.column :domain_name, :string, :null => false, :default => 'localhost'
203
+
204
+ # Keep a close watch on those who
205
+ t.column :throttle_count, :integer, :default => 0
206
+ t.column :suspicious, :boolean, :default => false
207
+ end
208
+
209
+ add_index(:pasaporte_profiles, [:nickname, :domain_name], :unique)
210
+
211
+ create_table :pasaporte_settings do |t|
212
+ t.column :setting, :string
213
+ t.column :value, :binary
214
+ end
215
+
216
+ create_table :pasaporte_associations do |t|
217
+ # server_url is blob, because URLs could be longer
218
+ # than db can handle as a string
219
+ t.column :server_url, :binary
220
+ t.column :handle, :string
221
+ t.column :secret, :binary
222
+ t.column :issued, :integer
223
+ t.column :lifetime, :integer
224
+ t.column :assoc_type, :string
225
+ end
226
+
227
+ create_table :pasaporte_nonces do |t|
228
+ t.column :nonce, :string
229
+ t.column :created, :integer
230
+ end
231
+
232
+ create_table :pasaporte_throttles do |t|
233
+ t.column :created_at, :datetime
234
+ t.column :client_fingerprint, :string, MAX => 40
235
+ end
236
+ end
237
+
238
+ def self.down
239
+ drop_table :pasaporte_profiles
240
+ drop_table :pasaporte_settings
241
+ drop_table :pasaporte_associations
242
+ drop_table :pasaporte_nonces
243
+ drop_table :pasaporte_throttles
244
+ end
245
+ end
246
+ class AddAprovals < V(1.1)
247
+ def self.up
248
+ create_table :pasaporte_approvals do | t |
249
+ t.column :profile_id, :integer, :null => false
250
+ t.column :trust_root, :string, :null => false
251
+ end
252
+ add_index(:pasaporte_approvals, [:profile_id, :trust_root], :unique)
253
+ end
254
+
255
+ def self.down
256
+ drop_table :pasaporte_approvals
257
+ end
258
+ end
259
+
260
+ class MigrateOpenidTables < V(1.2)
261
+ def self.up
262
+ drop_table :pasaporte_settings
263
+ drop_table :pasaporte_nonces
264
+ create_table :pasaporte_nonces, :force => true do |t|
265
+ t.column :server_url, :string, :null => false
266
+ t.column :timestamp, :integer, :null => false
267
+ t.column :salt, :string, :null => false
268
+ end
269
+ end
270
+
271
+ def self.down
272
+ drop_table :pasaporte_nonces
273
+ create_table :pasaporte_nonces, :force => true do |t|
274
+ t.column "nonce", :string
275
+ t.column "created", :integer
276
+ end
277
+
278
+ create_table :pasaporte_settings, :force => true do |t|
279
+ t.column "setting", :string
280
+ t.column "value", :binary
281
+ end
282
+ end
283
+ end
284
+
285
+ class ShardOpenidTables < V(1.3)
286
+ def self.up
287
+ add_column :pasaporte_associations, :pasaporte_domain, :string, :null => false, :default => 'localhost'
288
+ add_column :pasaporte_nonces, :pasaporte_domain, :string, :null => false, :default => 'localhost'
289
+ end
290
+
291
+ def self.down
292
+ remove_column :pasaporte_nonces, :pasaporte_domain
293
+ remove_column :pasaporte_associations, :pasaporte_domain
294
+ end
295
+ end
296
+
297
+ # Minimal info we store about people. It's the container for the sreg data
298
+ # in the first place.
299
+ class Profile < Base
300
+ before_create { |p| p.secret_salt = rand(Time.now) }
301
+ before_save :validate_delegate_uris
302
+ validates_presence_of :nickname
303
+ validates_presence_of :domain_name
304
+ validates_uniqueness_of :nickname, :scope => :domain_name
305
+ attr_protected :domain_name, :nickname
306
+ has_many :approvals, :dependent => :delete_all
307
+
308
+ any_url_present = lambda do |r|
309
+ !r.openid_server.blank? || !r.openid_server.blank?
310
+ end
311
+ %w(openid_server openid_delegate).map do | c |
312
+ validates_presence_of c, :if => any_url_present
313
+ end
314
+
315
+ # Convert the profile to sreg according to the spec (YYYY-MM-DD for dob and such)
316
+ def to_sreg_fields(fields_to_extract = nil)
317
+ fields_to_extract ||= %w( nickname email fullname dob gender postcode country language timezone )
318
+ fields_to_extract.inject({}) do | out, field |
319
+ v = self[field]
320
+ v.blank? ? out : (out[field.to_s] = v.to_s; out)
321
+ end
322
+ end
323
+
324
+ # We have to override that because we want our protected attributes
325
+ def self.find_or_create_by_nickname_and_domain_name(nick, domain)
326
+ returning(super(nick, domain)) do | me |
327
+ ((me.nickname, me.domain_name = nick, domain) && me.save) if me.new_record?
328
+ end
329
+ end
330
+ class << self
331
+ alias_method :find_or_create_by_domain_name_and_nickname,
332
+ :find_or_create_by_nickname_and_domain_name
333
+ end
334
+
335
+ def generate_sess_key
336
+ self.secret_salt ||= rand(Time.now)
337
+ s = [nickname, secret_salt, Time.now.year, Time.now.month].join('|')
338
+ OpenSSL::Digest::SHA1.new(s).hexdigest.to_s
339
+ end
340
+
341
+ # Check if this profile wants us to delegate his openid to a different identity provider.
342
+ # If both delegate and server are filled in properly this will return true
343
+ def delegates_openid?
344
+ ALLOW_DELEGATION && (!openid_server.blank? && !openid_delegate.blank?)
345
+ end
346
+
347
+ def delegates_openid=(nv)
348
+ (self.openid_server, self.openid_delegate = nil, nil) if [false, '0', 0, 'no'].include?(nv)
349
+ end
350
+ alias_method :delegates_openid, :delegates_openid? # for checkboxes
351
+
352
+ def to_s; nickname; end
353
+
354
+ private
355
+ def validate_delegate_uris
356
+ if ([self.openid_server, self.openid_delegate].select{|i| i.blank?}).length == 1
357
+ errors.add(:delegate_server, "If you use delegation you have to specify both addresses")
358
+ false
359
+ end
360
+
361
+ %w(openid_server openid_delegate).map do | attr |
362
+ return if self[attr].blank?
363
+ begin
364
+ self[attr] = OpenID::URINorm.urinorm(self[attr])
365
+ rescue Exception => e
366
+ errors.add(attr, e.message)
367
+ end
368
+ end
369
+ end
370
+ end
371
+
372
+ # A token that the user has approved a site (a site's trust root) as legal
373
+ # recipient of his information
374
+ class Approval < Base
375
+ belongs_to :profile
376
+ validates_presence_of :profile_id, :trust_root
377
+ validates_uniqueness_of :trust_root, :scope => :profile_id
378
+ def to_s; trust_root; end
379
+ end
380
+
381
+ # Openid setting
382
+ class Setting < Base; end
383
+
384
+ # Openid nonces
385
+ class Nonce < Base; end
386
+
387
+ # Openid assocs
388
+ class Association < Base
389
+ def from_record
390
+ OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
391
+ end
392
+
393
+ def expired?
394
+ Time.now.to_i > (issued + lifetime)
395
+ end
396
+ end
397
+
398
+ # Set throttles
399
+ class Throttle < Base
400
+
401
+ # Set a throttle with the environment of the request
402
+ def self.set!(e)
403
+ create(:client_fingerprint => env_hash(e))
404
+ end
405
+
406
+ # Check if an environment is throttled
407
+ def self.throttled?(e)
408
+ prune!
409
+ count(:conditions => ["client_fingerprint = ? AND created_at > ?", env_hash(e), cutoff]) > 0
410
+ end
411
+
412
+ private
413
+ def self.prune!; delete_all "created_at < '#{cutoff.to_s(:db)}'"; end
414
+ def self.cutoff; Time.now - THROTTLE_FOR; end
415
+ def self.env_hash(e)
416
+ OpenSSL::Digest::SHA1.new([e['REMOTE_ADDR'], e['HTTP_USER_AGENT']].map(&:to_s).join('|')).to_s
417
+ end
418
+ end
419
+ end
420
+
421
+ require File.dirname(__FILE__) + '/pasaporte/pasaporte_store'
422
+
423
+ module Controllers
424
+
425
+ # Wraps the "get" and "post" with nickname passed in the path. When
426
+ # get_with_nick or post_with_nick is called, @nickname is already there.
427
+ module Nicknames
428
+ def get(*extras)
429
+ raise "Nickname is required for this action" unless (@nickname = extras.shift)
430
+ raise "#{self.class} does not respond to get_with_nick" unless respond_to?(:get_with_nick)
431
+ get_with_nick(*extras)
432
+ end
433
+
434
+ def post(*extras)
435
+ raise "Nickname is required for this action" unless (@nickname = extras.shift)
436
+ raise "#{self.class} does not respond to post_with_nick" unless respond_to?(:post_with_nick)
437
+ post_with_nick(*extras)
438
+ end
439
+
440
+ # So that we can define put_with_nick and Camping sees it as #put being available
441
+ def respond_to?(m, *whatever)
442
+ super(m.to_sym) || super("#{m}_with_nick".to_sym)
443
+ end
444
+ end
445
+
446
+ # Make a controller for a specfic user
447
+ def self.personal(uri = nil)
448
+ returning(R(['/([0-9A-Za-z\-\_]+)', uri].compact.join("/"))) do | ak |
449
+ ak.send(:include, Secure::CheckMethods)
450
+ ak.send(:include, Nicknames)
451
+ end
452
+ end
453
+
454
+ # Performs the actual OpenID tasks. POST is for the
455
+ # requesting party, GET is for the browser
456
+ class Openid < personal(:openid)
457
+ include OpenID::Server
458
+
459
+ class Err < RuntimeError; end #:nodoc
460
+ class NeedsApproval < RuntimeError; end #:nodoc
461
+ class Denied < Err; end #:nodoc
462
+ class NoOpenidRequest < RuntimeError; end #:nodoc
463
+
464
+ def get_with_nick
465
+ begin
466
+ @oid_request = openid_request_from_input_or_session
467
+
468
+ LOGGER.info "pasaporte: user #{@nickname} must not be throttled"
469
+ deny_throttled!
470
+
471
+ LOGGER.info "pasaporte: nick must match the identity URL"
472
+ check_nickname_matches_identity_url
473
+
474
+ LOGGER.info "pasaporte: user must be logged in"
475
+ check_logged_in
476
+
477
+ @profile = profile_by_nickname(@nickname)
478
+
479
+ LOGGER.info "pasaporte: trust root is on the approvals list"
480
+ check_if_previously_approved
481
+
482
+ LOGGER.info "pasaporte: OpenID verified, redirecting"
483
+
484
+ succesful_resp = @oid_request.answer(true)
485
+ add_sreg(@oid_request, succesful_resp)
486
+ send_openid_response(succesful_resp)
487
+ rescue NoOpenidRequest
488
+ return 'This is an OpenID server endpoint.'
489
+ rescue ProtocolError => e
490
+ LOGGER.error "pasaporte: Cannot decode the OpenID request - #{e.message}"
491
+ return "Something went wrong processing your request"
492
+ rescue PleaseLogin => e
493
+ # There is a subtlety here. If the user had NO session before entering
494
+ # this, he will get a new SID upon arriving at the signon page and thus
495
+ # will loose his openid request
496
+ force_session_save!
497
+ LOGGER.warn "pasaporte: suspend - the user needs to login first, saving session"
498
+ @oid_request.immediate ? ask_user_to_approve : (raise e)
499
+ rescue NeedsApproval
500
+ LOGGER.warn "pasaporte: suspend - the URL needs approval first"
501
+ ask_user_to_approve
502
+ rescue Denied => d
503
+ LOGGER.warn "pasaporte: deny OpenID to #{@nickname} - #{d.message}"
504
+ send_openid_response(@oid_request.answer(false))
505
+ rescue Secure::Throttled => e
506
+ LOGGER.warn "pasaporte: deny OpenID to #{@nickname} - user is throttled"
507
+ send_openid_response(@oid_request.answer(false))
508
+ end
509
+ end
510
+
511
+ def post_with_nick
512
+ req = openid_server.decode_request(input)
513
+ raise ProtocolError, "The decoded request was nil" if req.nil?
514
+ # Check for dumb mode HIER!
515
+ resp = openid_server.handle_request(req)
516
+ # we need to preserve the session on POST actions
517
+ send_openid_response(resp, true)
518
+ end
519
+
520
+ private
521
+
522
+ def openid_request_from_input_or_session
523
+ if input.keys.grep(/openid/).any?
524
+ @state.delete(:pending_openid)
525
+ r = openid_server.decode_request(input)
526
+ LOGGER.info "Starting a new OpenID session with #{r.trust_root}"
527
+ @state.pending_openid = r
528
+ elsif @state.pending_openid
529
+ LOGGER.info "Resuming an OpenID session with #{@state.pending_openid.trust_root}"
530
+ @state.pending_openid
531
+ else
532
+ raise NoOpenidRequest
533
+ end
534
+ end
535
+
536
+ def check_nickname_matches_identity_url
537
+ nick_from_uri = @oid_request.identity.to_s.split(/\//)[-2]
538
+ if (nick_from_uri != @nickname)
539
+ raise Denied, "The identity '#{@oid_request.claimed_id}' does not mach the URL realm"
540
+ end
541
+
542
+ if (@state.nickname && (nick_from_uri != @state.nickname))
543
+ raise Denied, "The identity '#{@oid_request.claimed_id}' is not the one of the current user"
544
+ end
545
+ end
546
+
547
+ def check_logged_in
548
+ message = "Before authorizing '%s' you will need to login" % input["openid.trust_root"]
549
+ require_login(:pending_openid => @oid_request, :msg => message)
550
+ end
551
+
552
+ def ask_user_to_approve
553
+ @state.pending_openid = @oid_request
554
+ if @oid_request.immediate
555
+ absolutized_signon_url = unless is_logged_in?
556
+ File.dirname(_our_endpoint_uri) + '/signon'
557
+ else
558
+ File.dirname(_our_endpoint_uri) + '/decide'
559
+ end
560
+ LOGGER.info "Will need to ask the remote to setup at #{absolutized_signon_url}"
561
+ resp = @oid_request.answer(false, absolutized_signon_url)
562
+ send_openid_response(resp, true); return
563
+ else
564
+ redirect R(Decide, @nickname)
565
+ end
566
+ end
567
+
568
+ # This has to be fixed. By default, we answer _false_ to immediate mode
569
+ # if we need the user to sign in or do other tasks, this is important
570
+ def check_if_previously_approved
571
+ unless @profile.approvals.find_by_trust_root(@oid_request.trust_root)
572
+ raise NeedsApproval.new(@oid_request)
573
+ end
574
+ end
575
+ end
576
+
577
+ # Return the yadis autodiscovery XML for the user
578
+ class Yadis < personal(:yadis)
579
+ YADIS_TPL = %{
580
+ <xrds:XRDS xmlns:xrds="xri://$xrds"
581
+ xmlns="xri://$xrd*($v*2.0)"
582
+ xmlns:openid="http://openid.net/xmlns/1.0">
583
+ <XRD>
584
+ <Service priority="1">
585
+ <Type>http://openid.net/signon/1.0</Type>
586
+ <URI>%s</URI>
587
+ <openid:Delegate>%s</openid:Delegate>
588
+ </Service>
589
+ </XRD>
590
+ </xrds:XRDS>
591
+ }
592
+
593
+ def get_with_nick
594
+ @headers["Content-type"] = "application/xrds+xml"
595
+ @skip_layout = true
596
+ YADIS_TPL % get_endpoints
597
+ end
598
+
599
+ private
600
+ def get_endpoints
601
+ defaults = [_our_endpoint_uri, _our_endpoint_uri]
602
+ @profile = Profile.find_by_nickname_and_domain_name(@nickname, my_domain)
603
+ return defaults unless @profile && @profile.delegates_openid?
604
+ [@profile.openid_server, @profile.openid_delegate]
605
+ end
606
+ end
607
+
608
+ class ApprovalsPage < personal(:approvals)
609
+ def get_with_nick
610
+ require_login
611
+ @approvals = @profile.approvals
612
+ if @approvals.empty?
613
+ @msg = 'You currently do not have any associations with other sites through us'
614
+ return redirect(DashPage, @nickname)
615
+ end
616
+
617
+ render :approvals_page
618
+ end
619
+ end
620
+
621
+ class DeleteApproval < personal('disapprove/(\d+)')
622
+ def get_with_nick(appr_id)
623
+ require_login
624
+ ap = @profile.approvals.find(appr_id); ap.destroy
625
+ show_message "The site #{ap} has been removed from your approvals list"
626
+ redirect R(ApprovalsPage, @nickname)
627
+ end
628
+ alias_method :post_with_nick, :get_with_nick
629
+ end
630
+
631
+ # Show the login form and accept the input
632
+ class Signon < R('/', "/([^\/]+)/signon")
633
+ include Secure::CheckMethods
634
+
635
+ def get(nick=nil)
636
+ LOGGER.info "Entered signon with #{@cookies.inspect}"
637
+ deny_throttled!
638
+ return redirect(DashPage, @state.nickname) if @state.nickname
639
+ if nick && @state.pending_openid
640
+ humane = URI.parse(@state.pending_openid.trust_root).host
641
+ show_message "Before authorizing with <b>#{humane}</b> you will need to login"
642
+ end
643
+ @nickname = nick;
644
+ render :signon_form
645
+ end
646
+
647
+ def post(n=nil)
648
+ begin
649
+ deny_throttled!
650
+ rescue Pasaporte::Secure::Throttled => th
651
+ if @state.pending_openid
652
+ buggeroff = @state.delete(:pending_openid).answer(false)
653
+ send_openid_response(buggeroff); return
654
+ end
655
+ raise th
656
+ end
657
+ @nickname = @input.login || n || (raise "No nickname to authenticate")
658
+ # The throttling logic must be moved into throttles apparently
659
+ # Start counting
660
+ @state.failed_logins ||= 0
661
+
662
+ # If the user reaches the failed login limit we ban him for a while and
663
+ # tell the OpenID requesting party to go away
664
+ if Pasaporte::AUTH.call(@nickname, input.pass, my_domain)
665
+ LOGGER.info "#{@nickname} logged in, setting state"
666
+ # TODO - Special case - if the login ultimately differs from the one entered
667
+ # we need to take care of that and tell the OID consumer that we want to restart
668
+ # from a different profile URL
669
+ @state.nickname = @nickname
670
+ @profile = profile_by_nickname(@nickname)
671
+
672
+ # Recet the grace counter
673
+ @state.failed_logins = 0
674
+
675
+ # If we have a suspended OpenID procedure going on - continue
676
+ redirect R((@state.pending_openid ? Openid : DashPage), @nickname); return
677
+ else
678
+ show_error "Oops.. cannot find you there"
679
+ # Raise the grace counter
680
+ @state.failed_logins += 1
681
+ if @state.failed_logins >= MAX_FAILED_LOGIN_ATTEMPTS
682
+ LOGGER.info("%s - failed %s times, taking action" %
683
+ [@nickname, MAX_FAILED_LOGIN_ATTEMPTS])
684
+ punish_the_violator
685
+ else
686
+ @state.delete(:nickname)
687
+ render :signon_form
688
+ end
689
+ end
690
+ end
691
+
692
+ private
693
+ def punish_the_violator
694
+ @report = "I am stopping you from sending logins for some time so that you can " +
695
+ "lookup (or remember) your password. See you later."
696
+ Throttle.set!(env)
697
+
698
+ @state.failed_logins = 0
699
+ LOGGER.info "#{env['REMOTE_ADDR']} - Throttling #{@nickname} for #{THROTTLE_FOR} seconds"
700
+
701
+ # If we still have an OpenID request hanging about we need
702
+ # to let the remote party know that there is nothing left to hope for
703
+ if @state.pending_openid
704
+ # Mark the profile as suspicious - failed logins mean that maybe he delegated everything
705
+ # but is no longer listed on the backend
706
+ if p = profile_by_nickname(@nickname)
707
+ p.update_attributes :suspicious => true, :throttle_count => (p.throttle_count + 1)
708
+ if p.throttle_count > 3
709
+ LOGGER.error "#{@nickname}@#{my_domain} has been throttled 3 times but is still trying hard"
710
+ end
711
+ end
712
+ # Reset the state so that the session is regenerated. Something wrong is
713
+ # going on so we better be sure
714
+ @oid_request, @state = @state.delete(:pending_openid), Camping::H.new
715
+
716
+ # And send the flowers away
717
+ bugger_off = @oid_request.answer(false)
718
+ return send_openid_response(bugger_off)
719
+ else
720
+ render :bailout
721
+ end
722
+ end
723
+ end
724
+
725
+ # Logout the user, remove associations and session
726
+ class Signout < personal(:signout)
727
+ def get_with_nick
728
+ (redirect R(Signon, @nickname); return) unless is_logged_in?
729
+ # reset the session in our part only
730
+ @state = Camping::H.new
731
+ @state.msg = "Thanks for using the service and goodbye"
732
+ redirect R(Signon, @nickname)
733
+ end
734
+ end
735
+
736
+ # Figure out if we want to pass info to a site or not. This gets called
737
+ # when associating for the first time
738
+ class Decide < personal(:decide)
739
+ def get_with_nick
740
+ require_login
741
+ @oid_request = @state.pending_openid
742
+ render :decide
743
+ end
744
+
745
+ def post_with_nick
746
+ require_login
747
+ if !@state.pending_openid
748
+ @report = "There is no OpenID request to approve anymore. Looks like it went out already."
749
+ render :bailout
750
+ elsif input.nope
751
+ @oid_request = @state.delete(:pending_openid)
752
+ send_openid_response @oid_request.answer(false); return
753
+ else
754
+ @profile.approvals.create(:trust_root => @state.pending_openid.trust_root)
755
+ redirect R(Openid, @nickname); return
756
+ end
757
+ end
758
+ end
759
+
760
+ # Allows the user to modify the settings
761
+ class EditProfile < personal(:edit)
762
+ def get_with_nick
763
+ require_login
764
+ render :profile_form
765
+ end
766
+
767
+ def post_with_nick
768
+ require_login
769
+ _collapse_checkbox_input(input)
770
+
771
+ if @profile.update_attributes(input.profile)
772
+ show_message "Your settings have been changed"
773
+ redirect R(DashPage, @nickname); return
774
+ end
775
+ show_error "Cannot save your profile: <b>%s</b>" % @profile.errors.full_messages
776
+ render :profile_form
777
+ end
778
+ end
779
+
780
+ # Catchall for static files, with some caching support
781
+ class Assets < R('/assets/(.+)')
782
+ MIME_TYPES = {
783
+ 'html' => 'text/html', 'css' => 'text/css', 'js' => 'text/javascript',
784
+ 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png'
785
+ }
786
+ ASSETS = File.join File.dirname(Pasaporte::PATH), 'pasaporte/assets/'
787
+
788
+ def get(path)
789
+ if env["HTTP_IF_MODIFIED_SINCE"]
790
+ @status = 304
791
+ return 'Not Modified'
792
+ end
793
+
794
+ # Squeeze out all possible directory traversals
795
+ path = File.join ASSETS, path.gsub(/\.\./, '').gsub(/\s/, '')
796
+ path.squeeze!('/')
797
+
798
+ # Somehow determine if we support sendfile, because sometimes we dont
799
+ if File.exist?(path)
800
+ ext = File.extname(path).gsub(/^\./, '')
801
+ magic_headers = {
802
+ 'Last-Modified' => 2.days.ago.to_s(:http),
803
+ 'Expires' => 30.days.from_now.to_s(:http),
804
+ 'Cache-Control' => "public; max-age=#{360.hours}",
805
+ 'Last-Modified' => 2.hours.ago.to_s(:http),
806
+ }
807
+ @headers.merge!(magic_headers)
808
+ @headers['Content-Type'] = MIME_TYPES[ext] || "text/plain"
809
+ @headers['X-Sendfile'] = path
810
+ else
811
+ @status = 404
812
+ self << "No such item"
813
+ end
814
+ end
815
+ end
816
+
817
+ # This gets shown if the user is throttled
818
+ class ThrottledPage < R('/you-have-been-throttled')
819
+ def get
820
+ @report = "You have been throttled for #{THROTTLE_FOR} seconds"
821
+ render :bailout
822
+ end
823
+ end
824
+
825
+ # Just show a public profile page. Before the user logs in for the first time
826
+ # it works like a dummy page. After the user has been succesfully authenticated he can
827
+ # show his personal info on this page (this is his identity URL).
828
+ class ProfilePage < personal
829
+ def get(nick)
830
+ @nickname = nick
831
+ @headers['X-XRDS-Location'] = _our_identity_url + '/yadis'
832
+ @title = "#{@nickname}'s profile"
833
+ @profile = Profile.find_by_nickname_and_domain_name(@nickname, my_domain)
834
+ @no_toolbar = true
835
+ render(@profile && @profile.shared? ? :profile_public_page : :endpoint_splash)
836
+ end
837
+ end
838
+
839
+ class DashPage < personal(:prefs)
840
+ def get_with_nick; require_login; render :dash; end
841
+ end
842
+ end
843
+
844
+ module Helpers
845
+
846
+ def is_logged_in?
847
+ (@state.nickname && (@state.nickname == @nickname))
848
+ end
849
+
850
+ # Return a RELATIVELY reliable domain key
851
+ def my_domain
852
+ env["SERVER_NAME"].gsub(/^www\./i, '').chars.downcase.to_s
853
+ end
854
+
855
+ # Camping processes double values (hidden field with 0 and checkbox with 1) as an array.
856
+ # This collapses the ["0","1"] array to a single value of "1"
857
+ def _collapse_checkbox_input(ha)
858
+ ha.each_pair do | k,v |
859
+ (v == ["0", "1"]) ? (ha[k] = "1") : (v.is_a?(Hash) ? _collapse_checkbox_input(v) : true)
860
+ end
861
+ end
862
+
863
+ def openid_server
864
+ @store ||= PasaporteStore.new
865
+
866
+ # Associations etc are sharded per domain on which Pasaporte sits
867
+ @store.pasaporte_domain = @env['SERVER_NAME']
868
+
869
+ # we also need to provide endopint URL - this is where Pasaporte is mounted
870
+ @server ||= OpenID::Server::Server.new(@store, @env['SERVER_NAME'])
871
+ @server
872
+ end
873
+
874
+ # Add sreg details from the profile to the response
875
+ def add_sreg(request, response)
876
+ when_sreg_is_required(request) do | fields, policy_url |
877
+ addition = OpenID::SReg::Response.new(@profile.to_sreg_fields(fields))
878
+ response.add_extension(addition)
879
+ end
880
+ end
881
+
882
+ # Runs the block if the passed request contains an SReg requirement. Yields
883
+ # an array of fields and the policy URL
884
+ def when_sreg_is_required(openid_request)
885
+ fetch_request = OpenID::SReg::Request.from_openid_request(openid_request)
886
+ return unless fetch_request
887
+ if block_given? && fetch_request.were_fields_requested?
888
+ yield(fetch_request.all_requested_fields, fetch_request.policy_url)
889
+ else
890
+ false
891
+ end
892
+ end
893
+
894
+ # Convert from the absurd response object to something Camping can fret
895
+ # The second argument determines if the session needs to be cleared as well
896
+ def send_openid_response(oid_resp, keep_in_session = false)
897
+ web_response = openid_server.encode_response(oid_resp)
898
+ @state.delete(:pending_openid) unless keep_in_session
899
+ case web_response.code
900
+ when OpenID::Server::HTTP_OK
901
+ @body = web_response.body
902
+ when OpenID::Server::HTTP_REDIRECT
903
+ redirect web_response.headers['location']
904
+ else # This is a 400 response, we do not support something
905
+ @status, @body = 400, web_response.body
906
+ end
907
+ end
908
+
909
+ def _todo(msg=nil)
910
+ # LOGGER.error " "
911
+ # LOGGER.error("FIXME! - %s" % msg)
912
+ # LOGGER.info caller[0..2].join("\n")
913
+ if self.respond_to?(:div)
914
+ div(:style=>'color:red;font-size:1.1em'){"TODO!"}
915
+ end
916
+ end
917
+ end
918
+
919
+ module Views
920
+
921
+ # Harsh but necessary
922
+ def bailout
923
+ h2 "Sorry but it's true"
924
+ self << p(@report)
925
+ end
926
+
927
+ # Render the dash
928
+ def dash
929
+ h2 { "Welcome <b>#{@nickname}</b>, nice to have you back" }
930
+ p { "Your OpenID is <b>#{_our_identity_url}</b>" }
931
+ ul.bigz! do
932
+ li.profButn! { a "Change your profile", :href => R(EditProfile, @nickname) }
933
+ if @profile.approvals.count > 0
934
+ li.apprButn! { a "See the sites that logged you in", :href => R(ApprovalsPage, @nickname) }
935
+ end
936
+ end
937
+ end
938
+
939
+ # Render the public profile page
940
+ def profile_public_page
941
+ h2 do
942
+ _h(@profile.fullname.blank? ? @profile.nickname : @profile.fullname)
943
+ end
944
+ p _h(@profile.info)
945
+ end
946
+
947
+ def signon_form
948
+ form.signon! :method => :post do
949
+ label :for => 'login' do
950
+ self << "Your name:"
951
+ # We include a hidden input here but it's only ever used by PublicSignon
952
+ if @nickname && @state.pending_openid
953
+ b(@nickname)
954
+ input(:name => "login", :value => @nickname, :type => :hidden)
955
+ else
956
+ input.login!(:name => "login", :value => @nickname)
957
+ end
958
+ end
959
+ label :for => 'pass' do
960
+ self << "Your password:"
961
+ input.pass! :name => "pass", :type => "password"
962
+ end
963
+ input :type => :submit, :value => 'Log me in'
964
+ end
965
+ end
966
+
967
+ def layout
968
+ @headers['Cache-Control'] = 'no-cache; must-revalidate'
969
+ if @skip_layout
970
+ self << yield; return
971
+ end
972
+
973
+ @headers['Content-type'] = 'text/html'
974
+ xhtml_transitional do
975
+ head do
976
+ meta 'http-equiv' => 'X-XRDS-Location', :content => (_our_identity_url + '/yadis')
977
+ link :rel => "openid.server", :href => _openid_server_uri
978
+ link :rel => "openid.delegate", :href => _openid_delegate_uri
979
+ link :rel => "stylesheet", :href => _s("pasaporte.css")
980
+ script :type=>'text/javascript', :src => _s("pasaporte.js")
981
+ title(@title || ('%s : pasaporte' % env['SERVER_NAME']))
982
+ end
983
+ body :class => @ctr do
984
+ unless @no_toolbar
985
+ div.toolbar! do
986
+ if is_logged_in?
987
+ b.profBtn! "Logged in as #{@nickname.capitalize}"
988
+ a.loginBtn! "Log out", :href => R(Signout, @nickname)
989
+ else
990
+ b.loginBtn! "You are not logged in"
991
+ end
992
+ img :src => _s('openid.png'), :alt => 'OpenID system'
993
+ end
994
+ end
995
+
996
+ div.work! :class => (is_logged_in? ? "logdin" : "notAuth") do
997
+ returning(@err || @msg) {| m | div.msg!(:class =>(@err ? 'e' : 'm')){m} if m }
998
+ self << yield
999
+ end
1000
+ end
1001
+ end
1002
+ end
1003
+
1004
+ # link to a static file
1005
+ def _s(file)
1006
+ R(Assets, file)
1007
+ end
1008
+
1009
+ # Render either our server URL or the URL of the delegate
1010
+ def _openid_server_uri
1011
+ (@profile && @profile.delegates_openid?) ? @profile.openid_server : _our_endpoint_uri
1012
+ end
1013
+
1014
+ # Render either our providing URL or the URL of the delegate
1015
+ def _openid_delegate_uri
1016
+ (@profile && @profile.delegates_openid?) ? @profile.openid_delegate : _our_endpoint_uri
1017
+ end
1018
+
1019
+ # Canonicalized URL of our endpoint
1020
+ def _our_endpoint_uri
1021
+ uri = "#{@env.HTTPS.to_s.downcase == 'on' ? 'https' : 'http'}://" + [env["HTTP_HOST"], env["SCRIPT_NAME"], R(Openid, @nickname)].join('/').squeeze('/')
1022
+ OpenID::URINorm.urinorm(uri)
1023
+ uri
1024
+ end
1025
+
1026
+ def _our_identity_url
1027
+ uri = "#{@env.HTTPS.to_s.downcase == 'on' ? 'https' : 'http'}://" + [env["HTTP_HOST"], env["SCRIPT_NAME"], R(ProfilePage, @nickname)].join('/').squeeze('/')
1028
+ OpenID::URINorm.urinorm(uri)
1029
+ end
1030
+
1031
+ # HTML esc. snatched off ERB
1032
+ def _h(s)
1033
+ s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
1034
+ end
1035
+
1036
+ # Let the user decide what data he wants to transfer
1037
+ def decide
1038
+ h2{ "Please approve <b>#{_h(@oid_request.trust_root)}</b>" }
1039
+ p "You never logged into that site with us. Do you want us to confirm that you do have an account here?"
1040
+
1041
+ when_sreg_is_required(@oid_request) do | asked_fields, policy |
1042
+ p{ "Additionally, the site wants to know your <b>#{asked_fields.to_sentence}.</b> These will be sent along."}
1043
+ if policy
1044
+ p do
1045
+ self << "That site has a "
1046
+ a("policy on it's handling of user data", :href => policy)
1047
+ self << " that you might want to read beforehand."
1048
+ end
1049
+ end
1050
+ end
1051
+
1052
+ form :method => :post do
1053
+ input :name => :pleasedo, :type => :submit, :value => " Yes, do allow "
1054
+ self << '&#160;'
1055
+ input :name => :nope, :type => :submit, :value => " No, they are villains! "
1056
+ end
1057
+ end
1058
+
1059
+ def _toolbar
1060
+ # my profile button
1061
+ # log me out button
1062
+ end
1063
+
1064
+ def _approval_block(appr)
1065
+ h4 appr
1066
+ a "Remove this site", :href => R(DeleteApproval, @nickname, appr)
1067
+ end
1068
+
1069
+ def approvals_page
1070
+ h2 "The sites you trust"
1071
+ if @approvals.any?
1072
+ p { "These sites will be able to automatically log you in without first asking you to approve" }
1073
+ p { "Removing a site from the list will force that site to ask your permission next time when checking your OpenID" }
1074
+ ul.apprList! do
1075
+ @approvals.map { | ap | li { _approval_block(ap) } }
1076
+ end
1077
+ else
1078
+ p "You did not yet authorize any sites to use your OpenID"
1079
+ end
1080
+ end
1081
+
1082
+ def _tf(obj_name, field, t, opts = {})
1083
+ obj, field_name, id = instance_variable_get("@#{obj_name}"), "#{obj_name}[#{field}]", "#{obj_name}_#{field}"
1084
+ label.fb(:for => id) do
1085
+ self << t
1086
+ input opts.merge(:type => "text", :value => obj[field], :id => id, :name => field_name)
1087
+ end
1088
+ end
1089
+
1090
+ def _cbox(obj_name, field, opts = {})
1091
+ obj, field_name = instance_variable_get("@#{obj_name}"), "#{obj_name}[#{field}]"
1092
+ input :type=>:hidden, :name => field_name, :value => 0
1093
+
1094
+ opts[:id] ||= "#{obj_name}_#{field}"
1095
+ if !!obj.send(field)
1096
+ input opts.merge(:type=>:checkbox, :name => field_name, :value => 1, :checked => :checked)
1097
+ else
1098
+ input opts.merge(:type=>:checkbox, :name => field_name, :value => 1)
1099
+ end
1100
+ end
1101
+
1102
+ def profile_form
1103
+ form(:method => :post) do
1104
+
1105
+ h2 "Your profile"
1106
+
1107
+ label.cblabel :for => :share_info do
1108
+ _cbox :profile, :shared, :id => :share_info
1109
+ self << '&#160; Share your info on your OpenID page'
1110
+ self << " ("
1111
+ b { a(:href => _profile_url, :target => '_new') { _profile_url } }
1112
+ self << ")"
1113
+ end
1114
+
1115
+ div.persinfo! :style => "display: #{@profile.shared? ? 'block' : 'none'}" do
1116
+ textarea(:rows => 10, :name => 'profile[info]') { @profile.info }
1117
+ end
1118
+
1119
+ script(:type=>"text/javascript") do
1120
+ self << %Q[ attachCheckbox("share_info", "persinfo");]
1121
+ end
1122
+
1123
+ h2 "Simple registration info"
1124
+ p.sml 'When you register on some sites they can ' +
1125
+ 'use this info to fill their registration forms ' +
1126
+ 'for you'
1127
+
1128
+ _tf(:profile, :fullname, "Your full name:")
1129
+ _tf(:profile, :email, "Your e-mail:")
1130
+
1131
+ div.rad do
1132
+ self << "You are &#160;"
1133
+ {'m' => '&#160;a guy&#160;', 'f' => '&#160;a gal&#160;'}.each_pair do | v, t |
1134
+ opts = {:id => "gend_#{v}", :type=>:radio, :name => 'profile[gender]', :value => v}
1135
+ opts[:checked] = :checked if @profile.gender == v
1136
+ label(:for => opts[:id]) { input(opts); self << t }
1137
+ end
1138
+ end
1139
+
1140
+ label.sel(:for => 'countries') do
1141
+ self << "Your country of residence"
1142
+ select :id=>:countries, :name => 'profile[country]' do
1143
+ COUNTRIES.values.sort.map do | c |
1144
+ code = COUNTRIES.index(c)
1145
+ opts = {:value => code}
1146
+ if (@profile.country && @profile.country == code) || DEFAULT_COUNTRY == code
1147
+ opts[:selected] = true
1148
+ end
1149
+ option(opts) { c }
1150
+ end
1151
+ end
1152
+ end
1153
+
1154
+ label.sel do
1155
+ self << "Your date of birth"
1156
+ span.inlineSelects do
1157
+ self << date_select(:profile, :dob, :start_year => 1930, :end_year => (Date.today.year - 15))
1158
+ end
1159
+ end
1160
+
1161
+ label "The timezone you are in"
1162
+ select :name => 'profile[timezone]', :style => 'width: 100%' do
1163
+ TIMEZONES.map do | formal, humane |
1164
+ opts = {:value => formal}
1165
+ opts.merge! :selected => :selected if (formal == @profile.timezone)
1166
+ option(humane, opts)
1167
+ end
1168
+ end
1169
+
1170
+ if ALLOW_DELEGATION
1171
+ h2 "OpenID delegation"
1172
+ label.cblabel :for => :delegate do
1173
+ _cbox(:profile, :delegates_openid)
1174
+ self << "&#160; Use another site " +
1175
+ "as your <a href='http://openid.net/wiki/index.php/Delegation'>OpenID provider.</a>"
1176
+ end
1177
+
1178
+ div.smaller :id => 'delegationDetails' do
1179
+ p "The site that you want to use for OpenID should have given you the necessary URLs."
1180
+ _tf(:profile, :openid_server, "Server URL:")
1181
+ _tf(:profile, :openid_delegate, "Delegate URL:")
1182
+ end
1183
+ end
1184
+
1185
+ script(:type=>"text/javascript") do
1186
+ self << %Q[ attachCheckbox("profile_delegates_openid", "delegationDetails");]
1187
+ end
1188
+
1189
+ hr
1190
+ input :type=>:submit, :value=>'Save my settings' # or Cancel
1191
+ end
1192
+ end
1193
+
1194
+ # Canonicalized identity URL of the current profile
1195
+ def _profile_url
1196
+ "http://" + [env["HTTP_HOST"], env["SCRIPT_NAME"], @profile.nickname].join('/').squeeze('/')
1197
+ end
1198
+
1199
+ # A dummy page that gets shown if the user hides his profile info
1200
+ # or he hasn't authenticated with us yet
1201
+ def endpoint_splash
1202
+ h3 { "This is <b>#{@nickname}'s</b> page" }
1203
+ end
1204
+ end
1205
+
1206
+ def self.create
1207
+ JulikState.create_schema
1208
+ self::Models.create_schema
1209
+ LOGGER.warn "Deleting stale sessions"
1210
+ JulikState::State.delete_all
1211
+ LOGGER.warn "Deleting set throttles"
1212
+ self::Models::Throttle.delete_all
1213
+ end
1214
+ end