Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.adapter.subscription.SaasProvisioningServlet Maven / Gradle / Ivy
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.adapter.subscription;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cds.feature.mt.ExecutorUtils;
import com.sap.cds.feature.mt.SaasClient;
import com.sap.cds.mtx.impl.Authenticator;
import com.sap.cds.mtx.impl.ClientCredentialJwtAccess;
import com.sap.cds.mtx.impl.ClientCredentialJwtReader;
import com.sap.cds.mtx.impl.XsuaaParams;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.environment.CdsProperties.MultiTenancy.AppUi;
import com.sap.cds.services.mt.DeploymentService;
import com.sap.cds.services.request.FeatureTogglesInfo;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.services.utils.StringUtils;
import com.sap.cloud.mt.subscription.SaasRegistry;
import com.sap.cloud.mt.subscription.ServiceSpecification;
import com.sap.cloud.mt.subscription.UiUrlCreator;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.tools.api.RequestEnhancer;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultDestinationLoader;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Servlet providing the Saas Registry / CIS subscription endpoints.
*/
public class SaasProvisioningServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final ObjectMapper mapper = new ObjectMapper();
private static final Logger logger = LoggerFactory.getLogger(SaasProvisioningServlet.class);
public static final String HEADER_STATUS_CALLBACK = "STATUS_CALLBACK";
private static final String DEPENDENCIES = "/dependencies";
private static final String TENANTS = "/tenants/";
private static final String FALLBACK_APP_URL = "tenant successfully subscribed - no application URL provided";
private final CdsRuntime runtime;
private final DeploymentService deploymentService;
private final SaasRegistry saasRegistry;
public SaasProvisioningServlet(CdsRuntime runtime) {
this.runtime = runtime;
this.deploymentService = runtime.getServiceCatalog().getService(DeploymentService.class, DeploymentService.DEFAULT_NAME);
this.saasRegistry = SaasClient.findBinding(runtime).map((saasBinding) -> {
logger.info("Asynchronous subscription for saas-registry service '{}' available", saasBinding.getName().get());
Map credentials = saasBinding.getCredentials();
Authenticator authenticator = new ClientCredentialJwtAccess(new ClientCredentialJwtReader(new XsuaaParams(credentials)));
RequestEnhancer authenticationEnhancer = request -> request.addHeader(HttpHeaders.AUTHORIZATION, authenticator.getAuthorization().get());
String saasRegistryUrl = (String) credentials.get("saas_registry_url");
// TODO sync with other destinations and SaasClient
// DestinationResolver.getDestinationForXsuaaBasedServiceBinding(saasRegistryUrl, saasBinding, OnBehalfOf.TECHNICAL_USER_PROVIDER);
DefaultDestinationLoader loader = new DefaultDestinationLoader();
loader.registerDestination(DefaultHttpDestination.builder(saasRegistryUrl)
.name(SaasRegistry.SAAS_REGISTRY_DESTINATION)
.build());
DestinationAccessor.prependDestinationLoader(loader);
return new SaasRegistry(ServiceSpecification.Builder.create()
.requestEnhancer(authenticationEnhancer)
.build());
}).orElse(null);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
processRequest(req, res, p -> DEPENDENCIES.equals(p), () -> {
checkAuthorization();
List> dependencies = deploymentService.dependencies();
setContentType(res, ContentType.APPLICATION_JSON);
res.setStatus(HttpServletResponse.SC_OK);
res.getWriter().write(mapper.writeValueAsString(dependencies));
});
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res) {
processRequest(req, res, p -> p.startsWith(TENANTS), () -> {
checkAuthorization();
String tenantId = getTenantId(req);
logger.info("Creating subscription for tenant '{}'", tenantId);
Map payload = toMap(req.getInputStream());
String callbackUrl = req.getHeader(HEADER_STATUS_CALLBACK);
if (callbackUrl != null) {
logger.debug("Processing subscription for tenant '{}' asynchronously", tenantId);
long startTime = System.currentTimeMillis();
ExecutorUtils.runAsynchronously(runtime, () -> {
boolean success = false;
try {
deploymentService.subscribe(tenantId, payload);
success = true;
logger.info("Subscription for tenant '{}' finished successfully", tenantId);
} catch (Throwable e) {
logger.error("Subscription for tenant '{}' failed", tenantId, e);
}
try {
handleSaasRegistryRaceCondition(startTime);
saasRegistry.callBackSaasRegistry(success, success ? "Subscription succeeded" : "Subscription failed", getAppUiUrl(payload), callbackUrl);
} catch (Throwable e) {
logger.error("Failed to report status for tenant '{}' to SaaS Registry", tenantId, e);
}
});
res.setStatus(HttpServletResponse.SC_ACCEPTED);
} else {
logger.debug("Processing subscription for tenant '{}' synchronously", tenantId);
deploymentService.subscribe(tenantId, payload);
res.setStatus(HttpServletResponse.SC_CREATED);
setContentType(res, ContentType.TEXT_PLAIN);
res.getWriter().write(getAppUiUrl(payload));
}
});
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse res) {
processRequest(req, res, p -> p.startsWith(TENANTS), () -> {
checkAuthorization();
String tenantId = getTenantId(req);
logger.info("Deleting subscription for tenant '{}'", tenantId);
Map payload = toMap(req.getInputStream());
String callbackUrl = req.getHeader(HEADER_STATUS_CALLBACK);
if (callbackUrl != null) {
logger.debug("Processing unsubscription for tenant '{}' asynchronously", tenantId);
long startTime = System.currentTimeMillis();
ExecutorUtils.runAsynchronously(runtime, () -> {
boolean success = false;
try {
deploymentService.unsubscribe(tenantId, payload);
success = true;
logger.info("Unsubscription for tenant '{}' finished successfully", tenantId);
} catch (Throwable e) {
logger.error("Unsubscription for tenant '{}' failed", tenantId, e);
}
try {
handleSaasRegistryRaceCondition(startTime);
saasRegistry.callBackSaasRegistry(success, success ? "Removing subscription succeeded" : "Removing subscription failed", null, callbackUrl);
} catch (Throwable e) {
logger.error("Failed to report status for tenant '{}' to SaaS Registry", tenantId, e);
}
});
res.setStatus(HttpServletResponse.SC_ACCEPTED);
} else {
logger.debug("Processing unsubscription for tenant '{}' synchronously", tenantId);
deploymentService.unsubscribe(tenantId, payload);
res.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
});
}
private void checkAuthorization() {
RequestContext requestContext = RequestContext.getCurrent(runtime);
if (requestContext.getUserInfo().isPrivileged() || requestContext.getUserInfo().isInternalUser()) {
return;
}
String callbackScope = runtime.getEnvironment().getCdsProperties().getMultiTenancy().getSecurity().getSubscriptionScope();
if (!requestContext.getUserInfo().hasRole(callbackScope)) {
throw new ErrorStatusException(ErrorStatuses.FORBIDDEN);
}
}
private static String getTenantId(HttpServletRequest req) {
String tenantId = req.getPathInfo().split("/")[2];
if(StringUtils.isEmpty(tenantId)) {
throw new ErrorStatusException(ErrorStatuses.NOT_FOUND);
}
return tenantId;
}
private static void setContentType(HttpServletResponse resp, ContentType contType) {
resp.setContentType(contType.getMimeType());
resp.setCharacterEncoding(contType.getCharset().toString());
}
private static Map toMap(InputStream stream) throws IOException {
try {
TypeReference> typeRef = new TypeReference>() {};
return mapper.readValue(stream, typeRef);
} catch (DatabindException e) {
throw new ErrorStatusException(ErrorStatuses.BAD_REQUEST, e);
}
}
private static void handleSaasRegistryRaceCondition(long startTime) {
long raceConditionWaitTime = 10000; // 10s
long elapsed = System.currentTimeMillis() - startTime;
// process might have happened too fast, saas-registry might not yet be ready to accept status reports
if (elapsed < raceConditionWaitTime) {
try {
Thread.sleep(Math.max(raceConditionWaitTime - elapsed, 0));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// TODO Introduce custom handler, if required
private String getAppUiUrl(Map payload) {
// configured URL
try {
AppUi appUi = runtime.getEnvironment().getCdsProperties().getMultiTenancy().getAppUi();
String configuredUrl = UiUrlCreator.createUrl((String) payload.get("subscribedSubdomain"), appUi.getUrl(), appUi.getTenantSeparator());
return StringUtils.isEmpty(configuredUrl) ? FALLBACK_APP_URL : configuredUrl;
} catch (InternalError e) {
throw new RuntimeException(e);
}
}
@FunctionalInterface
private static interface Processor {
void process() throws IOException;
}
private void processRequest(HttpServletRequest req, HttpServletResponse res, Predicate pathMatcher, Processor processor) {
if(pathMatcher.test(req.getPathInfo())) {
//Cannot be switched to 'systemUserProvider()' yet due to internal communication with classic sidecar in MtDeploymentServiceHandler.forwardToken()
runtime.requestContext().modifyUser(user -> user.setTenant(null)).featureToggles(FeatureTogglesInfo.all()).run(requestContext -> {
try {
processor.process();
} catch (ServiceException e) {
if (e.getErrorStatus().getHttpStatus() >= 500 &&
e.getErrorStatus().getHttpStatus() < 600) {
logger.error("Unexpected error", e);
} else {
logger.debug("Service exception thrown", e);
}
res.setStatus(e.getErrorStatus().getHttpStatus());
try {
String message = e.getLocalizedMessage(requestContext.getParameterInfo().getLocale());
if (message != null) {
try (PrintWriter writer = res.getWriter()) {
writer.write(message);
}
}
} catch (IOException e1) {
logger.error("Failed to write error message to response", e1);
}
} catch (Throwable t) {
logger.error("Unexpected error", t);
res.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
}
});
} else {
res.setStatus(HttpStatus.SC_NOT_FOUND);
}
}
}