Skip to content

Commit

Permalink
Security Remediation
Browse files Browse the repository at this point in the history
  • Loading branch information
siarhei-charniak committed Jul 20, 2023
1 parent e7f651e commit cb67855
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 24 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ API for Data Export Spring module.
| DB_DATABASE | okapi_modules | Postgres database name |
| KAFKA_HOST | kafka | Kafka broker hostname |
| KAFKA_PORT | 9092 | Kafka broker port |
| SYSTEM\_USER\_NAME | data-export-system-user | Username of the system user |
| SYSTEM\_USER\_PASSWORD | - | Password of the system user |
| ENV | folio | Logical name of the deployment, must be set if Kafka/Elasticsearch are shared for environments, `a-z (any case)`, `0-9`, `-`, `_` symbols only allowed|


Expand Down
1 change: 1 addition & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
"users.item.post",
"users.item.put",
"login.item.post",
"login.item.delete",
"perms.users.item.post",
"perms.users.get",
"configuration.entries.collection.get",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.folio.des;

import org.apache.commons.lang3.StringUtils;
import org.folio.de.entity.JobCommand;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand All @@ -10,8 +11,13 @@
@EnableFeignClients
@EntityScan(basePackageClasses = JobCommand.class)
public class ModDataExportSpringApplication {
public static final String SYSTEM_USER_PASSWORD = "SYSTEM_USER_PASSWORD";

public static void main(String[] args) {
if (StringUtils.isEmpty(System.getenv(SYSTEM_USER_PASSWORD))) {
throw new IllegalArgumentException("Required environment variable is missing: " + SYSTEM_USER_PASSWORD);
}

SpringApplication.run(ModDataExportSpringApplication.class, args);
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/folio/des/client/AuthClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient("authn")
public interface AuthClient {
Expand All @@ -15,4 +17,7 @@ public interface AuthClient {

@PostMapping(value = "/credentials", consumes = MediaType.APPLICATION_JSON_VALUE)
void saveCredentials(@RequestBody SystemUserParameters systemUserParameters);

@DeleteMapping(value = "/credentials", consumes = MediaType.APPLICATION_JSON_VALUE)
void deleteCredentials(@RequestParam("userId") String userId);
}
10 changes: 9 additions & 1 deletion src/main/java/org/folio/des/security/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ public class AuthService {

@Value("${folio.system.username}")
private String username;
@Value("${folio.system.password}")
private String password;

public SystemUserParameters loginSystemUser(String tenant, String url) {
SystemUserParameters userParameters =
SystemUserParameters.builder()
.okapiUrl(url)
.tenantId(tenant)
.username(username)
.password(username)
.password(password)
.build();

log.info("Login with url={} tenant={} username={}.", url, tenant, username);
Expand All @@ -52,6 +54,12 @@ private boolean isNotEmpty(java.util.List<String> token) {
return CollectionUtils.isNotEmpty(token) && StringUtils.isNotBlank(token.get(0));
}

public void deleteCredentials(String userId) {
authClient.deleteCredentials(userId);

log.info("Removed credentials for user {}.", userId);
}

public void saveCredentials(SystemUserParameters systemUserParameters) {
authClient.saveCredentials(systemUserParameters);

Expand Down
22 changes: 15 additions & 7 deletions src/main/java/org/folio/des/security/SecurityManagerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class SecurityManagerService {

@Value("${folio.system.username}")
private String username;
@Value("${folio.system.password}")
private String password;

public void prepareSystemUser(String okapiUrl, String tenantId) {
Optional<User> userOptional = getUser(username);
Expand All @@ -45,15 +47,21 @@ public void prepareSystemUser(String okapiUrl, String tenantId) {
updateUser(user);
} else {
user = createUser(username);
authService.saveCredentials(SystemUserParameters.builder()
.id(UUID.randomUUID())
.username(username)
.password(username)
.okapiUrl(okapiUrl)
.tenantId(tenantId)
.build());
}

try {
authService.deleteCredentials(user.getId());
} catch (feign.FeignException.NotFound e) {
// ignore if not exist
}
authService.saveCredentials(SystemUserParameters.builder()
.id(UUID.randomUUID())
.username(username)
.password(password)
.okapiUrl(okapiUrl)
.tenantId(tenantId)
.build());

Optional<PermissionUser> permissionUserOptional = permissionsClient.get("userId==" + user.getId())
.getPermissionUsers()
.stream()
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
folio:
system:
username: data-export-system-user
username: ${SYSTEM_USER_NAME:data-export-system-user}
password: ${SYSTEM_USER_PASSWORD}
tenant:
validation:
enabled: true
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/org/folio/des/InstallUpgradeIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ class InstallUpgradeIT {
.withEnv("DB_PASSWORD", "password")
.withEnv("DB_DATABASE", "postgres")
.withEnv("KAFKA_HOST", "mykafka")
.withEnv("KAFKA_PORT", "9092");
.withEnv("KAFKA_PORT", "9092")
.withEnv("SYSTEM_USER_PASSWORD", "password");

private static void mockPath(MockServerClient mockServerClient, String path, String jsonBody) {
mockServerClient.when(request(path))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.folio.des;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertThrows;

import org.junit.jupiter.api.Test;

class ModDataExportSpringApplicationTest {

@Test
void exceptionOnMissingSystemUserPassword() {
var e = assertThrows(IllegalArgumentException.class, () -> ModDataExportSpringApplication.main(null));
assertThat(e.getMessage(), containsString(ModDataExportSpringApplication.SYSTEM_USER_PASSWORD));
}

}
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
package org.folio.des.security;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;

import org.folio.des.config.FolioExecutionContextHelper;
import org.folio.des.support.BaseTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.folio.spring.DefaultFolioExecutionContext;
import org.folio.spring.FolioModuleMetadata;
import org.folio.spring.integration.XOkapiHeaders;
import org.folio.spring.scope.FolioExecutionContextSetter;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class SecurityManagerServiceTest extends BaseTest {

@Autowired private SecurityManagerService securityManagerService;
@Autowired private FolioExecutionContextHelper contextHelper;
@Autowired private FolioModuleMetadata folioModuleMetadata;
private static final String SYS_USER_EXIST_RESPONSE =
"{\n"
+ " \"users\": [\n"
Expand Down Expand Up @@ -50,15 +60,6 @@ class SecurityManagerServiceTest extends BaseTest {
private static final String USER_PERMS_RESPONSE =
"{ \"permissionUsers\": [],\n \"totalRecords\": 0,\n \"resultInfo\": {\n \"totalRecords\": 0,\n \"facets\": [],\n \"diagnostics\": []\n }\n}";

@BeforeEach
void setUp() {
contextHelper.initScope(TENANT);
}

@AfterEach
void tearDown() {
contextHelper.finishContext();
}

@Test
@DisplayName("Update user")
Expand All @@ -78,11 +79,73 @@ void prepareSystemUser() {
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(USER_PERMS_RESPONSE)));

securityManagerService.prepareSystemUser(wireMockServer.baseUrl(), TENANT);
wireMockServer.stubFor(
delete(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59"))
.willReturn(
aResponse()
.withStatus(HttpStatus.NO_CONTENT.value())));

Map<String, Collection<String>> tenantOkapiHeaders = new HashMap<>() {{
put(XOkapiHeaders.TENANT, List.of(TENANT));
put(XOkapiHeaders.URL, List.of(wireMockServer.baseUrl()));
put(XOkapiHeaders.TOKEN, List.of(TOKEN));
}};

try (var context = new FolioExecutionContextSetter(new DefaultFolioExecutionContext(folioModuleMetadata, tenantOkapiHeaders))) {
securityManagerService.prepareSystemUser(wireMockServer.baseUrl(), TENANT);
}

wireMockServer.verify(
getRequestedFor(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user")));
wireMockServer.verify(
putRequestedFor(urlEqualTo("/users/a85c45b7-d427-4122-8532-5570219c5e59")));
wireMockServer.verify(
deleteRequestedFor(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59")));
wireMockServer.verify(
postRequestedFor(urlEqualTo("/authn/credentials")));
}

@Test
@DisplayName("Update user without previous password")
void prepareSystemUserWithoutPreviousPassword() {

wireMockServer.stubFor(
get(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user"))
.willReturn(
aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(SYS_USER_EXIST_RESPONSE)));

wireMockServer.stubFor(
get(urlEqualTo("/perms/users?query=userId%3D%3Da85c45b7-d427-4122-8532-5570219c5e59"))
.willReturn(
aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(USER_PERMS_RESPONSE)));

wireMockServer.stubFor(
delete(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59"))
.willReturn(
aResponse()
.withStatus(HttpStatus.NOT_FOUND.value())));

Map<String, Collection<String>> tenantOkapiHeaders = new HashMap<>() {{
put(XOkapiHeaders.TENANT, List.of(TENANT));
put(XOkapiHeaders.URL, List.of(wireMockServer.baseUrl()));
put(XOkapiHeaders.TOKEN, List.of(TOKEN));
}};

try (var context = new FolioExecutionContextSetter(new DefaultFolioExecutionContext(folioModuleMetadata, tenantOkapiHeaders))) {
securityManagerService.prepareSystemUser(wireMockServer.baseUrl(), TENANT);
}

wireMockServer.verify(
getRequestedFor(urlEqualTo("/users?query=username%3D%3Ddata-export-system-user")));
wireMockServer.verify(
putRequestedFor(urlEqualTo("/users/a85c45b7-d427-4122-8532-5570219c5e59")));
wireMockServer.verify(
deleteRequestedFor(urlEqualTo("/authn/credentials?userId=a85c45b7-d427-4122-8532-5570219c5e59")));
wireMockServer.verify(
postRequestedFor(urlEqualTo("/authn/credentials")));
}
}
3 changes: 3 additions & 0 deletions src/test/resources/config/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
folio:
system:
password: testpassword
9 changes: 9 additions & 0 deletions src/test/resources/mappings/authn.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@
}
}
},
{
"request": {
"method": "DELETE",
"url": "/authn/credentials"
},
"response": {
"status": 204
}
},
{
"request": {
"method": "POST",
Expand Down

0 comments on commit cb67855

Please sign in to comment.