Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/node management endpoint #42

Merged
merged 24 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3f52d67
removed commented out code
akomii Jun 19, 2024
0de5f11
refactored PropertyFileAPIKeys and ApiKeyPropertiesAuthProvider slightly
akomii Jul 3, 2024
37caa78
Added new features in PropertyFileAPIKeys:
akomii Jul 3, 2024
8a0ea77
Added new features in ApiKeyPropertiesAuthProvider:
akomii Jul 3, 2024
e186e36
Added ApiKeyManagementEndpoint to retrieve api keys and add additiona…
akomii Jul 3, 2024
5239a02
added enum for apiKey status, reordered methods in PropertyFileAPIKey…
akomii Jul 3, 2024
2b24dd6
added feature to change apiKey status via endpoint
akomii Jul 3, 2024
5290c9b
renamed addApiKey() to putApiKey to better reflect its functionality
akomii Jul 4, 2024
63767c9
added feature that api key state remains unaffected in case of update
akomii Jul 4, 2024
9eab0c3
added feature that admin api key is not editable
akomii Jul 4, 2024
11fb942
added java docs to ApiKeyStatus and PropertyFileAPIKeys
akomii Jul 4, 2024
43a5381
refactored ApiKeyPropertiesAuthProvider and ApiKeyManagementEndpoint …
akomii Jul 4, 2024
5da3027
added doc to ApiKeyPropertiesAuthProvider and ApiKeyManagementEndpoint
akomii Jul 4, 2024
cbed9c0
adjusted test api-keys.properties for new active/inactive state of ap…
akomii Jul 4, 2024
9e0f628
Revert "adjusted test api-keys.properties for new active/inactive sta…
akomii Jul 5, 2024
281bf83
Revert "added doc to ApiKeyPropertiesAuthProvider and ApiKeyManagemen…
akomii Jul 5, 2024
a11e0b9
Revert "refactored ApiKeyPropertiesAuthProvider and ApiKeyManagementE…
akomii Jul 5, 2024
8450cf0
Revert "added java docs to ApiKeyStatus and PropertyFileAPIKeys"
akomii Jul 5, 2024
c58967d
removed status argument from putApiKey(), changed lookupAuthInfo to b…
akomii Jul 5, 2024
0b05739
removed api key validation, updated create and update api key operati…
akomii Jul 8, 2024
1becaa8
added DTO for apiKey, adjusted endpoint to changes in AuthProvider, r…
akomii Jul 8, 2024
8ca076c
added JavaDocs
akomii Jul 8, 2024
80f6e6c
adjustments to review of pull request:
akomii Jul 11, 2024
e006a96
changed the PUT operation for (de-)activating api keys to POST
akomii Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 [email protected]
*/
@XmlRootElement(name = "apiKeyCred")
akomii marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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.PUT;
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 [email protected]
*/
@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: <ul>
* <li>{@code 201} - API key created successfully</li>
* <li>{@code 400} - API key or client DN are not provided</li>
* <li>{@code 409} - API key already exists</li>
* <li>{@code 500} - When there is an error in saving the properties file or API keys instance is not initialized</li>
* </ul>
*/
@POST
@Consumes(MediaType.APPLICATION_XML)
public Response postApiKey(ApiKeyDTO apiKeyDTO) {
akomii marked this conversation as resolved.
Show resolved Hide resolved
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.
*/
@PUT
akomii marked this conversation as resolved.
Show resolved Hide resolved
@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.
*/
@PUT
akomii marked this conversation as resolved.
Show resolved Hide resolved
@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: <ul>
* <li>{@code 200} - Status was changed or API key was already in that state</li>
* <li>{@code 404} - API key does not exist</li>
* <li>{@code 403} - When attempting to modify an admin API key</li>
* <li>{@code 400} - When ApiKeyStatus is unknown, or apiKey or status are not provided</li>
* <li>{@code 500} - When there is an error in saving the properties file or API keys instance is not initialized</li>
* </ul>
*/
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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,180 @@

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 [email protected]
*/
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 {
akomii marked this conversation as resolved.
Show resolved Hide resolved
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 {
akomii marked this conversation as resolved.
Show resolved Hide resolved
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:
return clientDn.endsWith(ApiKeyStatus.INACTIVE.name()) ? clientDn : clientDn + "," + ApiKeyStatus.INACTIVE.name();
akomii marked this conversation as resolved.
Show resolved Hide resolved
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<Object, Class<?>> binder) {
binder.accept(this, ApiKeyPropertiesAuthProvider.class);
}
}
Loading
Loading