Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
Implement auth store method to delete auth token records from CouchDB (
Browse files Browse the repository at this point in the history
…#243)

* Add implementation to delete token documents from CouchDB

Signed-off-by: Eamonn Mansour <[email protected]>

* Add missing 'rev' query parameter to CouchDB DELETE document requests

Signed-off-by: Eamonn Mansour <[email protected]>

* Use IInternalAuthToken interface in CouchDB auth store

Signed-off-by: Eamonn Mansour <[email protected]>

* Empty commit to kick off build

Signed-off-by: Eamonn Mansour <[email protected]>

---------

Signed-off-by: Eamonn Mansour <[email protected]>
  • Loading branch information
eamansour authored Jul 3, 2024
1 parent 43f2eb9 commit 0478a3d
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import dev.galasa.extensions.common.couchdb.CouchdbValidator;
import dev.galasa.extensions.common.couchdb.pojos.ViewRow;
import dev.galasa.extensions.common.api.HttpRequestFactory;
import dev.galasa.framework.spi.auth.IAuthToken;
import dev.galasa.framework.spi.auth.IInternalAuthToken;
import dev.galasa.framework.spi.auth.IAuthStore;
import dev.galasa.framework.spi.auth.User;
import dev.galasa.framework.spi.utils.ITimeService;
Expand All @@ -31,7 +31,7 @@
* When CouchDB is being used to store user-related information, including information
* about authentication tokens (but not the tokens themselves), this class is called
* upon to implement the auth store.
*
*
* This class registers the auth store as the only auth store in the framework, and is
* only used when Galasa is running in an ecosystem. It gets all of its data from a
* CouchDB server.
Expand Down Expand Up @@ -61,10 +61,10 @@ public CouchdbAuthStore(
}

@Override
public List<IAuthToken> getTokens() throws AuthStoreException {
public List<IInternalAuthToken> getTokens() throws AuthStoreException {
logger.info("Retrieving tokens from CouchDB");
List<ViewRow> tokenDocuments = new ArrayList<>();
List<IAuthToken> tokens = new ArrayList<>();
List<IInternalAuthToken> tokens = new ArrayList<>();

try {
// Get all of the documents in the tokens database
Expand Down Expand Up @@ -106,6 +106,16 @@ public void storeToken(String clientId, String description, User owner) throws A
}
}

@Override
public void deleteToken(String tokenId) throws AuthStoreException {
try {
deleteDocumentFromDatabase(TOKENS_DATABASE_NAME, tokenId);
} catch (CouchdbException e) {
String errorMessage = ERROR_FAILED_TO_DELETE_TOKEN_DOCUMENT.getMessage(e.getMessage());
throw new AuthStoreException(errorMessage, e);
}
}

/**
* Gets an auth token from a CouchDB document with the given document ID.
* The document is assumed to be within the tokens database in the CouchDB server.
Expand All @@ -114,7 +124,7 @@ public void storeToken(String clientId, String description, User owner) throws A
* @return the auth token stored within the given document
* @throws AuthStoreException if there was a problem accessing the auth store or its response
*/
private IAuthToken getAuthTokenFromDocument(String documentId) throws CouchdbException {
private IInternalAuthToken getAuthTokenFromDocument(String documentId) throws CouchdbException {
return getDocumentFromDatabase(TOKENS_DATABASE_NAME, documentId, CouchdbAuthToken.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

import java.time.Instant;

import dev.galasa.framework.spi.auth.IAuthToken;
import dev.galasa.framework.spi.auth.IInternalAuthToken;
import dev.galasa.framework.spi.auth.User;

public class CouchdbAuthToken implements IAuthToken {
public class CouchdbAuthToken implements IInternalAuthToken {

private String _id;
private String dexClientId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import dev.galasa.auth.couchdb.internal.CouchdbAuthStore;
import dev.galasa.auth.couchdb.internal.CouchdbAuthToken;
import dev.galasa.extensions.common.couchdb.pojos.IdRev;
import dev.galasa.extensions.common.couchdb.pojos.PutPostResponse;
import dev.galasa.extensions.common.couchdb.pojos.ViewResponse;
import dev.galasa.extensions.common.couchdb.pojos.ViewRow;
Expand All @@ -31,7 +32,7 @@
import dev.galasa.extensions.mocks.MockTimeService;
import dev.galasa.extensions.mocks.couchdb.MockCouchdbValidator;
import dev.galasa.framework.spi.auth.AuthStoreException;
import dev.galasa.framework.spi.auth.IAuthToken;
import dev.galasa.framework.spi.auth.IInternalAuthToken;
import dev.galasa.framework.spi.auth.User;

public class TestCouchdbAuthStore {
Expand All @@ -50,11 +51,11 @@ public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeEx
}
}

class GetTokenDocumentInteraction extends BaseHttpInteraction {
class GetTokenDocumentInteraction<T> extends BaseHttpInteraction {

public GetTokenDocumentInteraction(String expectedUri, int responseStatusCode, CouchdbAuthToken tokenToReturn) {
public GetTokenDocumentInteraction(String expectedUri, int responseStatusCode, T responseObjToReturn) {
super(expectedUri, responseStatusCode);
setResponsePayload(tokenToReturn);
setResponsePayload(responseObjToReturn);
}

@Override
Expand All @@ -64,6 +65,20 @@ public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeEx
}
}

class DeleteTokenDocumentInteraction extends BaseHttpInteraction {

public DeleteTokenDocumentInteraction(String expectedUri, int responseStatusCode) {
super(expectedUri, responseStatusCode);
setResponsePayload("");
}

@Override
public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeException {
super.validateRequest(host,request);
assertThat(request.getRequestLine().getMethod()).isEqualTo("DELETE");
}
}

class CreateTokenDocInteraction extends BaseHttpInteraction {

public CreateTokenDocInteraction(String expectedUri, int responseStatusCode) {
Expand Down Expand Up @@ -123,7 +138,7 @@ public void testGetTokensReturnsTokensFromCouchdbOK() throws Exception {
CouchdbAuthToken mockToken = new CouchdbAuthToken("token1", "dex-client", "my test token", Instant.now(), new User("johndoe"));
List<HttpInteraction> interactions = new ArrayList<HttpInteraction>();
interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_all_docs", HttpStatus.SC_OK, mockAllDocsResponse));
interactions.add(new GetTokenDocumentInteraction("https://my-auth-store/galasa_tokens/token1", HttpStatus.SC_OK, mockToken));
interactions.add(new GetTokenDocumentInteraction<CouchdbAuthToken>("https://my-auth-store/galasa_tokens/token1", HttpStatus.SC_OK, mockToken));

MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions);

Expand All @@ -133,12 +148,12 @@ public void testGetTokensReturnsTokensFromCouchdbOK() throws Exception {
CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService);

// When...
List<IAuthToken> tokens = authStore.getTokens();
List<IInternalAuthToken> tokens = authStore.getTokens();

// Then...
assertThat(tokens).hasSize(1);

IAuthToken actualToken = tokens.get(0);
IInternalAuthToken actualToken = tokens.get(0);
assertThat(actualToken).usingRecursiveComparison().isEqualTo(mockToken);
}

Expand Down Expand Up @@ -187,4 +202,174 @@ public void testStoreTokenWithFailingRequestToCreateTokenDocumentReturnsError()
assertThat(thrown).isNotNull();
assertThat(thrown.getMessage()).contains("GAL6102E", "Failed to store auth token in the CouchDB tokens database");
}

@Test
public void testDeleteTokenSendsRequestToDeleteTokenDocumentOK() throws Exception {
// Given...
URI authStoreUri = URI.create("couchdb:https://my-auth-store");
MockLogFactory logFactory = new MockLogFactory();

String tokenIdToDelete = "my-old-token";

IdRev mockIdRev = new IdRev();
mockIdRev._rev = "this-is-a-revision";

String expectedGetRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete;
String expectedDeleteRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete + "?rev=" + mockIdRev._rev;

List<HttpInteraction> interactions = new ArrayList<HttpInteraction>();
interactions.add(new GetTokenDocumentInteraction<IdRev>(expectedGetRequestUrl, HttpStatus.SC_OK, mockIdRev));
interactions.add(new DeleteTokenDocumentInteraction(expectedDeleteRequestUrl, HttpStatus.SC_OK));

MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions);

MockHttpClientFactory httpClientFactory = new MockHttpClientFactory(mockHttpClient);
MockTimeService mockTimeService = new MockTimeService(Instant.now());

CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService);

// When...
authStore.deleteToken(tokenIdToDelete);

// Then the assertions made in the document interactions shouldn't have failed.
}

@Test
public void testDeleteTokenWithAcceptedRequestToDeleteTokenDocumentDoesNotError() throws Exception {
// Given...
URI authStoreUri = URI.create("couchdb:https://my-auth-store");
MockLogFactory logFactory = new MockLogFactory();

String tokenIdToDelete = "my-old-token";

IdRev mockIdRev = new IdRev();
mockIdRev._rev = "this-is-a-revision";

String expectedGetRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete;
String expectedDeleteRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete + "?rev=" + mockIdRev._rev;

List<HttpInteraction> interactions = new ArrayList<HttpInteraction>();
interactions.add(new GetTokenDocumentInteraction<IdRev>(expectedGetRequestUrl, HttpStatus.SC_OK, mockIdRev));

// The DELETE request may return a 202 Accepted, which shouldn't be a problem for us
interactions.add(new DeleteTokenDocumentInteraction(expectedDeleteRequestUrl, HttpStatus.SC_ACCEPTED));

MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions);

MockHttpClientFactory httpClientFactory = new MockHttpClientFactory(mockHttpClient);
MockTimeService mockTimeService = new MockTimeService(Instant.now());

CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService);

// When...
authStore.deleteToken(tokenIdToDelete);

// Then the assertions made in the document interactions shouldn't have failed.
}

@Test
public void testDeleteTokenWithFailingRequestToDeleteTokenDocumentThrowsError() throws Exception {
// Given...
URI authStoreUri = URI.create("couchdb:https://my-auth-store");
MockLogFactory logFactory = new MockLogFactory();

String tokenIdToDelete = "my-old-token";

IdRev mockIdRev = new IdRev();
mockIdRev._rev = "this-is-a-revision";

String expectedGetRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete;
String expectedDeleteRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete + "?rev=" + mockIdRev._rev;

List<HttpInteraction> interactions = new ArrayList<HttpInteraction>();
interactions.add(new GetTokenDocumentInteraction<IdRev>(expectedGetRequestUrl, HttpStatus.SC_OK, mockIdRev));

// Simulate an internal server error
interactions.add(new DeleteTokenDocumentInteraction(expectedDeleteRequestUrl, HttpStatus.SC_INTERNAL_SERVER_ERROR));

MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions);

MockHttpClientFactory httpClientFactory = new MockHttpClientFactory(mockHttpClient);
MockTimeService mockTimeService = new MockTimeService(Instant.now());

CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService);

// When...
AuthStoreException thrown = catchThrowableOfType(() -> {
authStore.deleteToken(tokenIdToDelete);
}, AuthStoreException.class);

// Then...
assertThat(thrown).isNotNull();
assertThat(thrown.getMessage()).contains("GAL6104E", "Failed to delete auth token from the CouchDB tokens database");
assertThat(thrown.getMessage()).contains("GAL6007E", "Expected status code(s) [200, 202] but received 500");
}

@Test
public void testDeleteTokenWithFailingRequestToGetTokenDocumentThrowsError() throws Exception {
// Given...
URI authStoreUri = URI.create("couchdb:https://my-auth-store");
MockLogFactory logFactory = new MockLogFactory();

String tokenIdToDelete = "my-old-token";

IdRev mockIdRev = new IdRev();
mockIdRev._rev = "this-is-a-revision";

String expectedGetRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete;

List<HttpInteraction> interactions = new ArrayList<HttpInteraction>();

// Simulate an internal server error
interactions.add(new GetTokenDocumentInteraction<IdRev>(expectedGetRequestUrl, HttpStatus.SC_INTERNAL_SERVER_ERROR, mockIdRev));

MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions);

MockHttpClientFactory httpClientFactory = new MockHttpClientFactory(mockHttpClient);
MockTimeService mockTimeService = new MockTimeService(Instant.now());

CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService);

