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

com.sap.cds.adapter.subscription.SaasProvisioningServlet 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.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);
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy