aker 3.0.0.pre

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 (118) hide show
  1. data/CHANGELOG.md +210 -0
  2. data/README.md +282 -0
  3. data/assets/aker/form/login.css +73 -0
  4. data/assets/aker/form/login.html.erb +44 -0
  5. data/lib/aker/authorities/automatic_access.rb +36 -0
  6. data/lib/aker/authorities/composite.rb +301 -0
  7. data/lib/aker/authorities/static.rb +283 -0
  8. data/lib/aker/authorities/support/find_sole_user.rb +24 -0
  9. data/lib/aker/authorities/support.rb +9 -0
  10. data/lib/aker/authorities.rb +46 -0
  11. data/lib/aker/cas/authority.rb +79 -0
  12. data/lib/aker/cas/configuration_helper.rb +85 -0
  13. data/lib/aker/cas/middleware/logout_responder.rb +49 -0
  14. data/lib/aker/cas/middleware/ticket_remover.rb +35 -0
  15. data/lib/aker/cas/middleware.rb +6 -0
  16. data/lib/aker/cas/proxy_mode.rb +108 -0
  17. data/lib/aker/cas/rack_proxy_callback.rb +188 -0
  18. data/lib/aker/cas/service_mode.rb +88 -0
  19. data/lib/aker/cas/service_url.rb +62 -0
  20. data/lib/aker/cas/user_ext.rb +64 -0
  21. data/lib/aker/cas.rb +31 -0
  22. data/lib/aker/central_parameters.rb +101 -0
  23. data/lib/aker/configuration.rb +534 -0
  24. data/lib/aker/deprecation.rb +105 -0
  25. data/lib/aker/form/custom_views_mode.rb +80 -0
  26. data/lib/aker/form/login_form_asset_provider.rb +56 -0
  27. data/lib/aker/form/middleware/custom_view_login_responder.rb +19 -0
  28. data/lib/aker/form/middleware/login_renderer.rb +72 -0
  29. data/lib/aker/form/middleware/login_responder.rb +71 -0
  30. data/lib/aker/form/middleware/logout_responder.rb +26 -0
  31. data/lib/aker/form/middleware.rb +10 -0
  32. data/lib/aker/form/mode.rb +118 -0
  33. data/lib/aker/form.rb +26 -0
  34. data/lib/aker/group.rb +67 -0
  35. data/lib/aker/group_membership.rb +162 -0
  36. data/lib/aker/ldap/authority.rb +392 -0
  37. data/lib/aker/ldap/user_ext.rb +19 -0
  38. data/lib/aker/ldap.rb +22 -0
  39. data/lib/aker/modes/base.rb +85 -0
  40. data/lib/aker/modes/http_basic.rb +100 -0
  41. data/lib/aker/modes/support/attempted_path.rb +22 -0
  42. data/lib/aker/modes/support/rfc_2617.rb +32 -0
  43. data/lib/aker/modes/support.rb +12 -0
  44. data/lib/aker/modes.rb +48 -0
  45. data/lib/aker/rack/authenticate.rb +37 -0
  46. data/lib/aker/rack/configuration_helper.rb +18 -0
  47. data/lib/aker/rack/default_logout_responder.rb +36 -0
  48. data/lib/aker/rack/environment_helper.rb +34 -0
  49. data/lib/aker/rack/facade.rb +102 -0
  50. data/lib/aker/rack/failure.rb +69 -0
  51. data/lib/aker/rack/logout.rb +63 -0
  52. data/lib/aker/rack/request_ext.rb +19 -0
  53. data/lib/aker/rack/session_timer.rb +95 -0
  54. data/lib/aker/rack/setup.rb +77 -0
  55. data/lib/aker/rack.rb +107 -0
  56. data/lib/aker/test/helpers.rb +22 -0
  57. data/lib/aker/test.rb +8 -0
  58. data/lib/aker/user.rb +231 -0
  59. data/lib/aker/version.rb +3 -0
  60. data/lib/aker.rb +51 -0
  61. data/spec/aker/aker-sample.yml +11 -0
  62. data/spec/aker/authorities/automatic_access_spec.rb +52 -0
  63. data/spec/aker/authorities/composite_spec.rb +488 -0
  64. data/spec/aker/authorities/nu-schema.jar +0 -0
  65. data/spec/aker/authorities/static_spec.rb +455 -0
  66. data/spec/aker/authorities/support/find_sole_user_spec.rb +33 -0
  67. data/spec/aker/authorities_spec.rb +16 -0
  68. data/spec/aker/cas/authority_spec.rb +106 -0
  69. data/spec/aker/cas/configuration_helper_spec.rb +92 -0
  70. data/spec/aker/cas/middleware/logout_responder_spec.rb +47 -0
  71. data/spec/aker/cas/middleware/ticket_remover_spec.rb +49 -0
  72. data/spec/aker/cas/proxy_mode_spec.rb +185 -0
  73. data/spec/aker/cas/rack_proxy_callback_spec.rb +190 -0
  74. data/spec/aker/cas/service_mode_spec.rb +122 -0
  75. data/spec/aker/cas/service_url_spec.rb +114 -0
  76. data/spec/aker/cas/user_ext_spec.rb +27 -0
  77. data/spec/aker/cas_spec.rb +19 -0
  78. data/spec/aker/central_parameters_spec.rb +44 -0
  79. data/spec/aker/configuration_spec.rb +465 -0
  80. data/spec/aker/deprecation_spec.rb +115 -0
  81. data/spec/aker/form/a_form_mode.rb +129 -0
  82. data/spec/aker/form/custom_views_mode_spec.rb +34 -0
  83. data/spec/aker/form/login_form_asset_provider_spec.rb +80 -0
  84. data/spec/aker/form/middleware/a_form_login_responder.rb +89 -0
  85. data/spec/aker/form/middleware/custom_view_login_responder_spec.rb +47 -0
  86. data/spec/aker/form/middleware/login_renderer_spec.rb +56 -0
  87. data/spec/aker/form/middleware/login_responder_spec.rb +34 -0
  88. data/spec/aker/form/middleware/logout_responder_spec.rb +55 -0
  89. data/spec/aker/form/mode_spec.rb +15 -0
  90. data/spec/aker/form_spec.rb +11 -0
  91. data/spec/aker/group_membership_spec.rb +208 -0
  92. data/spec/aker/group_spec.rb +66 -0
  93. data/spec/aker/ldap/authority_spec.rb +414 -0
  94. data/spec/aker/ldap/ldap-users.ldif +197 -0
  95. data/spec/aker/ldap_spec.rb +11 -0
  96. data/spec/aker/modes/a_aker_mode.rb +41 -0
  97. data/spec/aker/modes/http_basic_spec.rb +127 -0
  98. data/spec/aker/modes/support/attempted_path_spec.rb +32 -0
  99. data/spec/aker/modes_spec.rb +11 -0
  100. data/spec/aker/rack/authenticate_spec.rb +78 -0
  101. data/spec/aker/rack/default_logout_responder_spec.rb +67 -0
  102. data/spec/aker/rack/facade_spec.rb +154 -0
  103. data/spec/aker/rack/failure_spec.rb +151 -0
  104. data/spec/aker/rack/logout_spec.rb +63 -0
  105. data/spec/aker/rack/request_ext_spec.rb +29 -0
  106. data/spec/aker/rack/session_timer_spec.rb +134 -0
  107. data/spec/aker/rack/setup_spec.rb +87 -0
  108. data/spec/aker/rack_spec.rb +216 -0
  109. data/spec/aker/test/helpers_spec.rb +44 -0
  110. data/spec/aker/user_spec.rb +362 -0
  111. data/spec/aker_spec.rb +80 -0
  112. data/spec/deprecation_helper.rb +58 -0
  113. data/spec/java_helper.rb +5 -0
  114. data/spec/logger_helper.rb +17 -0
  115. data/spec/matchers.rb +31 -0
  116. data/spec/mock_builder.rb +25 -0
  117. data/spec/spec_helper.rb +52 -0
  118. metadata +265 -0
