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     if (mustDenyAccessForSecuredProxy(geoService)) {
144       return false;
145     }
146     final boolean allowed = isAuthorizedByRules(geoService.getAuthorizationRules())
147         .equals(Optional.of(AuthorizationRuleDecision.ALLOW));
148     logger.trace(
149         "User is{} allowed to view GeoService: {} (isAuthorizedByRules={}).",
150         allowed ? "" : " not",
151         geoService.getTitle(),
152         allowed);
153     return allowed;
154   }
155 
156   /**
157    * Verifies that the (authenticated) user may view the layer in context of the geoService.
158    *
159    * @param geoService the GeoService to check
160    * @param layer the GeoServiceLayer to check
161    * @return the result from the access control checks.
162    */
163   public boolean userAllowedToViewGeoServiceLayer(GeoService geoService, GeoServiceLayer layer) {
164     logger.trace(
165         "Checking if user is allowed to view GeoService '{}' and layer {} ({}).",
166         geoService.getTitle(),
167         layer.getName(),
168         layer.getTitle());
169     // check if user is allowed to view the geoService
170     Optional<AuthorizationRuleDecision> geoserviceDecision =
171         isAuthorizedByRules(geoService.getAuthorizationRules());
172     if (geoserviceDecision.equals(Optional.of(AuthorizationRuleDecision.DENY))) {
173       logger.trace("Viewing GeoService {} is denied for user.", geoService.getTitle());
174       return false;
175     }
176 
177     GeoServiceLayerSettings layerSettings =
178         geoService.getSettings().getLayerSettings().get(layer.getName());
179     if (layerSettings != null && layerSettings.getAuthorizationRules() != null) {
180       logger.trace(
181           "Checking layer settings rules for GeoService '{}' and layer '{}'. \nRules: {}",
182           geoService.getTitle(),
183           layer.getName(),
184           layerSettings.getAuthorizationRules());
185       List<AuthorizationRule> combinedRules = new ArrayList<>(geoService.getAuthorizationRules());
186       for (AuthorizationRule rule : layerSettings.getAuthorizationRules()) {
187         // replace any rule with the same group name, so we end up with a merged
188         // set of rules where the layer rules override
189         combinedRules.removeIf(r -> r.getGroupName().equals(rule.getGroupName()));
190         combinedRules.add(rule);
191       }
192       logger.trace(
193           "Combined rules for GeoService '{}' and layer '{}': \n{}",
194           geoService.getTitle(),
195           layer.getName(),
196           combinedRules);
197 
198       Optional<AuthorizationRuleDecision> decision = isAuthorizedByRules(combinedRules);
199       // If no authorization rules are present, fall back to geoService authorization.
200       if (decision.isPresent() || !layerSettings.getAuthorizationRules().isEmpty()) {
201         boolean allowed = decision.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
202 
203         logger.trace(
204             "Viewing GeoService '{}' and layer '{}' ({}) is {} for user.",
205             geoService.getTitle(),
206             layer.getName(),
207             layer.getTitle(),
208             (allowed ? "allowed" : "denied"));
209         return allowed;
210       }
211     }
212 
213     boolean allowed = geoserviceDecision.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
214     logger.trace(
215         "Viewing GeoService '{}' and layer '{}' ({}) is {} for user because service access is {3}.",
216         geoService.getTitle(), layer.getName(), layer.getTitle(), (allowed ? "allowed" : "denied"));
217     return allowed;
218   }
219 
220   /**
221    * To avoid exposing a secured service by proxying it to everyone, do not proxy a secured GeoService when the user
222    * is not logged in.
223    *
224    * @param geoService The geo service to check
225    * @return Whether to deny proxying this service
226    */
227   public boolean mustDenyAccessForSecuredProxy(GeoService geoService) {
228     if (!Boolean.TRUE.equals(geoService.getSettings().getUseProxy())) {
229       return false;
230     }
231     if (geoService.getAuthentication() == null) {
232       return false;
233     }
234     Authentication auth = SecurityContextHolder.getContext().getAuthentication();
235     return auth == null || auth instanceof AnonymousAuthenticationToken;
236   }
237 }