cassette 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MzAxZTBjNTNjZjEwYWY0YzQ4Y2U0YjliYzFhMzYyNjAyM2Y1NDUxYw==
5
+ data.tar.gz: !binary |-
6
+ NDMwZGU1NmEzNWNjMDAxODY2NjNmODgwNDM5MDgwM2QzY2VlZTBjYg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OWNmZjk2MGUyMjI5NzkwNjhkMDczOTI2NjJkZWVjMDgxYzRlZjBkZDRjYWM0
10
+ ODUyZjAzMTBiOGMyM2ZhYWMyZTc4MDhlN2Q1ZmRlZWJhMzQ1MGYyYmVmZWZk
11
+ MWI5YzAxZDgzMTQ4NDQ3NjVkYjQxM2RhMDIwZmY4Zjg2MTI4ZWE=
12
+ data.tar.gz: !binary |-
13
+ Yzc1ZjM0MTk4OTQyYTJmYTMxODRiYWRjOWY0N2ZjMDBmMDcwZDI4ZDYxOTg1
14
+ YTI5M2U3ODhhZTMyNjc4MGYyZDE5OTU2MzgxODQxOWY1NDIyMmI3MTU4Yjg1
15
+ MjIxOWRhNWFiMDc2YTIxYjVhZjNjNGY4NGNhNjkyOTgxNmFlZjY=
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Cassette::Client
2
+
3
+ Library to generate and validate STs and TGTs
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'cassette'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ ## Usage
16
+
17
+ Require this library and create an intializer to set its configuration:
18
+
19
+
20
+ Cassette.config = config
21
+
22
+
23
+ where config is an object that responds to the methods #base for the base CAS uri, #username and #password
24
+ if you are authenticating on other systems and #service and #base\_authority if you are using the authentication filter
25
+ to authenticate your app
26
+
27
+
28
+ You may also set the caching backend using the .backend= module method:
29
+
30
+
31
+ Cassette::Cache.backend = ActiveSupport::Cache::MemcacheStorage.new
32
+
33
+
34
+ By default, Cassette::Cache will check if you have Rails.cache defined or instantiate a new ActiveSupport::Cache::MemoryStore
35
+
36
+
37
+ To authenticate your Rails app, add to your ApplicationController (or any authenticated controller):
38
+
39
+
40
+ class ApplicationController < ActionController::Base
41
+ include Cassette::Authentication::Filter
42
+
43
+
44
+ (...)
45
+
46
+ end
47
+
48
+
49
+ You should also rescue from Cassette::Errors::Forbidden with more friendly errors
50
+
51
+ If you wish to have actions that skip the authentication filter, add to your controller:
52
+
53
+
54
+ skip_authentication [options]
55
+
56
+
57
+ Where options are the same options you can pass to Rails' __skip_before_filter__ method
58
+
59
+ ## RubyCAS client helpers
60
+
61
+
62
+ If you are authenticating users with RubyCAS and want role checking, in your rubycas initializer:
63
+
64
+
65
+ require "cas/rubycas"
66
+
67
+
68
+ And in your ApplicationController (or any authenticated controller):
69
+
70
+
71
+ include Cassette::Rubycas::Helper
72
+
73
+ # - Allow only employees:
74
+ #
75
+ # before_filter :employee_only_filter
76
+ #
77
+ # rescue_from Cassette::Errors::NotAnEmployee d
78
+ # redirect_to '/403.html'
79
+ # end
80
+
81
+ # - Allow only customers:
82
+ #
83
+ # before_filter :customer_only_filter
84
+ #
85
+ # rescue_from Cassette::Errors::NotACustomer do
86
+ # redirect_to '/403.html'
87
+ # end
88
+
89
+
90
+ ## Instantiating Cassette::Client and Cassette::Authentication
91
+
92
+ You can create your own instances of __Cassette::Client__ (st/tgt generator) and __Cassette::Authentication__ (st validator).
93
+
94
+ The constructor accepts a hash with keys (as symbols) for the values of cache, logger, http_client and configuration.
95
+
96
+ All values default to the same values used when accessing the class methods directly.
97
+
98
+ Please check the constructors or integration specs for details.
99
+
100
+ ## Contributing
101
+
102
+ 1. Fork it
103
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
104
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
105
+ 4. Push to the branch (`git push origin my-new-feature`)
106
+ 5. Create new Pull Request
@@ -0,0 +1,37 @@
1
+ # encoding: UTF-8
2
+
3
+ require "cassette/authentication"
4
+
5
+ class Cassette::Authentication::Authorities
6
+ def self.parse(authorities, base_authority = nil)
7
+ new(authorities, base_authority)
8
+ end
9
+
10
+ def base
11
+ @base_authority.to_s.upcase
12
+ end
13
+
14
+ def has_raw_role?(role)
15
+ return true if ENV["NOAUTH"]
16
+ @authorities.include?(role)
17
+ end
18
+
19
+ def has_role?(role)
20
+ return true if ENV["NOAUTH"]
21
+ has_raw_role?("#{base}_#{role.to_s.upcase.gsub("_", "-")}")
22
+ end
23
+
24
+ def initialize(authorities, base_authority = nil)
25
+ @base_authority = base_authority || Cassette.config.base_authority
26
+
27
+ if authorities.is_a?(String)
28
+ @authorities = authorities.gsub(/^\[(.*)\]$/, "\\1").split(",").map(&:strip)
29
+ else
30
+ @authorities = Array(authorities).map(&:strip)
31
+ end
32
+ end
33
+
34
+ def authorities
35
+ @authorities.dup
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+
3
+ require "cassette/authentication"
4
+ require "cassette/cache"
5
+
6
+ class Cassette::Authentication::Cache
7
+ include Cassette::Cache
8
+
9
+ def initialize(logger)
10
+ self.logger = logger
11
+ end
12
+
13
+ def fetch_authentication(ticket, options = {}, &block)
14
+ options = {expires_in: 5 * 60, max_uses: 5000, force: false}.merge(options)
15
+ fetch("Cassette::Authentication.validate_ticket(#{ticket})", options) do
16
+ logger.info("Authentication for #{ticket} is not cached")
17
+ block.call
18
+ end
19
+ end
20
+
21
+ def clear_authentication_cache!
22
+ backend.delete_matched("Cassette::Authentication.validate_ticket*")
23
+ backend.delete_matched("#{uses_key("Cassette::Authentication.validate_ticket")}*")
24
+ end
25
+
26
+ protected
27
+
28
+ attr_accessor :logger
29
+ end
30
+
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+
3
+ require "active_support/concern"
4
+ require "cassette/authentication/user"
5
+
6
+ module Cassette::Authentication::Filter
7
+ extend ActiveSupport::Concern
8
+
9
+ included do |controller|
10
+ controller.before_filter(:validate_authentication_ticket)
11
+ controller.send(:attr_accessor, :current_user)
12
+ end
13
+
14
+ module ClassMethods
15
+ def skip_authentication(*options)
16
+ skip_before_filter :validate_authentication_ticket, *options
17
+ end
18
+ end
19
+
20
+ def validate_authentication_ticket(service = Cassette.config.service)
21
+ ticket = request.headers["Service-Ticket"] || params[:ticket]
22
+
23
+ if ENV["NOAUTH"] && !ticket
24
+ Cassette.logger.debug "NOAUTH set and no Service Ticket, skipping authentication"
25
+ self.current_user = Cassette::Authentication::User.new
26
+ return
27
+ end
28
+
29
+ self.current_user = Cassette::Authentication.validate_ticket(ticket, service)
30
+ end
31
+
32
+ def validate_role!(role)
33
+ return if ENV["NOAUTH"]
34
+ raise Cassette::Errors::Forbidden unless current_user.has_role?(role)
35
+ end
36
+
37
+ def validate_raw_role!(role)
38
+ return if ENV["NOAUTH"]
39
+ raise Cassette::Errors::Forbidden unless current_user.has_raw_role?(role)
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ require "cassette/authentication"
4
+ require "cassette/authentication/authorities"
5
+ require "delegate"
6
+
7
+ class Cassette::Authentication::User
8
+ attr_accessor :login, :name, :authorities, :email, :ticket
9
+ delegate :has_role?, :has_raw_role?, to: :@authorities
10
+
11
+ def initialize(attrs = {})
12
+ config = attrs[:config]
13
+ @login = attrs[:login]
14
+ @name = attrs[:name]
15
+ @type = attrs[:type]
16
+ @email = attrs[:email]
17
+ @ticket = attrs[:ticket]
18
+ @authorities = Cassette::Authentication::Authorities
19
+ .parse(attrs.fetch(:authorities, "[]"), config && config.base_authority)
20
+ end
21
+
22
+ %w(customer employee).each do |type|
23
+ define_method :"#{type}?" do
24
+ !@type.nil? && @type.to_s.downcase == type.to_s
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,72 @@
1
+ # encoding: UTF-8
2
+
3
+ require "active_support/xml_mini"
4
+ ActiveSupport::XmlMini.backend = 'LibXML'
5
+
6
+ module Cassette
7
+ class Authentication
8
+ def self.method_missing(name, *args)
9
+ @@default_authentication ||= new
10
+ @@default_authentication.send(name, *args)
11
+ end
12
+
13
+ def initialize(opts = {})
14
+ self.config = opts.fetch(:config, Cassette.config)
15
+ self.logger = opts.fetch(:logger, Cassette.logger)
16
+ self.http = opts.fetch(:http_client, Cassette)
17
+ self.cache = opts.fetch(:cache, Cassette::Authentication::Cache.new(logger))
18
+ end
19
+
20
+ def validate_ticket(ticket, service = config.service)
21
+ logger.debug "Cassette::Authentication validating ticket: #{ticket}"
22
+ raise Cassette::Errors::AuthorizationRequired if ticket.nil? || ticket.blank?
23
+
24
+ user = ticket_user(ticket, service)
25
+ logger.info "Cassette::Authentication user: #{user.inspect}"
26
+
27
+ raise Cassette::Errors::Forbidden unless user
28
+
29
+ user
30
+ end
31
+
32
+ def ticket_user(ticket, service = config.service)
33
+ cache.fetch_authentication(ticket) do
34
+ begin
35
+ logger.info("Validating #{ticket} on #{validate_uri}")
36
+ response = http.post(validate_uri, ticket: ticket, service: service).body
37
+
38
+ logger.info("Validation resut: #{response.inspect}")
39
+
40
+ user = nil
41
+
42
+ ActiveSupport::XmlMini.with_backend("LibXML") do
43
+ result = ActiveSupport::XmlMini.parse(response)
44
+
45
+ login = result.try(:[], "serviceResponse").try(:[], "authenticationSuccess").try(:[], "user").try(:[], "__content__")
46
+
47
+ if login
48
+ attributes = result["serviceResponse"]["authenticationSuccess"]["attributes"]
49
+ name = attributes.try(:[], "cn").try(:[], "__content__")
50
+ authorities = attributes.try(:[], "authorities").try(:[], "__content__")
51
+
52
+ user = Cassette::Authentication::User.new(login: login, name: name, authorities: authorities, ticket: ticket, config: config)
53
+ end
54
+ end
55
+
56
+ user
57
+ rescue => exception
58
+ logger.error "Error while authenticating ticket #{ticket}: #{exception.message}"
59
+ raise Cassette::Errors::Forbidden.new(exception.message)
60
+ end
61
+ end
62
+ end
63
+
64
+ protected
65
+
66
+ attr_accessor :cache, :logger, :http, :config
67
+
68
+ def validate_uri
69
+ "#{config.base.gsub(/\/?$/, "")}/serviceValidate"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: UTF-8
2
+
3
+ require "active_support/cache"
4
+
5
+ module Cassette
6
+ module Cache
7
+ def backend
8
+ @backend ||= begin
9
+ if defined?(Rails) && Rails.cache
10
+ Rails.cache
11
+ else
12
+ ActiveSupport::Cache::MemoryStore.new
13
+ end
14
+ end
15
+ end
16
+
17
+ def backend=(backend)
18
+ @backend = backend
19
+ end
20
+
21
+ def uses_key(key)
22
+ "uses:#{key}"
23
+ end
24
+
25
+ def fetch(key, options = {}, &block)
26
+ if options[:max_uses].to_i != 0
27
+ uses_key = self.uses_key(key)
28
+ uses = backend.read(uses_key, raw: true)
29
+ backend.write(uses_key, 0, raw: true, expires_in: options[:expires_in]) if uses.nil?
30
+
31
+ if uses.to_i >= options[:max_uses].to_i
32
+ options[:force] = true
33
+ backend.write(uses_key, 0, raw: true, expires_in: options[:expires_in])
34
+ else
35
+ backend.increment(uses_key)
36
+ end
37
+ end
38
+
39
+ backend.fetch(key, options, &block)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: UTF-8
2
+
3
+ require "cassette/client"
4
+ require "cassette/cache"
5
+
6
+ class Cassette::Client::Cache
7
+ include Cassette::Cache
8
+
9
+ def initialize(logger)
10
+ self.logger = logger
11
+ end
12
+
13
+ def fetch_tgt(options = {}, &block)
14
+ options = {expires_in: 4 * 3600, max_uses: 5000, force: false}.merge(options)
15
+ fetch("Cassette::Client.tgt", options) do
16
+ self.clear_st_cache!
17
+ logger.info "TGT is not cached"
18
+ yield
19
+ end
20
+ end
21
+
22
+ def fetch_st(service, options = {}, &block)
23
+ options = {max_uses: 2000, expires_in: 252, force: false}.merge(options)
24
+ fetch("Cassette::Client.st(#{service})", options) do
25
+ logger.info "ST for #{service} is not cached"
26
+ yield
27
+ end
28
+ end
29
+
30
+ def clear_tgt_cache!
31
+ backend.delete("Cassette::Client.tgt")
32
+ backend.delete("#{uses_key("Cassette::Client.tgt")}")
33
+ end
34
+
35
+ def clear_st_cache!
36
+ backend.delete_matched("Cassette::Client.st*")
37
+ backend.delete_matched("#{uses_key("Cassette::Client.st")}*")
38
+ end
39
+
40
+ protected
41
+
42
+ attr_accessor :logger
43
+ end
@@ -0,0 +1,68 @@
1
+ # encoding: UTF-8
2
+
3
+ module Cassette
4
+ class Client
5
+ def self.method_missing(name, *args)
6
+ @@default_client ||= new
7
+ @@default_client.send(name, *args)
8
+ end
9
+
10
+ def initialize(opts = {})
11
+ self.config = opts.fetch(:config, Cassette.config)
12
+ self.logger = opts.fetch(:logger, Cassette.logger)
13
+ self.http = opts.fetch(:http_client, Cassette)
14
+ self.cache = opts.fetch(:cache, Cassette::Client::Cache.new(logger))
15
+ end
16
+
17
+ def health_check
18
+ st_for("monitoring")
19
+ end
20
+
21
+ def tgt(usr, pwd, force = false)
22
+ logger.info "Requesting TGT"
23
+ cache.fetch_tgt(force: force) do
24
+ response = http.post(tickets_uri, username: usr, password: pwd)
25
+ tgt = $1 if response.headers["Location"] =~ /tickets\/(.*)/
26
+ logger.info "TGT is #{tgt}"
27
+ tgt
28
+ end
29
+ end
30
+
31
+ def st(tgt, service, force = false)
32
+ logger.info "Requesting ST for #{service}"
33
+ cache.fetch_st(service, force: force) do
34
+ response = http.post("#{tickets_uri}/#{tgt}", service: service)
35
+ response.body.tap do |st|
36
+ logger.info "ST is #{st}"
37
+ end
38
+ end
39
+ end
40
+
41
+ def st_for(service_name)
42
+ st_with_retry(config.username, config.password, service_name)
43
+ end
44
+
45
+ protected
46
+
47
+ attr_accessor :cache, :logger, :http, :config
48
+
49
+ def st_with_retry(user, pass, service)
50
+ retrying = false
51
+ begin
52
+ st(tgt(user, pass, retrying), service)
53
+ rescue Cassette::Errors::NotFound => e
54
+ unless retrying
55
+ logger.info "Got 404 response, regenerating TGT"
56
+ retrying = true
57
+ retry
58
+ end
59
+ raise e
60
+ end
61
+ end
62
+
63
+ def tickets_uri
64
+ "#{config.base.gsub(/\/?$/, "")}/v1/tickets"
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ require "cassette/errors"
4
+
5
+ module Cassette
6
+ module Errors
7
+ class NotACustomer < Cassette::Errors::Base
8
+ def code
9
+ 403
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ require "cassette/errors"
4
+
5
+ module Cassette
6
+ module Errors
7
+ class NotAnEmployee < Cassette::Errors::Base
8
+ def code
9
+ 403
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,44 @@
1
+ # encoding: UTF-8
2
+
3
+ require "active_support/inflector"
4
+
5
+ module Cassette
6
+ module Errors
7
+ TYPES = {
8
+ 401 => :authorization_required,
9
+ 400 => :bad_request,
10
+ 403 => :forbidden,
11
+ 500 => :internal_server_error,
12
+ 404 => :not_found,
13
+ 412 => :precondition_failed,
14
+ }
15
+
16
+ def self.raise_by_code(code)
17
+ name = TYPES[code.to_i]
18
+
19
+ if name
20
+ raise error_class(name)
21
+ else
22
+ raise error_class(:internal_server_error)
23
+ end
24
+ end
25
+
26
+ def self.error_class(name)
27
+ "Cassette::Errors::#{name.to_s.camelize}".constantize
28
+ end
29
+
30
+ class Base < StandardError
31
+ def code
32
+ self.class.const_get("CODE")
33
+ end
34
+ end
35
+
36
+ TYPES.each do |status, name|
37
+ const_set(name.to_s.camelize, Class.new(Errors::Base))
38
+ error_class(name).const_set("CODE", status)
39
+ end
40
+ end
41
+ end
42
+
43
+ require "cassette/errors/not_an_employee"
44
+ require "cassette/errors/not_a_customer"
@@ -0,0 +1,78 @@
1
+ # encoding: UTF-8
2
+
3
+ require "active_support/concern"
4
+
5
+ module Cassette
6
+ module Rubycas
7
+ module Helper
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_filter :validate_authentication_ticket
12
+ helper_method :current_user
13
+ end
14
+
15
+ module ClassMethods
16
+ def skip_authentication(*options)
17
+ skip_before_filter :validate_authentication_ticket, *options
18
+ end
19
+ end
20
+
21
+ def validate_authentication_ticket
22
+ return if ENV["NOAUTH"]
23
+ ::CASClient::Frameworks::Rails::Filter.filter(self)
24
+ end
25
+
26
+ def employee_only_filter
27
+ return if ENV["NOAUTH"] or current_user.blank?
28
+ raise Cassette::Errors::NotAnEmployee unless current_user.employee?
29
+ end
30
+
31
+ def customer_only_filter
32
+ return if ENV["NOAUTH"] or current_user.blank?
33
+ raise Cassette::Errors::NotACustomer unless current_user.customer?
34
+ end
35
+
36
+ def cas_logout(to = root_url)
37
+ session.destroy
38
+ ::CASClient::Frameworks::Rails::Filter.logout(self, to)
39
+ end
40
+
41
+ def fake_user
42
+ Cassette::Authentication::User.new({
43
+ login: "fake.user",
44
+ name: "Fake User",
45
+ email: "fake.user@locaweb.com.br",
46
+ authorities: [],
47
+ type: "customer"
48
+ })
49
+ end
50
+
51
+ def validate_role!(role)
52
+ return if ENV["NOAUTH"]
53
+ raise Cassette::Errors::Forbidden unless current_user.has_role?(role)
54
+ end
55
+
56
+ def validate_raw_role!(role)
57
+ return if ENV["NOAUTH"]
58
+ raise Cassette::Errors::Forbidden unless current_user.has_raw_role?(role)
59
+ end
60
+
61
+ def current_user
62
+ return fake_user if ENV["NOAUTH"]
63
+ return nil unless session[:cas_user]
64
+
65
+ @current_user ||= begin
66
+ attributes = session[:cas_extra_attributes]
67
+ Cassette::Authentication::User.new({
68
+ login: session[:cas_user],
69
+ name: attributes.try(:[], :cn),
70
+ email: attributes.try(:[], :email),
71
+ authorities: attributes.try(:[], :authorities),
72
+ type: attributes.try(:[], :type).try(:downcase)
73
+ })
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: UTF-8
2
+
3
+ require "cassette/rubycas/single_sign_out_constraint"
4
+
5
+ module Cassette
6
+ module Rubycas
7
+ class NotSingleSignOutConstraint < SingleSignOutConstraint
8
+ def matches?(request)
9
+ !super(request)
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ module Cassette
4
+ module Rubycas
5
+ class SingleSignOutConstraint
6
+ def matches?(request)
7
+ if (content_type = request.headers["CONTENT_TYPE"]) &&
8
+ content_type =~ /^multipart\//
9
+ return false
10
+ end
11
+
12
+ if request.post? &&
13
+ request.request_parameters['logoutRequest'] &&
14
+ [request.request_parameters['logoutRequest'],
15
+ URI.unescape(request.request_parameters['logoutRequest'])]
16
+ .find { |xml| xml =~ /^<samlp:LogoutRequest.*?<samlp:SessionIndex>(.*)<\/samlp:SessionIndex>/m }
17
+
18
+ Cassette.logger.debug "Intercepted a single sign out request on #{request}"
19
+ return true
20
+ end
21
+
22
+ false
23
+ end
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+
3
+ require "cassette/rubycas/helper"
4
+ require "cassette/rubycas/single_sign_out_constraint"
5
+ require "cassette/rubycas/not_single_sign_out_constraint"
6
+
7
+ module Cassette
8
+ module Rubycas
9
+ end
10
+ end
11
+