Skip to content

Commit 5168a34

Browse files
committed
Response#attributes now returns Attributes collection instead of Hash.
This collection can be queried for both the default first value or all values of the attribute.
1 parent ec7d7ba commit 5168a34

6 files changed

Lines changed: 141 additions & 46 deletions

File tree

lib/onelogin/ruby-saml/attribute_value.rb

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
module OneLogin
2+
module RubySaml
3+
# Wraps all attributes and provides means to query them for single or multiple values.
4+
#
5+
# For backwards compatibility Attributes#[] returns *first* value for the attribute.
6+
# Turn off compatibility to make it return all values as an array:
7+
# Attributes.single_value_compatibility = false
8+
class Attributes
9+
include Enumerable
10+
11+
# By default Attributes#[] is backwards compatible and
12+
# returns only the first value for the attribute
13+
# Setting this to `false` returns all values for an attribute
14+
@@single_value_compatibility = true
15+
16+
# Get current status of backwards compatibility mode.
17+
def self.single_value_compatibility
18+
@@single_value_compatibility
19+
end
20+
21+
# Sets the backwards compatibility mode on/off.
22+
def self.single_value_compatibility=(value)
23+
@@single_value_compatibility = value
24+
end
25+
26+
# Initialize Attributes collection, optionally taking a Hash of attribute names and values.
27+
#
28+
# The +attrs+ must be a Hash with attribute names as keys and **arrays** as values:
29+
# Attributes.new({
30+
# 'name' => ['value1', 'value2'],
31+
# 'mail' => ['value1'],
32+
# })
33+
def initialize(attrs = {})
34+
@attributes = attrs
35+
end
36+
37+
38+
# Iterate over all attributes
39+
def each
40+
attributes.each{|name, values| yield name, values}
41+
end
42+
43+
# Test attribute presence by name
44+
def include?(name)
45+
attributes.has_key?(canonize_name(name))
46+
end
47+
48+
# Return first value for an attribute
49+
def single(name)
50+
attributes[canonize_name(name)].first
51+
end
52+
53+
# Return all values for an attribute
54+
def multi(name)
55+
attributes[canonize_name(name)]
56+
end
57+
58+
# By default returns first value for an attribute.
59+
#
60+
# Depending on the single value compatibility status this returns first value
61+
# Attributes.single_value_compatibility = true # Default
62+
# response.attributes['mail'] # => 'user@example.com'
63+
#
64+
# Or all values:
65+
# Attributes.single_value_compatibility = false
66+
# response.attributes['mail'] # => ['user@example.com','user@example.net']
67+
def [](name)
68+
self.class.single_value_compatibility ? single(canonize_name(name)) : multi(canonize_name(name))
69+
end
70+
71+
# Return all attributes as an array
72+
def all
73+
attributes
74+
end
75+
76+
# Set values for an attribute, overwriting all existing values
77+
def set(name, values)
78+
attributes[canonize_name(name)] = values
79+
end
80+
alias_method :[]=, :set
81+
82+
# Add new attribute or new value(s) to an existing attribute
83+
def add(name, values = [])
84+
attributes[canonize_name(name)] ||= []
85+
attributes[canonize_name(name)] += Array(values)
86+
end
87+
88+
# Make comparable to another Attributes collection based on attributes
89+
def ==(other)
90+
if other.is_a?(Attributes)
91+
all == other.all
92+
else
93+
super
94+
end
95+
end
96+
97+
protected
98+
99+
# stringifies all names so both 'email' and :email return the same result
100+
def canonize_name(name)
101+
name.to_s
102+
end
103+
104+
def attributes
105+
@attributes
106+
end
107+
end
108+
end
109+
end

lib/onelogin/ruby-saml/response.rb

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,23 @@ def sessionindex
4848
end
4949
end
5050

51-
# A hash of all the attributes with the response.
52-
# Multiple values will be returned in the AttributeValue#values array
53-
# in reverse order, when compared to XML
51+
# Returns OneLogin::RubySaml::Attributes enumerable collection.
52+
# All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+
53+
#
54+
# For backwards compatibility ruby-saml returns by default only the first value for a given attribute with
55+
# attributes['name']
56+
# To get all of the attributes, use:
57+
# attributes.multi('name')
58+
# Or turn off the compatibility:
59+
# OneLogin::RubySaml::Attributes.single_value_compatibility = false
60+
# Now this will return an array:
61+
# attributes['name']
5462
def attributes
5563
@attr_statements ||= begin
56-
result = {}
64+
attributes = Attributes.new
5765

