From be4e99128c1ac62ecaad8969cce4ae84b381225d Mon Sep 17 00:00:00 2001
From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>
Date: Tue, 29 Oct 2024 11:12:57 +0000
Subject: [PATCH] feat: Add description, lastUpdatedTime, and lastUpdatedBy to
 secrets

Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com>
---
 .secrets.baseline                             |   8 +-
 .../internal/TokenPayloadValidator.java       |  20 +-
 .../api/common/ServletErrorMessage.java       |   3 +-
 .../resources/BaseResourceValidator.java      |  46 +++
 .../api/common/resources/Secret.java          |   9 +-
 .../framework/api/common/BaseServletTest.java |   1 +
 .../src/main/resources/openapi.yaml           |  18 +-
 .../api/resources/ResourcesServlet.java       |   9 +-
 .../processors/GalasaPropertyProcessor.java   |   2 +-
 .../processors/GalasaSecretProcessor.java     |  39 ++-
 .../processors/IGalasaResourceProcessor.java  |   3 +-
 .../api/resources/routes/ResourcesRoute.java  |  28 +-
 .../api/resources/ResourcesServletTest.java   |  30 +-
 .../resources/mocks/MockResourcesServlet.java |   8 +
 .../GalasaPropertyProcessorTest.java          |  48 ++--
 .../processors/GalasaSecretProcessorTest.java | 218 ++++++++++----
 .../resources/routes/TestResourcesRoute.java  |  72 +++--
 .../framework/api/secrets/SecretsServlet.java |  11 +-
 .../internal/SecretRequestValidator.java      |  15 +-
 .../UpdateSecretRequestValidator.java         |   6 +-
 .../internal/routes/AbstractSecretsRoute.java |  43 ++-
 .../internal/routes/SecretDetailsRoute.java   |  17 +-
 .../secrets/internal/routes/SecretsRoute.java |  13 +-
 .../api/secrets/internal/MockCredentials.java |  36 +++
 .../internal/SecretDetailsRouteTest.java      | 271 +++++++++++++-----
 .../secrets/internal/SecretsRouteTest.java    | 224 +++++++++++----
 .../secrets/internal/SecretsServletTest.java  |  41 ++-
 .../api/secrets/mocks/MockSecretsServlet.java |  14 +-
 .../spi/creds/AbstractCredentials.java        |  80 ++++++
 .../framework/spi/creds/Credentials.java      |   1 -
 .../framework/spi/creds/CredentialsToken.java |   5 +-
 .../spi/creds/CredentialsUsername.java        |   4 +-
 .../creds/CredentialsUsernamePassword.java    |   6 +-
 .../spi/creds/CredentialsUsernameToken.java   |   6 +-
 .../framework/mocks/MockCredentials.java      |  18 --
 .../main/java/dev/galasa/ICredentials.java    |  10 +
 36 files changed, 1058 insertions(+), 325 deletions(-)
 create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/BaseResourceValidator.java
 create mode 100644 galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.java
 delete mode 100644 galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java

diff --git a/.secrets.baseline b/.secrets.baseline
index 750f2ecf9..2882e431f 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -119,7 +119,7 @@
         "hashed_secret": "0ea7458942ab65e0a340cf4fd28ca00d93c494f3",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 321,
+        "line_number": 710,
         "type": "Secret Keyword",
         "verified_result": null
       }
@@ -129,7 +129,7 @@
         "hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 285,
+        "line_number": 293,
         "type": "Secret Keyword",
         "verified_result": null
       },
@@ -137,7 +137,7 @@
         "hashed_secret": "89e7fc0c50091804bfeb26cddefc0e701dd60fab",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 724,
+        "line_number": 732,
         "type": "Secret Keyword",
         "verified_result": null
       }
@@ -147,7 +147,7 @@
         "hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 636,
+        "line_number": 670,
         "type": "Secret Keyword",
         "verified_result": null
       }
diff --git a/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java b/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java
index 1b0fb6763..3ddf109e9 100644
--- a/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java
+++ b/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java
@@ -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.BaseResourceValidator;
 
 import static dev.galasa.framework.api.common.ServletErrorMessage.*;
 
 import javax.servlet.http.HttpServletResponse;
 
