Skip to content

Commit

Permalink
Merge pull request #1343 from clnative/main-3.x-multiInstanceIdentity…
Browse files Browse the repository at this point in the history
…ServicesPropertySourceFactory

Enable IdentityServicesPropertySourceFactory to Map More Than Two XSUAA Service Instances [3.x]
  • Loading branch information
finkmanAtSap authored Dec 12, 2023
2 parents 8b09147 + 60b2808 commit d27e885
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public OAuth2ServiceConfiguration getIasConfiguration() {
*
* @return the service configurations grouped by service
*/
@Override
public Map<Service, List<OAuth2ServiceConfiguration>> getServiceConfigurationsAsList() {
if (serviceConfigurations == null) {
this.readServiceConfigurations();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
*/
package com.sap.cloud.security.config;

import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;



/**
* Central entry point to access the OAuth configuration
* ({@link OAuth2ServiceConfiguration}) of a supported identity {@link Service}.
*/
public interface Environment {
/**
* Return OAuth service configuration of Xsuaa identity service instance.
*
* Return the primary OAuth service configuration of Xsuaa identity service instance.
*
* @return the OAuth service configuration or null, in case there is no instance
*/
@Nullable
Expand All @@ -30,7 +34,7 @@ public interface Environment {
OAuth2ServiceConfiguration getIasConfiguration();

/**
* Returns number of Xsuaa identity service instances.
* Returns the number of Xsuaa identity service instances.
*
* @return the number Xsuaa identity service instances.
*
Expand All @@ -39,7 +43,7 @@ public interface Environment {

/**
* In case there is only one Xsuaa identity service instance, this one gets
* returned. In case there are multiple bindings the one of plan "broker" gets
* returned. In case there are multiple bindings the primary one of plan "broker" gets
* returned.
*
* @return the service configuration to be used for token exchange
Expand All @@ -49,6 +53,13 @@ public interface Environment {
*/
@Nullable
OAuth2ServiceConfiguration getXsuaaConfigurationForTokenExchange();

/**
* Gives access to all service configurations parsed from the environment.
*
* @return the service configurations grouped by service
*/
Map<Service, List<OAuth2ServiceConfiguration>> getServiceConfigurationsAsList();

Map<Service, Map<ServiceConstants.Plan, OAuth2ServiceConfiguration>> getServiceConfigurations();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package com.sap.cloud.security.spring.autoconfig;

import com.sap.cloud.security.spring.config.IdentityServiceConfiguration;
import com.sap.cloud.security.config.ServiceConstants;
import com.sap.cloud.security.spring.config.XsuaaServiceConfiguration;
import com.sap.cloud.security.spring.config.XsuaaServiceConfigurations;
import com.sap.cloud.security.spring.token.authentication.JwtDecoderBuilder;
Expand All @@ -25,6 +26,8 @@
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;

import java.util.List;

import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET;

/**
Expand Down Expand Up @@ -79,9 +82,18 @@ public JwtDecoder hybridJwtDecoder(XsuaaServiceConfiguration xsuaaConfig,
@ConditionalOnProperty("sap.security.services.xsuaa[0].uaadomain")
public JwtDecoder hybridJwtDecoderMultiXsuaaServices(IdentityServiceConfiguration identityConfig) {
LOGGER.debug("auto-configures HybridJwtDecoder when bound to multiple xsuaa service instances.");

/* Use only primary XSUAA config and up to 1 more config of type BROKER to stay backward-compatible now that XsuaaServiceConfigurations contains all XSUAA
configurations instead of only two. */
List<XsuaaServiceConfiguration> allXsuaaConfigs = xsuaaConfigs.getConfigurations();
List<XsuaaServiceConfiguration> usedXsuaaConfigs = allXsuaaConfigs.subList(0, Math.min(2, allXsuaaConfigs.size()));
if (usedXsuaaConfigs.size() == 2 && !ServiceConstants.Plan.BROKER.toString().equals(usedXsuaaConfigs.get(1).getProperty(ServiceConstants.SERVICE_PLAN))) {
usedXsuaaConfigs = usedXsuaaConfigs.subList(0, 1);
}

return new JwtDecoderBuilder()
.withIasServiceConfiguration(identityConfig)
.withXsuaaServiceConfigurations(xsuaaConfigs.getConfigurations())
.withXsuaaServiceConfigurations(usedXsuaaConfigs)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import com.sap.cloud.security.config.Environment;
import com.sap.cloud.security.config.Environments;
import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
import com.sap.cloud.security.config.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.PropertiesPropertySource;
Expand All @@ -20,6 +22,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;

import static com.sap.cloud.security.config.ServiceConstants.IAS.DOMAINS;

Expand Down Expand Up @@ -56,6 +59,8 @@ public class IdentityServicesPropertySourceFactory implements PropertySourceFact
private static final List<String> IAS_ATTRIBUTES = Collections.unmodifiableList(Arrays
.asList("clientid", "clientsecret", "domains", "url"));

private Properties properties;

@Override
@SuppressWarnings("squid:S2259") // false positive
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
Expand All @@ -64,49 +69,84 @@ public PropertySource<?> createPropertySource(String name, EncodedResource resou
&& resource.getResource().getFilename() != null && !resource.getResource().getFilename().isEmpty()) {
environment = Environments.readFromInput(resource.getResource().getInputStream());
}
boolean multipleXsuaaServicesBound = environment.getNumberOfXsuaaConfigurations() > 1;

Properties properties = getXsuaaProperties(environment, multipleXsuaaServicesBound);
properties.putAll(getIasProperties(environment));
logger.debug("Parsed {} properties from identity services. {}", properties.size(),
properties.stringPropertyNames());
return new PropertiesPropertySource(PROPERTIES_KEY, properties);

this.properties = new Properties();

mapXsuaaProperties(environment);
mapIasProperties(environment);
logger.debug("Parsed {} properties from identity services. {}", this.properties.size(),
this.properties.stringPropertyNames());

return new PropertiesPropertySource(name == null ? PROPERTIES_KEY : name, this.properties);
}

@Nonnull
private Properties getXsuaaProperties(Environment environment, boolean multipleXsuaaServicesBound) {
Properties properties = new Properties();
if (environment.getXsuaaConfiguration() != null) {
String xsuaaPrefix = multipleXsuaaServicesBound ? PROPERTIES_KEY + ".xsuaa[0]." : XSUAA_PREFIX;
for (String key : XSUAA_ATTRIBUTES) {
if (environment.getXsuaaConfiguration().hasProperty(key)) {
properties.put(xsuaaPrefix + key, environment.getXsuaaConfiguration().getProperty(key));
}
private void mapXsuaaAttributesSingleInstance(final OAuth2ServiceConfiguration oAuth2ServiceConfiguration, final String prefix) {
for (String key : XSUAA_ATTRIBUTES) {
if (oAuth2ServiceConfiguration.hasProperty(key)) {
this.properties.put(prefix + key, oAuth2ServiceConfiguration.getProperty(key));
}
}
if (multipleXsuaaServicesBound) {
for (String key : XSUAA_ATTRIBUTES) {
if (environment.getXsuaaConfigurationForTokenExchange().hasProperty(key)) {
properties.put(PROPERTIES_KEY + ".xsuaa[1]." + key,
environment.getXsuaaConfigurationForTokenExchange().getProperty(key));
}
}
}

private void mapXsuaaProperties(@Nonnull Environment environment) {
final int numberOfXsuaaConfigurations = environment.getNumberOfXsuaaConfigurations();
if (numberOfXsuaaConfigurations == 0) {
return;
}

/*
* Case "single XSUAA service configuration":
* Then we do not use an array for describing the properties.
*/
final OAuth2ServiceConfiguration xsuaaConfiguration = environment.getXsuaaConfiguration();
if (numberOfXsuaaConfigurations == 1) {
mapXsuaaAttributesSingleInstance(xsuaaConfiguration, XSUAA_PREFIX);
return;
}

/*
* Case "multiple XSUAA service configurations":
* For historic reasons, the first two items in the array have a special meaning:
* - Item 0 is exclusively used for environment.getXsuaaConfiguration() ("an arbitrary Xsuaa configuration" of plan "application").
* - Item 1 is optionally used for environment.getXsuaaConfigurationForTokenExchange() ("an arbitrary Xsuaa configuration" of plan "broker").
*/
mapXsuaaAttributesSingleInstance(xsuaaConfiguration, PROPERTIES_KEY + ".xsuaa[0].");

int position = 1;
final OAuth2ServiceConfiguration xsuaaConfigurationForTokenExchange = environment.getXsuaaConfigurationForTokenExchange();
if (xsuaaConfigurationForTokenExchange != null) {
mapXsuaaAttributesSingleInstance(xsuaaConfigurationForTokenExchange, PROPERTIES_KEY + ".xsuaa[1].");
position = 2;
}

/*
* For all other items coming thereafter, there is no order defined anymore.
* However, we must not duplicate the instances...
*/
final List<OAuth2ServiceConfiguration> remainingXsuaaConfigurations = environment.getServiceConfigurationsAsList().get(Service.XSUAA)
.stream()
.filter(e -> e != xsuaaConfiguration && e != xsuaaConfigurationForTokenExchange)
.collect(Collectors.toList());

/* Usage of ".forEach" would have been preferred here,
* but Closures in JDK8 do not permit accessing non-final "position".
*/
for (OAuth2ServiceConfiguration config : remainingXsuaaConfigurations) {
final String prefix = String.format(PROPERTIES_KEY + ".xsuaa[%d].", position++);
this.mapXsuaaAttributesSingleInstance(config, prefix);
}
return properties;
}

@Nonnull
private Properties getIasProperties(Environment environment) {
Properties properties = new Properties();
if (environment.getIasConfiguration() != null) {
private void mapIasProperties(@Nonnull Environment environment) {
final OAuth2ServiceConfiguration iasConfiguration = environment.getIasConfiguration();
if (iasConfiguration != null) {
for (String key : IAS_ATTRIBUTES) {
if (environment.getIasConfiguration().hasProperty(key)) { // will not find "domains" among properties
properties.put(IAS_PREFIX + key, environment.getIasConfiguration().getProperty(key));
if (iasConfiguration.hasProperty(key)) { // will not find "domains" among properties
this.properties.put(IAS_PREFIX + key, iasConfiguration.getProperty(key));
}
}
properties.put(IAS_PREFIX + DOMAINS, environment.getIasConfiguration().getDomains());
this.properties.put(IAS_PREFIX + DOMAINS, iasConfiguration.getDomains());
}
return properties;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* SPDX-FileCopyrightText: 2018-2023 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors
*<p>
* SPDX-License-Identifier: Apache-2.0
*/
package com.sap.cloud.security.spring.config;

import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@SpringBootTest(classes = { BrokerHoleTestConfigurationFromFile.class })
class IdentityServicesPropertySourceFactoryBrokerNoHoleTest {

@Autowired
BrokerHoleTestConfigurationFromFile configuration;

@Test
void testInjectedPropertyValues_fourXsuaaBindings() {
/* Index 0 */
assertEquals("client-id2", configuration.xsuaaClientId0);
assertEquals("client-secret2", configuration.xsuaaClientSecret0);
assertEquals("http://domain.xsuaadomain", configuration.xsuaaUrl0);
assertEquals("xsuaadomain", configuration.xsuaaDomain0);
assertEquals("xsappname2", configuration.xsuaaAppName0);
assertEquals("", configuration.unknown0);

/* Index 1 */
assertEquals("client-id", configuration.xsuaaClientId1);
assertEquals("client-secret", configuration.xsuaaClientSecret1);
assertEquals("xsappname", configuration.xsuaaAppName1);

/* Index 2 */
assertEquals("none", configuration.xsuaaClientId2);
assertEquals("none", configuration.xsuaaClientSecret2);

/* IAS */
assertEquals("client-id-ias", configuration.identityClientId);
assertEquals("client-secret-ias", configuration.identityClientSecret);
assertTrue(configuration.identityDomains.contains("iasdomain"));
assertTrue(configuration.identityDomains.contains("iasdomain.com"));
assertEquals(2, configuration.identityDomains.size());
}
}

@Configuration
@PropertySource(factory = IdentityServicesPropertySourceFactory.class, value = {
"classpath:xsuaaBindingsTwoApplicationsNoBroker.json" })
class BrokerHoleTestConfigurationFromFile {

/* Index 0 */

@Value("${sap.security.services.xsuaa[0].url:}")
public String xsuaaUrl0;

@Value("${sap.security.services.xsuaa[0].uaadomain:}")
public String xsuaaDomain0;

@Value("${sap.security.services.xsuaa[0].clientid:}")
public String xsuaaClientId0;

@Value("${sap.security.services.xsuaa[0].clientsecret:}")
public String xsuaaClientSecret0;

@Value("${sap.security.services.xsuaa[0].xsappname:}")
public String xsuaaAppName0;

@Value("${sap.security.services.xsuaa[0].unknown:}")
public String unknown0;


/* Index 1 */

@Value("${sap.security.services.xsuaa[1].clientid:none}")
public String xsuaaClientId1;

@Value("${sap.security.services.xsuaa[1].clientsecret:none}")
public String xsuaaClientSecret1;

@Value("${sap.security.services.xsuaa[1].xsappname}")
public String xsuaaAppName1;

/* Index 2 */

@Value("${sap.security.services.xsuaa[2].clientid:none}")
public String xsuaaClientId2;

@Value("${sap.security.services.xsuaa[2].clientsecret:none}")
public String xsuaaClientSecret2;



/* IAS */

@Value("${sap.security.services.identity.clientid:}")
public String identityClientId;

@Value("${sap.security.services.identity.clientsecret:}")
public String identityClientSecret;

@Value("${sap.security.services.identity.domains:}")
public List<String> identityDomains;
}
Loading

0 comments on commit d27e885

Please sign in to comment.