
Disclaimer: This blog post is only applicable for the SAP Cloud SDK version of at most 2.19.2. We plan to continuously migrate these blog posts into our List of Tutorials. Feel free to check out our updated Tutorials on the SAP Cloud SDK.
mvn archetype:generate -DarchetypeGroupId=com.sap.cloud.s4hana.archetypes -DarchetypeArtifactId=scp-cf-spring -DarchetypeVersion=LATEST
application
cx-server
integration-tests
unit-tests
application
folder contains the source code and configuration of your actual Spring Boot application:src/main/java | You will add your application logic here. The folder already contains the classes that implement a simple Spring Boot "Hello World" application, such as Application.java, HelloWorldController.java, HelloWorldResponse.java. |
src/main/resources | Here, we add resources required in the application logic, such as configuration files. application.yml file is already added here and we will extend it in the later steps for setting up the persistency. |
src/test/java | Additional test classes. |
src/test/resources | Additional resources for attached test modules. |
pom.xml | This is your project management file for Maven where you can maintain other sdk and open source dependencies or use plugins that ease your build environment. |
<!-- SDK framework adaptions -->
<dependency>
<groupId>com.sap.cloud.s4hana.frameworks</groupId>
<artifactId>cxf</artifactId>
</dependency>
<!-- Additional Spring Boot dependencies for JPA integration and cloud platform service customizing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-spring-service-connector</artifactId>
<version>1.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-connector</artifactId>
<version>1.2.3.RELEASE</version>
</dependency>
<!-- Liquibase for database migration -->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>3.5.3</version>
</dependency>
Now, when the project structure is generated, we can get start extending our simple "Hello World" application. As promised, we will add a capability to persist the data in a database considering multitenant environment.
First of all, we need to adapt the default configuration of Hibernate to make it tenant-aware.
Luckily, when using Spring Boot framework, it can be done with just a few lines of code.
The first class that we need for this purpose is HibernateConfig.java. I add this class in the newly created package com.mycompany.config
package com.mycompany.config;
import org.hibernate.MultiTenancyStrategy;
import org.hibernate.cfg.Environment;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class HibernateConfig {
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, MultiTenantConnectionProvider multiTenantConnectionProvider,
CurrentTenantIdentifierResolver tenantIdentifierResolver) {
final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.mycompany.models");
em.setJpaVendorAdapter(this.jpaVendorAdapter());
final Map<String, Object> jpaProperties = new HashMap<>();
jpaProperties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaProperties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
jpaProperties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
jpaProperties.put(Environment.FORMAT_SQL, true);
em.setJpaPropertyMap(jpaProperties);
return em;
}
}
jpaProperties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaProperties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
jpaProperties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
package com.mycompany.config;
import com.mycompany.util.TenantUtil;
import org.hibernate.HibernateException;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class SchemaPerTenantConnectionProvider implements MultiTenantConnectionProvider {
@Value("${multitenant.defaultTenant}")
String defaultTenant;
@Autowired
private DataSource dataSource;
@Override
public Connection getAnyConnection() throws SQLException {
return this.dataSource.getConnection();
}
@Override
public void releaseAnyConnection(final Connection connection) throws SQLException {
connection.close();
}
@Override
public Connection getConnection(final String tenantIdentifier) throws SQLException {
final Connection connection = this.getAnyConnection();
try {
connection.setSchema(TenantUtil.createSchemaName(tenantIdentifier));
} catch (SQLException e) {
throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",
e);
}
return connection;
}
@Override
public void releaseConnection(final String tenantIdentifier, final Connection connection) throws SQLException {
try {
connection.setSchema(TenantUtil.createSchemaName(defaultTenant));
} catch (SQLException e) {
throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",
e);
}
connection.close();
}
@Override
public boolean supportsAggressiveRelease() {
return true;
}
@Override
public boolean isUnwrappableAs(final Class aClass) {
return false;
}
@Override
public <T> T unwrap(final Class<T> aClass) {
return null;
}
}
getConnection | This method returns a connection set up for a database schema name dependently on a tenant id. TenantUtil is an additional util class that we will implement in order to apply a schema name derivation from the given tenant id. |
releaseConnection | For the given connection, this method changes the schema in the connection to the default schema (we configure the default schema name in application.yml that will be discussed later) and closes the connection. |
package com.mycompany.util;
public class TenantUtil {
public static String createSchemaName(final String tenantId) {
return String.format("tenant_%s", tenantId);
}
}
package com.mycompany.config;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor;
import com.sap.cloud.sdk.cloudplatform.tenant.exception.TenantNotFoundException;
@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
private static final Logger logger = CloudLoggerFactory.getLogger(TenantIdentifierResolver.class);
@Value("${multitenant.defaultTenant}")
String defaultTenant;
@Override
public String resolveCurrentTenantIdentifier() {
try {
return TenantAccessor.getCurrentTenant().getTenantId();
} catch (TenantNotFoundException e) {
logger.warn("Tenant not found", e);
return defaultTenant;
}
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
multitenant:
defaultTenant: public
After the Hibernate configuration is done, we can move forward with the first services supported by the application.
When you develop a multitenant application for SAP Cloud Platform, Cloud Foundry, you will need to create a service for tenant onboarding and offboarding that will be called by SAP Cloud Platfrom each time, when an account subscribes your application.
While publishing your multitenant application, you will need to register your application callback URL that will be called when a consumer tenant subscribes/unsubscribes to an application; callback URL has to comply to following convention: <some-url-path>/<tenantId> and must implement PUT (=subscribe) and DELETE (=unsubscribe).
When we implement schema separated multitemancy, this service needs to take care of creation and cleaning up database schemas for corresponding tenants. Let us provide a simple implementation of such services in our project.
First of all, let us build a tenant provisioning controller that will handle corresponding HTTP requests:
package com.mycompany.controllers;
import com.mycompany.service.TenantProvisioningService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.annotation.RequestScope;
@Component
@RestController
@RequestScope
@RequestMapping(path = "/callback/tenant")
public class TenantProvisioningController {
private static final Logger logger = LoggerFactory.getLogger(CostCenterController.class);
@Autowired
TenantProvisioningService tenantProvisioningService;
@PutMapping("/{tenantId}")
public void subscribeTenant(@PathVariable(value = "tenantId") String tenantId){
logger.info("Tenant callback service was called with method PUT for tenant {}.", tenantId);
tenantProvisioningService.subscribeTenant(tenantId);
}
@DeleteMapping("/{tenantId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void unsubscribeTenant(@PathVariable(value = "tenantId") String tenantId) {
logger.info("Tenant callback service was called with method DELETE for tenant {}.", tenantId);
tenantProvisioningService.unsubscribeTenant(tenantId);
}
}
package com.mycompany.service;
public interface TenantProvisioningService {
void subscribeTenant(String tenantId);
void unsubscribeTenant(String tenantId);
}
package com.mycompany.service;
import com.mycompany.util.TenantUtil;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import javax.ws.rs.BadRequestException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.regex.Pattern;
@Service
public class DefaultTenantProvisioningService implements TenantProvisioningService {
public static final String LIQUIBASE_PATH = "db/changelog/db.changelog-master.yaml";
@Autowired
private DataSource dataSource;
private static final Pattern TENANT_PATTERN = Pattern.compile("[-\\w]+");
private static final Logger logger = LoggerFactory.getLogger(DefaultTenantProvisioningService.class);
@Override
public void subscribeTenant(final String tenantId) {
String defaultSchemaName;
try {
Validate.isTrue(isValidTenantId(tenantId), String.format("Invalid tenant id: \"%s\"", tenantId));
final String schemaName = TenantUtil.createSchemaName(tenantId);
final Connection connection = dataSource.getConnection();
final Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
try (Statement statement = connection.createStatement()) {
statement.execute(String.format("CREATE SCHEMA IF NOT EXISTS \"%s\"", schemaName));
connection.commit();
defaultSchemaName = database.getDefaultSchemaName();
database.setDefaultSchemaName(schemaName);
final String filePath = LIQUIBASE_PATH;
final Liquibase liquibase = new liquibase.Liquibase(filePath,
new ClassLoaderResourceAccessor(), database);
liquibase.update(new Contexts(), new LabelExpression());
database.setDefaultSchemaName(defaultSchemaName);
}
} catch (SQLException | LiquibaseException | IllegalArgumentException e) {
final BadRequestException badRequestException = new BadRequestException();
logger.error("Tenant subscription failed for {}.", tenantId, e);
throw badRequestException;
}
}
@Override
public void unsubscribeTenant(final String tenantId) {
try {
Validate.isTrue(isValidTenantId(tenantId), String.format("Invalid tenant id: \"%s\"", tenantId));
final String schemaName = TenantUtil.createSchemaName(tenantId);
final Connection connection = dataSource.getConnection();
try (Statement statement = connection.createStatement()) {
statement.execute(String.format("DROP SCHEMA IF EXISTS \"%s\" CASCADE", schemaName));
}
} catch (SQLException | IllegalArgumentException e) {
final BadRequestException badRequestException = new BadRequestException();
logger.error("Tenant unsubscription failed for {}.", tenantId, e);
throw badRequestException;
}
}
private boolean isValidTenantId(final String tenantId) {
return tenantId != null && TENANT_PATTERN.matcher(tenantId).matches();
}
}
databaseChangeLog:
- changeSet:
id: 1
author: myuser
changes:
- createTable:
tableName: costcenterforecast
columns:
- column:
name: name
type: varchar(100)
constraints:
primaryKey: true
nullable: false
- column:
name: forecast
type: float
package com.mycompany.models;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CostCenterRepository extends CrudRepository<CostCenterForecast, Long> {}
package com.mycompany.models;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Table( name = "CostCenterForecast" )
@Entity
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Accessors(chain = true)
public class CostCenterForecast
{
@Id
@Column( name = "NAME", length = 100 )
@Getter
@Setter
private String name;
@Column( name = "FORECAST" )
@Getter
@Setter
private Double forecast;
}
package com.mycompany.controllers;
import com.google.common.collect.Lists;
import com.mycompany.models.CostCenterForecast;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.CrudRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class CostCenterController
{
@Autowired
CrudRepository costCenterRepository;
@RequestMapping(path = "/cost-center", method = RequestMethod.GET)
public ResponseEntity<List<CostCenterForecast>> getCostCenters(){
final List<CostCenterForecast> costCenters = Lists.newArrayList(costCenterRepository.findAll());
return ResponseEntity.ok(costCenters);
}
@RequestMapping(path = "/cost-center", method = RequestMethod.POST)
public ResponseEntity<List<CostCenterForecast>> postCostCenter(@RequestBody CostCenterForecast costCenter){
costCenterRepository.save(costCenter);
final List<CostCenterForecast> costCenters = Lists.newArrayList(costCenterRepository.findAll());
return ResponseEntity.ok(costCenters);
}
}
<!-- PostgreSQL driver that will be used for local testing -->
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4.1208-jdbc42-atlassian-hosted</version>
</dependency>
logging:
level:
com.mycompany: DEBUG
com.sap.cloud.sdk: INFO
root: WARN
server:
port: 8080
multitenant:
defaultTenant: public
spring:
jpa:
generate-ddl: true
database-platform: org.hibernate.dialect.PostgreSQLDialect
datasource:
url: jdbc:postgresql://localhost:<your postgresql port>/<your db name>
username: <your user>
password: <your password>
package com.mycompany;
import org.apache.commons.io.IOUtils;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import com.sap.cloud.sdk.cloudplatform.servlet.Executable;
import com.sap.cloud.sdk.testutil.MockUtil;
import static java.lang.Thread.currentThread;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class HelloWorldServiceTest
{
private static final MockUtil mockUtil = new MockUtil();
@Autowired
private MockMvc mvc;
@BeforeClass
public static void beforeClass()
{
mockUtil.mockDefaults();
}
@Test
public void test() throws Exception
{
mockUtil.requestContextExecutor().execute(new Executable()
{
@Override
public void execute() throws Exception
{
mvc.perform(MockMvcRequestBuilders.get("/hello"))
.andExpect(status().isOk())
.andExpect(content().json(
IOUtils.toString(
currentThread().getContextClassLoader().getResourceAsStream("expected.json"))));
}
});
}
}
package com.mycompany;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.mycompany.models.CostCenterForecast;
import com.mycompany.models.CostCenterRepository;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.sap.cloud.sdk.cloudplatform.servlet.Executable;
import com.sap.cloud.sdk.testutil.MockUtil;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CostCenterServiceIntegrationTest
{
private static final String COSTCENTER_ID_1 = "name1";
private static final String COSTCENTER_ID_2 = "name2";
private static final String TENANT_ID_1 = "tenant1";
private static final String TENANT_ID_2 = "tenant2";
private static final MockUtil mockSdk = new MockUtil();
public static final double FORECAST = 50.0;
@Autowired
private MockMvc mockMvc;
@Autowired
private CostCenterRepository costCenterRepository;
@BeforeClass
public static void beforeClass() {
mockSdk.mockDefaults();
}
@Before
public void before() {
mockSdk.mockCurrentTenant(TENANT_ID_1);
}
@Test
public void testHttpGet() throws Exception {
mockSdk.requestContextExecutor().execute(new Executable() {
@Override
public void execute() throws Exception {
ResultActions action = mockMvc.perform(MockMvcRequestBuilders
.put("/callback/tenant/" + TENANT_ID_1));
action.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
action = mockMvc.perform(MockMvcRequestBuilders
.get("/cost-center"));
action.andExpect(MockMvcResultMatchers.status().isOk());
action = mockMvc.perform(MockMvcRequestBuilders
.delete("/callback/tenant/" + TENANT_ID_1));
action.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
}
});
}
@Test
public void testHttpPost() throws Exception {
final String newCostCenterJson = buildCostCenterJson(COSTCENTER_ID_1);
mockSdk.requestContextExecutor().execute(new Executable() {
@Override
public void execute() throws Exception {
ResultActions action = mockMvc.perform(MockMvcRequestBuilders
.put("/callback/tenant/" + TENANT_ID_1));
action.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
action = mockMvc
.perform(MockMvcRequestBuilders
.request(HttpMethod.POST, "/cost-center")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(newCostCenterJson));
action.andExpect(MockMvcResultMatchers.status().isOk());
action = mockMvc.perform(MockMvcRequestBuilders
.delete("/callback/tenant/" + TENANT_ID_1));
action.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
}
});
}
@Test
public void testWithTwoTenants() throws Exception {
// onboard and create data
mockSdk.mockCurrentTenant(TENANT_ID_1);
mockSdk.requestContextExecutor().execute(new Executable() {
@Override
public void execute() throws Exception {
onboardTenant(TENANT_ID_1);
createDataInTenant(COSTCENTER_ID_1);
}
});
mockSdk.mockCurrentTenant(TENANT_ID_2);
mockSdk.requestContextExecutor().execute(new Executable() {
@Override
public void execute() throws Exception {
onboardTenant(TENANT_ID_2);
createDataInTenant(COSTCENTER_ID_2);
}
});
// read and validate data
mockSdk.mockCurrentTenant(TENANT_ID_1);
mockSdk.requestContextExecutor().execute(new Executable() {
@Override
public void execute() throws Exception {
readAndValidateDataInTenant(COSTCENTER_ID_1);
}
});
mockSdk.mockCurrentTenant(TENANT_ID_2);
mockSdk.requestContextExecutor().execute(new Executable() {
@Override
public void execute() throws Exception {
readAndValidateDataInTenant(COSTCENTER_ID_2);
}
});
mockSdk.requestContextExecutor().execute(new Executable() {
@Override
public void execute() throws Exception {
offboardTenant(TENANT_ID_1);
offboardTenant(TENANT_ID_2);
}
});
}
private void offboardTenant(final String tenant) throws Exception {
ResultActions action = mockMvc.perform(MockMvcRequestBuilders
.delete("/callback/tenant/" + tenant));
action.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
}
private void readAndValidateDataInTenant(final String costCenter) throws Exception {
ResultActions action = mockMvc.perform(MockMvcRequestBuilders
.get("/cost-center"));
action.andExpect(MockMvcResultMatchers.status().isOk());
final String result = action.andReturn().getResponse().getContentAsString();
final String expected = new Gson().toJson(Lists.newArrayList(new CostCenterForecast(costCenter, FORECAST)));
Assert.assertEquals(expected, result);
}
private void createDataInTenant(String costCenter) throws Exception {
final String newCostCenterJson = buildCostCenterJson(costCenter);
ResultActions action = mockMvc
.perform(MockMvcRequestBuilders
.request(HttpMethod.POST, "/cost-center")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(newCostCenterJson));
action.andExpect(MockMvcResultMatchers.status().isOk());
}
private void onboardTenant(String tenant) throws Exception {
ResultActions action = mockMvc.perform(MockMvcRequestBuilders
.put("/callback/tenant/" + tenant));
action.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
}
private String buildCostCenterJson(String costCenterName) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(new CostCenterForecast()
.setName(costCenterName).setForecast(FORECAST));
}
}
testHttpGet | Validates GET method on the cost center endpoint: This method first onboards a tenant and then verifies that HTTP GET method on the cost center endpoint returns success. After that, the corresponding tenant offboarded. |
testHttpPost | Validates POST method on the cost center endpoint: This method first onboards a tenant and then verifies that HTTP POST method on the cost center endpoint returns success. After that, the corresponding tenant offboarded. |
testWithTwoTenants | Validates tenant isolation: This method onboards two tenants and creates tenant-isolated data, then it read the data for each tenant ensuring the isolation, and finally, it offboards the tenants. |
mvn clean package
package com.mycompany.config;
import org.springframework.cloud.config.java.ServiceScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("cloud")
@ServiceScan
public class CloudConfig {
}
---
applications:
- name: persistence
memory: 768M
host: persistence-i042557
path: application/target/persistence-application.jar
buildpack: sap_java_buildpack
env:
TARGET_RUNTIME: main
JBP_CONFIG_SAPJVM_MEMORY_SIZES: 'metaspace:96m..'
SPRING_PROFILES_ACTIVE: cloud
services:
- mydb
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2
http://www.springframework.org/schema/security/spring-security-oauth2-1.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<!-- protect secure resource endpoints ================================================ -->
<sec:http pattern="/**" create-session="never"
entry-point-ref="oauthAuthenticationEntryPoint"
access-decision-manager-ref="accessDecisionManager"
authentication-manager-ref="authenticationManager"
use-expressions="true">
<sec:anonymous enabled="false" />
<sec:csrf disabled="true"/>
<!-- section to protect your endpoints -->
<!-- Example: Check a specific OAuth Scope (i.e., authorization) on a resource -->
<!--<sec:intercept-url pattern="/hello" access="#oauth2.hasScope('${xs.appname}.Display')" method="GET" />-->
<!-- Example: Check only authentication on a resource -->
<sec:intercept-url pattern="/**" access="isAuthenticated()" method="GET" />
<sec:intercept-url pattern="/**" access="isAuthenticated()" method="PUT" />
<sec:intercept-url pattern="/**" access="isAuthenticated()" method="POST" />
<sec:intercept-url pattern="/**" access="isAuthenticated()" method="DELETE" />
<sec:custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
<sec:access-denied-handler ref="oauthAccessDeniedHandler" />
</sec:http>
<bean id="oauthAuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
</bean>
<bean id="oauthWebExpressionHandler"
class="org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityExpressionHandler">
</bean>
<bean id="accessDecisionManager"
class="org.springframework.security.access.vote.UnanimousBased">
<constructor-arg>
<list>
<bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
<property name="expressionHandler" ref="oauthWebExpressionHandler" />
</bean>
<bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
</list>
</constructor-arg>
</bean>
<sec:authentication-manager alias="authenticationManager"/>
<oauth:resource-server id="resourceServerFilter"
resource-id="springsec" token-services-ref="offlineTokenServices" />
<bean id="offlineTokenServices"
class="com.sap.xs2.security.commons.SAPOfflineTokenServices">
<property name="verificationKey" value="${xs.uaa.verificationkey}" />
<property name="trustedClientId" value="${xs.uaa.clientid}" />
<property name="trustedIdentityZone" value="${xs.uaa.identityzone}" />
</bean>
<bean id="oauthAccessDeniedHandler"
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />
<!-- define properties file =========================================================== -->
<bean class="com.sap.xs2.security.commons.SAPPropertyPlaceholderConfigurer">
<property name="location" value="classpath:/application.properties" />
</bean>
</beans>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
xs.appname=${artifactId}
logging.level.com.mycompany=DEBUG
logging.level.com.sap.cloud.sdk=INFO
logging.level.root=WARN
server.port=8080
multitenant.defaultTenant=public
spring.jpa.generate-ddl=true
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.url=jdbc:postgresql://localhost:5432/<local db name>
spring.datasource.username=<local db username>
spring.datasource.password=<local db password>
@ImportResource("classpath:/spring/spring-security.xml")
package com.mycompany.config;
import org.springframework.cloud.config.java.ServiceScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("cloud")
@ImportResource("classpath:/spring/spring-security.xml")
@ServiceScan
public class CloudConfig {
}
package com.mycompany.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
@Profile("test")
public class TestConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll();
http.csrf().disable();
}
}
@ActiveProfiles("test")
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
11 | |
6 | |
5 | |
4 | |
4 | |
4 | |
4 | |
3 | |
3 | |
3 |