synapse-rubycas-server 1.1.3alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG +353 -0
  3. data/Gemfile +12 -0
  4. data/LICENSE +26 -0
  5. data/README.md +38 -0
  6. data/Rakefile +3 -0
  7. data/bin/rubycas-server +30 -0
  8. data/config/config.example.yml +552 -0
  9. data/config/unicorn.rb +88 -0
  10. data/config.ru +11 -0
  11. data/db/migrate/001_create_initial_structure.rb +47 -0
  12. data/db/migrate/002_add_indexes_for_performance.rb +15 -0
  13. data/lib/casserver/authenticators/active_directory_ldap.rb +17 -0
  14. data/lib/casserver/authenticators/active_resource.rb +113 -0
  15. data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
  16. data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
  17. data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
  18. data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
  19. data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
  20. data/lib/casserver/authenticators/base.rb +70 -0
  21. data/lib/casserver/authenticators/client_certificate.rb +47 -0
  22. data/lib/casserver/authenticators/google.rb +62 -0
  23. data/lib/casserver/authenticators/ldap.rb +131 -0
  24. data/lib/casserver/authenticators/ntlm.rb +88 -0
  25. data/lib/casserver/authenticators/open_id.rb +19 -0
  26. data/lib/casserver/authenticators/sql.rb +158 -0
  27. data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
  28. data/lib/casserver/authenticators/sql_bcrypt.rb +17 -0
  29. data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
  30. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  31. data/lib/casserver/authenticators/sql_rest_auth.rb +82 -0
  32. data/lib/casserver/authenticators/test.rb +21 -0
  33. data/lib/casserver/base.rb +13 -0
  34. data/lib/casserver/cas.rb +324 -0
  35. data/lib/casserver/core_ext/directory_user.rb +81 -0
  36. data/lib/casserver/core_ext/securerandom.rb +17 -0
  37. data/lib/casserver/core_ext/string.rb +22 -0
  38. data/lib/casserver/core_ext.rb +12 -0
  39. data/lib/casserver/model/consumable.rb +31 -0
  40. data/lib/casserver/model/ticket.rb +19 -0
  41. data/lib/casserver/model.rb +248 -0
  42. data/lib/casserver/server.rb +796 -0
  43. data/lib/casserver/utils.rb +20 -0
  44. data/lib/casserver/views/_login_form.erb +42 -0
  45. data/lib/casserver/views/layout.erb +18 -0
  46. data/lib/casserver/views/login.erb +30 -0
  47. data/lib/casserver/views/proxy.builder +13 -0
  48. data/lib/casserver/views/proxy_validate.builder +31 -0
  49. data/lib/casserver/views/service_validate.builder +24 -0
  50. data/lib/casserver/views/validate.erb +2 -0
  51. data/lib/casserver.rb +19 -0
  52. data/locales/de.yml +27 -0
  53. data/locales/en.yml +26 -0
  54. data/locales/es.yml +26 -0
  55. data/locales/es_ar.yml +26 -0
  56. data/locales/fr.yml +26 -0
  57. data/locales/it.yml +26 -0
  58. data/locales/jp.yml +26 -0
  59. data/locales/pl.yml +26 -0
  60. data/locales/pt.yml +26 -0
  61. data/locales/ru.yml +26 -0
  62. data/locales/zh.yml +26 -0
  63. data/locales/zh_tw.yml +26 -0
  64. data/public/themes/cas.css +126 -0
  65. data/public/themes/notice.png +0 -0
  66. data/public/themes/ok.png +0 -0
  67. data/public/themes/simple/bg.png +0 -0
  68. data/public/themes/simple/favicon.png +0 -0
  69. data/public/themes/simple/login_box_bg.png +0 -0
  70. data/public/themes/simple/logo.png +0 -0
  71. data/public/themes/simple/theme.css +28 -0
  72. data/public/themes/warning.png +0 -0
  73. data/resources/init.d.sh +58 -0
  74. data/spec/casserver/authenticators/active_resource_spec.rb +116 -0
  75. data/spec/casserver/authenticators/ldap_spec.rb +57 -0
  76. data/spec/casserver/cas_spec.rb +148 -0
  77. data/spec/casserver/model_spec.rb +42 -0
  78. data/spec/casserver/utils_spec.rb +24 -0
  79. data/spec/casserver_spec.rb +221 -0
  80. data/spec/config/alt_config.yml +50 -0
  81. data/spec/config/default_config.yml +56 -0
  82. data/spec/core_ext/string_spec.rb +28 -0
  83. data/spec/spec.opts +4 -0
  84. data/spec/spec_helper.rb +126 -0
  85. data/tasks/bundler.rake +4 -0
  86. data/tasks/db/migrate.rake +12 -0
  87. data/tasks/spec.rake +10 -0
  88. metadata +405 -0
