Skip to content

Commit

Permalink
[JN-624] Add support for multiple B2C tenants (#613)
Browse files Browse the repository at this point in the history
  • Loading branch information
denniscunningham authored Nov 1, 2023
1 parent 35e28ad commit 9df57ba
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 34 deletions.
3 changes: 2 additions & 1 deletion api-participant/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
id 'de.undercouch.download'
id 'com.google.cloud.tools.jib'
id 'com.srcclr.gradle'

id "io.freefair.lombok" version "6.5.1"
id 'com.gorylenko.gradle-git-properties' version '2.3.1'
}

Expand Down Expand Up @@ -42,6 +42,7 @@ dependencies {
exclude group: 'com.vaadin.external.google', module: 'android-json'
}
testImplementation 'org.mockito:mockito-inline'
testImplementation 'commons-io:commons-io:2.13.0'
// See https://stackoverflow.com/questions/5644011/multi-project-test-dependencies-with-gradle/60138176#60138176
testImplementation(testFixtures(project(":core")))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "b2c")
public record B2CConfiguration(
String tenantName, String clientId, String policyName, String changePasswordPolicyName) {}
public record B2CConfiguration(String configFile) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package bio.terra.pearl.api.participant.config;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;

@Slf4j
@Service
public class B2CConfigurationService {
private final B2CConfiguration b2cConfiguration;
@Getter private B2CPortalConfiguration portalConfiguration;
private final Map<String, Map<String, String>> portalToConfig = new HashMap<>();

public B2CConfigurationService(B2CConfiguration b2cConfiguration) {
this.b2cConfiguration = b2cConfiguration;
portalConfiguration = new B2CPortalConfiguration();
}

/** Get the B2C configuration for a portal. Returns null if B2C is not configured for portal */
public Map<String, String> getB2CForPortal(String portalShortcode) {
return portalToConfig.get(portalShortcode);
}

/**
* Initialize B2C configuration from a yaml file. The file can be specified as an absolute path or
* a relative path. If relative, it is loaded from the classpath.
*/
@EventListener(ApplicationReadyEvent.class)
public void initB2CConfig() {
String b2cConfigFile = b2cConfiguration.configFile();
if (StringUtils.isBlank(b2cConfigFile)) {
log.error("b2c-config-file property is not set");
return;
}

log.info("b2c-config-file = '{}'", b2cConfigFile);
Yaml yaml = new Yaml(new Constructor(B2CPortalConfiguration.class, new LoaderOptions()));

// for deployments into k8s, the config file is mounted into the container as a volume
// for local deployments, the config file is on the classpath, at least for now
File file = new File(b2cConfigFile);
if (file.isAbsolute()) {
// absolute path, load from file system
if (!file.exists()) {
log.error(
"b2c-config-file property is set to an absolute path that does not exist: {}",
b2cConfigFile);
return;
}

try (InputStream str = new FileInputStream(file)) {
portalConfiguration = yaml.load(str);
} catch (Exception e) {
log.error("Error loading b2c config file: {}", b2cConfigFile, e);
return;
}
} else {
// relative path, load from classpath
ClassPathResource cpr = new ClassPathResource(b2cConfigFile);
try (InputStream str = cpr.getInputStream()) {
portalConfiguration = yaml.load(str);
} catch (Exception e) {
log.error("Error loading b2c config file: {}", b2cConfigFile, e);
return;
}
}
buildPortalToConfig();
}

protected void buildPortalToConfig() {
portalToConfig.clear();
for (String portal : portalConfiguration.getB2CProperties().keySet()) {
portalToConfig.put(portal, buildConfigMap(portal));
}
}

protected Map<String, String> buildConfigMap(String portalShortcode) {
B2CPortalConfiguration.B2CProperties b2cConfig =
getPortalConfiguration().getPortalProperties(portalShortcode);
if (b2cConfig == null) {
return Collections.emptyMap();
}
Map<String, String> config =
Map.of(
"b2cTenantName",
StringUtils.defaultIfEmpty(b2cConfig.getTenantName(), ""),
"b2cClientId",
StringUtils.defaultIfEmpty(b2cConfig.getClientId(), ""),
"b2cPolicyName",
StringUtils.defaultIfEmpty(b2cConfig.getPolicyName(), ""),
"b2cChangePasswordPolicyName",
StringUtils.defaultIfEmpty(b2cConfig.getChangePasswordPolicyName(), ""));
portalToConfig.put(portalShortcode, config);
return config;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package bio.terra.pearl.api.participant.config;

import java.util.Map;
import lombok.Data;
import lombok.Setter;

@Setter
public class B2CPortalConfiguration {
private Map<String, B2CProperties> b2c;

public B2CPortalConfiguration() {
createDefaultProperties();
}

protected void createDefaultProperties() {
b2c = Map.of("missingB2CProperties", new B2CProperties());
}

public B2CPortalConfiguration.B2CProperties getPortalProperties(String portal) {
return b2c.get(portal);
}

Map<String, B2CPortalConfiguration.B2CProperties> getB2CProperties() {
return b2c;
}

@Data
public static class B2CProperties {
private String tenantName;
private String clientId;
private String policyName;
private String changePasswordPolicyName;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package bio.terra.pearl.api.participant.controller;

import bio.terra.pearl.api.participant.api.PublicApi;
import bio.terra.pearl.api.participant.config.B2CConfiguration;
import bio.terra.pearl.api.participant.config.B2CConfigurationService;
import bio.terra.pearl.api.participant.config.VersionConfiguration;
import bio.terra.pearl.api.participant.model.SystemStatus;
import bio.terra.pearl.api.participant.model.VersionProperties;
Expand All @@ -28,7 +28,7 @@

@Controller
public class PublicApiController implements PublicApi {
private final B2CConfiguration b2CConfiguration;
private final B2CConfigurationService b2CConfigurationService;
private final SiteImageService siteImageService;
private final PortalService portalService;
private final StatusService statusService;
Expand All @@ -37,13 +37,13 @@ public class PublicApiController implements PublicApi {

@Autowired
public PublicApiController(
B2CConfiguration b2CConfiguration,
B2CConfigurationService b2CConfigurationService,
SiteImageService siteImageService,
PortalService portalService,
StatusService statusService,
VersionConfiguration versionConfiguration,
Environment env) {
this.b2CConfiguration = b2CConfiguration;
this.b2CConfigurationService = b2CConfigurationService;
this.siteImageService = siteImageService;
this.portalService = portalService;
this.statusService = statusService;
Expand All @@ -69,10 +69,19 @@ public ResponseEntity<VersionProperties> getVersion() {
return ResponseEntity.ok(currentVersion);
}

@Override
public ResponseEntity<Object> getConfig() {
var config = buildConfigMap();
return ResponseEntity.ok(config);
@GetMapping(value = "/config")
public ResponseEntity<Object> getConfig(HttpServletRequest request) {
Optional<PortalEnvironmentDescriptor> portal = getPortalDescriptorForRequest(request);
if (portal.isEmpty()) {
return ResponseEntity.notFound().build();
}
String portalShortcode = portal.get().shortcode();

Map<String, String> portalConfig = b2CConfigurationService.getB2CForPortal(portalShortcode);
if (portalConfig == null || portalConfig.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(portalConfig);
}

/**
Expand Down Expand Up @@ -140,14 +149,6 @@ public String getIndex(HttpServletRequest request) {
return "forward:/";
}

private Map<String, String> buildConfigMap() {
return Map.of(
"b2cTenantName", b2CConfiguration.tenantName(),
"b2cClientId", b2CConfiguration.clientId(),
"b2cPolicyName", b2CConfiguration.policyName(),
"b2cChangePasswordPolicyName", b2CConfiguration.changePasswordPolicyName());
}

private Optional<PortalEnvironmentDescriptor> getPortalDescriptorForRequest(
HttpServletRequest request) {
String hostname = request.getServerName();
Expand Down
10 changes: 2 additions & 8 deletions api-participant/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ env:
# if true, the swagger UI page will be made available at swagger-ui.html -- should be false for production
enabled: ${SWAGGER_ENABLED:false}
b2c:
tenantName: ${B2C_TENANT_NAME:missing_tenant_name}
clientId: ${B2C_CLIENT_ID:missing_client_id}
policyName: ${B2C_POLICY_NAME:missing_policy_name}
changePasswordPolicyName: ${B2C_CHANGE_PASSWORD_POLICY_NAME:missing_policy_name}
config-file: ${B2C_CONFIG_FILE:b2c-config.yml}
email:
sendgridApiKey: ${SENDGRID_API_KEY:}
supportEmailAddress: ${SUPPORT_EMAIL_ADDRESS:[email protected]}
Expand Down Expand Up @@ -85,10 +82,7 @@ hibernate:
packages-to-scan: bio.terra.pearl.core.model

b2c:
tenantName: ${env.b2c.tenantName}
clientId: ${env.b2c.clientId}
policyName: ${env.b2c.policyName}
changePasswordPolicyName: ${env.b2c.changePasswordPolicyName}
config-file: ${env.b2c.config-file}

javatemplate:
ingress:
Expand Down
11 changes: 11 additions & 0 deletions api-participant/src/main/resources/b2c-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
b2c:
ourhealth:
tenantName: ddpdevb2c
clientId: 705c09dc-5cca-43d3-ae06-07de78bad29a
policyName: B2C_1A_ddp_participant_signup_signin_dev
changePasswordPolicyName: B2C_1A_ddp_participant_change_password_dev
hearthive:
tenantName: hearthivedev
clientId: 8c778931-b7f6-4503-b30e-e975ab8ea615
policyName: B2C_1A_ddp_participant_signup_signin_hearthive-dev
changePasswordPolicyName: B2C_1A_ddp_participant_change_password_hearthive-dev
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import bio.terra.pearl.api.participant.config.B2CConfiguration;
import bio.terra.pearl.api.participant.config.B2CConfigurationService;
import bio.terra.pearl.api.participant.config.VersionConfiguration;
import bio.terra.pearl.api.participant.controller.PublicApiController;
import bio.terra.pearl.api.participant.model.SystemStatus;
Expand All @@ -24,8 +24,7 @@ class PublicApiControllerTest {

@Autowired private MockMvc mockMvc;

@MockBean private B2CConfiguration b2CConfiguration;

@MockBean private B2CConfigurationService b2CConfigurationService;
@MockBean private SiteImageService siteImageService;
@MockBean private PortalService portalService;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package bio.terra.pearl.api.participant.config;

import static io.jsonwebtoken.lang.Assert.notNull;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.File;
import java.net.URL;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class B2CConfigurationServiceTest {

@TempDir File tempDir;

@Test
void testInitB2CConfigRelativePath() {
B2CConfigurationService b2CService =
new B2CConfigurationService(new B2CConfiguration("b2c-config.yml"));
b2CService.initB2CConfig();
B2CPortalConfiguration b2cConfig = b2CService.getPortalConfiguration();
notNull(b2cConfig, "b2cConfig should not be null");
B2CPortalConfiguration.B2CProperties b2cProperties = b2cConfig.getPortalProperties("ourhealth");
assertThat(b2cProperties.getTenantName(), equalTo("ddpdevb2c"));
}

@Test
void testInitB2CConfigAbsPath() {
// create a file that is not on the resource path
File tempFile = null;
try {
tempFile = new File(tempDir, "b2c-config.yml");
URL url = Thread.currentThread().getContextClassLoader().getResource("test-b2c-config.yml");
FileUtils.copyFile(new File(url.getPath()), tempFile);
} catch (Exception e) {
fail("Failed to create b2c-config.yml", e);
}

// ensure initialization
B2CConfigurationService b2CService =
new B2CConfigurationService(new B2CConfiguration(tempFile.getPath()));
b2CService.initB2CConfig();
B2CPortalConfiguration b2cConfig = b2CService.getPortalConfiguration();
notNull(b2cConfig, "b2cConfig should not be null");
B2CPortalConfiguration.B2CProperties b2cProperties = b2cConfig.getPortalProperties("portal2");
assertThat(b2cProperties.getTenantName(), equalTo("def"));

// ensure config map building
Map<String, String> configMap = b2CService.getB2CForPortal("portal2");
assertThat(configMap.size(), equalTo(4));
assertThat(configMap.get("b2cTenantName"), equalTo("def"));
assertThat(
configMap.get("b2cPolicyName"), equalTo("B2C_1B_ddp_participant_signup_signin_test"));
}
}
11 changes: 11 additions & 0 deletions api-participant/src/test/resources/test-b2c-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
b2c:
portal1:
tenantName: "abc"
clientId: "705c09dc-a"
policyName: "B2C_1A_ddp_participant_signup_signin_test"
changePasswordPolicyName: "B2C_1A_ddp_participant_change_password_test"
portal2:
tenantName: "def"
clientId: "705c09dc-b"
policyName: "B2C_1B_ddp_participant_signup_signin_test"
changePasswordPolicyName: "B2C_1B_ddp_participant_change_password_test"
5 changes: 1 addition & 4 deletions local-dev/render_environment_vars.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ ADMIN_API_ENV_VARS=(

PARTICIPANT_API_ENV_VARS=(
"REDIRECT_ALL_EMAILS_TO:static:$DEV_EMAIL"
"B2C_TENANT_NAME:static:ddpdevb2c"
"B2C_CLIENT_ID:vault:vault read -field value secret/dsp/ddp/b2c/dev/application_id"
"B2C_POLICY_NAME:static:B2C_1A_ddp_participant_signup_signin_dev"
"B2C_CHANGE_PASSWORD_POLICY_NAME:static:B2C_1A_ddp_participant_change_password_dev"
"B2C_CONFIG_FILE:static:b2c-config.yml"
"SENDGRID_API_KEY:vault:vault read -field=api_key secret/dsp/ddp/d2p/dev/sendgrid"
)

Expand Down

0 comments on commit 9df57ba

Please sign in to comment.