Skip to content

Commit

Permalink
Use token-based login system for Poppy
Browse files Browse the repository at this point in the history
  • Loading branch information
ncovercash committed Oct 9, 2023
1 parent 2295615 commit f182fec
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 115 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,12 @@
<version>3.6.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.folio.okapi</groupId>
<artifactId>okapi-common</artifactId>
<version>5.1.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.folio</groupId>
<artifactId>folio-kafka-wrapper</artifactId>
Expand Down
39 changes: 34 additions & 5 deletions src/main/java/org/folio/service/auth/AuthClient.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.folio.service.auth;

import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import java.util.Optional;
import lombok.AllArgsConstructor;
Expand All @@ -8,18 +10,45 @@
import lombok.NoArgsConstructor;
import org.apache.http.client.methods.HttpPost;
import org.folio.dataimport.util.OkapiConnectionParams;
import org.folio.okapi.common.WebClientFactory;
import org.folio.okapi.common.refreshtoken.client.ClientOptions;
import org.folio.okapi.common.refreshtoken.client.impl.LoginClient;
import org.folio.okapi.common.refreshtoken.tokencache.TenantUserCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AuthClient extends ApiClient {

private static final String LOGIN_ENDPOINT = "authn/login";
private static final String CREDENTIALS_ENDPOINT = "authn/credentials";

public String login(OkapiConnectionParams params, LoginCredentials payload) {
return post(params, LOGIN_ENDPOINT, payload)
.orElseThrow()
.getString("okapiToken");
private static final int CACHE_SIZE = 5;

private TenantUserCache cache;
private Vertx vertx;

@Autowired
public AuthClient(Vertx vertx) {
this.vertx = vertx;

this.cache = new TenantUserCache(CACHE_SIZE);
}

public Future<String> login(
OkapiConnectionParams params,
LoginCredentials payload
) {
// use standardized RTR token utility
return new LoginClient(
new ClientOptions()
.okapiUrl(params.getOkapiUrl())
.webClient(WebClientFactory.getWebClient(vertx)),
cache,
payload.getTenant(),
payload.getUsername(),
() -> Future.succeededFuture(payload.getPassword())
)
.getToken();
}

public void saveCredentials(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.folio.service.auth;

import io.vertx.core.Future;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
Expand Down Expand Up @@ -94,7 +95,9 @@ public void initializeSystemUser(Map<String, String> headers) {
LOGGER.info("System user created successfully!");
}

public String getAuthToken(OkapiConnectionParams okapiConnectionParams) {
public Future<String> getAuthToken(
OkapiConnectionParams okapiConnectionParams
) {
LOGGER.info("Attempting {}", getLoginCredentials(okapiConnectionParams));

return authClient.login(
Expand Down
158 changes: 81 additions & 77 deletions src/main/java/org/folio/service/file/S3JobRunningVerticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,76 +143,78 @@ protected Future<QueueJob> processQueueItem(DataImportQueueItem queueItem) {
queueItem.getJobExecutionId()
);

OkapiConnectionParams params = getConnectionParams(queueItem);

// we need to store out here to ensure it is properly deleted
// on failure and success
AtomicReference<File> localFile = new AtomicReference<>();

return Future
.succeededFuture(new QueueJob().withQueueItem(queueItem))
.compose((QueueJob job) ->
createLocalFile(queueItem)
.map((File file) -> {
localFile.set(file);
return job.withFile(file);
return getConnectionParams(queueItem)
.compose(params ->
Future
.succeededFuture(new QueueJob().withQueueItem(queueItem))
.compose((QueueJob job) ->
createLocalFile(queueItem)
.map((File file) -> {
localFile.set(file);
return job.withFile(file);
})
)
.compose(job ->
uploadDefinitionService
.getJobExecutionById(queueItem.getJobExecutionId(), params)
.map(job::withJobExecution)
)
.compose(job ->
updateJobExecutionStatusSafely(
job.getJobExecution().getId(),
new StatusDto()
.withStatus(StatusDto.Status.PROCESSING_IN_PROGRESS),
params
)
.map(job)
)
.compose(this::downloadFromS3)
.compose(job ->
fileProcessor
.processFile(
job.getFile(),
job.getJobExecution().getId(),
// this is the only part used on our end
new JobProfileInfo()
.withDataType(
JobProfileInfo.DataType.fromValue(
job.getQueueItem().getDataType()
)
),
params
)
.map(job)
)
.onFailure((Throwable err) -> {
LOGGER.error("Unable to start chunk {}", queueItem, err);

updateJobExecutionStatusSafely(
queueItem.getJobExecutionId(),
new StatusDto()
.withErrorStatus(ErrorStatus.FILE_PROCESSING_ERROR)
.withStatus(StatusDto.Status.ERROR),
params
);
})
)
.compose(job ->
uploadDefinitionService
.getJobExecutionById(queueItem.getJobExecutionId(), params)
.map(job::withJobExecution)
)
.compose(job ->
updateJobExecutionStatusSafely(
job.getJobExecution().getId(),
new StatusDto().withStatus(StatusDto.Status.PROCESSING_IN_PROGRESS),
params
)
.map(job)
)
.compose(this::downloadFromS3)
.compose(job ->
fileProcessor
.processFile(
job.getFile(),
job.getJobExecution().getId(),
// this is the only part used on our end
new JobProfileInfo()
.withDataType(
JobProfileInfo.DataType.fromValue(
job.getQueueItem().getDataType()
)
),
params
.onSuccess((QueueJob result) ->
LOGGER.info(
"Completed processing job execution {}!",
queueItem.getJobExecutionId()
)
)
.map(job)
)
.onFailure((Throwable err) -> {
LOGGER.error("Unable to start chunk {}", queueItem, err);

updateJobExecutionStatusSafely(
queueItem.getJobExecutionId(),
new StatusDto()
.withErrorStatus(ErrorStatus.FILE_PROCESSING_ERROR)
.withStatus(StatusDto.Status.ERROR),
params
);
})
.onSuccess((QueueJob result) ->
LOGGER.info(
"Completed processing job execution {}!",
queueItem.getJobExecutionId()
)
)
.onComplete((AsyncResult<QueueJob> v) -> {
queueItemDao.deleteQueueItemById(queueItem.getId());
.onComplete((AsyncResult<QueueJob> v) -> {
queueItemDao.deleteQueueItemById(queueItem.getId());

File file = localFile.get();
if (file != null) {
vertx.fileSystem().delete(file.toString());
}
});
File file = localFile.get();
if (file != null) {
vertx.fileSystem().delete(file.toString());
}
})
);
}

protected Future<Void> updateJobExecutionStatusSafely(
Expand Down Expand Up @@ -272,7 +274,7 @@ protected Future<File> createLocalFile(DataImportQueueItem queueItem) {
/**
* Authenticate and get connection parameters (Okapi URL/token)
*/
protected OkapiConnectionParams getConnectionParams(
protected Future<OkapiConnectionParams> getConnectionParams(
DataImportQueueItem queueItem
) {
OkapiConnectionParams provisionalParams = new OkapiConnectionParams(
Expand All @@ -288,19 +290,21 @@ protected OkapiConnectionParams getConnectionParams(
vertx
);

String token = systemUserService.getAuthToken(provisionalParams);

return new OkapiConnectionParams(
Map.of(
XOkapiHeaders.URL.toLowerCase(),
queueItem.getOkapiUrl(),
XOkapiHeaders.TENANT.toLowerCase(),
queueItem.getTenant(),
XOkapiHeaders.TOKEN.toLowerCase(),
token
),
vertx
);
return systemUserService
.getAuthToken(provisionalParams)
.map(token ->
new OkapiConnectionParams(
Map.of(
XOkapiHeaders.URL.toLowerCase(),
queueItem.getOkapiUrl(),
XOkapiHeaders.TENANT.toLowerCase(),
queueItem.getTenant(),
XOkapiHeaders.TOKEN.toLowerCase(),
token
),
vertx
)
);
}

@Override
Expand Down
57 changes: 40 additions & 17 deletions src/test/java/org/folio/service/auth/AuthClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,35 @@
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThrows;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
import java.util.Map;
import java.util.NoSuchElementException;
import org.folio.dataimport.util.OkapiConnectionParams;
import org.folio.service.auth.AuthClient.LoginCredentials;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(VertxUnitRunner.class)
public class AuthClientTest {

private static final String LOGIN_ENDPOINT = "/authn/login";
private static final String LOGIN_ENDPOINT = "/authn/login-with-expiry";
private static final String CREDENTIALS_ENDPOINT = "/authn/credentials";

AuthClient client = new AuthClient();
AuthClient client = new AuthClient(Vertx.vertx());

LoginCredentials testLoginCredentials = LoginCredentials
.builder()
Expand Down Expand Up @@ -74,30 +78,49 @@ public void teardown() {
}

@Test
public void testLogin() {
public void testLogin(TestContext context) {
mockServer.stubFor(
post(LOGIN_ENDPOINT)
.withRequestBody(
equalToJson(JsonObject.mapFrom(testLoginCredentials).toString())
equalToJson(
JsonObject
.of("username", "username", "password", "password")
.toString()
)
)
.willReturn(
WireMock.created().withHeader("Set-Cookie", "folioAccessToken=result")
)
.willReturn(okJson(JsonObject.of("okapiToken", "result").toString()))
);

assertThat(client.login(params, testLoginCredentials), is("result"));

mockServer.verify(exactly(1), anyRequestedFor(urlMatching(LOGIN_ENDPOINT)));
client
.login(params, testLoginCredentials)
.onComplete(
context.asyncAssertSuccess(token -> {
assertThat(token, is("result"));

mockServer.verify(
exactly(1),
anyRequestedFor(urlMatching(LOGIN_ENDPOINT))
);
})
);
}

@Test
public void testLoginError() {
public void testLoginError(TestContext context) {
mockServer.stubFor(post(LOGIN_ENDPOINT).willReturn(badRequest()));

assertThrows(
NoSuchElementException.class,
() -> client.login(params, testLoginCredentials)
);

mockServer.verify(exactly(1), anyRequestedFor(urlMatching(LOGIN_ENDPOINT)));
client
.login(params, testLoginCredentials)
.onComplete(
context.asyncAssertFailure(v -> {
mockServer.verify(
exactly(1),
anyRequestedFor(urlMatching(LOGIN_ENDPOINT))
);
})
);
}

@Test
Expand Down
Loading

0 comments on commit f182fec

Please sign in to comment.