Skip to content

Commit b6cb65b

Browse files
duanemaypeterhaochen47
authored andcommitted
wip: Identity Zone data for default zone
Signed-off-by: Duane May <[email protected]> Signed-off-by: Peter Chen <[email protected]>
1 parent 70019ee commit b6cb65b

File tree

6 files changed

+184
-66
lines changed

6 files changed

+184
-66
lines changed

server/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ dependencies {
112112

113113
testImplementation(libraries.jsonPathAssert)
114114
testImplementation(libraries.guavaTestLib)
115+
testImplementation(libraries.xmlUnit)
115116

116117
implementation(libraries.commonsIo)
117118
}

server/src/main/java/org/cloudfoundry/identity/uaa/impl/config/IdentityZoneConfigurationBootstrap.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public void afterPropertiesSet() throws InvalidIdentityZoneDetailsException {
8686
definition.getLinks().getSelfService().setSelfServiceLinksEnabled(selfServiceLinksEnabled);
8787
definition.getLinks().setHomeRedirect(homeRedirect);
8888
definition.getSamlConfig().setCertificate(samlSpCertificate);
89+
// TODO: This needs to pull from the default saml config
90+
definition.getSamlConfig().setWantAssertionSigned(false);
8991
definition.getSamlConfig().setPrivateKey(samlSpPrivateKey);
9092
definition.getSamlConfig().setPrivateKeyPassword(samlSpPrivateKeyPassphrase);
9193
definition.getSamlConfig().setDisableInResponseToCheck(disableSamlInResponseToCheck);
Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.cloudfoundry.identity.uaa.provider.saml;
22

33
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
4+
import org.cloudfoundry.identity.uaa.zone.SamlConfig;
45
import org.cloudfoundry.identity.uaa.zone.ZoneAware;
6+
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
57
import org.opensaml.saml.common.xml.SAMLConstants;
68
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
79
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
@@ -11,14 +13,11 @@
1113
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
1214
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
1315
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
14-
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
15-
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
1616
import org.springframework.util.Assert;
1717
import org.springframework.web.bind.annotation.GetMapping;
1818
import org.springframework.web.bind.annotation.PathVariable;
1919
import org.springframework.web.bind.annotation.RestController;
2020

21-
import javax.servlet.http.HttpServletRequest;
2221
import javax.servlet.http.HttpServletResponse;
2322
import java.net.URLEncoder;
2423
import java.nio.charset.StandardCharsets;
@@ -27,80 +26,73 @@
2726
@RestController
2827
public class SamlMetadataEndpoint implements ZoneAware {
2928
public static final String DEFAULT_REGISTRATION_ID = "example";
30-
private static final String DEFAULT_FILE_NAME = "saml-sp.xml";
3129
private static final String APPLICATION_XML_CHARSET_UTF_8 = "application/xml; charset=UTF-8";
32-
private static final String CONTENT_DISPOSITION_FORMAT = "attachment; filename=\"%s\"; filename*=UTF-8''%s";
3330

34-
// @todo - this should be a Zone aware resolver
35-
private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver;
3631
private final Saml2MetadataResolver saml2MetadataResolver;
32+
private final IdentityZoneManager identityZoneManager;
3733

38-
private String fileName;
39-
private String encodedFileName;
40-
41-
private final Boolean wantAssertionSigned;
4234
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
4335

4436
public SamlMetadataEndpoint(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
45-
SamlConfigProps samlConfigProps) {
37+
IdentityZoneManager identityZoneManager) {
4638
Assert.notNull(relyingPartyRegistrationRepository, "relyingPartyRegistrationRepository cannot be null");
4739
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
48-
this.relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository);
40+
this.identityZoneManager = identityZoneManager;
4941
OpenSamlMetadataResolver resolver = new OpenSamlMetadataResolver();
5042
this.saml2MetadataResolver = resolver;
5143
resolver.setEntityDescriptorCustomizer(new EntityDescriptorCustomizer());
52-
this.wantAssertionSigned = samlConfigProps.getWantAssertionSigned();
53-
setFileName(DEFAULT_FILE_NAME);
5444
}
5545

5646
private class EntityDescriptorCustomizer implements Consumer<OpenSamlMetadataResolver.EntityDescriptorParameters> {
5747
@Override
5848
public void accept(OpenSamlMetadataResolver.EntityDescriptorParameters entityDescriptorParameters) {
49+
SamlConfig samlConfig = identityZoneManager.getCurrentIdentityZone().getConfig().getSamlConfig();
50+
5951
EntityDescriptor descriptor = entityDescriptorParameters.getEntityDescriptor();
6052
SPSSODescriptor spssodescriptor = descriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS);
61-
spssodescriptor.setWantAssertionsSigned(wantAssertionSigned);
53+
spssodescriptor.setWantAssertionsSigned(samlConfig.isWantAssertionSigned());
6254
spssodescriptor.setAuthnRequestsSigned(entityDescriptorParameters.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned());
6355
}
6456
}
6557

