AppRestControllerAdvice.java
/*
* Copyright (C) 2022 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import org.tailormap.api.annotation.AppRestController;
import org.tailormap.api.persistence.Application;
import org.tailormap.api.persistence.GeoService;
import org.tailormap.api.persistence.helper.ApplicationHelper;
import org.tailormap.api.persistence.json.AppTreeLayerNode;
import org.tailormap.api.persistence.json.GeoServiceLayer;
import org.tailormap.api.repository.ApplicationRepository;
import org.tailormap.api.repository.GeoServiceRepository;
import org.tailormap.api.security.AuthorisationService;
import org.tailormap.api.viewer.model.ErrorResponse;
import org.tailormap.api.viewer.model.RedirectResponse;
import org.tailormap.api.viewer.model.ViewerResponse;
@RestControllerAdvice(annotations = AppRestController.class)
public class AppRestControllerAdvice {
private final ApplicationRepository applicationRepository;
private final GeoServiceRepository geoServiceRepository;
private final ApplicationHelper applicationHelper;
private final AuthorisationService authorisationService;
@Value("${tailormap-api.base-path}")
private String basePath;
public AppRestControllerAdvice(
ApplicationRepository applicationRepository,
GeoServiceRepository geoServiceRepository,
ApplicationHelper applicationHelper,
AuthorisationService authorisationService) {
this.applicationRepository = applicationRepository;
this.geoServiceRepository = geoServiceRepository;
this.applicationHelper = applicationHelper;
this.authorisationService = authorisationService;
}
@InitBinder
protected void initBinder(WebDataBinder binder) {
// WARNING! These fields must NOT match properties of ModelAttribute classes, otherwise they
// will be overwritten (for instance GeoServiceLayer.name might be set to the app name)
binder.setAllowedFields("viewerName", "appLayerId", "base", "projection");
}
@ExceptionHandler(ResponseStatusException.class)
protected ResponseEntity<?> handleResponseStatusException(ResponseStatusException ex) {
if (HttpStatus.UNAUTHORIZED.equals(ex.getStatusCode())) {
return ResponseEntity.status(ex.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(new RedirectResponse());
}
return ResponseEntity.status(ex.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(new ErrorResponse()
.message(
ex.getReason() != null
? ex.getReason()
: ex.getBody().getTitle())
.code(ex.getStatusCode().value()));
}
@ModelAttribute
public ViewerResponse.KindEnum populateViewerKind(HttpServletRequest request) {
if (request.getServletPath().startsWith(basePath + "/app/")) {
return ViewerResponse.KindEnum.APP;
} else if (request.getServletPath().startsWith(basePath + "/service/")) {
return ViewerResponse.KindEnum.SERVICE;
} else {
return null;
}
}
@ModelAttribute
public Application populateApplication(
@ModelAttribute ViewerResponse.KindEnum viewerKind,
@PathVariable(required = false) String viewerName,
@RequestParam(required = false) String base,
@RequestParam(required = false) String projection) {
if (viewerKind == null || viewerName == null) {
// No binding required for ViewerController.defaultApp()
return null;
}
Application app;
if (viewerKind == ViewerResponse.KindEnum.APP) {
app = applicationRepository.findByName(viewerName);
if (app == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
} else if (viewerKind == ViewerResponse.KindEnum.SERVICE) {
GeoService service = geoServiceRepository.findById(viewerName).orElse(null);
if (service == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
if (!authorisationService.userAllowedToViewGeoService(service)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
// TODO: skip this check for users with admin role
if (!service.isPublished()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
app = applicationHelper.getServiceApplication(base, projection, service);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
if (!this.authorisationService.userAllowedToViewApplication(app)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
return app;
}
@ModelAttribute
public AppTreeLayerNode populateAppTreeLayerNode(
@ModelAttribute Application app, @PathVariable(required = false) String appLayerId) {
if (app == null || appLayerId == null) {
// No binding
return null;
}
final AppTreeLayerNode layerNode = app.getAllAppTreeLayerNode()
.filter(r -> r.getId().equals(appLayerId))
.findFirst()
.orElse(null);
if (layerNode == null) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Application layer with id " + appLayerId + " not found");
}
// TODO
// if (!this.authorizationService.userAllowedToViewApplication(applicationLayer, application)) {
// throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
// }
return layerNode;
}
@ModelAttribute
public GeoService populateGeoService(@ModelAttribute AppTreeLayerNode appTreeLayerNode) {
if (appTreeLayerNode == null) {
// No binding
return null;
}
if (appTreeLayerNode.getServiceId() == null) {
return null;
}
GeoService service =
geoServiceRepository.findById(appTreeLayerNode.getServiceId()).orElse(null);
if (service == null) {
return null;
}
if (authorisationService.mustDenyAccessForSecuredProxy(service)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
if (!authorisationService.userAllowedToViewGeoService(service)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
return service;
}
@ModelAttribute
public GeoServiceLayer populateGeoServiceLayer(
@ModelAttribute AppTreeLayerNode appTreeLayerNode, @ModelAttribute GeoService service) {
if (service == null) {
// No binding
return null;
}
GeoServiceLayer layer = service.getLayers().stream()
.filter(l -> appTreeLayerNode.getLayerName().equals(l.getName()))
.findFirst()
.orElse(null);
if (layer != null && !authorisationService.userAllowedToViewGeoServiceLayer(service, layer)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
return layer;
}
}