5866
stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
59-
return {} if stmt_element.nil?
67+
return attributes if stmt_element.nil?
6068

6169
stmt_element.elements.each do |attr_element|
6270
name = attr_element.attributes["Name"]
@@ -66,24 +74,10 @@ def attributes
6674
["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
6775
}
6876

69-
# Monkey-patch first value to contain all values (so that the type is retained)
70-
attr_value = values.first
71-
attr_value.extend AttributeValue
72-
attr_value.values = values.reverse # retain XML order
73-
74-
# Merge values if the Attribute has already been seen
75-
if result[name]
76-
attr_value.values += result[name].values
77-
end
78-
79-
result[name] = attr_value
80-
end
81-
82-
result.keys.each do |key|
83-
result[key.intern] = result[key]
77+
attributes.add(name, values)
8478
end
8579

86-
result
80+
attributes
8781
end
8882
end
8983

lib/ruby-saml.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
require 'onelogin/ruby-saml/authrequest'
33
require 'onelogin/ruby-saml/logoutrequest'
44
require 'onelogin/ruby-saml/logoutresponse'
5-
require 'onelogin/ruby-saml/attribute_value'
5+
require 'onelogin/ruby-saml/attributes'
66
require 'onelogin/ruby-saml/response'
77
require 'onelogin/ruby-saml/settings'
88
require 'onelogin/ruby-saml/validation_error'

test/response_test.rb

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ class RubySamlTest < Test::Unit::TestCase
221221

222222
should "not raise on responses without attributes" do
223223
response = OneLogin::RubySaml::Response.new(response_document_4)
224-
assert_equal Hash.new, response.attributes
224+
assert_equal OneLogin::RubySaml::Attributes.new, response.attributes
225225
end
226226

227227
context "#multiple values" do
@@ -235,19 +235,19 @@ class RubySamlTest < Test::Unit::TestCase
235235
assert_equal 'value1', response.attributes[:another_value]
236236
end
237237

238-
should "return array with all attributes when asked" do
238+
should "return array with all attributes when asked in XML order" do
239239
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
240-
assert_equal ['value2', 'value1'], response.attributes[:another_value].values
240+
assert_equal ['value1', 'value2'], response.attributes.multi(:another_value)
241241
end
242242

243-
should "return last of multiple values when multiple Attribute tags in XML" do
243+
should "return first of multiple values when multiple Attribute tags in XML" do
244244
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
245-
assert_equal 'role2', response.attributes[:role]
245+
assert_equal 'role1', response.attributes[:role]
246246
end
247247

248248
should "return all of multiple values in reverse order when multiple Attribute tags in XML" do
249249
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
250-
assert_equal ['role2', 'role1'], response.attributes[:role].values
250+
assert_equal ['role1', 'role2', 'role3'], response.attributes.multi(:role)
251251
end
252252

253253
should "return nil value correctly" do
@@ -257,7 +257,15 @@ class RubySamlTest < Test::Unit::TestCase
257257

258258
should "return multiple values including nil and empty string" do
259259
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
260-
assert_equal [nil, nil, "valuePresent", ""], response.attributes[:attribute_with_nils_and_empty_strings].values
260+
assert_equal ["", "valuePresent", nil, nil], response.attributes.multi(:attribute_with_nils_and_empty_strings)
261+
end
262+
263+
should "return multiple values from [] when not in compatibility mode" do
264+
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
265+
OneLogin::RubySaml::Attributes.single_value_compatibility = false
266+
assert_equal ["", "valuePresent", nil, nil], response.attributes[:attribute_with_nils_and_empty_strings]
267+
# classes are not reloaded between tests so restore default
268+
OneLogin::RubySaml::Attributes.single_value_compatibility = true
261269
end
262270
end
263271
end

test/responses/response_with_multiple_attribute_values.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
</saml:Attribute>
5252
<saml:Attribute Name="role">
5353
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role2</saml:AttributeValue>
54+
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role3</saml:AttributeValue>
5455
</saml:Attribute>
5556
<saml:Attribute Name="attribute_with_nil_value">
5657
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>

0 commit comments

Comments
 (0)