// When...
AuthStoreException thrown = catchThrowableOfType(() -> {
authStore.deleteToken(tokenIdToDelete);
}, AuthStoreException.class);

// Then...
assertThat(thrown).isNotNull();
assertThat(thrown.getMessage()).contains("GAL6104E", "Failed to delete auth token from the CouchDB tokens database");
}

@Test
public void testDeleteTokenWithBadGetTokenDocumentResponseBodyThrowsError() throws Exception {
// Given...
URI authStoreUri = URI.create("couchdb:https://my-auth-store");
MockLogFactory logFactory = new MockLogFactory();

String tokenIdToDelete = "my-old-token";

String expectedGetRequestUrl = "https://my-auth-store/galasa_tokens/" + tokenIdToDelete;

List<HttpInteraction> interactions = new ArrayList<HttpInteraction>();

// Simulate an internal server error
interactions.add(new GetTokenDocumentInteraction<IdRev>(expectedGetRequestUrl, HttpStatus.SC_OK, null));

MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions);

MockHttpClientFactory httpClientFactory = new MockHttpClientFactory(mockHttpClient);
MockTimeService mockTimeService = new MockTimeService(Instant.now());

CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService);

// When...
AuthStoreException thrown = catchThrowableOfType(() -> {
authStore.deleteToken(tokenIdToDelete);
}, AuthStoreException.class);

