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.sms.SmsProvisioningServlet Maven / Gradle / Ivy
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.adapter.sms;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cds.feature.mt.ExecutorUtils;
import com.sap.cds.feature.mt.SmsClient;
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.mt.SmsCallback;
import com.sap.cds.services.mt.SmsSubscriptionRequest;
import com.sap.cds.services.mt.SmsUnsubscriptionRequest;
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.environment.servicebinding.api.ServiceBinding;
import com.sap.cloud.mt.subscription.UiUrlCreator;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Servlet providing the Subscription Manager Service (SMS) subscription endpoints for IAS tenants.
*/
public class SmsProvisioningServlet extends HttpServlet {
private final ObjectMapper mapper = new ObjectMapper();
private static final Logger logger = LoggerFactory.getLogger(SmsProvisioningServlet.class);
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 static final String HEADER_STATUS_CALLBACK = "STATUS_CALLBACK";
private final CdsRuntime runtime;
private final DeploymentService deploymentService;
private final SmsClient sms;
private final CertValidator certValidator;
public SmsProvisioningServlet(CdsRuntime runtime) {
this.runtime = runtime;
this.deploymentService = runtime.getServiceCatalog().getService(DeploymentService.class,
DeploymentService.DEFAULT_NAME);
ServiceBinding smsBinding = SmsClient.findBinding(runtime).orElse(null);
this.sms = smsBinding != null ? new SmsClient(smsBinding) : null;
this.certValidator = smsBinding != null ? CertValidator.create(runtime, smsBinding) : null;
}
/*
* Endpoint for getting the dependencies: GET https:////{app_tid}/dependencies.
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
processRequest(req, res, p -> p.endsWith(DEPENDENCIES), () -> {
List> dependencies = deploymentService.dependencies();
setContentType(res, ContentType.APPLICATION_JSON);
res.setStatus(HttpServletResponse.SC_OK);
res.getWriter().write(mapper.writeValueAsString(dependencies));
});
}
/*
* Endpoint for subscription: PUT https:////tenants/{app_tid}.
*/
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res) {
processRequest(req, res, p -> p.startsWith(TENANTS), () -> {
String tenantId = getTenantId(req);
logger.info("Subscribing IAS tenant '{}'", tenantId);
SmsSubscriptionRequest subReq = SmsSubscriptionRequest.create(toMap(req.getInputStream()));
String callbackUrl = req.getHeader(HEADER_STATUS_CALLBACK);
String appUiUrl = getAppUiUrl(subReq);
if (callbackUrl != null) {
if (sms == null) {
logger.error("Asynchronous callbacks to Subscription Manager Service require a subscription-manager binding.");
throw new ErrorStatusException(ErrorStatuses.SERVER_ERROR);
}
logger.debug("Processing subscription for IAS tenant '{}' asynchronously", tenantId);
ExecutorUtils.runAsynchronously(runtime, () -> {
boolean success = false;
try {
deploymentService.subscribe(tenantId, subReq);
success = true;
logger.info("Subscription for IAS tenant '{}' finished successfully", tenantId);
} catch (Throwable e) {
logger.error("Subscription for IAS tenant '{}' failed", tenantId, e);
}
try {
SmsCallback callback = SmsCallback.create();
callback.setApplicationUrl(appUiUrl);
if (success) {
callback.setStatus("SUCCEEDED");
callback.setMessage("Subscription succeeded");
} else {
callback.setStatus("FAILED");
callback.setMessage("Subscription failed");
}
sms.putRequest(callbackUrl, mapper.convertValue(callback, JsonNode.class));
} catch (Throwable e) {
logger.error("Failed to report status for IAS tenant '{}' to Subscription Manager Service", tenantId, e);
}
});
res.setStatus(HttpServletResponse.SC_ACCEPTED);
} else {
logger.debug("Processing subscription for IAS tenant '{}' synchronously", tenantId);
deploymentService.subscribe(tenantId, subReq);
res.setStatus(HttpServletResponse.SC_OK); // not SC_CREATED!
setContentType(res, ContentType.TEXT_PLAIN);
Map response = new HashMap<>();
response.put("applicationURL", appUiUrl);
res.getWriter().write(mapper.writeValueAsString(response));
}
});
}
private Map parseQueryString(HttpServletRequest req) {
Map parsed = new HashMap<>();
String queryString = req.getQueryString();
String[] parameters = queryString.split("&");
for (String parameter : parameters) {
String[] keyVal = parameter.split("=");
if (keyVal.length == 2) {
String key = keyVal[0];
String val = keyVal[1];
if (val != null) {
parsed.put(key, val);
}
}
}
return parsed;
}
/*
* Endpoint for unsubscription: DELETE https:////tenants/{app_tid}.
*/
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse res) {
processRequest(req, res, p -> p.startsWith(TENANTS), () -> {
String tenantId = getTenantId(req);
logger.info("Unsubscribing IAS tenant '{}'", tenantId);
SmsUnsubscriptionRequest deleteSubRequest = SmsUnsubscriptionRequest.create(parseQueryString(req));
String callbackUrl = req.getHeader(HEADER_STATUS_CALLBACK);
if (callbackUrl != null) {
if (sms == null) {
logger.error("Asynchronous callbacks to SMS require an SMS binding.");
throw new ErrorStatusException(ErrorStatuses.SERVER_ERROR);
}
logger.debug("Processing unsubscription for tenant '{}' asynchronously", tenantId);
ExecutorUtils.runAsynchronously(runtime, () -> {
boolean success = false;
try {
deploymentService.unsubscribe(tenantId, deleteSubRequest);
success = true;
logger.info("Unsubscription for tenant '{}' finished successfully", tenantId);
} catch (Throwable e) {
logger.error("Unsubscription for tenant '{}' failed", tenantId, e);
}
try {
SmsCallback callback = SmsCallback.create();
if (success) {
callback.setStatus("SUCCEEDED");
callback.setMessage("Removing subscription succeeded");
} else {
callback.setStatus("FAILED");
callback.setMessage("Removing subscription failed");
}
sms.putRequest(callbackUrl, mapper.convertValue(callback, JsonNode.class));
} catch (Throwable e) {
logger.error("Failed to report status for tenant '{}' to Subscription Manager Service", tenantId, e);
}
});
res.setStatus(HttpServletResponse.SC_ACCEPTED);
} else {
logger.debug("Processing unsubscription for tenant '{}' synchronously", tenantId);
deploymentService.unsubscribe(tenantId, deleteSubRequest);
res.setStatus(HttpServletResponse.SC_OK); // not SC_NO_CONTENT
}
});
}
private void checkAuthorization() {
RequestContext requestContext = RequestContext.getCurrent(runtime);
if (requestContext.getUserInfo().isPrivileged() || requestContext.getUserInfo().isInternalUser()) {
return;
}
this.certValidator.validateCertFromRequestContext(requestContext);
}
private static String getTenantId(HttpServletRequest req) {
// gets the app_tid from the request path
String tenantId = req.getPathInfo().split("/")[2];
if(StringUtils.isEmpty(tenantId)) {
throw new ErrorStatusException(ErrorStatuses.BAD_REQUEST);
}
return tenantId;
}
private static void setContentType(HttpServletResponse resp, ContentType contType) {
resp.setContentType(contType.getMimeType());
resp.setCharacterEncoding(contType.getCharset().toString());
}
private Map toMap(InputStream stream) {
try {
TypeReference> typeRef = new TypeReference<>() {};
return mapper.readValue(stream, typeRef);
} catch (Exception e) {
throw new ErrorStatusException(ErrorStatuses.BAD_REQUEST, e);
}
}
// TODO Introduce custom handler, if required
private String getAppUiUrl(SmsSubscriptionRequest req) {
// configured URL
try {
AppUi appUi = runtime.getEnvironment().getCdsProperties().getMultiTenancy().getAppUi();
String configuredUrl = UiUrlCreator.createUrl(req.getSubscriber().getSubaccountSubdomain(), appUi.getUrl(), appUi.getTenantSeparator());
return StringUtils.isEmpty(configuredUrl) ? FALLBACK_APP_URL : configuredUrl;
} catch (InternalError e) {
logger.error("Failed to create app UI URL.", e);
throw new RuntimeException(e);
}
}
@FunctionalInterface
private interface Processor {
void process() throws IOException;
}
private static void handleException(HttpServletResponse res, Locale locale, 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(locale);
if (message != null) {
try (PrintWriter writer = res.getWriter()) {
writer.write(message);
}
}
} catch (IOException e1) {
logger.error("Failed to write error message to response", e1);
}
}
private void processRequest(HttpServletRequest req, HttpServletResponse res, Predicate pathMatcher, Processor processor) {
if (pathMatcher.test(req.getPathInfo())) {
try {
checkAuthorization();
} catch (ServiceException e) {
handleException(res, null, e);
return;
} catch (Throwable t) {
logger.error("Unexpected error", t);
res.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
return;
}
// privileged user required for MtSubscriptionServiceCompatibilityHandler#checkAuthorization(EventContext)
runtime.requestContext().systemUserProvider().privilegedUser()
.run(requestContext -> {
try {
processor.process();
} catch (ServiceException e) {
handleException(res, requestContext.getParameterInfo().getLocale(), e);
} catch (Throwable t) {
logger.error("Unexpected error", t);
res.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
}
});
} else {
res.setStatus(HttpStatus.SC_NOT_FOUND);
}
}
}