roleNames) {
+ if (null == roleNames) {
+ this.roles = Set.of();
+ return this;
+ }
+ this.roles = Set.copyOf(roleNames);
+ return this;
+ }
+
+ public AccessSummaryRequest build() {
+ if (this.user == null && this.roles.isEmpty()) {
+ throw new IllegalStateException(
+ "AccessSummaryRequest requires user and roles not to be null or empty at the same time. Got user: "
+ + user
+ + ", roles: "
+ + roles);
+ }
+ return new AccessSummaryRequest(this.user, this.roles);
+ }
+ }
+}
diff --git a/src/application/authorization-api/src/main/java/org/geoserver/acl/authorization/AuthorizationService.java b/src/application/authorization-api/src/main/java/org/geoserver/acl/authorization/AuthorizationService.java
index 5096425..90ddc51 100644
--- a/src/application/authorization-api/src/main/java/org/geoserver/acl/authorization/AuthorizationService.java
+++ b/src/application/authorization-api/src/main/java/org/geoserver/acl/authorization/AuthorizationService.java
@@ -7,16 +7,21 @@
package org.geoserver.acl.authorization;
+import org.geoserver.acl.domain.adminrules.AdminRule;
+import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
import org.geoserver.acl.domain.rules.Rule;
+import org.geoserver.acl.domain.rules.RuleAdminService;
import java.util.List;
/**
- * Operations on
+ * The {@code AuthorizationService} implements the business logic to grant or deny access to layers
+ * by processing the {@link Rule}s in the {@link RuleAdminService}, and to determine the admin
+ * access level to workspaces based on the {@link AdminRule}s in the {@link AdminRuleAdminService}.
*
* @author Emanuele Tajariol (etj at geo-solutions.it) (originally as part of GeoFence's
* AdminRuleService)
- * @author Gabriel Roldan adapt from RuleFilter to immutable parameters and return types
+ * @author Gabriel Roldan adapt from {@code RuleFilter} to immutable parameters and return types
*/
public interface AuthorizationService {
@@ -37,7 +42,13 @@ public interface AuthorizationService {
AdminAccessInfo getAdminAuthorization(AdminAccessRequest request);
/**
- * Return the unprocessed {@link Rule} list matching a given filter, sorted by priority.
+ * Returns a summary of workspace names and the layers a user denoted by the {@code request} can
+ * somehow see, and in the case of workspaces, whether it's an administrator of.
+ */
+ AccessSummary getUserAccessSummary(AccessSummaryRequest request);
+
+ /**
+ * Return the unprocessed {@link Rule} list matching a given request, sorted by priority.
*
* Use {@link #getAccessInfo(AccessRequest)} and {@link
* #getAdminAuthorization(AdminAccessRequest)} if you need the resulting coalesced access info.
diff --git a/src/application/authorization-api/src/main/java/org/geoserver/acl/authorization/WorkspaceAccessSummary.java b/src/application/authorization-api/src/main/java/org/geoserver/acl/authorization/WorkspaceAccessSummary.java
new file mode 100644
index 0000000..ea382ac
--- /dev/null
+++ b/src/application/authorization-api/src/main/java/org/geoserver/acl/authorization/WorkspaceAccessSummary.java
@@ -0,0 +1,128 @@
+/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+package org.geoserver.acl.authorization;
+
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+
+import org.geoserver.acl.domain.adminrules.AdminGrantType;
+import org.geoserver.acl.domain.rules.GrantType;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Represents the converged set of visible layer names of a specific workspace for for a {@link
+ * AccessSummaryRequest}
+ *
+ * @since 2.3
+ * @see AccessSummaryRequest
+ */
+@Value
+@Builder(builderClassName = "Builder")
+public class WorkspaceAccessSummary implements Comparable {
+ public static final String NO_WORKSPACE = "";
+ public static final String ANY = "*";
+
+ /**
+ * The workspace name. The special value {@link #NO_WORKSPACE} represents global entities such
+ * as global layer groups
+ */
+ @NonNull private String workspace;
+
+ /**
+ * Whether the user from the {@link AccessSummaryRequest} is an administrator for {@link
+ * #workspace}
+ */
+ private AdminGrantType adminAccess;
+
+ /**
+ * The set of visible layer names in {@link #workspace} the user from the {@link
+ * AccessSummaryRequest} can somehow see, even if only under specific circumstances like for a
+ * given OWS/request combination, resulting from {@link GrantType#ALLOW allow} rules.
+ */
+ @NonNull private Set allowed;
+
+ /**
+ * The set of forbidden layer names in {@link #workspace} the user from the {@link
+ * AccessSummaryRequest} definitely cannot see, resulting from {@link GrantType#DENY deny}
+ * rules.
+ *
+ * Complements the {@link #allowed} list as there may be rules allowing access all layers in
+ * a workspace after a rules denying access to specific layers in the same workspace.
+ */
+ @NonNull private Set forbidden;
+
+ @Override
+ public String toString() {
+ return "[%s: admin: %s, allowed: %s, forbidden: %s]"
+ .formatted(workspace, adminAccess, allowed, forbidden);
+ }
+
+ public boolean canSeeLayer(String layerName) {
+ if (allowed.contains(ANY)) {
+ return !forbidden.contains(layerName);
+ }
+ return allowed.contains(layerName);
+ }
+
+ public static class Builder {
+
+ private String workspace = ANY;
+ private AdminGrantType adminAccess;
+ private Set allowed = new TreeSet<>();
+ private Set forbidden = new TreeSet<>();
+
+ public Builder allowed(@NonNull Set layers) {
+ this.allowed = new TreeSet<>(layers);
+ forbidden.removeAll(layers);
+ return this;
+ }
+
+ public Builder forbidden(@NonNull Set layers) {
+ this.forbidden = new TreeSet<>(layers);
+ allowed.removeAll(layers);
+ return this;
+ }
+
+ public Builder addAllowed(@NonNull String layer) {
+ allowed.add(layer);
+ forbidden.remove(layer);
+ return this;
+ }
+
+ public Builder addForbidden(@NonNull String layer) {
+ forbidden.add(layer);
+ allowed.remove(layer);
+ return this;
+ }
+
+ public WorkspaceAccessSummary build() {
+ Set allowedLayers = conflate(allowed);
+ Set forbiddenLayers = conflate(forbidden);
+
+ return new WorkspaceAccessSummary(
+ workspace, adminAccess, allowedLayers, forbiddenLayers);
+ }
+
+ private static Set conflate(Set layers) {
+ return layers.contains(ANY) ? Set.of(ANY) : Set.copyOf(layers);
+ }
+ }
+
+ @Override
+ public int compareTo(WorkspaceAccessSummary o) {
+ return workspace.compareTo(o.getWorkspace());
+ }
+
+ public boolean isAdmin() {
+ return adminAccess == AdminGrantType.ADMIN;
+ }
+
+ public boolean isUser() {
+ return adminAccess == AdminGrantType.USER || adminAccess == AdminGrantType.ADMIN;
+ }
+}
diff --git a/src/application/authorization-api/src/test/java/org/geoserver/acl/authorization/AccessSummaryRequestTest.java b/src/application/authorization-api/src/test/java/org/geoserver/acl/authorization/AccessSummaryRequestTest.java
new file mode 100644
index 0000000..78df902
--- /dev/null
+++ b/src/application/authorization-api/src/test/java/org/geoserver/acl/authorization/AccessSummaryRequestTest.java
@@ -0,0 +1,46 @@
+/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+package org.geoserver.acl.authorization;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.geoserver.acl.authorization.AccessSummaryRequest.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+class AccessSummaryRequestTest {
+
+ @Test
+ void testPreconditions() {
+ IllegalStateException ex = assertThrows(IllegalStateException.class, builder()::build);
+ assertThat(ex)
+ .hasMessageContaining(
+ "AccessSummaryRequest requires user and roles not to be null or empty at the same time");
+ }
+
+ @Test
+ void testBuild() {
+ AccessSummaryRequest req =
+ builder().user("user").roles("ROLE_ADMINISTRATOR", "ROLE_AUTHENTICATED").build();
+ assertThat(req.getUser()).isEqualTo("user");
+ assertThat(req.getRoles())
+ .containsExactlyInAnyOrder("ROLE_ADMINISTRATOR", "ROLE_AUTHENTICATED");
+
+ req = builder().user("user").roles(Set.of("ROLE_1")).build();
+ assertThat(req.getRoles()).containsExactlyInAnyOrder("ROLE_1");
+
+ req = builder().user("user").roles(Set.of("ROLE_1", "ROLE_2", "ROLE_3")).build();
+ assertThat(req.getRoles()).containsExactlyInAnyOrder("ROLE_1", "ROLE_2", "ROLE_3");
+ }
+
+ @Test
+ void testNullUserAllowedIfRolesIsNotEmpty() {
+ AccessSummaryRequest req = builder().roles("ROLE_ANONYMOUS").build();
+ assertThat(req.getUser()).isNull();
+ assertThat(req.getRoles()).isEqualTo(Set.of("ROLE_ANONYMOUS"));
+ }
+}
diff --git a/src/application/authorization-api/src/test/java/org/geoserver/acl/authorization/WorkspaceAccessSummaryTest.java b/src/application/authorization-api/src/test/java/org/geoserver/acl/authorization/WorkspaceAccessSummaryTest.java
new file mode 100644
index 0000000..2c5d90f
--- /dev/null
+++ b/src/application/authorization-api/src/test/java/org/geoserver/acl/authorization/WorkspaceAccessSummaryTest.java
@@ -0,0 +1,147 @@
+/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+package org.geoserver.acl.authorization;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.geoserver.acl.authorization.WorkspaceAccessSummary.builder;
+
+import org.geoserver.acl.authorization.WorkspaceAccessSummary.Builder;
+import org.geoserver.acl.domain.adminrules.AdminGrantType;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+class WorkspaceAccessSummaryTest {
+
+ @Test
+ void buildDefaults() {
+ WorkspaceAccessSummary was = builder().build();
+ assertThat(was.getWorkspace()).isEqualTo("*");
+ assertThat(was.getAllowed()).isEmpty();
+ assertThat(was.getForbidden()).isEmpty();
+ assertThat(was.getAdminAccess()).isNull();
+ assertThat(was.isAdmin()).isFalse();
+ assertThat(was.isUser()).isFalse();
+ }
+
+ @Test
+ void buildOnlyWorkspace() {
+ WorkspaceAccessSummary was = builderWithWorkspace().build();
+ assertThat(was.getWorkspace()).isEqualTo("cite");
+ assertThat(was.getAllowed()).isEmpty();
+ assertThat(was.getForbidden()).isEmpty();
+ assertThat(was.getAdminAccess()).isNull();
+ assertThat(was.isAdmin()).isFalse();
+ assertThat(was.isUser()).isFalse();
+ }
+
+ @Test
+ void buildAdmin() {
+ WorkspaceAccessSummary was =
+ builderWithWorkspace().adminAccess(AdminGrantType.ADMIN).build();
+ assertThat(was.getWorkspace()).isEqualTo("cite");
+ assertThat(was.getAdminAccess()).isEqualTo(AdminGrantType.ADMIN);
+ assertThat(was.isAdmin()).isTrue();
+ assertThat(was.isUser()).isTrue();
+ }
+
+ @Test
+ void buildUser() {
+ WorkspaceAccessSummary was =
+ builderWithWorkspace().adminAccess(AdminGrantType.USER).build();
+ assertThat(was.getWorkspace()).isEqualTo("cite");
+ assertThat(was.getAdminAccess()).isEqualTo(AdminGrantType.USER);
+ assertThat(was.isAdmin()).isFalse();
+ assertThat(was.isUser()).isTrue();
+ }
+
+ @Test
+ void allowedLayers() {
+ WorkspaceAccessSummary was = builderWithWorkspace().allowed(Set.of("*")).build();
+ assertThat(was.getAllowed()).isEqualTo(Set.of("*"));
+
+ was = builderWithWorkspace().allowed(Set.of("layer1", "layer2")).build();
+ assertThat(was.getAllowed()).isEqualTo(Set.of("layer1", "layer2"));
+ }
+
+ @Test
+ void addAllowedLayers() {
+ WorkspaceAccessSummary was = builderWithWorkspace().addAllowed("*").build();
+ assertThat(was.getWorkspace()).isEqualTo("cite");
+ assertThat(was.getAllowed()).isEqualTo(Set.of("*"));
+
+ was = builderWithWorkspace().addAllowed("layer1").addAllowed("layer2").build();
+ assertThat(was.getAllowed()).isEqualTo(Set.of("layer1", "layer2"));
+ }
+
+ @Test
+ void allowedLayersConflates() {
+ WorkspaceAccessSummary was =
+ builderWithWorkspace()
+ .addAllowed("layer1")
+ .addAllowed("layer2")
+ .addAllowed("*")
+ .build();
+ assertThat(was.getAllowed()).isEqualTo(Set.of("*"));
+
+ was =
+ builderWithWorkspace()
+ .addAllowed("layer1")
+ .addAllowed("layer2")
+ .addAllowed("*")
+ .addAllowed("layer3")
+ .addAllowed("layer4")
+ .build();
+ assertThat(was.getAllowed()).isEqualTo(Set.of("*"));
+ }
+
+ @Test
+ void forbiddenLayers() {
+ WorkspaceAccessSummary was = builderWithWorkspace().forbidden(Set.of("*")).build();
+ assertThat(was.getForbidden()).isEqualTo(Set.of("*"));
+ assertThat(was.getAllowed()).isEmpty();
+
+ was = builderWithWorkspace().forbidden(Set.of("l1", "l2")).build();
+ assertThat(was.getForbidden()).isEqualTo(Set.of("l1", "l2"));
+ assertThat(was.getAllowed()).isEmpty();
+ }
+
+ @Test
+ void addForbiddenLayers() {
+ WorkspaceAccessSummary was = builderWithWorkspace().addForbidden("*").build();
+ assertThat(was.getForbidden()).isEqualTo(Set.of("*"));
+
+ was = builderWithWorkspace().addForbidden("l1").addForbidden("l2").build();
+ assertThat(was.getForbidden()).isEqualTo(Set.of("l1", "l2"));
+ assertThat(was.getAllowed()).isEmpty();
+ }
+
+ @Test
+ void canSeeLayer() {
+ var was = builderWithWorkspace().addForbidden("*").addAllowed("L1").build();
+ assertThat(was.canSeeLayer("L1")).isTrue();
+ assertThat(was.canSeeLayer("L2")).isFalse();
+
+ was = builderWithWorkspace().addAllowed("*").addForbidden("L1").build();
+ assertThat(was.canSeeLayer("L1")).isFalse();
+ assertThat(was.canSeeLayer("L2")).isTrue();
+
+ was = builderWithWorkspace().addAllowed("L1").addForbidden("L2").build();
+ assertThat(was.canSeeLayer("L1")).isTrue();
+ assertThat(was.canSeeLayer("L2")).isFalse();
+
+ was = builderWithWorkspace().addForbidden("L2").addAllowed("L1").build();
+ assertThat(was.canSeeLayer("L1")).isTrue();
+ assertThat(was.canSeeLayer("L2")).isFalse();
+
+ was = builderWithWorkspace().addAllowed("L1").addForbidden("L1").addForbidden("L2").build();
+ assertThat(was.canSeeLayer("L2")).isFalse();
+ assertThat(was.canSeeLayer("L1")).isFalse();
+ }
+
+ private Builder builderWithWorkspace() {
+ return builder().workspace("cite");
+ }
+}
diff --git a/src/application/authorization-impl/src/main/java/org/geoserver/acl/authorization/AuthorizationServiceImpl.java b/src/application/authorization-impl/src/main/java/org/geoserver/acl/authorization/AuthorizationServiceImpl.java
index 1b9eb65..fffa4fd 100644
--- a/src/application/authorization-impl/src/main/java/org/geoserver/acl/authorization/AuthorizationServiceImpl.java
+++ b/src/application/authorization-impl/src/main/java/org/geoserver/acl/authorization/AuthorizationServiceImpl.java
@@ -20,10 +20,14 @@
import static org.geoserver.acl.domain.rules.SpatialFilterType.CLIP;
import static org.geoserver.acl.domain.rules.SpatialFilterType.INTERSECT;
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.naturalOrder;
+
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.geoserver.acl.domain.adminrules.AdminGrantType;
import org.geoserver.acl.domain.adminrules.AdminRule;
import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
import org.geoserver.acl.domain.adminrules.AdminRuleFilter;
@@ -48,6 +52,7 @@
import org.locationtech.jts.geom.Geometry;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -56,6 +61,11 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* Note: service and request params are usually set by the client, and by
@@ -124,6 +134,157 @@ public AdminAccessInfo getAdminAuthorization(@NonNull AdminAccessRequest request
.build();
}
+ @Override
+ public AccessSummary getUserAccessSummary(AccessSummaryRequest request) {
+ String user = request.getUser();
+ Set roles = request.getRoles();
+
+ Map> wsAdminRules = getAdminRulesByWorkspace(user, roles);
+ Map> wsRules = getRulesByWorkspace(user, roles);
+
+ Set workspaces = union(wsAdminRules.keySet(), wsRules.keySet());
+
+ List summaries =
+ workspaces.stream()
+ .map(ws -> conflateViewables(ws, wsAdminRules, wsRules))
+ .toList();
+
+ return AccessSummary.of(summaries);
+ }
+
+ private Set union(Set s1, Set s2) {
+ return Stream.concat(s1.stream(), s2.stream()).collect(Collectors.toSet());
+ }
+
+ private WorkspaceAccessSummary conflateViewables(
+ String workspace,
+ Map> adminRulesByWorkspace,
+ Map> rulesByWorkspace) {
+
+ var builder = WorkspaceAccessSummary.builder();
+ builder.workspace(workspace);
+ conflateAdminRules(builder, adminRulesByWorkspace.getOrDefault(workspace, List.of()));
+ conflateRules(builder, rulesByWorkspace.getOrDefault(workspace, List.of()));
+ return builder.build();
+ }
+
+ void conflateAdminRules(WorkspaceAccessSummary.Builder builder, List rules) {
+
+ AdminRule rule =
+ rules.stream()
+ .sorted(Comparator.comparingLong(AdminRule::getPriority))
+ .findFirst()
+ .orElse(null);
+ if (rule != null) {
+ AdminGrantType adminAccess = rule.getAccess();
+ builder.adminAccess(adminAccess);
+ }
+ }
+
+ void conflateRules(WorkspaceAccessSummary.Builder builder, List rules) {
+
+ // LIMIT rules don't provide access level
+ Predicate notLimitRule = r -> r.getIdentifier().getAccess() != GrantType.LIMIT;
+ // reverse priority sort so the most important ones are added the latest and the
+ // builder creates the allowed/forbidden sets correctly
+ Comparator reversePriority = Comparator.comparing(Rule::getPriority).reversed();
+
+ // add deny rules first, and allow rules after, so allow rules prevail (i.e.
+ // their layer names get removed from the summary's "forbidden" list, since this
+ // is a summary of somehow visible layers, we give preference to allow rules
+ // regardless of the priority
+ assert GrantType.ALLOW.compareTo(GrantType.DENY) < 0;
+ Comparator comparator =
+ Comparator.comparing(Rule::access).reversed().thenComparing(reversePriority);
+
+ rules.stream()
+ .filter(notLimitRule)
+ .sorted(comparator)
+ .forEach(
+ r -> {
+ GrantType access = r.getIdentifier().getAccess();
+ String layer = r.getIdentifier().getLayer();
+ if (null == layer) layer = WorkspaceAccessSummary.ANY;
+ switch (access) {
+ case ALLOW -> builder.addAllowed(layer);
+ case DENY -> {
+ // only add forbidden layers if they're so for all services, to
+ // comply with the "somehow can see" motto of the summary
+ String service = r.getIdentifier().getService();
+ if (null == service) {
+ builder.addForbidden(layer);
+ }
+ }
+ default -> throw new IllegalArgumentException();
+ }
+ });
+ }
+
+ Map> getAdminRulesByWorkspace(String user, Set roles) {
+
+ // Filter is SpecialFilterType.ANY to return all rules
+ AdminRuleFilter filter = new AdminRuleFilter(SpecialFilterType.ANY);
+ filter.getUser().setHeuristically(user);
+ filter.getRole().setHeuristically(roles);
+
+ Function workspaceMapper =
+ workspaceMapper(r -> r.getIdentifier().getWorkspace());
+ Comparator workspaceComparator = workspaceComparator(workspaceMapper);
+ RuleQuery query = RuleQuery.of(filter);
+ BinaryOperator> mergeFunction = mergeFunction();
+ try (Stream all = this.adminRuleService.getAll(query)) {
+ return all.sorted(workspaceComparator)
+ .distinct()
+ .collect(Collectors.toMap(workspaceMapper, List::of, mergeFunction));
+ }
+ }
+
+ Map> getRulesByWorkspace(String user, Set roles) {
+
+ // Filter is SpecialFilterType.ANY to return all rules
+ RuleFilter filter = new RuleFilter(SpecialFilterType.ANY);
+ filter.getUser().setHeuristically(user);
+ filter.getRole().setHeuristically(roles);
+ List rules = getRulesByRoleIncludeDefault(filter);
+
+ Function workspaceMapper =
+ workspaceMapper(r -> r.getIdentifier().getWorkspace());
+ Comparator workspaceComparator = workspaceComparator(workspaceMapper);
+
+ Function> valueMapper = List::of;
+ BinaryOperator> mergeFunction = mergeFunction();
+ return rules.stream()
+ .sorted(workspaceComparator)
+ .distinct()
+ .sorted(Comparator.comparing(Rule::getPriority))
+ .collect(Collectors.toMap(workspaceMapper, valueMapper, mergeFunction));
+ }
+
+ private Comparator workspaceComparator(Function workspaceMapper) {
+ return comparing(workspaceMapper, naturalOrder());
+ }
+
+ private Function workspaceMapper(Function workspaceExtractor) {
+ return workspaceExtractor.andThen(ws -> null == ws ? WorkspaceAccessSummary.ANY : ws);
+ }
+
+ private BinaryOperator> mergeFunction() {
+ return (l1, l2) -> {
+ if (l1 instanceof ArrayList) {
+ l1.addAll(l2);
+ return l1;
+ }
+ if (l2 instanceof ArrayList) {
+ l2.addAll(l1);
+ return l2;
+ }
+ List ret = new ArrayList<>();
+ ret.addAll(l1);
+ ret.addAll(l2);
+ return ret;
+ };
+ }
+
private AccessInfo enlargeAccessInfo(AccessInfo baseAccess, AccessInfo moreAccess) {
if (baseAccess == null) {
if (moreAccess == null) return null;
@@ -325,7 +486,8 @@ private AccessInfo buildAllowAccessInfo(Rule rule, List limits) {
accessInfo.catalogMode(cmode);
if (area != null) {
- // if we have a clip area we apply clip type since is more restrictive, otherwise we
+ // if we have a clip area we apply clip type since is more restrictive,
+ // otherwise we
// keep the intersect
org.geolatte.geom.Geometry> finalArea = org.geolatte.geom.jts.JTS.from(area);
if (atLeastOneClip) {
@@ -431,6 +593,7 @@ protected static CatalogMode getLarger(CatalogMode m1, CatalogMode m2) {
* @return a Map having role names as keys, and the list of matching Rules as values. The NULL
* key holds the rules for the DEFAULT group.
*/
+ @SuppressWarnings("java:S125")
protected Map> getMatchingRulesByRole(AccessRequest request)
throws IllegalArgumentException {
@@ -458,17 +621,25 @@ protected Map> getMatchingRulesByRole(AccessRequest request)
List found = ruleService.getAll(RuleQuery.of(filter)).toList();
ret.put(null, found);
} else {
- for (String role : finalRoleFilter) {
- List found = getRulesByRole(filter, role);
- ret.put(role, found);
+ // used to be: for(role: finalRoleFilter) getRulesByRole(filter, role);,
+ // conflated to a single query with all roles here
+ List rules = getRulesByRoleIncludeDefault(filter);
+ finalRoleFilter.forEach(r -> ret.put(r, new ArrayList<>()));
+ for (Rule rule : rules) {
+ String rolename = rule.getIdentifier().getRolename();
+ boolean isdefault = null == rolename;
+ if (isdefault) {
+ finalRoleFilter.forEach(role -> ret.get(role).add(rule));
+ } else {
+ ret.get(rolename).add(rule);
+ }
}
}
return ret;
}
- private List getRulesByRole(RuleFilter filter, String role) {
+ private List getRulesByRoleIncludeDefault(RuleFilter filter) {
filter = filter.clone();
- filter.setRole(role);
filter.getRole().setIncludeDefault(true);
return ruleService.getAll(RuleQuery.of(filter)).toList();
}
diff --git a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceAccessSummaryTest.java b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceAccessSummaryTest.java
new file mode 100644
index 0000000..d7532c5
--- /dev/null
+++ b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceAccessSummaryTest.java
@@ -0,0 +1,245 @@
+/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+
+package org.geoserver.acl.authorization;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.geoserver.acl.domain.adminrules.AdminGrantType.*;
+import static org.geoserver.acl.domain.rules.GrantType.ALLOW;
+import static org.geoserver.acl.domain.rules.GrantType.DENY;
+
+import lombok.NonNull;
+
+import org.geoserver.acl.domain.adminrules.AdminGrantType;
+import org.geoserver.acl.domain.rules.GrantType;
+import org.geoserver.acl.domain.rules.Rule;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Integration/comformance test for {@link
+ * AuthorizationService#getUserAccessSummary(AccessSummaryRequest)}
+ *
+ * @since 2.3
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public abstract class AuthorizationServiceAccessSummaryTest extends BaseAuthorizationServiceTest {
+
+ AccessSummaryRequest req(@NonNull String user, @NonNull String... roles) {
+ return AccessSummaryRequest.builder().user(user).roles(Set.copyOf(List.of(roles))).build();
+ }
+
+ @Test
+ @Order(9)
+ void adminRules() {
+ insert(ADMIN, 1, "*", "ROLE_1", "ws1");
+ var req = req("*", "ROLE_1");
+ Set allowedLayers = Set.of();
+ Set forbiddenLayers = Set.of();
+ var expected = summary(workspace(ADMIN, "ws1", allowedLayers, forbiddenLayers));
+ var actual = authorizationService.getUserAccessSummary(req);
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ @Order(10)
+ @DisplayName("given an empty rule database, nothing is visible")
+ void empty() {
+ var req = req("user1", "ROLE_1");
+ var viewables = authorizationService.getUserAccessSummary(req);
+ assertThat(viewables).isNotNull();
+ assertThat(viewables.getWorkspaces()).isEmpty();
+ }
+
+ @Test
+ @Order(20)
+ @DisplayName(
+ "given a single matching rule on role with no layer, all layers in the workspace are visible")
+ void singleWorkspaceRuleAllowAllLayers() {
+ var req = req("user1", "ROLE_1");
+ insert(1, "*", "ROLE_1", "w1", "*", ALLOW);
+ var viewables = authorizationService.getUserAccessSummary(req);
+ var expected = summary(workspace("w1", "*"));
+ assertThat(viewables).isEqualTo(expected);
+ }
+
+ @Test
+ @Order(30)
+ void rulesMatchingUsernameAndRoles() {
+ var req = req("user1", "ROLE_1", "ROLE_2");
+ insert(1, "*", "ROLE_1", "w1", "*", ALLOW);
+ insert(2, "*", "ROLE_2", "w2", "allowed1", ALLOW);
+ insert(3, "user1", null, "w3", "L3", ALLOW);
+ var viewables = authorizationService.getUserAccessSummary(req);
+ var expected =
+ summary(workspace("w1", "*"), workspace("w2", "allowed1"), workspace("w3", "L3"));
+ assertThat(viewables).isEqualTo(expected);
+ }
+
+ @Test
+ @Order(40)
+ void denyRulePreserverdIfCatchesAllRequests() {
+ AuthorizationService service = authorizationService;
+ var req = req("user1", "ROLE_1", "ROLE_2");
+ insert(1, "user1", "*", "w1", "hidden1", DENY);
+ insert(2, "*", "ROLE_1", "w1", "*", ALLOW);
+
+ var accessRequestBuilder =
+ AccessRequest.builder().user("user1").roles("ROLE_1", "ROLE_2").workspace("w1");
+
+ // preflight
+ var accessInfo = service.getAccessInfo(accessRequestBuilder.layer("visible").build());
+ assertThat(accessInfo.getGrant()).isEqualTo(ALLOW);
+
+ accessInfo = service.getAccessInfo(accessRequestBuilder.layer("hidden1").build());
+ assertThat(accessInfo.getGrant()).isEqualTo(DENY);
+
+ // summary test
+ var viewables = authorizationService.getUserAccessSummary(req);
+ Set allowed = Set.of("*");
+ Set forbidden = Set.of("hidden1");
+ var expected = summary(workspace("w1", allowed, forbidden));
+ assertThat(viewables).isEqualTo(expected);
+ }
+
+ @Test
+ @Order(41)
+ void denyRuleRemovedIfNotCatchAll() {
+ // rule 1 is denies access to layer hidden1 for a specific service, but rule 2 grants access
+ // to all, so rule 2 prevails because the summary tells layers that are somehow visible and
+ // only list forbidden layers that can never be visible
+ insert(1, "user1", "*", null, "WMS", null, null, "w1", "hidden1", DENY);
+ insert(2, "*", "ROLE_1", "w1", "*", ALLOW);
+
+ var req = req("user1", "ROLE_1", "ROLE_2");
+ var viewables = authorizationService.getUserAccessSummary(req);
+ Set allowed = Set.of("*");
+ Set forbidden = Set.of();
+ var expected = summary(workspace("w1", allowed, forbidden));
+ assertThat(viewables).isEqualTo(expected);
+ }
+
+ @Test
+ @Order(42)
+ void denyAllPreservedButExplicitAllowRuleAlsoPreserved() {
+ // a deny all rule does not prevent specific allowed layers to be seen
+ var req = req("user1", "ROLE_1", "ROLE_2");
+ insert(1, "*", "ROLE_1", "w1", "L1", ALLOW);
+ insert(2, "*", "ROLE_2", "w1", "L2", ALLOW);
+ insert(3, "user1", "*", "w1", "*", DENY);
+
+ var viewables = authorizationService.getUserAccessSummary(req);
+ Set allowed = Set.of("L1", "L2");
+ Set forbidden = Set.of("*");
+ var expected = summary(workspace("w1", allowed, forbidden));
+ assertThat(viewables).isEqualTo(expected);
+ }
+
+ @Test
+ @Order(50)
+ void singleRoleMatchAndDefaultAllow() {
+ var req = req("user1", "ROLE_1");
+ // matching rule
+ insert(1, "user1", "ROLE_1", "w1", "*", ALLOW);
+ // default rule allowing all on w2
+ insert(2, null, null, "w2", null, ALLOW);
+ var expected = summary(workspace("w1", "*"), workspace("w2", "*"));
+ var viewables = authorizationService.getUserAccessSummary(req);
+ assertThat(viewables).isEqualTo(expected);
+ }
+
+ @Test
+ @Order(60)
+ void workspaceAdminMustAdhereToExplicitlyHiddenLayers() {
+ // matching rule
+ insert(1, "user1", "*", "w1", null, ALLOW);
+ insert(2, "*", "ROLE_1", "w1", "hiddenlayer", DENY);
+ insert(3, "*", "ROLE_2", "w1", "*", ALLOW);
+
+ // preflight, layer initially hidden
+ var expected = summary(workspace("w1", "*", "hiddenlayer"));
+
+ var req = req("user1", "ROLE_1", "ROLE_2");
+ var viewables = authorizationService.getUserAccessSummary(req);
+ assertThat(viewables).isEqualTo(expected);
+
+ insert(4, "*", "ROLE_2", "*", "*", ALLOW);
+ // make it an admin, still can't see w1:hiddenlayer
+ insert(ADMIN, 1, "*", "ROLE_1", "w1");
+
+ expected =
+ summary(
+ workspace("*", "*"),
+ workspace(ADMIN, "w1", Set.of("*"), Set.of("hiddenlayer")));
+ viewables = authorizationService.getUserAccessSummary(req);
+ assertThat(viewables).isEqualTo(expected);
+ }
+
+ protected WorkspaceAccessSummary workspace(
+ @NonNull String workspace, @NonNull String allowedLayer) {
+ return workspace(workspace, Set.of(allowedLayer), Set.of());
+ }
+
+ protected WorkspaceAccessSummary workspace(
+ @NonNull String workspace,
+ @NonNull String allowedLayer,
+ @NonNull String forbiddenLayer) {
+ return workspace(workspace, Set.of(allowedLayer), Set.of(forbiddenLayer));
+ }
+
+ protected WorkspaceAccessSummary workspace(
+ AdminGrantType admin, @NonNull String workspace, @NonNull String allowedLayer) {
+ return WorkspaceAccessSummary.builder()
+ .workspace(workspace)
+ .adminAccess(admin)
+ .addAllowed(allowedLayer)
+ .build();
+ }
+
+ protected WorkspaceAccessSummary workspace(
+ @NonNull String workspace,
+ @NonNull Set allowedLayers,
+ @NonNull Set forbiddenLayers) {
+ return workspace(null, workspace, allowedLayers, forbiddenLayers);
+ }
+
+ protected WorkspaceAccessSummary workspace(
+ AdminGrantType admin,
+ @NonNull String workspace,
+ @NonNull Set allowedLayers,
+ @NonNull Set forbiddenLayers) {
+ return WorkspaceAccessSummary.builder()
+ .workspace(workspace)
+ .adminAccess(admin)
+ .allowed(allowedLayers)
+ .forbidden(forbiddenLayers)
+ .build();
+ }
+
+ protected Rule insert(
+ int priority,
+ String user,
+ String role,
+ String workspace,
+ String layer,
+ GrantType access) {
+ if ("*".equals(user)) user = null;
+ if ("*".equals(role)) role = null;
+ if ("*".equals(workspace)) workspace = null;
+ if ("*".equals(layer)) layer = null;
+ return super.insert(priority, user, role, null, null, null, null, workspace, layer, access);
+ }
+
+ private AccessSummary summary(WorkspaceAccessSummary... workspaceSummaries) {
+ return AccessSummary.of(Arrays.asList(workspaceSummaries));
+ }
+}
diff --git a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceGeomTest.java b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceGeomTest.java
index d742dd1..8c9fc88 100644
--- a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceGeomTest.java
+++ b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceGeomTest.java
@@ -31,9 +31,10 @@
/**
* {@link AuthorizationService} integration/conformance test working with geometries
*
- * Concrete implementations must supply the required services in {@link ServiceTestBase}
+ *
Concrete implementations must supply the required services in {@link
+ * BaseAuthorizationServiceTest}
*/
-public abstract class AuthorizationServiceGeomTest extends AuthorizationServiceTest {
+public abstract class AuthorizationServiceGeomTest extends BaseAuthorizationServiceTest {
private static final String WKT_WGS84_1 =
"SRID=4326;MultiPolygon (((-1.93327272727272859 5.5959090909090925, 2.22727272727272707 5.67609090909091041, 2.00454545454545441 4.07245454545454599, -1.92436363636363761 4.54463636363636425, -1.92436363636363761 4.54463636363636425, -1.93327272727272859 5.5959090909090925)))";
private static final String WKT_WGS84_2 =
diff --git a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplAccessSummaryTest.java b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplAccessSummaryTest.java
new file mode 100644
index 0000000..98f1cac
--- /dev/null
+++ b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplAccessSummaryTest.java
@@ -0,0 +1,40 @@
+/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+
+package org.geoserver.acl.authorization;
+
+import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
+import org.geoserver.acl.domain.adminrules.AdminRuleAdminServiceImpl;
+import org.geoserver.acl.domain.adminrules.MemoryAdminRuleRepository;
+import org.geoserver.acl.domain.rules.MemoryRuleRepository;
+import org.geoserver.acl.domain.rules.RuleAdminService;
+import org.geoserver.acl.domain.rules.RuleAdminServiceImpl;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.TestMethodOrder;
+
+/**
+ * Integration/conformance test for {@link
+ * AuthorizationService#getUserAccessSummary(AccessSummaryRequest)}
+ *
+ * @since 2.3
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class AuthorizationServiceImplAccessSummaryTest extends AuthorizationServiceAccessSummaryTest {
+
+ @Override
+ protected RuleAdminService getRuleAdminService() {
+ return new RuleAdminServiceImpl(new MemoryRuleRepository());
+ }
+
+ @Override
+ protected AdminRuleAdminService getAdminRuleAdminService() {
+ return new AdminRuleAdminServiceImpl(new MemoryAdminRuleRepository());
+ }
+
+ @Override
+ protected AuthorizationService getAuthorizationService() {
+ return new AuthorizationServiceImpl(super.adminruleAdminService, super.ruleAdminService);
+ }
+}
diff --git a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplGeomTest.java b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplGeomTest.java
index f6c1823..2149282 100644
--- a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplGeomTest.java
+++ b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplGeomTest.java
@@ -14,7 +14,8 @@
/**
* {@link AuthorizationService} integration/conformance test working with geometries
*
- *
Concrete implementations must supply the required services in {@link ServiceTestBase}
+ *
Concrete implementations must supply the required services in {@link
+ * BaseAuthorizationServiceTest}
*/
class AuthorizationServiceImplGeomTest extends AuthorizationServiceGeomTest {
diff --git a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplTest.java b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplTest.java
index c9f32f5..84b994f 100644
--- a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplTest.java
+++ b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceImplTest.java
@@ -7,21 +7,39 @@
package org.geoserver.acl.authorization;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.geoserver.acl.domain.adminrules.AdminGrantType.ADMIN;
+import static org.geoserver.acl.domain.adminrules.AdminGrantType.USER;
+import static org.geoserver.acl.domain.rules.GrantType.ALLOW;
+import static org.geoserver.acl.domain.rules.GrantType.DENY;
+import static org.geoserver.acl.domain.rules.GrantType.LIMIT;
+
+import org.geoserver.acl.authorization.WorkspaceAccessSummary.Builder;
+import org.geoserver.acl.domain.adminrules.AdminRule;
import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
import org.geoserver.acl.domain.adminrules.AdminRuleAdminServiceImpl;
import org.geoserver.acl.domain.adminrules.MemoryAdminRuleRepository;
+import org.geoserver.acl.domain.rules.GrantType;
import org.geoserver.acl.domain.rules.MemoryRuleRepository;
+import org.geoserver.acl.domain.rules.Rule;
import org.geoserver.acl.domain.rules.RuleAdminService;
import org.geoserver.acl.domain.rules.RuleAdminServiceImpl;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* {@link AuthorizationService} integration/conformance test
*
- *
Concrete implementations must supply the required services in {@link ServiceTestBase}
+ *
Concrete implementations must supply the required services in {@link
+ * BaseAuthorizationServiceTest}
*
* @author Emanuele Tajariol (etj at geo-solutions.it) (originally as part of GeoFence)
*/
-class AuthorizationServiceImplTest extends AuthorizationServiceTest {
+@SuppressWarnings("java:S5786") // class is public cause it's inherited
+public class AuthorizationServiceImplTest extends AuthorizationServiceTest {
@Override
protected RuleAdminService getRuleAdminService() {
@@ -37,4 +55,161 @@ protected AdminRuleAdminService getAdminRuleAdminService() {
protected AuthorizationService getAuthorizationService() {
return new AuthorizationServiceImpl(super.adminruleAdminService, super.ruleAdminService);
}
+
+ @Test
+ void getAdminRulesByWorkspace() {
+ AuthorizationServiceImpl service = (AuthorizationServiceImpl) super.authorizationService;
+ String user = "user1";
+ Set roles = Set.of("ROLE_1", "ROLE_2");
+
+ Map> adminRules;
+
+ adminRules = service.getAdminRulesByWorkspace(user, roles);
+ assertThat(adminRules).isEmpty();
+
+ AdminRule r1 = insert(ADMIN, 1, "*", "ROLE_1", "ws1");
+ adminRules = service.getAdminRulesByWorkspace(user, roles);
+ assertThat(adminRules).isEqualTo(Map.of("ws1", list(r1)));
+
+ AdminRule r2 = insert(ADMIN, 2, "*", "ROLE_2", "ws2");
+ adminRules = service.getAdminRulesByWorkspace(user, roles);
+ assertThat(adminRules).isEqualTo(Map.of("ws1", list(r1), "ws2", list(r2)));
+
+ AdminRule ws3UserRule = insert(USER, 3, "user1", "*", "ws3");
+ adminRules = service.getAdminRulesByWorkspace(user, roles);
+ assertThat(adminRules)
+ .isEqualTo(Map.of("ws1", list(r1), "ws2", list(r2), "ws3", list(ws3UserRule)));
+
+ AdminRule ws3RoleRule = insert(ADMIN, 4, "*", "ROLE_2", "ws3");
+ adminRules = service.getAdminRulesByWorkspace(user, roles);
+ assertThat(adminRules)
+ .isEqualTo(
+ Map.of(
+ "ws1",
+ list(r1),
+ "ws2",
+ list(r2),
+ "ws3",
+ list(ws3UserRule, ws3RoleRule)));
+ }
+
+ @Test
+ void conflateAdminRules() {
+ AuthorizationServiceImpl service = (AuthorizationServiceImpl) super.authorizationService;
+
+ AdminRule r1 = insert(ADMIN, 1, "*", "ROLE_1", "ws1");
+ AdminRule r2 = insert(ADMIN, 2, "*", "ROLE_2", "ws2");
+ AdminRule r3 = insert(USER, 3, "user1", "*", "ws3");
+ AdminRule r4 = insert(ADMIN, 4, "*", "ROLE_2", "ws3");
+
+ AdminAccessRequest ws3AdminReq =
+ AdminAccessRequest.builder()
+ .user("user1")
+ .roles("ROLE_1", "ROLE_2")
+ .workspace("ws3")
+ .build();
+ // verify r3 takes over r4
+ AdminAccessInfo ws3Auth = service.getAdminAuthorization(ws3AdminReq);
+ assertThat(ws3Auth.isAdmin()).isFalse();
+
+ // conflate should give only user access
+ var builder = WorkspaceAccessSummary.builder().workspace("ws3");
+ service.conflateAdminRules(builder, List.of(r3, r4));
+ var wsSummary = builder.build();
+ assertThat(wsSummary.getAdminAccess()).isEqualTo(USER);
+
+ builder = WorkspaceAccessSummary.builder().workspace("ws3");
+ service.conflateAdminRules(builder, List.of(r4, r3));
+ wsSummary = builder.build();
+ assertThat(wsSummary.getAdminAccess()).isEqualTo(USER);
+ }
+
+ @Test
+ void getRulesByWorkspace() {
+ String user = "user1";
+ Set roles = Set.of("ROLE_1", "ROLE_2");
+ AuthorizationServiceImpl service = (AuthorizationServiceImpl) super.authorizationService;
+ Map> actual;
+
+ actual = service.getRulesByWorkspace(user, roles);
+ assertThat(actual).isEmpty();
+
+ Rule r1 = insert(ALLOW, 1, "*", "ROLE_1", "ws1", "*");
+ Rule r2 = insert(ALLOW, 2, "*", "ROLE_2", "ws2", "*");
+ Rule r3 = insert(ALLOW, 3, "user1", "*", "ws1", "*");
+ Rule r4 = insert(ALLOW, 4, "user1", "*", "ws2", "*");
+ insert(ALLOW, 5, "user2", "*", "ws2", "*");
+ insert(DENY, 6, "*", "ROLE_3", "ws2", "*");
+
+ actual = service.getRulesByWorkspace(user, roles);
+ assertThat(actual)
+ .isEqualTo(
+ Map.of(
+ "ws1", list(r1, r3),
+ "ws2", list(r2, r4)));
+ }
+
+ @Test
+ void getRulesByWorkspace2() {
+ Rule r1 = insert(ALLOW, 1, "*", "ROLE_1", "w1", "L1");
+ Rule r2 = insert(ALLOW, 2, "*", "ROLE_2", "w1", "L2");
+ Rule r3 = insert(DENY, 3, "user1", "*", "w1", "*");
+ insert(ALLOW, 5, "user2", "*", "w1", "*");
+
+ AuthorizationServiceImpl service = (AuthorizationServiceImpl) super.authorizationService;
+ String user = "user1";
+ Set roles = Set.of("ROLE_1", "ROLE_2");
+ var actual = service.getRulesByWorkspace(user, roles);
+ assertThat(actual).isEqualTo(Map.of("w1", list(r1, r2, r3)));
+ }
+
+ @Test
+ void conflateRules() {
+ Rule r1 = insert(LIMIT, 1, "*", "ROLE_1", "ws1", "L1");
+ var wsSummary = conflateRules("ws1", r1);
+ assertThat(wsSummary.getAllowed()).isEmpty();
+ assertThat(wsSummary.getForbidden()).isEmpty();
+
+ Rule r2 = insert(ALLOW, 2, "*", "ROLE_2", "ws1", "L1");
+ wsSummary = conflateRules("ws1", r1, r2);
+ assertThat(wsSummary.getAllowed()).containsOnly("L1");
+ assertThat(wsSummary.getForbidden()).isEmpty();
+
+ Rule r3 = insert(ALLOW, 3, "user1", "*", "ws1", "*");
+ Rule r4 = insert(DENY, 4, "user1", "*", "ws1", "L3");
+
+ wsSummary = conflateRules("ws1", r1, r2, r3, r4);
+ assertThat(wsSummary.getAllowed()).containsOnly("*");
+ assertThat(wsSummary.getForbidden()).containsOnly("L3");
+ }
+
+ WorkspaceAccessSummary conflateRules(String ws, Rule... rules) {
+ Builder builder = builder(ws);
+ AuthorizationServiceImpl service = (AuthorizationServiceImpl) super.authorizationService;
+ service.conflateRules(builder, list(rules));
+ return builder.build();
+ }
+
+ private WorkspaceAccessSummary.Builder builder(String ws) {
+ return WorkspaceAccessSummary.builder().workspace(ws);
+ }
+
+ private List list(@SuppressWarnings("unchecked") T... items) {
+ return List.of(items);
+ }
+
+ protected Rule insert(
+ GrantType access,
+ long priority,
+ String user,
+ String role,
+ String workspace,
+ String layer) {
+
+ if ("*".equals(user)) user = null;
+ if ("*".equals(role)) role = null;
+ if ("*".equals(workspace)) workspace = null;
+ if ("*".equals(layer)) layer = null;
+ return insert(priority, user, role, null, null, null, null, workspace, layer, access);
+ }
}
diff --git a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceTest.java b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceTest.java
index 2b04de0..869b362 100644
--- a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceTest.java
+++ b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/AuthorizationServiceTest.java
@@ -37,11 +37,12 @@
/**
* {@link AuthorizationService} integration/conformance test
*
- * Concrete implementations must supply the required services in {@link ServiceTestBase}
+ *
Concrete implementations must supply the required services in {@link
+ * BaseAuthorizationServiceTest}
*
* @author Emanuele Tajariol (etj at geo-solutions.it) (originally as part of GeoFence)
*/
-public abstract class AuthorizationServiceTest extends ServiceTestBase {
+public abstract class AuthorizationServiceTest extends BaseAuthorizationServiceTest {
protected abstract RuleAdminService getRuleAdminService();
diff --git a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/ServiceTestBase.java b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/BaseAuthorizationServiceTest.java
similarity index 81%
rename from src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/ServiceTestBase.java
rename to src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/BaseAuthorizationServiceTest.java
index 1f34177..09dea87 100644
--- a/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/ServiceTestBase.java
+++ b/src/application/authorization-impl/src/test/java/org/geoserver/acl/authorization/BaseAuthorizationServiceTest.java
@@ -8,8 +8,10 @@
import static org.assertj.core.api.Assertions.assertThat;
+import org.geoserver.acl.domain.adminrules.AdminGrantType;
import org.geoserver.acl.domain.adminrules.AdminRule;
import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
+import org.geoserver.acl.domain.adminrules.AdminRuleIdentifier;
import org.geoserver.acl.domain.rules.GrantType;
import org.geoserver.acl.domain.rules.Rule;
import org.geoserver.acl.domain.rules.RuleAdminService;
@@ -24,7 +26,7 @@
/**
* @author Emanuele Tajariol (etj at geo-solutions.it) (originally as part of GeoFence)
*/
-public abstract class ServiceTestBase {
+public abstract class BaseAuthorizationServiceTest {
protected RuleAdminService ruleAdminService;
protected AdminRuleAdminService adminruleAdminService;
@@ -125,4 +127,21 @@ protected Rule rule(
protected AdminRule insert(AdminRule adminRule) {
return adminruleAdminService.insert(adminRule);
}
+
+ protected AdminRule insert(
+ AdminGrantType admin, int priority, String user, String role, String workspace) {
+ if ("*".equals(user)) user = null;
+ if ("*".equals(role)) role = null;
+ if ("*".equals(workspace)) workspace = null;
+ AdminRuleAdminService service = getAdminRuleAdminService();
+ AdminRuleIdentifier identifier =
+ AdminRuleIdentifier.builder()
+ .username(user)
+ .rolename(role)
+ .workspace(workspace)
+ .build();
+ AdminRule rule =
+ AdminRule.builder().priority(priority).access(admin).identifier(identifier).build();
+ return insert(rule);
+ }
}
diff --git a/src/domain/adminrule-management/src/main/java/org/geoserver/acl/domain/adminrules/AdminRule.java b/src/domain/adminrule-management/src/main/java/org/geoserver/acl/domain/adminrules/AdminRule.java
index f9d4923..fb6885e 100644
--- a/src/domain/adminrule-management/src/main/java/org/geoserver/acl/domain/adminrules/AdminRule.java
+++ b/src/domain/adminrule-management/src/main/java/org/geoserver/acl/domain/adminrules/AdminRule.java
@@ -49,6 +49,10 @@ public class AdminRule {
return String.format("AdminRule[id: %s, %s]", id, toShortString());
}
+ public boolean isAdmin() {
+ return access == AdminGrantType.ADMIN;
+ }
+
public AdminRule withUsername(String username) {
return withIdentifier(identifier.withUsername(username));
}
diff --git a/src/domain/rule-management/src/main/java/org/geoserver/acl/domain/rules/Rule.java b/src/domain/rule-management/src/main/java/org/geoserver/acl/domain/rules/Rule.java
index ee99db9..fbbc9b5 100644
--- a/src/domain/rule-management/src/main/java/org/geoserver/acl/domain/rules/Rule.java
+++ b/src/domain/rule-management/src/main/java/org/geoserver/acl/domain/rules/Rule.java
@@ -41,6 +41,10 @@ public class Rule {
return String.format("Rule[id: %s, %s]", id, toShortString());
}
+ public GrantType access() {
+ return getIdentifier().getAccess();
+ }
+
public String ipAddressRange() {
return getIdentifier().getAddressRange();
}
diff --git a/src/integration/openapi/java-client/src/main/java/org/geoserver/acl/api/client/integration/AuthorizationServiceClientAdaptor.java b/src/integration/openapi/java-client/src/main/java/org/geoserver/acl/api/client/integration/AuthorizationServiceClientAdaptor.java
index 56ec6dc..8388a74 100644
--- a/src/integration/openapi/java-client/src/main/java/org/geoserver/acl/api/client/integration/AuthorizationServiceClientAdaptor.java
+++ b/src/integration/openapi/java-client/src/main/java/org/geoserver/acl/api/client/integration/AuthorizationServiceClientAdaptor.java
@@ -12,6 +12,8 @@
import org.geoserver.acl.api.mapper.AuthorizationModelApiMapper;
import org.geoserver.acl.api.mapper.RuleApiMapper;
import org.geoserver.acl.authorization.AccessInfo;
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.AccessSummaryRequest;
import org.geoserver.acl.authorization.AdminAccessInfo;
import org.geoserver.acl.authorization.AuthorizationService;
import org.geoserver.acl.domain.rules.Rule;
@@ -74,4 +76,20 @@ public List getMatchingRules(
throw e;
}
}
+
+ @Override
+ public AccessSummary getUserAccessSummary(AccessSummaryRequest request) {
+
+ org.geoserver.acl.api.model.AccessSummaryRequest apiRequest;
+ org.geoserver.acl.api.model.AccessSummary apiResponse;
+
+ try {
+ apiRequest = mapper.toApi(request);
+ apiResponse = apiClient.getUserAccessSummary(apiRequest);
+ return mapper.toModel(apiResponse);
+ } catch (RuntimeException e) {
+ log.error("Error getting user access summary for {}", request, e);
+ throw e;
+ }
+ }
}
diff --git a/src/integration/openapi/java-e2e/src/test/java/org/geoserver/acl/api/it/accesscontrol/AuthorizationServiceClientAdaptorAccessSummaryIT.java b/src/integration/openapi/java-e2e/src/test/java/org/geoserver/acl/api/it/accesscontrol/AuthorizationServiceClientAdaptorAccessSummaryIT.java
new file mode 100644
index 0000000..990b728
--- /dev/null
+++ b/src/integration/openapi/java-e2e/src/test/java/org/geoserver/acl/api/it/accesscontrol/AuthorizationServiceClientAdaptorAccessSummaryIT.java
@@ -0,0 +1,105 @@
+/* (c) 2023 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+package org.geoserver.acl.api.it.accesscontrol;
+
+import org.geoserver.acl.api.client.integration.AuthorizationServiceClientAdaptor;
+import org.geoserver.acl.api.it.support.ClientContextSupport;
+import org.geoserver.acl.api.it.support.IntegrationTestsApplication;
+import org.geoserver.acl.api.it.support.ServerContextSupport;
+import org.geoserver.acl.authorization.AuthorizationService;
+import org.geoserver.acl.authorization.AuthorizationServiceAccessSummaryTest;
+import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
+import org.geoserver.acl.domain.rules.RuleAdminService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.annotation.DirtiesContext;
+
+/**
+ * {@link AuthorizationServiceAccessSummaryTest} end to end integration test with {@link
+ * AuthorizationServiceClientAdaptor} hitting the authorization API directly through HTTP.
+ *
+ * {@code
+ * CLIENT: AuthorizationServiceClientAdaptor <>
+ * |
+ * |
+ * |
+ * SERVER: |
+ * v
+ * AuthorizationApiController
+ * |
+ * v
+ * AuthorizationApiImpl
+ * |
+ * v
+ * AuthorizationServiceImpl
+ * | |
+ * | |
+ * v v
+ * RuleAdminService AdminRuleAdminService
+ * | |
+ * v v
+ * RuleRepositoryJpaAdaptor AdminRuleRepositoryJpaAdaptor
+ * \ /
+ * \ /
+ * \ /
+ * \_____________/
+ * | |
+ * | Database |
+ * |_____________|
+ *
+ * }
+ *
+ * @since 2.3
+ * @see AuthorizationServiceAccessSummaryTest
+ */
+@DirtiesContext
+@SpringBootTest(
+ webEnvironment = WebEnvironment.RANDOM_PORT,
+ properties = {
+ "geoserver.acl.jpa.show-sql=false",
+ "geoserver.acl.jpa.properties.hibernate.hbm2ddl.auto=create",
+ "geoserver.acl.datasource.url=jdbc:h2:mem:geoserver-acl"
+ },
+ classes = {IntegrationTestsApplication.class})
+class AuthorizationServiceClientAdaptorAccessSummaryIT
+ extends AuthorizationServiceAccessSummaryTest {
+
+ private @Autowired ServerContextSupport serverContext;
+ private @LocalServerPort int serverPort;
+
+ private ClientContextSupport clientContext;
+
+ @Override
+ @BeforeEach
+ protected void setUp() throws Exception {
+ clientContext = new ClientContextSupport().log(false).serverPort(serverPort).setUp();
+ serverContext.setUp();
+ super.setUp();
+ }
+
+ @AfterEach
+ void tearDown() {
+ clientContext.close();
+ }
+
+ @Override
+ protected RuleAdminService getRuleAdminService() {
+ return clientContext.getRuleAdminServiceClient();
+ }
+
+ @Override
+ protected AdminRuleAdminService getAdminRuleAdminService() {
+ return clientContext.getAdminRuleAdminServiceClient();
+ }
+
+ @Override
+ protected AuthorizationService getAuthorizationService() {
+ return clientContext.getAuthorizationServiceClientAdaptor();
+ }
+}
diff --git a/src/integration/openapi/java-e2e/src/test/java/org/geoserver/acl/api/it/accesscontrol/AuthorizationServiceImplAccessSummaryApiIT.java b/src/integration/openapi/java-e2e/src/test/java/org/geoserver/acl/api/it/accesscontrol/AuthorizationServiceImplAccessSummaryApiIT.java
new file mode 100644
index 0000000..c29cc89
--- /dev/null
+++ b/src/integration/openapi/java-e2e/src/test/java/org/geoserver/acl/api/it/accesscontrol/AuthorizationServiceImplAccessSummaryApiIT.java
@@ -0,0 +1,118 @@
+/* (c) 2023 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+package org.geoserver.acl.api.it.accesscontrol;
+
+import org.geoserver.acl.api.it.support.ClientContextSupport;
+import org.geoserver.acl.api.it.support.IntegrationTestsApplication;
+import org.geoserver.acl.api.it.support.ServerContextSupport;
+import org.geoserver.acl.authorization.AuthorizationService;
+import org.geoserver.acl.authorization.AuthorizationServiceAccessSummaryTest;
+import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
+import org.geoserver.acl.domain.adminrules.AdminRuleRepository;
+import org.geoserver.acl.domain.rules.RuleAdminService;
+import org.geoserver.acl.domain.rules.RuleRepository;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.annotation.DirtiesContext;
+
+/**
+ * {@link AuthorizationService} end to end integration test with {@link AuthorizationService}
+ * hitting {@link RuleAdminService} and {@link AdminRuleAdminService} backed by OpenAPI Java Client
+ * adaptors for {@link RuleRepository} and {@link AdminRuleRepository}, which in turn make real HTTP
+ * calls to a running server API.
+ *
+ * {@code
+ * CLIENT: AuthorizationServiceImpl
+ * | |
+ * v v
+ * RuleAdminService AdminRuleAdminService
+ * | |
+ * v v
+ * RuleRepositoryClientAdaptor AdminRuleRepositoryClientAdaptor
+ * | |
+ * v v
+ * RulesApi AdminRulesApi
+ * | |
+ * | |
+ * | |
+ * SERVER: | |
+ * v v
+ * DataRulesApiController WorkspaceAdminRulesApiController
+ * | |
+ * v v
+ * DataRulesApiImpl WorkspaceAdminRulesApiImpl
+ * | |
+ * v v
+ * RuleAdminService AdminRuleAdminService
+ * | |
+ * v v
+ * RuleRepositoryJpaAdaptor AdminRuleRepositoryJpaAdaptor
+ * \ /
+ * \ /
+ * \ /
+ * \_____________/
+ * | |
+ * | Database |
+ * |_____________|
+ *
+ * }
+ *
+ * @since 2.3
+ * @see AuthorizationServiceAccessSummaryTest
+ */
+@DirtiesContext
+@SpringBootTest(
+ webEnvironment = WebEnvironment.RANDOM_PORT,
+ properties = {
+ "geoserver.acl.jpa.show-sql=false",
+ "geoserver.acl.jpa.properties.hibernate.hbm2ddl.auto=create",
+ "geoserver.acl.datasource.url=jdbc:h2:mem:geoserver-acl"
+ },
+ classes = {IntegrationTestsApplication.class})
+class AuthorizationServiceImplAccessSummaryApiIT extends AuthorizationServiceAccessSummaryTest {
+
+ private @Autowired ServerContextSupport serverContext;
+ private @LocalServerPort int serverPort;
+
+ private ClientContextSupport clientContext;
+
+ @Override
+ @BeforeEach
+ protected void setUp() throws Exception {
+ clientContext =
+ new ClientContextSupport()
+ // logging breaks client exception handling, only enable if need to see the
+ // request/response bodies
+ .log(false)
+ .serverPort(serverPort)
+ .setUp();
+ serverContext.setUp();
+ super.setUp();
+ }
+
+ @AfterEach
+ void tearDown() {
+ clientContext.close();
+ }
+
+ @Override
+ protected RuleAdminService getRuleAdminService() {
+ return clientContext.getRuleAdminServiceClient();
+ }
+
+ @Override
+ protected AdminRuleAdminService getAdminRuleAdminService() {
+ return clientContext.getAdminRuleAdminServiceClient();
+ }
+
+ @Override
+ protected AuthorizationService getAuthorizationService() {
+ return clientContext.getInProcessAuthorizationService();
+ }
+}
diff --git a/src/integration/openapi/model-mapping/src/main/java/org/geoserver/acl/api/mapper/AuthorizationModelApiMapper.java b/src/integration/openapi/model-mapping/src/main/java/org/geoserver/acl/api/mapper/AuthorizationModelApiMapper.java
index f91959f..eac4bc4 100644
--- a/src/integration/openapi/model-mapping/src/main/java/org/geoserver/acl/api/mapper/AuthorizationModelApiMapper.java
+++ b/src/integration/openapi/model-mapping/src/main/java/org/geoserver/acl/api/mapper/AuthorizationModelApiMapper.java
@@ -6,12 +6,18 @@
import org.geoserver.acl.api.model.AccessInfo;
import org.geoserver.acl.api.model.AccessRequest;
+import org.geoserver.acl.api.model.AccessSummaryRequest;
import org.geoserver.acl.api.model.AdminAccessInfo;
import org.geoserver.acl.api.model.AdminAccessRequest;
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.WorkspaceAccessSummary;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;
+import java.util.List;
+import java.util.Optional;
+
@Mapper(
componentModel = "spring",
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
@@ -34,4 +40,24 @@ public interface AuthorizationModelApiMapper {
AdminAccessInfo toApi(org.geoserver.acl.authorization.AdminAccessInfo grant);
org.geoserver.acl.authorization.AdminAccessInfo toModel(AdminAccessInfo grant);
+
+ AccessSummaryRequest toApi(org.geoserver.acl.authorization.AccessSummaryRequest request);
+
+ org.geoserver.acl.authorization.AccessSummaryRequest toModel(AccessSummaryRequest request);
+
+ org.geoserver.acl.api.model.AccessSummary toApi(AccessSummary apiResponse);
+
+ default AccessSummary toModel(org.geoserver.acl.api.model.AccessSummary apiResponse) {
+ if (apiResponse == null) {
+ return null;
+ }
+ List workspaces =
+ Optional.ofNullable(apiResponse.getWorkspaces()).orElse(List.of()).stream()
+ .map(this::workspaceAccessSummary)
+ .toList();
+ return AccessSummary.of(workspaces);
+ }
+
+ WorkspaceAccessSummary workspaceAccessSummary(
+ org.geoserver.acl.api.model.WorkspaceAccessSummary workspaceAccessSummary);
}
diff --git a/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/authorization/AuthorizationApiImpl.java b/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/authorization/AuthorizationApiImpl.java
index e110dbf..ec08851 100644
--- a/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/authorization/AuthorizationApiImpl.java
+++ b/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/authorization/AuthorizationApiImpl.java
@@ -9,6 +9,8 @@
import org.geoserver.acl.api.model.AccessInfo;
import org.geoserver.acl.api.model.AccessRequest;
+import org.geoserver.acl.api.model.AccessSummary;
+import org.geoserver.acl.api.model.AccessSummaryRequest;
import org.geoserver.acl.api.model.AdminAccessInfo;
import org.geoserver.acl.api.model.AdminAccessRequest;
import org.geoserver.acl.api.model.Rule;
@@ -65,4 +67,15 @@ public ResponseEntity> getMatchingRules(AccessRequest accessRequest)
List apiResponse = modelResponse.stream().map(support::toApi).toList();
return ResponseEntity.ok(apiResponse);
}
+
+ @Override
+ public ResponseEntity getUserAccessSummary(AccessSummaryRequest request) {
+ org.geoserver.acl.authorization.AccessSummaryRequest modelRequest;
+ org.geoserver.acl.authorization.AccessSummary modelResponse;
+
+ modelRequest = support.toModel(request);
+ modelResponse = service.getUserAccessSummary(modelRequest);
+ AccessSummary apiResponse = support.toApi(modelResponse);
+ return ResponseEntity.ok(apiResponse);
+ }
}
diff --git a/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/support/AuthorizationApiSupport.java b/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/support/AuthorizationApiSupport.java
index 16f66c9..fa77397 100644
--- a/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/support/AuthorizationApiSupport.java
+++ b/src/integration/openapi/spring-server/src/main/java/org/geoserver/acl/api/server/support/AuthorizationApiSupport.java
@@ -13,6 +13,8 @@
import org.geoserver.acl.api.model.AdminAccessInfo;
import org.geoserver.acl.api.model.AdminAccessRequest;
import org.geoserver.acl.api.model.Rule;
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.AccessSummaryRequest;
import org.springframework.web.context.request.NativeWebRequest;
public class AuthorizationApiSupport
@@ -58,4 +60,12 @@ public AdminAccessInfo toApi(org.geoserver.acl.authorization.AdminAccessInfo acc
public org.geoserver.acl.authorization.AdminAccessInfo toModel(AdminAccessInfo access) {
return mapper.toModel(access);
}
+
+ public AccessSummaryRequest toModel(org.geoserver.acl.api.model.AccessSummaryRequest request) {
+ return mapper.toModel(request);
+ }
+
+ public org.geoserver.acl.api.model.AccessSummary toApi(AccessSummary response) {
+ return mapper.toApi(response);
+ }
}
diff --git a/src/integration/persistence-jpa/integration/src/test/java/org/geoserver/acl/integration/jpa/it/AuthorizationServiceImplAccessSummaryJpaIT.java b/src/integration/persistence-jpa/integration/src/test/java/org/geoserver/acl/integration/jpa/it/AuthorizationServiceImplAccessSummaryJpaIT.java
new file mode 100644
index 0000000..9f78003
--- /dev/null
+++ b/src/integration/persistence-jpa/integration/src/test/java/org/geoserver/acl/integration/jpa/it/AuthorizationServiceImplAccessSummaryJpaIT.java
@@ -0,0 +1,73 @@
+/* (c) 2023 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ */
+package org.geoserver.acl.integration.jpa.it;
+
+import org.geoserver.acl.authorization.AuthorizationService;
+import org.geoserver.acl.authorization.AuthorizationServiceAccessSummaryTest;
+import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
+import org.geoserver.acl.domain.rules.RuleAdminService;
+import org.geoserver.acl.integration.jpa.config.AuthorizationJPAPropertiesTestConfiguration;
+import org.geoserver.acl.integration.jpa.config.JPAIntegrationConfiguration;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+/**
+ * {@link AuthorizationServiceAccessSummaryTest} integration test with JPA-backed repositories
+ *
+ * {@code
+ * AuthorizationService
+ * | |
+ * v v
+ * RuleAdminService AdminRuleAdminService
+ * | |
+ * v v
+ * RuleRepositoryJpaAdaptor AdminRuleRepositoryJpaAdaptor
+ * \ /
+ * \ /
+ * \ /
+ * \_____________/
+ * | |
+ * | Database |
+ * |_____________|
+ *
+ * }
+ *
+ * @since 2.3
+ */
+@SpringBootTest(
+ classes = {
+ AuthorizationJPAPropertiesTestConfiguration.class,
+ JPAIntegrationConfiguration.class,
+ JpaIntegrationTestSupport.class
+ })
+@ActiveProfiles("test") // see config props in src/test/resource/application-test.yaml
+class AuthorizationServiceImplAccessSummaryJpaIT extends AuthorizationServiceAccessSummaryTest {
+
+ private @Autowired JpaIntegrationTestSupport support;
+
+ @Override
+ @BeforeEach
+ protected void setUp() throws Exception {
+ support.setUp();
+ super.setUp();
+ }
+
+ @Override
+ protected RuleAdminService getRuleAdminService() {
+ return support.getRuleAdminService();
+ }
+
+ @Override
+ protected AdminRuleAdminService getAdminRuleAdminService() {
+ return support.getAdminruleAdminService();
+ }
+
+ @Override
+ protected AuthorizationService getAuthorizationService() {
+ return support.getAuthorizationService();
+ }
+}
diff --git a/src/integration/persistence-jpa/integration/src/test/java/org/geoserver/acl/integration/jpa/it/AuthorizationServiceImplJpaIT.java b/src/integration/persistence-jpa/integration/src/test/java/org/geoserver/acl/integration/jpa/it/AuthorizationServiceImplJpaIT.java
index cf417e1..756e4fc 100644
--- a/src/integration/persistence-jpa/integration/src/test/java/org/geoserver/acl/integration/jpa/it/AuthorizationServiceImplJpaIT.java
+++ b/src/integration/persistence-jpa/integration/src/test/java/org/geoserver/acl/integration/jpa/it/AuthorizationServiceImplJpaIT.java
@@ -5,7 +5,7 @@
package org.geoserver.acl.integration.jpa.it;
import org.geoserver.acl.authorization.AuthorizationService;
-import org.geoserver.acl.authorization.AuthorizationServiceTest;
+import org.geoserver.acl.authorization.AuthorizationServiceImplTest;
import org.geoserver.acl.domain.adminrules.AdminRuleAdminService;
import org.geoserver.acl.domain.rules.RuleAdminService;
import org.geoserver.acl.integration.jpa.config.AuthorizationJPAPropertiesTestConfiguration;
@@ -45,7 +45,7 @@
JpaIntegrationTestSupport.class
})
@ActiveProfiles("test") // see config props in src/test/resource/application-test.yaml
-class AuthorizationServiceImplJpaIT extends AuthorizationServiceTest {
+class AuthorizationServiceImplJpaIT extends AuthorizationServiceImplTest {
private @Autowired JpaIntegrationTestSupport support;
diff --git a/src/integration/persistence-jpa/integration/src/test/resources/application-test.yaml b/src/integration/persistence-jpa/integration/src/test/resources/application-test.yaml
index f7caf61..db7801f 100644
--- a/src/integration/persistence-jpa/integration/src/test/resources/application-test.yaml
+++ b/src/integration/persistence-jpa/integration/src/test/resources/application-test.yaml
@@ -19,7 +19,7 @@ geoserver.acl:
logging:
level:
root: warn
- '[org.geoserver.acl]': warn
+ '[org.geoserver.acl]': info
'[org.springframework.boot.autoconfigure]': warn
'[org.springframework.boot.test.context]': warn
'[org.testcontainers]': info
diff --git a/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationService.java b/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationService.java
index 908b30f..d5e3d93 100644
--- a/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationService.java
+++ b/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationService.java
@@ -9,6 +9,8 @@
import org.geoserver.acl.authorization.AccessInfo;
import org.geoserver.acl.authorization.AccessRequest;
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.AccessSummaryRequest;
import org.geoserver.acl.authorization.AdminAccessInfo;
import org.geoserver.acl.authorization.AdminAccessRequest;
import org.geoserver.acl.authorization.AuthorizationService;
@@ -33,26 +35,23 @@ public class CachingAuthorizationService extends ForwardingAuthorizationService
private final ConcurrentMap ruleAccessCache;
private final ConcurrentMap adminRuleAccessCache;
+ private final ConcurrentMap viewablesCache;
public CachingAuthorizationService(
@NonNull AuthorizationService delegate,
@NonNull ConcurrentMap dataAccessCache,
- @NonNull ConcurrentMap adminAccessCache) {
+ @NonNull ConcurrentMap adminAccessCache,
+ @NonNull ConcurrentMap viewablesCache) {
super(delegate);
this.ruleAccessCache = dataAccessCache;
this.adminRuleAccessCache = adminAccessCache;
+ this.viewablesCache = viewablesCache;
}
@Override
public AccessInfo getAccessInfo(@NonNull AccessRequest request) {
- AccessInfo grant = ruleAccessCache.computeIfAbsent(request, this::load);
- if (grant.getMatchingRules().isEmpty()) {
- // do not cache results with no matching rules. It'll make it impossible to evict them
- this.ruleAccessCache.remove(request);
- }
-
- return grant;
+ return ruleAccessCache.computeIfAbsent(request, this::load);
}
private AccessInfo load(AccessRequest request) {
@@ -61,18 +60,22 @@ private AccessInfo load(AccessRequest request) {
@Override
public AdminAccessInfo getAdminAuthorization(@NonNull AdminAccessRequest request) {
- AdminAccessInfo grant = adminRuleAccessCache.computeIfAbsent(request, this::load);
- if (grant.getMatchingAdminRule() == null) {
- // do not cache results with no matching rules. It'll make it impossible to evict them
- this.adminRuleAccessCache.remove(request);
- }
- return grant;
+ return adminRuleAccessCache.computeIfAbsent(request, this::load);
}
private AdminAccessInfo load(AdminAccessRequest request) {
return logLoaded(request, super.getAdminAuthorization(request));
}
+ @Override
+ public AccessSummary getUserAccessSummary(@NonNull AccessSummaryRequest request) {
+ return viewablesCache.computeIfAbsent(request, this::load);
+ }
+
+ private AccessSummary load(AccessSummaryRequest request) {
+ return logLoaded(request, super.getUserAccessSummary(request));
+ }
+
private A logLoaded(Object request, A accessInfo) {
log.debug("loaded and cached {} -> {}", request, accessInfo);
return accessInfo;
@@ -81,18 +84,24 @@ private A logLoaded(Object request, A accessInfo) {
@EventListener(RuleEvent.class)
public void onRuleEvent(RuleEvent event) {
int evictCount = evictAll(ruleAccessCache);
+ evictViewables();
log.debug("evicted all {} authorizations upon event {}", evictCount, event);
}
+ @EventListener(AdminRuleEvent.class)
+ public void onAdminRuleEvent(AdminRuleEvent event) {
+ int evictCount = evictAll(adminRuleAccessCache);
+ evictViewables();
+ log.debug("evicted all {} admin authorizations upon event {}", evictCount, event);
+ }
+
private int evictAll(Map, ?> cache) {
int size = cache.size();
cache.clear();
return size;
}
- @EventListener(AdminRuleEvent.class)
- public void onAdminRuleEvent(AdminRuleEvent event) {
- int evictCount = evictAll(adminRuleAccessCache);
- log.debug("evicted all {} admin authorizations upon event {}", evictCount, event);
+ void evictViewables() {
+ evictAll(viewablesCache);
}
}
diff --git a/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceConfiguration.java b/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceConfiguration.java
index 9765332..a9a746c 100644
--- a/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceConfiguration.java
+++ b/src/integration/spring/cache/src/main/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceConfiguration.java
@@ -9,6 +9,8 @@
import org.geoserver.acl.authorization.AccessInfo;
import org.geoserver.acl.authorization.AccessRequest;
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.AccessSummaryRequest;
import org.geoserver.acl.authorization.AdminAccessInfo;
import org.geoserver.acl.authorization.AdminAccessRequest;
import org.geoserver.acl.authorization.AuthorizationService;
@@ -33,10 +35,11 @@ public class CachingAuthorizationServiceConfiguration {
CachingAuthorizationService cachingAuthorizationService(
AuthorizationService delegate,
ConcurrentMap authorizationCache,
- ConcurrentMap adminAuthorizationCache) {
+ ConcurrentMap adminAuthorizationCache,
+ ConcurrentMap viewablesCache) {
return new CachingAuthorizationService(
- delegate, authorizationCache, adminAuthorizationCache);
+ delegate, authorizationCache, adminAuthorizationCache, viewablesCache);
}
@Bean
@@ -50,11 +53,17 @@ ConcurrentMap aclAdminAuthCache(
return getCache(cacheManager, "acl-admin-grants");
}
+ @Bean
+ ConcurrentMap aclViewablesCache(
+ CacheManager cacheManager) {
+ return getCache(cacheManager, "acl-access-summary");
+ }
+
+ @SuppressWarnings("unchecked")
private ConcurrentMap getCache(CacheManager cacheManager, String cacheName) {
if (cacheManager instanceof CaffeineCacheManager ccf) {
org.springframework.cache.Cache cache = ccf.getCache(cacheName);
if (cache != null) {
- @SuppressWarnings("unchecked")
Cache caffeineCache = (Cache) cache.getNativeCache();
return caffeineCache.asMap();
}
diff --git a/src/integration/spring/cache/src/test/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceTest.java b/src/integration/spring/cache/src/test/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceTest.java
index 4b1a484..2fb8550 100644
--- a/src/integration/spring/cache/src/test/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceTest.java
+++ b/src/integration/spring/cache/src/test/java/org/geoserver/acl/authorization/cache/CachingAuthorizationServiceTest.java
@@ -5,8 +5,7 @@
package org.geoserver.acl.authorization.cache;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -14,6 +13,8 @@
import org.geoserver.acl.authorization.AccessInfo;
import org.geoserver.acl.authorization.AccessRequest;
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.AccessSummaryRequest;
import org.geoserver.acl.authorization.AdminAccessInfo;
import org.geoserver.acl.authorization.AdminAccessRequest;
import org.geoserver.acl.authorization.AuthorizationService;
@@ -35,13 +36,17 @@ class CachingAuthorizationServiceTest {
private AuthorizationService delegate;
private ConcurrentMap dataAccessCache;
private ConcurrentMap adminAccessCache;
+ private ConcurrentMap viewablesCache;
@BeforeEach
void setUp() throws Exception {
delegate = mock(AuthorizationService.class);
dataAccessCache = new ConcurrentHashMap<>();
adminAccessCache = new ConcurrentHashMap<>();
- caching = new CachingAuthorizationService(delegate, dataAccessCache, adminAccessCache);
+ viewablesCache = new ConcurrentHashMap<>();
+ caching =
+ new CachingAuthorizationService(
+ delegate, dataAccessCache, adminAccessCache, viewablesCache);
}
@Test
@@ -49,8 +54,24 @@ void testCachingAuthorizationService() {
var npe = NullPointerException.class;
assertThrows(
npe,
- () -> new CachingAuthorizationService(null, dataAccessCache, adminAccessCache));
- assertThrows(npe, () -> new CachingAuthorizationService(delegate, null, adminAccessCache));
+ () ->
+ new CachingAuthorizationService(
+ null, dataAccessCache, adminAccessCache, viewablesCache));
+ assertThrows(
+ npe,
+ () ->
+ new CachingAuthorizationService(
+ delegate, null, adminAccessCache, viewablesCache));
+ assertThrows(
+ npe,
+ () ->
+ new CachingAuthorizationService(
+ delegate, dataAccessCache, null, viewablesCache));
+ assertThrows(
+ npe,
+ () ->
+ new CachingAuthorizationService(
+ delegate, dataAccessCache, adminAccessCache, null));
}
@Test
diff --git a/src/openapi/acl-api.yaml b/src/openapi/acl-api.yaml
index 0c35d82..f4e1093 100644
--- a/src/openapi/acl-api.yaml
+++ b/src/openapi/acl-api.yaml
@@ -629,6 +629,23 @@ paths:
responses:
'200':
$ref: '#/components/responses/PageOfRules'
+ /authorization/accesssummary:
+ post:
+ operationId: getUserAccessSummary
+ tags:
+ - Authorization
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AccessSummaryRequest'
+ application/x-jackson-smile:
+ schema:
+ $ref: '#/components/schemas/AccessSummaryRequest'
+ responses:
+ '200':
+ $ref: '#/components/responses/AccessSummary'
components:
securitySchemes:
@@ -757,6 +774,15 @@ components:
application/x-jackson-smile:
schema:
$ref: '#/components/schemas/AdminAccessInfo'
+ AccessSummary:
+ description: The list of per-workspace access summary for a user
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AccessSummary'
+ application/x-jackson-smile:
+ schema:
+ $ref: '#/components/schemas/AccessSummary'
schemas:
CatalogMode:
type: string
@@ -1156,4 +1182,58 @@ components:
type: string
matchingAdminRule:
type: string
-
\ No newline at end of file
+
+ AccessSummaryRequest:
+ type: object
+ required:
+ - user
+ - roles
+ properties:
+ user:
+ description: the authentication user name
+ type: string
+ roles:
+ description: The roles the requesting user belongs to
+ type: array
+ nullable: false
+ uniqueItems: true
+ items:
+ type: string
+
+ AccessSummary:
+ type: object
+ properties:
+ workspaces:
+ type: array
+ items:
+ $ref: '#/components/schemas/WorkspaceAccessSummary'
+
+ WorkspaceAccessSummary:
+ type: object
+ properties:
+ workspace:
+ type: string
+ adminAccess:
+ $ref: '#/components/schemas/AdminGrantType'
+ description: The roles the requesting user belongs to
+ allowed:
+ description: |-
+ The set of visible layer names in `workspace` the user from the
+ `AccessSummaryRequest` can somehow see, even if only under specific circumstances like for a
+ given OWS/request combination, resulting from `GrantType.ALLOW` rules.
+ type: array
+ nullable: true
+ uniqueItems: true
+ items:
+ type: string
+ forbidden:
+ description: |-
+ The set of forbidden layer names in `workspace` the user from the
+ `AccessSummaryRequest` definitely cannot see, resulting from `GrantType.DENY rules.
+ Complements the `allowed` list as there may be rules allowing access all layers in
+ a workspace after a rules denying access to specific layers in the same workspace.
+ type: array
+ nullable: true
+ uniqueItems: true
+ items:
+ type: string
diff --git a/src/plugin/accessmanager/src/main/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManager.java b/src/plugin/accessmanager/src/main/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManager.java
index 0a62dcb..02764c8 100644
--- a/src/plugin/accessmanager/src/main/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManager.java
+++ b/src/plugin/accessmanager/src/main/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManager.java
@@ -6,6 +6,11 @@
*/
package org.geoserver.acl.plugin.accessmanager;
+import static org.geoserver.acl.domain.rules.GrantType.ALLOW;
+import static org.geoserver.acl.domain.rules.GrantType.DENY;
+import static org.geoserver.acl.domain.rules.GrantType.LIMIT;
+import static org.geoserver.acl.plugin.accessmanager.CatalogSecurityFilterBuilder.buildSecurityFilter;
+
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;
@@ -14,8 +19,8 @@
import org.apache.commons.collections4.CollectionUtils;
import org.geoserver.acl.authorization.AccessInfo;
import org.geoserver.acl.authorization.AccessRequest;
-import org.geoserver.acl.authorization.AdminAccessInfo;
-import org.geoserver.acl.authorization.AdminAccessRequest;
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.AccessSummaryRequest;
import org.geoserver.acl.authorization.AuthorizationService;
import org.geoserver.acl.domain.rules.GrantType;
import org.geoserver.acl.domain.rules.LayerAttribute;
@@ -24,6 +29,7 @@
import org.geoserver.acl.plugin.accessmanager.wps.WPSAccessInfo;
import org.geoserver.acl.plugin.accessmanager.wps.WPSHelper;
import org.geoserver.acl.plugin.support.GeomHelper;
+import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.FeatureTypeInfo;
@@ -57,6 +63,7 @@
import org.geotools.api.filter.Filter;
import org.geotools.api.filter.FilterFactory;
import org.geotools.api.filter.expression.PropertyName;
+import org.geotools.api.filter.spatial.Intersects;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.text.cql2.CQLException;
@@ -77,6 +84,8 @@
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* {@link ResourceAccessManager} to make GeoServer use the ACL {@link AuthorizationService} to
@@ -99,7 +108,7 @@ enum PropertyAccessMode {
static final CatalogMode DEFAULT_CATALOG_MODE = CatalogMode.HIDE;
- private AuthorizationService aclService;
+ private AuthorizationService authorizationService;
private final AccessManagerConfigProvider configProvider;
@@ -113,45 +122,56 @@ public ACLResourceAccessManager(
AccessManagerConfigProvider configurationManager,
WPSHelper wpsHelper) {
- this.aclService = aclService;
+ this.authorizationService = aclService;
this.configProvider = configurationManager;
this.groupsCache = groupsCache;
this.wpsHelper = wpsHelper;
}
- public AccessManagerConfig getConfig() {
- return this.configProvider.get();
- }
-
- static boolean isAuthenticated(Authentication user) {
- return (user != null) && !(user instanceof AnonymousAuthenticationToken);
+ @Override
+ public int getPriority() {
+ return ExtensionPriority.LOWEST;
}
- static boolean isAdmin(Authentication user) {
- if (isAuthenticated(user)) {
- return user.getAuthorities().stream()
- .map(GrantedAuthority::getAuthority)
- .anyMatch(GeoServerRole.ADMIN_ROLE.getAuthority()::equals);
+ /**
+ * {@inheritDoc}
+ *
+ * Returns a {@link Filter} selecting only the objects authorized by the manager. May return
+ * null in which case the caller is responsible for building a filter based on calls to the
+ * manager's other methods.
+ *
+ * @return {@link Filter#INCLUDE INCLUDE} if {@code user} is an {@link GeoServerRole#ADMIN_ROLE
+ * administrator}, {@link Filter#EXCLUDE EXCLUDE} if {@code user == null}, otherwise the
+ * filter built by {@link CatalogSecurityFilterBuilder} for the user's {@link AccessSummary}
+ * and {@link CatalogInfo} {@code infoType}
+ * @see AuthorizationService#getUserAccessSummary(AccessSummaryRequest)
+ * @see CatalogSecurityFilterBuilder
+ */
+ @Override
+ public Filter getSecurityFilter(Authentication user, Class extends CatalogInfo> infoType) {
+ if (null == user) {
+ return Filter.EXCLUDE;
}
- return false;
+ if (isAdmin(user)) {
+ return Filter.INCLUDE;
+ }
+ AccessSummary viewables = getAccessSummary(user);
+ return buildSecurityFilter(viewables, infoType);
}
+ /** {@inheritDoc} */
@Override
public WorkspaceAccessLimits getAccessLimits(Authentication user, WorkspaceInfo workspace) {
CatalogMode catalogMode = DEFAULT_CATALOG_MODE;
boolean canRead;
boolean canWrite;
boolean canAdmin;
- if (isAuthenticated(user)) {
- // shortcut, if the user is the admin, he can do everything
- if (isAdmin(user)) {
- canRead = canWrite = canAdmin = true;
- } else {
- canRead = true;
- canWrite = configProvider.get().isGrantWriteToWorkspacesToAuthenticatedUsers();
- String workspaceName = workspace.getName();
- canAdmin = isWorkspaceAdmin(user, workspaceName);
- }
+ if (isAdmin(user)) {
+ canRead = canWrite = canAdmin = true;
+ } else if (isAuthenticated(user)) {
+ canRead = true;
+ canWrite = configProvider.get().isGrantWriteToWorkspacesToAuthenticatedUsers();
+ canAdmin = isWorkspaceAdmin(user, workspace);
} else {
// further logic disabled because of
// https://github.com/geosolutions-it/geofence/issues/6 (gone)
@@ -162,30 +182,32 @@ public WorkspaceAccessLimits getAccessLimits(Authentication user, WorkspaceInfo
return new WorkspaceAccessLimits(catalogMode, canRead, canWrite, canAdmin);
}
- /** We expect the user not to be null and not to be admin */
- private boolean isWorkspaceAdmin(Authentication user, String workspaceName) {
- log(
- FINE,
- "Getting admin auth for user {0} on workspace {1}",
- user.getName(),
- workspaceName);
-
- AdminAccessRequest request =
- new AdminAccessRequestBuilder(configProvider.get())
- .user(user)
- .workspace(workspaceName)
- .build();
-
- AdminAccessInfo grant = aclService.getAdminAuthorization(request);
-
- log(
- FINE,
- "Admin auth for user {0} on workspace {1}: {2}",
- user.getName(),
- workspaceName,
- grant.isAdmin());
+ /**
+ * Overrides the default {@link ResourceAccessManager#isWorkspaceAdmin} to use the more
+ * efficient {@link AccessSummary#hasAdminRightsToAnyWorkspace()}. {@link AccessSummary} is
+ * obtained through {@link AuthorizationService#getUserAccessSummary(AccessSummaryRequest)} and
+ * provides a quick view of adminable workspaces and which layers can be seen.
+ *
+ * @see AuthorizationService#getUserAccessSummary(AccessSummaryRequest)
+ * @see #getSecurityFilter(Authentication, Class)
+ * @apiNote this method's {@code @Override} annotation is commented out while the GeoServer
+ * maintenance version is on the {@code 2.24.x} series, for the {@code
+ * ACLResourceAccessManager} to keep working and building against it. Add it back once the
+ * GeoServer maintenance version moves to the {@code 2.25.x} series.
+ */
+ // @Override
+ public boolean isWorkspaceAdmin(Authentication user, Catalog catalog) {
+ AccessSummary accessSumary = getAccessSummary(user);
+ // revisit: catalog is unsused in this implementation, but maybe verify at least
+ // one of the workspaces in AccessSummary exists in catalog
+ return accessSumary.hasAdminRightsToAnyWorkspace();
+ }
- return grant.isAdmin();
+ /** We expect the user not to be null and not to be admin */
+ private boolean isWorkspaceAdmin(Authentication user, WorkspaceInfo workspace) {
+ String workspaceName = workspace.getName();
+ AccessSummary accessSummary = getAccessSummary(user);
+ return accessSummary.hasAdminWriteAccess(workspaceName);
}
@Override
@@ -194,20 +216,23 @@ public StyleAccessLimits getAccessLimits(Authentication user, StyleInfo style) {
return null;
}
+ /** {@inheritDoc} */
@Override
public LayerGroupAccessLimits getAccessLimits(Authentication user, LayerGroupInfo layerInfo) {
return getAccessLimits(user, layerInfo, Collections.emptyList());
}
+ /** {@inheritDoc} */
@Override
public DataAccessLimits getAccessLimits(Authentication user, LayerInfo layer) {
- log(Level.FINE, "Getting access limits for Layer {0}", layer.getName());
+ log(FINE, "Getting access limits for Layer {0}", layer.getName());
return getAccessLimits(user, layer, Collections.emptyList());
}
+ /** {@inheritDoc} */
@Override
public DataAccessLimits getAccessLimits(Authentication user, ResourceInfo resource) {
- log(Level.FINE, "Getting access limits for Resource {0}", resource.getName());
+ log(FINE, "Getting access limits for Resource {0}", resource.getName());
// extract the user name
String workspace = resource.getStore().getWorkspace().getName();
String layer = resource.getName();
@@ -215,6 +240,7 @@ public DataAccessLimits getAccessLimits(Authentication user, ResourceInfo resour
getAccessLimits(user, resource, layer, workspace, Collections.emptyList());
}
+ /** {@inheritDoc} */
@Override
public DataAccessLimits getAccessLimits(
Authentication user, LayerInfo layer, List containers) {
@@ -223,6 +249,7 @@ public DataAccessLimits getAccessLimits(
return (DataAccessLimits) getAccessLimits(user, layer, layerName, workspace, containers);
}
+ /** {@inheritDoc} */
@Override
public LayerGroupAccessLimits getAccessLimits(
Authentication user, LayerGroupInfo layerGroup, List containers) {
@@ -233,6 +260,25 @@ public LayerGroupAccessLimits getAccessLimits(
getAccessLimits(user, layerGroup, layer, workspace, containers);
}
+ public AccessManagerConfig getConfig() {
+ return this.configProvider.get();
+ }
+
+ static boolean isAuthenticated(Authentication user) {
+ return (user != null) && !(user instanceof AnonymousAuthenticationToken);
+ }
+
+ static boolean isAdmin(Authentication user) {
+ if (isAuthenticated(user)) {
+ return roles(user).anyMatch(GeoServerRole.ADMIN_ROLE.getAuthority()::equals);
+ }
+ return false;
+ }
+
+ static Stream roles(Authentication user) {
+ return user.getAuthorities().stream().map(GrantedAuthority::getAuthority);
+ }
+
private AccessLimits getAccessLimits(
Authentication user,
CatalogInfo info,
@@ -263,7 +309,7 @@ private AccessLimits getAccessLimits(
boolean noneSingle = noneSingle(summaries);
// all opaque we deny and don't perform any resolution of group limits.
if (allOpaque) {
- accessInfo = accessInfo.withGrant(GrantType.DENY);
+ accessInfo = accessInfo.withGrant(DENY);
} else if (noneSingle) {
// if a single group is present we don't apply any limit from containers.
processingResult =
@@ -272,8 +318,8 @@ private AccessLimits getAccessLimits(
}
}
} else if (layerGroupsRequested) {
- // layer is requested in context of a layer group, we need to process the containers
- // limits.
+ // layer is requested in context of a layer group, we need to process the
+ // containers limits.
processingResult =
getContainerResolverResult(info, layer, workspace, user, containers, List.of());
}
@@ -335,7 +381,7 @@ private ProcessingResult wpsProcessingResult(
private AccessInfo getAccessInfo(AccessRequest accessRequest) {
final Level timeLogLevel = FINE;
final Stopwatch sw = LOGGER.isLoggable(timeLogLevel) ? Stopwatch.createStarted() : null;
- AccessInfo accessInfo = aclService.getAccessInfo(accessRequest);
+ AccessInfo accessInfo = authorizationService.getAccessInfo(accessRequest);
if (null != sw) {
sw.stop();
log(timeLogLevel, "ACL auth run in {0}: {1} -> {2}", sw, accessRequest, accessInfo);
@@ -487,9 +533,9 @@ private VectorAccessLimits buildVectorAccessLimits(
Filter readFilter = toFilter(accessInfo.getGrant(), accessInfo.getCqlFilterRead());
Filter writeFilter = toFilter(accessInfo.getGrant(), accessInfo.getCqlFilterWrite());
if (intersectsArea != null) {
- Filter areaFilter = FF.intersects(FF.property(""), FF.literal(intersectsArea));
+ Filter areaFilter = intersects(intersectsArea);
if (clipArea != null) {
- Filter intersectClipArea = FF.intersects(FF.property(""), FF.literal(clipArea));
+ Filter intersectClipArea = intersects(clipArea);
areaFilter = FF.or(areaFilter, intersectClipArea);
}
readFilter = mergeFilter(readFilter, areaFilter);
@@ -510,6 +556,10 @@ private VectorAccessLimits buildVectorAccessLimits(
return accessLimits;
}
+ private Intersects intersects(final Geometry intersectsArea) {
+ return FF.intersects(FF.property(""), FF.literal(intersectsArea));
+ }
+
private Geometry resolveIntersectsArea(
ResourceInfo info, AccessInfo accessInfo, ProcessingResult resultLimits) {
@@ -545,7 +595,7 @@ private Filter toFilter(GrantType actualGrant, @Nullable String cqlFilter) {
"Invalid cql filter found: " + e.getMessage(), e);
}
}
- boolean includeFilter = actualGrant == GrantType.ALLOW || actualGrant == GrantType.LIMIT;
+ boolean includeFilter = actualGrant == ALLOW || actualGrant == LIMIT;
return includeFilter ? Filter.INCLUDE : Filter.EXCLUDE;
}
@@ -557,7 +607,7 @@ LayerGroupAccessLimits buildLayerGroupAccessLimits(AccessInfo accessInfo) {
GrantType grant = accessInfo.getGrant();
// the SecureCatalog will grant access to the layerGroup
// if AccessLimits are null
- if (grant.equals(GrantType.ALLOW) || grant.equals(GrantType.LIMIT)) {
+ if (grant.equals(ALLOW) || grant.equals(LIMIT)) {
return null; // null == no-limits
}
CatalogMode catalogMode = convert(accessInfo.getCatalogMode());
@@ -575,7 +625,13 @@ private ProcessingResult getContainerResolverResult(
AccessManagerConfig configuration = configProvider.get();
ContainerLimitResolver resolver =
ContainerLimitResolver.of(
- containers, summaries, aclService, user, layer, workspace, configuration);
+ containers,
+ summaries,
+ authorizationService,
+ user,
+ layer,
+ workspace,
+ configuration);
ProcessingResult result = resolver.resolveResourceInGroupLimits();
Geometry intersect = result.getIntersectArea();
@@ -687,14 +743,15 @@ private List toPropertyNames(
return result;
}
- @Override
- public Filter getSecurityFilter(Authentication user, Class extends CatalogInfo> clazz) {
- // resort to the in-process filter until we can provide a faster alternative
- return super.getSecurityFilter(user, clazz);
+ private AccessSummary getAccessSummary(Authentication user) {
+ AccessSummaryRequest request = buildAccessSummaryRequest(user);
+
+ return authorizationService.getUserAccessSummary(request);
}
- @Override
- public int getPriority() {
- return ExtensionPriority.LOWEST;
+ private AccessSummaryRequest buildAccessSummaryRequest(Authentication user) {
+ String username = user.getName();
+ Set roles = roles(user).collect(Collectors.toSet());
+ return AccessSummaryRequest.builder().user(username).roles(roles).build();
}
}
diff --git a/src/plugin/accessmanager/src/main/java/org/geoserver/acl/plugin/accessmanager/CatalogSecurityFilterBuilder.java b/src/plugin/accessmanager/src/main/java/org/geoserver/acl/plugin/accessmanager/CatalogSecurityFilterBuilder.java
new file mode 100644
index 0000000..7ba0274
--- /dev/null
+++ b/src/plugin/accessmanager/src/main/java/org/geoserver/acl/plugin/accessmanager/CatalogSecurityFilterBuilder.java
@@ -0,0 +1,261 @@
+/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ *
+ * Original from GeoServer 2.24-SNAPSHOT under GPL 2.0 license
+ */
+package org.geoserver.acl.plugin.accessmanager;
+
+import static org.geoserver.acl.authorization.WorkspaceAccessSummary.ANY;
+import static org.geoserver.acl.authorization.WorkspaceAccessSummary.NO_WORKSPACE;
+import static org.geoserver.catalog.Predicates.*;
+import static org.geoserver.catalog.Predicates.and;
+import static org.geoserver.catalog.Predicates.equal;
+import static org.geoserver.catalog.Predicates.in;
+import static org.geoserver.catalog.Predicates.isInstanceOf;
+import static org.geoserver.catalog.Predicates.isNull;
+import static org.geoserver.catalog.Predicates.not;
+import static org.geoserver.catalog.Predicates.or;
+import static org.geotools.api.filter.Filter.EXCLUDE;
+import static org.geotools.api.filter.Filter.INCLUDE;
+
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.WorkspaceAccessSummary;
+import org.geoserver.catalog.CatalogInfo;
+import org.geoserver.catalog.LayerGroupInfo;
+import org.geoserver.catalog.LayerInfo;
+import org.geoserver.catalog.NamespaceInfo;
+import org.geoserver.catalog.PublishedInfo;
+import org.geoserver.catalog.ResourceInfo;
+import org.geoserver.catalog.StoreInfo;
+import org.geoserver.catalog.StyleInfo;
+import org.geoserver.catalog.WorkspaceInfo;
+import org.geotools.api.filter.Filter;
+import org.geotools.filter.visitor.SimplifyingFilterVisitor;
+import org.springframework.lang.NonNull;
+import org.springframework.util.Assert;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * @author Gabriel Roldan - Camptocamp
+ */
+public class CatalogSecurityFilterBuilder {
+
+ private final AccessSummary viewables;
+
+ public CatalogSecurityFilterBuilder(AccessSummary viewables) {
+ this.viewables = Objects.requireNonNull(viewables);
+ }
+
+ public static Filter buildSecurityFilter(
+ AccessSummary viewables, Class extends CatalogInfo> infoType) {
+ return new CatalogSecurityFilterBuilder(viewables).build(infoType);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Filter build(Class extends CatalogInfo> clazz) {
+ Objects.requireNonNull(clazz);
+ if (viewables.getWorkspaces().isEmpty()) {
+ return EXCLUDE;
+ }
+ if (WorkspaceInfo.class.isAssignableFrom(clazz)) {
+ return workspaceNameFilter("name");
+ }
+ if (NamespaceInfo.class.isAssignableFrom(clazz)) {
+ return workspaceNameFilter("prefix");
+ }
+ if (StoreInfo.class.isAssignableFrom(clazz)) {
+ return workspaceNameFilter("workspace.name");
+ }
+ if (ResourceInfo.class.isAssignableFrom(clazz)) {
+ return layerFilter("store.workspace.name", "name", ResourceInfo.class);
+ }
+ if (PublishedInfo.class.isAssignableFrom(clazz)) {
+ return publishedInfoFilter((Class extends PublishedInfo>) clazz);
+ }
+ if (StyleInfo.class.isAssignableFrom(clazz)) {
+ return styleFilter();
+ }
+ throw new UnsupportedOperationException(
+ "Unknown CatalogInfo type: " + clazz.getCanonicalName());
+ }
+
+ private Filter styleFilter() {
+ return workspaceNameFilter("workspace.name");
+ }
+
+ private Filter publishedInfoFilter(Class extends PublishedInfo> clazz) {
+ if (LayerInfo.class.isAssignableFrom(clazz)) {
+ return layerFilter("resource.store.workspace.name", "name", LayerInfo.class);
+ }
+ if (LayerGroupInfo.class.isAssignableFrom(clazz)) {
+ return layerFilter("workspace.name", "name", LayerGroupInfo.class);
+ }
+ Filter layerInfoFilter = build(LayerInfo.class);
+ Filter layerGroupInfoFilter = build(LayerGroupInfo.class);
+
+ Filter layerFilter = and(isInstanceOf(LayerInfo.class), layerInfoFilter);
+ Filter groupFilter = and(isInstanceOf(LayerGroupInfo.class), layerGroupInfoFilter);
+
+ if (EXCLUDE.equals(layerInfoFilter)) {
+ return groupFilter;
+ } else if (EXCLUDE.equals(layerGroupInfoFilter)) {
+ return layerFilter;
+ }
+
+ return or(layerFilter, groupFilter);
+ }
+
+ private Filter layerFilter(
+ String workspaceProperty, String nameProperty, Class extends CatalogInfo> type) {
+ List summaries = viewables.getWorkspaces();
+
+ Filter filter = acceptNone();
+ Set hideAllWorkspaceNames = new TreeSet<>();
+ for (WorkspaceAccessSummary wsSummary : summaries) {
+ String workspace = wsSummary.getWorkspace();
+ if (isHideAll(wsSummary)) {
+ hideAllWorkspaceNames.add(workspace);
+ } else {
+ boolean isNullWorkspace = NO_WORKSPACE.equals(workspace);
+ boolean supportsNullWorkspace = LayerGroupInfo.class.equals(type);
+ // ignore if workspace is null and type is LayerInfo or ResourceInfo
+ if (!isNullWorkspace || supportsNullWorkspace) {
+ Filter wsLayersFitler =
+ filterLayersOnWorkspace(wsSummary, workspaceProperty, nameProperty);
+
+ if (EXCLUDE.equals(filter)) {
+ filter = wsLayersFitler;
+ } else {
+ filter = or(filter, wsLayersFitler);
+ }
+ }
+ }
+ }
+ filter = prependHideAllWorkspaces(filter, workspaceProperty, hideAllWorkspaceNames);
+ return SimplifyingFilterVisitor.simplify(filter);
+ }
+
+ private Filter prependHideAllWorkspaces(
+ Filter filter, String workspaceProperty, Set hideAllWorkspaceNames) {
+ if (hideAllWorkspaceNames.isEmpty()) {
+ return filter;
+ }
+ Filter hiddenWorkspaces = denyWorkspacesFilter(workspaceProperty, hideAllWorkspaceNames);
+ if (INCLUDE.equals(filter)) {
+ return hiddenWorkspaces;
+ }
+ return and(hiddenWorkspaces, filter);
+ }
+
+ private Filter denyWorkspacesFilter(
+ String workspaceProperty, Set hideAllWorkspaceNames) {
+ Assert.isTrue(!hideAllWorkspaceNames.isEmpty(), "hidden workspace names can't be empty");
+ return notEqualOrIn(workspaceProperty, hideAllWorkspaceNames, EXCLUDE);
+ }
+
+ private boolean isHideAll(WorkspaceAccessSummary ws) {
+ return ws.getAllowed().isEmpty() && ws.getForbidden().contains(ANY);
+ }
+
+ @NonNull
+ private Filter filterLayersOnWorkspace(
+ WorkspaceAccessSummary vl, String workspaceProperty, String nameProperty) {
+
+ final String workspace = vl.getWorkspace();
+ final Set allowed = vl.getAllowed();
+ final Set forbidden = vl.getForbidden();
+
+ Filter workspaceFilter = workspaceNameFilter(workspaceProperty, Set.of(workspace));
+ Filter filter;
+ if (allowed.isEmpty() && forbidden.isEmpty()) {
+ filter = workspaceFilter;
+ } else {
+ Filter layerFilter = mergeLayers(nameProperty, allowed, forbidden);
+ filter = and(workspaceFilter, layerFilter);
+ }
+ return filter;
+ }
+
+ private Filter mergeLayers(String nameProperty, Set allowed, Set forbidden) {
+ Filter allowFilter = equalOrIn(nameProperty, allowed, INCLUDE);
+ Filter hideFilter = notEqualOrIn(nameProperty, forbidden, EXCLUDE);
+ if (INCLUDE.equals(hideFilter)) {
+ return allowFilter;
+ }
+ if (INCLUDE.equals(allowFilter)) {
+ return hideFilter;
+ }
+ // neither is include, conflates to the allow filter
+ return allowFilter;
+ }
+
+ /**
+ * @return {@link Filter#INCLUDE} if {@code names} is empty, {@code not(equalOrIn(nameProperty,
+ * names)} otherwise
+ */
+ private Filter notEqualOrIn(String nameProperty, Set names, Filter defaultIfAny) {
+ if (names.isEmpty()) return INCLUDE;
+ if (names.contains(ANY)) return defaultIfAny;
+
+ if (names.size() == 1) {
+ return notEqual(nameProperty, names.iterator().next());
+ }
+ return not(equalOrIn(nameProperty, names, /* has no effect */ defaultIfAny));
+ }
+
+ /**
+ * @return {@link Filter#INCLUDE} if {@code names} is empty, {@code defaultIfAny} if {@code
+ * names} contains {@code *}, a "{@code name = names.get(0)}" filter if {@code names} has a
+ * single element, a "{@code name IN(names)}" filter if {@code names} has multiple elements.
+ */
+ private Filter equalOrIn(String nameProperty, Set names, Filter defaultIfAny) {
+ if (names.isEmpty()) return INCLUDE;
+ if (names.contains(ANY)) return defaultIfAny;
+ if (names.size() == 1) {
+ return equal(nameProperty, names.iterator().next());
+ }
+ return in(nameProperty, List.copyOf(names));
+ }
+
+ private Set getVisibleWorkspaces() {
+ return viewables.visibleWorkspaces();
+ }
+
+ private Filter workspaceNameFilter(String workspaceProperty) {
+ Set visibleWorkspaces = getVisibleWorkspaces();
+ return workspaceNameFilter(workspaceProperty, visibleWorkspaces);
+ }
+
+ private Filter workspaceNameFilter(String workspaceProperty, Set visibleWorkspaces) {
+ if (visibleWorkspaces.contains(ANY)) {
+ return acceptAll();
+ }
+ Filter filter = acceptAll();
+ if (visibleWorkspaces.contains(NO_WORKSPACE)) {
+ filter = isNull(workspaceProperty);
+ visibleWorkspaces = new HashSet<>(visibleWorkspaces);
+ visibleWorkspaces.remove(NO_WORKSPACE);
+ }
+ if (!visibleWorkspaces.isEmpty()) {
+ List workspaces = List.copyOf(visibleWorkspaces);
+ Filter namesFilter;
+ if (workspaces.size() == 1) {
+ namesFilter = equal(workspaceProperty, workspaces.get(0));
+ } else {
+ namesFilter = in(workspaceProperty, workspaces);
+ }
+ if (INCLUDE.equals(filter)) {
+ filter = namesFilter;
+ } else {
+ filter = or(filter, namesFilter);
+ }
+ }
+ return filter;
+ }
+}
diff --git a/src/plugin/accessmanager/src/test/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManagerTest.java b/src/plugin/accessmanager/src/test/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManagerTest.java
index e056fcc..dcfef2c 100644
--- a/src/plugin/accessmanager/src/test/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManagerTest.java
+++ b/src/plugin/accessmanager/src/test/java/org/geoserver/acl/plugin/accessmanager/ACLResourceAccessManagerTest.java
@@ -44,6 +44,7 @@
import org.locationtech.jts.io.WKTReader;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
public class ACLResourceAccessManagerTest extends AclBaseTest {
@@ -181,6 +182,7 @@ public void testCiteLayerAccess() {
@Test
public void testWmsLimited() {
Authentication user = getUser("wmsuser", "wmsuser", "ROLE_AUTHENTICATED");
+ SecurityContextHolder.getContext().setAuthentication(user);
// check layer in the sf workspace with a wfs request
Request request = new Request();
diff --git a/src/plugin/accessmanager/src/test/java/org/geoserver/acl/plugin/accessmanager/CatalogSecurityFilterBuilderTest.java b/src/plugin/accessmanager/src/test/java/org/geoserver/acl/plugin/accessmanager/CatalogSecurityFilterBuilderTest.java
new file mode 100644
index 0000000..82d33d1
--- /dev/null
+++ b/src/plugin/accessmanager/src/test/java/org/geoserver/acl/plugin/accessmanager/CatalogSecurityFilterBuilderTest.java
@@ -0,0 +1,533 @@
+/* (c) 2024 Open Source Geospatial Foundation - all rights reserved
+ * This code is licensed under the GPL 2.0 license, available at the root
+ * application directory.
+ *
+ * Original from GeoServer 2.24-SNAPSHOT under GPL 2.0 license
+ */
+package org.geoserver.acl.plugin.accessmanager;
+
+import static org.geoserver.acl.authorization.AccessSummary.of;
+import static org.geoserver.acl.plugin.accessmanager.CatalogSecurityFilterBuilder.buildSecurityFilter;
+import static org.geoserver.catalog.Predicates.*;
+import static org.geoserver.catalog.Predicates.and;
+import static org.geoserver.catalog.Predicates.in;
+import static org.geoserver.catalog.Predicates.isInstanceOf;
+import static org.geoserver.catalog.Predicates.isNull;
+import static org.geoserver.catalog.Predicates.not;
+import static org.geoserver.catalog.Predicates.or;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import org.geoserver.acl.authorization.AccessSummary;
+import org.geoserver.acl.authorization.WorkspaceAccessSummary;
+import org.geoserver.acl.domain.adminrules.AdminGrantType;
+import org.geoserver.catalog.CatalogInfo;
+import org.geoserver.catalog.CoverageInfo;
+import org.geoserver.catalog.CoverageStoreInfo;
+import org.geoserver.catalog.DataStoreInfo;
+import org.geoserver.catalog.FeatureTypeInfo;
+import org.geoserver.catalog.LayerGroupInfo;
+import org.geoserver.catalog.LayerInfo;
+import org.geoserver.catalog.NamespaceInfo;
+import org.geoserver.catalog.PublishedInfo;
+import org.geoserver.catalog.ResourceInfo;
+import org.geoserver.catalog.StoreInfo;
+import org.geoserver.catalog.StyleInfo;
+import org.geoserver.catalog.WMSLayerInfo;
+import org.geoserver.catalog.WMSStoreInfo;
+import org.geoserver.catalog.WMTSLayerInfo;
+import org.geoserver.catalog.WMTSStoreInfo;
+import org.geoserver.catalog.WorkspaceInfo;
+import org.geotools.api.filter.Filter;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Set;
+
+public class CatalogSecurityFilterBuilderTest {
+
+ @Test
+ public void unknownCatalogInfoArgument() {
+ AccessSummary accessSummary = of(workspace("cite"));
+ UnsupportedOperationException ex =
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> buildSecurityFilter(accessSummary, CatalogInfo.class));
+ assertThat(ex.getMessage(), containsString("Unknown CatalogInfo type"));
+ }
+
+ @Test
+ public void emptyAccessSummary() {
+ AccessSummary accessSummary = AccessSummary.of(List.of());
+ Filter expected = Filter.EXCLUDE;
+ List.of(
+ WorkspaceInfo.class,
+ NamespaceInfo.class,
+ StoreInfo.class,
+ ResourceInfo.class,
+ PublishedInfo.class,
+ LayerInfo.class,
+ LayerGroupInfo.class,
+ StyleInfo.class)
+ .forEach(type -> assertEquals(expected, buildSecurityFilter(accessSummary, type)));
+ }
+
+ @Test
+ public void workspaceInfoFilter() {
+ AccessSummary accessSummary = of(workspace("cite"));
+ Filter filter = buildSecurityFilter(accessSummary, WorkspaceInfo.class);
+ Filter expected = equal("name", "cite");
+ assertEquals(expected, filter);
+ }
+
+ @Test
+ public void workspaceInfoFilterMany() {
+ AccessSummary accessSummary = of(workspace("cite"), workspace("ne"), workspace("topp"));
+ Filter filter = buildSecurityFilter(accessSummary, WorkspaceInfo.class);
+ Filter expected = in("name", List.of("cite", "ne", "topp"));
+ assertEquals(expected, filter);
+ }
+
+ @Test
+ public void workspaceInfoFilterManyContainsWildcard() {
+ AccessSummary accessSummary =
+ of(workspace("*"), builder("ne").adminAccess(AdminGrantType.ADMIN).build());
+ Filter filter = buildSecurityFilter(accessSummary, WorkspaceInfo.class);
+ Filter expected = Filter.INCLUDE;
+ assertEquals(expected, filter);
+ }
+
+ @Test
+ public void namespaceInfoFilter() {
+ AccessSummary accessSummary = of(workspace("cite"));
+ Filter filter = buildSecurityFilter(accessSummary, NamespaceInfo.class);
+ Filter expected = equal("prefix", "cite");
+ assertEquals(expected, filter);
+ }
+
+ @Test
+ public void namespaceInfoFilterMany() {
+ AccessSummary accessSummary = of(workspace("cite"), workspace("ne"), workspace("topp"));
+ Filter filter = buildSecurityFilter(accessSummary, NamespaceInfo.class);
+ Filter expected = in("prefix", List.of("cite", "ne", "topp"));
+ assertEquals(expected, filter);
+ }
+
+ @Test
+ public void storeInfoFilter() {
+ AccessSummary accessSummary = of(workspace("cite"));
+ Filter expected = equal("workspace.name", "cite");
+ List.of(
+ StoreInfo.class,
+ DataStoreInfo.class,
+ CoverageStoreInfo.class,
+ WMSStoreInfo.class,
+ WMTSStoreInfo.class)
+ .forEach(type -> assertEquals(expected, buildSecurityFilter(accessSummary, type)));
+ }
+
+ @Test
+ public void storeInfoFilterMany() {
+ AccessSummary accessSummary = of(workspace("cite"), workspace("ne"), workspace("topp"));
+ Filter expected = in("workspace.name", List.of("cite", "ne", "topp"));
+ List.of(
+ StoreInfo.class,
+ DataStoreInfo.class,
+ CoverageStoreInfo.class,
+ WMSStoreInfo.class,
+ WMTSStoreInfo.class)
+ .forEach(type -> assertEquals(expected, buildSecurityFilter(accessSummary, type)));
+ }
+
+ @Test
+ public void resourceInfoFilterWhenWorkspaceSummaryHasNoLayers() {
+ AccessSummary accessSummary = of(workspace("cite"));
+ Filter expected = equal("store.workspace.name", "cite");
+ List.of(
+ ResourceInfo.class,
+ FeatureTypeInfo.class,
+ CoverageInfo.class,
+ WMSLayerInfo.class,
+ WMTSLayerInfo.class)
+ .forEach(type -> assertEquals(expected, buildSecurityFilter(accessSummary, type)));
+ }
+
+ @Test
+ public void resourceInfoFilterManyWhenWorkspaceSummaryHasNoLayers() {
+ AccessSummary accessSummary = of(workspace("cite"), workspace("ne"), workspace("topp"));
+ Filter expected =
+ or(
+ equal("store.workspace.name", "cite"),
+ equal("store.workspace.name", "ne"),
+ equal("store.workspace.name", "topp"));
+ List.of(
+ ResourceInfo.class,
+ FeatureTypeInfo.class,
+ CoverageInfo.class,
+ WMSLayerInfo.class,
+ WMTSLayerInfo.class)
+ .forEach(type -> assertEquals(expected, buildSecurityFilter(accessSummary, type)));
+ }
+
+ @Test
+ public void resourceInfoFilterSingleLayer() {
+ AccessSummary accessSummary = of(workspace("cite", "layer1"));
+ Filter expected = and(equal("store.workspace.name", "cite"), equal("name", "layer1"));
+ List.of(
+ ResourceInfo.class,
+ FeatureTypeInfo.class,
+ CoverageInfo.class,
+ WMSLayerInfo.class,
+ WMTSLayerInfo.class)
+ .forEach(type -> assertEquals(expected, buildSecurityFilter(accessSummary, type)));
+ }
+
+ @Test
+ public void resourceInfoFilterMultipleLayers() {
+ AccessSummary accessSummary = of(workspace("cite", "layer1", "layer2"));
+ List layers = List.copyOf(accessSummary.workspace("cite").getAllowed());
+ Filter expected = and(equal("store.workspace.name", "cite"), in("name", layers));
+ assertEquals(expected, buildSecurityFilter(accessSummary, ResourceInfo.class));
+ }
+
+ @Test
+ public void resourceInfoFilterMultipleWorkspacesMultipleLayers() {
+ AccessSummary accessSummary =
+ of( //
+ workspace("cite", "layer1", "layer2"), //
+ workspace("ne", "layer3", "layer4"), //
+ workspace("topp") //
+ );
+
+ List citelayers = List.copyOf(accessSummary.workspace("cite").getAllowed());
+ Filter citefilter = and(equal("store.workspace.name", "cite"), in("name", citelayers));
+
+ List nelayers = List.copyOf(accessSummary.workspace("ne").getAllowed());
+ Filter nefilter = and(equal("store.workspace.name", "ne"), in("name", nelayers));
+
+ Filter toppfilter = equal("store.workspace.name", "topp");
+
+ Filter expected = or(citefilter, nefilter, toppfilter);
+ assertEquals(expected, buildSecurityFilter(accessSummary, ResourceInfo.class));
+ }
+
+ @Test
+ public void resourceInfoFilterMultipleLayersAndWildcard() {
+ AccessSummary accessSummary = of(workspace("cite", "layer1", "layer2", "*"));
+ Filter expected = equal("store.workspace.name", "cite");
+ assertEquals(expected, buildSecurityFilter(accessSummary, ResourceInfo.class));
+ }
+
+ @Test
+ public void layerInfoFilter() {
+ AccessSummary accessSummary = of(workspace("cite", "layer1", "layer2"));
+ List layers = List.copyOf(accessSummary.workspace("cite").getAllowed());
+ Filter expected = and(equal("resource.store.workspace.name", "cite"), in("name", layers));
+ assertEquals(expected, buildSecurityFilter(accessSummary, LayerInfo.class));
+ }
+
+ @Test
+ public void layerInfoFilterAllVisibleAndSomeHiddenLayers() {
+ Set visible = Set.of("*");
+ Set hidden = Set.of("hidden1", "hidden2");
+ AccessSummary accessSummary = of(workspace("cite", visible, hidden));
+
+ List layers = List.copyOf(accessSummary.workspace("cite").getForbidden());
+ assertEquals(hidden, Set.copyOf(layers));
+
+ // conflates to negating the hidden ones only
+ Filter expected =
+ and(equal("resource.store.workspace.name", "cite"), not(in("name", layers)));
+
+ Filter actual = buildSecurityFilter(accessSummary, LayerInfo.class);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void layerInfoFilterAllHiddenAndSomeVisibleLayers() {
+ Set visible = Set.of("visible1", "visible2");
+ Set hidden = Set.of("*");
+ AccessSummary accessSummary = of(workspace("cite", visible, hidden));
+
+ List visibleNames = List.copyOf(accessSummary.workspace("cite").getAllowed());
+ assertEquals(visible, Set.copyOf(visibleNames));
+
+ // conflates to visibles only
+ Filter expected =
+ and(equal("resource.store.workspace.name", "cite"), in("name", visibleNames));
+ Filter actual = buildSecurityFilter(accessSummary, LayerInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void layerInfoFilterSomeVisibleAndSomeHiddenLayers() {
+ Set visible = Set.of("visible1", "visible2");
+ Set hidden = Set.of("hidden1", "hidden2");
+ AccessSummary accessSummary = of(workspace("cite", visible, hidden));
+
+ List visibleNames = List.copyOf(accessSummary.workspace("cite").getAllowed());
+ assertEquals(visible, Set.copyOf(visibleNames));
+
+ // conflates to visibles only
+ Filter expected =
+ and(equal("resource.store.workspace.name", "cite"), in("name", visibleNames));
+
+ Filter actual = buildSecurityFilter(accessSummary, LayerInfo.class);
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void layerInfoFilterAllowAllButOneWorkspace() {
+ AccessSummary accessSummary =
+ of(
+ // allow all
+ workspace("*", "*"),
+ // but hide all from cite
+ workspace("cite", Set.of(), Set.of("*")));
+
+ // conflates to hidding the cite workspace
+ Filter expected = notEqual("resource.store.workspace.name", "cite");
+
+ Filter actual = buildSecurityFilter(accessSummary, LayerInfo.class);
+ assertEquals(expected, actual);
+
+ // same thing if the order is reversed
+ accessSummary =
+ of(
+ // hide all from cite
+ workspace("cite", Set.of(), Set.of("*")),
+ // allow everything else
+ workspace("*", "*"));
+
+ actual = buildSecurityFilter(accessSummary, LayerInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void layerInfoFilterAllowAllButSomeWorkspaces() {
+ AccessSummary accessSummary =
+ of(
+ workspace("*", "*"), // allow all
+ // but hide all from cite and ne workspaces
+ workspace("cite", Set.of(), Set.of("*")),
+ workspace("ne", Set.of(), Set.of("*")));
+
+ // conflates to hidding the cite and ne workspaces
+ Filter expected = not(in("resource.store.workspace.name", List.of("cite", "ne")));
+
+ Filter actual = buildSecurityFilter(accessSummary, LayerInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void layerInfoFilterMultipleWorkspacesMultipleLayers() {
+ AccessSummary accessSummary =
+ of( //
+ workspace("cite", "layer1", "layer2"), //
+ workspace("ne", "layer3", "layer4"), //
+ workspace("topp") //
+ );
+
+ List citelayers = List.copyOf(accessSummary.workspace("cite").getAllowed());
+ Filter citefilter =
+ and(equal("resource.store.workspace.name", "cite"), in("name", citelayers));
+
+ List nelayers = List.copyOf(accessSummary.workspace("ne").getAllowed());
+ Filter nefilter = and(equal("resource.store.workspace.name", "ne"), in("name", nelayers));
+
+ Filter toppfilter = equal("resource.store.workspace.name", "topp");
+
+ Filter expected = or(citefilter, nefilter, toppfilter);
+ Filter actual = buildSecurityFilter(accessSummary, LayerInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void layerGroupInfoFilterNoWorkspace() {
+ AccessSummary accessSummary = of(workspace("", "lg1"));
+ Filter workspaceFilter = isNull("workspace.name");
+ Filter nameFilter = equal("name", "lg1");
+ Filter expected = and(workspaceFilter, nameFilter);
+ assertEquals(expected, buildSecurityFilter(accessSummary, LayerGroupInfo.class));
+ }
+
+ @Test
+ public void layerGroupInfoFilterNoWorkspaceAll() {
+ AccessSummary accessSummary = of(workspace("", "*"));
+ Filter workspaceFilter = isNull("workspace.name");
+ assertEquals(workspaceFilter, buildSecurityFilter(accessSummary, LayerGroupInfo.class));
+ }
+
+ @Test
+ public void layerGroupInfoFilterNoWorkspaceMany() {
+ AccessSummary accessSummary = of(workspace("", "lg1", "lg2"));
+ List layers = List.copyOf(accessSummary.workspace("").getAllowed());
+ Filter workspaceFilter = isNull("workspace.name");
+ Filter nameFilter = in("name", layers);
+ Filter expected = and(workspaceFilter, nameFilter);
+ assertEquals(expected, buildSecurityFilter(accessSummary, LayerGroupInfo.class));
+ }
+
+ @Test
+ public void publishedInfoInfoFilterOnlyRootLayerGroups() {
+ AccessSummary accessSummary = of(workspace("", "lg1", "lg2"));
+
+ List rootLgs = List.copyOf(accessSummary.workspace("").getAllowed());
+ Filter rootLgFilters = and(isNull("workspace.name"), in("name", rootLgs));
+
+ Filter expected = and(isInstanceOf(LayerGroupInfo.class), rootLgFilters);
+ Filter actual = buildSecurityFilter(accessSummary, PublishedInfo.class);
+
+ // workaround for IsInstanceOf function not implementing equals()
+ assertEquals(expected.toString(), actual.toString());
+ }
+
+ /**
+ *
+ *
+ *
+ *
+ * [
+ * [
+ * [ IsInstanceOf(interface org.geoserver.catalog.LayerInfo) = true ]
+ * AND [ resource.store.workspace.name = ne ] AND [ in([name], [populated_places], [world]) = true ]
+ * ]
+ * OR
+ * [
+ * [ IsInstanceOf(interface org.geoserver.catalog.LayerGroupInfo) = true ]
+ * AND [
+ * [[ workspace.name IS NULL ] AND [ in([name], [lg2], [lg1]) = true ]]
+ * OR
+ * [[ workspace.name = ne ] AND [ in([name], [populated_places], [world]) = true ]]
+ * ]
+ * ]
+ * ]
+ *
+ *
+ */
+ @Test
+ public void publishedInfoInfoFilterMixingRootLayerGroupsAndWorkspaceLayerNames() {
+ AccessSummary accessSummary =
+ of(workspace("", "lg1", "lg2"), workspace("ne", "world", "populated_places"));
+
+ List nelayers = List.copyOf(accessSummary.workspace("ne").getAllowed());
+
+ Filter layerInfoFilter =
+ and(
+ isInstanceOf(LayerInfo.class),
+ and(equal("resource.store.workspace.name", "ne"), in("name", nelayers)));
+
+ List rootLgs = List.copyOf(accessSummary.workspace("").getAllowed());
+ Filter rootLgFilters = and(isNull("workspace.name"), in("name", rootLgs));
+ Filter workspaceLgFilters = and(equal("workspace.name", "ne"), in("name", nelayers));
+
+ Filter layerGroupInfoFilter =
+ and(isInstanceOf(LayerGroupInfo.class), or(rootLgFilters, workspaceLgFilters));
+
+ Filter expected = or(layerInfoFilter, layerGroupInfoFilter);
+ Filter actual = buildSecurityFilter(accessSummary, PublishedInfo.class);
+ // workaround for IsInstanceOf function not implementing equals()
+ assertEquals(expected.toString(), actual.toString());
+ }
+
+ @Test
+ public void publishedInfoInfoFilterMultipleWorkspaces() {
+ AccessSummary accessSummary =
+ of(workspace("ne", "world"), workspace("cite", "test"), workspace("topp", "*"));
+
+ Filter layerFilter =
+ and(
+ isInstanceOf(LayerInfo.class),
+ or(
+ and(
+ equal("resource.store.workspace.name", "ne"),
+ equal("name", "world")),
+ and(
+ equal("resource.store.workspace.name", "cite"),
+ equal("name", "test")),
+ equal("resource.store.workspace.name", "topp")));
+
+ Filter layerGroupFilter =
+ and(
+ isInstanceOf(LayerGroupInfo.class),
+ or(
+ and(equal("workspace.name", "ne"), equal("name", "world")),
+ and(equal("workspace.name", "cite"), equal("name", "test")),
+ equal("workspace.name", "topp")));
+
+ Filter expected = or(layerFilter, layerGroupFilter);
+ Filter actual = buildSecurityFilter(accessSummary, PublishedInfo.class);
+ // workaround for IsInstanceOf function not implementing equals()
+ assertEquals(expected.toString(), actual.toString());
+ }
+
+ @Test
+ public void styleWorkspaceNullFilter() {
+ AccessSummary accessSummary = of(workspace("", "rootlg"));
+
+ Filter expected = isNull("workspace.name");
+ Filter actual = buildSecurityFilter(accessSummary, StyleInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void styleWorkspaceFilter() {
+ AccessSummary accessSummary = of(workspace("ne", "world"));
+
+ Filter expected = equal("workspace.name", "ne");
+ Filter actual = buildSecurityFilter(accessSummary, StyleInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void styleMultipleWorkspaceFilter() {
+ AccessSummary accessSummary = of(workspace("ne", "world"), workspace("cite", "*"));
+
+ Filter expected = in("workspace.name", List.of("ne", "cite"));
+ Filter actual = buildSecurityFilter(accessSummary, StyleInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void styleHiddenWorkspaceFilter() {
+
+ Set visible = Set.of("*");
+ Set hidden = Set.of("hidden1", "hidden2");
+ AccessSummary accessSummary = of(workspace("cite", visible, hidden));
+
+ Filter expected = equal("workspace.name", "cite");
+ Filter actual = buildSecurityFilter(accessSummary, StyleInfo.class);
+ assertEquals(
+ "hidden layers do not affect workspace visibility for styles", expected, actual);
+ }
+
+ @Test
+ public void styleMultipleWorkspaceAndNoWorkspaceFilter() {
+ AccessSummary accessSummary =
+ of(workspace("ne", "world"), workspace("cite", "*"), workspace("", "rootlg"));
+
+ Filter expected = or(isNull("workspace.name"), in("workspace.name", List.of("ne", "cite")));
+ Filter actual = buildSecurityFilter(accessSummary, StyleInfo.class);
+ assertEquals(expected, actual);
+ }
+
+ private WorkspaceAccessSummary workspace(String workspace, String... visibleLayers) {
+ Set allowed = null == visibleLayers ? Set.of() : Set.of(visibleLayers);
+ return workspace(workspace, allowed, Set.of());
+ }
+
+ private WorkspaceAccessSummary workspace(
+ String workspace, Set visibleLayers, Set hiddenLayers) {
+ return builder(workspace).allowed(visibleLayers).forbidden(hiddenLayers).build();
+ }
+
+ private WorkspaceAccessSummary.Builder builder(String workspace) {
+ // adminGrant is irrelevant to build the filter
+ AdminGrantType adminGrant = AdminGrantType.USER;
+ return WorkspaceAccessSummary.builder().workspace(workspace).adminAccess(adminGrant);
+ }
+}