casino_core 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.
Files changed (58) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +48 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +24 -0
  6. data/Gemfile.lock +64 -0
  7. data/LICENSE.txt +20 -0
  8. data/README.md +19 -0
  9. data/Rakefile +44 -0
  10. data/VERSION +1 -0
  11. data/casino_core.gemspec +126 -0
  12. data/config/cas.yml +26 -0
  13. data/config/database.yml +18 -0
  14. data/db/migrate/20121112154930_create_ticket_granting_tickets.rb +11 -0
  15. data/db/migrate/20121112160009_create_login_tickets.rb +9 -0
  16. data/db/migrate/20121112165804_ticket_should_not_be_null.rb +5 -0
  17. data/db/migrate/20121122180310_add_user_agent_to_ticket_granting_tickets.rb +5 -0
  18. data/db/migrate/20121124170004_add_index_for_username_to_ticket_granting_tickets.rb +5 -0
  19. data/db/migrate/20121124183542_create_service_tickets.rb +13 -0
  20. data/db/migrate/20121124183732_add_ticket_indexes.rb +6 -0
  21. data/db/migrate/20121124195013_add_consumed_to_service_tickets.rb +5 -0
  22. data/db/migrate/20121125091934_add_issued_from_credentials_to_service_tickets.rb +5 -0
  23. data/db/migrate/20121125185415_create_proxy_granting_tickets.rb +14 -0
  24. data/db/migrate/20121125190013_tickets_should_be_unique.rb +8 -0
  25. data/db/schema.rb +61 -0
  26. data/lib/casino_core.rb +33 -0
  27. data/lib/casino_core/authenticator.rb +9 -0
  28. data/lib/casino_core/authenticator/static.rb +23 -0
  29. data/lib/casino_core/helper.rb +21 -0
  30. data/lib/casino_core/helper/browser.rb +16 -0
  31. data/lib/casino_core/helper/service_tickets.rb +30 -0
  32. data/lib/casino_core/model.rb +10 -0
  33. data/lib/casino_core/model/login_ticket.rb +11 -0
  34. data/lib/casino_core/model/proxy_granting_ticket.rb +8 -0
  35. data/lib/casino_core/model/service_ticket.rb +32 -0
  36. data/lib/casino_core/model/service_ticket/single_sign_out_notifier.rb +53 -0
  37. data/lib/casino_core/model/ticket_granting_ticket.rb +9 -0
  38. data/lib/casino_core/processor.rb +15 -0
  39. data/lib/casino_core/processor/legacy_validator.rb +49 -0
  40. data/lib/casino_core/processor/login_credential_acceptor.rb +67 -0
  41. data/lib/casino_core/processor/login_credential_requestor.rb +45 -0
  42. data/lib/casino_core/processor/logout.rb +23 -0
  43. data/lib/casino_core/processor/session_destroyer.rb +17 -0
  44. data/lib/casino_core/railtie.rb +10 -0
  45. data/lib/casino_core/rake_tasks.rb +14 -0
  46. data/lib/casino_core/settings.rb +26 -0
  47. data/lib/casino_core/tasks/cleanup.rake +26 -0
  48. data/lib/casino_core/tasks/database.rake +60 -0
  49. data/spec/authenticator/static_spec.rb +42 -0
  50. data/spec/model/login_ticket_spec.rb +16 -0
  51. data/spec/model/service_ticket_spec.rb +45 -0
  52. data/spec/processor/legacy_validator_spec.rb +87 -0
  53. data/spec/processor/login_credential_acceptor_spec.rb +70 -0
  54. data/spec/processor/login_credential_requestor_spec.rb +75 -0
  55. data/spec/processor/logout_spec.rb +55 -0
  56. data/spec/processor/session_destroyer_spec.rb +68 -0
  57. data/spec/spec_helper.rb +33 -0
  58. metadata +234 -0
