Skip to content

Commit

Permalink
WIP API to obtain a quick summary of adminable workspaces and visible…
Browse files Browse the repository at this point in the history
… layers
  • Loading branch information
groldan committed Aug 3, 2024
1 parent c0b4568 commit f95f911
Show file tree
Hide file tree
Showing 33 changed files with 2,348 additions and 111 deletions.
468 changes: 468 additions & 0 deletions docs/api/index.html

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/application/authorization-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,10 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* (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.EqualsAndHashCode;
import lombok.NonNull;

import org.geoserver.acl.domain.adminrules.AdminGrantType;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;

@EqualsAndHashCode
public class AccessSummary {

private static final WorkspaceAccessSummary HIDDEN =
WorkspaceAccessSummary.builder()
.workspace("*")
.adminAccess(null)
.addForbidden("*")
.build();

/** Immutable mapping of workspace name to summary */
private Map<String, WorkspaceAccessSummary> workspaceSummaries;

private AccessSummary(Map<String, WorkspaceAccessSummary> workspaceSummaries) {
this.workspaceSummaries = workspaceSummaries;
}

public static AccessSummary of(List<WorkspaceAccessSummary> workspaces) {
Map<String, WorkspaceAccessSummary> summaries =
workspaces.stream()
.collect(
Collectors.toMap(
WorkspaceAccessSummary::getWorkspace, Function.identity()));
return new AccessSummary(summaries);
}

public List<WorkspaceAccessSummary> getWorkspaces() {
return List.copyOf(workspaceSummaries.values());
}

public WorkspaceAccessSummary workspace(String workspace) {
return workspaceSummaries.get(workspace);
}

public boolean hasAdminReadAccess(@NonNull String workspaceName) {
boolean user = workspaceSummaries.getOrDefault("*", HIDDEN).isUser();
return user ? user : workspaceSummaries.getOrDefault(workspaceName, HIDDEN).isUser();
}

public boolean hasAdminWriteAccess(@NonNull String workspaceName) {
boolean admin = workspaceSummaries.getOrDefault("*", HIDDEN).isAdmin();
return admin ? admin : workspaceSummaries.getOrDefault(workspaceName, HIDDEN).isAdmin();
}

public boolean canSeeLayer(String workspaceName, @NonNull String layerName) {
if (null == workspaceName) workspaceName = WorkspaceAccessSummary.NO_WORKSPACE;
WorkspaceAccessSummary summary = summary(workspaceName);
return summary.canSeeLayer(layerName);
}

private WorkspaceAccessSummary summary(@NonNull String workspaceName) {
var summary = workspaceSummaries.get(workspaceName);
if (null == summary) summary = workspaceSummaries.getOrDefault("*", HIDDEN);
return summary;
}

public Set<String> visibleWorkspaces() {
return workspaceSummaries.keySet();
}

public Set<String> adminableWorkspaces() {
return workspaceSummaries.keySet().stream()
.filter(this::hasAdminWriteAccess)
.collect(Collectors.toSet());
}

@Override
public String toString() {
var values = new TreeMap<>(workspaceSummaries).values();
return "%s[%s]".formatted(getClass().getSimpleName(), values);
}

public boolean hasAdminRightsToAnyWorkspace() {
return workspaceSummaries.values().stream()
.map(WorkspaceAccessSummary::getAdminAccess)
.anyMatch(AdminGrantType.ADMIN::equals);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* (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 java.util.Set;

/**
* Represents the converged set of visible layer names of a specific workspace for for a {@link
* AccessSummaryRequest}
*
* @since 2.3
* @see WorkspaceAccessSummary
*/
@Value
@Builder(builderClassName = "Builder")
public class AccessSummaryRequest {

/**
* The authentication user name on behalf of which the reques is performed, {@code null} only if
* the request is anonymous, and hence {@link #roles} would contain some role name with
* anonymous meaning (usually {@literal ROLE_ANONYMOUS}).
*/
private String user;

/** The authentication role names on behalf of which the request is performed. */
@NonNull private Set<String> roles;

public static class Builder {
// Ignore squid:S1068, private field required for the lombok-generated build() method
@SuppressWarnings({"unused", "squid:S1068"})
private Set<String> roles = Set.of();

public Builder roles(String... roleNames) {
if (null == roleNames) return roles(Set.of());
return roles(Set.of(roleNames));
}

public Builder roles(Set<String> roleNames) {
if (null == roleNames) {
this.roles = Set.of();
return this;
}
this.roles = Set.copyOf(roleNames);
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public interface AuthorizationService {
*/
AdminAccessInfo getAdminAuthorization(AdminAccessRequest request);

AccessSummary getUserAccessSummary(AccessSummaryRequest request);

/**
* Return the unprocessed {@link Rule} list matching a given filter, sorted by priority.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WorkspaceAccessSummary> {
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<String> 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.
*
* <p>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<String> 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<String> allowed = new TreeSet<>();
private Set<String> forbidden = new TreeSet<>();

public Builder allowed(@NonNull Set<String> layers) {
this.allowed = new TreeSet<>(layers);
forbidden.removeAll(layers);
return this;
}

public Builder forbidden(@NonNull Set<String> 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<String> allowedLayers = conflate(allowed);
Set<String> forbiddenLayers = conflate(forbidden);

return new WorkspaceAccessSummary(
workspace, adminAccess, allowedLayers, forbiddenLayers);
}

private static Set<String> conflate(Set<String> 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;
}
}
Loading

0 comments on commit f95f911

Please sign in to comment.