pasaporte 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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