ror-rubycas-server 1.0.a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. data/CHANGELOG +292 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +26 -0
  4. data/README.textile +129 -0
  5. data/Rakefile +1 -0
  6. data/bin/rubycas-server +16 -0
  7. data/lib/casserver.rb +11 -0
  8. data/lib/casserver/authenticators/active_directory_ldap.rb +19 -0
  9. data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
  10. data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
  11. data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
  12. data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
  13. data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
  14. data/lib/casserver/authenticators/base.rb +67 -0
  15. data/lib/casserver/authenticators/client_certificate.rb +47 -0
  16. data/lib/casserver/authenticators/google.rb +58 -0
  17. data/lib/casserver/authenticators/ldap.rb +147 -0
  18. data/lib/casserver/authenticators/ntlm.rb +88 -0
  19. data/lib/casserver/authenticators/open_id.rb +22 -0
  20. data/lib/casserver/authenticators/sql.rb +133 -0
  21. data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
  22. data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
  23. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  24. data/lib/casserver/authenticators/sql_rest_auth.rb +85 -0
  25. data/lib/casserver/authenticators/test.rb +22 -0
  26. data/lib/casserver/cas.rb +315 -0
  27. data/lib/casserver/localization.rb +91 -0
  28. data/lib/casserver/model.rb +270 -0
  29. data/lib/casserver/options_hash.rb +44 -0
  30. data/lib/casserver/server.rb +706 -0
  31. data/lib/casserver/utils.rb +32 -0
  32. data/lib/casserver/views/_login_form.erb +42 -0
  33. data/lib/casserver/views/layout.erb +18 -0
  34. data/lib/casserver/views/login.erb +30 -0
  35. data/lib/casserver/views/proxy.builder +12 -0
  36. data/lib/casserver/views/proxy_validate.builder +25 -0
  37. data/lib/casserver/views/service_validate.builder +18 -0
  38. data/lib/casserver/views/validate.erb +2 -0
  39. data/po/de_DE/rubycas-server.po +127 -0
  40. data/po/es_ES/rubycas-server.po +123 -0
  41. data/po/fr_FR/rubycas-server.po +128 -0
  42. data/po/ja_JP/rubycas-server.po +126 -0
  43. data/po/pl_PL/rubycas-server.po +123 -0
  44. data/po/pt_BR/rubycas-server.po +123 -0
  45. data/po/ru_RU/rubycas-server.po +118 -0
  46. data/po/rubycas-server.pot +112 -0
  47. data/po/zh_CN/rubycas-server.po +113 -0
  48. data/po/zh_TW/rubycas-server.po +113 -0
  49. data/public/themes/cas.css +121 -0
  50. data/public/themes/notice.png +0 -0
  51. data/public/themes/ok.png +0 -0
  52. data/public/themes/simple/bg.png +0 -0
  53. data/public/themes/simple/favicon.png +0 -0
  54. data/public/themes/simple/login_box_bg.png +0 -0
  55. data/public/themes/simple/logo.png +0 -0
  56. data/public/themes/simple/theme.css +28 -0
  57. data/public/themes/urbacon/bg.png +0 -0
  58. data/public/themes/urbacon/login_box_bg.png +0 -0
  59. data/public/themes/urbacon/logo.png +0 -0
  60. data/public/themes/urbacon/theme.css +33 -0
  61. data/public/themes/warning.png +0 -0
  62. data/resources/init.d.sh +58 -0
  63. data/rubycas-server.gemspec +57 -0
  64. data/setup.rb +1585 -0
  65. data/spec/alt_config.yml +50 -0
  66. data/spec/authenticators/ldap_spec.rb +53 -0
  67. data/spec/casserver_spec.rb +141 -0
  68. data/spec/database.yml +5 -0
  69. data/spec/default_config.yml +73 -0
  70. data/spec/model_spec.rb +42 -0
  71. data/spec/options_hash_spec.rb +146 -0
  72. data/spec/spec.opts +4 -0
  73. data/spec/spec_helper.rb +90 -0
  74. data/spec/utils_spec.rb +53 -0
  75. data/tasks/bundler.rake +4 -0
  76. data/tasks/db/migrate.rake +12 -0
  77. data/tasks/localization.rake +13 -0
  78. data/tasks/spec.rake +10 -0
  79. metadata +356 -0