6658
@GetMapping(value = "/saml/metadata", produces = APPLICATION_XML_CHARSET_UTF_8)
67-
public ResponseEntity<String> legacyMetadataEndpoint(HttpServletRequest request) {
68-
return metadataEndpoint(DEFAULT_REGISTRATION_ID, request);
59+
public ResponseEntity<String> legacyMetadataEndpoint() {
60+
return metadataEndpoint(DEFAULT_REGISTRATION_ID);
6961
}
7062

7163
@GetMapping(value = "/saml/metadata/{registrationId}", produces = APPLICATION_XML_CHARSET_UTF_8)
72-
public ResponseEntity<String> metadataEndpoint(@PathVariable String registrationId, HttpServletRequest request) {
64+
public ResponseEntity<String> metadataEndpoint(@PathVariable String registrationId) {
7365
RelyingPartyRegistration relyingPartyRegistration = relyingPartyRegistrationRepository.findByRegistrationId(registrationId);
7466
if (relyingPartyRegistration == null) {
7567
return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).build();
7668
}
7769
String metadata = saml2MetadataResolver.resolve(relyingPartyRegistration);
7870

79-
// @todo - fileName may need to be dynamic based on registrationID
80-
String[] fileNames = retrieveZoneAwareFileNames();
71+
String contentDisposition = ContentDispositionFilename.getContentDisposition(retrieveZone());
8172
return ResponseEntity.ok()
82-
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(
83-
CONTENT_DISPOSITION_FORMAT, fileNames[0], fileNames[1]))
73+
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
8474
.body(metadata);
8575
}
76+
}
8677

