View Javadoc
1   /*
2    * Copyright (C) 2022 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.security;
7   
8   import java.lang.invoke.MethodHandles;
9   import java.util.ArrayList;
10  import java.util.HashSet;
11  import java.util.List;
12  import java.util.Optional;
13  import java.util.Set;
14  import org.slf4j.Logger;
15  import org.slf4j.LoggerFactory;
16  import org.springframework.security.authentication.AnonymousAuthenticationToken;
17  import org.springframework.security.core.Authentication;
18  import org.springframework.security.core.GrantedAuthority;
19  import org.springframework.security.core.context.SecurityContextHolder;
20  import org.springframework.stereotype.Service;
21  import org.tailormap.api.persistence.Application;
22  import org.tailormap.api.persistence.GeoService;
23  import org.tailormap.api.persistence.Group;
24  import org.tailormap.api.persistence.Page;
25  import org.tailormap.api.persistence.json.AuthorizationRule;
26  import org.tailormap.api.persistence.json.AuthorizationRuleDecision;
27  import org.tailormap.api.persistence.json.GeoServiceLayer;
28  import org.tailormap.api.persistence.json.GeoServiceLayerSettings;
29  import org.tailormap.api.persistence.json.PageTile;
30  
31  /**
32   * Validates access control rules. Any call to userAllowedToViewApplication will verify that the currently logged-in
33   * user is not only allowed to read the current object, but any object above and below it in the hierarchy.
34   */
35  @Service
36  public class AuthorisationService {
37    private static final Logger logger =
38        LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
39  
40    public static final String ACCESS_TYPE_VIEW = "read";
41  
42    private Optional<AuthorizationRuleDecision> isAuthorizedByRules(List<AuthorizationRule> rules) {
43      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
44      Set<String> groups;
45  
46      if (auth == null || auth instanceof AnonymousAuthenticationToken) {
47        groups = Set.of(Group.ANONYMOUS);
48      } else {
49        groups = new HashSet<>();
50        groups.add(Group.ANONYMOUS);
51        groups.add(Group.AUTHENTICATED);
52  
53        for (GrantedAuthority authority : auth.getAuthorities()) {
54          groups.add(authority.getAuthority());
55        }
56      }
57      logger.trace("Groups to check rules against: {}", groups);
58  
59      // Admins are allowed access to anything.
60      if (groups.contains(Group.ADMIN)) {
61        logger.trace(
62            "Returning {} because {} is allowed access to anything.",
63            AuthorizationRuleDecision.ALLOW,
64            Group.ADMIN);
65        return Optional.of(AuthorizationRuleDecision.ALLOW);
66      }
67  
68      boolean hasValidRule = false;
69  
70      for (AuthorizationRule rule : rules) {
71        if (logger.isTraceEnabled()) {
72          logger.trace("Checking rule: \n{} against groups {}.", rule, groups);
73        }
74  
75        boolean matchesGroup = groups.contains(rule.getGroupName());
76        if (!matchesGroup) {
77          continue;
78        }
79  
80        hasValidRule = true;
81  
82        AuthorizationRuleDecision value = rule.getDecisions().get(AuthorisationService.ACCESS_TYPE_VIEW);
83        if (value == null) {
84          logger.trace(
85              "No decision found for rule: \n{} and access: {}, returning <EMPTY>.",
86              rule,
87              AuthorisationService.ACCESS_TYPE_VIEW);
88          return Optional.empty();
89        }
90  
91        if (value.equals(AuthorizationRuleDecision.ALLOW)) {
92          logger.trace(
93              "Returning {} because rule: \n{} allows {} access for access: {}.",
94              value,
95              rule,
96              rule.getGroupName(),
97              AuthorisationService.ACCESS_TYPE_VIEW);
98          return Optional.of(value);
99        }
100     }
101 
102     if (hasValidRule) {
103       logger.trace(
104           "Returning {} because no valid rule allowed access for access: {}.",
105           AuthorizationRuleDecision.DENY,
106           AuthorisationService.ACCESS_TYPE_VIEW);
107       return Optional.of(AuthorizationRuleDecision.DENY);
108     }
109 
110     logger.trace(
111         "Returning <EMPTY> because no rules matched for access: {}.", AuthorisationService.ACCESS_TYPE_VIEW);
112     return Optional.empty();
113   }
114 
115   /**
116    * Verifies that the (authenticated) user may view/open the page.
117    *
118    * @param page the Page to check
119    * @return the result from the access control checks.
120    */
121   public boolean userAllowedToViewPage(Page page) {
122     logger.trace("Checking if user is allowed to view page {}.", page.getName());
123     final boolean allowed =
124         isAuthorizedByRules(page.getAuthorizationRules()).equals(Optional.of(AuthorizationRuleDecision.ALLOW));
125     logger.trace(
126         "User is{} allowed to view page: {} (isAuthorizedByRules={}).",
127         allowed ? "" : " not",
128         page.getName(),
129         allowed);
130     return allowed;
131   }
132 
133   /**
134    * Verifies that the (authenticated) user may view/open the page tile.
135    *
136    * @param pageTile the Page tile to check
137    * @return the result from the access control checks.
138    */
139   public boolean userAllowedToViewPageTile(PageTile pageTile) {
140     logger.trace("Checking if user is allowed to view page tile {}.", pageTile.getTitle());
141     final boolean allowed = isAuthorizedByRules(pageTile.getAuthorizationRules())
142         .equals(Optional.of(AuthorizationRuleDecision.ALLOW));
143     logger.trace(
144         "User is{} allowed to view page tile: {} (isAuthorizedByRules={}).",
145         allowed ? "" : " not",
146         pageTile.getTitle(),
147         allowed);
148     return allowed;
149   }
150 
151   /**
152    * Verifies that the (authenticated) user may view/open the application.
153    *
154    * @param application the Application to check
155    * @return the result from the access control checks.
156    */
157   public boolean userAllowedToViewApplication(Application application) {
158     logger.trace(
159         "Checking if user is allowed to view Application {} ({}).",
160         application.getTitle(),
161         application.getTitle());
162     final boolean allowed = isAuthorizedByRules(application.getAuthorizationRules())
163         .equals(Optional.of(AuthorizationRuleDecision.ALLOW));
164     logger.trace(
165         "User is{} allowed to view application: {} (isAuthorizedByRules={}).",
166         allowed ? "" : " not",
167         application.getName(),
168         allowed);
169     return allowed;
170   }
171 
172   /**
173    * Verifies that the (authenticated) user may view this geoService.
174    *
175    * @param geoService the GeoService to check
176    * @return the result from the access control checks.
177    */
178   public boolean userAllowedToViewGeoService(GeoService geoService) {
179     logger.trace(
180         "Checking if user is allowed to view GeoService {} ({}).", geoService.getId(), geoService.getTitle());
181     if (mustDenyAccessForSecuredProxy(geoService)) {
182       return false;
183     }
184     final boolean allowed = isAuthorizedByRules(geoService.getAuthorizationRules())
185         .equals(Optional.of(AuthorizationRuleDecision.ALLOW));
186     logger.trace(
187         "User is{} allowed to view GeoService: {} (isAuthorizedByRules={}).",
188         allowed ? "" : " not",
189         geoService.getTitle(),
190         allowed);
191     return allowed;
192   }
193 
194   /**
195    * Verifies that the (authenticated) user may view the layer in context of the geoService.
196    *
197    * @param geoService the GeoService to check
198    * @param layer the GeoServiceLayer to check
199    * @return the result from the access control checks.
200    */
201   public boolean userAllowedToViewGeoServiceLayer(GeoService geoService, GeoServiceLayer layer) {
202     logger.trace(
203         "Checking if user is allowed to view GeoService '{}' and layer {} ({}).",
204         geoService.getTitle(),
205         layer.getName(),
206         layer.getTitle());
207     // check if user is allowed to view the geoService
208     Optional<AuthorizationRuleDecision> geoserviceDecision =
209         isAuthorizedByRules(geoService.getAuthorizationRules());
210     if (geoserviceDecision.equals(Optional.of(AuthorizationRuleDecision.DENY))) {
211       logger.trace("Viewing GeoService {} is denied for user.", geoService.getTitle());
212       return false;
213     }
214 
215     GeoServiceLayerSettings layerSettings =
216         geoService.getSettings().getLayerSettings().get(layer.getName());
217     if (layerSettings != null && layerSettings.getAuthorizationRules() != null) {
218       logger.trace(
219           "Checking layer settings rules for GeoService '{}' and layer '{}'. \nRules: {}",
220           geoService.getTitle(),
221           layer.getName(),
222           layerSettings.getAuthorizationRules());
223       List<AuthorizationRule> combinedRules = new ArrayList<>(geoService.getAuthorizationRules());
224       for (AuthorizationRule rule : layerSettings.getAuthorizationRules()) {
225         // replace any rule with the same group name, so we end up with a merged
226         // set of rules where the layer rules override
227         combinedRules.removeIf(r -> r.getGroupName().equals(rule.getGroupName()));
228         combinedRules.add(rule);
229       }
230       logger.trace(
231           "Combined rules for GeoService '{}' and layer '{}': \n{}",
232           geoService.getTitle(),
233           layer.getName(),
234           combinedRules);
235 
236       Optional<AuthorizationRuleDecision> decision = isAuthorizedByRules(combinedRules);
237       // If no authorization rules are present, fall back to geoService authorization.
238       if (decision.isPresent() || !layerSettings.getAuthorizationRules().isEmpty()) {
239         boolean allowed = decision.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
240 
241         logger.trace(
242             "Viewing GeoService '{}' and layer '{}' ({}) is {} for user.",
243             geoService.getTitle(),
244             layer.getName(),
245             layer.getTitle(),
246             (allowed ? "allowed" : "denied"));
247         return allowed;
248       }
249     }
250 
251     boolean allowed = geoserviceDecision.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
252     logger.trace(
253         "Viewing GeoService '{}' and layer '{}' ({}) is {} for user because service access is {3}.",
254         geoService.getTitle(), layer.getName(), layer.getTitle(), (allowed ? "allowed" : "denied"));
255     return allowed;
256   }
257 
258   /**
259    * To avoid exposing a secured service by proxying it to everyone, do not proxy a secured GeoService when the user
260    * is not logged in.
261    *
262    * @param geoService The geo service to check
263    * @return Whether to deny proxying this service
264    */
265   public boolean mustDenyAccessForSecuredProxy(GeoService geoService) {
266     if (!Boolean.TRUE.equals(geoService.getSettings().getUseProxy())) {
267       return false;
268     }
269     if (geoService.getAuthentication() == null) {
270       return false;
271     }
272     Authentication auth = SecurityContextHolder.getContext().getAuthentication();
273     return auth == null || auth instanceof AnonymousAuthenticationToken;
274   }
275 }