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