diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyDTO.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyDTO.java
new file mode 100644
index 00000000..826c6d19
--- /dev/null
+++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyDTO.java
@@ -0,0 +1,35 @@
+package org.aktin.broker.auth.apikey;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * Data Transfer Object (DTO) for API key information. This class is used to transfer API key and client DN information between client and server in a
+ * XML format.
+ *
+ * @author akombeiz@ukaachen.de
+ */
+@XmlRootElement(name = "ApiKeyCred")
+public class ApiKeyDTO {
+
+ private String apiKey;
+ private String clientDn;
+
+ public ApiKeyDTO() {
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getClientDn() {
+ return clientDn;
+ }
+
+ public void setClientDn(String clientDn) {
+ this.clientDn = clientDn;
+ }
+}
diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyManagementEndpoint.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyManagementEndpoint.java
new file mode 100644
index 00000000..9168f495
--- /dev/null
+++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyManagementEndpoint.java
@@ -0,0 +1,149 @@
+package org.aktin.broker.auth.apikey;
+
+import java.io.IOException;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.logging.Logger;
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import org.aktin.broker.rest.Authenticated;
+import org.aktin.broker.rest.RequireAdmin;
+
+/**
+ * RESTful endpoint for managing API keys. This class provides operations to retrieve, create, activate, and deactivate API keys. Access to these
+ * operations requires authentication and admin privileges.
+ *
+ * @author akombeiz@ukaachen.de
+ */
+@Authenticated
+@RequireAdmin
+@Path("api-keys")
+public class ApiKeyManagementEndpoint {
+
+ private static final Logger log = Logger.getLogger(ApiKeyManagementEndpoint.class.getName());
+
+ @Inject
+ ApiKeyPropertiesAuthProvider authProvider;
+
+ /**
+ * Retrieves all API keys.
+ *
+ * @return A Response with {@code 200} containing all API keys as a string, or {@code 500} if retrieval fails.
+ */
+ @GET
+ public Response getApiKeys() {
+ try {
+ PropertyFileAPIKeys apiKeys = authProvider.getInstance();
+ Properties props = apiKeys.getProperties();
+ return Response.ok(convertPropertiesToString(props)).build();
+ } catch (IOException e) {
+ log.severe("Error retrieving API keys: " + e.getMessage());
+ return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+ /**
+ * Converts Properties to a string representation.
+ *
+ * @param props The Properties object to convert.
+ * @return A string representation of the properties.
+ */
+ private String convertPropertiesToString(Properties props) {
+ StringBuilder response = new StringBuilder();
+ for (String name : props.stringPropertyNames()) {
+ response.append(name).append("=").append(props.getProperty(name)).append("\n");
+ }
+ return response.toString();
+ }
+
+ /**
+ * Creates a new API key.
+ *
+ * @param apiKeyDTO The DTO containing the API key and client DN.
+ * @return A Response indicating the result of the operation:
+ *
{@code 201} - API key created successfully
+ *
{@code 400} - API key or client DN are not provided
+ *
{@code 409} - API key already exists
+ *
{@code 500} - When there is an error in saving the properties file or API keys instance is not initialized
+ *
+ */
+ @POST
+ @Consumes(MediaType.APPLICATION_XML)
+ public Response createApiKey(ApiKeyDTO apiKeyDTO) {
+ String apiKey = apiKeyDTO.getApiKey();
+ String clientDn = apiKeyDTO.getClientDn();
+ if (apiKey == null || clientDn == null) {
+ return Response.status(Status.BAD_REQUEST).build();
+ }
+ try {
+ authProvider.addNewApiKeyAndUpdatePropertiesFile(apiKey, clientDn);
+ return Response.status(Status.CREATED).build();
+ } catch (IllegalArgumentException e) {
+ return Response.status(Status.CONFLICT).build();
+ } catch (IOException e) {
+ return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+ /**
+ * Activates an API key.
+ *
+ * @param apiKey The API key to activate.
+ * @return A Response indicating the result of the operation.
+ */
+ @POST
+ @Path("{apiKey}/activate")
+ public Response activateApiKey(@PathParam("apiKey") String apiKey) {
+ return setApiKeyStatus(apiKey, ApiKeyStatus.ACTIVE);
+ }
+
+ /**
+ * Deactivates an API key.
+ *
+ * @param apiKey The API key to deactivate.
+ * @return A Response indicating the result of the operation.
+ */
+ @POST
+ @Path("{apiKey}/deactivate")
+ public Response deactivateApiKey(@PathParam("apiKey") String apiKey) {
+ return setApiKeyStatus(apiKey, ApiKeyStatus.INACTIVE);
+ }
+
+ /**
+ * Sets the status of an API key.
+ *
+ * @param apiKey The API key to update.
+ * @param status The new status for the API key.
+ * @return A Response indicating the result of the operation:
+ *
{@code 200} - Status was changed or API key was already in that state
+ *
{@code 404} - API key does not exist
+ *
{@code 403} - When attempting to modify an admin API key
+ *
{@code 400} - When ApiKeyStatus is unknown, or apiKey or status are not provided
+ *
{@code 500} - When there is an error in saving the properties file or API keys instance is not initialized
+ *
+ */
+ private Response setApiKeyStatus(String apiKey, ApiKeyStatus status) {
+ if (apiKey == null || status == null) {
+ return Response.status(Status.BAD_REQUEST).build();
+ }
+ try {
+ authProvider.setStateOfApiKeyAndUpdatePropertiesFile(apiKey, status);
+ return Response.ok().build();
+ } catch (NoSuchElementException e) {
+ return Response.status(Status.NOT_FOUND).build();
+ } catch (SecurityException e) {
+ return Response.status(Status.FORBIDDEN).build();
+ } catch (IllegalArgumentException e) {
+ return Response.status(Status.BAD_REQUEST).build();
+ } catch (IOException e) {
+ return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+}
diff --git a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java
index fe87ae30..7cfd58e9 100644
--- a/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java
+++ b/broker-auth-local/src/main/java/org/aktin/broker/auth/apikey/ApiKeyPropertiesAuthProvider.java
@@ -2,36 +2,186 @@
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-
+import java.nio.file.Path;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+import java.util.function.BiConsumer;
import org.aktin.broker.server.auth.AbstractAuthProvider;
+/**
+ * Provides authentication services using API keys stored in a properties file. This class extends {@link AbstractAuthProvider} and manages the
+ * lifecycle of API keys, including loading, adding, updating, and persisting them.
+ *
+ * @author akombeiz@ukaachen.de
+ */
public class ApiKeyPropertiesAuthProvider extends AbstractAuthProvider {
- private PropertyFileAPIKeys keys;
-
- public ApiKeyPropertiesAuthProvider() {
- keys = null; // lazy init, load later in getInstance by using supplied path
- }
- public ApiKeyPropertiesAuthProvider(InputStream in) throws IOException {
- this.keys = new PropertyFileAPIKeys(in);
- }
-
-
- @Override
- public PropertyFileAPIKeys getInstance() throws IOException {
- if( this.keys == null ) {
- // not previously loaded
- try( InputStream in = Files.newInputStream(path.resolve("api-keys.properties")) ){
- this.keys = new PropertyFileAPIKeys(in);
- }
- }else {
- ;// already loaded, use existing one
- }
- return keys;
-// if( System.getProperty("rewriteNodeDN") != null ){
-// int count = BrokerImpl.updatePrincipalDN(ds, keys.getMap());
-// // output/log what happened, use count returned from above method
-// System.out.println("Rewritten "+count+" node DN strings.");
-// }
- }
+
+ private PropertyFileAPIKeys keys;
+
+ /**
+ * Constructs a new ApiKeyPropertiesAuthProvider with lazy initialization. The API keys will be loaded later when getInstance() is called.
+ */
+ public ApiKeyPropertiesAuthProvider() {
+ keys = null;
+ }
+
+ /**
+ * Constructs a new ApiKeyPropertiesAuthProvider and immediately loads API keys from the given input stream.
+ *
+ * @param in The input stream containing the API keys properties.
+ * @throws IOException If an I/O error occurs while reading from the input stream.
+ */
+ public ApiKeyPropertiesAuthProvider(InputStream in) throws IOException {
+ this.keys = new PropertyFileAPIKeys(in);
+ }
+
+ /**
+ * Retrieves or initializes the PropertyFileAPIKeys instance.
+ *
+ * @return The PropertyFileAPIKeys instance.
+ * @throws IOException If an I/O error occurs while loading the properties file.
+ */
+ @Override
+ public PropertyFileAPIKeys getInstance() throws IOException {
+ if (this.keys == null) {
+ try (InputStream in = Files.newInputStream(getPropertiesPath())) {
+ this.keys = new PropertyFileAPIKeys(in);
+ }
+ }
+ return keys;
+ }
+
+ /**
+ * @return The Path object representing the location of API keys properties file
+ */
+ private Path getPropertiesPath() {
+ return path.resolve("api-keys.properties");
+ }
+
+ /**
+ * Adds a new API key and updates the properties file.
+ *
+ * @param apiKey The new API key to add.
+ * @param clientDn The distinguished name of the client associated with the API key.
+ * @throws IOException If an I/O error occurs while updating the properties file.
+ * @throws IllegalArgumentException If the API key already exists.
+ * @throws IllegalStateException If the PropertyFileAPIKeys instance is not initialized.
+ */
+ public void addNewApiKeyAndUpdatePropertiesFile(String apiKey, String clientDn)
+ throws IOException, IllegalArgumentException, IllegalStateException {
+ checkKeysInitialized();
+ Properties properties = keys.getProperties();
+ if (properties.containsKey(apiKey)) {
+ throw new IllegalArgumentException("API key already exists: " + apiKey);
+ }
+ keys.putApiKey(apiKey, clientDn);
+ saveProperties(keys);
+ }
+
+ /**
+ * Sets the state of an existing API key and updates the properties file.
+ *
+ * @param apiKey The API key to update.
+ * @param status The new status to set for the API key.
+ * @throws IOException If an I/O error occurs while updating the properties file.
+ * @throws NoSuchElementException If the API key does not exist.
+ * @throws SecurityException If an attempt is made to modify an admin API key.
+ * @throws IllegalStateException If the PropertyFileAPIKeys instance is not initialized.
+ */
+ public void setStateOfApiKeyAndUpdatePropertiesFile(String apiKey, ApiKeyStatus status)
+ throws IOException, NoSuchElementException, SecurityException, IllegalStateException {
+ checkKeysInitialized();
+ Properties properties = keys.getProperties();
+ if (!properties.containsKey(apiKey)) {
+ throw new NoSuchElementException("API key does not exist");
+ }
+ String clientDn = properties.getProperty(apiKey);
+ checkNotAdminKey(clientDn);
+ String updatedClientDn = setStatusInClientDn(clientDn, status);
+ if (!clientDn.equals(updatedClientDn)) {
+ keys.putApiKey(apiKey, updatedClientDn);
+ saveProperties(keys);
+ }
+ }
+
+ /**
+ * Checks if the given client DN belongs to an admin key.
+ *
+ * @param clientDn The client distinguished name to check.
+ * @throws SecurityException If the client DN indicates an admin key.
+ */
+ private void checkNotAdminKey(String clientDn) {
+ if (clientDn != null && clientDn.contains("OU=admin")) {
+ throw new SecurityException("Admin API key state cannot be modified");
+ }
+ }
+
+ /**
+ * Updates the status in the client DN string.
+ *
+ * @param clientDn The original client DN.
+ * @param status The new status to set.
+ * @return The updated client DN string with the new status.
+ * @throws IllegalArgumentException If an unknown status is provided.
+ */
+ private String setStatusInClientDn(String clientDn, ApiKeyStatus status) {
+ switch (status) {
+ case ACTIVE:
+ return clientDn.replace("," + ApiKeyStatus.INACTIVE.name(), "");
+ case INACTIVE:
+ if (clientDn.endsWith(ApiKeyStatus.INACTIVE.name())) {
+ return clientDn;
+ } else {
+ return clientDn + "," + ApiKeyStatus.INACTIVE.name();
+ }
+ default:
+ throw new IllegalArgumentException("Unknown status: " + status.name());
+ }
+ }
+
+ /**
+ * Checks if the API keys instance is initialized.
+ *
+ * @throws IllegalStateException If the API keys instance is not initialized.
+ */
+ private void checkKeysInitialized() {
+ if (this.keys == null) {
+ throw new IllegalStateException("API keys instance is not initialized");
+ }
+ }
+
+ /**
+ * Saves the current state of API keys to the properties file in a latin-1 encoding.
+ *
+ * @param instance The PropertyFileAPIKeys instance to save.
+ * @throws IOException If an I/O error occurs while writing to the properties file.
+ */
+ private void saveProperties(PropertyFileAPIKeys instance) throws IOException {
+ try (OutputStream out = Files.newOutputStream(getPropertiesPath())) {
+ instance.storeProperties(out, StandardCharsets.ISO_8859_1);
+ }
+ }
+
+ /**
+ * Returns the array of endpoint classes associated with this auth provider.
+ *
+ * @return An array containing the ApiKeyManagementEndpoint class.
+ */
+ @Override
+ public Class>[] getEndpoints() {
+ return new Class>[]{ApiKeyManagementEndpoint.class};
+ }
+
+ /**
+ * Binds this instance to the ApiKeyPropertiesAuthProvider class so it can be injected into the associated endpoints of {@link #getEndpoints()}.
+ *
+ * @param binder The binder function to use for binding.
+ */
+ @Override
+ public void bindSingletons(BiConsumer