Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API security: define authentication methods and secure the APIs #15

Merged
merged 2 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions docker-compose-acl-config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


jndi:
datasources:
acl:
Expand All @@ -14,11 +12,33 @@ jndi:
connection-timeout: 3000
idle-timeout: 60000

acl:
db:
jndiName: java:comp/env/jdbc/acl
hbm2ddl.auto: update
schema: acl
acl.db.jndiName: java:comp/env/jdbc/acl
acl.db.schema: acl
acl.db.hbm2ddl.auto: update

geoserver:
acl:
security:
headers:
enabled: true
user-header: sec-username
roles-header: sec-roles
admin-roles: ["ROLE_ADMINISTRATOR"]
internal:
enabled: true
users:
admin:
admin: true
enabled: ${acl.admin.enabled:true}
# password is a bcrypt encoded value for s3cr3t
password: "${acl.admin.password:{bcrypt}$2a$10$FE62N3ejbKm56EX5VrtSQeDDka8YjwgjwF9sSEKbatGZuZ8e7S9v.}"
#for a plain-text password (e.g. coming from a docker or kubernetes secret,
# use the {noop} prefix, as in: password: "{noop}plaintextpwd}", or password: "{noop}${ACL_ADMIN_PASSWORD}"
user:
admin: false
enabled: true
# password is a bcrypt encoded value for s3cr3t
password: "{bcrypt}$2a$10$eMyaZRLZBAZdor8nOX.qwuwOyWazXjR2hddGLCT6f6c382WiwdQGG"

logging:
level:
Expand All @@ -37,3 +57,7 @@ management:
exposure:
include:
- '*'
---
# local profile, used for development only. Other settings like config and eureka urls in gs_cloud_bootstrap_profiles.yml
spring.config.activate.on-profile: local
server.port: 9091
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ services:
cpus: '4.0'
memory: 2G
ports:
- 5432:5432
- 6432:5432

acl:
image: geoservercloud/geoserver-acl:1.0-SNAPSHOT
Expand Down
39 changes: 0 additions & 39 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,42 +35,3 @@ access rules. So if you're familiar with GeoFence, it'll be easy to reason
about GeoServer ACL.


## Dependency graph

```mermaid
flowchart LR
subgraph domain
adminrule-management --> object-model & rule-management
authorization --> adminrule-management & rule-management
rule-management --> object-model
end
subgraph openapi-codegen
openapi-server --> openapi-model
openapi-client --> openapi-model
end
subgraph integration
subgraph persistence-jpa
jpa-integration --> jpa-persistence
end
subgraph spring-integration
domain-spring-integration -. optional> .-> rule-management & adminrule-management & authorization
end
subgraph openapi-integration
api-model-mapper --> object-model & openapi-model
api-impl --> api-model-mapper & openapi-server & domain-spring-integration & rule-management & adminrule-management
api-client --> api-model-mapper & openapi-client
end
subgraph spring-boot-integration
spring-boot-autoconfiguration --> domain-spring-integration & api-impl & jpa-integration
end
end
subgraph geoserver-plugin
plugin-webui --> plugin-accessmanager
plugin-rest --> plugin-accessmanager & api-impl
plugin --> plugin-accessmanager & plugin-rest & plugin-webui
end
subgraph application
rest-app --> spring-boot-autoconfiguration
end
```

11 changes: 9 additions & 2 deletions src/artifacts/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ FROM eclipse-temurin:17-jre
LABEL maintainer="GeoServer PSC <[email protected]>"

WORKDIR /opt/app/bin
ENV JAVA_TOOL_OPTS="\
ENV DEF_JAVA_TOOL_OPTS="\
--add-exports=java.desktop/sun.awt.image=ALL-UNNAMED \
--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.util=ALL-UNNAMED \
Expand All @@ -22,6 +22,9 @@ ENV JAVA_TOOL_OPTS="\
--add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED \
-Djava.awt.headless=true"
ENV JAVA_OPTS="-XX:MaxRAMPercentage=80 -XshowSettings:system"

ENV JAVA_TOOL_OPTS="$DEF_JAVA_TOOL_OPTS $JAVA_OPTS"

EXPOSE 8080

COPY --from=builder dependencies/ ./
Expand All @@ -36,4 +39,8 @@ HEALTHCHECK \
--retries=5 \
CMD curl -f -s -o /dev/null localhost:8080/actuator/health || exit 1

