View Javadoc
1   /*
2    * Copyright (C) 2022 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.controller;
7   
8   import jakarta.servlet.http.HttpServletRequest;
9   import org.springframework.beans.factory.annotation.Value;
10  import org.springframework.http.HttpStatus;
11  import org.springframework.http.MediaType;
12  import org.springframework.http.ResponseEntity;
13  import org.springframework.web.bind.WebDataBinder;
14  import org.springframework.web.bind.annotation.ExceptionHandler;
15  import org.springframework.web.bind.annotation.InitBinder;
16  import org.springframework.web.bind.annotation.ModelAttribute;
17  import org.springframework.web.bind.annotation.PathVariable;
18  import org.springframework.web.bind.annotation.RequestParam;
19  import org.springframework.web.bind.annotation.RestControllerAdvice;
20  import org.springframework.web.server.ResponseStatusException;
21  import org.tailormap.api.annotation.AppRestController;
22  import org.tailormap.api.persistence.Application;
23  import org.tailormap.api.persistence.GeoService;
24  import org.tailormap.api.persistence.helper.ApplicationHelper;
25  import org.tailormap.api.persistence.json.AppTreeLayerNode;
26  import org.tailormap.api.persistence.json.GeoServiceLayer;
27  import org.tailormap.api.repository.ApplicationRepository;
28  import org.tailormap.api.repository.GeoServiceRepository;
29  import org.tailormap.api.security.AuthorizationService;
30  import org.tailormap.api.viewer.model.ErrorResponse;
31  import org.tailormap.api.viewer.model.RedirectResponse;
32  import org.tailormap.api.viewer.model.ViewerResponse;
33  
34  @RestControllerAdvice(annotations = AppRestController.class)
35  public class AppRestControllerAdvice {
36    private final ApplicationRepository applicationRepository;
37    private final GeoServiceRepository geoServiceRepository;
38    private final ApplicationHelper applicationHelper;
39    private final AuthorizationService authorizationService;
40  
41    @Value("${tailormap-api.base-path}")
42    private String basePath;
43  
44    public AppRestControllerAdvice(
45        ApplicationRepository applicationRepository,
46        GeoServiceRepository geoServiceRepository,
47        ApplicationHelper applicationHelper,
48        AuthorizationService authorizationService) {
49      this.applicationRepository = applicationRepository;
50      this.geoServiceRepository = geoServiceRepository;
51      this.applicationHelper = applicationHelper;
52      this.authorizationService = authorizationService;
53    }
54  
55    @InitBinder
56    protected void initBinder(WebDataBinder binder) {
57      // WARNING! These fields must NOT match properties of ModelAttribute classes, otherwise they
58      // will be overwritten (for instance GeoServiceLayer.name might be set to the app name)
59      binder.setAllowedFields("viewerName", "appLayerId", "base", "projection");
60    }
61  
62    @ExceptionHandler(ResponseStatusException.class)
63    protected ResponseEntity<?> handleResponseStatusException(ResponseStatusException ex) {
64      if (HttpStatus.UNAUTHORIZED.equals(ex.getStatusCode())) {
65        return ResponseEntity.status(ex.getStatusCode())
66            .contentType(MediaType.APPLICATION_JSON)
67            .body(new RedirectResponse());
68      }
69      return ResponseEntity.status(ex.getStatusCode())
70          .contentType(MediaType.APPLICATION_JSON)
71          .body(new ErrorResponse()
72              .message(
73                  ex.getReason() != null
74                      ? ex.getReason()
75                      : ex.getBody().getTitle())
76              .code(ex.getStatusCode().value()));
77    }
78  
79    @ModelAttribute
80    public ViewerResponse.KindEnum populateViewerKind(HttpServletRequest request) {
81      if (request.getServletPath().startsWith(basePath + "/app/")) {
82        return ViewerResponse.KindEnum.APP;
83      } else if (request.getServletPath().startsWith(basePath + "/service/")) {
84        return ViewerResponse.KindEnum.SERVICE;
85      } else {
86        return null;
87      }
88    }
89  
90    @ModelAttribute
91    public Application populateApplication(
92        @ModelAttribute ViewerResponse.KindEnum viewerKind,
93        @PathVariable(required = false) String viewerName,
94        @RequestParam(required = false) String base,
95        @RequestParam(required = false) String projection) {
96      if (viewerKind == null || viewerName == null) {
97        // No binding required for ViewerController.defaultApp()
98        return null;
99      }
100 
101     Application app;
102     if (viewerKind == ViewerResponse.KindEnum.APP) {
103       app = applicationRepository.findByName(viewerName);
104       if (app == null) {
105         throw new ResponseStatusException(HttpStatus.NOT_FOUND);
106       }
107     } else if (viewerKind == ViewerResponse.KindEnum.SERVICE) {
108       GeoService service = geoServiceRepository.findById(viewerName).orElse(null);
109 
110       if (service == null) {
111         throw new ResponseStatusException(HttpStatus.NOT_FOUND);
112       }
113 
114       if (!authorizationService.mayUserRead(service)) {
115         throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
116       }
117 
118       // TODO: skip this check for users with admin role
119       if (!service.isPublished()) {
120         throw new ResponseStatusException(HttpStatus.NOT_FOUND);
121       }
122       app = applicationHelper.getServiceApplication(base, projection, service);
123     } else {
124       throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
125     }
126 
127     if (!this.authorizationService.mayUserRead(app)) {
128       throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
129     }
130     return app;
131   }
132 
133   @ModelAttribute
134   public AppTreeLayerNode populateAppTreeLayerNode(
135       @ModelAttribute Application app, @PathVariable(required = false) String appLayerId) {
136     if (app == null || appLayerId == null) {
137       // No binding
138       return null;
139     }
140 
141     final AppTreeLayerNode layerNode = app.getAllAppTreeLayerNode()
142         .filter(r -> r.getId().equals(appLayerId))
143         .findFirst()
144         .orElse(null);
145     if (layerNode == null) {
146       throw new ResponseStatusException(
147           HttpStatus.NOT_FOUND, "Application layer with id " + appLayerId + " not found");
148     }
149 
150     // TODO
151     //    if (!this.authorizationService.mayUserRead(applicationLayer, application)) {
152     //      throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
153     //    }
154     return layerNode;
155   }
156 
157   @ModelAttribute
158   public GeoService populateGeoService(
159       @ModelAttribute Application app, @ModelAttribute AppTreeLayerNode appTreeLayerNode) {
160     if (appTreeLayerNode == null) {
161       // No binding
162       return null;
163     }
164     if (appTreeLayerNode.getServiceId() == null) {
165       return null;
166     }
167     GeoService service =
168         geoServiceRepository.findById(appTreeLayerNode.getServiceId()).orElse(null);
169     if (service != null && !authorizationService.mayUserRead(service)) {
170       throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
171     }
172 
173     if (service != null && authorizationService.mustDenyAccessForSecuredProxy(app, service)) {
174       throw new ResponseStatusException(HttpStatus.FORBIDDEN);
175     }
176 
177     return service;
178   }
179 
180   @ModelAttribute
181   public GeoServiceLayer populateGeoServiceLayer(
182       @ModelAttribute AppTreeLayerNode appTreeLayerNode, @ModelAttribute GeoService service) {
183     if (service == null) {
184       // No binding
185       return null;
186     }
187     GeoServiceLayer layer = service.getLayers().stream()
188         .filter(l -> appTreeLayerNode.getLayerName().equals(l.getName()))
189         .findFirst()
190         .orElse(null);
191 
192     if (layer != null && !authorizationService.mayUserRead(service, layer)) {
193       throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
194     }
195 
196     return layer;
197   }
198 }