@@ -0,0 +1,91 @@
1
+ require "gettext"
2
+ require "gettext/cgi"
3
+ require 'active_support'
4
+
5
+ module CASServer
6
+ module Localization
7
+ def self.included(mod)
8
+ mod.module_eval do
9
+ include GetText
10
+ end
11
+ end
12
+
13
+ include GetText
14
+ bindtextdomain("rubycas-server", :path => File.join(File.dirname(File.expand_path(__FILE__)), "../../locale"))
15
+
16
+ def determine_locale(request)
17
+ source = nil
18
+ lang = case
19
+ when !request.params['lang'].blank?
20
+ source = "'lang' request variable"
21
+ request.cookies['lang'] = request.params['lang']
22
+ request.params['lang']
23
+ when !request.cookies['lang'].blank?
24
+ source = "'lang' cookie"
25
+ request.cookies['lang']
26
+ when !request.env['HTTP_ACCEPT_LANGUAGE'].blank?
27
+ source = "'HTTP_ACCEPT_LANGUAGE' header"
28
+ lang = request.env['HTTP_ACCEPT_LANGUAGE']
29
+ when !request.env['HTTP_USER_AGENT'].blank? && request.env['HTTP_USER_AGENT'] =~ /[^a-z]([a-z]{2}(-[a-z]{2})?)[^a-z]/i
30
+ source = "'HTTP_USER_AGENT' header"
31
+ $~[1]
32
+ # when !$CONF['default_locale'].blank?
33
+ # source = "'default_locale' config option"
34
+ # $CONF[:default_locale]
35
+ else
36
+ source = "default"
37
+ "en"
38
+ end
39
+
40
+ $LOG.debug "Detected locale is #{lang.inspect} (from #{source})"
41
+
42
+ lang.gsub!('_','-')
43
+
44
+ # TODO: Need to confirm that this method of splitting the accepted
45
+ # language string is correct.
46
+ if lang =~ /[,;\|]/
47
+ langs = lang.split(/[,;\|]/)
48
+ else
49
+ langs = [lang]
50
+ end
51
+
52
+ # TODO: This method of selecting the desired language might not be
53
+ # standards-compliant. For example, http://www.w3.org/TR/ltli/
54
+ # suggests that de-de and de-*-DE might be acceptable identifiers
55
+ # for selecting various wildcards. The algorithm below does not
56
+ # currently support anything like this.
57
+
58
+ available = available_locales
59
+
60
+ if available.length == 1
61
+ $LOG.warn "Only the #{available.first.inspect} localization is available. You should run `rake localization:mo` to compile support for additional languages!"
62
+ elsif available.length == 0 # this should never actually happen
63
+ $LOG.error "No localizations available! Run `rake localization:mo` to compile support for additional languages."
64
+ end
65
+
66
+ # Try to pick a locale exactly matching the desired identifier, otherwise
67
+ # fall back to locale without region (i.e. given "en-US; de-DE", we would
68
+ # first look for "en-US", then "en", then "de-DE", then "de").
69
+
70
+ chosen_lang = nil
71
+ langs.each do |l|
72
+ a = available.find{ |a| a =~ Regexp.new("\\A#{l}\\Z", 'i') ||
73
+ a =~ Regexp.new("#{l}-\w*", 'i') }
74
+ if a
75
+ chosen_lang = a
76
+ break
77
+ end
78
+ end
79
+
80
+ chosen_lang = "en" if chosen_lang.blank?
81
+
82
+ $LOG.debug "Chosen locale is #{chosen_lang.inspect}"
83
+
84
+ return chosen_lang
85
+ end
86
+
87
+ def available_locales
88
+ (Dir.glob(File.join(File.dirname(File.expand_path(__FILE__)), "../../locale/[a-z]*")).map{|path| File.basename(path)} << "en").uniq.collect{|l| l.gsub('_','-')}
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,270 @@
1
+ require 'active_record'
2
+ require 'active_record/base'
3
+
4
+ module CASServer::Model
5
+
6
+ module Consumable
7
+ def consume!
8
+ self.consumed = Time.now
9
+ self.save!
10
+ end
11
+
12
+ def self.included(mod)
13
+ mod.extend(ClassMethods)
14
+ end
15
+
16
+ module ClassMethods
17
+ def cleanup(max_lifetime, max_unconsumed_lifetime)
18
+ transaction do
19
+ conditions = ["created_on < ? OR (consumed IS NULL AND created_on < ?)",
20
+ Time.now - max_lifetime,
21
+ Time.now - max_unconsumed_lifetime]
22
+ puts all.count
23
+ expired_tickets_count = count(:conditions => conditions)
24
+
25
+ $LOG.debug("Destroying #{expired_tickets_count} expired #{self.name.demodulize}"+
26
+ "#{'s' if expired_tickets_count > 1}.") if expired_tickets_count > 0
27
+
28
+ destroy_all(conditions)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ class Base < ActiveRecord::Base
35
+ end
36
+
37
+ class Ticket < Base
38
+ def to_s
39
+ ticket
40
+ end
41
+
42
+ def self.cleanup(max_lifetime)
43
+ transaction do
44
+ conditions = ["created_on < ?", Time.now - max_lifetime]
45
+ expired_tickets_count = count(:conditions => conditions)
46
+
47
+ $LOG.debug("Destroying #{expired_tickets_count} expired #{self.name.demodulize}"+
48
+ "#{'s' if expired_tickets_count > 1}.") if expired_tickets_count > 0
49
+
50
+ destroy_all(conditions)
51
+ end
52
+ end
53
+ end
54
+
55
+ class LoginTicket < Ticket
56
+ set_table_name 'casserver_lt'
57
+ include Consumable
58
+ end
59
+
60
+ class ServiceTicket < Ticket
61
+ set_table_name 'casserver_st'
62
+ include Consumable
63
+
64
+ belongs_to :granted_by_tgt,
65
+ :class_name => 'CASServer::Model::TicketGrantingTicket',
66
+ :foreign_key => :granted_by_tgt_id
67
+ has_one :proxy_granting_ticket,
68
+ :foreign_key => :created_by_st_id
69
+
70
+ def matches_service?(service)
71
+ CASServer::CAS.clean_service_url(self.service) ==
72
+ CASServer::CAS.clean_service_url(service)
73
+ end
74
+ end
75
+
76
+ class ProxyTicket < ServiceTicket
77
+ belongs_to :granted_by_pgt,
78
+ :class_name => 'CASServer::Model::ProxyGrantingTicket',
79
+ :foreign_key => :granted_by_pgt_id
80
+ end
81
+
82
+ class TicketGrantingTicket < Ticket
83
+ set_table_name 'casserver_tgt'
84
+
85
+ serialize :extra_attributes
86
+
87
+ has_many :granted_service_tickets,
88
+ :class_name => 'CASServer::Model::ServiceTicket',
89
+ :foreign_key => :granted_by_tgt_id
90
+ end
91
+
92
+ class ProxyGrantingTicket < Ticket
93
+ set_table_name 'casserver_pgt'
94
+ belongs_to :service_ticket
95
+ has_many :granted_proxy_tickets,
96
+ :class_name => 'CASServer::Model::ProxyTicket',
97
+ :foreign_key => :granted_by_pgt_id
98
+ end
99
+
100
+ class Error
101
+ attr_reader :code, :message
102
+
103
+ def initialize(code, message)
104
+ @code = code
105
+ @message = message
106
+ end
107
+
108
+ def to_s
109
+ message
110
+ end
111
+ end
112
+
113
+ # class CreateCASServer < V 0.1
114
+ # def self.up
115
+ # if ActiveRecord::Base.connection.table_alias_length > 30
116
+ # $LOG.info("Creating database with long table names...")
117
+ #
118
+ # create_table :casserver_login_tickets, :force => true do |t|
119
+ # t.column :ticket, :string, :null => false
120
+ # t.column :created_on, :timestamp, :null => false
121
+ # t.column :consumed, :datetime, :null => true
122
+ # t.column :client_hostname, :string, :null => false
123
+ # end
124
+ #
125
+ # create_table :casserver_service_tickets, :force => true do |t|
126
+ # t.column :ticket, :string, :null => false
127
+ # t.column :service, :string, :null => false
128
+ # t.column :created_on, :timestamp, :null => false
129
+ # t.column :consumed, :datetime, :null => true
130
+ # t.column :client_hostname, :string, :null => false
131
+ # t.column :username, :string, :null => false
132
+ # t.column :type, :string, :null => false
133
+ # t.column :proxy_granting_ticket_id, :integer, :null => true
134
+ # end
135
+ #
136
+ # create_table :casserver_ticket_granting_tickets, :force => true do |t|
137
+ # t.column :ticket, :string, :null => false
138
+ # t.column :created_on, :timestamp, :null => false
139
+ # t.column :client_hostname, :string, :null => false
140
+ # t.column :username, :string, :null => false
141
+ # end
142
+ #
143
+ # create_table :casserver_proxy_granting_tickets, :force => true do |t|
144
+ # t.column :ticket, :string, :null => false
145
+ # t.column :created_on, :timestamp, :null => false
146
+ # t.column :client_hostname, :string, :null => false
147
+ # t.column :iou, :string, :null => false
148
+ # t.column :service_ticket_id, :integer, :null => false
149
+ # end
150
+ # end
151
+ # end
152
+ #
153
+ # def self.down
154
+ # if ActiveRecord::Base.connection.table_alias_length > 30
155
+ # drop_table :casserver_proxy_granting_tickets
156
+ # drop_table :casserver_ticket_granting_tickets
157
+ # drop_table :casserver_service_tickets
158
+ # drop_table :casserver_login_tickets
159
+ # end
160
+ # end
161
+ # end
162
+ #
163
+ # # Oracle table names cannot exceed 30 chars...
164
+ # # See http://code.google.com/p/rubycas-server/issues/detail?id=15
165
+ # class ShortenTableNames < V 0.5
166
+ # def self.up
167
+ # if ActiveRecord::Base.connection.table_alias_length > 30
168
+ # $LOG.info("Shortening table names")
169
+ # rename_table :casserver_login_tickets, :casserver_lt
170
+ # rename_table :casserver_service_tickets, :casserver_st
171
+ # rename_table :casserver_ticket_granting_tickets, :casserver_tgt
172
+ # rename_table :casserver_proxy_granting_tickets, :casserver_pgt
173
+ # else
174
+ # create_table :casserver_lt, :force => true do |t|
175
+ # t.column :ticket, :string, :null => false
176
+ # t.column :created_on, :timestamp, :null => false
177
+ # t.column :consumed, :datetime, :null => true
178
+ # t.column :client_hostname, :string, :null => false
179
+ # end
180
+ #
181
+ # create_table :casserver_st, :force => true do |t|
182
+ # t.column :ticket, :string, :null => false
183
+ # t.column :service, :string, :null => false
184
+ # t.column :created_on, :timestamp, :null => false
185
+ # t.column :consumed, :datetime, :null => true
186
+ # t.column :client_hostname, :string, :null => false
187
+ # t.column :username, :string, :null => false
188
+ # t.column :type, :string, :null => false
189
+ # t.column :proxy_granting_ticket_id, :integer, :null => true
190
+ # end
191
+ #
192
+ # create_table :casserver_tgt, :force => true do |t|
193
+ # t.column :ticket, :string, :null => false
194
+ # t.column :created_on, :timestamp, :null => false
195
+ # t.column :client_hostname, :string, :null => false
196
+ # t.column :username, :string, :null => false
197
+ # end
198
+ #
199
+ # create_table :casserver_pgt, :force => true do |t|
200
+ # t.column :ticket, :string, :null => false
201
+ # t.column :created_on, :timestamp, :null => false
202
+ # t.column :client_hostname, :string, :null => false
203
+ # t.column :iou, :string, :null => false
204
+ # t.column :service_ticket_id, :integer, :null => false
205
+ # end
206
+ # end
207
+ # end
208
+ #
209
+ # def self.down
210
+ # if ActiveRecord::Base.connection.table_alias_length > 30
211
+ # rename_table :casserver_lt, :cassserver_login_tickets
212
+ # rename_table :casserver_st, :casserver_service_tickets
213
+ # rename_table :casserver_tgt, :casserver_ticket_granting_tickets
214
+ # rename_table :casserver_pgt, :casserver_proxy_granting_tickets
215
+ # else
216
+ # drop_table :casserver_pgt
217
+ # drop_table :casserver_tgt
218
+ # drop_table :casserver_st
219
+ # drop_table :casserver_lt
220
+ # end
221
+ # end
222
+ # end
223
+ #
224
+ # class AddTgtToSt < V 0.7
225
+ # def self.up
226
+ # add_column :casserver_st, :tgt_id, :integer, :null => true
227
+ # end
228
+ #
229
+ # def self.down
230
+ # remove_column :casserver_st, :tgt_id, :integer
231
+ # end
232
+ # end
233
+ #
234
+ # class ChangeServiceToText < V 0.71
235
+ # def self.up
236
+ # # using change_column to change the column type from :string to :text
237
+ # # doesn't seem to work, at least under MySQL, so we drop and re-create
238
+ # # the column instead
239
+ # remove_column :casserver_st, :service
240
+ # say "WARNING: All existing service tickets are being deleted."
241
+ # add_column :casserver_st, :service, :text
242
+ # end
243
+ #
244
+ # def self.down
245
+ # change_column :casserver_st, :service, :string
246
+ # end
247
+ # end
248
+ #
249
+ # class AddExtraAttributes < V 0.72
250
+ # def self.up
251
+ # add_column :casserver_tgt, :extra_attributes, :text
252
+ # end
253
+ #
254
+ # def self.down
255
+ # remove_column :casserver_tgt, :extra_attributes
256
+ # end
257
+ # end
258
+ #
259
+ # class RenamePgtForeignKeys < V 0.80
260
+ # def self.up
261
+ # rename_column :casserver_st, :proxy_granting_ticket_id, :granted_by_pgt_id
262
+ # rename_column :casserver_st, :tgt_id, :granted_by_tgt_id
263
+ # end
264
+ #
265
+ # def self.down
266
+ # rename_column :casserver_st, :granted_by_pgt_id, :proxy_granting_ticket_id
267
+ # rename_column :casserver_st, :granted_by_tgt_id, :tgt_id
268
+ # end
269
+ # end
270
+ end
@@ -0,0 +1,44 @@
1
+ require "active_support"
2
+ require "active_record"
3
+ class OptionsHash < HashWithIndifferentAccess
4
+
5
+ class EnvironmentMissing < Exception
6
+ end
7
+
8
+ attr_accessor :environment
9
+
10
+ def load_config file
11
+ load file do |file|
12
+ merge! HashWithIndifferentAccess.new(YAML.load file )[@environment]
13
+ end
14
+ end
15
+
16
+ def load_database_config file
17
+ load file do |file|
18
+ self[:database] = HashWithIndifferentAccess.new(YAML.load file )[@environment]
19
+ end
20
+ end
21
+
22
+ def inherit_authenticator_database!
23
+ if self[:authenticator][:database] == "inherit"
24
+ self[:authenticator][:database] = self[:database]
25
+ end
26
+ end
27
+
28
+
29
+ protected
30
+
31
+ def load file
32
+ raise EnvironmentMissing if @environment.nil?
33
+
34
+ case file
35
+ when File
36
+ # do nothing..
37
+ when String
38
+ file = File.open file
39
+ else
40
+ raise ArgumentError
41
+ end
42
+ yield file
43
+ end
44
+ end
@@ -0,0 +1,706 @@
1
+ require 'sinatra/base'
2
+ require 'casserver/localization'
3
+ require 'casserver/utils'
4
+ require 'casserver/cas'
5
+ require "casserver/options_hash"
6
+
7
+ require 'logger'
8
+ $LOG ||= Logger.new(STDOUT)
9
+
10
+ module CASServer
11
+ class Server < Sinatra::Base
12
+ CONFIG_FILE = ENV['CONFIG_FILE'] || "/etc/rubycas-server/config.yml"
13
+
14
+ include CASServer::CAS # CAS protocol helpers
15
+ include Localization
16
+
17
+ set :app_file, __FILE__
18
+ set :public, Proc.new { settings.config[:public_dir] || File.join(root, "..", "..", "public") }
19
+
20
+ config = OptionsHash.new(
21
+ :maximum_unused_login_ticket_lifetime => 5.minutes,
22
+ :maximum_unused_service_ticket_lifetime => 5.minutes, # CAS Protocol Spec, sec. 3.2.1 (recommended expiry time)
23
+ :maximum_session_lifetime => 2.days, # all tickets are deleted after this period of time
24
+ :log => {:file => 'casserver.log', :level => 'DEBUG'},
25
+ :uri_path => ""
26
+ )
27
+ config.environment = environment
28
+ set :config, config
29
+
30
+ def self.uri_path
31
+ config[:uri_path]
32
+ end
33
+
34
+ # Strip the config.uri_path from the request.path_info...
35
+ # FIXME: do we really need to override all of Sinatra's #static! to make this happen?
36
+ def static!
37
+ return if (public_dir = settings.public).nil?
38
+ public_dir = File.expand_path(public_dir)
39
+
40
+ path = File.expand_path(public_dir + unescape(request.path_info.gsub(/^#{settings.config[:uri_path]}/,'')))
41
+ return if path[0, public_dir.length] != public_dir
42
+ return unless File.file?(path)
43
+
44
+ env['sinatra.static_file'] = path
45
+ send_file path, :disposition => nil
46
+ end
47
+
48
+ def self.run!(options={})
49
+ set options
50
+
51
+ handler = detect_rack_handler
52
+ handler_name = handler.name.gsub(/.*::/, '')
53
+
54
+ puts "== RubyCAS-Server is starting up " +
55
+ "on port #{config[:port] || port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
56
+
57
+ begin
58
+ opts = handler_options
59
+ rescue Exception => e
60
+ print_cli_message e, :error
61
+ raise e
62
+ end
63
+
64
+ handler.run self, opts do |server|
65
+ [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
66
+ set :running, true
67
+ end
68
+ rescue Errno::EADDRINUSE => e
69
+ puts "== Something is already running on port #{port}!"
70
+ end
71
+
72
+ def self.quit!(server, handler_name)
73
+ ## Use thins' hard #stop! if available, otherwise just #stop
74
+ server.respond_to?(:stop!) ? server.stop! : server.stop
75
+ puts "\n== RubyCAS-Server is shutting down" unless handler_name =~/cgi/i
76
+ end
77
+
78
+ def self.print_cli_message(msg, type = :info)
79
+ if respond_to?(:config) && config && config[:quiet]
80
+ return
81
+ end
82
+
83
+ if type == :error
84
+ io = $stderr
85
+ prefix = "!!! "
86
+ else
87
+ io = $stdout
88
+ prefix = ">>> "
89
+ end
90
+
91
+ io.puts
92
+ io.puts "#{prefix}#{msg}"
93
+ io.puts
94
+ end
95
+
96
+ def self.load_config_file(config_file)
97
+ begin
98
+ config_file = File.open(config_file)
99
+ rescue Errno::ENOENT => e
100
+
101
+ print_cli_message "Config file #{config_file} does not exist!", :error
102
+ print_cli_message "Would you like the default config file copied to #{config_file.inspect}? [y/N]"
103
+ if gets.strip.downcase == 'y'
104
+ require 'fileutils'
105
+ default_config = File.dirname(__FILE__) + '/../../config/config.example.yml'
106
+
107
+ if !File.exists?(File.dirname(config_file))
108
+ print_cli_message "Creating config directory..."
109
+ FileUtils.mkdir_p(File.dirname(config_file), :verbose => true)
110
+ end
111
+
112
+ print_cli_message "Copying #{default_config.inspect} to #{config_file.inspect}..."
113
+ FileUtils.cp(default_config, config_file, :verbose => true)
114
+ print_cli_message "The default config has been copied. You should now edit it and try starting again."
115
+ exit
116
+ else
117
+ print_cli_message "Cannot start RubyCAS-Server without a valid config file.", :error
118
+ raise e
119
+ end
120
+ rescue Errno::EACCES => e
121
+ print_cli_message "Config file #{config_file.inspect} is not readable (permission denied)!", :error
122
+ raise e
123
+ rescue => e
124
+ print_cli_message "Config file #{config_file.inspect} could not be read!", :error
125
+ raise e
126
+ end
127
+
128
+ # This is the meat of the options hash functionality
129
+ config.load_config config_file
130
+ if File.exists? "config/database.yml"
131
+ config.load_database_config "config/database.yml"
132
+ end
133
+ config.inherit_authenticator_database!
134
+
135
+ set :server, config[:server] || 'webrick'
136
+ end
137
+
138
+ def self.reconfigure!(config)
139
+ config.each do |key, val|
140
+ self.config[key] = val
141
+ end
142
+ init_database!
143
+ init_logger!
144
+ init_authenticators!
145
+ end
146
+
147
+ def self.handler_options
148
+ handler_options = {
149
+ :Host => bind || config[:bind_address],
150
+ :Port => config[:port] || 443
151
+ }
152
+
153
+ handler_options.merge(handler_ssl_options).to_hash.symbolize_keys!
154
+ end
155
+
156
+ def self.handler_ssl_options
157
+ return {} unless config[:ssl_cert]
158
+
159
+ cert_path = config[:ssl_cert]
160
+ key_path = config[:ssl_key] || config[:ssl_cert]
161
+
162
+ unless cert_path.nil? && key_path.nil?
163
+ raise "The ssl_cert and ssl_key options cannot be used with mongrel. You will have to run your " +
164
+ " server behind a reverse proxy if you want SSL under mongrel." if
165
+ config[:server] == 'mongrel'
166
+
167
+ raise "The specified certificate file #{cert_path.inspect} does not exist or is not readable. " +
168
+ " Your 'ssl_cert' configuration setting must be a path to a valid " +
169
+ " ssl certificate." unless
170
+ File.exists? cert_path
171
+
172
+ raise "The specified key file #{key_path.inspect} does not exist or is not readable. " +
173
+ " Your 'ssl_key' configuration setting must be a path to a valid " +
174
+ " ssl private key." unless
175
+ File.exists? key_path
176
+
177
+ require 'openssl'
178
+ require 'webrick/https'
179
+
180
+ cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
181
+ key = OpenSSL::PKey::RSA.new(File.read(key_path))
182
+
183
+ {
184
+ :SSLEnable => true,
185
+ :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
186
+ :SSLCertificate => cert,
187
+ :SSLPrivateKey => key
188
+ }
189
+ end
190
+ end
191
+
192
+ def self.init_authenticators!
193
+ auth = []
194
+
195
+ if config[:authenticator].nil?
196
+ print_cli_message "No authenticators have been configured. Please double-check your config file (#{CONFIG_FILE.inspect}).", :error
197
+ exit 1
198
+ end
199
+
200
+ begin
201
+ # attempt to instantiate the authenticator
202
+ config[:authenticator] = [config[:authenticator]] unless config[:authenticator].instance_of? Array
203
+ config[:authenticator].each { |authenticator| auth << authenticator[:class].constantize}
204
+ rescue NameError
205
+ if config[:authenticator].instance_of? Array
206
+ config[:authenticator].each do |authenticator|
207
+ if !authenticator[:source].nil?
208
+ # config.yml explicitly names source file
209
+ require authenticator[:source]
210
+ else
211
+ # the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
212
+ auth_rb = authenticator[:class].underscore.gsub('cas_server/', '')
213
+ require 'casserver/'+auth_rb
214
+ end
215
+ auth << authenticator[:class].constantize
216
+ end
217
+ else
218
+ if config[:authenticator][:source]
219
+ # config.yml explicitly names source file
220
+ require config[:authenticator][:source]
221
+ else
222
+ # the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
223
+ auth_rb = config[:authenticator][:class].underscore.gsub('cas_server/', '')
224
+ require 'casserver/'+auth_rb
225
+ end
226
+
227
+ auth << config[:authenticator][:class].constantize
228
+ config[:authenticator] = [config[:authenticator]]
229
+ end
230
+ end
231
+
232
+ auth.zip(config[:authenticator]).each_with_index{ |auth_conf, index|
233
+ authenticator, conf = auth_conf
234
+ $LOG.debug "About to setup #{authenticator} with #{conf.inspect}..."
235
+ authenticator.setup(conf.merge('auth_index' => index)) if authenticator.respond_to?(:setup)
236
+ $LOG.debug "Done setting up #{authenticator}."
237
+ }
238
+
239
+ set :auth, auth
240
+ end
241
+
242
+ def self.init_logger!
243
+ if config[:log]
244
+ if $LOG && config[:log][:file]
245
+ print_cli_message "Redirecting RubyCAS-Server log to #{config[:log][:file]}"
246
+ #$LOG.close
247
+ $LOG = Logger.new(config[:log][:file])
248
+ end
249
+ $LOG.level = Logger.const_get(config[:log][:level]) if config[:log][:level]
250
+ end
251
+
252
+ if config[:db_log]
253
+ if $LOG && config[:db_log][:file]
254
+ $LOG.debug "Redirecting ActiveRecord log to #{config[:log][:file]}"
255
+ #$LOG.close
256
+ ActiveRecord::Base.logger = Logger.new(config[:db_log][:file])
257
+ end
258
+ ActiveRecord::Base.logger.level = Logger.const_get(config[:db+log][:level]) if config[:db_log][:level]
259
+ end
260
+ end
261
+
262
+ def self.init_database!
263
+ #CASServer::Model::Base.establish_connection(config[:database])
264
+ $LOG.debug "About to connect to database. Database config - #{config[:database].inspect}"
265
+ ActiveRecord::Base.establish_connection(config[:database])
266
+
267
+ unless config[:disable_auto_migrations]
268
+ print_cli_message "Running migrations to make sure your database schema is up to date..."
269
+ prev_db_log = ActiveRecord::Base.logger
270
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
271
+ ActiveRecord::Migration.verbose = true
272
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + "#{ENV["APP_ROOT"]}/db/migrate")
273
+ ActiveRecord::Base.logger = prev_db_log
274
+ print_cli_message "Your database is now up to date."
275
+ end
276
+ end
277
+
278
+ configure do
279
+ load_config_file(CONFIG_FILE)
280
+ init_logger!
281
+ init_database!
282
+ init_authenticators!
283
+ end
284
+
285
+ before do
286
+ GetText.locale = determine_locale(request)
287
+ content_type :html, 'charset' => 'utf-8'
288
+ @theme = settings.config[:theme]
289
+ @organization = settings.config[:organization]
290
+ @uri_path = settings.config[:uri_path]
291
+ @infoline = settings.config[:infoline]
292
+ @custom_views = settings.config[:custom_views]
293
+ end
294
+
295
+ # The #.#.# comments (e.g. "2.1.3") refer to section numbers in the CAS protocol spec
296
+ # under http://www.ja-sig.org/products/cas/overview/protocol/index.html
297
+
298
+ # 2.1 :: Login
299
+
300
+ # 2.1.1
301
+ get "#{uri_path}/login" do
302
+ CASServer::Utils::log_controller_action(self.class, params)
303
+
304
+ # make sure there's no caching
305
+ headers['Pragma'] = 'no-cache'
306
+ headers['Cache-Control'] = 'no-store'
307
+ headers['Expires'] = (Time.now - 1.year).rfc2822
308
+
309
+ # optional params
310
+ @service = clean_service_url(params['service'])
311
+ @renew = params['renew']
312
+ @gateway = params['gateway'] == 'true' || params['gateway'] == '1'
313
+
314
+ if tgc = request.cookies['tgt']
315
+ tgt, tgt_error = validate_ticket_granting_ticket(tgc)
316
+ end
317
+
318
+ if tgt and !tgt_error
319
+ @message = {:type => 'notice',
320
+ :message => _("You are currently logged in as '%s'. If this is not you, please log in below.") % tgt.username }
321
+ end
322
+
323
+ if params['redirection_loop_intercepted']
324
+ @message = {:type => 'mistake',
325
+ :message => _("The client and server are unable to negotiate authentication. Please try logging in again later.")}
326
+ end
327
+
328
+ begin
329
+ if @service
330
+ if !@renew && tgt && !tgt_error
331
+ st = generate_service_ticket(@service, tgt.username, tgt)
332
+ service_with_ticket = service_uri_with_ticket(@service, st)
333
+ $LOG.info("User '#{tgt.username}' authenticated based on ticket granting cookie. Redirecting to service '#{@service}'.")
334
+ redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
335
+ elsif @gateway
336
+ $LOG.info("Redirecting unauthenticated gateway request to service '#{@service}'.")
337
+ redirect @service, 303
338
+ end
339
+ elsif @gateway
340
+ $LOG.error("This is a gateway request but no service parameter was given!")
341
+ @message = {:type => 'mistake',
342
+ :message => _("The server cannot fulfill this gateway request because no service parameter was given.")}
343
+ end
344
+ rescue URI::InvalidURIError
345
+ $LOG.error("The service '#{@service}' is not a valid URI!")
346
+ @message = {:type => 'mistake',
347
+ :message => _("The target service your browser supplied appears to be invalid. Please contact your system administrator for help.")}
348
+ end
349
+
350
+ lt = generate_login_ticket
351
+
352
+ $LOG.debug("Rendering login form with lt: #{lt}, service: #{@service}, renew: #{@renew}, gateway: #{@gateway}")
353
+
354
+ @lt = lt.ticket
355
+
356
+ #$LOG.debug(env)
357
+
358
+ # If the 'onlyLoginForm' parameter is specified, we will only return the
359
+ # login form part of the page. This is useful for when you want to
360
+ # embed the login form in some external page (as an IFRAME, or otherwise).
361
+ # The optional 'submitToURI' parameter can be given to explicitly set the
362
+ # action for the form, otherwise the server will try to guess this for you.
363
+ if params.has_key? 'onlyLoginForm'
364
+ if @env['HTTP_HOST']
365
+ guessed_login_uri = "http#{@env['HTTPS'] && @env['HTTPS'] == 'on' ? 's' : ''}://#{@env['REQUEST_URI']}#{self / '/login'}"
366
+ else
367
+ guessed_login_uri = nil
368
+ end
369
+
370
+ @form_action = params['submitToURI'] || guessed_login_uri
371
+
372
+ if @form_action
373
+ render :login_form
374
+ else
375
+ status 500
376
+ render _("Could not guess the CAS login URI. Please supply a submitToURI parameter with your request.")
377
+ end
378
+ else
379
+ render :erb, :login
380
+ end
381
+ end
382
+
383
+
384
+ # 2.2
385
+ post "#{uri_path}/login" do
386
+ Utils::log_controller_action(self.class, params)
387
+
388
+ # 2.2.1 (optional)
389
+ @service = clean_service_url(params['service'])
390
+
391
+ # 2.2.2 (required)
392
+ @username = params['username']
393
+ @password = params['password']
394
+ @lt = params['lt']
395
+
396
+ # Remove leading and trailing widespace from username.
397
+ @username.strip! if @username
398
+
399
+ if @username && settings.config[:downcase_username]
400
+ $LOG.debug("Converting username #{@username.inspect} to lowercase because 'downcase_username' option is enabled.")
401
+ @username.downcase!
402
+ end
403
+
404
+ if error = validate_login_ticket(@lt)
405
+ @message = {:type => 'mistake', :message => error}
406
+ # generate another login ticket to allow for re-submitting the form
407
+ @lt = generate_login_ticket.ticket
408
+ status 500
409
+ render :erb, :login
410
+ end
411
+
412
+ # generate another login ticket to allow for re-submitting the form after a post
413
+ @lt = generate_login_ticket.ticket
414
+
415
+ $LOG.debug("Logging in with username: #{@username}, lt: #{@lt}, service: #{@service}, auth: #{settings.auth.inspect}")
416
+
417
+ credentials_are_valid = false
418
+ extra_attributes = {}
419
+ successful_authenticator = nil
420
+ begin
421
+ auth_index = 0
422
+ settings.auth.each do |auth_class|
423
+ auth = auth_class.new
424
+
425
+ auth_config = settings.config[:authenticator][auth_index]
426
+ # pass the authenticator index to the configuration hash in case the authenticator needs to know
427
+ # it splace in the authenticator queue
428
+ auth.configure(auth_config.merge('auth_index' => auth_index))
429
+
430
+ credentials_are_valid = auth.validate(
431
+ :username => @username,
432
+ :password => @password,
433
+ :service => @service,
434
+ :request => @env
435
+ )
436
+ if credentials_are_valid
437
+ extra_attributes.merge!(auth.extra_attributes) unless auth.extra_attributes.blank?
438
+ successful_authenticator = auth
439
+ break
440
+ end
441
+
442
+ auth_index += 1
443
+ end
444
+
445
+ if credentials_are_valid
446
+ $LOG.info("Credentials for username '#{@username}' successfully validated using #{successful_authenticator.class.name}.")
447
+ $LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
448
+
449
+ # 3.6 (ticket-granting cookie)
450
+ tgt = generate_ticket_granting_ticket(@username, extra_attributes)
451
+ response.set_cookie('tgt', tgt.to_s)
452
+
453
+ $LOG.debug("Ticket granting cookie '#{request.cookies['tgt'].inspect}' granted to #{@username.inspect}")
454
+
455
+ if @service.blank?
456
+ $LOG.info("Successfully authenticated user '#{@username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
457
+ @message = {:type => 'confirmation', :message => _("You have successfully logged in.")}
458
+ else
459
+ @st = generate_service_ticket(@service, @username, tgt)
460
+
461
+ begin
462
+ service_with_ticket = service_uri_with_ticket(@service, @st)
463
+
464
+ $LOG.info("Redirecting authenticated user '#{@username}' at '#{@st.client_hostname}' to service '#{@service}'")
465
+ redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
466
+ rescue URI::InvalidURIError
467
+ $LOG.error("The service '#{@service}' is not a valid URI!")
468
+ @message = {
469
+ :type => 'mistake',
470
+ :message => _("The target service your browser supplied appears to be invalid. Please contact your system administrator for help.")
471
+ }
472
+ end
473
+ end
474
+ else
475
+ $LOG.warn("Invalid credentials given for user '#{@username}'")
476
+ @message = {:type => 'mistake', :message => _("Incorrect username or password.")}
477
+ status 401
478
+ end
479
+ rescue CASServer::AuthenticatorError => e
480
+ $LOG.error(e)
481
+ # generate another login ticket to allow for re-submitting the form
482
+ @lt = generate_login_ticket.ticket
483
+ @message = {:type => 'mistake', :message => _(e.to_s)}
484
+ status 401
485
+ end
486
+
487
+ render :erb, :login
488
+ end
489
+
490
+ get /^#{uri_path}\/?$/ do
491
+ redirect "#{config['uri_path']}/login", 303
492
+ end
493
+
494
+
495
+ # 2.3
496
+
497
+ # 2.3.1
498
+ get "#{uri_path}/logout" do
499
+ CASServer::Utils::log_controller_action(self.class, params)
500
+
501
+ # The behaviour here is somewhat non-standard. Rather than showing just a blank
502
+ # "logout" page, we take the user back to the login page with a "you have been logged out"
503
+ # message, allowing for an opportunity to immediately log back in. This makes it
504
+ # easier for the user to log out and log in as someone else.
505
+ @service = clean_service_url(params['service'] || params['destination'])
506
+ @continue_url = params['url']
507
+
508
+ @gateway = params['gateway'] == 'true' || params['gateway'] == '1'
509
+
510
+ tgt = CASServer::Model::TicketGrantingTicket.find_by_ticket(request.cookies['tgt'])
511
+
512
+ response.delete_cookie 'tgt'
513
+
514
+ if tgt
515
+ CASServer::Model::TicketGrantingTicket.transaction do
516
+ $LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
517
+ tgt.granted_service_tickets.each do |st|
518
+ send_logout_notification_for_service_ticket(st) if config[:enable_single_sign_out]
519
+ # TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
520
+ # (the above method returns false if the POST results in a non-200 HTTP response).
521
+ $LOG.debug "Deleting #{st.class.name.demodulize} #{st.ticket.inspect} for service #{st.service}."
522
+ st.destroy
523
+ end
524
+
525
+ pgts = CASServer::Model::ProxyGrantingTicket.find(:all,
526
+ :conditions => [CASServer::Model::Base.connection.quote_table_name(CASServer::Model::ServiceTicket.table_name)+".username = ?", tgt.username],
527
+ :include => :service_ticket)
528
+ pgts.each do |pgt|
529
+ $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
530
+ pgt.destroy
531
+ end
532
+
533
+ $LOG.debug("Deleting #{tgt.class.name.demodulize} '#{tgt}' for user '#{tgt.username}'")
534
+ tgt.destroy
535
+ end
536
+
537
+ $LOG.info("User '#{tgt.username}' logged out.")
538
+ else
539
+ $LOG.warn("User tried to log out without a valid ticket-granting ticket.")
540
+ end
541
+
542
+ @message = {:type => 'confirmation', :message => _("You have successfully logged out.")}
543
+
544
+ @message[:message] +=_(" Please click on the following link to continue:") if @continue_url
545
+
546
+ @lt = generate_login_ticket
547
+
548
+ if @gateway && @service
549
+ redirect @service, 303
550
+ elsif @continue_url
551
+ render :erb, :logout
552
+ else
553
+ render :erb, :login
554
+ end
555
+ end
556
+
557
+
558
+ # 2.4
559
+
560
+ # 2.4.1
561
+ get "#{uri_path}/validate" do
562
+ CASServer::Utils::log_controller_action(self.class, params)
563
+
564
+ # required
565
+ @service = clean_service_url(params['service'])
566
+ @ticket = params['ticket']
567
+ # optional
568
+ @renew = params['renew']
569
+
570
+ st, @error = validate_service_ticket(@service, @ticket)
571
+ @success = st && !@error
572
+
573
+ @username = st.username if @success
574
+
575
+ status response_status_from_error(@error) if @error
576
+
577
+ render :erb, :validate, :layout => false
578
+ end
579
+
580
+
581
+ # 2.5
582
+
583
+ # 2.5.1
584
+ get "#{uri_path}/serviceValidate" do
585
+ CASServer::Utils::log_controller_action(self.class, params)
586
+
587
+ # required
588
+ @service = clean_service_url(params['service'])
589
+ @ticket = params['ticket']
590
+ # optional
591
+ @renew = params['renew']
592
+
593
+ st, @error = validate_service_ticket(@service, @ticket)
594
+ @success = st && !@error
595
+
596
+ if @success
597
+ @username = st.username
598
+ if @pgt_url
599
+ pgt = generate_proxy_granting_ticket(@pgt_url, st)
600
+ @pgtiou = pgt.iou if pgt
601
+ end
602
+ @extra_attributes = st.granted_by_tgt.extra_attributes || {}
603
+ end
604
+
605
+ status response_status_from_error(@error) if @error
606
+
607
+ render :builder, :proxy_validate
608
+ end
609
+
610
+
611
+ # 2.6
612
+
613
+ # 2.6.1
614
+ get "#{uri_path}/proxyValidate" do
615
+ CASServer::Utils::log_controller_action(self.class, params)
616
+
617
+ # required
618
+ @service = clean_service_url(params['service'])
619
+ @ticket = params['ticket']
620
+ # optional
621
+ @pgt_url = params['pgtUrl']
622
+ @renew = params['renew']
623
+
624
+ @proxies = []
625
+
626
+ t, @error = validate_proxy_ticket(@service, @ticket)
627
+ @success = t && !@error
628
+
629
+ @extra_attributes = {}
630
+ if @success
631
+ @username = t.username
632
+
633
+ if t.kind_of? CASServer::Model::ProxyTicket
634
+ @proxies << t.granted_by_pgt.service_ticket.service
635
+ end
636
+
637
+ if @pgt_url
638
+ pgt = generate_proxy_granting_ticket(@pgt_url, t)
639
+ @pgtiou = pgt.iou if pgt
640
+ end
641
+
642
+ @extra_attributes = t.granted_by_tgt.extra_attributes || {}
643
+ end
644
+
645
+ status response_status_from_error(@error) if @error
646
+
647
+ render :builder, :proxy_validate
648
+ end
649
+
650
+
651
+ # 2.7
652
+ get "#{uri_path}/proxy" do
653
+ CASServer::Utils::log_controller_action(self.class, params)
654
+
655
+ # required
656
+ @ticket = params['pgt']
657
+ @target_service = params['targetService']
658
+
659
+ pgt, @error = validate_proxy_granting_ticket(@ticket)
660
+ @success = pgt && !@error
661
+
662
+ if @success
663
+ @pt = generate_proxy_ticket(@target_service, pgt)
664
+ end
665
+
666
+ status response_status_from_error(@error) if @error
667
+
668
+ render :builder, :proxy
669
+ end
670
+
671
+
672
+
673
+ # Helpers
674
+
675
+ def response_status_from_error(error)
676
+ case error.code.to_s
677
+ when /^INVALID_/, 'BAD_PGT'
678
+ 422
679
+ when 'INTERNAL_ERROR'
680
+ 500
681
+ else
682
+ 500
683
+ end
684
+ end
685
+
686
+ def serialize_extra_attribute(builder, key, value)
687
+ if value.kind_of?(String)
688
+ builder.tag! key, value
689
+ elsif value.kind_of?(Numeric)
690
+ builder.tag! key, value.to_s
691
+ else
692
+ builder.tag! key do
693
+ builder.cdata! value.to_yaml
694
+ end
695
+ end
696
+ end
697
+
698
+ def compile_template(engine, data, options, views)
699
+ super engine, data, options, @custom_views || views
700
+ rescue Errno::ENOENT
701
+ raise unless @custom_views
702
+ super engine, data, options, views
703
+ end
704
+ end
705
+ end
706
+