AuthorisationService.java
/*
* Copyright (C) 2022 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.security;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.tailormap.api.persistence.Application;
import org.tailormap.api.persistence.GeoService;
import org.tailormap.api.persistence.Group;
import org.tailormap.api.persistence.Page;
import org.tailormap.api.persistence.json.AuthorizationRule;
import org.tailormap.api.persistence.json.AuthorizationRuleDecision;
import org.tailormap.api.persistence.json.GeoServiceLayer;
import org.tailormap.api.persistence.json.GeoServiceLayerSettings;
import org.tailormap.api.persistence.json.PageTile;
/**
* Validates access control rules. Any call to userAllowedToViewApplication will verify that the currently logged-in
* user is not only allowed to read the current object, but any object above and below it in the hierarchy.
*/
@Service
public class AuthorisationService {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String ACCESS_TYPE_VIEW = "read";
private Optional<AuthorizationRuleDecision> isAuthorizedByRules(List<AuthorizationRule> rules) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Set<String> groups;
if (auth == null || auth instanceof AnonymousAuthenticationToken) {
groups = Set.of(Group.ANONYMOUS);
} else {
groups = new HashSet<>();
groups.add(Group.ANONYMOUS);
groups.add(Group.AUTHENTICATED);
for (GrantedAuthority authority : auth.getAuthorities()) {
groups.add(authority.getAuthority());
}
}
logger.trace("Groups to check rules against: {}", groups);
// Admins are allowed access to anything.
if (groups.contains(Group.ADMIN)) {
logger.trace(
"Returning {} because {} is allowed access to anything.",
AuthorizationRuleDecision.ALLOW,
Group.ADMIN);
return Optional.of(AuthorizationRuleDecision.ALLOW);
}
boolean hasValidRule = false;
for (AuthorizationRule rule : rules) {
if (logger.isTraceEnabled()) {
logger.trace("Checking rule: \n{} against groups {}.", rule, groups);
}
boolean matchesGroup = groups.contains(rule.getGroupName());
if (!matchesGroup) {
continue;
}
hasValidRule = true;
AuthorizationRuleDecision value = rule.getDecisions().get(AuthorisationService.ACCESS_TYPE_VIEW);
if (value == null) {
logger.trace(
"No decision found for rule: \n{} and access: {}, returning <EMPTY>.",
rule,
AuthorisationService.ACCESS_TYPE_VIEW);
return Optional.empty();
}
if (value.equals(AuthorizationRuleDecision.ALLOW)) {
logger.trace(
"Returning {} because rule: \n{} allows {} access for access: {}.",
value,
rule,
rule.getGroupName(),
AuthorisationService.ACCESS_TYPE_VIEW);
return Optional.of(value);
}
}
if (hasValidRule) {
logger.trace(
"Returning {} because no valid rule allowed access for access: {}.",
AuthorizationRuleDecision.DENY,
AuthorisationService.ACCESS_TYPE_VIEW);
return Optional.of(AuthorizationRuleDecision.DENY);
}
logger.trace(
"Returning <EMPTY> because no rules matched for access: {}.", AuthorisationService.ACCESS_TYPE_VIEW);
return Optional.empty();
}
/**
* Verifies that the (authenticated) user may view/open the page.
*
* @param page the Page to check
* @return the result from the access control checks.
*/
public boolean userAllowedToViewPage(Page page) {
logger.trace("Checking if user is allowed to view page {}.", page.getName());
final boolean allowed =
isAuthorizedByRules(page.getAuthorizationRules()).equals(Optional.of(AuthorizationRuleDecision.ALLOW));
logger.trace(
"User is{} allowed to view page: {} (isAuthorizedByRules={}).",
allowed ? "" : " not",
page.getName(),
allowed);
return allowed;
}
/**
* Verifies that the (authenticated) user may view/open the page tile.
*
* @param pageTile the Page tile to check
* @return the result from the access control checks.
*/
public boolean userAllowedToViewPageTile(PageTile pageTile) {
logger.trace("Checking if user is allowed to view page tile {}.", pageTile.getTitle());
final boolean allowed = isAuthorizedByRules(pageTile.getAuthorizationRules())
.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
logger.trace(
"User is{} allowed to view page tile: {} (isAuthorizedByRules={}).",
allowed ? "" : " not",
pageTile.getTitle(),
allowed);
return allowed;
}
/**
* Verifies that the (authenticated) user may view/open the application.
*
* @param application the Application to check
* @return the result from the access control checks.
*/
public boolean userAllowedToViewApplication(Application application) {
logger.trace(
"Checking if user is allowed to view Application {} ({}).",
application.getTitle(),
application.getTitle());
final boolean allowed = isAuthorizedByRules(application.getAuthorizationRules())
.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
logger.trace(
"User is{} allowed to view application: {} (isAuthorizedByRules={}).",
allowed ? "" : " not",
application.getName(),
allowed);
return allowed;
}
/**
* Verifies that the (authenticated) user may view this geoService.
*
* @param geoService the GeoService to check
* @return the result from the access control checks.
*/
public boolean userAllowedToViewGeoService(GeoService geoService) {
logger.trace(
"Checking if user is allowed to view GeoService {} ({}).", geoService.getId(), geoService.getTitle());
if (mustDenyAccessForSecuredProxy(geoService)) {
return false;
}
final boolean allowed = isAuthorizedByRules(geoService.getAuthorizationRules())
.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
logger.trace(
"User is{} allowed to view GeoService: {} (isAuthorizedByRules={}).",
allowed ? "" : " not",
geoService.getTitle(),
allowed);
return allowed;
}
/**
* Verifies that the (authenticated) user may view the layer in context of the geoService.
*
* @param geoService the GeoService to check
* @param layer the GeoServiceLayer to check
* @return the result from the access control checks.
*/
public boolean userAllowedToViewGeoServiceLayer(GeoService geoService, GeoServiceLayer layer) {
logger.trace(
"Checking if user is allowed to view GeoService '{}' and layer {} ({}).",
geoService.getTitle(),
layer.getName(),
layer.getTitle());
// check if user is allowed to view the geoService
Optional<AuthorizationRuleDecision> geoserviceDecision =
isAuthorizedByRules(geoService.getAuthorizationRules());
if (geoserviceDecision.equals(Optional.of(AuthorizationRuleDecision.DENY))) {
logger.trace("Viewing GeoService {} is denied for user.", geoService.getTitle());
return false;
}
GeoServiceLayerSettings layerSettings =
geoService.getSettings().getLayerSettings().get(layer.getName());
if (layerSettings != null && layerSettings.getAuthorizationRules() != null) {
logger.trace(
"Checking layer settings rules for GeoService '{}' and layer '{}'. \nRules: {}",
geoService.getTitle(),
layer.getName(),
layerSettings.getAuthorizationRules());
List<AuthorizationRule> combinedRules = new ArrayList<>(geoService.getAuthorizationRules());
for (AuthorizationRule rule : layerSettings.getAuthorizationRules()) {
// replace any rule with the same group name, so we end up with a merged
// set of rules where the layer rules override
combinedRules.removeIf(r -> r.getGroupName().equals(rule.getGroupName()));
combinedRules.add(rule);
}
logger.trace(
"Combined rules for GeoService '{}' and layer '{}': \n{}",
geoService.getTitle(),
layer.getName(),
combinedRules);
Optional<AuthorizationRuleDecision> decision = isAuthorizedByRules(combinedRules);
// If no authorization rules are present, fall back to geoService authorization.
if (decision.isPresent() || !layerSettings.getAuthorizationRules().isEmpty()) {
boolean allowed = decision.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
logger.trace(
"Viewing GeoService '{}' and layer '{}' ({}) is {} for user.",
geoService.getTitle(),
layer.getName(),
layer.getTitle(),
(allowed ? "allowed" : "denied"));
return allowed;
}
}
boolean allowed = geoserviceDecision.equals(Optional.of(AuthorizationRuleDecision.ALLOW));
logger.trace(
"Viewing GeoService '{}' and layer '{}' ({}) is {} for user because service access is {3}.",
geoService.getTitle(), layer.getName(), layer.getTitle(), (allowed ? "allowed" : "denied"));
return allowed;
}
/**
* To avoid exposing a secured service by proxying it to everyone, do not proxy a secured GeoService when the user
* is not logged in.
*
* @param geoService The geo service to check
* @return Whether to deny proxying this service
*/
public boolean mustDenyAccessForSecuredProxy(GeoService geoService) {
if (!Boolean.TRUE.equals(geoService.getSettings().getUseProxy())) {
return false;
}
if (geoService.getAuthentication() == null) {
return false;
}
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth == null || auth instanceof AnonymousAuthenticationToken;
}
}