All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.adapter.sms.SmsProvisioningServlet Maven / Gradle / Ivy

There is a newer version: 3.2.0
Show newest version
/**************************************************************************
 * (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);
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy