pasaporte 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +2 -0
- data/Manifest.txt +41 -0
- data/README.txt +72 -0
- data/Rakefile +111 -0
- data/TODO.txt +2 -0
- data/bin/pasaporte-fcgi.rb +17 -0
- data/lib/pasaporte/.DS_Store +0 -0
- data/lib/pasaporte/assets/.DS_Store +0 -0
- data/lib/pasaporte/assets/bgbar.png +0 -0
- data/lib/pasaporte/assets/lock.png +0 -0
- data/lib/pasaporte/assets/mainbg_green.gif +0 -0
- data/lib/pasaporte/assets/mainbg_red.gif +0 -0
- data/lib/pasaporte/assets/openid.png +0 -0
- data/lib/pasaporte/assets/pasaporte.css +192 -0
- data/lib/pasaporte/assets/pasaporte.js +10 -0
- data/lib/pasaporte/assets/user.png +0 -0
- data/lib/pasaporte/auth/cascade.rb +16 -0
- data/lib/pasaporte/auth/remote_web_workplace.rb +61 -0
- data/lib/pasaporte/auth/yaml_digest_table.rb +23 -0
- data/lib/pasaporte/auth/yaml_table.rb +43 -0
- data/lib/pasaporte/faster_openid.rb +39 -0
- data/lib/pasaporte/iso_countries.yml +247 -0
- data/lib/pasaporte/julik_state.rb +42 -0
- data/lib/pasaporte/markaby_ext.rb +8 -0
- data/lib/pasaporte/pasaporte_store.rb +60 -0
- data/lib/pasaporte/timezones.yml +797 -0
- data/lib/pasaporte.rb +1214 -0
- data/test/fixtures/pasaporte_approvals.yml +12 -0
- data/test/fixtures/pasaporte_profiles.yml +45 -0
- data/test/fixtures/pasaporte_throttles.yml +4 -0
- data/test/helper.rb +66 -0
- data/test/mosquito.rb +596 -0
- data/test/test_approval.rb +33 -0
- data/test/test_auth_backends.rb +59 -0
- data/test/test_openid.rb +363 -0
- data/test/test_pasaporte.rb +326 -0
- data/test/test_profile.rb +165 -0
- data/test/test_settings.rb +27 -0
- data/test/test_throttle.rb +70 -0
- data/test/testable_openid_fetcher.rb +82 -0
- 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(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
|
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 << ' '
|
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 << '  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  "
|
1133
|
+
{'m' => ' a guy ', 'f' => ' a gal '}.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 << "  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
|