// Then...
assertThat(thrown).isNotNull();
assertThat(thrown.getMessage()).contains("GAL6104E", "Failed to delete auth token from the CouchDB tokens database");
assertThat(thrown.getMessage()).contains("GAL6011E", "Failed to get document with ID 'my-old-token' from the 'galasa_tokens' database");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ public enum Errors {
ERROR_FAILED_TO_CREATE_COUCHDB_DATABASE (6004,"GAL6004E: Internal server error. Failed to create CouchDB database ''{0}''. Status code {1} from CouchDB server is not 201. The CouchDB server could be experiencing temporary issues or is not correctly configured. Report the problem to your Galasa Ecosystem owner."),
ERROR_OUTDATED_COUCHDB_VERSION (6005,"GAL6005E: Outdated CouchDB server version ''{0}'' detected. Expected version ''{1}'' or above. Report the problem to your Galasa Ecosystem owner."),
ERROR_FAILED_TO_CREATE_COUCHDB_DOCUMENT (6006,"GAL6006E: Internal server error. Failed to create new document in the CouchDB database ''{0}''. The CouchDB server could be experiencing temporary issues or is not correctly configured. Report the problem to your Galasa Ecosystem owner."),
ERROR_UNEXPECTED_COUCHDB_HTTP_RESPONSE (6007,"GAL6007E: Internal server error. Unexpected response received from CouchDB server after sending a HTTP request to ''{0}''. Expected status code {1} but received {2}. The CouchDB server could be experiencing temporary issues. Report the problem to your Galasa Ecosystem owner."),
ERROR_UNEXPECTED_COUCHDB_HTTP_RESPONSE (6007,"GAL6007E: Internal server error. Unexpected response received from CouchDB server after sending a HTTP request to ''{0}''. Expected status code(s) [{1}] but received {2}. The CouchDB server could be experiencing temporary issues. Report the problem to your Galasa Ecosystem owner."),
ERROR_FAILURE_OCCURRED_WHEN_CONTACTING_COUCHDB (6008,"GAL6008E: Internal server error. Unexpected failure occurred during HTTP request to CouchDB server at URL ''{0}''. Cause: ''{1}''"),
ERROR_FAILED_TO_GET_DOCUMENTS_FROM_DATABASE (6009,"GAL6009E: Internal server error. Failed to get all documents in the ''{0}'' database. Invalid JSON response returned from CouchDB. CouchDB could be experiencing temporary issues or is not correctly configured. Report the problem to your Galasa Ecosystem owner."),
ERROR_INVALID_COUCHDB_VERSION_FORMAT (6010,"GAL6010E: Invalid CouchDB server version format detected. The CouchDB version ''{0}'' must be in the semantic versioning format (e.g. major.minor.patch). Expected version ''{1}'' or above. Report the problem to your Galasa Ecosystem owner."),
ERROR_FAILED_TO_GET_DOCUMENT_FROM_DATABASE (6011,"GAL6011E: Internal server error. Failed to get document with ID ''{0}'' from the ''{1}'' database. Invalid JSON response returned from CouchDB. CouchDB could be experiencing temporary issues or is not correctly configured. Report the problem to your Galasa Ecosystem owner."),

// CouchDB Auth Store errors
ERROR_GALASA_AUTH_STORE_SHUTDOWN_FAILED (6100,"GAL6100E: Failed to shut down Galasa CouchDB auth store. Cause: {0}"),
ERROR_FAILED_TO_RETRIEVE_TOKENS (6101,"GAL6101E: Failed to get auth tokens from the CouchDB auth store. Cause: {0}"),
ERROR_FAILED_TO_CREATE_TOKEN_DOCUMENT (6102,"GAL6102E: Failed to store auth token in the CouchDB tokens database. Cause: {0}"),
ERROR_FAILED_TO_INITIALISE_AUTH_STORE (6103,"GAL6103E: Failed to initialise the Galasa CouchDB auth store. Cause: {0}"),
ERROR_FAILED_TO_DELETE_TOKEN_DOCUMENT (6104,"GAL6104E: Failed to delete auth token from the CouchDB tokens database. Cause: {0}"),

// REST CPS errors
ERROR_GALASA_WRONG_NUMBER_OF_PARAMETERS_IN_MESSAGE (6999,"GAL6999E: Failed to render message template. Not the expected number of parameters. Got ''{0}''. Expected ''{1}''"),
Expand Down
Loading

0 comments on commit 0478a3d

Please sign in to comment.