castanet 0.0.1
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.
- data/History.md +4 -0
- data/LICENSE +20 -0
- data/README.md +63 -0
- data/lib/castanet.rb +8 -0
- data/lib/castanet/client.rb +207 -0
- data/lib/castanet/proxy_ticket.rb +133 -0
- data/lib/castanet/proxy_ticket_error.rb +6 -0
- data/lib/castanet/query_building.rb +60 -0
- data/lib/castanet/responses.rb +24 -0
- data/lib/castanet/responses/common.rl +44 -0
- data/lib/castanet/responses/proxy.rb +526 -0
- data/lib/castanet/responses/proxy.rl +120 -0
- data/lib/castanet/responses/ticket_validate.rb +720 -0
- data/lib/castanet/responses/ticket_validate.rl +172 -0
- data/lib/castanet/service_ticket.rb +180 -0
- data/lib/castanet/version.rb +3 -0
- metadata +184 -0
data/History.md
ADDED
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
|