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