-public class TokenPayloadValidator implements IBeanValidator<TokenPayload> {
+public class TokenPayloadValidator extends BaseResourceValidator implements IBeanValidator<TokenPayload> {
 
     @Override
     public void validate(TokenPayload tokenPayload) throws InternalServletException {
@@ -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;
-    }
 }
diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java
index 4a59d2913..86e6b4fe3 100644
--- a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java
+++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java
@@ -128,7 +128,7 @@ public enum ServletErrorMessage {
     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 or contain only spaces or tabs. Check your request payload and try again."),
+    GAL5092_INVALID_SECRET_NAME_PROVIDED              (5092, "E: Invalid secret name provided. The name of a Galasa secret cannot be empty or contain only spaces or tabs, 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."),
@@ -138,6 +138,7 @@ public enum ServletErrorMessage {
     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."),
     ;
 
 
diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/BaseResourceValidator.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/BaseResourceValidator.java
new file mode 100644
index 000000000..cab2bc07b
--- /dev/null
+++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/BaseResourceValidator.java
@@ -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 class BaseResourceValidator {
+
+    /**
+     * 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;
+    }
+}
diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java
index 90f7d5888..272e1e8fe 100644
--- a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java
+++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java
@@ -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.*;
 
@@ -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() {
@@ -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);
diff --git a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java
index 93b8163d7..b37ba0e26 100644
--- a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java
+++ b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java
@@ -28,6 +28,7 @@ public class BaseServletTest {
     //   "iat": 1516239022
     // }
     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();
 
diff --git a/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml b/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml
index 07d2372c8..adf6411ff 100644
--- a/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml
+++ b/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml
@@ -2042,7 +2042,20 @@ components:
             name:
               type: string
               description: |
-                The name of the Galasa Secret to perform a resource action on.
+                The name that identifies the Galasa Secret.
+            description:
+              type: string
+              description: |
+                The description to be associated with the Galasa Secret.
+            lastUpdatedTime:
+              type: string
+              format: date-time
+              description: |
+                The timestamp at which the Galasa Secret was last updated.
+            lastUpdatedBy:
+              type: string
+              description: |
+                The ID of the last user that updated the Galasa Secret.
             encoding:
               type: string
               description: |
@@ -2109,6 +2122,9 @@ components:
         name:
           type: string
           description: The name of the secret to create or update
+        description:
+          type: string
+          description: The description to associate with the secret to create or update
         type:
           type: string
           description: The type of the secret to create or update
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java
index 645687562..c22bc2833 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java
@@ -18,11 +18,15 @@
 import dev.galasa.framework.FileSystem;
 import dev.galasa.framework.IFileSystem;
 import dev.galasa.framework.api.common.BaseServlet;
+import dev.galasa.framework.api.common.Environment;
+import dev.galasa.framework.api.common.SystemEnvironment;
 import dev.galasa.framework.api.common.resources.CPSFacade;
 import dev.galasa.framework.api.resources.routes.ResourcesRoute;
 import dev.galasa.framework.spi.ConfigurationPropertyStoreException;
 import dev.galasa.framework.spi.IFramework;
 import dev.galasa.framework.spi.creds.CredentialsException;
+import dev.galasa.framework.spi.utils.ITimeService;
+import dev.galasa.framework.spi.utils.SystemTimeService;
 /*
  * Proxy Servlet for the /resources/* endpoints
  */
@@ -38,6 +42,9 @@ public class ResourcesServlet extends BaseServlet {
 	protected Log  logger  =  LogFactory.getLog(this.getClass());
 
 	protected IFileSystem fileSystem = new FileSystem();
+
+    protected ITimeService timeService = new SystemTimeService();
+    protected Environment env = new SystemEnvironment();
 	
 	protected IFramework getFramework() {
         return this.framework;
@@ -54,7 +61,7 @@ public void init() throws ServletException {
 		super.init();
 
 		try {
-            addRoute(new ResourcesRoute(getResponseBuilder(), new CPSFacade(framework), framework.getCredentialsService()));
+            addRoute(new ResourcesRoute(getResponseBuilder(), new CPSFacade(framework), framework.getCredentialsService(), timeService, env));
         } catch (ConfigurationPropertyStoreException | CredentialsException e) {
             logger.error("Failed to initialise the Resources servlet", e);
             throw new ServletException("Failed to initialise the Resources servlet", e);
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java
index ea93ab5ce..17bfe23b6 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java
@@ -36,7 +36,7 @@ public GalasaPropertyProcessor(CPSFacade cps) {
     }
 
     @Override
-    public List<String> processResource(JsonObject resource, ResourceAction action) throws InternalServletException {
+    public List<String> processResource(JsonObject resource, ResourceAction action, String username) throws InternalServletException {
         List<String> errors = checkGalasaPropertyJsonStructure(resource, action);
         try {
             if (errors.isEmpty()) {
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java
index 068a8efc9..11a36c8d6 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java
@@ -26,8 +26,10 @@
 import dev.galasa.ICredentials;
 import dev.galasa.framework.api.beans.generated.GalasaSecret;
 import dev.galasa.framework.api.beans.generated.GalasaSecretdata;
+import dev.galasa.framework.api.beans.generated.GalasaSecretmetadata;
 import dev.galasa.framework.api.common.InternalServletException;
 import dev.galasa.framework.api.common.ServletError;
+import dev.galasa.framework.api.common.resources.BaseResourceValidator;
 import dev.galasa.framework.api.common.resources.GalasaSecretType;
 import dev.galasa.framework.api.common.resources.ResourceAction;
 import dev.galasa.framework.api.common.resources.Secret;
@@ -36,6 +38,7 @@
 import dev.galasa.framework.spi.creds.CredentialsUsernamePassword;
 import dev.galasa.framework.spi.creds.CredentialsUsernameToken;
 import dev.galasa.framework.spi.creds.ICredentialsService;
+import dev.galasa.framework.spi.utils.ITimeService;
 
 /**
  * Processor class to handle creating, updating, and deleting GalasaSecret resources
@@ -47,20 +50,24 @@ public class GalasaSecretProcessor extends AbstractGalasaResourceProcessor imple
     private static final List<String> SUPPORTED_ENCODING_SCHEMES = List.of("base64");
 
     private ICredentialsService credentialsService;
+    private ITimeService timeService;
 
-    public GalasaSecretProcessor(ICredentialsService credentialsService) {
+    private BaseResourceValidator validator = new BaseResourceValidator();
+
+    public GalasaSecretProcessor(ICredentialsService credentialsService, ITimeService timeService) {
         this.credentialsService = credentialsService;
+        this.timeService = timeService;
     }
 
     @Override
-    public List<String> processResource(JsonObject resourceJson, ResourceAction action) throws InternalServletException {
+    public List<String> processResource(JsonObject resourceJson, ResourceAction action, String username) throws InternalServletException {
         logger.info("Processing GalasaSecret resource");
         List<String> errors = checkGalasaSecretJsonStructure(resourceJson, action);
         if (errors.isEmpty()) {
             logger.info("GalasaSecret validated successfully");
             GalasaSecret galasaSecret = gson.fromJson(resourceJson, GalasaSecret.class);
             String credentialsId = galasaSecret.getmetadata().getname();
-            Secret secret = new Secret(credentialsService, credentialsId);
+            Secret secret = new Secret(credentialsService, credentialsId, timeService);
 
             if (action == DELETE) {
                 logger.info("Deleting secret from credentials store");
@@ -77,12 +84,13 @@ public List<String> processResource(JsonObject resourceJson, ResourceAction acti
                     throw new InternalServletException(error, HttpServletResponse.SC_NOT_FOUND);
                 }
                 
-                GalasaSecretType secretType = GalasaSecretType.getFromString(galasaSecret.getmetadata().gettype().toString());
+                GalasaSecretmetadata metadata = galasaSecret.getmetadata();
+                GalasaSecretType secretType = GalasaSecretType.getFromString(metadata.gettype().toString());
                 GalasaSecretdata decodedData = decodeSecretData(galasaSecret);
-                ICredentials credentials = getCredentialsFromSecret(secretType, decodedData);
+                ICredentials credentials = getCredentialsFromSecret(secretType, decodedData, metadata);
 
                 logger.info("Setting secret in credentials store");
-                secret.setSecretToCredentialsStore(credentials);
+                secret.setSecretToCredentialsStore(credentials, username);
                 logger.info("Secret set in credentials store OK");
             }
             logger.info("Processed GalasaSecret resource OK");
@@ -140,7 +148,11 @@ private List<String> checkGalasaSecretJsonStructure(JsonObject secretJson, Resou
         return validationErrors;
     }
 
-    private ICredentials getCredentialsFromSecret(GalasaSecretType secretType, GalasaSecretdata decodedData) {
+    private ICredentials getCredentialsFromSecret(
+        GalasaSecretType secretType,
+        GalasaSecretdata decodedData,
+        GalasaSecretmetadata metadata
+    ) {
         ICredentials credentials = null;
         switch (secretType) {
             case USERNAME:
@@ -158,6 +170,10 @@ private ICredentials getCredentialsFromSecret(GalasaSecretType secretType, Galas
             default:
                 break;
         }
+
+        if (credentials != null) {
+            credentials.setDescription(metadata.getdescription());
+        }
         return credentials;
     }
 
@@ -170,6 +186,15 @@ private void validateSecretMetadata(JsonObject secretJson, List<String> validati
             validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage());
         }
 
+        // If a description is provided, check that it is valid
+        if (metadata.has("description")) {
+            String description = metadata.get("description").getAsString();
+            if (description.isBlank() || !validator.isLatin1(description)) {
+                ServletError error = new ServletError(GAL5102_INVALID_SECRET_DESCRIPTION_PROVIDED);
+                validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage());
+            }
+        }
+
         // Check if the given secret type is a valid type
         if (metadata.has("type")) {
             GalasaSecretType secretType = GalasaSecretType.getFromString(metadata.get("type").getAsString());
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java
index 5679b1c34..58a0b401e 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java
@@ -19,8 +19,9 @@ public interface IGalasaResourceProcessor {
      * 
      * @param resourceJson the resource to perform an action on
      * @param action the action to perform
+     * @param username the username of the user performing the action
      * @return a list of validation errors encountered when processing the given JSON payload
      * @throws InternalServletException if there was an issue processing the resource
      */
-    List<String> processResource(JsonObject resourceJson, ResourceAction action) throws InternalServletException;
+    List<String> processResource(JsonObject resourceJson, ResourceAction action, String username) throws InternalServletException;
 }
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java
index ee5e6ebb5..89442da82 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java
@@ -23,7 +23,9 @@
 import com.google.gson.JsonObject;
 
 import dev.galasa.framework.api.common.BaseRoute;
+import dev.galasa.framework.api.common.Environment;
 import dev.galasa.framework.api.common.InternalServletException;
+import dev.galasa.framework.api.common.JwtWrapper;
 import dev.galasa.framework.api.common.QueryParameters;
 import dev.galasa.framework.api.common.ResponseBuilder;
 import dev.galasa.framework.api.common.ServletError;
@@ -37,6 +39,7 @@
 import dev.galasa.framework.spi.FrameworkException;
 import dev.galasa.framework.spi.creds.ICredentialsService;
 import dev.galasa.framework.spi.utils.GalasaGson;
+import dev.galasa.framework.spi.utils.ITimeService;
 
 public class ResourcesRoute  extends BaseRoute{
 
@@ -50,11 +53,20 @@ public class ResourcesRoute  extends BaseRoute{
     
     protected List<String> errors = new ArrayList<String>();
 
-    public ResourcesRoute(ResponseBuilder responseBuilder, CPSFacade cps, ICredentialsService credentialsService) {
+    private Environment env;
+
+    public ResourcesRoute(
+        ResponseBuilder responseBuilder,
+        CPSFacade cps,
+        ICredentialsService credentialsService,
+        ITimeService timeService,
+        Environment env
+    ) {
         super(responseBuilder, path);
+        this.env = env;
 
         resourceProcessors.put(GALASA_PROPERTY, new GalasaPropertyProcessor(cps));
-        resourceProcessors.put(GALASA_SECRET, new GalasaSecretProcessor(credentialsService));
+        resourceProcessors.put(GALASA_SECRET, new GalasaSecretProcessor(credentialsService, timeService));
     }
 
     @Override
@@ -63,7 +75,9 @@ public HttpServletResponse handlePostRequest(String pathInfo, QueryParameters qu
         logger.info("ResourcesRoute - handlePostRequest() entered");
 
         JsonObject jsonBody = parseRequestBody(request, JsonObject.class);
-        List<String> errorsList = processRequest(jsonBody);
+
+        String requestUsername = new JwtWrapper(request, env).getUsername();
+        List<String> errorsList = processRequest(jsonBody, requestUsername);
         if (errorsList.size() >0){
             response = getResponseBuilder().buildResponse(request, response, "application/json", getErrorsAsJson(errorsList), HttpServletResponse.SC_BAD_REQUEST);
         } else {
@@ -76,12 +90,12 @@ public HttpServletResponse handlePostRequest(String pathInfo, QueryParameters qu
 
     }
 
-    protected List<String> processRequest(JsonObject body) throws InternalServletException{
+    protected List<String> processRequest(JsonObject body, String username) throws InternalServletException{
         String actionStr = body.get("action").getAsString().toLowerCase().trim();
         ResourceAction action = ResourceAction.getFromString(actionStr);
         if (action != null){
             JsonArray jsonArray = body.get("data").getAsJsonArray();
-            processDataArray(jsonArray, action);
+            processDataArray(jsonArray, action, username);
         } else {
             ServletError error = new ServletError(GAL5025_UNSUPPORTED_ACTION);
             throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
@@ -105,7 +119,7 @@ protected String getErrorsAsJson(List<String> errorsList){
         return gson.toJson(json);
     }
 
-    protected void processDataArray(JsonArray jsonArray, ResourceAction action) throws InternalServletException{
+    protected void processDataArray(JsonArray jsonArray, ResourceAction action, String username) throws InternalServletException{
         for (JsonElement element: jsonArray) {
             try {
                 checkJsonElementIsValidJSON(element);
@@ -118,7 +132,7 @@ protected void processDataArray(JsonArray jsonArray, ResourceAction action) thro
                     throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
                 }
 
-                errors.addAll(resourceProcessors.get(kind).processResource(resource, action));
+                errors.addAll(resourceProcessors.get(kind).processResource(resource, action, username));
 
             } catch (InternalServletException s) {
                 errors.add(s.getMessage());
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java
index 3d306a87c..a267ce0bf 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java
@@ -9,6 +9,7 @@
 import static org.assertj.core.api.Assertions.*;
 
 import java.io.PrintWriter;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 
@@ -21,6 +22,7 @@
 import com.google.gson.JsonObject;
 
 import dev.galasa.framework.api.common.BaseServletTest;
+import dev.galasa.framework.api.common.EnvironmentVariables;
 import dev.galasa.framework.api.common.ResponseBuilder;
 import dev.galasa.framework.api.common.mocks.MockEnvironment;
 import dev.galasa.framework.api.common.mocks.MockFramework;
@@ -28,7 +30,7 @@
 import dev.galasa.framework.api.common.mocks.MockHttpServletResponse;
 import dev.galasa.framework.api.common.mocks.MockIConfigurationPropertyStoreService;
 import dev.galasa.framework.api.common.mocks.MockServletOutputStream;
-
+import dev.galasa.framework.api.common.mocks.MockTimeService;
 import dev.galasa.framework.api.resources.mocks.MockResourcesServlet;
 import dev.galasa.framework.spi.ConfigurationPropertyStoreException;
 import dev.galasa.framework.spi.IConfigurationPropertyStoreService;
@@ -43,6 +45,8 @@ public class ResourcesServletTest extends BaseServletTest {
 	HttpServletRequest req;
 	HttpServletResponse resp;
 
+    private Map<String, String> headers = Map.of("Authorization", "Bearer " + BaseServletTest.DUMMY_JWT);
+
 	private class MockICPSServiceWithError extends MockIConfigurationPropertyStoreService {
         protected MockICPSServiceWithError(String namespace){
             super.namespaceInput= namespace;
@@ -55,13 +59,6 @@ public void deleteProperty(@NotNull String name) throws ConfigurationPropertySto
     }
 
 	protected void setServlet(String namespace){
-		this.servlet = new MockResourcesServlet();
-        servlet.setResponseBuilder(new ResponseBuilder(new MockEnvironment()));
-
-        ServletOutputStream outStream = new MockServletOutputStream();
-		PrintWriter writer = new PrintWriter(outStream);
-        this.resp = new MockHttpServletResponse(writer, outStream);
-
 		IConfigurationPropertyStoreService cpsstore;
 		if (namespace != null){
 			cpsstore = new MockIConfigurationPropertyStoreService(namespace);
@@ -69,17 +66,28 @@ protected void setServlet(String namespace){
 			cpsstore = new MockICPSServiceWithError("framework");
 		}
 		IFramework framework = new MockFramework(cpsstore);
-		this.servlet.setFramework(framework);
+
+        MockEnvironment env = new MockEnvironment();
+        env.setenv(EnvironmentVariables.GALASA_USERNAME_CLAIMS, "preferred_username");
+
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+		this.servlet = new MockResourcesServlet(framework, env, timeService);
+
+        servlet.setResponseBuilder(new ResponseBuilder(env));
+
+        ServletOutputStream outStream = new MockServletOutputStream();
+		PrintWriter writer = new PrintWriter(outStream);
+        this.resp = new MockHttpServletResponse(writer, outStream);
 	}
 	
 	protected void setServlet(String path,String namespace, Map<String, String[]> parameterMap){
 		setServlet(namespace);
-		this.req = new MockHttpServletRequest(parameterMap,path);
+		this.req = new MockHttpServletRequest(parameterMap, path, headers);
 	}
 
 	protected void setServlet( String path,String namespace, JsonObject requestBody, String method){
 		setServlet(namespace);
-		this.req = new MockHttpServletRequest(path, gson.toJson(requestBody), method);
+		this.req = new MockHttpServletRequest(path, gson.toJson(requestBody), method, headers);
 	}
 
 	protected void setServlet( String path,String namespace, JsonObject requestBody, String method, Map<String,String> headerMap) {
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java
index 89f5c63f5..29e1a5ff0 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java
@@ -7,11 +7,19 @@
 
 import dev.galasa.framework.IFileSystem;
 import dev.galasa.framework.api.common.mocks.IServletUnderTest;
+import dev.galasa.framework.api.common.mocks.MockEnvironment;
+import dev.galasa.framework.api.common.mocks.MockTimeService;
 import dev.galasa.framework.api.resources.ResourcesServlet;
 import dev.galasa.framework.spi.IFramework;
 
 public class MockResourcesServlet extends ResourcesServlet implements IServletUnderTest{
 
+    public MockResourcesServlet(IFramework framework, MockEnvironment env, MockTimeService timeService) {
+        this.framework = framework;
+        this.env = env;
+        this.timeService = timeService;
+    }
+
 	@Override
 	public void setFramework(IFramework framework) {
 		super.setFramework(framework);
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java
index 363d33989..8105f0e9d 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java
@@ -24,6 +24,7 @@ public class GalasaPropertyProcessorTest extends ResourcesServletTest {
     @Test
     public void testProcessGalasaPropertyValidPropertyReturnsOK() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "myvalue";
@@ -34,7 +35,7 @@ public void testProcessGalasaPropertyValidPropertyReturnsOK() throws Exception {
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        propertyProcessor.processResource(propertyJson, APPLY);
+        propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         checkPropertyInNamespace(namespace,propertyname,value);
@@ -43,6 +44,7 @@ public void testProcessGalasaPropertyValidPropertyReturnsOK() throws Exception {
     @Test
     public void testProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "newnamespace";
         String propertyname = "property.name";
         String value = "myvalue";
@@ -53,7 +55,7 @@ public void testProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        propertyProcessor.processResource(propertyJson, APPLY);
+        propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         checkPropertyInNamespace(namespace,propertyname,value);
@@ -62,6 +64,7 @@ public void testProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws
     @Test
     public void testProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property1!";
         String value = "myvalue";
@@ -72,7 +75,7 @@ public void testProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Ex
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
 
         //Then...
@@ -85,6 +88,7 @@ public void testProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Ex
     @Test
     public void testProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name.";
         String value = "myvalue";
@@ -95,7 +99,7 @@ public void testProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() t
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -107,6 +111,7 @@ public void testProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() t
     @Test
     public void testProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = ".property.name";
         String value = "myvalue";
@@ -117,7 +122,7 @@ public void testProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() th
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -129,6 +134,7 @@ public void testProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() th
     @Test
     public void testProcessGalasaPropertyBadPropertyNameReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property";
         String value = "myvalue";
@@ -139,7 +145,7 @@ public void testProcessGalasaPropertyBadPropertyNameReturnsError() throws Except
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -151,6 +157,7 @@ public void testProcessGalasaPropertyBadPropertyNameReturnsError() throws Except
     @Test
     public void testProcessGalasaPropertyMissingPropertyNameReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "";
         String value = "myvalue";
@@ -161,7 +168,7 @@ public void testProcessGalasaPropertyMissingPropertyNameReturnsError() throws Ex
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -173,6 +180,7 @@ public void testProcessGalasaPropertyMissingPropertyNameReturnsError() throws Ex
     @Test
     public void testProcessGalasaPropertyMissingPropertyNamespaceReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "";
         String propertyname = "property.name";
         String value = "myvalue";
@@ -183,7 +191,7 @@ public void testProcessGalasaPropertyMissingPropertyNamespaceReturnsError() thro
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -195,6 +203,7 @@ public void testProcessGalasaPropertyMissingPropertyNamespaceReturnsError() thro
     @Test
     public void testProcessGalasaPropertyBadNamespaceReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "namespace@";
         String propertyname = "property.name";
         String value = "myvalue";
@@ -205,7 +214,7 @@ public void testProcessGalasaPropertyBadNamespaceReturnsError() throws Exception
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -217,6 +226,7 @@ public void testProcessGalasaPropertyBadNamespaceReturnsError() throws Exception
     @Test
     public void testProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "namespace.";
         String propertyname = "property.name";
         String value = "myvalue";
@@ -227,7 +237,7 @@ public void testProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() thro
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -239,6 +249,7 @@ public void testProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() thro
     @Test
     public void testProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = ".namespace";
         String propertyname = "property.name";
         String value = "myvalue";
@@ -249,7 +260,7 @@ public void testProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throw
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -261,6 +272,7 @@ public void testProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throw
     @Test
     public void testProcessGalasaPropertyMissingPropertyValueReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "";
@@ -271,7 +283,7 @@ public void testProcessGalasaPropertyMissingPropertyValueReturnsError() throws E
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -284,6 +296,7 @@ public void testProcessGalasaPropertyMissingPropertyValueReturnsError() throws E
     @Test
     public void testProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "";
         String propertyname = "";
         String value = "";
@@ -294,7 +307,7 @@ public void testProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception
         JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1");
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -308,6 +321,7 @@ public void testProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception
     @Test
     public void testProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "";
         String propertyname = "";
         String value = "";
@@ -319,7 +333,7 @@ public void testProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Excep
         JsonObject propertyJson = JsonParser.parseString(jsonString).getAsJsonObject();
 
         //When...
-        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY);
+        List<String> errors = propertyProcessor.processResource(propertyJson, APPLY, username);
 
         //Then...
         assertThat(errors).isNotNull();
@@ -334,6 +348,7 @@ public void testProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Excep
     @Test
     public void testProcessGalasaPropertyMissingApiVersionReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -345,7 +360,7 @@ public void testProcessGalasaPropertyMissingApiVersionReturnsError() throws Exce
 
         //When...
         Throwable thrown = catchThrowable(() -> {
-            propertyProcessor.processResource(propertyJson, APPLY);
+            propertyProcessor.processResource(propertyJson, APPLY, username);
         });
 
         //Then...
@@ -357,6 +372,7 @@ public void testProcessGalasaPropertyMissingApiVersionReturnsError() throws Exce
     @Test
     public void testProcessGalasaPropertyBadJsonReturnsError() throws Exception {
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -369,7 +385,7 @@ public void testProcessGalasaPropertyBadJsonReturnsError() throws Exception {
 
         //When...
         Throwable thrown = catchThrowable(() -> {
-            propertyProcessor.processResource(propertyJson, APPLY);
+            propertyProcessor.processResource(propertyJson, APPLY, username);
         });
 
         //Then...
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java
index eb8767ded..4bbe382d3 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java
@@ -8,6 +8,7 @@
 import static org.assertj.core.api.Assertions.*;
 import static dev.galasa.framework.api.common.resources.ResourceAction.*;
 
+import java.time.Instant;
 import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
@@ -21,6 +22,7 @@
 import dev.galasa.ICredentials;
 import dev.galasa.framework.api.common.InternalServletException;
 import dev.galasa.framework.api.common.mocks.MockCredentialsService;
+import dev.galasa.framework.api.common.mocks.MockTimeService;
 import dev.galasa.framework.api.resources.ResourcesServletTest;
 import dev.galasa.framework.spi.creds.CredentialsToken;
 import dev.galasa.framework.spi.creds.CredentialsUsername;
@@ -29,17 +31,29 @@
 
 public class GalasaSecretProcessorTest extends ResourcesServletTest {
 
-	private JsonObject generateSecretJson(String secretName, String type, String encoding, String username, String password) {
+    private JsonObject generateSecretJson(String secretName, String type, String encoding, String username, String password) {
         return generateSecretJson(secretName, type, encoding, username, password, null);
-	}
+    }
+
+    private JsonObject generateSecretJson(
+        String secretName,
+        String type,
+        String encoding,
+        String username,
+        String password,
+        String description
+    ) {
+        return generateSecretJson(secretName, type, encoding, username, password, null, description);
+    }
 
-	private JsonObject generateSecretJson(
+    private JsonObject generateSecretJson(
         String secretName,
         String type,
         String encoding,
         String username,
         String password,
-        String token
+        String token,
+        String description
     ) {
         JsonObject secretJson = new JsonObject();
         secretJson.addProperty("apiVersion", "galasa-dev/v1alpha1");
@@ -49,6 +63,10 @@ private JsonObject generateSecretJson(
         secretMetadata.addProperty("name", secretName);
         secretMetadata.addProperty("type", type);
 
+        if (description != null) {
+            secretMetadata.addProperty("description", description);
+        }
+
         if (encoding != null) {
             secretMetadata.addProperty("encoding", encoding);
         }
@@ -82,14 +100,16 @@ private JsonObject generateSecretJson(
         //         "username": "a-username"
         //     }
         // }
-		return secretJson;
-	}
+        return secretJson;
+    }
 
     @Test
     public void testApplySecretWithMissingNameReturnsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = null;
@@ -101,7 +121,7 @@ public void testApplySecretWithMissingNameReturnsError() throws Exception {
         secretJson.get("metadata").getAsJsonObject().remove("name");
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, APPLY);
+        List<String> errors = secretProcessor.processResource(secretJson, APPLY, requestUsername);
 
         // Then...
         assertThat(errors).hasSize(1);
@@ -113,8 +133,10 @@ public void testApplySecretWithMissingNameReturnsError() throws Exception {
     @Test
     public void testApplySecretWithMissingSecretTypeReturnsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = null;
@@ -126,7 +148,7 @@ public void testApplySecretWithMissingSecretTypeReturnsError() throws Exception
         secretJson.get("metadata").getAsJsonObject().remove("type");
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, APPLY);
+        List<String> errors = secretProcessor.processResource(secretJson, APPLY, requestUsername);
 
         // Then...
         assertThat(errors).hasSize(1);
@@ -138,8 +160,10 @@ public void testApplySecretWithMissingSecretTypeReturnsError() throws Exception
     @Test
     public void testApplySecretWithMissingDataThrowsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = null;
@@ -152,7 +176,7 @@ public void testApplySecretWithMissingDataThrowsError() throws Exception {
 
         // When...
         InternalServletException thrown = catchThrowableOfType(() -> {
-            secretProcessor.processResource(secretJson, APPLY);
+            secretProcessor.processResource(secretJson, APPLY, requestUsername);
         }, InternalServletException.class);
 
         // Then...
@@ -165,8 +189,10 @@ public void testApplySecretWithMissingDataThrowsError() throws Exception {
     @Test
     public void testApplySecretWithMissingMetadataThrowsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = null;
@@ -179,7 +205,7 @@ public void testApplySecretWithMissingMetadataThrowsError() throws Exception {
 
         // When...
         InternalServletException thrown = catchThrowableOfType(() -> {
-            secretProcessor.processResource(secretJson, APPLY);
+            secretProcessor.processResource(secretJson, APPLY, requestUsername);
         }, InternalServletException.class);
 
         // Then...
@@ -192,8 +218,10 @@ public void testApplySecretWithMissingMetadataThrowsError() throws Exception {
     @Test
     public void testApplySecretWithMissingApiVersionThrowsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = null;
@@ -206,7 +234,7 @@ public void testApplySecretWithMissingApiVersionThrowsError() throws Exception {
 
         // When...
         InternalServletException thrown = catchThrowableOfType(() -> {
-            secretProcessor.processResource(secretJson, APPLY);
+            secretProcessor.processResource(secretJson, APPLY, requestUsername);
         }, InternalServletException.class);
 
         // Then...
@@ -219,8 +247,10 @@ public void testApplySecretWithMissingApiVersionThrowsError() throws Exception {
     @Test
     public void testApplySecretWithMissingUsernamePasswordFieldsReturnsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = null;
@@ -229,7 +259,7 @@ public void testApplySecretWithMissingUsernamePasswordFieldsReturnsError() throw
         JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, APPLY);
+        List<String> errors = secretProcessor.processResource(secretJson, APPLY, requestUsername);
 
         // Then...
         assertThat(errors).hasSize(1);
@@ -241,8 +271,10 @@ public void testApplySecretWithMissingUsernamePasswordFieldsReturnsError() throw
     @Test
     public void testApplySecretWithUnsupportedEncodingReturnsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = "UNKNOWN!!!";
@@ -251,7 +283,7 @@ public void testApplySecretWithUnsupportedEncodingReturnsError() throws Exceptio
         JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, APPLY);
+        List<String> errors = secretProcessor.processResource(secretJson, APPLY, requestUsername);
 
         // Then...
         assertThat(errors).hasSize(1);
@@ -262,8 +294,10 @@ public void testApplySecretWithUnsupportedEncodingReturnsError() throws Exceptio
     @Test
     public void testApplySecretWithUnknownSecretTypeReturnsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UNKNOWN TYPE!";
         String encoding = null;
@@ -272,7 +306,7 @@ public void testApplySecretWithUnknownSecretTypeReturnsError() throws Exception
         JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, APPLY);
+        List<String> errors = secretProcessor.processResource(secretJson, APPLY, requestUsername);
 
         // Then...
         assertThat(errors).hasSize(1);
@@ -283,8 +317,10 @@ public void testApplySecretWithUnknownSecretTypeReturnsError() throws Exception
     @Test
     public void testApplySecretWithNoNameAndUnknownSecretTypeAndUnknownEncodingReturnsMultipleErrors() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UNKNOWN TYPE!";
         String encoding = "UNKNOWN ENCODING!";
@@ -296,7 +332,7 @@ public void testApplySecretWithNoNameAndUnknownSecretTypeAndUnknownEncodingRetur
         secretJson.get("metadata").getAsJsonObject().remove("name");
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, APPLY);
+        List<String> errors = secretProcessor.processResource(secretJson, APPLY, requestUsername);
 
         // Then...
         assertThat(errors).hasSize(3);
@@ -312,17 +348,21 @@ public void testApplySecretWithNoNameAndUnknownSecretTypeAndUnknownEncodingRetur
     @Test
     public void testCreateUsernamePasswordSecretSetsCredentialsOk() throws Exception {
         // Given...
+        Instant lastUpdatedTime = Instant.EPOCH;
+        MockTimeService mockTimeService = new MockTimeService(lastUpdatedTime);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
         String encoding = null;
         String username = "my-username";
         String password = "a-password";
-        JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password);
+        String description = "my new credentials";
+        JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password, description);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, CREATE);
+        List<String> errors = secretProcessor.processResource(secretJson, CREATE, requestUsername);
 
         // Then...
         assertThat(errors).isEmpty();
@@ -331,13 +371,18 @@ public void testCreateUsernamePasswordSecretSetsCredentialsOk() throws Exception
         assertThat(credentials).isNotNull();
         assertThat(credentials.getUsername()).isEqualTo(username);
         assertThat(credentials.getPassword()).isEqualTo(password);
+        assertThat(credentials.getDescription()).isEqualTo(description);
+        assertThat(credentials.getLastUpdatedTime()).isEqualTo(lastUpdatedTime);
+        assertThat(credentials.getLastUpdatedByUser()).isEqualTo(requestUsername);
     }
 
     @Test
     public void testCreateEncodedUsernamePasswordSecretSetsCredentialsOk() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernamePassword";
 
@@ -352,7 +397,7 @@ public void testCreateEncodedUsernamePasswordSecretSetsCredentialsOk() throws Ex
         JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, encodedPassword);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, CREATE);
+        List<String> errors = secretProcessor.processResource(secretJson, CREATE, requestUsername);
 
         // Then...
         assertThat(errors).isEmpty();
@@ -367,8 +412,10 @@ public void testCreateEncodedUsernamePasswordSecretSetsCredentialsOk() throws Ex
     @Test
     public void testCreateEncodedTokenSecretSetsCredentialsOk() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "Token";
 
@@ -378,10 +425,10 @@ public void testCreateEncodedTokenSecretSetsCredentialsOk() throws Exception {
         String token = "my-token";
         String encodedToken = encoder.encodeToString(token.getBytes());
 
-        JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, encodedToken);
+        JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, encodedToken, null);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, CREATE);
+        List<String> errors = secretProcessor.processResource(secretJson, CREATE, requestUsername);
 
         // Then...
         assertThat(errors).isEmpty();
@@ -395,8 +442,10 @@ public void testCreateEncodedTokenSecretSetsCredentialsOk() throws Exception {
     @Test
     public void testCreateEncodedUsernameTokenSecretSetsCredentialsOk() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "UsernameToken";
 
@@ -408,10 +457,10 @@ public void testCreateEncodedUsernameTokenSecretSetsCredentialsOk() throws Excep
         String encodedUsername = encoder.encodeToString(username.getBytes());
         String encodedToken = encoder.encodeToString(token.getBytes());
 
-        JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, encodedToken);
+        JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, encodedToken, null);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, CREATE);
+        List<String> errors = secretProcessor.processResource(secretJson, CREATE, requestUsername);
 
         // Then...
         assertThat(errors).isEmpty();
@@ -426,8 +475,10 @@ public void testCreateEncodedUsernameTokenSecretSetsCredentialsOk() throws Excep
     @Test
     public void testCreateEncodedUsernameSecretSetsCredentialsOk() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "Username";
 
@@ -440,7 +491,7 @@ public void testCreateEncodedUsernameSecretSetsCredentialsOk() throws Exception
         JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, null);
 
         // When...
-        List<String> errors = secretProcessor.processResource(secretJson, CREATE);
+        List<String> errors = secretProcessor.processResource(secretJson, CREATE, requestUsername);
 
         // Then...
         assertThat(errors).isEmpty();
@@ -454,6 +505,7 @@ public void testCreateEncodedUsernameSecretSetsCredentialsOk() throws Exception
     @Test
     public void testDeleteSecretDeletesCredentialsOk() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         String secretName = "ABC";
         String username = "my-username";
         Map<String, ICredentials> existingCreds = new HashMap<>();
@@ -461,7 +513,8 @@ public void testDeleteSecretDeletesCredentialsOk() throws Exception {
         existingCreds.put("another-secret", new CredentialsUsername("another-username"));
 
         MockCredentialsService mockCreds = new MockCredentialsService(existingCreds);
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String type = "Username";
         String encoding = null;
 
@@ -470,7 +523,7 @@ public void testDeleteSecretDeletesCredentialsOk() throws Exception {
         // When...
         assertThat(mockCreds.getAllCredentials()).hasSize(2);
         assertThat(mockCreds.getCredentials(secretName)).isNotNull();
-        List<String> errors = secretProcessor.processResource(secretJson, DELETE);
+        List<String> errors = secretProcessor.processResource(secretJson, DELETE, requestUsername);
 
         // Then...
         assertThat(errors).isEmpty();
@@ -481,6 +534,7 @@ public void testDeleteSecretDeletesCredentialsOk() throws Exception {
     @Test
     public void testDeleteSecretDoesNotInsistOnData() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         String secretName = "ABC";
         String username = "my-username";
         Map<String, ICredentials> existingCreds = new HashMap<>();
@@ -488,7 +542,8 @@ public void testDeleteSecretDoesNotInsistOnData() throws Exception {
         existingCreds.put("another-secret", new CredentialsUsername("another-username"));
 
         MockCredentialsService mockCreds = new MockCredentialsService(existingCreds);
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String type = "Username";
         String encoding = null;
 
@@ -500,7 +555,7 @@ public void testDeleteSecretDoesNotInsistOnData() throws Exception {
         // When...
         assertThat(mockCreds.getAllCredentials()).hasSize(2);
         assertThat(mockCreds.getCredentials(secretName)).isNotNull();
-        List<String> errors = secretProcessor.processResource(secretJson, DELETE);
+        List<String> errors = secretProcessor.processResource(secretJson, DELETE, requestUsername);
 
         // Then...
         assertThat(errors).isEmpty();
@@ -511,11 +566,13 @@ public void testDeleteSecretDoesNotInsistOnData() throws Exception {
     @Test
     public void testCreateSecretThatAlreadyExistsThrowsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         Map<String, ICredentials> credsMap = new HashMap<>();
         credsMap.put("ABC", new CredentialsUsername("my-username"));
 
         MockCredentialsService mockCreds = new MockCredentialsService(credsMap);
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "Username";
         String encoding = null;
@@ -525,7 +582,7 @@ public void testCreateSecretThatAlreadyExistsThrowsError() throws Exception {
 
         // When...
         InternalServletException thrown = catchThrowableOfType(() -> {
-            secretProcessor.processResource(secretJson, CREATE);
+            secretProcessor.processResource(secretJson, CREATE, requestUsername);
         }, InternalServletException.class);
 
         // Then...
@@ -537,18 +594,20 @@ public void testCreateSecretThatAlreadyExistsThrowsError() throws Exception {
     @Test
     public void testUpdateSecretThatDoesNotExistThrowsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "Token";
         String encoding = null;
         String token = "another-token";
 
-        JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token);
+        JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token, null);
 
         // When...
         InternalServletException thrown = catchThrowableOfType(() -> {
-            secretProcessor.processResource(secretJson, UPDATE);
+            secretProcessor.processResource(secretJson, UPDATE, requestUsername);
         }, InternalServletException.class);
 
         // Then...
@@ -560,20 +619,22 @@ public void testUpdateSecretThatDoesNotExistThrowsError() throws Exception {
     @Test
     public void testApplySecretWithFailingCredsServiceThrowsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
         mockCreds.setThrowError(true);
 
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "Token";
         String encoding = null;
         String token = "a-token";
 
-        JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token);
+        JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token, null);
 
         // When...
         InternalServletException thrown = catchThrowableOfType(() -> {
-            secretProcessor.processResource(secretJson, APPLY);
+            secretProcessor.processResource(secretJson, APPLY, requestUsername);
         }, InternalServletException.class);
 
         // Then...
@@ -585,10 +646,12 @@ public void testApplySecretWithFailingCredsServiceThrowsError() throws Exception
     @Test
     public void testDeleteSecretWithFailingCredsServiceThrowsError() throws Exception {
         // Given...
+        MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH);
         MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
         mockCreds.setThrowError(true);
 
-        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds);
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
         String secretName = "ABC";
         String type = "Token";
         String encoding = null;
@@ -598,7 +661,7 @@ public void testDeleteSecretWithFailingCredsServiceThrowsError() throws Exceptio
 
         // When...
         InternalServletException thrown = catchThrowableOfType(() -> {
-            secretProcessor.processResource(secretJson, DELETE);
+            secretProcessor.processResource(secretJson, DELETE, requestUsername);
         }, InternalServletException.class);
 
         // Then...
@@ -606,4 +669,57 @@ public void testDeleteSecretWithFailingCredsServiceThrowsError() throws Exceptio
         checkErrorStructure(thrown.getMessage(), 5078, "GAL5078E",
             "Failed to delete a secret with the given ID from the credentials store");
     }
+
+    @Test
+    public void testCreateUsernamePasswordSecretWithBlankDescriptionThrowsError() throws Exception {
+        // Given...
+        Instant lastUpdatedTime = Instant.EPOCH;
+        MockTimeService mockTimeService = new MockTimeService(lastUpdatedTime);
+        MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
+        String secretName = "ABC";
+        String type = "UsernamePassword";
+        String encoding = null;
+        String username = "my-username";
+        String password = "a-password";
+        String description = "    ";
+        JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password, description);
+
+        // When...
+        List<String> errors = secretProcessor.processResource(secretJson, CREATE, requestUsername);
+
+        // Then...
+        assertThat(errors).hasSize(1);
+        checkErrorStructure(errors.get(0), 5102, "GAL5102E",
+            "Invalid secret description provided");
+    }
+
+    @Test
+    public void testCreateUsernamePasswordSecretWithNonLatin1DescriptionThrowsError() throws Exception {
+        // Given...
+        Instant lastUpdatedTime = Instant.EPOCH;
+        MockTimeService mockTimeService = new MockTimeService(lastUpdatedTime);
+        MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>());
+        GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService);
+        String requestUsername = "myuser";
+        String secretName = "ABC";
+        String type = "UsernamePassword";
+        String encoding = null;
+        String username = "my-username";
+        String password = "a-password";
+
+        // Latin-1 characters are in the 0-255 range, so set one that is outside this range
+        char nonLatin1Character = (char) 300;
+        String description = "this is my bad description " + nonLatin1Character;
+        JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password, description);
+
+        // When...
+        List<String> errors = secretProcessor.processResource(secretJson, CREATE, requestUsername);
+
+        // Then...
+        assertThat(errors).hasSize(1);
+        checkErrorStructure(errors.get(0), 5102, "GAL5102E",
+            "Invalid secret description provided");
+    }
 }
diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java
index 81994a37e..446f293fc 100644
--- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java
+++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java
@@ -120,18 +120,19 @@ public void TestPathRegexMultipleForwardSlashPathReturnsFalse(){
     @Test
     public void TestProcessDataArrayBadJsonArrayReturnsError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString = "[{},{},{}]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, APPLY);
+        resourcesRoute.processDataArray(propertyJson, APPLY, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -143,18 +144,19 @@ public void TestProcessDataArrayBadJsonArrayReturnsError() throws Exception{
     @Test
     public void TestProcessDataArrayBadJsonReturnsError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString = "[{\"kind\":\"GalasaProperty\",\"apiVersion\":\"galasa-dev/v1alpha1\","+namespace+"."+propertyname+":"+value+"}]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, APPLY);
+        resourcesRoute.processDataArray(propertyJson, APPLY, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -166,18 +168,19 @@ public void TestProcessDataArrayBadJsonReturnsError() throws Exception{
     @Test
     public void TestProcessDataArrayBadKindReturnsError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString = "[{\"kind\":\"GalasaProperly\",\"apiVersion\":\"v1alpha1\","+namespace+"."+propertyname+":"+value+"}]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, APPLY);
+        resourcesRoute.processDataArray(propertyJson, APPLY, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -189,16 +192,17 @@ public void TestProcessDataArrayBadKindReturnsError() throws Exception{
     @Test
     public void TestProcessDataArrayNullJsonObjectReturnsError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString = "[null]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, APPLY);
+        resourcesRoute.processDataArray(propertyJson, APPLY, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -209,17 +213,18 @@ public void TestProcessDataArrayNullJsonObjectReturnsError() throws Exception{
     @Test
     public void TestProcessDataArrayCorrectJSONReturnsOK() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         JsonArray propertyJson = generatePropertyArrayJson(namespace,propertyname,value,"galasa-dev/v1alpha1");
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, APPLY);
+        resourcesRoute.processDataArray(propertyJson, APPLY, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -230,19 +235,20 @@ public void TestProcessDataArrayCorrectJSONReturnsOK() throws Exception{
     @Test
     public void TestProcessDataArrayThreeBadJsonReturnsErrors() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString = "[null, {\"kind\":\"GalasaProperty\",\"apiVersion\":\"galasa-dev/v1alpha1\","+namespace+"."+propertyname+":"+value+"},"+
             "{\"kind\":\"GalasaProperly\",\"apiVersion\":\"v1alpha1\","+namespace+"."+propertyname+":"+value+"},{}]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, APPLY);
+        resourcesRoute.processDataArray(propertyJson, APPLY, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -257,6 +263,7 @@ public void TestProcessDataArrayThreeBadJsonReturnsErrors() throws Exception{
     @Test
     public void TestProcessDataArrayCreateWithOneExistingRecordJSONReturnsOneError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -265,13 +272,13 @@ public void TestProcessDataArrayCreateWithOneExistingRecordJSONReturnsOneError()
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1");
         jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, CREATE);
+        resourcesRoute.processDataArray(propertyJson, CREATE, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -285,6 +292,7 @@ public void TestProcessDataArrayCreateWithOneExistingRecordJSONReturnsOneError()
     @Test
     public void TestProcessDataArrayCreateWithTwoExistingRecordsJSONReturnsTwoErrors() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.1";
         String value = "value";
@@ -293,13 +301,13 @@ public void TestProcessDataArrayCreateWithTwoExistingRecordsJSONReturnsTwoErrors
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1");
         jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, CREATE);
+        resourcesRoute.processDataArray(propertyJson, CREATE, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -315,6 +323,7 @@ public void TestProcessDataArrayCreateWithTwoExistingRecordsJSONReturnsTwoErrors
     @Test
     public void TestProcessDataArrayUpdateWithOneNewRecordJSONReturnsOneError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -323,13 +332,13 @@ public void TestProcessDataArrayUpdateWithOneNewRecordJSONReturnsOneError() thro
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1");
         jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, UPDATE);
+        resourcesRoute.processDataArray(propertyJson, UPDATE, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -342,6 +351,7 @@ public void TestProcessDataArrayUpdateWithOneNewRecordJSONReturnsOneError() thro
     @Test
     public void TestProcessDataArrayUpdateWithTwoNewRecordsJSONReturnsTwoError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -350,13 +360,13 @@ public void TestProcessDataArrayUpdateWithTwoNewRecordsJSONReturnsTwoError() thr
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1");
         jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]";
         JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray();
 
         //When...
-        resourcesRoute.processDataArray(propertyJson, UPDATE);
+        resourcesRoute.processDataArray(propertyJson, UPDATE, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -374,6 +384,7 @@ public void TestProcessDataArrayUpdateWithTwoNewRecordsJSONReturnsTwoError() thr
      @Test
     public void TestProcessRequestApplyActionReturnsOK() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -381,11 +392,11 @@ public void TestProcessRequestApplyActionReturnsOK() throws Exception{
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         JsonObject requestJson = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1");
 
         //When...
-        resourcesRoute.processRequest(requestJson);
+        resourcesRoute.processRequest(requestJson, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -396,6 +407,7 @@ public void TestProcessRequestApplyActionReturnsOK() throws Exception{
     @Test
     public void TestProcessRequestCreateActionReturnsOK() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -403,11 +415,11 @@ public void TestProcessRequestCreateActionReturnsOK() throws Exception{
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1");
 
         //When...
-        resourcesRoute.processRequest(jsonString);
+        resourcesRoute.processRequest(jsonString, username);
         List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -418,6 +430,7 @@ public void TestProcessRequestCreateActionReturnsOK() throws Exception{
     @Test
     public void TestProcessRequestUpdateActionReturnsOK() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.1";
         String value = "value";
@@ -425,11 +438,11 @@ public void TestProcessRequestUpdateActionReturnsOK() throws Exception{
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1");
 
         //When...
-        resourcesRoute.processRequest(jsonString);
+        resourcesRoute.processRequest(jsonString, username);
          List<String> errors = resourcesRoute.errors;
 
         //Then...
@@ -440,6 +453,7 @@ public void TestProcessRequestUpdateActionReturnsOK() throws Exception{
     @Test
     public void TestProcessRequestBadActionReturnsError() throws Exception{
         //Given...
+        String username = "myuser";
         String namespace = "framework";
         String propertyname = "property.name";
         String value = "value";
@@ -447,12 +461,12 @@ public void TestProcessRequestBadActionReturnsError() throws Exception{
         setServlet(namespace);
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
         JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1");
 
         //When...
         Throwable thrown = catchThrowable(() -> {
-          resourcesRoute.processRequest(jsonString);
+          resourcesRoute.processRequest(jsonString, username);
         });
 
         //Then...
@@ -973,7 +987,7 @@ public void TestGetErrorsAsJsonReturnsJsonString() throws Exception{
         setServlet("framework");
         MockResourcesServlet servlet = getServlet();
         CPSFacade cps = new CPSFacade(servlet.getFramework());
-        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null);
+        ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null);
 
         // When...
         String json = resourcesRoute.getErrorsAsJson(errors);
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java
index 7a894e08e..151883711 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java
@@ -12,11 +12,15 @@
 import org.apache.commons.logging.LogFactory;
 
 import dev.galasa.framework.api.common.BaseServlet;
+import dev.galasa.framework.api.common.Environment;
+import dev.galasa.framework.api.common.SystemEnvironment;
 import dev.galasa.framework.api.secrets.internal.routes.SecretDetailsRoute;
 import dev.galasa.framework.api.secrets.internal.routes.SecretsRoute;
 import dev.galasa.framework.spi.IFramework;
 import dev.galasa.framework.spi.creds.CredentialsException;
 import dev.galasa.framework.spi.creds.ICredentialsService;
+import dev.galasa.framework.spi.utils.ITimeService;
+import dev.galasa.framework.spi.utils.SystemTimeService;
 
 import javax.servlet.Servlet;
 import javax.servlet.ServletException;
@@ -31,6 +35,9 @@ public class SecretsServlet extends BaseServlet {
 	@Reference
 	protected IFramework framework;
 
+    protected Environment env = new SystemEnvironment();
+    protected ITimeService timeService = new SystemTimeService();
+
 	private static final long serialVersionUID = 1L;
 
 	private Log logger = LogFactory.getLog(this.getClass());
@@ -41,8 +48,8 @@ public void init() throws ServletException {
 
         try {
             ICredentialsService credentialsService = framework.getCredentialsService();
-            addRoute(new SecretsRoute(getResponseBuilder(), credentialsService));
-            addRoute(new SecretDetailsRoute(getResponseBuilder(), credentialsService));
+            addRoute(new SecretsRoute(getResponseBuilder(), credentialsService, env, timeService));
+            addRoute(new SecretDetailsRoute(getResponseBuilder(), credentialsService, env, timeService));
         } catch (CredentialsException e) {
             throw new ServletException("Failed to initialise the Secrets servlet");
         }
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java
index 9af466efc..2ee427c33 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java
@@ -17,8 +17,9 @@
 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.BaseResourceValidator;
 
-public class SecretRequestValidator implements IBeanValidator<SecretRequest> {
+public class SecretRequestValidator extends BaseResourceValidator implements IBeanValidator<SecretRequest> {
 
     @Override
     public void validate(SecretRequest secretRequest) throws InternalServletException {
@@ -28,11 +29,13 @@ public void validate(SecretRequest secretRequest) throws InternalServletExceptio
 
         // Check that the secret has been given a name
         String secretName = secretRequest.getname();
-        if (secretName == null || secretName.isBlank()) {
+        if (secretName == null || secretName.isBlank() || !isLatin1(secretName)) {
             ServletError error = new ServletError(GAL5092_INVALID_SECRET_NAME_PROVIDED);
             throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST);
         }
 
+        validateDescription(secretRequest.getdescription());
+
         // Password and token are mutually exclusive, so error if both are provided
         if (password != null && token != null) {
             ServletError error = new ServletError(GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED);
@@ -77,4 +80,12 @@ private void validateField(String value, String encoding) throws InternalServlet
             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);
+        }
+    }
+
 }
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java
index 6da2cfc11..ba600f2b6 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java
@@ -49,6 +49,8 @@ private void validateCreateSecretRequest(SecretRequest secretRequest) throws Int
         SecretRequestpassword password = secretRequest.getpassword();
         SecretRequesttoken token = secretRequest.gettoken();
 
+        validateDescription(secretRequest.getdescription());
+
         // Password and token are mutually exclusive, so error if both are provided
         if (password != null && token != null) {
             ServletError error = new ServletError(GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED);
@@ -69,6 +71,8 @@ private void validateUpdateSecretRequest(SecretRequest secretRequest) throws Int
         SecretRequestpassword password = secretRequest.getpassword();
         SecretRequesttoken token = secretRequest.gettoken();
 
+        validateDescription(secretRequest.getdescription());
+
         // Password and token are mutually exclusive, so error if both are provided
         if (password != null && token != null) {
             ServletError error = new ServletError(GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED);
@@ -112,7 +116,7 @@ private void checkProvidedSecretFieldsAreRelevant(GalasaSecretType secretType, S
         JsonObject secretRequestJson = gson.toJsonTree(secretRequest).getAsJsonObject();
         Set<String> secretRequestFields = secretRequestJson.keySet()
             .stream()
-            .filter(key -> !key.equals("name") && !key.equals("type"))
+            .filter(key -> !key.equals("name") && !key.equals("type") && !key.equals("description"))
             .collect(Collectors.toSet());
 
         List<String> requiredTypeFields = Arrays.asList(secretType.getRequiredDataFields());
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java
index 416396b10..55b29dc64 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java
@@ -8,9 +8,11 @@
 import static dev.galasa.framework.api.common.ServletErrorMessage.*;
 import static dev.galasa.framework.api.beans.generated.GalasaSecretType.*;
 
+import java.time.Instant;
 import java.util.Base64;
 import java.util.Map;
 
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import dev.galasa.ICredentials;
@@ -26,7 +28,9 @@
 import dev.galasa.framework.api.beans.generated.SecretRequesttoken;
 import dev.galasa.framework.api.beans.generated.SecretRequestusername;
 import dev.galasa.framework.api.common.BaseRoute;
+import dev.galasa.framework.api.common.Environment;
 import dev.galasa.framework.api.common.InternalServletException;
+import dev.galasa.framework.api.common.JwtWrapper;
 import dev.galasa.framework.api.common.ResponseBuilder;
 import dev.galasa.framework.api.common.ServletError;
 import dev.galasa.framework.api.common.resources.GalasaSecretType;
@@ -34,11 +38,15 @@
 import dev.galasa.framework.spi.creds.CredentialsUsername;
 import dev.galasa.framework.spi.creds.CredentialsUsernamePassword;
 import dev.galasa.framework.spi.creds.CredentialsUsernameToken;
+import dev.galasa.framework.spi.utils.ITimeService;
 
 public abstract class AbstractSecretsRoute extends BaseRoute {
 
     private static final String DEFAULT_RESPONSE_ENCODING = "base64";
 
+    private Environment env;
+    protected ITimeService timeService;
+
     private static final Map<Class<? extends ICredentials>, GalasaSecretType> credentialsToSecretTypes = Map.of(
         CredentialsUsername.class, GalasaSecretType.USERNAME,
         CredentialsToken.class, GalasaSecretType.TOKEN,
@@ -46,8 +54,10 @@ public abstract class AbstractSecretsRoute extends BaseRoute {
         CredentialsUsernameToken.class, GalasaSecretType.USERNAME_TOKEN
     );
 
-    public AbstractSecretsRoute(ResponseBuilder responseBuilder, String path) {
+    public AbstractSecretsRoute(ResponseBuilder responseBuilder, String path, Environment env, ITimeService timeService) {
         super(responseBuilder, path);
+        this.env = env;
+        this.timeService = timeService;
     }
 
     protected GalasaSecret createGalasaSecretFromCredentials(String secretName, ICredentials credentials) throws InternalServletException {
@@ -57,7 +67,7 @@ protected GalasaSecret createGalasaSecretFromCredentials(String secretName, ICre
         metadata.setname(secretName);
         metadata.setencoding(DEFAULT_RESPONSE_ENCODING);
         setSecretTypeValuesFromCredentials(metadata, data, credentials);
-
+        setSecretMetadata(metadata, credentials.getDescription(), credentials.getLastUpdatedByUser(), credentials.getLastUpdatedTime());
         GalasaSecret secret = new GalasaSecret();
         secret.setApiVersion(GalasaSecretType.DEFAULT_API_VERSION);
         secret.setdata(data);
@@ -66,7 +76,13 @@ protected GalasaSecret createGalasaSecretFromCredentials(String secretName, ICre
         return secret;
     }
 
-    protected ICredentials decodeCredentialsFromSecretPayload(SecretRequest secretRequest) throws InternalServletException {
+    protected ICredentials buildDecodedCredentialsToSet(SecretRequest secretRequest, String lastUpdatedByUser) throws InternalServletException {
+        ICredentials decodedSecret = decodeCredentialsFromSecretPayload(secretRequest);
+        setSecretMetadataProperties(decodedSecret, secretRequest.getdescription(), lastUpdatedByUser);
+        return decodedSecret;
+    }
+
+    private ICredentials decodeCredentialsFromSecretPayload(SecretRequest secretRequest) throws InternalServletException {
         ICredentials credentials = null;
         SecretRequestusername username = secretRequest.getusername();
         SecretRequestpassword password = secretRequest.getpassword();
@@ -142,6 +158,15 @@ private void setSecretTypeValuesFromCredentials(GalasaSecretmetadata metadata, G
         }
     }
 
+    private void setSecretMetadata(GalasaSecretmetadata metadata, String description, String username, Instant timestamp) {
+        metadata.setdescription(description);
+        metadata.setLastUpdatedBy(username);
+
+        if (timestamp != null) {
+            metadata.setLastUpdatedTime(timestamp.toString());
+        }
+    }
+
     private String encodeValue(String value) {
         String encodedValue = value;
         if (DEFAULT_RESPONSE_ENCODING.equals("base64")) {
@@ -157,4 +182,16 @@ protected GalasaSecretType getSecretType(ICredentials existingSecret) {
         }
         return existingSecretType;
     }
+
+    protected String getUsernameFromRequestJwt(HttpServletRequest request) throws InternalServletException {
+        return new JwtWrapper(request, env).getUsername();
+    }
+
+    protected void setSecretMetadataProperties(ICredentials secret, String description, String lastUpdatedByUser) {
+        if (description != null && !description.isBlank()) {
+            secret.setDescription(description);
+        }
+        secret.setLastUpdatedByUser(lastUpdatedByUser);
+        secret.setLastUpdatedTime(timeService.now());
+    }
 }
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java
index e08e3018a..3673d762e 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java
@@ -23,6 +23,7 @@
 import dev.galasa.framework.api.beans.generated.SecretRequestpassword;
 import dev.galasa.framework.api.beans.generated.SecretRequesttoken;
 import dev.galasa.framework.api.beans.generated.SecretRequestusername;
+import dev.galasa.framework.api.common.Environment;
 import dev.galasa.framework.api.common.InternalServletException;
 import dev.galasa.framework.api.common.QueryParameters;
 import dev.galasa.framework.api.common.ResponseBuilder;
@@ -37,6 +38,7 @@
 import dev.galasa.framework.spi.creds.CredentialsUsernamePassword;
 import dev.galasa.framework.spi.creds.CredentialsUsernameToken;
 import dev.galasa.framework.spi.creds.ICredentialsService;
+import dev.galasa.framework.spi.utils.ITimeService;
 
 public class SecretDetailsRoute extends AbstractSecretsRoute {
 
@@ -52,8 +54,13 @@ public class SecretDetailsRoute extends AbstractSecretsRoute {
 
     private Log logger = LogFactory.getLog(getClass());
 
-    public SecretDetailsRoute(ResponseBuilder responseBuilder, ICredentialsService credentialsService) {
-        super(responseBuilder, PATH_PATTERN);
+    public SecretDetailsRoute(
+        ResponseBuilder responseBuilder,
+        ICredentialsService credentialsService,
+        Environment env,
+        ITimeService timeService
+    ) {
+        super(responseBuilder, PATH_PATTERN, env, timeService);
         this.credentialsService = credentialsService;
     }
 
@@ -85,6 +92,7 @@ public HttpServletResponse handlePutRequest(
         checkRequestHasContent(request);
 
         String secretName = getSecretNameFromPath(pathInfo);
+        String lastUpdatedByUser = getUsernameFromRequestJwt(request);
         SecretRequest secretPayload = parseRequestBody(request, SecretRequest.class);
 
         ICredentials existingSecret = credentialsService.getCredentials(secretName);
@@ -95,16 +103,17 @@ public HttpServletResponse handlePutRequest(
         int responseCode = HttpServletResponse.SC_NO_CONTENT;
         if (existingSecret == null) {
             // No secret with the given name exists, so create a new one
-            decodedSecret = decodeCredentialsFromSecretPayload(secretPayload);
+            decodedSecret = buildDecodedCredentialsToSet(secretPayload, lastUpdatedByUser);
             responseCode = HttpServletResponse.SC_CREATED;
         } else if (secretPayload.gettype() != null) {
             // When a secret type is given, all relevant fields for that type are required,
             // so overwrite the existing secret to change its type
-            decodedSecret = decodeCredentialsFromSecretPayload(secretPayload);
+            decodedSecret = buildDecodedCredentialsToSet(secretPayload, lastUpdatedByUser);
         } else {
             // A secret already exists and no type was given, so just update the secret by
             // overriding its existing values with the values provided in the request
             decodedSecret = getOverriddenSecret(existingSecretType, existingSecret, secretPayload);
+            setSecretMetadataProperties(decodedSecret, secretPayload.getdescription(), lastUpdatedByUser);
         }
         credentialsService.setCredentials(secretName, decodedSecret);
 
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java
index 445168a51..4aa45227d 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java
@@ -22,6 +22,7 @@
 import dev.galasa.ICredentials;
 import dev.galasa.framework.api.beans.generated.GalasaSecret;
 import dev.galasa.framework.api.beans.generated.SecretRequest;
+import dev.galasa.framework.api.common.Environment;
 import dev.galasa.framework.api.common.InternalServletException;
 import dev.galasa.framework.api.common.QueryParameters;
 import dev.galasa.framework.api.common.ResponseBuilder;
@@ -29,6 +30,7 @@
 import dev.galasa.framework.api.secrets.internal.SecretRequestValidator;
 import dev.galasa.framework.spi.FrameworkException;
 import dev.galasa.framework.spi.creds.ICredentialsService;
+import dev.galasa.framework.spi.utils.ITimeService;
 
 public class SecretsRoute extends AbstractSecretsRoute {
 
@@ -40,8 +42,12 @@ public class SecretsRoute extends AbstractSecretsRoute {
 
     private Log logger = LogFactory.getLog(getClass());
 
-    public SecretsRoute(ResponseBuilder responseBuilder, ICredentialsService credentialsService) {
-        super(responseBuilder, PATH_PATTERN);
+    public SecretsRoute(
+        ResponseBuilder responseBuilder,
+        ICredentialsService credentialsService,
+        Environment env,
+        ITimeService timeService) {
+        super(responseBuilder, PATH_PATTERN, env, timeService);
         this.credentialsService = credentialsService;
     }
 
@@ -93,7 +99,8 @@ public HttpServletResponse handlePostRequest(
         }
 
         logger.info("Setting secret in credentials store");
-        ICredentials decodedSecret = decodeCredentialsFromSecretPayload(secretPayload);
+        String lastUpdatedByUser = getUsernameFromRequestJwt(request);
+        ICredentials decodedSecret = buildDecodedCredentialsToSet(secretPayload, lastUpdatedByUser);
         credentialsService.setCredentials(secretName, decodedSecret);
 
         logger.info("Secret set in credentials store OK");
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java
index 8034b1bf9..0f7925f5b 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java
@@ -5,6 +5,7 @@
  */
 package dev.galasa.framework.api.secrets.internal;
 
+import java.time.Instant;
 import java.util.Properties;
 
 import dev.galasa.ICredentials;
@@ -15,4 +16,39 @@ class MockCredentials implements ICredentials {
     public Properties toProperties(String credentialsId) {
         throw new UnsupportedOperationException("Unimplemented method 'toProperties'");
     }
+
+    @Override
+    public void setDescription(String description) {
+        throw new UnsupportedOperationException("Unimplemented method 'setDescription'");
+    }
+
+    @Override
+    public void setLastUpdatedByUser(String username) {
+        throw new UnsupportedOperationException("Unimplemented method 'setLastUpdatedByUser'");
+    }
+
+    @Override
+    public void setLastUpdatedTime(Instant time) {
+        throw new UnsupportedOperationException("Unimplemented method 'setLastUpdatedTime'");
+    }
+
+    @Override
+    public String getDescription() {
+        throw new UnsupportedOperationException("Unimplemented method 'getDescription'");
+    }
+
+    @Override
+    public String getLastUpdatedByUser() {
+        throw new UnsupportedOperationException("Unimplemented method 'getLastUpdatedByUser'");
+    }
+
+    @Override
+    public Instant getLastUpdatedTime() {
+        throw new UnsupportedOperationException("Unimplemented method 'getLastUpdatedTime'");
+    }
+
+    @Override
+    public Properties getMetadataProperties(String credentialsId) {
+        throw new UnsupportedOperationException("Unimplemented method 'getMetadataProperties'");
+    }
 }
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java
index eef30d7bf..df075a054 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java
@@ -8,6 +8,7 @@
 import static org.assertj.core.api.Assertions.*;
 import static dev.galasa.framework.api.common.resources.GalasaSecretType.*;
 
+import java.time.Instant;
 import java.util.Base64;
 import java.util.HashMap;
 import java.util.Map;
@@ -25,6 +26,7 @@
 import dev.galasa.framework.api.common.mocks.MockFramework;
 import dev.galasa.framework.api.common.mocks.MockHttpServletRequest;
 import dev.galasa.framework.api.common.mocks.MockHttpServletResponse;
+import dev.galasa.framework.api.common.mocks.MockTimeService;
 import dev.galasa.framework.api.secrets.internal.routes.SecretDetailsRoute;
 import dev.galasa.framework.api.secrets.mocks.MockSecretsServlet;
 import dev.galasa.framework.spi.creds.CredentialsToken;
@@ -37,7 +39,7 @@ public class SecretDetailsRouteTest extends SecretsServletTest {
     @Test
     public void testSecretDetailsRouteRegexMatchesExpectedPaths() throws Exception {
         // Given...
-        Pattern routePattern = new SecretDetailsRoute(null, null).getPath();
+        Pattern routePattern = new SecretDetailsRoute(null, null, null, null).getPath();
 
         // Then...
         // The servlet's whiteboard pattern will match /secrets, so this route should
@@ -69,9 +71,10 @@ public void testGetSecretByNameReturnsSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName);
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -98,9 +101,10 @@ public void testGetNonExistantSecretByNameReturnsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName);
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -128,9 +132,10 @@ public void testGetSecretByNameWithFailingCredsStoreReturnsError() throws Except
 
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName);
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS);
         mockRequest.setQueryParameter("name", secretName);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
@@ -157,9 +162,10 @@ public void testDeleteSecretDeletesSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName);
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS);
         mockRequest.setMethod(HttpMethod.DELETE.toString());
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
@@ -184,9 +190,10 @@ public void testDeleteNonExistantSecretReturnsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName);
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS);
         mockRequest.setMethod(HttpMethod.DELETE.toString());
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
@@ -215,9 +222,10 @@ public void testDeleteSecretWithFailingCredsStoreReturnsError() throws Exception
 
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName);
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS);
         mockRequest.setMethod(HttpMethod.DELETE.toString());
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
@@ -253,10 +261,10 @@ public void testUpdateSecretUsernameUpdatesSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -297,10 +305,10 @@ public void testUpdateSecretUsernamePasswordUpdatesSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -338,10 +346,10 @@ public void testUpdateTokenSecretUpdatesValueOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -379,10 +387,10 @@ public void testUpdateUsernameTokenSecretUpdatesValueOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -420,10 +428,10 @@ public void testUpdateUsernameSecretUpdatesValueOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -461,10 +469,10 @@ public void testUpdateSecretToTokenChangesSecretTypeOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -502,10 +510,10 @@ public void testUpdateSecretWithUnknownTypeReturnsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -541,10 +549,10 @@ public void testUpdateSecretWithTypeAndMissingFieldsReturnsError() throws Except
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -580,10 +588,10 @@ public void testUpdateSecretWithPasswordAndTokenPayloadReturnsError() throws Exc
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -621,10 +629,10 @@ public void testUpdateSecretWithMixedEncodingUpdatesSecretOk() throws Exception
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -658,10 +666,10 @@ public void testUpdateNonExistantSecretCreatesSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -697,10 +705,10 @@ public void testUpdateNonExistantSecretWithPasswordAndTokenPayloadReturnsError()
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -730,10 +738,10 @@ public void testUpdateNonExistantSecretWithPasswordOnlyPayloadReturnsError() thr
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -763,10 +771,10 @@ public void testUpdateSecretWithUnknownTokenEncodingReturnsError() throws Except
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -795,10 +803,10 @@ public void testUpdateSecretWithUnknownUsernameEncodingReturnsError() throws Exc
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -830,10 +838,10 @@ public void testUpdateSecretWithUnexpectedFieldsReturnsError() throws Exception
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -867,10 +875,10 @@ public void testUpdateSecretWithTooManyFieldsReturnsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -903,10 +911,10 @@ public void testUpdateSecretWithUnsupportedTypeReturnsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/" + secretName);
-        mockRequest.setMethod(HttpMethod.PUT.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -920,5 +928,134 @@ public void testUpdateSecretWithUnsupportedTypeReturnsError() throws Exception {
         checkErrorStructure(outStream.toString(), 5101, "GAL5101E",
             "Unknown secret type detected");
     }
+
+    @Test
+    public void testUpdateSecretWithBlankDescriptionReturnsError() throws Exception {
+        // Given...
+        Map<String, ICredentials> creds = new HashMap<>();
+        String secretName = "BOB";
+        String oldToken = "my-old-token";
+        String newToken = "my-new-token";
+        String newDescription = "   ";
+
+        creds.put(secretName, new CredentialsToken(oldToken));
+
+        JsonObject secretJson = new JsonObject();
+        secretJson.add("token", createSecretJson(newToken));
+        secretJson.addProperty("description", newDescription);
+
+        String secretJsonStr = gson.toJson(secretJson);
+
+        MockCredentialsService credsService = new MockCredentialsService(creds);
+        MockFramework mockFramework = new MockFramework(credsService);
+
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
+
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
+
+        MockHttpServletResponse servletResponse = new MockHttpServletResponse();
+        ServletOutputStream outStream = servletResponse.getOutputStream();
+
+        // When...
+        servlet.init();
+        servlet.doPut(mockRequest, servletResponse);
+
+        // Then...
+        assertThat(servletResponse.getStatus()).isEqualTo(400);
+        checkErrorStructure(outStream.toString(), 5102, "GAL5102E",
+            "Invalid secret description provided");
+    }
+
+    @Test
+    public void testUpdateSecretWithNonLatin1DescriptionReturnsError() throws Exception {
+        // Given...
+        Map<String, ICredentials> creds = new HashMap<>();
+        String secretName = "BOB";
+        String oldToken = "my-old-token";
+        String newToken = "my-new-token";
+
+        // Latin-1 characters are in the range 0-255, so get one that is outside this range
+        char nonLatin1Character = (char)300;
+        String description = Character.toString(nonLatin1Character) + " more text here!";
+
+        creds.put(secretName, new CredentialsToken(oldToken));
+
+        JsonObject secretJson = new JsonObject();
+        secretJson.add("token", createSecretJson(newToken));
+        secretJson.addProperty("description", description);
+
+        String secretJsonStr = gson.toJson(secretJson);
+
+        MockCredentialsService credsService = new MockCredentialsService(creds);
+        MockFramework mockFramework = new MockFramework(credsService);
+
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
+
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
+
+        MockHttpServletResponse servletResponse = new MockHttpServletResponse();
+        ServletOutputStream outStream = servletResponse.getOutputStream();
+
+        // When...
+        servlet.init();
+        servlet.doPut(mockRequest, servletResponse);
+
+        // Then...
+        assertThat(servletResponse.getStatus()).isEqualTo(400);
+        checkErrorStructure(outStream.toString(), 5102, "GAL5102E",
+            "Invalid secret description provided");
+    }
+
+    @Test
+    public void testUpdateSecretWithValidLatin1DescriptionUpdatesSecret() throws Exception {
+        // Given...
+        Map<String, ICredentials> creds = new HashMap<>();
+        String secretName = "BOB";
+        String oldToken = "my-old-token";
+        String newToken = "my-new-token";
+
+        Instant lastUpdatedTime = Instant.EPOCH;
+
+        // Latin-1 characters are in the range 0-255
+        char latin1Character = (char)255;
+        String description = Character.toString(latin1Character) + " more text here!";
+
+        creds.put(secretName, new CredentialsToken(oldToken));
+
+        JsonObject secretJson = new JsonObject();
+        secretJson.add("token", createSecretJson(newToken));
+        secretJson.addProperty("description", description);
+
+        String secretJsonStr = gson.toJson(secretJson);
+
+        MockCredentialsService credsService = new MockCredentialsService(creds);
+        MockFramework mockFramework = new MockFramework(credsService);
+
+        MockTimeService timeService = new MockTimeService(lastUpdatedTime);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
+
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS);
+
+        MockHttpServletResponse servletResponse = new MockHttpServletResponse();
+        ServletOutputStream outStream = servletResponse.getOutputStream();
+
+        // When...
+        servlet.init();
+        servlet.doPut(mockRequest, servletResponse);
+
+        // Then...
+        assertThat(servletResponse.getStatus()).isEqualTo(204);
+        assertThat(outStream.toString()).isEmpty();
+
+        assertThat(credsService.getAllCredentials()).hasSize(1);
+        CredentialsToken updatedCredentials = (CredentialsToken) credsService.getCredentials(secretName);
+        assertThat(updatedCredentials).isNotNull();
+        assertThat(updatedCredentials.getToken()).isEqualTo(newToken.getBytes());
+        assertThat(updatedCredentials.getDescription()).isEqualTo(description);
+        assertThat(updatedCredentials.getLastUpdatedTime()).isEqualTo(lastUpdatedTime);
+        assertThat(updatedCredentials.getLastUpdatedByUser()).isEqualTo(JWT_USERNAME);
+    }
 }
 
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java
index 0d07f1ee8..428e74495 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java
@@ -7,6 +7,7 @@
 
 import static org.assertj.core.api.Assertions.*;
 
+import java.time.Instant;
 import java.util.Base64;
 import java.util.HashMap;
 import java.util.Map;
@@ -25,18 +26,20 @@
 import dev.galasa.framework.api.common.mocks.MockFramework;
 import dev.galasa.framework.api.common.mocks.MockHttpServletRequest;
 import dev.galasa.framework.api.common.mocks.MockHttpServletResponse;
+import dev.galasa.framework.api.common.mocks.MockTimeService;
 import dev.galasa.framework.api.secrets.internal.routes.SecretsRoute;
 import dev.galasa.framework.api.secrets.mocks.MockSecretsServlet;
 import dev.galasa.framework.spi.creds.CredentialsToken;
 import dev.galasa.framework.spi.creds.CredentialsUsername;
 import dev.galasa.framework.spi.creds.CredentialsUsernamePassword;
+import dev.galasa.framework.spi.creds.CredentialsUsernameToken;
 
 public class SecretsRouteTest extends SecretsServletTest {
 
     @Test
     public void testSecretsRouteRegexMatchesExpectedPaths() throws Exception {
         // Given...
-        Pattern routePattern = new SecretsRoute(null, null).getPath();
+        Pattern routePattern = new SecretsRoute(null, null, null, null).getPath();
 
         // Then...
         // The servlet's whiteboard pattern will match /secrets, so the secrets route
@@ -56,23 +59,52 @@ public void testGetSecretsReturnsAllSecretsOk() throws Exception {
         String secretName1 = "BOB";
         String username1 = "my-username";
         String password1 = "not-a-password";
+        String description1 = "this is my first secret";
+        String lastUser1 = "user1";
+        Instant lastUpdated1 = Instant.EPOCH;
 
         String secretName2 = "ITS_BOB_AGAIN";
         String username2 = "another-username";
+        String description2 = "this is my second secret";
+        String lastUser2 = "user2";
+        Instant lastUpdated2 = Instant.EPOCH.plusMillis(1);
 
         String secretName3 = "not-b0b";
         String token3 = "this-is-a-token";
 
-        creds.put(secretName1, new CredentialsUsernamePassword(username1, password1));
-        creds.put(secretName2, new CredentialsUsername(username2));
+        String secretName4 = "new-bob";
+        String username4 = "this-is-yet-another-username";
+        String token4 = "this-is-another-token";
+        String lastUser4 = "user4";
+        Instant lastUpdated4 = Instant.EPOCH.plusMillis(4);
+
+        ICredentials secret1 = new CredentialsUsernamePassword(username1, password1);
+        secret1.setDescription(description1);
+        secret1.setLastUpdatedByUser(lastUser1);
+        secret1.setLastUpdatedTime(lastUpdated1);
+
+        ICredentials secret2 = new CredentialsUsername(username2);
+        secret2.setDescription(description2);
+        secret2.setLastUpdatedByUser(lastUser2);
+        secret2.setLastUpdatedTime(lastUpdated2);
+
+        ICredentials secret4 = new CredentialsUsernameToken(username4, token4);
+        secret4.setLastUpdatedByUser(username4);
+        secret4.setLastUpdatedByUser(lastUser4);
+        secret4.setLastUpdatedTime(lastUpdated4);
+
+        creds.put(secretName1, secret1);
+        creds.put(secretName2, secret2);
         creds.put(secretName3, new CredentialsToken(token3));
+        creds.put(secretName4, secret4);
 
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/");
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -83,8 +115,9 @@ public void testGetSecretsReturnsAllSecretsOk() throws Exception {
 
         // Then...
         JsonArray expectedJson = new JsonArray();
-        expectedJson.add(generateSecretJson(secretName2, "Username", username2, null, null));
-        expectedJson.add(generateSecretJson(secretName1, "UsernamePassword", username1, password1, null));
+        expectedJson.add(generateSecretJson(secretName4, "UsernameToken", username4, null, token4, null, lastUser4, lastUpdated4));
+        expectedJson.add(generateSecretJson(secretName2, "Username", username2, null, null, description2, lastUser2, lastUpdated2));
+        expectedJson.add(generateSecretJson(secretName1, "UsernamePassword", username1, password1, null, description1, lastUser1, lastUpdated1));
         expectedJson.add(generateSecretJson(secretName3, "Token", null, null, token3));
 
         String output = outStream.toString();
@@ -102,9 +135,10 @@ public void testGetSecretsWithUnknownSecretTypeReturnsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/");
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -136,10 +170,10 @@ public void testCreateUsernamePasswordSecretCreatesSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -175,10 +209,10 @@ public void testCreateTokenSecretCreatesSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -215,10 +249,10 @@ public void testCreateBase64EncodedSecretCreatesSecretOk() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -254,10 +288,10 @@ public void testCreateBase64EncodedSecretWithBadlyEncodedDataThrowsError() throw
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -290,10 +324,10 @@ public void testCreateSecretWithPasswordAndTokenThrowsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -324,10 +358,10 @@ public void testCreateSecretWithMissingSecretNameThrowsError() throws Exception
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -360,10 +394,10 @@ public void testCreateSecretWithBlankSecretNameThrowsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -396,10 +430,10 @@ public void testCreateSecretWithMissingUsernameValueThrowsError() throws Excepti
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -432,10 +466,10 @@ public void testCreateSecretWithMissingPasswordValueThrowsError() throws Excepti
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -468,10 +502,10 @@ public void testCreateSecretWithMissingTokenValueThrowsError() throws Exception
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -504,10 +538,10 @@ public void testCreateSecretWithBlankUsernameValueThrowsError() throws Exception
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -540,10 +574,10 @@ public void testCreateSecretWithBlankPasswordValueThrowsError() throws Exception
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -574,10 +608,10 @@ public void testCreateSecretWithBlankTokenValueThrowsError() throws Exception {
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -609,10 +643,10 @@ public void testCreateSecretWithUnknownEncodingValueThrowsError() throws Excepti
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -643,10 +677,10 @@ public void testCreateSecretWithPasswordAndMissingUsernameThrowsError() throws E
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -677,10 +711,10 @@ public void testCreateSecretWithExistingSecretNameThrowsError() throws Exception
         MockCredentialsService credsService = new MockCredentialsService(creds);
         MockFramework mockFramework = new MockFramework(credsService);
 
-        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework);
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
 
-        MockHttpServletRequest mockRequest = new MockHttpServletRequest(secretJsonStr, "/");
-        mockRequest.setMethod(HttpMethod.POST.toString());
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
 
         MockHttpServletResponse servletResponse = new MockHttpServletResponse();
         ServletOutputStream outStream = servletResponse.getOutputStream();
@@ -695,4 +729,80 @@ public void testCreateSecretWithExistingSecretNameThrowsError() throws Exception
         checkErrorStructure(output, 5075, "GAL5075E",
             "A secret with the provided name already exists");
     }
+
+    @Test
+    public void testCreateSecretWithNonLatin1SecretNameThrowsError() throws Exception {
+        // Given...
+        Map<String, ICredentials> creds = new HashMap<>();
+
+        // Latin-1 characters are in the range 0-255, so get one that is outside this range
+        char nonLatin1Character = (char)300;
+        String secretName = "MY-EXISTING-SECRET" + nonLatin1Character;
+
+
+        JsonObject secretJson = new JsonObject();
+        secretJson.addProperty("name", secretName);
+        secretJson.add("username", createSecretJson("my-new-username"));
+        String secretJsonStr = gson.toJson(secretJson);
+
+        MockCredentialsService credsService = new MockCredentialsService(creds);
+        MockFramework mockFramework = new MockFramework(credsService);
+
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
+
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
+
+        MockHttpServletResponse servletResponse = new MockHttpServletResponse();
+        ServletOutputStream outStream = servletResponse.getOutputStream();
+
+        // When...
+        servlet.init();
+        servlet.doPost(mockRequest, servletResponse);
+
+        // Then...
+        String output = outStream.toString();
+        assertThat(servletResponse.getStatus()).isEqualTo(400);
+        checkErrorStructure(output, 5092, "GAL5092E",
+            "Invalid secret name provided");
+    }
+
+    @Test
+    public void testCreateSecretWithNonLatin1DescriptionThrowsError() throws Exception {
+        // Given...
+        Map<String, ICredentials> creds = new HashMap<>();
+
+        // Latin-1 characters are in the range 0-255, so get one that is outside this range
+        char nonLatin1Character = (char)300;
+        String description = Character.toString(nonLatin1Character) + " more text here!";
+        String secretName = "MY-EXISTING-SECRET";
+
+
+        JsonObject secretJson = new JsonObject();
+        secretJson.addProperty("name", secretName);
+        secretJson.addProperty("description", description);
+        secretJson.add("username", createSecretJson("my-new-username"));
+        String secretJsonStr = gson.toJson(secretJson);
+
+        MockCredentialsService credsService = new MockCredentialsService(creds);
+        MockFramework mockFramework = new MockFramework(credsService);
+
+        MockTimeService timeService = new MockTimeService(Instant.EPOCH);
+        MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService);
+
+        MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS);
+
+        MockHttpServletResponse servletResponse = new MockHttpServletResponse();
+        ServletOutputStream outStream = servletResponse.getOutputStream();
+
+        // When...
+        servlet.init();
+        servlet.doPost(mockRequest, servletResponse);
+
+        // Then...
+        String output = outStream.toString();
+        assertThat(servletResponse.getStatus()).isEqualTo(400);
+        checkErrorStructure(output, 5102, "GAL5102E",
+            "Invalid secret description provided");
+    }
 }
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java
index 1e22ea485..ec45ba01e 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java
@@ -5,8 +5,10 @@
  */
 package dev.galasa.framework.api.secrets.internal;
 
+import java.time.Instant;
 import java.util.Base64;
 import java.util.Base64.Encoder;
+import java.util.Map;
 
 import com.google.gson.JsonObject;
 
@@ -15,6 +17,8 @@
 
 public class SecretsServletTest extends BaseServletTest {
 
+    protected static final Map<String, String> REQUEST_HEADERS = Map.of("Authorization", "Bearer " + BaseServletTest.DUMMY_JWT);
+
     protected JsonObject createSecretJson(String value, String encoding) {
         JsonObject secretJson = new JsonObject();
         if (value != null) {
@@ -38,11 +42,24 @@ protected JsonObject generateSecretJson(
         String username,
         String password,
         String token
+    ) {
+        return generateSecretJson(secretName, type, username, password, token, null, null, null);
+    }
+
+    protected JsonObject generateSecretJson(
+        String secretName,
+        String type,
+        String username,
+        String password,
+        String token,
+        String description,
+        String lastUpdatedUser,
+        Instant lastUpdatedTime
     ) {
         JsonObject secretJson = new JsonObject();
         secretJson.addProperty("apiVersion", GalasaSecretType.DEFAULT_API_VERSION);
 
-        secretJson.add("metadata", generateExpectedMetadata(secretName, type));
+        secretJson.add("metadata", generateExpectedMetadata(secretName, type, description, lastUpdatedUser, lastUpdatedTime));
         secretJson.add("data", generateExpectedData(username, password, token));
 
         secretJson.addProperty("kind", "GalasaSecret");
@@ -50,11 +67,31 @@ protected JsonObject generateSecretJson(
         return secretJson;
     }
 
-    private JsonObject generateExpectedMetadata(String secretName, String type) {
+    private JsonObject generateExpectedMetadata(
+        String secretName,
+        String type,
+        String description,
+        String lastUpdatedUser,
+        Instant lastUpdatedTime
+    ) {
         JsonObject metadata = new JsonObject();
         metadata.addProperty("name", secretName);
+        if (lastUpdatedTime != null) {
+            metadata.addProperty("lastUpdatedTime", lastUpdatedTime.toString());
+        }
+
+        if (lastUpdatedUser != null) {
+            metadata.addProperty("lastUpdatedBy", lastUpdatedUser);
+        }
+
         metadata.addProperty("encoding", "base64");
+
+        if (description != null) {
+            metadata.addProperty("description", description);
+        }
+
         metadata.addProperty("type", type);
+
         return metadata;
     }
 
diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java
index 6ceb170df..ed3108cb9 100644
--- a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java
+++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java
@@ -5,20 +5,26 @@
  */
 package dev.galasa.framework.api.secrets.mocks;
 
-import dev.galasa.framework.api.common.Environment;
+import dev.galasa.framework.api.common.EnvironmentVariables;
 import dev.galasa.framework.api.common.ResponseBuilder;
 import dev.galasa.framework.api.common.mocks.MockEnvironment;
 import dev.galasa.framework.api.common.mocks.MockFramework;
+import dev.galasa.framework.api.common.mocks.MockTimeService;
 import dev.galasa.framework.api.secrets.SecretsServlet;
+import dev.galasa.framework.spi.utils.ITimeService;
 
 public class MockSecretsServlet extends SecretsServlet {
 
-    public MockSecretsServlet(MockFramework framework) {
-        this(framework, new MockEnvironment());
+    public MockSecretsServlet(MockFramework framework, MockTimeService mockTimeService) {
+        this(framework, new MockEnvironment(), mockTimeService);
     }
 
-    public MockSecretsServlet(MockFramework framework, Environment env) {
+    public MockSecretsServlet(MockFramework framework, MockEnvironment env, ITimeService timeService) {
+        env.setenv(EnvironmentVariables.GALASA_USERNAME_CLAIMS, "preferred_username");
+
         this.framework = framework;
+        this.env = env;
+        this.timeService = timeService;
         setResponseBuilder(new ResponseBuilder(env));
     }
 }
diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.java
new file mode 100644
index 000000000..636269bee
--- /dev/null
+++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright contributors to the Galasa project
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package dev.galasa.framework.spi.creds;
+
+import java.time.Instant;
+import java.util.Properties;
+
+import javax.crypto.spec.SecretKeySpec;
+
+import dev.galasa.ICredentials;
+
+/**
+ * An abstract class where common credential-related details are stored.
+ */
+public abstract class AbstractCredentials extends Credentials implements ICredentials {
+
+    protected static final String CREDS_PROPERTY_PREFIX = "secure.credentials.";
+
+    private String description;
+    private String lastUpdatedByUser;
+    private Instant lastUpdatedTime;
+
+    public AbstractCredentials(SecretKeySpec key) throws CredentialsException {
+        super(key);
+    }
+
+    public AbstractCredentials() {
+        super();
+    }
+
+    @Override
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @Override
+    public void setLastUpdatedByUser(String username) {
+        this.lastUpdatedByUser = username;
+    }
+
+    @Override
+    public void setLastUpdatedTime(Instant time) {
+        this.lastUpdatedTime = time;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public String getLastUpdatedByUser() {
+        return lastUpdatedByUser;
+    }
+
+    @Override
+    public Instant getLastUpdatedTime() {
+        return lastUpdatedTime;
+    }
+
+    @Override
+    public Properties getMetadataProperties(String credentialsId) {
+        Properties properties = new Properties();
+        if (description != null) {
+            properties.put(CREDS_PROPERTY_PREFIX + credentialsId + ".description", description);
+        }
+
+        if (lastUpdatedTime != null) {
+            properties.put(CREDS_PROPERTY_PREFIX + credentialsId + ".lastUpdated.time", lastUpdatedTime.toString());
+        }
+
+        if (lastUpdatedByUser != null) {
+            properties.put(CREDS_PROPERTY_PREFIX + credentialsId + ".lastUpdated.user", lastUpdatedByUser);
+        }
+        return properties;
+    }
+}
diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java
index ccc96ecf1..f57df24a8 100644
--- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java
+++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java
@@ -64,5 +64,4 @@ protected byte[] decrypt(String encrypted) throws CredentialsException {
             throw new CredentialsException("Unable to decrypt credentials", e);
         }
     }
-
 }
diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java
index a5a41850f..40f76c47f 100644
--- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java
+++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java
@@ -11,7 +11,7 @@
 
 import dev.galasa.ICredentialsToken;
 
-public class CredentialsToken extends Credentials implements ICredentialsToken {
+public class CredentialsToken extends AbstractCredentials implements ICredentialsToken {
     private final byte[] token;
 
     public CredentialsToken(String plainTextToken) {
@@ -36,8 +36,7 @@ public byte[] getToken() {
     @Override
     public Properties toProperties(String credentialsId) {
         Properties credsProperties = new Properties();
-        credsProperties.setProperty("secure.credentials." + credentialsId + ".token" , new String(this.token));
+        credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".token" , new String(this.token));
         return credsProperties;
     }
-
 }
diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java
index 9e232ffc5..c9e1ad438 100644
--- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java
+++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java
@@ -12,7 +12,7 @@
 
 import dev.galasa.ICredentialsUsername;
 
-public class CredentialsUsername extends Credentials implements ICredentialsUsername {
+public class CredentialsUsername extends AbstractCredentials implements ICredentialsUsername {
     private String username;
 
     public CredentialsUsername(String plainTextUsername) {
@@ -36,7 +36,7 @@ public String getUsername() {
     @Override
     public Properties toProperties(String credentialsId) {
         Properties credsProperties = new Properties();
-        credsProperties.setProperty("secure.credentials." + credentialsId + ".username" , this.username);
+        credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".username" , this.username);
         return credsProperties;
     }
 
diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java
index 811509ed3..efa4eb03a 100644
--- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java
+++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java
@@ -12,7 +12,7 @@
 
 import dev.galasa.ICredentialsUsernamePassword;
 
-public class CredentialsUsernamePassword extends Credentials implements ICredentialsUsernamePassword {
+public class CredentialsUsernamePassword extends AbstractCredentials implements ICredentialsUsernamePassword {
     private String username;
     private String password;
 
@@ -48,8 +48,8 @@ public String getPassword() {
     @Override
     public Properties toProperties(String credentialsId) {
         Properties credsProperties = new Properties();
-        credsProperties.setProperty("secure.credentials." + credentialsId + ".username" , this.username);
-        credsProperties.setProperty("secure.credentials." + credentialsId + ".password" , this.password);
+        credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".username" , this.username);
+        credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".password" , this.password);
         return credsProperties;
     }
 }
diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java
index 37fb3c3cd..e11678409 100644
--- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java
+++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java
@@ -12,7 +12,7 @@
 
 import dev.galasa.ICredentialsUsernameToken;
 
-public class CredentialsUsernameToken extends Credentials implements ICredentialsUsernameToken {
+public class CredentialsUsernameToken extends AbstractCredentials implements ICredentialsUsernameToken {
     private String username;
     private byte[] token;
 
@@ -48,8 +48,8 @@ public byte[] getToken() {
     @Override
     public Properties toProperties(String credentialsId) {
         Properties credsProperties = new Properties();
-        credsProperties.setProperty("secure.credentials." + credentialsId + ".username" , this.username);
-        credsProperties.setProperty("secure.credentials." + credentialsId + ".token" , new String(this.token));
+        credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".username" , this.username);
+        credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".token" , new String(this.token));
         return credsProperties;
     }
 }
diff --git a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java
deleted file mode 100644
index 1abc94cbb..000000000
--- a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright contributors to the Galasa project
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package dev.galasa.framework.mocks;
-
-import java.util.Properties;
-
-import dev.galasa.ICredentials;
-
-class MockCredentials implements ICredentials {
-
-    @Override
-    public Properties toProperties(String credentialsId) {
-        throw new UnsupportedOperationException("Unimplemented method 'toProperties'");
-    }
-};
\ No newline at end of file
diff --git a/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java b/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java
index 4369db949..35db1ec2b 100644
--- a/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java
+++ b/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java
@@ -5,8 +5,18 @@
  */
 package dev.galasa;
 
+import java.time.Instant;
 import java.util.Properties;
 
 public interface ICredentials {
     Properties toProperties(String credentialsId);
+    Properties getMetadataProperties(String credentialsId);
+
+    void setDescription(String description);
+    void setLastUpdatedByUser(String username);
+    void setLastUpdatedTime(Instant time);
+
+    String getDescription();
+    String getLastUpdatedByUser();
+    Instant getLastUpdatedTime();
 }