Skip to content

Commit

Permalink
Authrorization response caching and spring cloud bus integration
Browse files Browse the repository at this point in the history
Implement a caching `AuthorizationService`, and integrate with the
GeoServer plugin. Enabled by default through the
`geoserver.acl.client.caching` boolean configuration property.

Integrate server and client event-bus communication through `RuleEvent`
and `AdminRuleEvent`, using the spring-cloud-bus and RabbitMQ as default
binder.
Cache eviction occurs upon rule events, evicting the cached
authorization responses affected by the changed rules.
Disabled by default, and enabled through the `geoserver.bus.enabled`
boolean configuration property.

The server configuration got the following new properties in
`values.yml` (copied to /etc/geoserver/acl-service.yml in the Docker
image):

```
geoserver.bus.enabled: false
rabbitmq.host: rabbitmq
rabbitmq.port: 5672
rabbitmq.user: guest
rabbitmq.password: guest
```

The client plugin configuration got the following new properties:

```
geoserver.acl.client.caching: true
geoserver.acl.client.startupCheck: true
geoserver.acl.client.initTimeout: 10
```
  • Loading branch information
groldan committed Mar 12, 2024
1 parent 0689940 commit 389ffa5
Show file tree
Hide file tree
Showing 44 changed files with 1,472 additions and 286 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,21 @@
<artifactId>gs-acl-domain-spring-integration</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.geoserver.acl.integration</groupId>
<artifactId>gs-acl-cache</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.geoserver.acl.integration</groupId>
<artifactId>spring-boot-simplejndi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.geoserver.acl.integration</groupId>
<artifactId>gs-acl-spring-cloud-bus</artifactId>
<version>${project.version}</version>
</dependency>
<!-- GeoServer plugin components -->
<dependency>
<groupId>org.geoserver.acl.plugin</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,19 @@ public class AccessInfo {

@Override
public String toString() {
return String.format(
"AccessInfo[grant: %s, catalogMode: %s, area: %s, clip: %s, styles[def: %s, allowed: %s], cql[r: %s, w: %s], atts: %s",
grant,
catalogMode,
area == null ? "no" : "yes",
clipArea == null ? "no" : "yes",
defaultStyle,
allowedStyles == null || allowedStyles.isEmpty() ? "no" : allowedStyles.size(),
cqlFilterRead == null ? "no" : "yes",
cqlFilterWrite == null ? "no" : "yes",
attributes == null || attributes.isEmpty() ? "no" : attributes.size());
StringBuilder sb = new StringBuilder("AccessInfo[grant: ").append(grant);
if (catalogMode != null) sb.append(", catalogMode: ").append(catalogMode);
if (area != null) sb.append(", area: yes");
if (clipArea != null) sb.append(", clipArea: yes");
if (defaultStyle != null) sb.append(", def style: ").append(defaultStyle);
if (null != allowedStyles && !allowedStyles.isEmpty())
sb.append("allowed styles: ").append(allowedStyles);
if (cqlFilterRead != null) sb.append(", cql read filter: present");
if (cqlFilterWrite != null) sb.append(", cql write filter: present");
if (null != attributes && !attributes.isEmpty())
sb.append(", attributes: ").append(attributes.size());
sb.append(", matchingRules: ").append(matchingRules);
return sb.append("]").toString();
}

public static class Builder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,16 @@ public AccessRequest validate() {
}

public @Override String toString() {
return String.format(
"%s[from:%s, by: %s(%s), for:%s:%s%s, layer:%s%s]",
getClass().getSimpleName(),
sourceAddress == null ? "<no IP>" : sourceAddress,
user,
roles.stream().collect(Collectors.joining(", ")),
service,
request,
subfield == null ? "" : "(" + subfield + ")",
workspace == null ? "<null ws>" : (workspace.isEmpty() ? "" : workspace + ":"),
layer);
StringBuilder sb = new StringBuilder("AccessRequest[");
sb.append("user: ").append(user == null ? "" : user);
sb.append(", roles: ").append(roles.stream().collect(Collectors.joining(",")));
if (null != sourceAddress) sb.append(", origin IP: ").append(sourceAddress);
if (null != service) sb.append(", service: ").append(service);
if (null != request) sb.append(", request: ").append(request);
if (null != subfield) sb.append(", subfield: ").append(subfield);
if (null != workspace) sb.append(", workspace: ").append(workspace);
if (null != layer) sb.append(", layer: ").append(layer);
return sb.append("]").toString();
}

public static class Builder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* (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 GeoFence 3.6 under GPL 2.0 license
*/
package org.geoserver.acl.authorization;

import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Delegate;

/**
* @since 2.0
*/
@RequiredArgsConstructor
public abstract class ForwardingAuthorizationService implements AuthorizationService {

@NonNull @Delegate @Getter private final AuthorizationService delegate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.geoserver.acl.domain.rules.SpatialFilterType.CLIP;
import static org.geoserver.acl.domain.rules.SpatialFilterType.INTERSECT;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -63,7 +64,7 @@
*
* @author Emanuele Tajariol (etj at geo-solutions.it) (originally as part of GeoFence)
*/
@Slf4j
@Slf4j(topic = "org.geoserver.acl.authorization")
@RequiredArgsConstructor
public class AuthorizationServiceImpl implements AuthorizationService {

Expand All @@ -75,7 +76,7 @@ public class AuthorizationServiceImpl implements AuthorizationService {
* @return a plain List of the grouped matching Rules.
*/
@Override
public List<Rule> getMatchingRules(AccessRequest request) {
public List<Rule> getMatchingRules(@NonNull AccessRequest request) {
request = request.validate();
Map<String, List<Rule>> found = getMatchingRulesByRole(request);
return flatten(found);
Expand All @@ -90,7 +91,7 @@ private List<Rule> flatten(Map<String, List<Rule>> found) {
}

@Override
public AccessInfo getAccessInfo(AccessRequest request) {
public AccessInfo getAccessInfo(@NonNull AccessRequest request) {
request = request.validate();

Map<String, List<Rule>> groupedRules = getMatchingRulesByRole(request);
Expand All @@ -107,12 +108,12 @@ public AccessInfo getAccessInfo(AccessRequest request) {

List<String> matchingIds = flatten(groupedRules).stream().map(Rule::getId).toList();
ret = ret.withMatchingRules(matchingIds);
log.debug("Request: {}, response: {}", ret, request);
log.debug("Request: {}, response: {}", request, ret);
return ret;
}

@Override
public AdminAccessInfo getAdminAuthorization(AdminAccessRequest request) {
public AdminAccessInfo getAdminAuthorization(@NonNull AdminAccessRequest request) {
Optional<AdminRule> adminAuth = getAdminAuth(request);
boolean adminRigths = isAdminAuth(adminAuth);
String adminRuleId = adminAuth.map(AdminRule::getId).orElse(null);
Expand Down
12 changes: 12 additions & 0 deletions src/artifacts/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@
<groupId>org.geoserver.acl.integration</groupId>
<artifactId>spring-boot-simplejndi</artifactId>
</dependency>
<dependency>
<groupId>org.geoserver.acl.integration</groupId>
<artifactId>gs-acl-spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-main</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* (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.autoconfigure.bus;

import lombok.extern.slf4j.Slf4j;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Import;

import javax.annotation.PostConstruct;

/**
* {@link org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration} is disabled in
* {@literal application.yml}; this auto configuration enables it when the {@literal
* geoserver.bus.enabled} configuration property is {@code true}.
*/
@AutoConfiguration
@ConditionalOnProperty(name = "geoserver.bus.enabled", havingValue = "true", matchIfMissing = false)
@Import(org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.class)
@Slf4j
public class RabbitAutoConfiguration {

@PostConstruct
void log() {
log.info("Loading RabbitMQ bus bridge");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ org.geoserver.acl.autoconfigure.security.AclServiceSecurityAutoConfiguration,\
org.geoserver.acl.autoconfigure.security.InternalSecurityConfiguration,\
org.geoserver.acl.autoconfigure.security.PreAuthenticationSecurityAutoConfiguration,\
org.geoserver.acl.autoconfigure.security.AuthenticationManagerAutoConfiguration,\
org.geoserver.acl.autoconfigure.springdoc.SpringDocAutoConfiguration
org.geoserver.acl.autoconfigure.springdoc.SpringDocAutoConfiguration,\
org.geoserver.acl.autoconfigure.bus.RabbitAutoConfiguration

123 changes: 76 additions & 47 deletions src/artifacts/api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
info:
component: Access Control List service
instance-id: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${spring.cloud.client.ip-address}}:${server.port}}
# acl-service main application configuration
# Do not edit this file. All configurable options are to be placed in the sibling values.yml,
# provided on a separate file and defined through spring.config.additional-location:file:<path to file>,
Expand Down Expand Up @@ -58,15 +61,38 @@ spring:
- org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
- org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
jpa:
open-in-view: false
rabbitmq:
host: ${rabbitmq.host:rabbitmq}
port: ${rabbitmq.port:5672}
username: ${rabbitmq.user:guest}
password: ${rabbitmq.password:guest}
virtual-host: ${rabbitmq.vhost:}
cloud:
bus:
enabled: ${geoserver.bus.enabled}
id: ${info.instance-id}
trace.enabled: false #switch on tracing of acks (default off).
stream:
bindings:
# same bindings as for geoserver cloud
springCloudBusOutput:
destination: geoserver
springCloudBusInput:
destination: geoserver

management:
server:
base-path: /acl
port: 8081
endpoint:
health.probes.enabled: true
health:
probes:
enabled: true
show-components: when-authorized
show-details: when-authorized
caches.enabled: true
endpoints:
web.exposure.include: ['*']
Expand Down Expand Up @@ -98,52 +124,54 @@ jndi:
connection-timeout: ${pg.pool.connectionTimeout:3000}
idle-timeout: ${pg.pool.idleTimeout:60000}

geoserver.acl:
datasource:
jndi-name: ${acl.db.jndiName:java:comp/env/jdbc/acl}
url: ${acl.db.url:}
username: ${acl.db.username:}
password: ${acl.db.password:}
hikari:
minimum-idle: ${acl.db.hikari.minimumIdle:1}
maximum-pool-size: ${acl.db.hikari.maximumPoolSize:20}
jpa:
show-sql: false
open-in-view: false
generate-ddl: false
properties:
hibernate:
format_sql: true
default_schema: ${pg.schema}
hbm2ddl.auto: ${acl.db.hbm2ddl.auto:validate}
dialect: ${acl.db.dialect:org.hibernate.spatial.dialect.postgis.PostgisPG10Dialect}
security:
headers:
enabled: ${acl.security.headers.enabled}
user-header: ${acl.security.headers.user-header}
roles-header: ${acl.security.headers.roles-header}
admin-roles: ${acl.security.headers.admin-roles}
internal:
enabled: ${acl.security.basic.enabled}
users:
admin:
admin: true
enabled: ${acl.users.admin.enabled}
password: "${acl.users.admin.password}"
# password is the bcrypt encoded value, for example, for pwd s3cr3t:
# password: "{bcrypt}$2a$10$eMyaZRLZBAZdor8nOX.qwuwOyWazXjR2hddGLCT6f6c382WiwdQGG"
geoserver:
# special user for GeoServer to ACL communication
# Using a `{noop}` default credentials for performance, since bcrypt adds a significant per-request overhead
# in the orther of 100ms. In production it should be replaced by a docker/k8s secret
admin: true
enabled: ${acl.users.geoserver.enabled}
password: "${acl.users.geoserver.password}"
# user:
# admin: false
# enabled: true
# # password is the bcrypt encoded value for s3cr3t
# password: "{bcrypt}$2a$10$eMyaZRLZBAZdor8nOX.qwuwOyWazXjR2hddGLCT6f6c382WiwdQGG"
geoserver:
bus.enabled: false
acl:
datasource:
jndi-name: ${acl.db.jndiName:java:comp/env/jdbc/acl}
url: ${acl.db.url:}
username: ${acl.db.username:}
password: ${acl.db.password:}
hikari:
minimum-idle: ${acl.db.hikari.minimumIdle:1}
maximum-pool-size: ${acl.db.hikari.maximumPoolSize:20}
jpa:
show-sql: false
open-in-view: false
generate-ddl: false
properties:
hibernate:
format_sql: true
default_schema: ${pg.schema}
hbm2ddl.auto: ${acl.db.hbm2ddl.auto:validate}
dialect: ${acl.db.dialect:org.hibernate.spatial.dialect.postgis.PostgisPG10Dialect}
security:
headers:
enabled: ${acl.security.headers.enabled}
user-header: ${acl.security.headers.user-header}
roles-header: ${acl.security.headers.roles-header}
admin-roles: ${acl.security.headers.admin-roles}
internal:
enabled: ${acl.security.basic.enabled}
users:
admin:
admin: true
enabled: ${acl.users.admin.enabled}
password: "${acl.users.admin.password}"
# password is the bcrypt encoded value, for example, for pwd s3cr3t:
# password: "{bcrypt}$2a$10$eMyaZRLZBAZdor8nOX.qwuwOyWazXjR2hddGLCT6f6c382WiwdQGG"
geoserver:
# special user for GeoServer to ACL communication
# Using a `{noop}` default credentials for performance, since bcrypt adds a significant per-request overhead
# in the orther of 100ms. In production it should be replaced by a docker/k8s secret
admin: true
enabled: ${acl.users.geoserver.enabled}
password: "${acl.users.geoserver.password}"
# user:
# admin: false
# enabled: true
# # password is the bcrypt encoded value for s3cr3t
# password: "{bcrypt}$2a$10$eMyaZRLZBAZdor8nOX.qwuwOyWazXjR2hddGLCT6f6c382WiwdQGG"

---
spring.config.activate.on-profile: local
Expand Down Expand Up @@ -208,3 +236,4 @@ geoserver:
logging:
level:
root: error
org.geoserver.acl: info
Loading

0 comments on commit 389ffa5

Please sign in to comment.