@@ -0,0 +1,21 @@
1
+ # encoding: UTF-8
2
+
3
+ # Dummy authenticator used for testing.
4
+ # Accepts any username as valid as long as the password is "testpassword"; otherwise authentication fails.
5
+ # Raises an AuthenticationError when username is "do_error" (this is useful to test the Exception
6
+ # handling functionality).
7
+ class CASServer::Authenticators::Test < CASServer::Authenticators::Base
8
+ def validate(credentials)
9
+ read_standard_credentials(credentials)
10
+
11
+ raise CASServer::AuthenticatorError, "Username is 'do_error'!" if @username == 'do_error'
12
+
13
+ @extra_attributes[:test_utf_string] = "Ютф"
14
+ @extra_attributes[:test_numeric] = 123.45
15
+ @extra_attributes[:test_serialized] = {:foo => 'bar', :alpha => [1,2,3]}
16
+
17
+ valid_password = options[:password] || "testpassword"
18
+
19
+ return @password == valid_password
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/r18n'
3
+ require 'logger'
4
+
5
+ $LOG ||= Logger.new(STDOUT)
6
+
7
+ module CASServer
8
+ class Base < Sinatra::Base
9
+ register Sinatra::R18n
10
+ R18n::I18n.default = 'en'
11
+ R18n.default_places { File.join(root,'..','..','locales') }
12
+ end
13
+ end
@@ -0,0 +1,324 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ require 'casserver/model'
5
+ require 'casserver/core_ext'
6
+
7
+ # Encapsulates CAS functionality. This module is meant to be included in
8
+ # the CASServer::Controllers module.
9
+ module CASServer::CAS
10
+
11
+ include CASServer::Model
12
+
13
+ def generate_login_ticket
14
+ # 3.5 (login ticket)
15
+ lt = LoginTicket.new
16
+ lt.ticket = "LT-" + String.random
17
+
18
+ lt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
19
+ lt.save!
20
+ $LOG.debug("Generated login ticket '#{lt.ticket}' for client" +
21
+ " at '#{lt.client_hostname}'")
22
+ lt
23
+ end
24
+
25
+ # Creates a TicketGrantingTicket for the given username. This is done when the user logs in
26
+ # for the first time to establish their SSO session (after their credentials have been validated).
27
+ #
28
+ # The optional 'extra_attributes' parameter takes a hash of additional attributes
29
+ # that will be sent along with the username in the CAS response to subsequent
30
+ # validation requests from clients.
31
+ def generate_ticket_granting_ticket(username, extra_attributes = {})
32
+ # 3.6 (ticket granting cookie/ticket)
33
+ tgt = TicketGrantingTicket.new
34
+ tgt.ticket = "TGC-" + String.random
35
+ tgt.username = username
36
+ tgt.extra_attributes = extra_attributes
37
+ tgt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
38
+ tgt.save!
39
+ $LOG.debug("Generated ticket granting ticket '#{tgt.ticket}' for user" +
40
+ " '#{tgt.username}' at '#{tgt.client_hostname}'" +
41
+ (extra_attributes.blank? ? "" : " with extra attributes #{extra_attributes.inspect}"))
42
+ tgt
43
+ end
44
+
45
+ def generate_service_ticket(service, username, tgt)
46
+ # 3.1 (service ticket)
47
+ st = ServiceTicket.new
48
+ st.ticket = "ST-" + String.random
49
+ st.service = service
50
+ st.username = username
51
+ st.granted_by_tgt_id = tgt.id
52
+ st.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
53
+ st.save!
54
+ $LOG.debug("Generated service ticket '#{st.ticket}' for service '#{st.service}'" +
55
+ " for user '#{st.username}' at '#{st.client_hostname}'")
56
+ st
57
+ end
58
+
59
+ def generate_proxy_ticket(target_service, pgt)
60
+ # 3.2 (proxy ticket)
61
+ pt = ProxyTicket.new
62
+ pt.ticket = "PT-" + String.random
63
+ pt.service = target_service
64
+ pt.username = pgt.service_ticket.username
65
+ pt.granted_by_pgt_id = pgt.id
66
+ pt.granted_by_tgt_id = pgt.service_ticket.granted_by_tgt_id
67
+ pt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
68
+ pt.save!
69
+ $LOG.debug("Generated proxy ticket '#{pt.ticket}' for target service '#{pt.service}'" +
70
+ " for user '#{pt.username}' at '#{pt.client_hostname}' using proxy-granting" +
71
+ " ticket '#{pgt.ticket}'")
72
+ pt
73
+ end
74
+
75
+ def generate_proxy_granting_ticket(pgt_url, st)
76
+ uri = URI.parse(pgt_url)
77
+ https = Net::HTTP.new(uri.host,uri.port)
78
+ https.use_ssl = true
79
+
80
+ # Here's what's going on here:
81
+ #
82
+ # 1. We generate a ProxyGrantingTicket (but don't store it in the database just yet)
83
+ # 2. Deposit the PGT and it's associated IOU at the proxy callback URL.
84
+ # 3. If the proxy callback URL responds with HTTP code 200, store the PGT and return it;
85
+ # otherwise don't save it and return nothing.
86
+ #
87
+ https.start do |conn|
88
+ path = uri.path.empty? ? '/' : uri.path
89
+ path += '?' + uri.query unless (uri.query.nil? || uri.query.empty?)
90
+
91
+ pgt = ProxyGrantingTicket.new
92
+ pgt.ticket = "PGT-" + String.random(60)
93
+ pgt.iou = "PGTIOU-" + String.random(57)
94
+ pgt.service_ticket_id = st.id
95
+ pgt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
96
+
97
+ # FIXME: The CAS protocol spec says to use 'pgt' as the parameter, but in practice
98
+ # the JA-SIG and Yale server implementations use pgtId. We'll go with the
99
+ # in-practice standard.
100
+ path += (uri.query.nil? || uri.query.empty? ? '?' : '&') + "pgtId=#{pgt.ticket}&pgtIou=#{pgt.iou}"
101
+
102
+ response = conn.request_get(path)
103
+ # TODO: follow redirects... 2.5.4 says that redirects MAY be followed
104
+ # NOTE: The following response codes are valid according to the JA-SIG implementation even without following redirects
105
+
106
+ if %w(200 202 301 302 304).include?(response.code)
107
+ # 3.4 (proxy-granting ticket IOU)
108
+ pgt.save!
109
+ $LOG.debug "PGT generated for pgt_url '#{pgt_url}': #{pgt.inspect}"
110
+ pgt
111
+ else
112
+ $LOG.warn "PGT callback server responded with a bad result code '#{response.code}'. PGT will not be stored."
113
+ nil
114
+ end
115
+ end
116
+ end
117
+
118
+ def validate_login_ticket(ticket)
119
+ $LOG.debug("Validating login ticket '#{ticket}'")
120
+
121
+ success = false
122
+ if ticket.nil?
123
+ error = t.error.no_login_ticket
124
+ $LOG.warn "Missing login ticket."
125
+ elsif lt = LoginTicket.find_by_ticket(ticket)
126
+ if lt.consumed?
127
+ error = t.error.login_ticket_already_used
128
+ $LOG.warn "Login ticket '#{ticket}' previously used up"
129
+ elsif Time.now - lt.created_on < settings.config[:maximum_unused_login_ticket_lifetime]
130
+ $LOG.info "Login ticket '#{ticket}' successfully validated"
131
+ else
132
+ error = t.error.login_timeout
133
+ $LOG.warn "Expired login ticket '#{ticket}'"
134
+ end
135
+ else
136
+ error = t.error.invalid_login_ticket
137
+ $LOG.warn "Invalid login ticket '#{ticket}'"
138
+ end
139
+
140
+ lt.consume! if lt
141
+
142
+ error
143
+ end
144
+
145
+ def validate_ticket_granting_ticket(ticket)
146
+ $LOG.debug("Validating ticket granting ticket '#{ticket}'")
147
+
148
+ if ticket.nil?
149
+ error = "No ticket granting ticket given."
150
+ $LOG.debug error
151
+ elsif tgt = TicketGrantingTicket.find_by_ticket(ticket)
152
+ if settings.config[:maximum_session_lifetime] && Time.now - tgt.created_on > settings.config[:maximum_session_lifetime]
153
+ tgt.destroy
154
+ error = "Your session has expired. Please log in again."
155
+ $LOG.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' expired."
156
+ else
157
+ $LOG.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' successfully validated."
158
+ end
159
+ else
160
+ error = "Invalid ticket granting ticket '#{ticket}' (no matching ticket found in the database)."
161
+ $LOG.warn(error)
162
+ end
163
+
164
+ [tgt, error]
165
+ end
166
+
167
+ def validate_service_ticket(service, ticket, allow_proxy_tickets = false)
168
+ $LOG.debug "Validating service/proxy ticket '#{ticket}' for service '#{service}'"
169
+
170
+ if service.nil? or ticket.nil?
171
+ error = Error.new(:INVALID_REQUEST, "Ticket or service parameter was missing in the request.")
172
+ $LOG.warn "#{error.code} - #{error.message}"
173
+ elsif st = ServiceTicket.find_by_ticket(ticket)
174
+ if st.consumed?
175
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has already been used up.")
176
+ $LOG.warn "#{error.code} - #{error.message}"
177
+ elsif st.kind_of?(CASServer::Model::ProxyTicket) && !allow_proxy_tickets
178
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' is a proxy ticket, but only service tickets are allowed here.")
179
+ $LOG.warn "#{error.code} - #{error.message}"
180
+ elsif Time.now - st.created_on > settings.config[:maximum_unused_service_ticket_lifetime]
181
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has expired.")
182
+ $LOG.warn "Ticket '#{ticket}' has expired."
183
+ elsif !st.matches_service? service
184
+ error = Error.new(:INVALID_SERVICE, "The ticket '#{ticket}' belonging to user '#{st.username}' is valid,"+
185
+ " but the requested service '#{service}' does not match the service '#{st.service}' associated with this ticket.")
186
+ $LOG.warn "#{error.code} - #{error.message}"
187
+ else
188
+ $LOG.info("Ticket '#{ticket}' for service '#{service}' for user '#{st.username}' successfully validated.")
189
+ end
190
+ else
191
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' not recognized.")
192
+ $LOG.warn("#{error.code} - #{error.message}")
193
+ end
194
+
195
+ if st
196
+ st.consume!
197
+ end
198
+
199
+
200
+ [st, error]
201
+ end
202
+
203
+ def validate_proxy_ticket(service, ticket)
204
+ pt, error = validate_service_ticket(service, ticket, true)
205
+
206
+ if pt.kind_of?(CASServer::Model::ProxyTicket) && !error
207
+ if not pt.granted_by_pgt
208
+ error = Error.new(:INTERNAL_ERROR, "Proxy ticket '#{pt}' belonging to user '#{pt.username}' is not associated with a proxy granting ticket.")
209
+ elsif not pt.granted_by_pgt.service_ticket
210
+ error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{pt.granted_by_pgt}'"+
211
+ " (associated with proxy ticket '#{pt}' and belonging to user '#{pt.username}' is not associated with a service ticket.")
212
+ end
213
+ end
214
+
215
+ [pt, error]
216
+ end
217
+
218
+ def validate_proxy_granting_ticket(ticket)
219
+ if ticket.nil?
220
+ error = Error.new(:INVALID_REQUEST, "pgt parameter was missing in the request.")
221
+ $LOG.warn("#{error.code} - #{error.message}")
222
+ elsif pgt = ProxyGrantingTicket.find_by_ticket(ticket)
223
+ if pgt.service_ticket
224
+ $LOG.info("Proxy granting ticket '#{ticket}' belonging to user '#{pgt.service_ticket.username}' successfully validated.")
225
+ else
226
+ error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{ticket}' is not associated with a service ticket.")
227
+ $LOG.error("#{error.code} - #{error.message}")
228
+ end
229
+ else
230
+ error = Error.new(:BAD_PGT, "Invalid proxy granting ticket '#{ticket}' (no matching ticket found in the database).")
231
+ $LOG.warn("#{error.code} - #{error.message}")
232
+ end
233
+
234
+ [pgt, error]
235
+ end
236
+
237
+ # Takes an existing ServiceTicket object (presumably pulled from the database)
238
+ # and sends a POST with logout information to the service that the ticket
239
+ # was generated for.
240
+ #
241
+ # This makes possible the "single sign-out" functionality added in CAS 3.1.
242
+ # See http://www.ja-sig.org/wiki/display/CASUM/Single+Sign+Out
243
+ def send_logout_notification_for_service_ticket(st)
244
+ uri = URI.parse(st.service)
245
+ uri.path = '/' if uri.path.empty?
246
+ time = Time.now
247
+ rand = String.random
248
+ path = uri.path
249
+ req = Net::HTTP::Post.new(path)
250
+ req.set_form_data('logoutRequest' => %{<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="#{rand}" Version="2.0" IssueInstant="#{time.rfc2822}">
251
+ <saml:NameID></saml:NameID>
252
+ <samlp:SessionIndex>#{st.ticket}</samlp:SessionIndex>
253
+ </samlp:LogoutRequest>})
254
+
255
+ begin
256
+ http = Net::HTTP.new(uri.host, uri.port)
257
+ http.use_ssl = true if uri.scheme =='https'
258
+
259
+ http.start do |conn|
260
+ response = conn.request(req)
261
+ if response.kind_of? Net::HTTPSuccess
262
+ $LOG.info "Logout notification successfully posted to #{st.service.inspect}."
263
+ return true
264
+ else
265
+ $LOG.error "Service #{st.service.inspect} responed to logout notification with code '#{response.code}'!"
266
+ return false
267
+ end
268
+ end
269
+ rescue Exception => e
270
+ $LOG.error "Failed to send logout notification to service #{st.service.inspect} due to #{e}"
271
+ return false
272
+ end
273
+ end
274
+
275
+ def service_uri_with_ticket(service, st)
276
+ raise ArgumentError, "Second argument must be a ServiceTicket!" unless st.kind_of? CASServer::Model::ServiceTicket
277
+
278
+ # This will choke with a URI::InvalidURIError if service URI is not properly URI-escaped...
279
+ # This exception is handled further upstream (i.e. in the controller).
280
+ service_uri = URI.parse(service)
281
+
282
+ if service.include? "?"
283
+ if service_uri.query.empty?
284
+ query_separator = ""
285
+ else
286
+ query_separator = "&"
287
+ end
288
+ else
289
+ query_separator = "?"
290
+ end
291
+
292
+ service_with_ticket = service + query_separator + "ticket=" + st.ticket
293
+ service_with_ticket
294
+ end
295
+
296
+ # Strips CAS-related parameters from a service URL and normalizes it,
297
+ # removing trailing / and ?. Also converts any spaces to +.
298
+ #
299
+ # For example, "http://google.com?ticket=12345" will be returned as
300
+ # "http://google.com". Also, "http://google.com/" would be returned as
301
+ # "http://google.com".
302
+ #
303
+ # Note that only the first occurance of each CAS-related parameter is
304
+ # removed, so that "http://google.com?ticket=12345&ticket=abcd" would be
305
+ # returned as "http://google.com?ticket=abcd".
306
+ def clean_service_url(dirty_service)
307
+ return dirty_service if dirty_service.blank?
308
+ clean_service = dirty_service.dup
309
+ ['service', 'ticket', 'gateway', 'renew'].each do |p|
310
+ clean_service.sub!(Regexp.new("&?#{p}=[^&]*"), '')
311
+ end
312
+
313
+ clean_service.gsub!(/[\/\?&]$/, '') # remove trailing ?, /, or &
314
+ clean_service.gsub!('?&', '?')
315
+ clean_service.gsub!(' ', '+')
316
+
317
+ $LOG.debug("Cleaned dirty service URL #{dirty_service.inspect} to #{clean_service.inspect}") if
318
+ dirty_service != clean_service
319
+
320
+ return clean_service
321
+ end
322
+ module_function :clean_service_url
323
+
324
+ end
@@ -0,0 +1,81 @@
1
+ # ActiveDirectory user class
2
+ class DirectoryUser
3
+ require 'net/ldap'
4
+
5
+ TREEBASE = "dc=synapsedev,dc=com"
6
+ FIELDS = [
7
+ "accountExpires", "displayName", "dn", "mail", "title", "sn", "givenName",
8
+ "c", "co", "company", "department", "employeeType", "facsimiletelephonenumber",
9
+ "hashedPassword", "l", "mail", "mobile", "manager", "physicalDeliveryOfficeName",
10
+ "pinNumber", "postalcode", "proxyAddresses", "pwdLastSet", "sAMAccountName",
11
+ "sAMAccountType", "streetAddress", "synapseAccessCardNumber",
12
+ "synapseConferencingStatus", "synapseConferencingUniqueID", "synapseExtendedAttributes",
13
+ "synapseExtendedAttributesTest", "synapseExtensionNumber", "synapseRecursiveGroups",
14
+ "synapseEmployeeStartDate", "synapsePersonalEmailAddress", "synapseEmergencyContact",
15
+ "synapseDateOfBirth", "synapseBusinessUnit", "synapseObjectGUID", "telephoneNumber",
16
+ "title", "userAccountControl", "userPrincipalName", "uSNChanged",
17
+ "uSNCreated", "whenCreated", "whenChanged"
18
+ ]
19
+
20
+ # Public: Change password for ActiveDirectory user
21
+ #
22
+ # pwd - The new password for the user
23
+ #
24
+ # Examples
25
+ # user = DirectoryUser.find_by_account_name('conference.test')
26
+ # user.change_password("MyNewPassword")
27
+ # # => true
28
+ #
29
+ # Returns true if the password was updated
30
+ # Return error if the password was not updated
31
+
32
+ def self.change_user_password(username, password)
33
+ filter = "(&(objectCategory=person)(objectClass=user)(samaccountname=#{username}))"
34
+ ldap_con = self.ldap_connect(username, password)
35
+
36
+ if ldap_con
37
+ result = self.ldap_search(TREEBASE, filter, FIELDS, ldap_con).first
38
+ dn = result[:dn].first
39
+ ops = [
40
+ [ :delete, :unicodePwd, [microsoft_encode_password(password)] ],
41
+ [ :add, :unicodePwd, [microsoft_encode_password(password)] ]
42
+ ]
43
+ ldap_con.modify(:dn => dn, :operations => ops)
44
+ if ldap_con.get_operation_result.code == 0
45
+ return true
46
+ else
47
+ raise StandardError, "Password field was not updated for #{username}. LDAP #{ldap_con.get_operation_result.code} Error: #{ldap_con.get_operation_result.message}"
48
+ end
49
+ else
50
+ raise StandardError, "Ldap failed to connect Error #{ldap_con.get_operation_result.message}"
51
+ end
52
+ end
53
+
54
+ def self.ldap_search(treebase, filter, attrs, ldap_con)
55
+ results = []
56
+ results = ldap_con.search( :base => treebase, :filter => filter, :attributes => attrs )
57
+ if results
58
+ results
59
+ else
60
+ raise StandardError, "No results for ldap search #{filter} in treebase #{treebase} LDAP Error: #{ldap_con.get_operation_result}"
61
+ end
62
+ end
63
+
64
+ def self.ldap_connect(username, password)
65
+ ldap = Net::LDAP.new(
66
+ :host => "core-dc-1.synapsedev.com",
67
+ :port => 636,
68
+ :encryption => :simple_tls
69
+ )
70
+ ldap.authenticate("SYNAPSEDEV\\#{username}", password)
71
+ ldap
72
+ end
73
+
74
+ def self.microsoft_encode_password(pwd)
75
+ ret = ""
76
+ pwd = "\"" + pwd + "\""
77
+ pwd.length.times{|i| ret+= "#{pwd[i..i]}\000" }
78
+ ret
79
+ end
80
+
81
+ end
@@ -0,0 +1,17 @@
1
+ require 'securerandom'
2
+
3
+ module CASServer::CoreExt
4
+ module SecureRandom
5
+ # This is a copypaste job from 1.9.3, ActiveSupport
6
+ # doesn't come with this method and it is an easier
7
+ # way to get a random string that's good for tickets.
8
+ # Less code to maintain means less things we can break.
9
+ def self.urlsafe_base64(n=nil, padding=false)
10
+ s = [::SecureRandom.random_bytes(n)].pack("m*")
11
+ s.delete!("\n")
12
+ s.tr!("+/","-_")
13
+ s.delete!("=") if !padding
14
+ s
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ require 'securerandom'
2
+
3
+ module CASServer
4
+ module CoreExt
5
+ module String
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ # if we're on ruby 1.9 we'll use the built in version
12
+ # this will break if someone trys to use ActiveSupport 3.2+
13
+ # with Ruby 1.8
14
+ def random(length = 29)
15
+ str = "#{Time.now.to_i}r#{SecureRandom.urlsafe_base64(length)}"
16
+ str.gsub!('_','-')
17
+ str[0..(length - 1)]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ require 'casserver/core_ext/string'
2
+
3
+ # We only need to modify securerandom if we're using
4
+ # ActiveSupport's implementation.
5
+ if RUBY_VERSION < "1.9"
6
+ require 'securerandom'
7
+ require 'casserver/core_ext/securerandom'
8
+
9
+ SecureRandom.send(:include, CASServer::CoreExt::SecureRandom)
10
+ end
11
+
12
+ String.send(:include, CASServer::CoreExt::String)
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ module CASServer::Model
4
+ module Consumable
5
+ def consume!
6
+ self.consumed = Time.now
7
+ self.save!
8
+ end
9
+
10
+ def self.included(mod)
11
+ mod.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ def cleanup(max_lifetime, max_unconsumed_lifetime)
16
+ transaction do
17
+ conditions = ["created_on < ? OR (consumed IS NULL AND created_on < ?)",
18
+ Time.now - max_lifetime,
19
+ Time.now - max_unconsumed_lifetime]
20
+
21
+ expired_tickets_count = count(:conditions => conditions)
22
+
23
+ $LOG.debug("Destroying #{expired_tickets_count} expired #{self.name.demodulize}"+
24
+ "#{'s' if expired_tickets_count > 1}.") if expired_tickets_count > 0
25
+
26
+ destroy_all(conditions)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ module CASServer::Model
2
+ module Ticket
3
+ def to_s
4
+ ticket
5
+ end
6
+
7
+ def self.cleanup(max_lifetime)
8
+ transaction do
9
+ conditions = ["created_on < ?", Time.now - max_lifetime]
10
+ expired_tickets_count = count(:conditions => conditions)
11
+
12
+ $LOG.debug("Destroying #{expired_tickets_count} expired #{self.name.demodulize}"+
13
+ "#{'s' if expired_tickets_count > 1}.") if expired_tickets_count > 0
14
+
15
+ destroy_all(conditions)
16
+ end
17
+ end
18
+ end
19
+ end