saml2 1.0.4 → 1.0.5
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.
- checksums.yaml +4 -4
- data/lib/saml2/attribute.rb +34 -14
- data/lib/saml2/attribute_consuming_service.rb +4 -2
- data/lib/saml2/base.rb +18 -0
- data/lib/saml2/conditions.rb +4 -2
- data/lib/saml2/namespaces.rb +9 -7
- data/lib/saml2/version.rb +1 -1
- data/spec/fixtures/response_with_attribute_signed.xml +8 -8
- data/spec/lib/attribute_spec.rb +67 -9
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b77541aa53787757d03865855dbbb84da27edaba
|
4
|
+
data.tar.gz: f53032507b4c09f95e1e0f2def917e8760a5f41a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2658dc70caec44d8a6b2d8b3e014f419a2887a82a43dc6e2280c1f43437d5d162bc86b9197f144d386709317f4f39b1d692d8bb03d0bd340943fd460ec8d430f
|
7
|
+
data.tar.gz: 5fcd948501794284e79ba05f8296c4d5e9492f4624210b80fafde2edd760f47de26476bed1f67b8f6e8607c682e5f6e732b40a796ba03a47503c324c8d037cfc
|
data/lib/saml2/attribute.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'date'
|
2
2
|
|
3
|
+
require 'active_support/core_ext/array/wrap'
|
4
|
+
|
3
5
|
require 'saml2/base'
|
4
6
|
require 'saml2/namespaces'
|
5
7
|
|
@@ -53,7 +55,7 @@ module SAML2
|
|
53
55
|
builder['saml'].Attribute('Name' => name) do |builder|
|
54
56
|
builder.parent['FriendlyName'] = friendly_name if friendly_name
|
55
57
|
builder.parent['NameFormat'] = name_format if name_format
|
56
|
-
Array(value).each do |
|
58
|
+
Array.wrap(value).each do |value|
|
57
59
|
xsi_type, val = convert_to_xsi(value)
|
58
60
|
builder['saml'].AttributeValue(val) do |builder|
|
59
61
|
builder.parent['xsi:type'] = xsi_type if xsi_type
|
@@ -67,7 +69,7 @@ module SAML2
|
|
67
69
|
@friendly_name = node['FriendlyName']
|
68
70
|
@name_format = node['NameFormat']
|
69
71
|
values = node.xpath('saml:AttributeValue', Namespaces::ALL).map do |node|
|
70
|
-
convert_from_xsi(node
|
72
|
+
convert_from_xsi(node.attribute_with_ns('type', Namespaces::XSI), node.content && node.content.strip)
|
71
73
|
end
|
72
74
|
@value = case values.length
|
73
75
|
when 0; nil
|
@@ -78,27 +80,36 @@ module SAML2
|
|
78
80
|
end
|
79
81
|
|
80
82
|
private
|
81
|
-
|
82
|
-
|
83
|
-
|
83
|
+
XS_TYPES = {
|
84
|
+
lookup_qname('xs:boolean', Namespaces::ALL) =>
|
85
|
+
[[TrueClass, FalseClass], nil, ->(v) { %w{true 1}.include?(v) ? true : false }],
|
86
|
+
lookup_qname('xs:string', Namespaces::ALL) =>
|
87
|
+
[String, nil, nil],
|
88
|
+
lookup_qname('xs:date', Namespaces::ALL) =>
|
89
|
+
[Date, nil, ->(v) { Date.parse(v) if v }],
|
90
|
+
lookup_qname('xs:dateTime', Namespaces::ALL) =>
|
91
|
+
[Time, ->(v) { v.iso8601 }, ->(v) { Time.parse(v) if v }]
|
84
92
|
}.freeze
|
85
93
|
|
86
94
|
def convert_to_xsi(value)
|
87
|
-
|
95
|
+
xs_type = nil
|
88
96
|
converter = nil
|
89
|
-
|
90
|
-
if klass === value
|
91
|
-
|
97
|
+
XS_TYPES.each do |type, (klasses, to_xsi, _from_xsi)|
|
98
|
+
if Array.wrap(klasses).any? { |klass| klass === value }
|
99
|
+
xs_type = "xs:#{type.last}"
|
92
100
|
converter = to_xsi
|
93
101
|
break
|
94
102
|
end
|
95
103
|
end
|
96
104
|
value = converter.call(value) if converter
|
97
|
-
[
|
105
|
+
[xs_type, value]
|
98
106
|
end
|
99
107
|
|
100
108
|
def convert_from_xsi(type, value)
|
101
|
-
|
109
|
+
return value unless type
|
110
|
+
qname = self.class.lookup_qname(type.value, type.namespaces)
|
111
|
+
|
112
|
+
info = XS_TYPES[qname]
|
102
113
|
if info && info.last
|
103
114
|
value = info.last.call(value)
|
104
115
|
end
|
@@ -106,15 +117,24 @@ module SAML2
|
|
106
117
|
end
|
107
118
|
end
|
108
119
|
|
109
|
-
class AttributeStatement
|
120
|
+
class AttributeStatement < Base
|
110
121
|
attr_reader :attributes
|
111
122
|
|
112
|
-
def initialize(attributes)
|
123
|
+
def initialize(attributes = [])
|
113
124
|
@attributes = attributes
|
114
125
|
end
|
115
126
|
|
127
|
+
def from_xml(node)
|
128
|
+
@attributes = node.xpath('saml:Attribute', Namespaces::ALL).map do |attr|
|
129
|
+
Attribute.from_xml(attr)
|
130
|
+
end
|
131
|
+
|
132
|
+
super
|
133
|
+
end
|
134
|
+
|
116
135
|
def build(builder)
|
117
|
-
builder['saml'].AttributeStatement('xmlns:
|
136
|
+
builder['saml'].AttributeStatement('xmlns:xs' => Namespaces::XS,
|
137
|
+
'xmlns:xsi' => Namespaces::XSI) do |builder|
|
118
138
|
@attributes.each { |attr| attr.build(builder) }
|
119
139
|
end
|
120
140
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
1
3
|
require 'saml2/attribute'
|
2
4
|
require 'saml2/indexed_object'
|
3
5
|
require 'saml2/namespaces'
|
@@ -34,7 +36,7 @@ module SAML2
|
|
34
36
|
def initialize(requested_attribute, provided_value)
|
35
37
|
super("Attribute #{requested_attribute.name} is provided value " \
|
36
38
|
"#{provided_value.inspect}, but only allows " \
|
37
|
-
"#{Array(requested_attribute.value).inspect}")
|
39
|
+
"#{Array.wrap(requested_attribute.value).inspect}")
|
38
40
|
@requested_attribute, @provided_value = requested_attribute, provided_value
|
39
41
|
end
|
40
42
|
end
|
@@ -76,7 +78,7 @@ module SAML2
|
|
76
78
|
end
|
77
79
|
if attr
|
78
80
|
if requested_attr.value &&
|
79
|
-
!Array(requested_attr.value).include?(attr.value)
|
81
|
+
!Array.wrap(requested_attr.value).include?(attr.value)
|
80
82
|
raise InvalidAttributeValue.new(requested_attr, attr.value)
|
81
83
|
end
|
82
84
|
attributes << attr
|
data/lib/saml2/base.rb
CHANGED
@@ -41,6 +41,11 @@ module SAML2
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
def self.lookup_qname(qname, namespaces)
|
45
|
+
prefix, local_name = split_qname(qname)
|
46
|
+
[lookup_namespace(prefix, namespaces), local_name]
|
47
|
+
end
|
48
|
+
|
44
49
|
protected
|
45
50
|
def load_string_array(node, element)
|
46
51
|
self.class.load_string_array(node, element)
|
@@ -49,5 +54,18 @@ module SAML2
|
|
49
54
|
def load_object_array(node, element, klass)
|
50
55
|
self.class.load_object_array(node, element, klass)
|
51
56
|
end
|
57
|
+
|
58
|
+
def self.split_qname(qname)
|
59
|
+
if qname.include?(':')
|
60
|
+
qname.split(':', 2)
|
61
|
+
else
|
62
|
+
[nil, qname]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.lookup_namespace(prefix, namespaces)
|
67
|
+
return nil if namespaces.empty?
|
68
|
+
namespaces[prefix.empty? ? 'xmlns' : "xmlns:#{prefix}"]
|
69
|
+
end
|
52
70
|
end
|
53
71
|
end
|
data/lib/saml2/conditions.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
1
3
|
module SAML2
|
2
4
|
class Conditions < Array
|
3
5
|
attr_accessor :not_before, :not_on_or_after
|
@@ -49,12 +51,12 @@ module SAML2
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def valid?(options)
|
52
|
-
Array(audience).include?(options[:audience]) ? :valid : :invalid
|
54
|
+
Array.wrap(audience).include?(options[:audience]) ? :valid : :invalid
|
53
55
|
end
|
54
56
|
|
55
57
|
def build(builder)
|
56
58
|
builder['saml'].AudienceRestriction do |builder|
|
57
|
-
Array(audience).each do |single_audience|
|
59
|
+
Array.wrap(audience).each do |single_audience|
|
58
60
|
builder['saml'].Audience(single_audience)
|
59
61
|
end
|
60
62
|
end
|
data/lib/saml2/namespaces.rb
CHANGED
@@ -5,17 +5,19 @@ module SAML2
|
|
5
5
|
SAML = "urn:oasis:names:tc:SAML:2.0:assertion".freeze
|
6
6
|
SAMLP = "urn:oasis:names:tc:SAML:2.0:protocol".freeze
|
7
7
|
XENC = "http://www.w3.org/2001/04/xmlenc#".freeze
|
8
|
+
XS = "http://www.w3.org/2001/XMLSchema".freeze
|
8
9
|
XSI = "http://www.w3.org/2001/XMLSchema-instance".freeze
|
9
10
|
X500 = "urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500".freeze
|
10
11
|
|
11
12
|
ALL = {
|
12
|
-
'dsig' => DSIG,
|
13
|
-
'md' => METADATA,
|
14
|
-
'saml' => SAML,
|
15
|
-
'samlp' => SAMLP,
|
16
|
-
'x500' => X500,
|
17
|
-
'xenc' => XENC,
|
18
|
-
'
|
13
|
+
'xmlns:dsig' => DSIG,
|
14
|
+
'xmlns:md' => METADATA,
|
15
|
+
'xmlns:saml' => SAML,
|
16
|
+
'xmlns:samlp' => SAMLP,
|
17
|
+
'xmlns:x500' => X500,
|
18
|
+
'xmlns:xenc' => XENC,
|
19
|
+
'xmlns:xs' => XS,
|
20
|
+
'xmlns:xsi' => XSI,
|
19
21
|
}.freeze
|
20
22
|
end
|
21
23
|
end
|
data/lib/saml2/version.rb
CHANGED
@@ -9,15 +9,15 @@
|
|
9
9
|
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
10
10
|
</Transforms>
|
11
11
|
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
12
|
-
<DigestValue>
|
12
|
+
<DigestValue>PTIbPIph/60z5i+AdxU32f7w2jiAD904pC3QNTs263E=</DigestValue>
|
13
13
|
</Reference>
|
14
14
|
</SignedInfo>
|
15
|
-
<SignatureValue>
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
<SignatureValue>BmxWVd1P+2L/qJgV5RYoktBkkXHamXHa4JAX7O2M6GSlCN9qwWaPe5fLqCcpNaMs
|
16
|
+
HLQF/cNEsY0ntgKlxI231Q6p/+ZYUwzMXXBTixYHgMzwvtqMwRLIP60c/oI3pQTY
|
17
|
+
420Psr3WmZMnpmouQB9fcSwO33BbppbMv2mwHaVp00BvQcnwS4SU1wr1XCSKr4kc
|
18
|
+
TocP7R7zK+2FlVJB5wPzgUo9kIbjRTupVj6NOKjjp42Gv8GRg+fY02JZ98zaOnJk
|
19
|
+
PCNICiQpxsN5XWYxfdtsHRpES/Y36lPYkIPDmJhuTxmKoTl0apq9Pg4m9oM6RRQW
|
20
|
+
zI1YBtwCWrEjk8eW/Wrz3w==</SignatureValue>
|
21
21
|
<KeyInfo>
|
22
22
|
<X509Data>
|
23
23
|
<X509Certificate>MIID+jCCAuKgAwIBAgIJAIz/He5UafnhMA0GCSqGSIb3DQEBBQUAMFsxCzAJBgNV
|
@@ -44,4 +44,4 @@ Cg8Yo62X9vWW6PaKXHs3N+g1Ig16NwjdVIYvcxLc2KY0vrqu/R5c8RbmCxMZyss9
|
|
44
44
|
ZtltN+yN40INHGRWnHc=</X509Certificate>
|
45
45
|
</X509Data>
|
46
46
|
</KeyInfo>
|
47
|
-
</Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">jacob</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2015-02-12T22:54:29Z" Recipient="https://siteadmin.test.instructure.com/saml_consume" InResponseTo="_bec424fa5103428909a30ff1e31168327f79474984"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2015-02-12T22:51:24Z" NotOnOrAfter="2015-02-12T22:51:59Z"><saml:AudienceRestriction><saml:Audience>http://siteadmin.instructure.com/saml2</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2015-02-12T22:51:29Z"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><saml:Attribute xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500" Name="urn:oid:2.5.4.42" FriendlyName="givenName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" x500:Encoding="LDAP"><saml:AttributeValue xsi:type="
|
47
|
+
</Signature><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">jacob</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2015-02-12T22:54:29Z" Recipient="https://siteadmin.test.instructure.com/saml_consume" InResponseTo="_bec424fa5103428909a30ff1e31168327f79474984"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2015-02-12T22:51:24Z" NotOnOrAfter="2015-02-12T22:51:59Z"><saml:AudienceRestriction><saml:Audience>http://siteadmin.instructure.com/saml2</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2015-02-12T22:51:29Z"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><saml:Attribute xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500" Name="urn:oid:2.5.4.42" FriendlyName="givenName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" x500:Encoding="LDAP"><saml:AttributeValue xsi:type="xs:string">cody</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>
|
data/spec/lib/attribute_spec.rb
CHANGED
@@ -2,9 +2,19 @@ require_relative '../spec_helper'
|
|
2
2
|
|
3
3
|
module SAML2
|
4
4
|
describe Attribute do
|
5
|
+
def serialize(attribute)
|
6
|
+
doc = Nokogiri::XML::Builder.new do |builder|
|
7
|
+
builder['saml'].Root('xmlns:saml' => Namespaces::SAML) do |builder|
|
8
|
+
attribute.build(builder)
|
9
|
+
builder.parent.child['xmlns:saml'] = Namespaces::SAML
|
10
|
+
end
|
11
|
+
end.doc
|
12
|
+
doc.root.child.to_s
|
13
|
+
end
|
14
|
+
|
5
15
|
let(:eduPersonPrincipalNameXML) { <<XML.strip
|
6
16
|
<saml:Attribute xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" FriendlyName="eduPersonPrincipalName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" x500:Encoding="LDAP" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
7
|
-
<saml:AttributeValue xsi:type="
|
17
|
+
<saml:AttributeValue xsi:type="xs:string">user@domain</saml:AttributeValue>
|
8
18
|
</saml:Attribute>
|
9
19
|
XML
|
10
20
|
}
|
@@ -26,14 +36,62 @@ XML
|
|
26
36
|
attr.friendly_name.must_equal 'eduPersonPrincipalName'
|
27
37
|
attr.name_format.must_equal Attribute::NameFormats::URI
|
28
38
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
39
|
+
serialize(attr).must_equal eduPersonPrincipalNameXML
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should parse and serialize boolean values" do
|
43
|
+
xml = <<XML.strip
|
44
|
+
<saml:AttributeStatement xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
45
|
+
<saml:Attribute Name="attr">
|
46
|
+
<saml:AttributeValue xsi:type="xs:boolean">1</saml:AttributeValue>
|
47
|
+
</saml:Attribute>
|
48
|
+
</saml:AttributeStatement>
|
49
|
+
XML
|
50
|
+
|
51
|
+
stmt = AttributeStatement.from_xml(Nokogiri::XML(xml).root)
|
52
|
+
stmt.attributes.first.value.must_equal true
|
53
|
+
|
54
|
+
# serializes canonically
|
55
|
+
serialize(stmt).must_equal(xml.sub('>1<', '>true<'))
|
37
56
|
end
|
57
|
+
|
58
|
+
it "should parse and serialize dateTime values" do
|
59
|
+
xml = <<XML.strip
|
60
|
+
<saml:AttributeStatement xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
61
|
+
<saml:Attribute Name="attr">
|
62
|
+
<saml:AttributeValue xsi:type="xs:dateTime">2015-06-29T18:37:03Z</saml:AttributeValue>
|
63
|
+
</saml:Attribute>
|
64
|
+
</saml:AttributeStatement>
|
65
|
+
XML
|
66
|
+
|
67
|
+
stmt = AttributeStatement.from_xml(Nokogiri::XML(xml).root)
|
68
|
+
stmt.attributes.first.value.must_equal Time.at(1435603023)
|
69
|
+
|
70
|
+
# serializes canonically
|
71
|
+
serialize(stmt).must_equal(xml)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should parse values with different namespace prefixes" do
|
75
|
+
xml = <<XML.strip
|
76
|
+
<saml:Attribute Name="attr" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xssi="http://www.w3.org/2001/XMLSchema-instance">
|
77
|
+
<saml:AttributeValue xssi:type="xsd:boolean">0</saml:AttributeValue>
|
78
|
+
</saml:Attribute>
|
79
|
+
XML
|
80
|
+
|
81
|
+
attr = Attribute.from_xml(Nokogiri::XML(xml).root)
|
82
|
+
attr.value.must_equal false
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should parse untagged values" do
|
86
|
+
xml = <<XML.strip
|
87
|
+
<saml:Attribute Name="attr" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
|
88
|
+
<saml:AttributeValue>something</saml:AttributeValue>
|
89
|
+
</saml:Attribute>
|
90
|
+
XML
|
91
|
+
|
92
|
+
attr = Attribute.from_xml(Nokogiri::XML(xml).root)
|
93
|
+
attr.value.must_equal "something"
|
94
|
+
end
|
95
|
+
|
38
96
|
end
|
39
97
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saml2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Cutrer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -50,6 +50,26 @@ dependencies:
|
|
50
50
|
- - ">="
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: 0.9.2
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: activesupport
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '3.2'
|
60
|
+
- - "<"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '5.0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.2'
|
70
|
+
- - "<"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '5.0'
|
53
73
|
- !ruby/object:Gem::Dependency
|
54
74
|
name: rake
|
55
75
|
requirement: !ruby/object:Gem::Requirement
|