@@ -0,0 +1,79 @@
1
+ require 'aker/authorities'
2
+
3
+ require 'castanet'
4
+
5
+ module Aker::Cas
6
+ ##
7
+ # An authority which verifies CAS tickets with an actual CAS server.
8
+ #
9
+ # @see Aker::Cas::UserExt
10
+ class Authority
11
+ include ConfigurationHelper
12
+ include Castanet::Client
13
+
14
+ attr_reader :configuration
15
+
16
+ ##
17
+ # Creates a new instance of this authority. It reads parameters
18
+ # from the `:cas` parameters section of the given configuration.
19
+ # See {Aker::Cas::ConfigurationHelper} for information about the
20
+ # meanings of these parameters.
21
+ def initialize(configuration)
22
+ @configuration = configuration
23
+
24
+ unless cas_url
25
+ raise ":base_url parameter is required for CAS"
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Verifies the given credentials with the CAS server. The `:cas`
31
+ # and `:cas_proxy` kinds are supported. Both kinds require two
32
+ # credentials in the following order:
33
+ #
34
+ # * The ticket (either a service ticket or proxy ticket)
35
+ # * The service URL associated with the ticket
36
+ #
37
+ # The returned user will be extended with {Aker::Cas::CasUser}.
38
+ #
39
+ # If CAS proxying is enabled, then this method also retrieves the
40
+ # proxy-granting ticket for the user.
41
+ #
42
+ # @see http://www.jasig.org/cas/protocol
43
+ # CAS 2 protocol specification, section 2.5.4
44
+ # @return [Aker::User,:unsupported,nil] a user if the credentials
45
+ # are valid, `:unsupported` if the kind is anything but `:cas`
46
+ # or `:cas_proxy`, and nil otherwise
47
+ def valid_credentials?(kind, *credentials)
48
+ return :unsupported unless [:cas, :cas_proxy].include?(kind)
49
+
50
+ ticket = ticket_for(kind, *credentials)
51
+ ticket.present!
52
+
53
+ return nil unless ticket.ok?
54
+
55
+ Aker::User.new(ticket.username).tap do |u|
56
+ u.extend Aker::Cas::UserExt
57
+
58
+ u.cas_url = cas_url
59
+ u.proxy_callback_url = proxy_callback_url
60
+ u.proxy_retrieval_url = proxy_retrieval_url
61
+
62
+ if ticket.pgt_iou
63
+ ticket.retrieve_pgt!
64
+
65
+ u.pgt = ticket.pgt
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def ticket_for(kind, ticket, service)
73
+ case kind
74
+ when :cas; service_ticket(ticket, service)
75
+ when :cas_proxy; proxy_ticket(ticket, service)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,85 @@
1
+ require 'aker/cas'
2
+ require 'uri'
3
+
4
+ module Aker::Cas
5
+ ##
6
+ # A helper for uniform creation of derived attributes for the CAS
7
+ # configuration. It expects to be mixed in to a context that
8
+ # provides a `configuration` method which returns a
9
+ # {Aker::Configuration}.
10
+ #
11
+ # @see Aker::Configuration
12
+ module ConfigurationHelper
13
+ ##
14
+ # The login URL on the CAS server. This may be set explicitly
15
+ # in the configuration as `parameters_for(:cas)[:login_url]`. If
16
+ # not set explicitly, it will be derived from the base URL.
17
+ #
18
+ # @return [String]
19
+ def cas_login_url
20
+ configuration.parameters_for(:cas)[:login_url] || URI.join(cas_url, 'login').to_s
21
+ end
22
+
23
+ ##
24
+ # The logout URL on the CAS server. This may be set explicitly
25
+ # in the configuration as `parameters_for(:cas)[:logout_url]`. If
26
+ # not set explicitly, it will be derived from the base URL.
27
+ #
28
+ # @return [String]
29
+ def cas_logout_url
30
+ configuration.parameters_for(:cas)[:logout_url] || URI.join(cas_url, 'logout').to_s
31
+ end
32
+
33
+ ##
34
+ # The base URL for all not-otherwise-explicitly-specified URLs on
35
+ # the CAS server. It may be set in the CAS parameters as either
36
+ # `:base_url` (preferred) or `:cas_base_url` (for backwards
37
+ # compatibility with aker 1.x).
38
+ #
39
+ # The base URL should end in a `/` (forward slash). If it does not, a
40
+ # trailing forward slash will be appended.
41
+ #
42
+ # @see http://www.ietf.org/rfc/rfc1808.txt
43
+ # RFC 1808, sections 4 and 5
44
+ # @return [String, nil]
45
+ def cas_url
46
+ appending_forward_slash do
47
+ configuration.parameters_for(:cas)[:base_url] ||
48
+ configuration.parameters_for(:cas)[:cas_base_url]
49
+ end
50
+ end
51
+
52
+ ##
53
+ # The URL that CAS will provide the PGT and PGTIOU to, per section
54
+ # 2.5.4 of the spec. Some CAS servers require that this be an
55
+ # SSL-protected resource. It is set in the CAS parameters as
56
+ # `:proxy_callback_url`.
57
+ #
58
+ # @return [String, nil]
59
+ def proxy_callback_url
60
+ configuration.parameters_for(:cas)[:proxy_callback_url]
61
+ end
62
+
63
+ ##
64
+ # The URL that the CAS client can retrieve the PGT from once it
65
+ # has been deposited at the {#proxy_callback_url} by the CAS
66
+ # server. It is set in the CAS parameters as
67
+ # `:proxy_retrieval_url`.
68
+ #
69
+ # (Note that this is not part of the CAS protocol — it is
70
+ # client-specific.)
71
+ #
72
+ # @return [String, nil]
73
+ def proxy_retrieval_url
74
+ configuration.parameters_for(:cas)[:proxy_retrieval_url]
75
+ end
76
+
77
+ private
78
+
79
+ def appending_forward_slash
80
+ url = yield
81
+
82
+ (url && url[-1].chr != '/') ? url + '/' : url
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,49 @@
1
+ require 'aker'
2
+
3
+ module Aker::Cas::Middleware
4
+ class LogoutResponder
5
+ include Aker::Rack::ConfigurationHelper
6
+
7
+ ##
8
+ # @param app a Rack app
9
+ # @param [String] cas_logout_url the CAS logout URL
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ ##
15
+ # Rack entry point.
16
+ #
17
+ # Given a `GET` to the configured logout path, redirects to
18
+ # {#cas_logout_url}. All other requests are passed through.
19
+ #
20
+ # @see http://www.jasig.org/cas/protocol
21
+ # Section 2.3 of the CAS 2 protocol
22
+ def call(env)
23
+ if env['REQUEST_METHOD'] == 'GET' && env['PATH_INFO'] == logout_path(env)
24
+ ::Rack::Response.new { |r| r.redirect(cas_logout_url(env)) }.finish
25
+ else
26
+ @app.call(env)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def cas_logout_url(env)
33
+ configuration(env).parameters_for(:cas)[:logout_url] || URI.join(cas_url(env), 'logout').to_s
34
+ end
35
+
36
+ def cas_url(env)
37
+ appending_forward_slash do
38
+ configuration(env).parameters_for(:cas)[:base_url] ||
39
+ configuration(env).parameters_for(:cas)[:cas_base_url]
40
+ end
41
+ end
42
+
43
+ def appending_forward_slash
44
+ url = yield
45
+
46
+ (url && url[-1].chr != '/') ? url + '/' : url
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ require 'aker'
2
+
3
+ module Aker::Cas::Middleware
4
+ ##
5
+ # Middleware which issues a redirect immediately after CAS
6
+ # authentication succeeds so that users never see a URL with the
7
+ # ticket in it. This prevents them from, e.g., bookmarking a URL
8
+ # with a ticket in it, keeping things cleaner and preventing
9
+ # requests to the CAS server for tickets which are definitely
10
+ # expired.
11
+ class TicketRemover
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ if authenticated?(env) && ticket_present?(env)
18
+ url = Aker::Cas::ServiceUrl.service_url(Rack::Request.new(env))
19
+ [301, { 'Location' => url }, ["Removing authenticated CAS ticket"] ]
20
+ else
21
+ @app.call(env)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def authenticated?(env)
28
+ env['aker.check'] && env['aker.check'].user
29
+ end
30
+
31
+ def ticket_present?(env)
32
+ env['QUERY_STRING'] =~ /ticket=/
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ require 'aker'
2
+
3
+ module Aker::Cas::Middleware
4
+ autoload :LogoutResponder, 'aker/cas/middleware/logout_responder'
5
+ autoload :TicketRemover, 'aker/cas/middleware/ticket_remover'
6
+ end
@@ -0,0 +1,108 @@
1
+ require 'aker'
2
+
3
+ module Aker
4
+ module Cas
5
+ ##
6
+ # A non-interactive mode that provides CAS proxy authentication conformant to
7
+ # CAS 2.
8
+ #
9
+ # This mode does _not_ handle interactive CAS authentication; see {Cas} for
10
+ # that.
11
+ #
12
+ # @see http://www.jasig.org/cas/protocol
13
+ # CAS 2 protocol specification
14
+ #
15
+ # @author David Yip
16
+ class ProxyMode < Aker::Modes::Base
17
+ include Aker::Modes::Support::Rfc2617
18
+
19
+ ##
20
+ # A key that refers to this mode; used for configuration convenience.
21
+ #
22
+ # @return [Symbol]
23
+ def self.key
24
+ :cas_proxy
25
+ end
26
+
27
+ ##
28
+ # The type of credentials supplied by this mode.
29
+ #
30
+ # @return [Symbol]
31
+ def kind
32
+ self.class.key
33
+ end
34
+
35
+ ##
36
+ # The supplied proxy ticket and the {#service_url service URL}.
37
+ #
38
+ # The proxy ticket is received in the HTTP `Authorization`
39
+ # header, per RFC2616. The scheme must be `CasProxy`. Example:
40
+ #
41
+ # > `Authorization: CasProxy PT-1272928074r13CBB9ACA794867F3E`
42
+ #
43
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol, section 3.7
44
+ # @return [Array<String>] the proxy ticket or an empty array
45
+ def credentials
46
+ key = 'HTTP_AUTHORIZATION'
47
+ matches = env[key].match(/CasProxy\s+([SP]T-[0-9A-Za-z\-]+)/) if env.has_key?(key)
48
+
49
+ if matches && matches[1]
50
+ [matches[1], service_url]
51
+ else
52
+ []
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Returns true if a proxy ticket is present, false otherwise.
58
+ def valid?
59
+ !credentials.empty?
60
+ end
61
+
62
+ ##
63
+ # Used to build a WWW-Authenticate header that will be returned to a
64
+ # client failing non-interactive authentication.
65
+ #
66
+ # @return [String]
67
+ def scheme
68
+ "CasProxy"
69
+ end
70
+
71
+ ##
72
+ # Builds the service URL for this application.
73
+ #
74
+ # Colloquially, the service URL is the web server URL plus the
75
+ # application mount point. It does not include anything
76
+ # about the specific resource being requested. For instance, if
77
+ # you had the resource
78
+ #
79
+ # > https://notis.nubic.northwestern.edu/lsdb/patients/105661
80
+ #
81
+ # which was part of the `/lsdb` application, the service URL
82
+ # would be
83
+ #
84
+ # > https://notis.nubic.northwestern.edu/lsdb
85
+ #
86
+ # A little more formally, the URL is `url scheme +
87
+ # hostname + script name`. The port is also included if it is
88
+ # not the default for the URL scheme.
89
+ #
90
+ # The service URL never ends with a `/`, even if the application
91
+ # is mounted at the root.
92
+ #
93
+ # @return [String] the service URL derived from the request
94
+ # environment
95
+ def service_url
96
+ url = "#{env['rack.url_scheme']}://"
97
+ if env['HTTP_HOST']
98
+ url << env['HTTP_HOST'] # includes the port
99
+ else
100
+ url << env['SERVER_NAME']
101
+ default_port = { "http" => "80", "https" => "443" }[env['rack.url_scheme']]
102
+ url << ":#{env["SERVER_PORT"]}" unless env["SERVER_PORT"].to_s == default_port
103
+ end
104
+ url << env["SCRIPT_NAME"]
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,188 @@
1
+ require 'aker/cas'
2
+ require 'pstore'
3
+ require 'tmpdir'
4
+
5
+ module Aker::Cas
6
+ ##
7
+ # Rack code for handling the PGT callback part of the CAS proxy
8
+ # authentication protocol. The class itself is middleware; it can
9
+ # also generate an {.application endpoint}.
10
+ #
11
+ # ## Behavior
12
+ #
13
+ # As middleware, this class intercepts and handles two paths and
14
+ # passes all other requests down the chain. The paths are:
15
+ #
16
+ # * `/receive_pgt`: implements the PGT callback process per section
17
+ # 2.5.4 of the CAS protocol.
18
+ # * `/retrieve_pgt`: allows an application to retrieve the PGT for
19
+ # a PGTIOU. The PGTIOU is returned to the application as part of
20
+ # the CAS ticket validation process. It should be passed to
21
+ # `/receive_pgt` as the `pgtIou` query parameter. Note that a
22
+ # given PGT may only be retrieved once.
23
+ #
24
+ # As a full rack app, it handles the same two paths and returns `404
25
+ # Not Found` for all other requests.
26
+ #
27
+ # ## Middleware vs. Application
28
+ #
29
+ # It is **only** appropriate to use the class as middleware in a
30
+ # **multithreaded or multiprocessing deployment**. If your application
31
+ # only has one executor at a time, using this class as middleware
32
+ # **will cause a deadlock** during CAS authentication.
33
+ #
34
+ # ## Based on
35
+ #
36
+ # This class was heavily influenced by `CasProxyCallbackController`
37
+ # in rubycas-client. That class has approximately the same
38
+ # behavior, but is Rails-specific.
39
+ #
40
+ # @see http://www.jasig.org/cas/protocol
41
+ # CAS protocol, section 2.5.4
42
+ class RackProxyCallback
43
+ RETRIEVE_PATH = "/retrieve_pgt"
44
+ RECEIVE_PATH = "/receive_pgt"
45
+
46
+ ##
47
+ # Create a new instance of the middleware.
48
+ #
49
+ # @param [#call] app the next rack application in the chain.
50
+ # @param [Hash] options
51
+ # @option options [String] :store the file where the middleware
52
+ # will store the received PGTs until they are retrieved.
53
+ def initialize(app, options={})
54
+ @app = app
55
+ @store_filename = options.delete(:store) or
56
+ raise "Please specify a filename for the PGT store"
57
+ end
58
+
59
+ ##
60
+ # Handles a single request in the manner specified in the class
61
+ # overview.
62
+ #
63
+ # @param [Hash] env the rack environment for the request.
64
+ #
65
+ # @return [Array] an appropriate rack response.
66
+ def call(env)
67
+ return receive(env) if env["PATH_INFO"] == RECEIVE_PATH
68
+ return retrieve(env) if env["PATH_INFO"] == RETRIEVE_PATH
69
+ @app.call(env)
70
+ end
71
+
72
+ ##
73
+ # Creates a rack application which responds as described in the
74
+ # class overview.
75
+ #
76
+ # @param [Hash] options the same options that you can pass to
77
+ # {#initialize}.
78
+ #
79
+ # @return [#call] a full rack application
80
+ def self.application(options={})
81
+ app = lambda { |env|
82
+ [404, { "Content-Type" => "text/plain" }, ["Unknown resource #{env['PATH_INFO']}"]]
83
+ }
84
+ RackProxyCallback.new(app, options)
85
+ end
86
+
87
+ protected
88
+
89
+ ##
90
+ # Associates the given PGTIOU and PGT.
91
+ #
92
+ # @param [String] pgt_iou
93
+ # @param [String] pgt
94
+ #
95
+ # @return [void]
96
+ def store_iou(pgt_iou, pgt)
97
+ pstore = open_pstore
98
+
99
+ pstore.transaction do
100
+ pstore[pgt_iou] = pgt
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Finds the PGT for the given PGTIOU. If there isn't one, it
106
+ # returns nil. If there is one, it deletes it from the store
107
+ # before returning it.
108
+ #
109
+ # @param [String] pgt_iou
110
+ # @return [String,nil]
111
+ def resolve_iou(pgt_iou)
112
+ pstore = open_pstore
113
+
114
+ pgt = nil
115
+ pstore.transaction do
116
+ pgt = pstore[pgt_iou]
117
+ pstore.delete(pgt_iou) if pgt
118
+ end
119
+
120
+ pgt
121
+ end
122
+
123
+ private
124
+
125
+ def receive(env)
126
+ req = Rack::Request.new(env)
127
+ resp = Rack::Response.new
128
+ resp.headers["Content-Type"] = "text/plain"
129
+
130
+ pgt = req.params["pgtId"]
131
+ pgt_iou = req.params["pgtIou"]
132
+
133
+ unless pgt && pgt_iou
134
+ missing = [("pgtId" unless pgt), ("pgtIou" unless pgt_iou)].compact
135
+ missing_msg =
136
+ if missing.size == 1
137
+ "#{missing.first} is a required query parameter."
138
+ else
139
+ "Both #{missing.join(' and ')} are required query parameters."
140
+ end
141
+ resp.status =
142
+ if missing.size == 2
143
+ #
144
+ # This oddity is required by the JA-SIG CAS Server.
145
+ #
146
+ 200
147
+ else
148
+ 400
149
+ end
150
+
151
+ resp.body = ["#{missing_msg}\nSee section 2.5.4 of the CAS protocol specification."]
152
+ else
153
+ store_iou(pgt_iou, pgt)
154
+
155
+ resp.body = ["PGT and PGTIOU received. Thanks, my robotic friend."]
156
+ end
157
+
158
+ resp.finish
159
+ end
160
+
161
+ def retrieve(env)
162
+ req = Rack::Request.new(env)
163
+ resp = Rack::Response.new
164
+ resp.headers["Content-Type"] = "text/plain"
165
+
166
+ pgt_iou = req.params["pgtIou"]
167
+
168
+ if pgt_iou
169
+ pgt = resolve_iou(pgt_iou)
170
+ if pgt
171
+ resp.body = [pgt]
172
+ else
173
+ resp.status = 404
174
+ resp.body = ["pgtIou=#{pgt_iou} does not exist. Perhaps it has already been retrieved."]
175
+ end
176
+ else
177
+ resp.status = 400
178
+ resp.body = ["pgtIou is a required query parameter."]
179
+ end
180
+
181
+ resp.finish
182
+ end
183
+
184
+ def open_pstore
185
+ PStore.new(@store_filename)
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,88 @@
1
+ require 'aker'
2
+ require 'rack'
3
+
4
+ module Aker
5
+ module Cas
6
+ ##
7
+ # An interactive mode that provides CAS authentication conformant to CAS 2.
8
+ #
9
+ # This mode does _not_ handle non-interactive CAS proxying. See
10
+ # {ProxyMode} for that.
11
+ #
12
+ # @see http://www.jasig.org/cas/protocol
13
+ # CAS 2 protocol specification
14
+ #
15
+ # @author David Yip
16
+ class ServiceMode < Aker::Modes::Base
17
+ include ConfigurationHelper
18
+ include ::Rack::Utils
19
+ include Aker::Modes::Support::AttemptedPath
20
+ include ServiceUrl
21
+
22
+ ##
23
+ # A key that refers to this mode; used for configuration convenience.
24
+ #
25
+ # @return [Symbol]
26
+ def self.key
27
+ :cas
28
+ end
29
+
30
+ ##
31
+ # Appends the {Middleware::LogoutResponder logout responder} and
32
+ # the {Middleware::TicketRemover ticket remover} to the Rack
33
+ # middleware stack.
34
+ def self.append_middleware(builder)
35
+ builder.use(Middleware::LogoutResponder)
36
+ builder.use(Middleware::TicketRemover)
37
+ end
38
+
39
+ ##
40
+ # The type of credentials supplied by this mode.
41
+ #
42
+ # @return [Symbol]
43
+ def kind
44
+ self.class.key
45
+ end
46
+
47
+ ##
48
+ # Extracts the service ticket from the request parameters.
49
+ #
50
+ # The service ticket is assumed to be a parameter named ST in either GET
51
+ # or POST data.
52
+ #
53
+ # @return [Array<String>,nil] a two-item array containing the
54
+ # service ticket and the service URL to which the ticket
55
+ # (it is asserted) applies
56
+ def credentials
57
+ if request['ticket']
58
+ [request['ticket'], service_url]
59
+ end
60
+ end
61
+
62
+ ##
63
+ # Returns true if a service ticket is present in the query string, false
64
+ # otherwise.
65
+ def valid?
66
+ credentials
67
+ end
68
+
69
+ ##
70
+ # Builds a Rack response that redirects to a CAS server's login page.
71
+ #
72
+ # The constructed response uses the URL of the resource for which
73
+ # authentication failed as the CAS service URL.
74
+ #
75
+ # @see http://www.jasig.org/cas/protocol
76
+ # Section 2.2.1 of the CAS 2 protocol
77
+ #
78
+ # @return [Rack::Response]
79
+ def on_ui_failure
80
+ ::Rack::Response.new do |resp|
81
+ login_uri = URI.parse(cas_login_url)
82
+ login_uri.query = "service=#{escape(service_url)}"
83
+ resp.redirect(login_uri.to_s)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,62 @@
1
+ require 'aker/cas'
2
+
3
+ module Aker::Cas
4
+ ##
5
+ # Provides logic for reconstructing the full requested URL from a
6
+ # rack request.
7
+ #
8
+ # If used as a mixin, the host class must have a `#request`
9
+ # accessor. It may optionally also have a `#attempted_path`
10
+ # accessor.
11
+ #
12
+ # @see ServiceMode
13
+ # @see Aker::Modes::Support::AttemptedPath
14
+ module ServiceUrl
15
+ ##
16
+ # The service URL supplied to the CAS login page. This is the
17
+ # requested URL, sans any service ticket.
18
+ #
19
+ # @return [String]
20
+ def service_url
21
+ ServiceUrl.service_url(
22
+ request,
23
+ (attempted_path if self.respond_to?(:attempted_path))
24
+ )
25
+ end
26
+
27
+ ##
28
+ # Builds the service URL that should be used for the given
29
+ # request. This is the requested URL (or the attempted_path, if
30
+ # given), sans any service ticket.
31
+ #
32
+ # @param [Rack::Request] request
33
+ # @param [String] attempted_path
34
+ # @return [String]
35
+ def self.service_url(request, attempted_path=nil)
36
+ requested = URI.parse(
37
+ if attempted_path
38
+ url = "#{request.scheme}://#{request.host}"
39
+
40
+ unless [ ["https", 443], ["http", 80] ].include?([request.scheme, request.port])
41
+ url << ":#{request.port}"
42
+ end
43
+
44
+ url << attempted_path
45
+ else
46
+ request.url
47
+ end
48
+ )
49
+ if requested.query
50
+ requested.query.gsub!(/(&?)ticket=ST-[^&]+(&?)/) do
51
+ if [$1, $2].uniq == ['&'] # in the middle
52
+ '&'
53
+ else
54
+ nil
55
+ end
56
+ end
57
+ requested.query = nil if requested.query.empty?
58
+ end
59
+ requested.to_s
60
+ end
61
+ end
62
+ end