castanet 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|