cassette 1.0.0

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.
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
+