Skip to content

Commit

Permalink
Cluster Api security (#125)
Browse files Browse the repository at this point in the history
Token based authentication to connect to Cluster Api

Co-authored-by: muralibasani <muralidahr.basani@aiven.io>
  • Loading branch information
muralibasani and muralibasani authored Oct 31, 2022
1 parent 92ee5ed commit 22b9783
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 62 deletions.
20 changes: 19 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@
<commons-text.version>1.10.0</commons-text.version>
<exec-maven-plugin.version>3.1.0</exec-maven-plugin.version>
<gson.version>2.9.0</gson.version>
<google-java-format.version>1.15.0</google-java-format.version>
<guava.version>31.1-jre</guava.version>
<h2.version>2.1.214</h2.version>
<google-java-format.version>1.15.0</google-java-format.version>
<jacoco-maven-plugin.version>0.8.8</jacoco-maven-plugin.version>
<jasyptencrypt.version>3.0.4</jasyptencrypt.version>
<jjwt.version>0.11.5</jjwt.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<maven-failsafe-plugin.version>2.22.2</maven-failsafe-plugin.version>
<netty-all.version>4.1.80.Final</netty-all.version>
Expand Down Expand Up @@ -234,6 +235,23 @@
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>

</dependencies>

Expand Down
95 changes: 47 additions & 48 deletions src/main/java/io/aiven/klaw/service/ClusterApiService.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,42 @@
import io.aiven.klaw.model.cluster.ClusterConnectorRequest;
import io.aiven.klaw.model.cluster.ClusterSchemaRequest;
import io.aiven.klaw.model.cluster.ClusterTopicRequest;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import javax.annotation.PostConstruct;
import javax.crypto.spec.SecretKeySpec;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.tomcat.util.codec.binary.Base64;
import org.jasypt.util.text.BasicTextEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
Expand All @@ -72,15 +78,13 @@
@Slf4j
public class ClusterApiService {

public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static final String URL_DELIMITER = "/";
@Autowired ManageDatabase manageDatabase;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String URL_DELIMITER = "/";

@Value("${server.ssl.trust-store:null}")
private String trustStore;

@Value("${server.ssl.trust-store-password:null}")
private String trustStorePwd;
private static final String URI_CREATE_TOPICS = "/topics/createTopics";
private static final String URI_UPDATE_TOPICS = "/topics/updateTopics";
private static final String URI_DELETE_TOPICS = "/topics/deleteTopics";
@Autowired private ManageDatabase manageDatabase;

@Value("${server.ssl.key-store:null}")
private String keyStore;
Expand All @@ -91,29 +95,20 @@ public class ClusterApiService {
@Value("${server.ssl.key-store-type:JKS}")
private String keyStoreType;

protected static HttpComponentsClientHttpRequestFactory requestFactory;

static String URI_CREATE_TOPICS = "/topics/createTopics";
static String URI_UPDATE_TOPICS = "/topics/updateTopics";
static String URI_DELETE_TOPICS = "/topics/deleteTopics";

@Value("${klaw.jasypt.encryptor.secretkey}")
private String encryptorSecretKey;

@Value("${klaw.clusterapi.access.username}")
private String clusterApiUser;

@Value("${klaw.clusterapi.access.password}")
private String clusterApiPwd;
@Value("${klaw.clusterapi.access.base64.secret:#{''}}")
private String clusterApiAccessBase64Secret;

private static String clusterConnUrl;
protected static HttpComponentsClientHttpRequestFactory requestFactory;
RestTemplate httpRestTemplate, httpsRestTemplate;

public ClusterApiService(ManageDatabase manageDatabase) {
this.manageDatabase = manageDatabase;
}

RestTemplate httpRestTemplate, httpsRestTemplate;

private RestTemplate getRestTemplate() {
if (clusterConnUrl.toLowerCase().startsWith("https")) {
if (this.httpsRestTemplate == null) {
Expand Down Expand Up @@ -390,7 +385,7 @@ public String approveConnectorRequests(
uri = clusterConnUrl + uriGetTopics + "deleteConnector";
}

HttpHeaders headers = createHeaders(clusterApiUser, clusterApiPwd);
HttpHeaders headers = createHeaders(clusterApiUser);
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<ClusterConnectorRequest> request =
Expand Down Expand Up @@ -457,7 +452,7 @@ public ResponseEntity<ApiResponse> approveTopicRequests(
uri = clusterConnUrl + URI_DELETE_TOPICS;
}

HttpHeaders headers = createHeaders(clusterApiUser, clusterApiPwd);
HttpHeaders headers = createHeaders(clusterApiUser);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<ClusterTopicRequest> request = new HttpEntity<>(clusterTopicRequest, headers);
response = getRestTemplate().postForEntity(uri, request, ApiResponse.class);
Expand Down Expand Up @@ -546,7 +541,7 @@ public ResponseEntity<ApiResponse> approveAclRequests(AclRequests aclReq, int te
clusterAclRequest.toBuilder().requestOperationType(RequestOperationType.DELETE).build();
}

HttpHeaders headers = createHeaders(clusterApiUser, clusterApiPwd);
HttpHeaders headers = createHeaders(clusterApiUser);
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<ClusterAclRequest> request = new HttpEntity<>(clusterAclRequest, headers);
Expand Down Expand Up @@ -583,7 +578,7 @@ ResponseEntity<ApiResponse> postSchema(
.fullSchema(schemaRequest.getSchemafull())
.build();

HttpHeaders headers = createHeaders(clusterApiUser, clusterApiPwd);
HttpHeaders headers = createHeaders(clusterApiUser);
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<ClusterSchemaRequest> request = new HttpEntity<>(clusterSchemaRequest, headers);
Expand Down Expand Up @@ -702,8 +697,7 @@ public Map<String, String> retrieveMetrics(String jmxUrl, String objectName)
String uriGetTopicsFull = clusterConnUrl + uriGetTopics;
RestTemplate restTemplate = getRestTemplate();

HttpHeaders headers =
createHeaders(clusterApiUser, clusterApiPwd); // createHeaders("user1", "pwd");
HttpHeaders headers = createHeaders(clusterApiUser);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);

Expand All @@ -719,16 +713,6 @@ public Map<String, String> retrieveMetrics(String jmxUrl, String objectName)
}
}

private HttpHeaders createHeaders(String username, String password) {
HttpHeaders httpHeaders = new HttpHeaders();
String auth = username + ":" + decodePwd(password);
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.US_ASCII));
String authHeader = "Basic " + new String(encodedAuth);
httpHeaders.set("Authorization", authHeader);

return httpHeaders;
}

// to connect to cluster api if https
@PostConstruct
private void setKwSSLContext() {
Expand Down Expand Up @@ -770,18 +754,33 @@ protected KeyStore getStore(String secret, String storeLoc)
return store;
}

private String decodePwd(String pwd) {
if (pwd != null) {
BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
textEncryptor.setPasswordCharArray(encryptorSecretKey.toCharArray());
private HttpHeaders createHeaders(String username) {
HttpHeaders httpHeaders = new HttpHeaders();
String authHeader = "Bearer " + generateToken(username);
httpHeaders.set("Authorization", authHeader);

return httpHeaders;
}

return textEncryptor.decrypt(pwd);
}
return "";
private String generateToken(String username) {
Key hmacKey =
new SecretKeySpec(
Base64.decodeBase64(clusterApiAccessBase64Secret),
SignatureAlgorithm.HS256.getJcaName());
Instant now = Instant.now();

return Jwts.builder()
.claim("name", username)
.setSubject(username)
.setId(UUID.randomUUID().toString())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(3L, ChronoUnit.MINUTES))) // expiry in 3 minutes
.signWith(hmacKey)
.compact();
}

private HttpEntity<String> getHttpEntity() {
HttpHeaders headers = createHeaders(clusterApiUser, clusterApiPwd);
HttpHeaders headers = createHeaders(clusterApiUser);
headers.setContentType(MediaType.APPLICATION_JSON);

headers.add("Accept", MediaType.APPLICATION_JSON_VALUE);
Expand Down
24 changes: 16 additions & 8 deletions src/main/java/io/aiven/klaw/service/ServerConfigService.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ public class ServerConfigService {

@Autowired private ClusterApiService clusterApiService;

@Autowired private CommonUtilsService commonUtilsService;
private final CommonUtilsService commonUtilsService;

private static List<ServerConfigProperties> listProps;

public ServerConfigService(Environment env) {
public ServerConfigService(Environment env, CommonUtilsService commonUtilsService) {
this.env = env;
this.commonUtilsService = commonUtilsService;
}

@PostConstruct
Expand All @@ -58,18 +59,21 @@ public void getAllProperties() {
log.info("All server properties being loaded");

List<ServerConfigProperties> listProps = new ArrayList<>();
List<String> allowedKeys =
Arrays.asList(
"spring.", "java.", "klaw.", "server.", "logging.", "management.", "endpoints.");
List<String> allowedKeys = Arrays.asList("spring.", "klaw.");

if (env instanceof ConfigurableEnvironment) {
for (PropertySource propertySource : ((ConfigurableEnvironment) env).getPropertySources()) {
for (PropertySource<?> propertySource :
((ConfigurableEnvironment) env).getPropertySources()) {
if (propertySource instanceof EnumerablePropertySource) {
for (String key : ((EnumerablePropertySource) propertySource).getPropertyNames()) {
for (String key : ((EnumerablePropertySource<?>) propertySource).getPropertyNames()) {

ServerConfigProperties props = new ServerConfigProperties();
props.setKey(key);
if (key.contains("password") || key.contains("license")) {
if (key.contains("password")
|| key.contains("license")
|| key.contains("pwd")
|| key.contains("cert")
|| key.contains("secret")) {
props.setValue("*******");
} else {
props.setValue(WordUtils.wrap(propertySource.getProperty(key) + "", 125, "\n", true));
Expand All @@ -91,6 +95,10 @@ public void getAllProperties() {
}

public List<ServerConfigProperties> getAllProps() {
if (commonUtilsService.isNotAuthorizedUser(
getPrincipal(), PermissionType.UPDATE_SERVERCONFIG)) {
return new ArrayList<>();
}
return listProps;
}

Expand Down
7 changes: 4 additions & 3 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,17 @@ spring.cache.type=NONE
spring.thymeleaf.cache=false

# application shutdown and health properties
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.include=health,info,metrics
management.endpoints.web.exposure.exclude=
management.health.ldap.enabled=false
management.endpoint.shutdown.enabled=true
management.endpoint.shutdown.enabled=false

#jasypt encryption pwd secret key
klaw.jasypt.encryptor.secretkey=kw2021secretkey

# ClusterApi access
klaw.clusterapi.access.username=kwclusterapiuser
klaw.clusterapi.access.password=d7DtnvRR7jq05ODBkvxLIGO6Qa/bVpkW
klaw.clusterapi.access.base64.secret=

# Monitoring
klaw.monitoring.metrics.enable=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ public void setUp() {
this.env = new Env();
env.setName("DEV");
ReflectionTestUtils.setField(clusterApiService, "httpRestTemplate", restTemplate);
ReflectionTestUtils.setField(clusterApiService, "clusterApiUser", "testuser");
ReflectionTestUtils.setField(
clusterApiService,
"clusterApiAccessBase64Secret",
"dGhpcyBpcyBhIHNlY3JldCB0byBhY2Nlc3MgY2x1c3RlcmFwaQ=="); // any base64 string

when(manageDatabase.getHandleDbRequests()).thenReturn(handleDbRequests);
when(manageDatabase.getKwPropertyValue(anyString(), anyInt())).thenReturn("http://cluster");
}
Expand Down
25 changes: 23 additions & 2 deletions src/test/java/io/aiven/klaw/service/ServerConfigServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,56 @@
package io.aiven.klaw.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import io.aiven.klaw.model.ServerConfigProperties;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
public class ServerConfigServiceTest {

@Mock private CommonUtilsService commonUtilsService;
ServerConfigService serverConfigService;

@Mock private UserDetails userDetails;

private Environment env;

@BeforeEach
public void setUp() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
this.env = context.getEnvironment();
loginMock();

serverConfigService = new ServerConfigService(env);
serverConfigService = new ServerConfigService(env, commonUtilsService);
}

@Test
public void getAllProps() {
when(commonUtilsService.isNotAuthorizedUser(any(), any())).thenReturn(false);
serverConfigService.getAllProperties();
List<ServerConfigProperties> list = serverConfigService.getAllProps();
assertThat(list).isNotEmpty();
assertThat(list).isEmpty(); // filtering for spring. and klaw.
}

private void loginMock() {
Authentication authentication = Mockito.mock(Authentication.class);
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.getPrincipal()).thenReturn(userDetails);
SecurityContextHolder.setContext(securityContext);
}
}

0 comments on commit 22b9783

Please sign in to comment.