Skip to content

Commit

Permalink
saml: adds helpers for response assertions, subject, issuer, and attr…
Browse files Browse the repository at this point in the history
…ibutes (#104)

* saml: adds helpers for response assertions, subject, and attributes

* fix up comment

* Restructure test, add coverage, add issuer helpers
  • Loading branch information
austingebauer authored Sep 15, 2023
1 parent 52c8419 commit a92758c
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 136 deletions.
71 changes: 70 additions & 1 deletion saml/models/core/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,73 @@ import (

// Response is a SAML Response element.
// See 3.3.3 http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type Response types.Response
type Response struct {
types.Response
}

// Assertions returns the assertions in the Response.
func (r *Response) Assertions() []Assertion {
assertions := make([]Assertion, 0, len(r.Response.Assertions))
for _, assertion := range r.Response.Assertions {
assertions = append(assertions, Assertion{Assertion: assertion})
}

return assertions
}

// Issuer returns the issuer of the Response if it exists.
// Otherwise, it returns an empty string.
func (r *Response) Issuer() string {
if r.Response.Issuer == nil {
return ""
}

return r.Response.Issuer.Value
}

// Assertion is a SAML Assertion element.
// See 2.3.3 http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type Assertion struct {
types.Assertion
}

// Attribute is a SAML Attribute element.
// See 2.7.3.1 http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
type Attribute struct {
types.Attribute
}

// Issuer returns the issuer of the Assertion if it exists.
// Otherwise, it returns an empty string.
func (a *Assertion) Issuer() string {
if a.Assertion.Issuer == nil {
return ""
}

return a.Assertion.Issuer.Value
}

// SubjectNameID returns the value of the NameID element if it exists in
// the Subject of the Assertion. Otherwise, it returns an empty string.
func (a *Assertion) SubjectNameID() string {
if a.Subject == nil || a.Subject.NameID == nil {
return ""
}

return a.Subject.NameID.Value
}

// Attributes returns the attributes of the Assertion. If there is no
// AttributeStatement or no contained Attributes, an empty list is returned.
func (a *Assertion) Attributes() []Attribute {
if a.AttributeStatement == nil {
return []Attribute{}
}

attributes := make([]Attribute, 0, len(a.AttributeStatement.Attributes))
for _, attribute := range a.AttributeStatement.Attributes {
attributes = append(attributes, Attribute{Attribute: attribute})
}

return attributes
}
251 changes: 117 additions & 134 deletions saml/models/core/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,104 +8,143 @@ import (
"github.com/stretchr/testify/require"
)

var responseXMLSignature = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<ds:Reference URI="#_03a4084d93f8df3cf3caf21878f20c08">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
<ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="xsd" />
</ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<ds:DigestValue>Hs5IUzabpy3X7gqpi0FbyGQoqgVaNwfAQvHymdEHJtE=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>jgRgXKmIhn/OGcScnKC2zkg/kIEnThE8CzxqkG1cM2UHgkjB+zB2CkxJ/TmjYL+qljjJmeijgkabwhiDMwVJ62tEYv2Ck5OliRyF2mvO+lV0XIFjbXIvJm20R3xP3US23Vj6UpFX/kqlgD//K/v8uS4KENVok0UCQgqXT8JtDTCSmg6aV+boE8KrgFsKXX75zH7ZpUDOIDakmNXDXsS/y7xTtu23YNHLCiP99Px22kJ+cDk30I7/w2DN85si6dvmfbV4jSwFQHyf4ZT6RRk0TkOjTCEkN6qDdEOsbUPDYurUXeDUD2WU2YMCE0JDaymPedh1JtNoQS64UQssjTduFA==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEBCwUAMBYxFDAS BgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4MDgyNDIxMTQwOVowFjEUMBIG A1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFK s71ufbQwoQoW7qkNAJRIANGA4iM0ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyj xj0uJ4lArgkr4AOEjj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVN c1klbN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF/cL5fOpd Va54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8nspXiH/MZW8o2cqWRkrw3 MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0GA1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE 4k2ZNTA0BgNVHREELTArggtzYW1sdGVzdC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lk cDANBgkqhkiG9w0BAQsFAAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3 YaMb2RSn7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHTTNiL ArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nblD1JJKSQ3AdhxK/we P3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcUZOpx4swtgGdeoSpeRyrtMvRwdcci NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
</saml2p:Response>`

var responseXMLContainer = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
</saml2p:Response>`

func Test_ParseResponse_ResponseContainer(t *testing.T) {
t.Parallel()
r := require.New(t)
func TestResponse(t *testing.T) {
tests := []struct {
name string
responseXML string
assertions func(*testing.T, core.Response)
}{
{
name: "response container",
responseXML: responseXMLContainer,
assertions: func(t *testing.T, response core.Response) {
require.Equal(t, response.Destination, "http://localhost:8000/saml/acs")
require.Equal(t, response.ID, "saml-response-id")
require.Equal(t, response.IssueInstant.String(), "2023-03-31 06:55:44.494 +0000 UTC")
require.Equal(t, response.Version, "2.0")
},
},
{
name: "assertions helper",
responseXML: responseXMLAssertion,
assertions: func(t *testing.T, response core.Response) {
assertions := response.Assertions()
require.Len(t, assertions, 1)
assertion := assertions[0]

require.Equal(t, "assertion-id", assertion.ID)
require.Equal(t, "2023-03-31 06:55:44.494 +0000 UTC", assertion.IssueInstant.String())
require.Equal(t, "2.0", assertion.Version)
},
},
{
name: "assertion subject helper",
responseXML: responseXMLAssertionSubject,
assertions: func(t *testing.T, response core.Response) {
assertions := response.Assertions()
require.Len(t, assertions, 1)
assertion := assertions[0]

require.Equal(t, "[email protected]", assertion.SubjectNameID())
require.EqualValues(t, core.ConfirmationMethodBearer, assertion.Subject.SubjectConfirmation.Method)
require.Equal(t, "http://localhost:8000/saml/acs", assertion.Subject.SubjectConfirmation.SubjectConfirmationData.Recipient)
require.Equal(t, "request-id", assertion.Subject.SubjectConfirmation.SubjectConfirmationData.InResponseTo)
},
},
{
name: "assertion issuer helper",
responseXML: responseXMLAssertionIssuer,
assertions: func(t *testing.T, response core.Response) {
assertions := response.Assertions()
require.Len(t, assertions, 1)
assertion := assertions[0]

require.Equal(t, "https://samltest.id/saml/idp", assertion.Issuer())
},
},
{
name: "response issuer helper",
responseXML: responseXMLIssuer,
assertions: func(t *testing.T, response core.Response) {
require.Equal(t, "https://samltest.id/saml/idp2", response.Issuer())
},
},
{
name: "response status code",
responseXML: responseXMLStatus,
assertions: func(t *testing.T, response core.Response) {
require.Equal(t, string(core.StatusCodeSuccess), response.Status.StatusCode.Value)
},
},
{
name: "assertion attributes helper",
responseXML: responseXMLAssertionAttributes,
assertions: func(t *testing.T, response core.Response) {
assertions := response.Assertions()
require.Len(t, assertions, 1)
assertion := assertions[0]
attributes := assertion.Attributes()
require.Len(t, attributes, 3)
require.Equal(t, "telephoneNumber", attributes[0].FriendlyName)
require.Equal(t, "+1-555-555-5555", attributes[0].Values[0].Value)
require.Equal(t, "+1-777-777-7777", attributes[0].Values[1].Value)
require.Equal(t, "email", attributes[1].FriendlyName)
require.Equal(t, "[email protected]", attributes[1].Values[0].Value)
require.Equal(t, "givenName", attributes[2].FriendlyName)
require.Equal(t, "Rick", attributes[2].Values[0].Value)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

response := responseXML(t, tt.responseXML)
tt.assertions(t, response)
})
}
}

res := responseXML(t, responseXMLContainer)
func responseXML(t *testing.T, ssoRes string) core.Response {
t.Helper()

r.Equal(res.Destination, "http://localhost:8000/saml/acs")
r.Equal(res.ID, "saml-response-id")
r.Equal(res.IssueInstant.String(), "2023-03-31 06:55:44.494 +0000 UTC")
r.Equal(res.Version, "2.0")
res := core.Response{}
err := xml.Unmarshal([]byte(ssoRes), &res)
require.NoError(t, err)
return res
}

var responseXMLIssuer = `<?xml version="1.0" encoding="UTF-8"?>
const (
responseXMLContainer = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://samltest.id/saml/idp</saml2:Issuer>
</saml2p:Response>`

func Test_ParseResponse_Issuer(t *testing.T) {
t.Parallel()
r := require.New(t)

iss := responseXML(t, responseXMLIssuer).Issuer

r.Equal(iss.Value, "https://samltest.id/saml/idp")
}
responseXMLIssuer = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://samltest.id/saml/idp2</saml2:Issuer>
</saml2p:Response>`

var responseXMLStatus = `<?xml version="1.0" encoding="UTF-8"?>
responseXMLStatus = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</saml2p:Status>
</saml2p:Response>`

var responseXMLAssertion = `<?xml version="1.0" encoding="UTF-8"?>
responseXMLAssertion = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="assertion-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
</saml2:Assertion>
</saml2p:Response>`

func Test_ParseResponse_Assertion(t *testing.T) {
t.Parallel()
r := require.New(t)

assert := responseXML(t, responseXMLAssertion).Assertions[0]

r.Equal("assertion-id", assert.ID)
r.Equal("2023-03-31 06:55:44.494 +0000 UTC", assert.IssueInstant.String())
r.Equal("2.0", assert.Version)

}

var responseXMLAssertionIssuer = `<?xml version="1.0" encoding="UTF-8"?>
responseXMLAssertionIssuer = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="assertion-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Issuer>https://samltest.id/saml/idp</saml2:Issuer>
</saml2:Assertion>
</saml2p:Response>`

func Test_ParseResponse_Assertion_Issuer(t *testing.T) {
t.Parallel()
r := require.New(t)

iss := responseXML(t, responseXMLAssertionIssuer).Assertions[0].Issuer

r.Equal("https://samltest.id/saml/idp", iss.Value)
}

var responseXMLAssertionSubject = `<?xml version="1.0" encoding="UTF-8"?>
responseXMLAssertionSubject = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_e640115ff8cb660afcc64dcc5d1b5849" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Subject>
Expand All @@ -117,77 +156,21 @@ var responseXMLAssertionSubject = `<?xml version="1.0" encoding="UTF-8"?>
</saml2:Assertion>
</saml2p:Response>`

func Test_ParseResponse_Assertion_Subject(t *testing.T) {
t.Parallel()
r := require.New(t)

sub := responseXML(t, responseXMLAssertionSubject).Assertions[0].Subject

r.Equal("[email protected]", sub.NameID.Value)
r.EqualValues(core.ConfirmationMethodBearer, sub.SubjectConfirmation.Method)
r.Equal("http://localhost:8000/saml/acs", sub.SubjectConfirmation.SubjectConfirmationData.Recipient)
r.Equal("request-id", sub.SubjectConfirmation.SubjectConfirmationData.InResponseTo)
}

var responseXMLAssertions = `<?xml version="1.0" encoding="UTF-8"?>
responseXMLAssertionAttributes = `<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="http://localhost:8000/saml/acs" ID="saml-response-id" InResponseTo="saml-request-id" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_e640115ff8cb660afcc64dcc5d1b5849" IssueInstant="2023-03-31T06:55:44.494Z" Version="2.0">
<saml2:Issuer>https://samltest.id/saml/idp</saml2:Issuer>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" NameQualifier="https://samltest.id/saml/idp" SPNameQualifier="http://saml.julz/example">[email protected]</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData Address="80.140.197.138" InResponseTo="08aef46c-69f5-bd8e-2e57-3cb0dd4682b6" NotOnOrAfter="2023-03-31T07:00:44.509Z" Recipient="http://localhost:8000/saml/acs" />
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2023-03-31T06:55:44.494Z" NotOnOrAfter="2023-03-31T07:00:44.494Z">
<saml2:AudienceRestriction>
<saml2:Audience>http://saml.julz/example</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement AuthnInstant="2023-03-31T06:55:41.139Z" SessionIndex="_590baa6b9534066a50f9cc50baa928e1">
<saml2:SubjectLocality Address="80.140.197.138" />
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute FriendlyName="eduPersonEntitlement" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.7" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>urn:mace:dir:entitlement:common-lib-terms</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>rick</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="urn:oasis:names:tc:SAML:attribute:subject-id" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">[email protected]</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="telephoneNumber" Name="urn:oid:2.5.4.20" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>+1-555-555-5515</saml2:AttributeValue>
<saml2:AttributeValue>+1-555-555-5555</saml2:AttributeValue>
<saml2:AttributeValue>+1-777-777-7777</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="role" Name="https://samltest.id/attributes/role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">[email protected]</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:Attribute FriendlyName="email" Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>[email protected]</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="sn" Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>Sanchez</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="displayName" Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>Rick Sanchez</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="givenName" Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue>Rick</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
</saml2:Assertion>
</saml2p:Response>`

func responseXML(t *testing.T, ssoRes string) core.Response {
t.Helper()

r := require.New(t)
res := core.Response{}
err := xml.Unmarshal([]byte(ssoRes), &res)
r.NoError(err)
return res
}
)
2 changes: 1 addition & 1 deletion saml/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (sp *ServiceProvider) ParseResponse(
}
}

return (*core.Response)(response), nil
return &core.Response{Response: *response}, nil
}

func (sp *ServiceProvider) internalParser(
Expand Down

0 comments on commit a92758c

Please sign in to comment.