Skip to content

Commit 828be53

Browse files
authored
Merge pull request #759 from johnnyshields/signed-document-info-refactor
[WIP] v2.x - Nokogiri Refactor Part 6 - Conversion of SignedDocument and final removal of REXML 🎉
2 parents 09b4067 + ba37e1f commit 828be53

25 files changed

+890
-953
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -929,22 +929,24 @@ or underscore, and can only contain letters, digits, underscores, hyphens, and p
929929

930930
Some IdPs may require to add SPs to add additional fields (Organization, ContactPerson, etc.)
931931
into the SP metadata. This can be done by extending the `RubySaml::Metadata` class and
932-
overriding the `#add_extras` method using a Nokogiri XML builder as per the following example:
932+
overriding the `#add_extras` method where the first arg is a
933+
[Nokogiri::XML::Builder](https://nokogiri.org/rdoc/Nokogiri/XML/Builder.html) object as per
934+
the following example:
933935

934936
```ruby
935937
class MyMetadata < RubySaml::Metadata
936938
private
937939

938940
def add_extras(xml, _settings)
939-
xml['md'].Organization do
940-
xml['md'].OrganizationName('ACME Inc.', 'xml:lang' => 'en-US')
941-
xml['md'].OrganizationDisplayName('ACME', 'xml:lang' => 'en-US')
942-
xml['md'].OrganizationURL('https://www.acme.com', 'xml:lang' => 'en-US')
941+
xml.Organization do
942+
xml.OrganizationName('xml:lang' => 'en-US') { xml.text 'ACME Inc.' }
943+
xml.OrganizationDisplayName('xml:lang' => 'en-US') { xml.text 'ACME' }
944+
xml.OrganizationURL('xml:lang' => 'en-US') { xml.text 'https://www.acme.com' }
943945
end
944946

945-
xml['md'].ContactPerson('contactType' => 'technical') do
946-
xml['md'].GivenName('ACME SAML Team')
947-
xml['md'].EmailAddress('saml@acme.com')
947+
xml.ContactPerson('contactType' => 'technical') do
948+
xml.GivenName { xml.text 'ACME SAML Team' }
949+
xml.EmailAddress { xml.text 'saml@acme.com' }
948950
end
949951
end
950952
end

UPGRADING.md

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Ruby SAML Migration Guide
22

3-
## Updating from 1.x to 2.0.0
3+
## Upgrading from 1.x to 2.0.0
44

55
**IMPORTANT: Please read this section carefully as it contains breaking changes!**
66

@@ -34,7 +34,7 @@ Note that the project folder structure has also been updated accordingly. Notabl
3434
`lib/onelogin/schemas` is now `lib/ruby_saml/schemas`.
3535

3636
For backward compatibility, the alias `OneLogin = Object` has been set, so `OneLogin::RubySaml::` will still work
37-
as before. This alias will be removed in RubySaml version `2.1.0`.
37+
as before. This alias will be removed in RubySaml version `3.0.0`.
3838

3939
### Deprecation and removal of "XMLSecurity" namespace
4040

@@ -74,24 +74,33 @@ settings.security[:signature_method] = RubySaml::XML::RSA_SHA1
7474
### Replacement of REXML with Nokogiri
7575

7676
RubySaml `1.x` used a combination of REXML and Nokogiri for XML parsing and generation.
77-
In `2.0.0`, REXML has been replaced with Nokogiri. This change should be transparent
78-
to most users, however, see note about Custom Metadata Fields below.
77+
In `2.0.0`, REXML has been replaced with Nokogiri. As a result, there are minor differences
78+
in how XML is generated, ncluding SAML requests and SP Metadata:
79+
80+
1. All XML namespace declarations will be on the root node of the XML. Previously,
81+
some declarations such as `xmlns:ds` were done on child nodes.
82+
2. The ordering of attributes on each node may be different.
83+
84+
These differences should not affect how the XML is parsed by various XML parsing libraries.
85+
However, if you are strictly asserting that the generated XML is an exact string in your tests,
86+
such tests may need to be adjusted accordingly.
7987

8088
### Custom Metadata Fields now use Nokogiri XML Builder
8189

8290
If you have added custom fields to your SP metadata generation by overriding
83-
the `RubySaml::Metadata#add_extras` method, you will need to update your code to use
84-
[Nokogiri::XML::Builder](https://nokogiri.org/rdoc/Nokogiri/XML/Builder.html) format
85-
instead of REXML. Here is an example of the new format:
91+
the `RubySaml::Metadata#add_extras` method, you will need to update your code
92+
so that the first arg of the method is a
93+
[Nokogiri::XML::Builder](https://nokogiri.org/rdoc/Nokogiri/XML/Builder.html)
94+
object. Here is an example of the new format:
8695

8796
```ruby
8897
class MyMetadata < RubySaml::Metadata
8998
private
9099

91-
def add_extras(xml, _settings)
92-
xml['md'].ContactPerson('contactType' => 'technical') do
93-
xml['md'].GivenName('ACME SAML Team')
94-
xml['md'].EmailAddress('saml@acme.com')
100+
def add_extras(builder, _settings)
101+
builder.ContactPerson('contactType' => 'technical') do
102+
builder.GivenName { builder.text 'ACME SAML Team' }
103+
builder.EmailAddress { builder.text 'saml@acme.com' }
95104
end
96105
end
97106
end
@@ -101,7 +110,7 @@ end
101110

102111
RubySaml now always uses double quotes for attribute values when generating XML.
103112
The `settings.double_quote_xml_attribute_values` parameter now always behaves as `true`,
104-
and will be removed in RubySaml 2.1.0.
113+
and will be removed in RubySaml 3.0.0.
105114

106115
The reasons for this change are:
107116
- RubySaml will use Nokogiri instead of REXML to generate XML. Nokogiri does not support
@@ -154,7 +163,7 @@ a different `sp_uuid_prefix` is passed-in on subsequent calls.
154163
### Deprecation of compression settings
155164

156165
The `settings.compress_request` and `settings.compress_response` parameters have been deprecated
157-
and are no longer functional. They will be removed in RubySaml 2.1.0. Please remove `compress_request`
166+
and are no longer functional. They will be removed in RubySaml 3.0.0. Please remove `compress_request`
158167
and `compress_response` everywhere within your project code.
159168

160169
The SAML SP request/response message compression behavior is now controlled automatically by the
@@ -166,13 +175,15 @@ compression may be achieved by enabling `Content-Encoding: gzip` on your webserv
166175
### Deprecation of IdP certificate fingerprint settings
167176

168177
The `settings.idp_cert_fingerprint` and `settings.idp_cert_fingerprint_algorithm` are deprecated
169-
and will be removed in RubySaml 2.1.0. Please use `settings.idp_cert` or `settings.idp_cert_multi` instead.
170-
The reasons for this deprecation are that (1) fingerprint cannot be used with HTTP-Redirect binding,
171-
and (2) fingerprint is theoretically susceptible to collision attacks.
178+
and will be removed in RubySaml 3.0.0. Please use `settings.idp_cert` or `settings.idp_cert_multi` instead.
179+
180+
The reasons for this deprecation are:
181+
- Fingerprint cannot be used with HTTP-Redirect binding
182+
- Fingerprint is theoretically susceptible to collision attacks.
172183

173184
### Other settings deprecations
174185

175-
The following parameters in `RubySaml::Settings` are deprecated and will be removed in RubySaml 2.1.0:
186+
The following parameters in `RubySaml::Settings` are deprecated and will be removed in RubySaml 3.0.0:
176187

177188
- `#issuer` is deprecated and replaced 1:1 by `#sp_entity_id`
178189
- `#idp_sso_target_url` is deprecated and replaced 1:1 by `#idp_sso_service_url`
@@ -212,7 +223,7 @@ and `#format_private_key` methods. Specifically:
212223
stripped out.
213224
- Case 7: If no valid certificates are found, the entire original string will be returned.
214225

215-
## Updating from 1.17.x to 1.18.0
226+
## Upgrading from 1.17.x to 1.18.0
216227

217228
Version `1.18.0` changes the way the toolkit validates SAML signatures. There is a new order
218229
how validation happens in the toolkit and also the toolkit by default will check malformed doc
@@ -222,7 +233,7 @@ The SignedDocument class defined at xml_security.rb experienced several changes.
222233
We don't expect compatibilty issues if you use the main methods offered by ruby-saml, but if
223234
you use a fork or customized usage, is possible that you need to adapt your code.
224235

225-
## Updating from 1.12.x to 1.13.0
236+
## Upgrading from 1.12.x to 1.13.0
226237

227238
Version `1.13.0` adds `settings.idp_sso_service_binding` and `settings.idp_slo_service_binding`, and
228239
deprecates `settings.security[:embed_sign]`. If specified, new binding parameters will be used in place of `:embed_sign`

lib/ruby_saml.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@
2020
require 'ruby_saml/utils'
2121
require 'ruby_saml/version'
2222

23-
# @deprecated This alias adds compatibility with v1.x and will be removed in v2.1.0
23+
# @deprecated This alias adds compatibility with v1.x and will be removed in v3.0.0
2424
OneLogin = Object

lib/ruby_saml/idp_metadata_parser.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def parse_to_array(idp_metadata, options = {})
161161
end
162162

163163
def parse_to_idp_metadata_array(idp_metadata, options = {})
164-
@document = Nokogiri::XML(idp_metadata)
164+
@document = Nokogiri::XML(idp_metadata) # TODO: RubySaml::XML.safe_load_nokogiri
165165
@options = options
166166

167167
idpsso_descriptors = self.class.get_idps(@document, options[:entity_id])
@@ -348,14 +348,14 @@ def certificates
348348
unless signing_nodes.empty?
349349
certs['signing'] = []
350350
signing_nodes.each do |cert_node|
351-
certs['signing'] << cert_node.content
351+
certs['signing'] << cert_node.text
352352
end
353353
end
354354

355355
unless encryption_nodes.empty?
356356
certs['encryption'] = []
357357
encryption_nodes.each do |cert_node|
358-
certs['encryption'] << cert_node.content
358+
certs['encryption'] << cert_node.text
359359
end
360360
end
361361
certs

lib/ruby_saml/logoutresponse.rb

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def initialize(response, settings = nil, options = {})
4141

4242
@options = options
4343
@response = RubySaml::XML::Decoder.decode_message(response, @settings&.message_max_bytesize)
44-
@document = RubySaml::XML::SignedDocument.new(@response)
44+
@document = RubySaml::XML.safe_load_nokogiri(@response)
4545
super()
4646
end
4747

@@ -60,47 +60,35 @@ def success?
6060
# @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
6161
#
6262
def in_response_to
63-
@in_response_to ||= begin
64-
node = REXML::XPath.first(
65-
document,
66-
"/p:LogoutResponse",
67-
{ "p" => RubySaml::XML::NS_PROTOCOL }
68-
)
69-
node.nil? ? nil : node.attributes['InResponseTo']
70-
end
63+
@in_response_to ||= document.at_xpath(
64+
"/p:LogoutResponse",
65+
{ "p" => RubySaml::XML::NS_PROTOCOL }
66+
)&.[]('InResponseTo')
7167
end
7268

7369
# @return [String] Gets the Issuer from the Logout Response.
7470
#
7571
def issuer
76-
@issuer ||= begin
77-
node = REXML::XPath.first(
78-
document,
79-
"/p:LogoutResponse/a:Issuer",
80-
{ "p" => RubySaml::XML::NS_PROTOCOL, "a" => RubySaml::XML::NS_ASSERTION }
81-
)
82-
Utils.element_text(node)
83-
end
72+
@issuer ||= document.at_xpath(
73+
"/p:LogoutResponse/a:Issuer",
74+
{ "p" => RubySaml::XML::NS_PROTOCOL, "a" => RubySaml::XML::NS_ASSERTION }
75+
)&.text
8476
end
8577

8678
# @return [String] Gets the StatusCode from a Logout Response.
8779
#
8880
def status_code
89-
@status_code ||= begin
90-
node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => RubySaml::XML::NS_PROTOCOL })
91-
node.nil? ? nil : node.attributes["Value"]
92-
end
81+
@status_code ||= document.at_xpath(
82+
"/p:LogoutResponse/p:Status/p:StatusCode",
83+
{ "p" => RubySaml::XML::NS_PROTOCOL }
84+
)&.[]('Value')
9385
end
9486

9587
def status_message
96-
@status_message ||= begin
97-
node = REXML::XPath.first(
98-
document,
99-
"/p:LogoutResponse/p:Status/p:StatusMessage",
100-
{ "p" => RubySaml::XML::NS_PROTOCOL }
101-
)
102-
Utils.element_text(node)
103-
end
88+
@status_message ||= document.at_xpath(
89+
"/p:LogoutResponse/p:Status/p:StatusMessage",
90+
{ "p" => RubySaml::XML::NS_PROTOCOL }
91+
)&.text
10492
end
10593

10694
# Aux function to validate the Logout Response

lib/ruby_saml/memoizable.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
module RubySaml
4+
# Mixin for memoizing methods
5+
module Memoizable
6+
# Creates a memoized method
7+
#
8+
# @param method_name [Symbol] the name of the method to memoize
9+
# @param original_method [Symbol, nil] the original method to memoize (defaults to method_name)
10+
def self.included(base)
11+
base.extend(ClassMethods)
12+
end
13+
14+
private
15+
16+
# Memoizes the result of a block using the given name as the cache key
17+
#
18+
# @param cache_key [Symbol, String] the name to use as the cache key
19+
# @yield the block whose result will be cached
20+
# @return [Object] the cached result or the result of the block
21+
def memoize(cache_key)
22+
cache_key = "@#{cache_key.to_s.delete_prefix('@')}"
23+
return instance_variable_get(cache_key) if instance_variable_defined?(cache_key)
24+
25+
instance_variable_set(cache_key, yield)
26+
end
27+
28+
# Class methods for memoization
29+
module ClassMethods
30+
# Defines multiple memoized methods
31+
#
32+
# @param method_names [Array<Symbol>] the names of the methods to memoize
33+
# @raise [ArgumentError] if any method has an arity greater than 0
34+
def memoize_method(*method_names)
35+
method_names.each do |method_name|
36+
method_obj = instance_method(method_name)
37+
38+
# Check method arity
39+
if method_obj.arity > 0 # rubocop:disable Style/IfUnlessModifier
40+
raise ArgumentError.new("Cannot memoize method '#{method_name}' with arity > 0")
41+
end
42+
43+
# Store the original method
44+
original_method_name = "#{method_name}_without_memoization"
45+
alias_method original_method_name, method_name
46+
private original_method_name
47+
48+
# Define the memoized version
49+
define_method(method_name) do |&block|
50+
cache_key = "@memoized_#{method_name}"
51+
memoize(cache_key) do
52+
send(original_method_name, &block)
53+
end
54+
end
55+
56+
# Preserve method visibility
57+
if private_method_defined?(original_method_name)
58+
private method_name
59+
elsif protected_method_defined?(original_method_name)
60+
protected method_name
61+
end
62+
end
63+
end
64+
end
65+
end
66+
end

0 commit comments

Comments
 (0)