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

Commit

Permalink
Add REST API endpoints for secrets (#662)
Browse files Browse the repository at this point in the history
* WIP: Start implementing Secrets APIs

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* feat: Add GET /secrets and PUT /secrets/{secret-name} endpoints

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* feat: Update secrets endpoints in openapi spec

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* fix: Add secretName path parameter to /secrets/{secretName} endpoints

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* feat: PUT /secrets/{secretName} now supports changing secret types and updating existing secrets

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* fix: Throw error if unexpected fields are given to update secrets

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* review: Replaced secret ID with secret name

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* feat: Add description, lastUpdatedTime, and lastUpdatedBy to secrets

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* refactor: Separate validation from resource processors

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

* fix: Retain secret description when updating a secret

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>

---------

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>
  • Loading branch information
eamansour authored Oct 31, 2024
1 parent aa5e226 commit fcc9e58
Show file tree
Hide file tree
Showing 55 changed files with 4,080 additions and 368 deletions.
42 changes: 40 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,22 @@
"verified_result": null
}
],
"galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java": [
{
"hashed_secret": "a5c1ad5f1dc7d24152e39cb14dfa99775fa1884d",
"is_secret": false,
"is_verified": false,
"line_number": 139,
"type": "Secret Keyword",
"verified_result": null
}
],
"galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java": [
{
"hashed_secret": "ef04cf5107d3be11fceddd045cc585d2bda031bb",
"is_secret": false,
"is_verified": false,
"line_number": 28,
"line_number": 30,
"type": "JSON Web Token",
"verified_result": null
}
Expand All @@ -109,7 +119,35 @@
"hashed_secret": "0ea7458942ab65e0a340cf4fd28ca00d93c494f3",
"is_secret": false,
"is_verified": false,
"line_number": 321,
"line_number": 710,
"type": "Secret Keyword",
"verified_result": null
}
],
"galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java": [
{
"hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13",
"is_secret": false,
"is_verified": false,
"line_number": 293,
"type": "Secret Keyword",
"verified_result": null
},
{
"hashed_secret": "89e7fc0c50091804bfeb26cddefc0e701dd60fab",
"is_secret": false,
"is_verified": false,
"line_number": 732,
"type": "Secret Keyword",
"verified_result": null
}
],
"galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java": [
{
"hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13",
"is_secret": false,
"is_verified": false,
"line_number": 670,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import dev.galasa.framework.api.common.IBeanValidator;
import dev.galasa.framework.api.common.InternalServletException;
import dev.galasa.framework.api.common.ServletError;
import dev.galasa.framework.api.common.resources.AbstractValidator;

import static dev.galasa.framework.api.common.ServletErrorMessage.*;

import javax.servlet.http.HttpServletResponse;

public class TokenPayloadValidator implements IBeanValidator<TokenPayload> {
public class TokenPayloadValidator extends AbstractValidator implements IBeanValidator<TokenPayload> {

@Override
public void validate(TokenPayload tokenPayload) throws InternalServletException {
Expand All @@ -36,21 +37,4 @@ public void validate(TokenPayload tokenPayload) throws InternalServletException
throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
}
}

/**
* Checks whether a given string contains only alphanumeric characters, '-', and '_'
*
* @param str the string to validate
* @return true if the string contains only alphanumeric characters, '-', and '_', or false otherwise
*/
private boolean isAlphanumWithDashes(String str) {
boolean isValid = true;
for (char c : str.toCharArray()) {
if (!Character.isLetterOrDigit(c) && c != '-' && c != '_') {
isValid = false;
break;
}
}
return isValid;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ public enum ServletErrorMessage {
GAL5072_INVALID_GALASA_SECRET_MISSING_TYPE_DATA (5072, "E: Invalid GalasaSecret provided. The ''{0}'' type was provided but the following fields are missing from the ''data'' field: [{1}]. Check that your request payload is correct and try again."),
GAL5073_UNSUPPORTED_GALASA_SECRET_ENCODING (5073, "E: Unsupported data encoding scheme provided. Supported encoding schemes are: [{0}]. Check that your request payload is correct and try again."),
GAL5074_UNKNOWN_GALASA_SECRET_TYPE (5074, "E: Unknown GalasaSecret type provided. Supported GalasaSecret types are: [{0}]. Check that your request payload is correct and try again."),
GAL5075_ERROR_SECRET_ALREADY_EXISTS (5075, "E: Error occurred when trying to create a secret with the given ID. A secret with the provided ID already exists."),
GAL5076_ERROR_SECRET_DOES_NOT_EXIST (5076, "E: Error occurred when trying to update a secret with the given ID. A secret with the provided ID does not exist and therefore cannot be updated."),
GAL5075_ERROR_SECRET_ALREADY_EXISTS (5075, "E: Error occurred when trying to create a secret with the given name. A secret with the provided name already exists."),
GAL5076_ERROR_SECRET_DOES_NOT_EXIST (5076, "E: Error occurred. A secret with the provided name does not exist. Check that your provided secret name is correct and try again."),
GAL5077_FAILED_TO_SET_SECRET (5077, "E: Failed to set a secret with the given ID in the credentials store. The credentials store might be experiencing temporary issues. Report the problem to your Galasa Ecosystem owner."),
GAL5078_FAILED_TO_DELETE_SECRET (5078, "E: Failed to delete a secret with the given ID from the credentials store. The credentials store might be experiencing temporary issues. Report the problem to your Galasa Ecosystem owner."),
GAL5079_FAILED_TO_GET_SECRET (5079, "E: Failed to retrieve the secret with the given ID from the credentials store. A secret with the provided ID does not exist and therefore cannot be updated."),
GAL5079_FAILED_TO_GET_SECRET (5079, "E: Failed to retrieve the secret with the given ID from the credentials store. A secret with the provided name does not exist and therefore cannot be updated."),

// Auth APIs...
GAL5051_INVALID_GALASA_TOKEN_PROVIDED (5051, "E: Invalid GALASA_TOKEN value provided. Please ensure you have set the correct GALASA_TOKEN property for the targeted ecosystem at ''{0}'' and try again."),
Expand All @@ -125,7 +125,20 @@ public enum ServletErrorMessage {

// User APIs...
GAL5081_INVALID_QUERY_PARAM_VALUE (5081, "E: A request to get the user details for a particular user failed. The query parameter provided is not valid. Supported values for the ‘loginId’ query parameter are : ‘me’. This problem is caused by the client program sending a bad request. Please report this problem to the owner of your client program."),
GAL5082_NO_LOGINID_PARAM_PROVIDED (5082, "E: A request to get the user details failed. The request did not supply a ‘loginId’ filter. A ‘loginId’ query parameter with a value of : ‘me’ was expected. This problem is caused by the client program sending a bad request. Please report this problem to the owner of your client program.")
GAL5082_NO_LOGINID_PARAM_PROVIDED (5082, "E: A request to get the user details failed. The request did not supply a ‘loginId’ filter. A ‘loginId’ query parameter with a value of : ‘me’ was expected. This problem is caused by the client program sending a bad request. Please report this problem to the owner of your client program."),

// Secrets APIs...
GAL5092_INVALID_SECRET_NAME_PROVIDED (5092, "E: Invalid secret name provided. The name of a Galasa secret cannot be empty, contain only spaces or tabs, or contain dots ('.'), and must only contain characters in the Latin-1 character set. Check your request payload and try again."),
GAL5093_ERROR_SECRET_NOT_FOUND (5093, "E: Unable to retrieve a secret with the given name. No such secret exists. Check your request query parameters and try again."),
GAL5094_FAILED_TO_GET_SECRET_FROM_CREDS (5094, "E: Failed to retrieve a secret with the given name from the credentials store. The credentials store might be badly configured or could be experiencing a temporary issue. Report the problem to your Galasa Ecosystem owner."),
GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED (5095, "E: Invalid secret payload provided. The ''password'' and ''token'' fields are mutually exclusive and cannot be provided in the same secret. Check your request payload and try again."),
GAL5096_ERROR_MISSING_SECRET_VALUE (5096, "E: Invalid secret payload provided. One or more secret fields in your request payload are missing a ''value''. Check your request payload and try again."),
GAL5097_FAILED_TO_DECODE_SECRET_VALUE (5097, "E: Failed to decode a provided secret value. Expected the value to be encoded in ''{0}'' format but it was not. Check your request values are properly encoded and try again."),
GAL5098_ERROR_PASSWORD_MISSING_USERNAME (5098, "E: Invalid secret payload provided. A ''password'' field was provided but the ''username'' field was missing. Check your request payload and try again."),
GAL5099_ERROR_MISSING_REQUIRED_SECRET_FIELD (5099, "E: Invalid secret payload provided. The ''{0}'' type was provided but the required ''{1}'' field was missing. Check your request payload and try again."),
GAL5100_ERROR_UNEXPECTED_SECRET_FIELD_PROVIDED (5100, "E: Invalid secret payload provided. An unexpected field was given to update a ''{0}'' secret. Only the following fields can be provided to update this secret: ''{1}''. Check your request payload and try again."),
GAL5101_ERROR_UNEXPECTED_SECRET_TYPE_DETECTED (5101, "E: Unknown secret type detected. A secret retrieved from the credentials store is in an unknown or unsupported format. Report the problem to your Galasa Ecosystem owner."),
GAL5102_INVALID_SECRET_DESCRIPTION_PROVIDED (5102, "E: Invalid secret description provided. The description should not only contain spaces or tabs. When provided, it must contain characters in the Latin-1 character set. Report the problem to your Galasa Ecosystem owner."),
;


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright contributors to the Galasa project
*
* SPDX-License-Identifier: EPL-2.0
*/
package dev.galasa.framework.api.common.resources;

/**
* A base validator class that contains commonly-used validation methods
*/
public abstract class AbstractValidator {

/**
* Checks whether a given string is in valid Latin-1 format (e.g. characters in the range 0 - 255)
*
* @param str the string to validate
* @return true if the string is in valid Latin-1 format, or false otherwise
*/
public boolean isLatin1(String str) {
boolean isValidLatin1 = true;
for (char i = 0; i < str.length(); i++) {
if (str.charAt(i) > 255) {
isValidLatin1 = false;
break;
}
}
return isValidLatin1;
}

/**
* Checks whether a given string contains only alphanumeric characters, '-', and '_'
*
* @param str the string to validate
* @return true if the string contains only alphanumeric characters, '-', and '_', or false otherwise
*/
public boolean isAlphanumWithDashes(String str) {
boolean isValid = true;
for (char c : str.toCharArray()) {
if (!Character.isLetterOrDigit(c) && c != '-' && c != '_') {
isValid = false;
break;
}
}
return isValid;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright contributors to the Galasa project
*
* SPDX-License-Identifier: EPL-2.0
*/
package dev.galasa.framework.api.common.resources;

import static dev.galasa.framework.api.common.ServletErrorMessage.*;
import static dev.galasa.framework.api.common.resources.ResourceAction.*;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import com.google.gson.JsonObject;

import dev.galasa.framework.api.common.IBeanValidator;
import dev.galasa.framework.api.common.InternalServletException;
import dev.galasa.framework.api.common.ServletError;

/**
* An abstract class containing the base methods used to validate Galasa resources.
*/
public abstract class GalasaResourceValidator<T> extends AbstractValidator implements IBeanValidator<T> {

public static final String DEFAULT_API_VERSION = "galasa-dev/v1alpha1";

protected List<String> validationErrors = new ArrayList<>();
protected ResourceAction action;

public GalasaResourceValidator() {}

public GalasaResourceValidator(ResourceAction action) {
this.action = action;
}

public List<String> getValidationErrors() {
return validationErrors;
}

private List<String> getRequiredResourceFields() {
List<String> requiredFields = new ArrayList<>();
requiredFields.add("apiVersion");
requiredFields.add("metadata");
if (action != DELETE) {
requiredFields.add("data");
}
return requiredFields;
}

protected List<String> getMissingResourceFields(JsonObject resourceJson, List<String> requiredFields) {
List<String> missingFields = new ArrayList<>();
for (String field : requiredFields) {
if (!resourceJson.has(field)) {
missingFields.add(field);
}
}
return missingFields;
}

protected void checkResourceHasRequiredFields(
JsonObject resourceJson,
String expectedApiVersion
) throws InternalServletException {
List<String> requiredFields = getRequiredResourceFields();
List<String> missingFields = getMissingResourceFields(resourceJson, requiredFields);
if (!missingFields.isEmpty()) {
ServletError error = new ServletError(GAL5069_MISSING_REQUIRED_FIELDS, String.join(", ", missingFields));
throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
}

String apiVersion = resourceJson.get("apiVersion").getAsString();
if (!apiVersion.equals(expectedApiVersion)) {
ServletError error = new ServletError(GAL5027_UNSUPPORTED_API_VERSION, expectedApiVersion);
throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: EPL-2.0
*/
package dev.galasa.framework.api.common.resources;

public enum GalasaSecretType {
USERNAME_PASSWORD("UsernamePassword", "username", "password"),
USERNAME_TOKEN("UsernameToken", "username", "token"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import dev.galasa.framework.api.common.ServletError;
import dev.galasa.framework.spi.creds.CredentialsException;
import dev.galasa.framework.spi.creds.ICredentialsService;
import dev.galasa.framework.spi.utils.ITimeService;

import static dev.galasa.framework.api.common.ServletErrorMessage.*;

Expand All @@ -19,11 +20,13 @@ public class Secret {

private String secretId;
private ICredentialsService credentialsService;
private ITimeService timeService;
private ICredentials value;

public Secret(ICredentialsService credentialsService, String secretName) {
public Secret(ICredentialsService credentialsService, String secretName, ITimeService timeService) {
this.secretId = secretName;
this.credentialsService = credentialsService;
this.timeService = timeService;
}

public boolean existsInCredentialsStore() {
Expand All @@ -39,8 +42,10 @@ public void loadValueFromCredentialsStore() throws InternalServletException {
}
}

public void setSecretToCredentialsStore(ICredentials newValue) throws InternalServletException {
public void setSecretToCredentialsStore(ICredentials newValue, String username) throws InternalServletException {
try {
newValue.setLastUpdatedTime(timeService.now());
newValue.setLastUpdatedByUser(username);
credentialsService.setCredentials(secretId, newValue);
} catch (CredentialsException e) {
ServletError error = new ServletError(GAL5077_FAILED_TO_SET_SECRET);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright contributors to the Galasa project
*
* SPDX-License-Identifier: EPL-2.0
*/
package dev.galasa.framework.api.common.resources;

import static dev.galasa.framework.api.common.ServletErrorMessage.*;

import java.util.List;

import javax.servlet.http.HttpServletResponse;

import dev.galasa.framework.api.common.InternalServletException;
import dev.galasa.framework.api.common.ServletError;

public abstract class SecretValidator<T> extends GalasaResourceValidator<T> {

public static final List<String> SUPPORTED_ENCODING_SCHEMES = List.of("base64");

public SecretValidator() {}

public SecretValidator(ResourceAction action) {
super(action);
}

protected void validateSecretName(String secretName) throws InternalServletException {
if (secretName == null || secretName.isBlank() || secretName.contains(".") || !isLatin1(secretName)) {
ServletError error = new ServletError(GAL5092_INVALID_SECRET_NAME_PROVIDED);
throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
}
}

protected void validateDescription(String description) throws InternalServletException {
if (description != null && (description.isBlank() || !isLatin1(description))) {
ServletError error = new ServletError(GAL5102_INVALID_SECRET_DESCRIPTION_PROVIDED);
throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import dev.galasa.framework.spi.utils.GalasaGson;

import static org.assertj.core.api.Assertions.*;

import java.util.Map;
Expand All @@ -25,7 +27,10 @@ public class BaseServletTest {
// "name": "Jack Skellington",
// "iat": 1516239022
// }
public final static String DUMMY_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0UmVxdWVzdG9yIiwic3ViIjoicmVxdWVzdG9ySWQiLCJuYW1lIjoiSmFjayBTa2VsbGluZ3RvbiIsImlhdCI6MTUxNjIzOTAyMn0.kW1arFknbywrtRrxsLjB2MiXcM6oSgnUrOpuAlE5dhk"; //Dummy JWT
public static final String DUMMY_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0UmVxdWVzdG9yIiwic3ViIjoicmVxdWVzdG9ySWQiLCJuYW1lIjoiSmFjayBTa2VsbGluZ3RvbiIsImlhdCI6MTUxNjIzOTAyMn0.kW1arFknbywrtRrxsLjB2MiXcM6oSgnUrOpuAlE5dhk"; //Dummy JWT
public static final String JWT_USERNAME = "testRequestor";

protected static final GalasaGson gson = new GalasaGson();

protected void checkErrorStructure(String jsonString , int expectedErrorCode , String... expectedErrorMessageParts ) throws Exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public MockFramework() {
// Do nothing...
}

public MockFramework(MockCredentialsService credsService) {
this.creds = credsService;
}

public MockFramework(IAuthStoreService authStoreService) {
this.authStoreService = authStoreService;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ public void setContentType(String contentType) {
this.contentType = contentType;
}

public void setQueryParameter(String parameter, String value) {
this.parameterMap.put(parameter, new String[] { value });
}

public void setHeader(String header, String value) {
this.headerMap.put(header, value);
}
Expand Down
Loading

0 comments on commit fcc9e58

Please sign in to comment.