87-
public void setFileName(String fileName) {
88-
encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
89-
this.fileName = fileName;
90-
}
78+
record ContentDispositionFilename(String fileName) {
79+
private static final String CONTENT_DISPOSITION_FORMAT = "attachment; filename=\"%s\"; filename*=UTF-8''%s";
80+
private static final String DEFAULT_FILE_NAME = "saml-sp.xml";
9181

92-
private String[] retrieveZoneAwareFileNames() {
93-
IdentityZone zone = retrieveZone();
94-
String[] fileNames = new String[2];
82+
static ContentDispositionFilename retrieveZoneAwareContentDispositionFilename(IdentityZone zone) {
9583
if (zone.isUaa()) {
96-
fileNames[0] = fileName;
97-
fileNames[1] = encodedFileName;
98-
}
99-
else {
100-
fileNames[0] = "saml-" + zone.getSubdomain() + "-sp.xml";
101-
fileNames[1] = URLEncoder.encode(fileNames[0],
102-
StandardCharsets.UTF_8);
84+
return new ContentDispositionFilename(DEFAULT_FILE_NAME);
10385
}
104-
return fileNames;
86+
String filename = "saml-%s-sp.xml".formatted(zone.getSubdomain());
87+
return new ContentDispositionFilename(filename);
88+
}
89+
90+
static String getContentDisposition(IdentityZone zone) {
91+
return retrieveZoneAwareContentDispositionFilename(zone).getContentDisposition();
92+
}
93+
94+
String getContentDisposition() {
95+
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
96+
return CONTENT_DISPOSITION_FORMAT.formatted(fileName, encodedFileName);
10597
}
10698
}

server/src/test/java/org/cloudfoundry/identity/uaa/config/IdentityZoneConfigurationBootstrapTests.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@ void defaultSamlKeys() throws Exception {
143143
assertThat(uaa.getConfig().getSamlConfig().getCertificate()).isEqualTo(SamlTestUtils.PROVIDER_CERTIFICATE);
144144
}
145145

146+
@Test
147+
void samlWantAssertionSigned() throws Exception {
148+
bootstrap.setSamlSpPrivateKey(SamlTestUtils.PROVIDER_PRIVATE_KEY);
149+
bootstrap.setSamlSpCertificate(SamlTestUtils.PROVIDER_CERTIFICATE);
150+
bootstrap.setSamlSpPrivateKeyPassphrase(SamlTestUtils.PROVIDER_PRIVATE_KEY_PASSWORD);
151+
bootstrap.afterPropertiesSet();
152+
IdentityZone uaa = provisioning.retrieve(IdentityZone.getUaaZoneId());
153+
assertThat(uaa.getConfig().getSamlConfig().isWantAssertionSigned()).isEqualTo(false);
154+
}
155+
146156
@Test
147157
void enableInResponseTo() throws Exception {
148158
bootstrap.setDisableSamlInResponseToCheck(false);
@@ -253,7 +263,6 @@ void logoutRedirect() throws Exception {
253263
assertThat(config.getLinks().getLogout().isDisableRedirectParameter()).isFalse();
254264
}
255265

256-
257266
@Test
258267
void testPrompts() throws Exception {
259268
List<Prompt> prompts = Arrays.asList(
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.cloudfoundry.identity.uaa.provider.saml;
2+
3+
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
4+
import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration;
5+
import org.cloudfoundry.identity.uaa.zone.SamlConfig;
6+
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.ExtendWith;
10+
import org.mockito.Mock;
11+
import org.mockito.junit.jupiter.MockitoExtension;
12+
import org.springframework.http.HttpHeaders;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.mock.web.MockHttpServletRequest;
15+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
16+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
17+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
18+
import org.xmlunit.assertj.XmlAssert;
19+
20+
import java.util.List;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.cloudfoundry.identity.uaa.provider.saml.Saml2TestUtils.xmlNamespaces;
24+
import static org.cloudfoundry.identity.uaa.provider.saml.TestSaml2X509Credentials.relyingPartySigningCredential;
25+
import static org.cloudfoundry.identity.uaa.provider.saml.TestSaml2X509Credentials.relyingPartyVerifyingCredential;
26+
import static org.mockito.Mockito.spy;
27+
import static org.mockito.Mockito.when;
28+
29+
@ExtendWith(MockitoExtension.class)
30+
class SamlMetadataEndpointTest {
31+
private static final String ASSERTION_CONSUMER_SERVICE = "https://acsl";
32+
private static final String REGISTRATION_ID = "regId";
33+
private static final String ENTITY_ID = "entityId";
34+
35+
SamlMetadataEndpoint endpoint;
36+
37+
@Mock
38+
RelyingPartyRegistrationRepository repository;
39+
@Mock
40+
IdentityZoneManager identityZoneManager;
41+
@Mock
42+
RelyingPartyRegistration registration;
43+
@Mock
44+
IdentityZone identityZone;
45+
@Mock
46+
IdentityZoneConfiguration identityZoneConfiguration;
47+
@Mock
48+
SamlConfig samlConfig;
49+
@Mock
50+
RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails;
51+
52+
@BeforeEach
53+
void setUp() {
54+
endpoint = spy(new SamlMetadataEndpoint(repository, identityZoneManager));
55+
when(repository.findByRegistrationId(REGISTRATION_ID)).thenReturn(registration);
56+
when(registration.getEntityId()).thenReturn(ENTITY_ID);
57+
when(registration.getSigningX509Credentials()).thenReturn(List.of(relyingPartySigningCredential()));
58+
when(registration.getDecryptionX509Credentials()).thenReturn(List.of(relyingPartyVerifyingCredential()));
59+
when(registration.getAssertionConsumerServiceBinding()).thenReturn(Saml2MessageBinding.REDIRECT);
60+
when(registration.getAssertionConsumerServiceLocation()).thenReturn(ASSERTION_CONSUMER_SERVICE);
61+
when(identityZoneManager.getCurrentIdentityZone()).thenReturn(identityZone);
62+
when(identityZone.getConfig()).thenReturn(identityZoneConfiguration);
63+
when(identityZoneConfiguration.getSamlConfig()).thenReturn(samlConfig);
64+
when(registration.getAssertingPartyDetails()).thenReturn(assertingPartyDetails);
65+
}
66+
67+
@Test
68+
void testDefaultFileName() {
69+
ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
70+
assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION))
71+
.isEqualTo("attachment; filename=\"saml-sp.xml\"; filename*=UTF-8''saml-sp.xml");
72+
}
73+
74+
@Test
75+
void testZonedFileName() {
76+
when(identityZone.isUaa()).thenReturn(false);
77+
when(identityZone.getSubdomain()).thenReturn("testzone1");
78+
when(endpoint.retrieveZone()).thenReturn(identityZone);
79+
80+
ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
81+
assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION))
82+
.isEqualTo("attachment; filename=\"saml-testzone1-sp.xml\"; filename*=UTF-8''saml-testzone1-sp.xml");
83+
}
84+
85+
@Test
86+
void testDefaultMetadataXml() {
87+
when(samlConfig.isWantAssertionSigned()).thenReturn(true);
88+
when(assertingPartyDetails.getWantAuthnRequestsSigned()).thenReturn(true);
89+
90+
ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
91+
XmlAssert xmlAssert =XmlAssert.assertThat(response.getBody()).withNamespaceContext(xmlNamespaces());
92+
xmlAssert.valueByXPath("//md:EntityDescriptor/@entityID").isEqualTo(ENTITY_ID);
93+
xmlAssert.valueByXPath("//md:SPSSODescriptor/@AuthnRequestsSigned").isEqualTo(true);
94+
xmlAssert.valueByXPath("//md:SPSSODescriptor/@WantAssertionsSigned").isEqualTo(true);
95+
xmlAssert.valueByXPath("//md:AssertionConsumerService/@Location").isEqualTo(ASSERTION_CONSUMER_SERVICE);
96+
}
97+
98+
@Test
99+
void testDefaultMetadataXml_alternateValues() {
100+
when(samlConfig.isWantAssertionSigned()).thenReturn(false);
101+
when(assertingPartyDetails.getWantAuthnRequestsSigned()).thenReturn(false);
102+
103+
ResponseEntity<String> response = endpoint.metadataEndpoint(REGISTRATION_ID);
104+
XmlAssert xmlAssert =XmlAssert.assertThat(response.getBody()).withNamespaceContext(xmlNamespaces());
105+
xmlAssert.valueByXPath("//md:SPSSODescriptor/@AuthnRequestsSigned").isEqualTo(false);
106+
xmlAssert.valueByXPath("//md:SPSSODescriptor/@WantAssertionsSigned").isEqualTo(false);
107+
}
108+
}

0 commit comments

Comments
 (0)