@@ -0,0 +1,5 @@
1
+ class AddUserAgentToTicketGrantingTickets < ActiveRecord::Migration
2
+ def change
3
+ add_column :ticket_granting_tickets, :user_agent, :string
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddIndexForUsernameToTicketGrantingTickets < ActiveRecord::Migration
2
+ def change
3
+ add_index :ticket_granting_tickets, :username
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ class CreateServiceTickets < ActiveRecord::Migration
2
+ def change
3
+ create_table :service_tickets do |t|
4
+ t.string :ticket, null: false, unique: true
5
+ t.string :service, null: false
6
+ t.integer :ticket_granting_ticket_id, null: false
7
+
8
+ t.timestamps
9
+ end
10
+ add_index :service_tickets, :ticket
11
+ add_index :service_tickets, :ticket_granting_ticket_id
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ class AddTicketIndexes < ActiveRecord::Migration
2
+ def change
3
+ add_index :ticket_granting_tickets, :ticket
4
+ add_index :login_tickets, :ticket
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddConsumedToServiceTickets < ActiveRecord::Migration
2
+ def change
3
+ add_column :service_tickets, :consumed, :boolean, null: false, default: false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddIssuedFromCredentialsToServiceTickets < ActiveRecord::Migration
2
+ def change
3
+ add_column :service_tickets, :issued_from_credentials, :boolean, null: false, default: false
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ class CreateProxyGrantingTickets < ActiveRecord::Migration
2
+ def change
3
+ create_table :proxy_granting_tickets do |t|
4
+ t.string :ticket, null: false
5
+ t.string :iou, null: false
6
+ t.integer :ticket_granting_ticket_id, null: false
7
+
8
+ t.timestamps
9
+ end
10
+ add_index :proxy_granting_tickets, :ticket, unique: true
11
+ add_index :proxy_granting_tickets, :iou, unique: true
12
+ add_index :proxy_granting_tickets, :ticket_granting_ticket_id
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ class TicketsShouldBeUnique < ActiveRecord::Migration
2
+ def change
3
+ [:login_tickets, :service_tickets, :ticket_granting_tickets].each do |table|
4
+ remove_index table, :ticket
5
+ add_index table, :ticket, unique: true
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: UTF-8
2
+ # This file is auto-generated from the current state of the database. Instead
3
+ # of editing this file, please use the migrations feature of Active Record to
4
+ # incrementally modify your database, and then regenerate this schema definition.
5
+ #
6
+ # Note that this schema.rb definition is the authoritative source for your
7
+ # database schema. If you need to create the application database on another
8
+ # system, you should be using db:schema:load, not running all the migrations
9
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
11
+ #
12
+ # It's strongly recommended to check this file into your version control system.
13
+
14
+ ActiveRecord::Schema.define(:version => 20121125190013) do
15
+
16
+ create_table "login_tickets", :force => true do |t|
17
+ t.string "ticket", :null => false
18
+ t.datetime "created_at", :null => false
19
+ t.datetime "updated_at", :null => false
20
+ end
21
+
22
+ add_index "login_tickets", ["ticket"], :name => "index_login_tickets_on_ticket", :unique => true
23
+
24
+ create_table "proxy_granting_tickets", :force => true do |t|
25
+ t.string "ticket", :null => false
26
+ t.string "iou", :null => false
27
+ t.integer "ticket_granting_ticket_id", :null => false
28
+ t.datetime "created_at", :null => false
29
+ t.datetime "updated_at", :null => false
30
+ end
31
+
32
+ add_index "proxy_granting_tickets", ["iou"], :name => "index_proxy_granting_tickets_on_iou", :unique => true
33
+ add_index "proxy_granting_tickets", ["ticket"], :name => "index_proxy_granting_tickets_on_ticket", :unique => true
34
+ add_index "proxy_granting_tickets", ["ticket_granting_ticket_id"], :name => "index_proxy_granting_tickets_on_ticket_granting_ticket_id"
35
+
36
+ create_table "service_tickets", :force => true do |t|
37
+ t.string "ticket", :null => false
38
+ t.string "service", :null => false
39
+ t.integer "ticket_granting_ticket_id", :null => false
40
+ t.datetime "created_at", :null => false
41
+ t.datetime "updated_at", :null => false
42
+ t.boolean "consumed", :default => false, :null => false
43
+ t.boolean "issued_from_credentials", :default => false, :null => false
44
+ end
45
+
46
+ add_index "service_tickets", ["ticket"], :name => "index_service_tickets_on_ticket", :unique => true
47
+ add_index "service_tickets", ["ticket_granting_ticket_id"], :name => "index_service_tickets_on_ticket_granting_ticket_id"
48
+
49
+ create_table "ticket_granting_tickets", :force => true do |t|
50
+ t.string "ticket", :null => false
51
+ t.string "username", :null => false
52
+ t.text "extra_attributes"
53
+ t.datetime "created_at", :null => false
54
+ t.datetime "updated_at", :null => false
55
+ t.string "user_agent"
56
+ end
57
+
58
+ add_index "ticket_granting_tickets", ["ticket"], :name => "index_ticket_granting_tickets_on_ticket", :unique => true
59
+ add_index "ticket_granting_tickets", ["username"], :name => "index_ticket_granting_tickets_on_username"
60
+
61
+ end
@@ -0,0 +1,33 @@
1
+ module CASinoCore
2
+ autoload :Authenticator, 'casino_core/authenticator.rb'
3
+ autoload :Helper, 'casino_core/helper.rb'
4
+ autoload :Model, 'casino_core/model.rb'
5
+ autoload :Processor, 'casino_core/processor.rb'
6
+ autoload :RakeTasks, 'casino_core/rake_tasks.rb'
7
+ autoload :Settings, 'casino_core/settings.rb'
8
+
9
+ require 'casino_core/railtie' if defined?(Rails)
10
+
11
+ class << self
12
+ def setup(environment = nil, options = {})
13
+ @environment = environment || 'development'
14
+ require 'active_record'
15
+ require 'yaml'
16
+ YAML::ENGINE.yamler = 'syck'
17
+ ActiveRecord::Base.establish_connection YAML.load_file('config/database.yml')[@environment]
18
+
19
+ config = YAML.load_file('config/cas.yml')[@environment].symbolize_keys
20
+ recursive_symbolize_keys!(config)
21
+ CASinoCore::Settings.init config
22
+ end
23
+
24
+ private
25
+ def recursive_symbolize_keys! hash
26
+ # ugly, ugly, ugly
27
+ # TODO refactor!
28
+ hash.symbolize_keys!
29
+ hash.values.select{|v| v.is_a? Hash}.each{|h| recursive_symbolize_keys!(h)}
30
+ hash.values.select{|v| v.is_a? Array}.each{|a| a.select{|v| v.is_a? Hash}.each{|h| recursive_symbolize_keys!(h)}}
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module CASinoCore
2
+ class Authenticator
3
+ autoload :Static, 'casino_core/authenticator/static.rb'
4
+
5
+ def validate(username, password)
6
+ raise NotImplementedError, "This method must be implemented by a class extending #{self.class}"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ require 'casino_core/authenticator'
2
+
3
+ # The static authenticator is just a simple example.
4
+ # Never ever us this authenticator in a productive environment!
5
+ class CASinoCore::Authenticator::Static < CASinoCore::Authenticator
6
+
7
+ # @param [Hash] options
8
+ def initialize(options)
9
+ @users = options[:users] || {}
10
+ end
11
+
12
+ def validate(username, password)
13
+ username = :"#{username}"
14
+ if @users.include?(username) && @users[username][:password] == password
15
+ {
16
+ username: "#{username}",
17
+ extra_attributes: @users[username].except(:password)
18
+ }
19
+ else
20
+ false
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ require 'logger'
2
+ require 'useragent'
3
+
4
+ module CASinoCore
5
+ module Helper
6
+ autoload :ServiceTickets, 'casino_core/helper/service_tickets.rb'
7
+ autoload :Browser, 'casino_core/helper/browser.rb'
8
+
9
+ def random_ticket_string(prefix, length = 40)
10
+ random_string = rand(36**length).to_s(36)
11
+ "#{prefix}-#{Time.now.to_i}-#{random_string}"
12
+ end
13
+
14
+ def logger
15
+ # TODO this is just a "silent logger", make logger a setting!
16
+ logger = ::Logger.new(STDOUT)
17
+ logger.level = ::Logger::Severity::UNKNOWN
18
+ logger
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ require 'addressable/uri'
2
+
3
+ module CASinoCore
4
+ module Helper
5
+ module Browser
6
+ def browser_info(user_agent)
7
+ user_agent = UserAgent.parse(user_agent)
8
+ "#{user_agent.browser} (#{user_agent.platform})"
9
+ end
10
+
11
+ def same_browser?(user_agent, other_user_agent)
12
+ user_agent == other_user_agent || browser_info(user_agent) == browser_info(other_user_agent)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ require 'addressable/uri'
2
+
3
+ module CASinoCore
4
+ module Helper
5
+ module ServiceTickets
6
+ include CASinoCore::Helper
7
+
8
+ def acquire_service_ticket(ticket_granting_ticket, service, credentials_supplied = nil)
9
+ ticket_granting_ticket.service_tickets.create!({
10
+ ticket: random_ticket_string('ST'),
11
+ service: clean_service_url(service),
12
+ issued_from_credentials: !!credentials_supplied
13
+ })
14
+ end
15
+
16
+ def clean_service_url(dirty_service)
17
+ return dirty_service if dirty_service.blank?
18
+ service_uri = Addressable::URI.parse dirty_service
19
+ unless service_uri.query_values.nil?
20
+ service_uri.query_values = service_uri.query_values.except('service', 'ticket', 'gateway', 'renew')
21
+ end
22
+ clean_service = service_uri.to_s
23
+
24
+ logger.debug("Cleaned dirty service URL '#{dirty_service}' to '#{clean_service}'") if dirty_service != clean_service
25
+
26
+ clean_service
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_record'
2
+
3
+ module CASinoCore
4
+ module Model
5
+ autoload :LoginTicket, 'casino_core/model/login_ticket.rb'
6
+ autoload :ServiceTicket, 'casino_core/model/service_ticket.rb'
7
+ autoload :ProxyGrantingTicket, 'casino_core/model/proxy_granting_ticket.rb'
8
+ autoload :TicketGrantingTicket, 'casino_core/model/ticket_granting_ticket.rb'
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ require 'casino_core/model'
2
+ require 'casino_core/settings'
3
+
4
+ class CASinoCore::Model::LoginTicket < ActiveRecord::Base
5
+ attr_accessible :ticket
6
+ validates :ticket, uniqueness: true
7
+
8
+ def self.cleanup
9
+ self.delete_all(['created_at < ?', CASinoCore::Settings.login_ticket[:lifetime].seconds.ago])
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ require 'casino_core/model'
2
+
3
+ class CASinoCore::Model::ProxyGrantingTicket < ActiveRecord::Base
4
+ attr_accessible :iou, :ticket, :ticket_granting_ticket_id
5
+ validates :ticket, uniqueness: true
6
+ validates :iou, uniqueness: true
7
+ belongs_to :ticket_granting_ticket
8
+ end
@@ -0,0 +1,32 @@
1
+ require 'casino_core/model'
2
+ require 'casino_core/settings'
3
+ require 'addressable/uri'
4
+
5
+ class CASinoCore::Model::ServiceTicket < ActiveRecord::Base
6
+ autoload :SingleSignOutNotifier, 'casino_core/model/service_ticket/single_sign_out_notifier.rb'
7
+
8
+ attr_accessible :ticket, :service
9
+ validates :ticket, uniqueness: true
10
+ belongs_to :ticket_granting_ticket
11
+ before_destroy :send_single_sing_out_notification
12
+
13
+ def self.cleanup_unconsumed
14
+ self.delete_all(['created_at < ? AND consumed = ?', CASinoCore::Settings.service_ticket[:lifetime_unconsumed].seconds.ago, false])
15
+ end
16
+
17
+ def self.cleanup_consumed
18
+ self.destroy_all(['created_at < ? AND consumed = ?', CASinoCore::Settings.service_ticket[:lifetime_consumed].seconds.ago, true]).length
19
+ end
20
+
21
+ def service_with_ticket_url
22
+ service_uri = Addressable::URI.parse(self.service)
23
+ service_uri.query_values = (service_uri.query_values || {}).merge(ticket: self.ticket)
24
+ service_uri.to_s
25
+ end
26
+
27
+ private
28
+ def send_single_sing_out_notification
29
+ notifier = SingleSignOutNotifier.new(self)
30
+ notifier.notify
31
+ end
32
+ end
@@ -0,0 +1,53 @@
1
+ require 'builder'
2
+ require 'net/https'
3
+ require 'casino_core/model/service_ticket'
4
+
5
+ class CASinoCore::Model::ServiceTicket::SingleSignOutNotifier
6
+ def initialize(service_ticket)
7
+ @service_ticket = service_ticket
8
+ end
9
+
10
+ def notify
11
+ xml = build_xml
12
+ uri = URI.parse(@service_ticket.service)
13
+ request = build_request(uri, xml)
14
+ send_notification(uri, request)
15
+ end
16
+
17
+ private
18
+ def build_xml
19
+ xml = Builder::XmlMarkup.new(indent: 2)
20
+ xml.samlp :LogoutRequest, ID: SecureRandom.uuid, Version: '2.0', IsseInstant: Time.now do |logout_request|
21
+ logout_request.samlp :NameID, '@NOT_USED@'
22
+ logout_request.samlp :SessionIndex, @service_ticket.ticket
23
+ end
24
+ xml.target!
25
+ end
26
+
27
+ def build_request(uri, xml)
28
+ request = Net::HTTP::Post.new(uri.path || '/')
29
+ request.set_form_data(logoutRequest: xml)
30
+ return request
31
+ end
32
+
33
+ def send_notification(uri, request)
34
+ begin
35
+ http = Net::HTTP.new(uri.host, uri.port)
36
+ http.use_ssl = true if uri.scheme =='https'
37
+
38
+ http.start do |conn|
39
+ response = conn.request(request)
40
+ if response.kind_of? Net::HTTPSuccess
41
+ logger.info "Logout notification successfully posted to #{uri}."
42
+ return true
43
+ else
44
+ logger.warn "Service #{uri} responed to logout notification with code '#{response.code}'!"
45
+ return false
46
+ end
47
+ end
48
+ rescue Exception => e
49
+ logger.warn "Failed to send logout notification to service #{uri} due to #{e}"
50
+ return false
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ require 'casino_core/model'
2
+
3
+ class CASinoCore::Model::TicketGrantingTicket < ActiveRecord::Base
4
+ attr_accessible :ticket, :username, :user_agent, :extra_attributes
5
+ serialize :extra_attributes, Hash
6
+ validates :ticket, uniqueness: true
7
+ has_many :service_tickets
8
+ has_many :proxy_granting_tickets
9
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_record'
2
+
3
+ module CASinoCore
4
+ class Processor
5
+ autoload :LegacyValidator, 'casino_core/processor/legacy_validator.rb'
6
+ autoload :LoginCredentialAcceptor, 'casino_core/processor/login_credential_acceptor.rb'
7
+ autoload :LoginCredentialRequestor, 'casino_core/processor/login_credential_requestor.rb'
8
+ autoload :Logout, 'casino_core/processor/logout.rb'
9
+ autoload :SessionDestroyer, 'casino_core/processor/session_destroyer.rb'
10
+
11
+ def initialize(listener)
12
+ @listener = listener
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,49 @@
1
+ require 'casino_core/processor'
2
+ require 'casino_core/helper'
3
+ require 'casino_core/model'
4
+
5
+ class CASinoCore::Processor::LegacyValidator < CASinoCore::Processor
6
+ include CASinoCore::Helper
7
+ include CASinoCore::Helper::ServiceTickets
8
+
9
+ def process(params = nil)
10
+ params ||= {}
11
+ ticket = CASinoCore::Model::ServiceTicket.where(ticket: params[:ticket]).first
12
+ if ticket_valid_for_service?(ticket, params[:service], !!params[:renew])
13
+ @listener.validation_succeeded("yes\n#{ticket.ticket_granting_ticket.username}\n")
14
+ else
15
+ @listener.validation_failed("no\n\n")
16
+ end
17
+ end
18
+
19
+ private
20
+ def ticket_valid_for_service?(ticket, service, renew = false)
21
+ ticket_valid = if service.nil? or ticket.nil?
22
+ logger.warn 'Invalid validate request: no valid ticket or no valid service given'
23
+ false
24
+ else
25
+ if ticket.consumed?
26
+ logger.warn "Service ticket '#{ticket.ticket}' already consumed"
27
+ false
28
+ elsif Time.now - ticket.created_at > CASinoCore::Settings.service_ticket[:lifetime_unconsumed]
29
+ logger.warn "Service ticket '#{ticket.ticket}' has expired"
30
+ false
31
+ elsif clean_service_url(service) != ticket.service
32
+ logger.warn "Service ticket '#{ticket.ticket}' is not valid for service '#{service}'"
33
+ false
34
+ elsif renew && !ticket.issued_from_credentials?
35
+ logger.info "Service ticket '#{ticket.ticket}' was not issued from credentials but service '#{service}' will only accept a renewed ticket"
36
+ false
37
+ else
38
+ logger.info "Service ticket '#{ticket.ticket}' for service '#{service}' successfully validated"
39
+ true
40
+ end
41
+ end
42
+ unless ticket.nil?
43
+ logger.debug "Consumed ticket '#{ticket.ticket}'"
44
+ ticket.consumed = true
45
+ ticket.save!
46
+ end
47
+ ticket_valid
48
+ end
49
+ end