CMD exec java $JAVA_OPTS $JAVA_TOOL_OPTS org.springframework.boot.loader.JarLauncher
ARG APP_ARGS=""

ENTRYPOINT [ "/bin/bash", "-c", "exec java $JAVA_TOOL_OPTS org.springframework.boot.loader.JarLauncher \"${@}\"", "--" ]

CMD ["${APP_ARGS}"]
4 changes: 2 additions & 2 deletions src/artifacts/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ With the application running at [http://localhost:8080/api](http://localhost:808
mvn clean install
```

will create a single-jar executable at `target/gs-cloud-acl-service-<version>-bin.jar`.
will create a single-jar executable at `target/gs-acl-service-<version>-bin.jar`.

## Run

Expand All @@ -27,7 +27,7 @@ Run in development mode with an in-memory H2 database, either with

or

java -jar target/gs-cloud-acl-service-1.0-SNAPSHOT-bin.jar --spring.profiles.active=dev
java -jar target/gs-acl-service-1.0-SNAPSHOT-bin.jar --spring.profiles.active=dev


## Dependency graph
Expand Down
8 changes: 8 additions & 0 deletions src/artifacts/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
Expand All @@ -57,6 +61,10 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,41 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Arrays;
import java.util.List;

@SpringBootApplication
public class AccesControlListApplication {

private static final String ENCODEPASSWORD = "encodepassword";

public static void main(String... args) {
List<String> arglist = Arrays.asList(args);

if (arglist.contains(ENCODEPASSWORD)) {
System.exit(encodePassword(arglist));
}

try {
SpringApplication.run(AccesControlListApplication.class, args);
} catch (RuntimeException e) {
System.exit(-1);
}
}

private static int encodePassword(List<String> arglist) {
int pwdIndex = 1 + arglist.indexOf(ENCODEPASSWORD);
if (arglist.size() < pwdIndex) {
System.err.println("Usage: encodepassword <password>");
return -1;
}
String pwd = arglist.get(pwdIndex);
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String encoded = encoder.encode(pwd);
System.out.println(encoded);
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* (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.security;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;

@AutoConfiguration
@EnableWebSecurity
@EnableConfigurationProperties(SecurityConfigProperties.class)
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j(topic = "org.geoserver.acl.autoconfigure.security")
public class AclServiceSecurityAutoConfiguration {

private @Autowired(required = false) RequestHeaderAuthenticationFilter preAuthFilter;

@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
AuthenticationManager authenticationManager,
SecurityConfigProperties config)
throws Exception {

http.csrf().disable();

if (!config.enabled()) {
log.warn("No security authentication method is defined!");
return http.build();
}

http.authenticationManager(authenticationManager);

if (null == preAuthFilter) {
log.info("Pre-authentication headers disabled");
} else {
log.info(
"Pre-authentication headers enabled for {}/{}. Admin roles: {}",
config.getHeaders().getUserHeader(),
config.getHeaders().getRolesHeader(),
config.getHeaders().getAdminRoles());
http.addFilterAfter(preAuthFilter, RequestHeaderAuthenticationFilter.class);
}

http.authorizeRequests()
.antMatchers("/", "/api/api-docs/**", "/api/swagger-ui.html", "/api/swagger-ui/**")
.permitAll()
.anyRequest()
.authenticated()
.and();

if (config.getInternal().isEnabled()) {
http.httpBasic();
}
return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* (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.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;

import java.util.List;

@Configuration
public class AuthenticationManagerAutoConfiguration {

@Bean
AuthenticationManager authenticationManager(List<AuthenticationProvider> providers)
throws Exception {
return new ProviderManager(providers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* (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.security;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConditionalOnProperty(
prefix = ConditionalOnInternalAuthenticationEnabled.PREFIX,
name = "enabled",
havingValue = "true",
matchIfMissing = false)
public @interface ConditionalOnInternalAuthenticationEnabled {

public static final String PREFIX = SecurityConfigProperties.PREFIX + ".internal";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* (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.security;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConditionalOnProperty(
prefix = ConditionalOnPreAuthenticationEnabled.PREFIX,
name = "enabled",
havingValue = "true",
matchIfMissing = false)
public @interface ConditionalOnPreAuthenticationEnabled {

public static final String PREFIX = SecurityConfigProperties.PREFIX + ".headers";
}
Loading