casino_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,67 @@
1
+ require 'casino_core/processor'
2
+ require 'casino_core/helper'
3
+
4
+ class CASinoCore::Processor::LoginCredentialAcceptor < CASinoCore::Processor
5
+ include CASinoCore::Helper
6
+ include CASinoCore::Helper::ServiceTickets
7
+
8
+ def process(params = nil, cookies = nil, user_agent = nil)
9
+ params ||= {}
10
+ cookies ||= {}
11
+ if login_ticket_valid?(params[:lt])
12
+ user_data = validate_login_credentials(params[:username], params[:password])
13
+ if !user_data.nil?
14
+ ticket_granting_ticket = acquire_ticket_granting_ticket(user_data, user_agent)
15
+ url = unless params[:service].nil?
16
+ acquire_service_ticket(ticket_granting_ticket, params[:service], true).service_with_ticket_url
17
+ end
18
+ @listener.user_logged_in(url, ticket_granting_ticket.ticket)
19
+ else
20
+ @listener.invalid_login_credentials
21
+ end
22
+ else
23
+ @listener.invalid_login_ticket
24
+ end
25
+ end
26
+
27
+ private
28
+ def login_ticket_valid?(lt)
29
+ ticket = CASinoCore::Model::LoginTicket.find_by_ticket lt
30
+ if ticket.nil?
31
+ logger.info "Login ticket '#{lt}' not found"
32
+ false
33
+ elsif ticket.created_at < CASinoCore::Settings.login_ticket[:lifetime].seconds.ago
34
+ logger.info "Login ticket '#{ticket.ticket}' expired"
35
+ false
36
+ else
37
+ logger.debug "Login ticket '#{ticket.ticket}' successfully validated"
38
+ ticket.delete
39
+ true
40
+ end
41
+ end
42
+
43
+ def validate_login_credentials(username, password)
44
+ user_data = nil
45
+ CASinoCore::Settings.authenticators.each do |authenticator|
46
+ data = authenticator.validate(username, password)
47
+ if data
48
+ if data[:username].nil?
49
+ data[:username] = username
50
+ end
51
+ user_data = data
52
+ logger.info("Credentials for username '#{data[:username]}' successfully validated using #{authenticator.class}")
53
+ break
54
+ end
55
+ end
56
+ user_data
57
+ end
58
+
59
+ def acquire_ticket_granting_ticket(user_data, user_agent = nil)
60
+ CASinoCore::Model::TicketGrantingTicket.create!({
61
+ ticket: random_ticket_string('TGC'),
62
+ username: user_data[:username],
63
+ extra_attributes: user_data[:extra_attributes],
64
+ user_agent: user_agent
65
+ })
66
+ end
67
+ end
@@ -0,0 +1,45 @@
1
+ require 'casino_core/processor'
2
+ require 'casino_core/helper'
3
+
4
+ class CASinoCore::Processor::LoginCredentialRequestor < CASinoCore::Processor
5
+ include CASinoCore::Helper
6
+ include CASinoCore::Helper::ServiceTickets
7
+ include CASinoCore::Helper::Browser
8
+
9
+ def process(params = nil, cookies = nil, user_agent = nil)
10
+ params ||= {}
11
+ cookies ||= {}
12
+ request_env ||= {}
13
+ if !params[:renew] && (ticket_granting_ticket = find_valid_ticket_granting_ticket(cookies[:tgt], user_agent))
14
+ # TODO create new service ticket and url
15
+ service_url_with_ticket = unless params[:service].nil?
16
+ acquire_service_ticket(ticket_granting_ticket, params[:service], true).service_with_ticket_url
17
+ end
18
+ @listener.user_logged_in(service_url_with_ticket)
19
+ else
20
+ login_ticket = acquire_login_ticket
21
+ @listener.user_not_logged_in(login_ticket)
22
+ end
23
+ end
24
+
25
+ private
26
+ def acquire_login_ticket
27
+ ticket = CASinoCore::Model::LoginTicket.create ticket: random_ticket_string('LT')
28
+ logger.debug "Created login ticket '#{ticket.ticket}'"
29
+ ticket
30
+ end
31
+
32
+ def find_valid_ticket_granting_ticket(tgt, user_agent)
33
+ ticket_granting_ticket = CASinoCore::Model::TicketGrantingTicket.where(ticket: tgt).first
34
+ unless ticket_granting_ticket.nil?
35
+ if same_browser?(ticket_granting_ticket.user_agent, user_agent)
36
+ ticket_granting_ticket.user_agent = user_agent
37
+ ticket_granting_ticket.save!
38
+ ticket_granting_ticket
39
+ else
40
+ logger.info 'User-Agent changed: ticket-granting ticket not valid for this browser'
41
+ nil
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ require 'casino_core/processor'
2
+ require 'casino_core/helper'
3
+ require 'casino_core/model'
4
+
5
+ class CASinoCore::Processor::Logout < CASinoCore::Processor
6
+ include CASinoCore::Helper
7
+
8
+ def process(params = nil, cookies = nil)
9
+ params = params || {}
10
+ cookies ||= {}
11
+ session_destroyer = CASinoCore::Processor::SessionDestroyer.new(DummyListener.new)
12
+ session_destroyer.process(cookies[:tgt])
13
+ @listener.user_logged_out(params[:url])
14
+ end
15
+
16
+ class DummyListener
17
+ def ticket_deleted(*args)
18
+ end
19
+
20
+ def ticket_not_found(*args)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ require 'casino_core/processor'
2
+ require 'casino_core/helper'
3
+ require 'casino_core/model'
4
+
5
+ class CASinoCore::Processor::SessionDestroyer < CASinoCore::Processor
6
+ include CASinoCore::Helper
7
+
8
+ def process(tgt)
9
+ ticket = CASinoCore::Model::TicketGrantingTicket.where(ticket: tgt).first
10
+ if ticket.nil?
11
+ @listener.ticket_not_found
12
+ else
13
+ ticket.destroy
14
+ @listener.ticket_deleted
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ require 'casino_core'
2
+ require 'rails'
3
+
4
+ module CASinoCore
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ CASinoCore::RakeTasks.load_tasks
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ module CASinoCore
2
+ class RakeTasks
3
+ class << self
4
+ def load_tasks
5
+ %w(
6
+ database
7
+ cleanup
8
+ ).each do |task|
9
+ load "casino_core/tasks/#{task}.rake"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ require 'casino_core/authenticator'
2
+
3
+ module CASinoCore
4
+ class Settings
5
+ class << self
6
+ attr_accessor :login_ticket, :service_ticket, :authenticators
7
+ def init(config = {})
8
+ config.each do |key,value|
9
+ if respond_to?("#{key}=")
10
+ send("#{key}=", value)
11
+ end
12
+ end
13
+ end
14
+
15
+ def authenticators=(authenticators)
16
+ @authenticators = []
17
+ authenticators.each do |authenticator|
18
+ unless authenticator.is_a?(CASinoCore::Authenticator)
19
+ authenticator = authenticator[:class].constantize.new(authenticator[:options])
20
+ end
21
+ @authenticators << authenticator
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ require 'yaml'
2
+ require 'logger'
3
+ require 'active_record'
4
+ require 'casino_core/model'
5
+
6
+ namespace :casino_core do
7
+ namespace :cleanup do
8
+ desc 'Remove expired service tickets.'
9
+ task service_tickets: 'casino_core:db:configure_connection' do
10
+ [:consumed, :unconsumed].each do |type|
11
+ rows_affected = CASinoCore::Model::ServiceTicket.send("cleanup_#{type}")
12
+ puts "Deleted #{rows_affected} #{type} service tickets."
13
+ end
14
+ end
15
+
16
+ desc 'Remove expired login tickets.'
17
+ task login_tickets: 'casino_core:db:configure_connection' do
18
+ rows_affected = CASinoCore::Model::LoginTicket.cleanup
19
+ puts "Deleted #{rows_affected} login tickets."
20
+ end
21
+
22
+ desc 'Perform all cleanup tasks.'
23
+ task all: [:service_tickets, :login_tickets] do
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ require 'yaml'
2
+ require 'logger'
3
+ require 'active_record'
4
+
5
+ namespace :casino_core do
6
+ namespace :db do
7
+ task :environment do
8
+ BASE_DIR = if Gem.loaded_specs['casino_core'].nil?
9
+ Dir.pwd
10
+ else
11
+ Gem.loaded_specs['casino_core'].full_gem_path
12
+ end
13
+ DATABASE_ENV = ENV['DATABASE_ENV'] || ENV['RAILS_ENV'] || 'development'
14
+ MIGRATIONS_DIR = File.join(BASE_DIR, 'db', 'migrate')
15
+ SCHEMA_PATH = ENV['SCHEMA'] || File.join(BASE_DIR, 'db', 'schema.rb')
16
+ end
17
+
18
+ task :configuration => :environment do
19
+ @config = YAML.load_file('config/database.yml')[DATABASE_ENV]
20
+ end
21
+
22
+ task :configure_connection => :configuration do
23
+ ActiveRecord::Base.establish_connection @config
24
+ ActiveRecord::Base.logger = Logger.new STDOUT if @config['logger']
25
+ end
26
+
27
+ desc 'Migrate the database (options: VERSION=x, VERBOSE=false)'
28
+ task :migrate => :configure_connection do
29
+ ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
30
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
31
+ ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
32
+ end
33
+ end
34
+
35
+ desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
36
+ task :rollback => [:environment, :load_config] do
37
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
38
+ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
39
+ end
40
+
41
+ namespace :schema do
42
+ desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
43
+ task :dump => :configure_connection do
44
+ require 'active_record/schema_dumper'
45
+ File.open(SCHEMA_PATH, "w:utf-8") do |file|
46
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
47
+ end
48
+ end
49
+
50
+ desc 'Load a schema.rb file into the database'
51
+ task :load => :configure_connection do
52
+ if File.exists?(SCHEMA_PATH)
53
+ load(SCHEMA_PATH)
54
+ else
55
+ abort %{#{SCHEMA_PATH} doesn't exist yet.}
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe CASinoCore::Authenticator::Static do
4
+ subject {
5
+ CASinoCore::Authenticator::Static.new({
6
+ users: {
7
+ user: {
8
+ password: 'testing123',
9
+ fullname: 'Example User'
10
+ }
11
+ }
12
+ })
13
+ }
14
+
15
+ context '#validate' do
16
+ context 'with invalid credentials' do
17
+ it 'returns false for an unknown username' do
18
+ subject.validate('foobar', 'test').should == false
19
+ end
20
+
21
+ it 'returns false for a known username with wrong password' do
22
+ subject.validate('user', 'test').should == false
23
+ end
24
+ end
25
+
26
+ context 'with valid credentials' do
27
+ let(:result) { subject.validate('user', 'testing123') }
28
+
29
+ it 'does not return false' do
30
+ result.should_not == false
31
+ end
32
+
33
+ it 'returns the username' do
34
+ result[:username].should == 'user'
35
+ end
36
+
37
+ it 'returns extra attributes' do
38
+ result[:extra_attributes][:fullname].should == 'Example User'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe CASinoCore::Model::LoginTicket do
4
+ describe '.cleanup' do
5
+ it 'deletes expired login tickets' do
6
+ ticket = described_class.new ticket: 'LT-12345'
7
+ ticket.save!
8
+ ticket.created_at = 10.hours.ago
9
+ ticket.save!
10
+ lambda do
11
+ described_class.cleanup
12
+ end.should change(described_class, :count).by(-1)
13
+ described_class.find_by_ticket('LT-12345').should be_false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe CASinoCore::Model::ServiceTicket do
4
+ let(:ticket) {
5
+ ticket = described_class.new ticket: 'ST-12345', service: 'https://example.com/cas-service'
6
+ ticket.ticket_granting_ticket_id = 1
7
+ ticket
8
+ }
9
+
10
+ describe '.cleanup_unconsumed' do
11
+ it 'deletes expired unconsumed service tickets' do
12
+ ticket.created_at = 10.hours.ago
13
+ ticket.save!
14
+ lambda do
15
+ described_class.cleanup_unconsumed
16
+ end.should change(described_class, :count).by(-1)
17
+ described_class.find_by_ticket('ST-12345').should be_false
18
+ end
19
+ end
20
+
21
+ describe '.cleanup_consumed' do
22
+ before(:each) do
23
+ described_class::SingleSignOutNotifier.any_instance.stub(:notify).and_return(true)
24
+ end
25
+
26
+ it 'deletes expired consumed service tickets' do
27
+ ticket.consumed = true
28
+ ticket.created_at = 10.days.ago
29
+ ticket.save!
30
+ lambda do
31
+ described_class.cleanup_consumed
32
+ end.should change(described_class, :count).by(-1)
33
+ described_class.find_by_ticket('ST-12345').should be_false
34
+ end
35
+ end
36
+
37
+ describe '.destroy' do
38
+ it 'sends out a single sign out notification' do
39
+ described_class::SingleSignOutNotifier.any_instance.should_receive(:notify).and_return(true)
40
+ ticket.consumed = true
41
+ ticket.save!
42
+ ticket.destroy
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe CASinoCore::Processor::LegacyValidator do
4
+ describe '#process' do
5
+ let(:listener) { Object.new }
6
+ let(:processor) { described_class.new(listener) }
7
+ let(:user_agent) { 'TestBrowser 1.0' }
8
+ let(:ticket_granting_ticket) {
9
+ CASinoCore::Model::TicketGrantingTicket.create!({
10
+ ticket: 'TGC-HXdkW233TsRtiqYGq4b8U7',
11
+ username: 'test',
12
+ extra_attributes: nil,
13
+ user_agent: user_agent
14
+ })
15
+ }
16
+ let(:service) { 'https://example.com/cas-service' }
17
+ let(:service_ticket) { ticket_granting_ticket.service_tickets.create! ticket: 'ST-2nOcXx56dtPTsB069yYf0h', service: service }
18
+ let(:parameters) { { service: service, ticket: service_ticket.ticket }}
19
+
20
+ before(:each) do
21
+ listener.stub(:validation_failed)
22
+ listener.stub(:validation_succeeded)
23
+ end
24
+
25
+ context 'with an unconsumed service ticket' do
26
+ context 'without renew flag' do
27
+ it 'consumes the service ticket' do
28
+ processor.process(parameters)
29
+ service_ticket.reload
30
+ service_ticket.consumed.should == true
31
+ end
32
+
33
+ it 'calls the #validation_succeeded method on the listener' do
34
+ listener.should_receive(:validation_succeeded).with("yes\ntest\n")
35
+ processor.process(parameters)
36
+ end
37
+ end
38
+
39
+ context 'with renew flag' do
40
+ let(:parameters) { { service: service, ticket: service_ticket.ticket, renew: 'true' }}
41
+
42
+ context 'with a service ticket without issued_from_credentials flag' do
43
+ it 'consumes the service ticket' do
44
+ processor.process(parameters)
45
+ service_ticket.reload
46
+ service_ticket.consumed.should == true
47
+ end
48
+
49
+ it 'calls the #validation_failed method on the listener' do
50
+ listener.should_receive(:validation_failed).with("no\n\n")
51
+ processor.process(parameters)
52
+ end
53
+ end
54
+
55
+ context 'with a service ticket with issued_from_credentials flag' do
56
+ before(:each) do
57
+ service_ticket.issued_from_credentials = true
58
+ service_ticket.save!
59
+ end
60
+
61
+ it 'consumes the service ticket' do
62
+ processor.process(parameters)
63
+ service_ticket.reload
64
+ service_ticket.consumed.should == true
65
+ end
66
+
67
+ it 'calls the #validation_succeeded method on the listener' do
68
+ listener.should_receive(:validation_succeeded).with("yes\ntest\n")
69
+ processor.process(parameters)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ context 'with a consumed service ticket' do
76
+ before(:each) do
77
+ service_ticket.consumed = true
78
+ service_ticket.save!
79
+ end
80
+
81
+ it 'calls the #validation_failed method on the listener' do
82
+ listener.should_receive(:validation_failed).with("no\n\n")
83
+ processor.process(parameters)
84
+ end
85
+ end
86
+ end
87
+ end