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(
72              new ErrorResponse()
73                  .message(ex.getReason() != null ? ex.getReason() : ex.getBody().getTitle())
74                  .code(ex.getStatusCode().value()));
75    }
76  
77    @ModelAttribute
78    public ViewerResponse.KindEnum populateViewerKind(HttpServletRequest request) {
79      if (request.getServletPath().startsWith(basePath + "/app/")) {
80        return ViewerResponse.KindEnum.APP;
81      } else if (request.getServletPath().startsWith(basePath + "/service/")) {
82        return ViewerResponse.KindEnum.SERVICE;
83      } else {
84        return null;
85      }
86    }
87  
88    @ModelAttribute
89    public Application populateApplication(
90        @ModelAttribute ViewerResponse.KindEnum viewerKind,
91        @PathVariable(required = false) String viewerName,
92        @RequestParam(required = false) String base,
93        @RequestParam(required = false) String projection) {
94      if (viewerKind == null || viewerName == null) {
95        // No binding required for ViewerController.defaultApp()
96        return null;
97      }
98  
99      Application app;
100     if (viewerKind == ViewerResponse.KindEnum.APP) {
101       app = applicationRepository.findByName(viewerName);
102       if (app == null) {
103         throw new ResponseStatusException(HttpStatus.NOT_FOUND);
104       }
105     } else if (viewerKind == ViewerResponse.KindEnum.SERVICE) {
106       GeoService service = geoServiceRepository.findById(viewerName).orElse(null);
107 
108       if (service == null) {
109         throw new ResponseStatusException(HttpStatus.NOT_FOUND);
110       }
111 
112       if (!authorizationService.mayUserRead(service)) {
113         throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
114       }
115 
116       // TODO: skip this check for users with admin role
117       if (!service.isPublished()) {
118         throw new ResponseStatusException(HttpStatus.NOT_FOUND);
119       }
120       app = applicationHelper.getServiceApplication(base, projection, service);
121     } else {
122       throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
123     }
124 
125     if (!this.authorizationService.mayUserRead(app)) {
126       throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
127     }
128     return app;
129   }
130 
131   @ModelAttribute
132   public AppTreeLayerNode populateAppTreeLayerNode(
133       @ModelAttribute Application app, @PathVariable(required = false) String appLayerId) {
134     if (app == null || appLayerId == null) {
135       // No binding
136       return null;
137     }
138 
139     final AppTreeLayerNode layerNode =
140         app.getAllAppTreeLayerNode()
141             .filter(r -> r.getId().equals(appLayerId))
142             .findFirst()
143             .orElse(null);
144     if (layerNode == null) {
145       throw new ResponseStatusException(
146           HttpStatus.NOT_FOUND, "Application layer with id " + appLayerId + " not found");
147     }
148 
149     // TODO
150     //    if (!this.authorizationService.mayUserRead(applicationLayer, application)) {
151     //      throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
152     //    }
153     return layerNode;
154   }
155 
156   @ModelAttribute
157   public GeoService populateGeoService(
158       @ModelAttribute Application app, @ModelAttribute AppTreeLayerNode appTreeLayerNode) {
159     if (appTreeLayerNode == null) {
160       // No binding
161       return null;
162     }
163     if (appTreeLayerNode.getServiceId() == null) {
164       return null;
165     }
166     GeoService service =
167         geoServiceRepository.findById(appTreeLayerNode.getServiceId()).orElse(null);
168     if (service != null && !authorizationService.mayUserRead(service)) {
169       throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
170     }
171 
172     if (service != null && authorizationService.mustDenyAccessForSecuredProxy(app, service)) {
173       throw new ResponseStatusException(HttpStatus.FORBIDDEN);
174     }
175 
176     return service;
177   }
178 
179   @ModelAttribute
180   public GeoServiceLayer populateGeoServiceLayer(
181       @ModelAttribute AppTreeLayerNode appTreeLayerNode, @ModelAttribute GeoService service) {
182     if (service == null) {
183       // No binding
184       return null;
185     }
186     GeoServiceLayer layer =
187         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 }