castanet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.md ADDED
@@ -0,0 +1,4 @@
1
+ 0.0.1 (unreleased)
2
+ ==================
3
+
4
+ - Initial release.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 David Yip
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ Castanet: a small, snappy CAS client library
2
+ ============================================
3
+
4
+ Castanet is a [Central Authentication Service](http://www.jasig.org/cas) (CAS)
5
+ client library. It implements version 2.0 of the CAS protocol.
6
+
7
+ Castanet was built at the [Northwestern University Biomedical Informatics
8
+ Center](http://www.nucats.northwestern.edu/clinical-research-resources/data-collection-biomedical-informatics-and-nubic/bioinformatics-overview.html)
9
+ as a replacement for [RubyCAS-Client](https://github.com/gunark/rubycas-client)
10
+ in internal software.
11
+
12
+ Castanet is tested on Ruby 1.8.7, Ruby 1.9.2, JRuby 1.5.6 in Ruby 1.8 mode, and Rubinius 1.2.0.
13
+ Continuous integration reports are available at [NUBIC's CI
14
+ server](https://ctms-ci.nubic.northwestern.edu/hudson/job/castanet/).
15
+
16
+ Getting started
17
+ ===============
18
+
19
+ Mix `Castanet::Client` into the objects that need CAS client behavior.
20
+
21
+ Objects that include `Castanet::Client` must implement `cas_url`,
22
+ `proxy_callback_url`, and `proxy_retrieval_url`.
23
+
24
+ See the documentation for `Castanet::Client` for more information and usage
25
+ examples.
26
+
27
+ Acknowledgments
28
+ ===============
29
+
30
+ Castanet's test harness was based off of code originally written by [Rhett
31
+ Sutphin](mailto:rhett@detailedbalance.net).
32
+
33
+ Query string building code was taken from [Rack](http://rack.rubyforge.org/).
34
+
35
+ Development
36
+ ===========
37
+
38
+ Castanet uses [Bundler](http://gembundler.com/) version `~> 1.0` for dependency
39
+ management.
40
+
41
+ Some of Castanet's development dependencies work best in certain versions of
42
+ Ruby. Additionally, some implementations of Ruby do not support constructs
43
+ (i.e. `fork`) used by Castanet's tests. For this reason, Castanet's Cucumber
44
+ scenarios use [RVM](http://rvm.beginrescueend.com/) to run servers in
45
+ appropriate Ruby implementations.
46
+
47
+ Castanet's CAS response parsers are implemented using
48
+ [Ragel](http://www.complang.org/ragel/).
49
+
50
+ Once you've got Bundler, RVM, and Ragel installed and set up:
51
+
52
+ $ bundle install
53
+ $ rake udaeta:install_dependencies --trace # because it helps to see what's going on
54
+ $ rake ci --trace # ditto
55
+
56
+ Assuming you cloned Castanet at a point where its CI build succeeded, all steps
57
+ should pass. If they don't, feel free to ping me.
58
+
59
+ License
60
+ =======
61
+
62
+ Copyright (c) 2011 David Yip. Released under the X11 (MIT) License; see LICENSE
63
+ for details.
data/lib/castanet.rb ADDED
@@ -0,0 +1,8 @@
1
+ module Castanet
2
+ autoload :Client, 'castanet/client'
3
+ autoload :ProxyTicket, 'castanet/proxy_ticket'
4
+ autoload :ProxyTicketError, 'castanet/proxy_ticket_error'
5
+ autoload :Responses, 'castanet/responses'
6
+ autoload :QueryBuilding, 'castanet/query_building'
7
+ autoload :ServiceTicket, 'castanet/service_ticket'
8
+ end
@@ -0,0 +1,207 @@
1
+ require 'castanet'
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module Castanet
7
+ ##
8
+ # A CAS client.
9
+ #
10
+ # Expected interface
11
+ # ==================
12
+ #
13
+ # Classes that mix in this module must define the method
14
+ #
15
+ # cas_url => String
16
+ #
17
+ # `cas_url` defines the base URL of the CAS server and must have a terminating /.
18
+ #
19
+ # If CAS proxying is desired, classes must further define
20
+ #
21
+ # proxy_callback_url => String
22
+ # proxy_retrieval_url => String
23
+ #
24
+ # `proxy_callback_url` is a URL of a service that will be used by the CAS
25
+ # server for depositing PGTs. (In the CAS protocol, it's the URL passed to
26
+ # `/serviceValidate` in the `pgtIou` parameter.)
27
+ #
28
+ # `proxy_retrieval_url` is a URL of a service that will be used to retrieve
29
+ # deposited PGTs.
30
+ #
31
+ #
32
+ # Security requirements
33
+ # =====================
34
+ #
35
+ # Section 2.5.4 of the CAS 2.0 protocol mandates that the proxy callback
36
+ # service pointed to by `proxy_callback_url` must
37
+ #
38
+ # 1. be accessible over HTTPS and
39
+ # 2. present an SSL certificate that
40
+ # 1. is valid and
41
+ # 2. has a canonical name that matches that of the proxy callback service.
42
+ #
43
+ # Secure channels are not required for any other part of the CAS protocol,
44
+ # but we still recommend using HTTPS for all communication involving any
45
+ # permutation of interactions between the CAS server, the user, and the
46
+ # application.
47
+ #
48
+ # Because of this ambiguity in the CAS protocol -- and because unencrypted
49
+ # transmission can be useful in isolated development environments -- Castanet
50
+ # will permit non-HTTPS communication with CAS servers. However, you must
51
+ # explicitly declare your intent in the class using this client by defining
52
+ # {#https_disabled} equal to `true`:
53
+ #
54
+ # class InsecureClient
55
+ # include Castanet::Client
56
+ #
57
+ # def https_disabled
58
+ # true
59
+ # end
60
+ # end
61
+ #
62
+ # Also keep in mind that future revisions of Castanet may remove this option.
63
+ #
64
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol, section 2.5.4
65
+ # @see http://www.daemonology.net/blog/2009-09-04-complexity-is-insecurity.html
66
+ # "Complexity is insecurity" by Colin Percival
67
+ #
68
+ # Examples
69
+ # ========
70
+ #
71
+ # Presenting a service ticket
72
+ # ---------------------------
73
+ #
74
+ # ticket = service_ticket('ST-1foo', 'https://service.example.edu')
75
+ # ticket.present!
76
+ #
77
+ # ticket.ok? # => true or false
78
+ #
79
+ #
80
+ # Retrieving a proxy-granting ticket
81
+ # ----------------------------------
82
+ #
83
+ # ticket = service_ticket(...)
84
+ # ticket.present!
85
+ # ticket.retrieve_pgt! # PGT can be retrieved from ticket.pgt
86
+ #
87
+ #
88
+ # Requesting a proxy ticket
89
+ # -------------------------
90
+ #
91
+ # ticket = proxy_ticket(pgt, service) # returns a ProxyTicket
92
+ #
93
+ # {ProxyTicket}s can be coerced into Strings.
94
+ #
95
+ #
96
+ # Validating a proxy ticket
97
+ # -------------------------
98
+ #
99
+ # ticket = proxy_ticket(pgt, service)
100
+ # ticket.present!
101
+ #
102
+ # ticket.ok? # => true or false
103
+ #
104
+ #
105
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol
106
+ module Client
107
+ ##
108
+ # Whether or not to disable HTTPS for CAS server communication. Defaults
109
+ # to false.
110
+ #
111
+ # @return [false]
112
+ def https_disabled
113
+ false
114
+ end
115
+
116
+ ##
117
+ # Returns the service ticket validation endpoint for the configured CAS URL.
118
+ #
119
+ # The service ticket validation endpoint is defined as `cas_url` +
120
+ # `"/serviceValidate"`.
121
+ #
122
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol, section 2.5
123
+ # @see #cas_url
124
+ # @return [String]
125
+ def service_validate_url
126
+ URI.join(cas_url, 'serviceValidate').to_s
127
+ end
128
+
129
+ ##
130
+ # Returns the proxy ticket grantor endpoint for the configured CAS URL.
131
+ #
132
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol, section 2.7
133
+ # @see #cas_url
134
+ # @return [String]
135
+ def proxy_url
136
+ URI.join(cas_url, 'proxy').to_s
137
+ end
138
+
139
+ ##
140
+ # Returns the proxy ticket validation endpoint for the configured CAS URL.
141
+ #
142
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol, section 2.6
143
+ # @see #cas_url
144
+ # @return [String]
145
+ def proxy_validate_url
146
+ URI.join(cas_url, 'proxyValidate').to_s
147
+ end
148
+
149
+ ##
150
+ # Prepares a {ServiceTicket} for the ticket `ticket` and the service URL
151
+ # `service`.
152
+ #
153
+ # The prepared {ServiceTicket} can be presented for validation at a later
154
+ # time.
155
+ #
156
+ # @param [String] ticket text of a service ticket
157
+ # @param [String] service a service URL
158
+ # @return [ServiceTicket]
159
+ def service_ticket(ticket, service)
160
+ ServiceTicket.new(ticket, service).tap do |st|
161
+ st.https_disabled = https_disabled
162
+ st.proxy_callback_url = proxy_callback_url
163
+ st.proxy_retrieval_url = proxy_retrieval_url
164
+ st.service_validate_url = service_validate_url
165
+ end
166
+ end
167
+
168
+ ##
169
+ # Given the PGT `pgt`, retrieves a proxy ticket for the service URL
170
+ # `service`.
171
+ #
172
+ # If a proxy ticket cannot be issued for any reason, this method raises a
173
+ # {ProxyTicketError} containing the failure code and reason returned by the
174
+ # CAS server.
175
+ #
176
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol, section 2.7
177
+ # @see {ProxyTicket#reify!}
178
+ # @raise [ProxyTicketError]
179
+ # @return [ProxyTicket] the issued proxy ticket
180
+ def issue_proxy_ticket(pgt, service)
181
+ ProxyTicket.new(nil, pgt, service).tap do |pt|
182
+ pt.https_disabled = https_disabled
183
+ pt.proxy_url = proxy_url
184
+ pt.proxy_validate_url = proxy_validate_url
185
+ end.reify!
186
+ end
187
+
188
+ ##
189
+ # Builds a {ProxyTicket} for the proxy ticket `pt` and service URL `service`.
190
+ #
191
+ # The returned {ProxyTicket} instance can be used to validate `pt` for
192
+ # `service` using `#present!`.
193
+ #
194
+ # @param [String, ProxyTicket] ticket the proxy ticket
195
+ # @param [String] service the service URL
196
+ # @return [ProxyTicket]
197
+ def proxy_ticket(ticket, service)
198
+ ProxyTicket.new(ticket.to_s, nil, service).tap do |pt|
199
+ pt.https_disabled = https_disabled
200
+ pt.proxy_callback_url = proxy_callback_url
201
+ pt.proxy_retrieval_url = proxy_retrieval_url
202
+ pt.proxy_url = proxy_url
203
+ pt.proxy_validate_url = proxy_validate_url
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,133 @@
1
+ require 'castanet'
2
+
3
+ require 'uri'
4
+
5
+ module Castanet
6
+ class ProxyTicket < ServiceTicket
7
+ ##
8
+ # The URL of the CAS server's proxy ticket granting service.
9
+ #
10
+ # @return [String]
11
+ attr_accessor :proxy_url
12
+
13
+ ##
14
+ # The URL of the CAS server's proxy ticket validation service.
15
+ #
16
+ # @return [String]
17
+ attr_accessor :proxy_validate_url
18
+
19
+ ##
20
+ # The `/proxy` response from the CAS server.
21
+ #
22
+ # This is set by {#reify!}, but can be set manually for testing purposes.
23
+ #
24
+ # @return [#ticket]
25
+ attr_accessor :proxy_response
26
+
27
+ def_delegator :proxy_response, :ok?, :issued?
28
+
29
+ def_delegators :proxy_response, :failure_code, :failure_reason
30
+
31
+ ##
32
+ # Initializes an instance of ProxyTicket.
33
+ #
34
+ # Instantiation guide
35
+ # ===================
36
+ #
37
+ # 1. If requesting a proxy ticket, set `pt` to nil, `service` to the
38
+ # service URL, and `pgt` to the proxy granting ticket.
39
+ # 2. If checking a proxy ticket, set `pt` to the proxy ticket, `service` to
40
+ # the service URL, and `pgt` to nil.
41
+ #
42
+ # @param [String, nil] pt the proxy ticket
43
+ # @param [String, nil] pgt the proxy granting ticket
44
+ # @param [String] service the service URL
45
+ def initialize(pt, pgt, service)
46
+ super(pt, service)
47
+
48
+ self.pgt = pgt
49
+ end
50
+
51
+ ##
52
+ # The proxy ticket wrapped by this object. This can come either from a
53
+ # proxy ticket issuance via {#reify!} or be set at instantiation. Tickets
54
+ # issued via {#reify!} have higher precedence.
55
+ #
56
+ # If a proxy ticket was neither supplied at instantiation nor requested via
57
+ # {#reify!}, then ticket will return nil.
58
+ #
59
+ # @return [String, nil] the proxy ticket
60
+ def ticket
61
+ proxy_response ? proxy_response.ticket : super
62
+ end
63
+
64
+ ##
65
+ # Returns the string representation of {#ticket}.
66
+ #
67
+ # If {#ticket} is not nil, then the return value of this method is
68
+ # {#ticket}; otherwise, it is `""`.
69
+ #
70
+ # @return [String] the ticket or empty string
71
+ def to_s
72
+ ticket.to_s
73
+ end
74
+
75
+ ##
76
+ # Requests a proxy ticket from {#proxy_url} and stores it in {#ticket}.
77
+ #
78
+ # If a proxy ticket cannot be issued for any reason, this method raises a
79
+ # {ProxyTicketError} containing the failure code and reason returned by the
80
+ # CAS server.
81
+ #
82
+ # This method should only be run once per `ProxyTicket` instance. It can be
83
+ # run multiple times, but each invocation will overwrite {#ticket} with a
84
+ # new ticket.
85
+ #
86
+ # This method is automatically called by {Client#proxy_ticket}, and as such
87
+ # should never need to be called by users of Castanet; however, in the
88
+ # interest of program organization, the method is public and located here.
89
+ # Also, if you're managing `ProxyTicket` instances manually for some reason,
90
+ # you may find this method useful.
91
+ #
92
+ # @raise [ProxyTicketError] if a proxy ticket cannot be issued
93
+ # @return void
94
+ def reify!
95
+ uri = URI.parse(proxy_url).tap do |u|
96
+ u.query = grant_parameters
97
+ end
98
+
99
+ http = Net::HTTP.new(uri.host, uri.port).tap do |h|
100
+ h.use_ssl = !https_disabled
101
+ end
102
+
103
+ http.start do |h|
104
+ cas_response = h.get(uri.to_s)
105
+
106
+ self.proxy_response = parsed_proxy_response(cas_response.body)
107
+
108
+ unless issued?
109
+ raise ProxyTicketError, "A proxy ticket could not be issued. Code: <#{failure_code}>, reason: <#{failure_reason}>."
110
+ end
111
+
112
+ self
113
+ end
114
+ end
115
+
116
+ protected
117
+
118
+ ##
119
+ # The URL to use for ticket validation.
120
+ #
121
+ # @return [String]
122
+ def validation_url
123
+ proxy_validate_url
124
+ end
125
+
126
+ private
127
+
128
+ def grant_parameters
129
+ query(['pgt', pgt],
130
+ ['targetService', service])
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,6 @@
1
+ require 'castanet'
2
+
3
+ module Castanet
4
+ class ProxyTicketError < StandardError
5
+ end
6
+ end