Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-picocli</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.hyperfoil.tools.h5m.api.svc;

import io.hyperfoil.tools.h5m.entity.ApiKey;

import java.util.List;

public interface ApiKeyServiceInterface {

String create(String username, String description);

List<ApiKey> listByUser(String username);

void revoke(long keyId);
}
3 changes: 3 additions & 0 deletions src/main/java/io/hyperfoil/tools/h5m/cli/AdminCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
AdminListTeams.class,
AdminListUsers.class,
AdminAddMember.class,
AdminCreateApiKey.class,
AdminListApiKeys.class,
AdminRevokeApiKey.class,
}
)
public class AdminCmd implements Callable<Integer> {
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/io/hyperfoil/tools/h5m/cli/AdminCreateApiKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.hyperfoil.tools.h5m.cli;

import io.hyperfoil.tools.h5m.api.svc.ApiKeyServiceInterface;
import jakarta.inject.Inject;
import picocli.CommandLine;

import java.util.concurrent.Callable;

@CommandLine.Command(name = "create-api-key", description = "create an API key for a user", mixinStandardHelpOptions = true)
public class AdminCreateApiKey implements Callable<Integer> {

@Inject
ApiKeyServiceInterface apiKeyService;

@CommandLine.Parameters(index = "0", description = "username")
public String username;

@CommandLine.Option(names = {"--description"}, description = "key description", defaultValue = "")
public String description;

@Override
public Integer call() {
String rawKey = apiKeyService.create(username, description);
System.out.println("API key created for user: " + username);
System.out.println("Key: " + rawKey);
System.out.println("WARNING: This key cannot be retrieved again. Store it securely.");
return 0;
}
}
33 changes: 33 additions & 0 deletions src/main/java/io/hyperfoil/tools/h5m/cli/AdminListApiKeys.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.hyperfoil.tools.h5m.cli;

import io.hyperfoil.tools.h5m.api.svc.ApiKeyServiceInterface;
import io.hyperfoil.tools.h5m.entity.ApiKey;
import jakarta.inject.Inject;
import picocli.CommandLine;

import java.time.Instant;
import java.util.List;

@CommandLine.Command(name = "list-api-keys", description = "list API keys for a user", mixinStandardHelpOptions = true)
public class AdminListApiKeys implements Runnable {

@Inject
ApiKeyServiceInterface apiKeyService;

@CommandLine.Parameters(index = "0", description = "username")
public String username;

@Override
public void run() {
List<ApiKey> keys = apiKeyService.listByUser(username);
Instant now = Instant.now();
System.out.println(ListCmd.table(100, keys,
List.of("id", "description", "created", "last_used", "revoked", "expired"),
List.of(k -> String.valueOf(k.id),
k -> k.description != null ? k.description : "",
k -> k.createdAt != null ? k.createdAt.toString() : "",
k -> k.lastUsedAt != null ? k.lastUsedAt.toString() : "never",
k -> String.valueOf(k.revoked),
k -> String.valueOf(k.isExpired(now)))));
}
}
24 changes: 24 additions & 0 deletions src/main/java/io/hyperfoil/tools/h5m/cli/AdminRevokeApiKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.hyperfoil.tools.h5m.cli;

import io.hyperfoil.tools.h5m.api.svc.ApiKeyServiceInterface;
import jakarta.inject.Inject;
import picocli.CommandLine;

import java.util.concurrent.Callable;

@CommandLine.Command(name = "revoke-api-key", description = "revoke an API key", mixinStandardHelpOptions = true)
public class AdminRevokeApiKey implements Callable<Integer> {

@Inject
ApiKeyServiceInterface apiKeyService;

@CommandLine.Parameters(index = "0", description = "API key ID")
public long keyId;

@Override
public Integer call() {
apiKeyService.revoke(keyId);
System.out.println("API key " + keyId + " revoked.");
return 0;
}
}
2 changes: 1 addition & 1 deletion src/main/java/io/hyperfoil/tools/h5m/entity/ApiKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class ApiKey extends PanacheEntity {
@Column(name = "key_hash")
public String keyHash; // SHA-256 hex

@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.EAGER)
public User user;

public String description;
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/io/hyperfoil/tools/h5m/rest/FolderResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import io.hyperfoil.tools.h5m.api.Folder;
import io.hyperfoil.tools.h5m.api.svc.FolderServiceInterface;
import io.hyperfoil.tools.yaup.json.Json;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
Expand All @@ -24,33 +26,38 @@ public class FolderResource {

@GET
@Path("{name}")
@PermitAll
@Operation(description = "Retrieve a folder by its name")
public Folder byName(@PathParam("name") String name) {
return folderService.byName(name);
}

@GET
@PermitAll
@Operation(description = "Get the upload count for all folders")
public Map<String, Integer> getFolderUploadCount() {
return folderService.getFolderUploadCount();
}

@POST
@Path("{name}")
@Authenticated
@Operation(description = "Create a new folder")
public long create(@PathParam("name") String name) {
return folderService.create(name);
}

@DELETE
@Path("{name}")
@Authenticated
@Operation(description = "Delete a folder by its name")
public long delete(@PathParam("name") String name) {
return folderService.delete(name);
}

@POST
@Path("{name}/upload")
@Authenticated
@Operation(description = "Upload JSON data to a folder")
public void upload(
@PathParam("name") String name,
Expand All @@ -61,13 +68,15 @@ public void upload(

@POST
@Path("{name}/recalculate")
@Authenticated
@Operation(description = "Recalculate all values in a folder")
public void recalculate(@PathParam("name") String name) {
folderService.recalculate(name);
}

@GET
@Path("{name}/structure")
@PermitAll
@Operation(description = "Get the structural representation of a folder")
public Json structure(@PathParam("name") String name) {
return folderService.structure(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import io.hyperfoil.tools.h5m.api.NodeGroup;
import io.hyperfoil.tools.h5m.api.svc.NodeGroupServiceInterface;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
Expand All @@ -19,13 +21,15 @@ public class NodeGroupResource {

@GET
@Path("{name}")
@PermitAll
@Operation(description = "Retrieve a node group by its name")
public NodeGroup byName(@PathParam("name") String groupName) {
return nodeGroupService.byName(groupName);
}

@DELETE
@Path("{id}")
@Authenticated
@Operation(description = "Delete a node group by its ID")
public void delete(@PathParam("id") Long groupId) {
nodeGroupService.delete(groupId);
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/io/hyperfoil/tools/h5m/rest/NodeResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import io.hyperfoil.tools.h5m.api.Node;
import io.hyperfoil.tools.h5m.api.NodeType;
import io.hyperfoil.tools.h5m.api.svc.NodeServiceInterface;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
Expand All @@ -25,6 +27,7 @@ public class NodeResource {
NodeServiceInterface nodeService;

@POST
@Authenticated
@Operation(description = "Create a new node with an operation")
public Long create(
@QueryParam("name") @NotEmpty String name,
Expand All @@ -36,6 +39,7 @@ public Long create(

@POST
@Path("configured")
@Authenticated
@Operation(description = "Create a new node with sources and configuration")
public Long createConfigured(
@QueryParam("name") @NotEmpty String name,
Expand All @@ -54,13 +58,15 @@ public Long createConfigured(

@DELETE
@Path("{id}")
@Authenticated
@Operation(description = "Delete a node by its ID")
public void delete(@PathParam("id") Long nodeId) {
nodeService.delete(nodeId);
}

@GET
@Path("find")
@PermitAll
@Operation(description = "Find nodes by FQDN within a specific group")
public List<Node> findNodeByFqdn(
@QueryParam("name") @Parameter(description = "FQDN of the node") String name,
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/io/hyperfoil/tools/h5m/rest/ValueResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import io.hyperfoil.tools.h5m.api.Value;
import io.hyperfoil.tools.h5m.api.svc.ValueServiceInterface;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
Expand All @@ -21,20 +23,23 @@ public class ValueResource {
ValueServiceInterface valueService;

@DELETE
@RolesAllowed("admin")
@Operation(description = "Purge all values")
public void purgeValues() {
valueService.purgeValues();
}

@GET
@Path("node/{nodeId}/descendants")
@PermitAll
@Operation(description = "Get descendant values of a specific node")
public List<Value> getNodeDescendantValues(@PathParam("nodeId") Long nodeId) {
return valueService.getNodeDescendantValues(nodeId);
}

@GET
@Path("node/{nodeId}/grouped")
@PermitAll
@Operation(description = "Get grouped values for a specific node")
public List<JsonNode> getGroupedValues(@PathParam("nodeId") Long nodeId) {
return valueService.getGroupedValues(nodeId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.hyperfoil.tools.h5m.server;

import java.util.Collections;
import java.util.Set;

import jakarta.enterprise.context.ApplicationScoped;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.BaseAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public class ApiKeyAuthenticationMechanism implements HttpAuthenticationMechanism {

private static final String BEARER_PREFIX = "Bearer ";
private static final String H5M_PREFIX = "H5M_";

@ConfigProperty(name = "h5m.security.enabled", defaultValue = "false")
boolean securityEnabled;

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
if (!securityEnabled) {
SecurityIdentity localAdmin = QuarkusSecurityIdentity.builder()
.setPrincipal(new QuarkusPrincipal("local"))
.addRole("admin")
.addRole("user")
.build();
return Uni.createFrom().item(localAdmin);
}
String authorization = context.request().headers().get("Authorization");
if (authorization == null || !authorization.startsWith(BEARER_PREFIX)) {
return Uni.createFrom().nullItem();
}
String token = authorization.substring(BEARER_PREFIX.length());
if (!token.startsWith(H5M_PREFIX)) {
return Uni.createFrom().nullItem();
}
return identityProviderManager.authenticate(new Request(token));
}

@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), null, null));
}

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Collections.singleton(Request.class);
}

@Override
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, "Bearer"));
}

public static class Request extends BaseAuthenticationRequest implements AuthenticationRequest {

private final String key;

public Request(String key) {
this.key = key;
}

public String getKey() {
return key;
}
}
}
Loading
Loading