Skip to content

Commit f1165cf

Browse files
Basic Spring-Boot sample app to create JWT tokens for dynamic log levels
This is work in progress. The JKS should be created automatically. API description needs to be improved. Security (Basic-Auth) needs to be added.
1 parent c22ae34 commit f1165cf

File tree

15 files changed

+597
-0
lines changed

15 files changed

+597
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
<module>cf-java-logging-support-jersey</module>
193193
<module>cf-java-monitoring-custom-metrics-clients</module>
194194
<module>sample</module>
195+
<module>sample-spring-boot</module>
195196
</modules>
196197

197198
<dependencies>

sample-spring-boot/pom.xml

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<artifactId>sample-app-spring-boot</artifactId>
7+
<name>sample-app-spring-boot</name>
8+
<description>Logging Sample App for Spring Boot</description>
9+
<parent>
10+
<groupId>com.sap.hcp.cf.logging</groupId>
11+
<artifactId>cf-java-logging-support-parent</artifactId>
12+
<version>3.2.1</version>
13+
<relativePath>../pom.xml</relativePath>
14+
</parent>
15+
16+
<properties>
17+
<java.version>11</java.version>
18+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
19+
<maven.compiler.source>11</maven.compiler.source>
20+
<maven.compiler.target>11</maven.compiler.target>
21+
<spring.boot.version>2.3.4.RELEASE</spring.boot.version>
22+
</properties>
23+
24+
<dependencyManagement>
25+
<dependencies>
26+
<dependency>
27+
<groupId>org.springframework.boot</groupId>
28+
<artifactId>spring-boot-dependencies</artifactId>
29+
<version>${spring.boot.version}</version>
30+
<type>pom</type>
31+
<scope>import</scope>
32+
</dependency>
33+
</dependencies>
34+
</dependencyManagement>
35+
36+
<dependencies>
37+
<dependency>
38+
<groupId>org.springframework.boot</groupId>
39+
<artifactId>spring-boot-starter-actuator</artifactId>
40+
<exclusions>
41+
<exclusion>
42+
<groupId>org.springframework.boot</groupId>
43+
<artifactId>spring-boot-starter-logging</artifactId>
44+
</exclusion>
45+
</exclusions>
46+
</dependency>
47+
<dependency>
48+
<groupId>org.springframework.boot</groupId>
49+
<artifactId>spring-boot-starter-web</artifactId>
50+
<exclusions>
51+
<exclusion>
52+
<groupId>org.springframework.boot</groupId>
53+
<artifactId>spring-boot-starter-logging</artifactId>
54+
</exclusion>
55+
</exclusions>
56+
</dependency>
57+
<dependency>
58+
<groupId>org.springframework.boot</groupId>
59+
<artifactId>spring-boot-configuration-processor</artifactId>
60+
<optional>true</optional>
61+
</dependency>
62+
63+
<!-- We're using the Servlet Filter instrumentation -->
64+
<dependency>
65+
<groupId>com.sap.hcp.cf.logging</groupId>
66+
<artifactId>cf-java-logging-support-servlet</artifactId>
67+
<version>${project.version}</version>
68+
</dependency>
69+
70+
<dependency>
71+
<groupId>org.springframework.boot</groupId>
72+
<artifactId>spring-boot-starter-test</artifactId>
73+
<scope>test</scope>
74+
<exclusions>
75+
<exclusion>
76+
<groupId>org.junit.vintage</groupId>
77+
<artifactId>junit-vintage-engine</artifactId>
78+
</exclusion>
79+
</exclusions>
80+
</dependency>
81+
</dependencies>
82+
83+
<build>
84+
<plugins>
85+
<plugin>
86+
<groupId>org.springframework.boot</groupId>
87+
<artifactId>spring-boot-maven-plugin</artifactId>
88+
</plugin>
89+
</plugins>
90+
</build>
91+
92+
<profiles>
93+
<profile>
94+
<id>logback</id>
95+
<activation>
96+
<activeByDefault>true</activeByDefault>
97+
</activation>
98+
99+
<dependencies>
100+
<dependency>
101+
<groupId>org.springframework.boot</groupId>
102+
<artifactId>spring-boot-starter-logging</artifactId>
103+
</dependency>
104+
<dependency>
105+
<groupId>com.sap.hcp.cf.logging</groupId>
106+
<artifactId>cf-java-logging-support-logback</artifactId>
107+
<version>${project.version}</version>
108+
</dependency>
109+
</dependencies>
110+
</profile>
111+
<profile>
112+
<id>log4j2</id>
113+
<activation>
114+
<activeByDefault>false</activeByDefault>
115+
</activation>
116+
117+
<dependencies>
118+
<dependency>
119+
<groupId>org.springframework.boot</groupId>
120+
<artifactId>spring-boot-starter-log4j2</artifactId>
121+
</dependency>
122+
<dependency>
123+
<groupId>com.sap.hcp.cf.logging</groupId>
124+
<artifactId>cf-java-logging-support-log4j2</artifactId>
125+
<version>${project.version}</version>
126+
</dependency>
127+
</dependencies>
128+
</profile>
129+
</profiles>
130+
131+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.sap.hcp.cf.logging.sample.springboot;
2+
3+
import java.time.Clock;
4+
5+
import javax.servlet.DispatcherType;
6+
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.SpringApplication;
9+
import org.springframework.boot.autoconfigure.SpringBootApplication;
10+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
13+
14+
import com.sap.hcp.cf.logging.sample.springboot.keystore.KeyStoreDynLogConfiguration;
15+
import com.sap.hcp.cf.logging.servlet.dynlog.DynLogConfiguration;
16+
import com.sap.hcp.cf.logging.servlet.filter.RequestLoggingFilter;
17+
18+
@SpringBootApplication
19+
@EnableWebMvc
20+
public class SampleAppSpringBootApplication {
21+
22+
public static void main(String[] args) {
23+
SpringApplication.run(SampleAppSpringBootApplication.class, args);
24+
}
25+
26+
/**
27+
* Registers a customized {@link RequestLoggingFilter} with the servlet.
28+
* We inject our own dynamic logging configuration, that contains the public RSA key from our keystore.
29+
*
30+
* @param dynLogConfig autowired with {@link KeyStoreDynLogConfiguration}
31+
* @return a registration of the {@link RequestLoggingFilter}
32+
*/
33+
@Bean
34+
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter(@Autowired DynLogConfiguration dynLogConfig) {
35+
FilterRegistrationBean<RequestLoggingFilter> registrationBean = new FilterRegistrationBean<>();
36+
registrationBean.setFilter(new RequestLoggingFilter(() -> dynLogConfig));
37+
registrationBean.setName("request-logging");
38+
registrationBean.addUrlPatterns("/*");
39+
registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
40+
return registrationBean;
41+
}
42+
43+
/**
44+
* Provides a global {@link Clock} instance. Useful for testing.
45+
* @return the global clock
46+
*/
47+
@Bean
48+
public Clock clock() {
49+
return Clock.systemUTC();
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.sap.hcp.cf.logging.sample.springboot.config;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
import org.springframework.context.annotation.Configuration;
5+
6+
@Configuration
7+
@ConfigurationProperties(prefix = "keystore.token")
8+
public class KeyStoreConfiguration {
9+
10+
private String location;
11+
private String type;
12+
private char[] password;
13+
private String keyAlias;
14+
private char[] keyPassword;
15+
16+
public String getType() {
17+
return type;
18+
}
19+
20+
public String getLocation() {
21+
return location;
22+
}
23+
24+
public char[] getPassword() {
25+
return password;
26+
}
27+
28+
public String getKeyAlias() {
29+
return keyAlias;
30+
}
31+
32+
public void setLocation(String location) {
33+
this.location = location;
34+
}
35+
36+
public void setType(String type) {
37+
this.type = type;
38+
}
39+
40+
public void setPassword(char[] password) {
41+
this.password = password;
42+
}
43+
44+
public void setKeyAlias(String keyAlias) {
45+
this.keyAlias = keyAlias;
46+
}
47+
48+
public void setKeyPassword(char[] keyPassword) {
49+
this.keyPassword = keyPassword;
50+
}
51+
52+
public char[] getKeyPassword() {
53+
return keyPassword;
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.sap.hcp.cf.logging.sample.springboot.config;
2+
3+
import java.time.Duration;
4+
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
@Configuration
9+
@ConfigurationProperties(prefix = "defaults.token")
10+
public class TokenDefaultsConfiguration {
11+
12+
private Duration expiration;
13+
private String issuer;
14+
15+
public Duration getExpiration() {
16+
return expiration;
17+
}
18+
19+
public void setExpiration(String expiration) {
20+
this.expiration = Duration.parse(expiration);
21+
}
22+
23+
public String getIssuer() {
24+
return issuer;
25+
}
26+
27+
public void setIssuer(String issuer) {
28+
this.issuer = issuer;
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.sap.hcp.cf.logging.sample.springboot.controller;
2+
3+
import java.time.Clock;
4+
import java.time.Instant;
5+
import java.util.Optional;
6+
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestParam;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
import com.sap.hcp.cf.logging.sample.springboot.config.TokenDefaultsConfiguration;
16+
import com.sap.hcp.cf.logging.sample.springboot.service.TokenGenerator;
17+
18+
/**
19+
* This controller provides and endpoint to create new JWT tokens. These token
20+
* can be used as headers of HTTP request to dynamically switch the log level.
21+
*/
22+
@RestController
23+
public class TokenController {
24+
25+
private static final Logger LOG = LoggerFactory.getLogger(TokenController.class);
26+
27+
private Clock clock;
28+
private TokenGenerator generator;
29+
private TokenDefaultsConfiguration defaults;
30+
31+
public TokenController(@Autowired Clock clock, @Autowired TokenGenerator generator,
32+
@Autowired TokenDefaultsConfiguration defaults) {
33+
this.clock = clock;
34+
this.generator = generator;
35+
this.defaults = defaults;
36+
}
37+
38+
/**
39+
* Return a JWT for changing the log level dynamically. It uses the keys
40+
* provided by the keystore to sign the JWT.
41+
*
42+
* @param logLevel the log level to use when JWT is applied
43+
* @param expiresAtParam (optional) the expiration of the JWT in epoch
44+
* milliseconds
45+
* @param packages (optional) the comma-separated list of packages for
46+
* which the log levels should be changed
47+
* @return the signed JWT to be used as HTTP header
48+
*/
49+
@GetMapping("/token/{logLevel}")
50+
public String createToken(@PathVariable("logLevel") String logLevel,
51+
@RequestParam(name = "exp") Optional<Long> expiresAtParam,
52+
@RequestParam(name = "p") Optional<String> packages) {
53+
Instant expiresAt = getExpiryOrDefault(expiresAtParam);
54+
Instant issuedAt = clock.instant();
55+
return generator.create(logLevel, packages, expiresAt, issuedAt);
56+
}
57+
58+
private Instant getExpiryOrDefault(Optional<Long> expiresAtParam) {
59+
if (expiresAtParam.isPresent()) {
60+
Instant result = expiresAtParam.map(Instant::ofEpochMilli).get();
61+
LOG.debug("Using user provided expiration at {}.", result);
62+
return result;
63+
}
64+
Instant result = clock.instant().plus(defaults.getExpiration());
65+
LOG.debug("Using default expiration to calculate {}.", result);
66+
return result;
67+
68+
}
69+
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.sap.hcp.cf.logging.sample.springboot.keystore;
2+
3+
import java.security.interfaces.RSAPublicKey;
4+
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.stereotype.Component;
8+
9+
import com.sap.hcp.cf.logging.servlet.dynlog.DynLogConfiguration;
10+
11+
/**
12+
* Provides the public key for JWT validation out of the keystore.
13+
* Otherwise adheres to the configuration by environment variables.
14+
*
15+
*/
16+
@Component
17+
public class KeyStoreDynLogConfiguration implements DynLogConfiguration {
18+
19+
private TokenKeyProvider keyProvider;
20+
private String dynLogHeaderKey;
21+
22+
public KeyStoreDynLogConfiguration(@Autowired TokenKeyProvider keyProvider,
23+
@Value("${DYN_LOG_HEADER:SAP-LOG-LEVEL}") String dynLogHeaderKey) {
24+
this.keyProvider = keyProvider;
25+
this.dynLogHeaderKey = dynLogHeaderKey;
26+
}
27+
28+
@Override
29+
public String getDynLogHeaderKey() {
30+
return dynLogHeaderKey;
31+
}
32+
33+
@Override
34+
public RSAPublicKey getRsaPublicKey() {
35+
String keyId = keyProvider.getPrivateKeyId();
36+
return keyProvider.getPublicKeyById(keyId);
37+
}
38+
39+
}

0 commit comments

Comments
 (0)