+ @@ -256,6 +308,38 @@

Assigned SSH Servers

] }); + var agentUrl = '/api/v1/agent/list'; + console.log("group id: ", groupId); + if (groupId && groupId !== '-1') { + url = `/api/v1/agent/list?groupId=${groupId}`; + console.log("url is " + url); + } + + $('#agent-table').DataTable({ + ajax: { + url: agentUrl, // list + dataSrc: '', // Specify the property where the data is located (e.g. use 'data' if response has a "data" field) + }, + columns: [ + { data: 'agentName' }, + { data: 'lastHeartbeat' }, + { + data: null, + render: function(data, type, row) { + /* + const groupId = row.group ? row.group.groupId : -1; // Access group.id + const id = row.id; + let + ret=` `; + if (canDelete) { + ret += ``; + }*/ + return ""; + } + } + + ] + }); }); diff --git a/api/src/test/java/io/sentrius/sso/controllers/api/CapabilitiesApiControllerIntegrationTest.java b/api/src/test/java/io/sentrius/sso/controllers/api/CapabilitiesApiControllerIntegrationTest.java new file mode 100644 index 00000000..ac7d597b --- /dev/null +++ b/api/src/test/java/io/sentrius/sso/controllers/api/CapabilitiesApiControllerIntegrationTest.java @@ -0,0 +1,108 @@ +package io.sentrius.sso.controllers.api; + +import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor; +import io.sentrius.sso.core.services.capabilities.EndpointScanningService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test for CapabilitiesApiController. + * This test runs with the full Spring context to verify that endpoint scanning works correctly. + */ +@SpringBootTest +@TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:testdb", + "spring.jpa.hibernate.ddl-auto=create-drop" +}) +public class CapabilitiesApiControllerIntegrationTest { + + @Autowired + private EndpointScanningService endpointScanningService; + + //@Test + public void testEndpointScanning() { + // When + List allEndpoints = endpointScanningService.getAllEndpoints(); + + // Then + assertNotNull(allEndpoints); + + System.out.println("=== ENDPOINT SCANNING RESULTS ==="); + System.out.println("Total endpoints found: " + allEndpoints.size()); + + // Count by type + long restCount = allEndpoints.stream().filter(e -> "REST".equals(e.getType())).count(); + long verbCount = allEndpoints.stream().filter(e -> "VERB".equals(e.getType())).count(); + + System.out.println("REST endpoints: " + restCount); + System.out.println("VERB endpoints: " + verbCount); + + // Print first 10 endpoints for inspection + System.out.println("\n=== SAMPLE ENDPOINTS ==="); + allEndpoints.stream().limit(10).forEach(endpoint -> { + System.out.println(String.format("%s: %s [%s] - %s", + endpoint.getType(), + endpoint.getName(), + endpoint.getHttpMethod() != null ? endpoint.getHttpMethod() + " " + endpoint.getPath() : "N/A", + endpoint.getDescription())); + }); + + // Verify we found some endpoints + assertTrue(allEndpoints.size() > 0, "Should have found some endpoints in full Spring context"); + + // Verify we found some REST endpoints (from the API controllers) + assertTrue(restCount > 0, "Should have found REST endpoints from API controllers"); + + // Look for our new capabilities endpoint + boolean foundCapabilitiesEndpoint = allEndpoints.stream() + .anyMatch(e -> "REST".equals(e.getType()) && + e.getPath() != null && + e.getPath().contains("/api/v1/capabilities")); + + assertTrue(foundCapabilitiesEndpoint, "Should have found our new capabilities endpoint"); + + // Look for some existing endpoints + boolean foundUserApiEndpoint = allEndpoints.stream() + .anyMatch(e -> "REST".equals(e.getType()) && + e.getClassName().contains("UserApiController")); + + assertTrue(foundUserApiEndpoint, "Should have found UserApiController endpoints"); + } + + //@Test + public void testEndpointFiltering() { + // When + List allEndpoints = endpointScanningService.getAllEndpoints(); + + // Filter REST endpoints + List restEndpoints = allEndpoints.stream() + .filter(e -> "REST".equals(e.getType())) + .toList(); + + // Filter VERB endpoints + List verbEndpoints = allEndpoints.stream() + .filter(e -> "VERB".equals(e.getType())) + .toList(); + + // Then + System.out.println("\n=== FILTERING TEST ==="); + System.out.println("Total: " + allEndpoints.size()); + System.out.println("REST only: " + restEndpoints.size()); + System.out.println("VERB only: " + verbEndpoints.size()); + + assertEquals(allEndpoints.size(), restEndpoints.size() + verbEndpoints.size(), + "Total should equal sum of REST and VERB endpoints"); + + // Verify REST endpoints have paths + restEndpoints.forEach(endpoint -> { + assertNotNull(endpoint.getPath(), "REST endpoint should have a path"); + assertNotNull(endpoint.getHttpMethod(), "REST endpoint should have HTTP method"); + }); + } +} \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 15341f74..85966178 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -193,6 +193,12 @@ io.opentelemetry opentelemetry-exporter-otlp + + + + io.github.classgraph + classgraph + diff --git a/core/src/main/java/io/sentrius/sso/config/ApplicationConfig.java b/core/src/main/java/io/sentrius/sso/config/ApplicationEnvironmentConfig.java similarity index 80% rename from core/src/main/java/io/sentrius/sso/config/ApplicationConfig.java rename to core/src/main/java/io/sentrius/sso/config/ApplicationEnvironmentConfig.java index a454c0d8..794a8e54 100644 --- a/core/src/main/java/io/sentrius/sso/config/ApplicationConfig.java +++ b/core/src/main/java/io/sentrius/sso/config/ApplicationEnvironmentConfig.java @@ -5,12 +5,12 @@ import org.springframework.stereotype.Component; @Component -public class ApplicationConfig { +public class ApplicationEnvironmentConfig { private final String serviceName; @Autowired - public ApplicationConfig(Environment environment) { + public ApplicationEnvironmentConfig(Environment environment) { this.serviceName = environment.getProperty("otel.resource.attributes.service.name", "unknown-service"); } diff --git a/core/src/main/java/io/sentrius/sso/core/dto/AgentDTO.java b/core/src/main/java/io/sentrius/sso/core/dto/AgentDTO.java index 9207ecae..ba8ee176 100644 --- a/core/src/main/java/io/sentrius/sso/core/dto/AgentDTO.java +++ b/core/src/main/java/io/sentrius/sso/core/dto/AgentDTO.java @@ -3,9 +3,10 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.experimental.SuperBuilder; @Getter -@Builder +@SuperBuilder(toBuilder = true) @AllArgsConstructor public class AgentDTO { private final String agentName; diff --git a/core/src/main/java/io/sentrius/sso/core/dto/AgentHistoryDTO.java b/core/src/main/java/io/sentrius/sso/core/dto/AgentHistoryDTO.java new file mode 100644 index 00000000..cd4ab1a7 --- /dev/null +++ b/core/src/main/java/io/sentrius/sso/core/dto/AgentHistoryDTO.java @@ -0,0 +1,13 @@ +package io.sentrius.sso.core.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder(toBuilder = true) + +public class AgentHistoryDTO extends AgentDTO{ + private String agentTypeName; +} diff --git a/core/src/main/java/io/sentrius/sso/core/dto/capabilities/AccessLimitations.java b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/AccessLimitations.java new file mode 100644 index 00000000..b65789f5 --- /dev/null +++ b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/AccessLimitations.java @@ -0,0 +1,36 @@ +package io.sentrius.sso.core.dto.capabilities; + +import io.sentrius.sso.core.data.EndpointThreat; +import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum; +import io.sentrius.sso.core.model.security.enums.IdentityType; +import io.sentrius.sso.core.model.security.enums.RuleAccessEnum; +import io.sentrius.sso.core.model.security.enums.SSHAccessEnum; +import io.sentrius.sso.core.model.security.enums.SystemOperationsEnum; +import io.sentrius.sso.core.model.security.enums.UserAccessEnum; +import io.sentrius.sso.core.model.security.enums.ZeroTrustAccessTokenEnum; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents access limitations extracted from @LimitAccess annotation. + */ +@Builder +@Data +@Getter +@Setter +public class AccessLimitations { + private String notificationMessage; + private IdentityType[] allowedIdentityTypes; + private UserAccessEnum[] userAccess; + private ApplicationAccessEnum[] applicationAccess; + private RuleAccessEnum[] ruleAccess; + private SSHAccessEnum[] sshAccess; + private SystemOperationsEnum[] systemOperations; + private ZeroTrustAccessTokenEnum[] ztatAccess; + private EndpointThreat endpointThreat; + + @Builder.Default + private boolean hasLimitAccess = false; +} \ No newline at end of file diff --git a/core/src/main/java/io/sentrius/sso/core/dto/capabilities/EndpointDescriptor.java b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/EndpointDescriptor.java new file mode 100644 index 00000000..c9813088 --- /dev/null +++ b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/EndpointDescriptor.java @@ -0,0 +1,37 @@ +package io.sentrius.sso.core.dto.capabilities; + +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents a unified descriptor for both REST API endpoints and Verb methods. + * This allows for a consistent way to describe all capabilities across the system. + */ +@Builder +@Data +@Getter +@Setter +public class EndpointDescriptor { + private String name; + private String description; + private String type; // "REST" or "VERB" + private String httpMethod; // GET, POST, etc. (null for verbs) + private String path; // REST path (null for verbs) + private String className; // Class containing the method + private String methodName; // Method name + private List parameters; + private AccessLimitations accessLimitations; + private Map metadata; // Additional metadata + + @Builder.Default + private boolean requiresAuthentication = true; + + @Builder.Default + private boolean requiresTokenManagement = false; + + private Class returnType; +} \ No newline at end of file diff --git a/core/src/main/java/io/sentrius/sso/core/dto/capabilities/ParameterDescriptor.java b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/ParameterDescriptor.java new file mode 100644 index 00000000..a108a1d1 --- /dev/null +++ b/core/src/main/java/io/sentrius/sso/core/dto/capabilities/ParameterDescriptor.java @@ -0,0 +1,22 @@ +package io.sentrius.sso.core.dto.capabilities; + +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +/** + * Describes a parameter for an endpoint (either REST or Verb). + */ +@Builder +@Data +@Getter +@Setter +public class ParameterDescriptor { + private String name; + private String description; + private Class type; + private boolean required; + private Object defaultValue; + private String source; // "PATH", "QUERY", "BODY", "HEADER", "METHOD_PARAM" +} \ No newline at end of file diff --git a/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbDescriptor.java b/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbDescriptor.java index 22ff342d..150a8716 100644 --- a/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbDescriptor.java +++ b/core/src/main/java/io/sentrius/sso/core/model/verbs/VerbDescriptor.java @@ -14,6 +14,7 @@ public class VerbDescriptor { private String name; private String description; + private Class returnType; // Optional: can be used to specify the expected return type @Deprecated private List params = new ArrayList<>(); private boolean requiresZtat; // Optional: move this to policy if preferred diff --git a/core/src/main/java/io/sentrius/sso/core/services/capabilities/EndpointScanningService.java b/core/src/main/java/io/sentrius/sso/core/services/capabilities/EndpointScanningService.java new file mode 100644 index 00000000..2ece69d2 --- /dev/null +++ b/core/src/main/java/io/sentrius/sso/core/services/capabilities/EndpointScanningService.java @@ -0,0 +1,318 @@ +package io.sentrius.sso.core.services.capabilities; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import io.sentrius.sso.core.annotations.LimitAccess; +import io.sentrius.sso.core.dto.capabilities.AccessLimitations; +import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor; +import io.sentrius.sso.core.dto.capabilities.ParameterDescriptor; +import io.sentrius.sso.core.model.verbs.Verb; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.stream.Stream; + +/** + * Service that scans for both REST API endpoints and Verb methods across the application. + * Provides a unified view of all capabilities available in the system. + */ +@Service +@Slf4j +public class EndpointScanningService { + + private final ApplicationContext applicationContext; + private final Map cachedEndpoints = new HashMap<>(); + private boolean cacheInitialized = false; + + public EndpointScanningService(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Scans for all endpoints (REST and Verb) across the application. + * Results are cached for performance. + */ + public List getAllEndpoints() { + if (!cacheInitialized) { + synchronized (this) { + if (!cacheInitialized) { + scanAndCacheEndpoints(); + cacheInitialized = true; + } + } + } + return new ArrayList<>(cachedEndpoints.values()); + } + + /** + * Forces a rescan of all endpoints, clearing the cache. + */ + public void refreshEndpoints() { + synchronized (this) { + cachedEndpoints.clear(); + cacheInitialized = false; + getAllEndpoints(); // Trigger rescan + } + } + + private void scanAndCacheEndpoints() { + log.info("Starting endpoint scanning..."); + + // Scan for REST endpoints + scanRestEndpoints(); + + // Scan for Verb methods + scanVerbEndpoints(); + + log.info("Endpoint scanning completed. Found {} endpoints", cachedEndpoints.size()); + } + + private void scanRestEndpoints() { + try (ScanResult scanResult = new ClassGraph() + .enableAllInfo() + .acceptPackages("io.sentrius") + .scan()) { + + scanResult.getClassesWithAnnotation(RestController.class.getName()).forEach(classInfo -> { + try { + Class clazz = classInfo.loadClass(); + scanRestControllerClass(clazz); + } catch (Exception e) { + log.warn("Failed to scan REST controller class: {}", classInfo.getName(), e); + } + }); + + scanResult.getClassesWithAnnotation(Controller.class.getName()).forEach(classInfo -> { + try { + Class clazz = classInfo.loadClass(); + scanRestControllerClass(clazz); + } catch (Exception e) { + log.warn("Failed to scan Controller class: {}", classInfo.getName(), e); + } + }); + } + } + + private void scanRestControllerClass(Class clazz) { + RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class); + String basePath = classMapping != null && classMapping.value().length > 0 ? classMapping.value()[0] : ""; + + for (Method method : clazz.getDeclaredMethods()) { + EndpointDescriptor descriptor = scanRestMethod(clazz, method, basePath); + if (descriptor != null) { + String key = descriptor.getType() + ":" + descriptor.getName(); + cachedEndpoints.put(key, descriptor); + } + } + } + + private EndpointDescriptor scanRestMethod(Class clazz, Method method, String basePath) { + String httpMethod = null; + String path = basePath; + + // Check for HTTP method annotations + if (method.isAnnotationPresent(GetMapping.class)) { + httpMethod = "GET"; + GetMapping mapping = method.getAnnotation(GetMapping.class); + if (mapping.value().length > 0) { + path += mapping.value()[0]; + } + } else if (method.isAnnotationPresent(PostMapping.class)) { + httpMethod = "POST"; + PostMapping mapping = method.getAnnotation(PostMapping.class); + if (mapping.value().length > 0) { + path += mapping.value()[0]; + } + } else if (method.isAnnotationPresent(PutMapping.class)) { + httpMethod = "PUT"; + PutMapping mapping = method.getAnnotation(PutMapping.class); + if (mapping.value().length > 0) { + path += mapping.value()[0]; + } + } else if (method.isAnnotationPresent(DeleteMapping.class)) { + httpMethod = "DELETE"; + DeleteMapping mapping = method.getAnnotation(DeleteMapping.class); + if (mapping.value().length > 0) { + path += mapping.value()[0]; + } + } else if (method.isAnnotationPresent(RequestMapping.class)) { + RequestMapping mapping = method.getAnnotation(RequestMapping.class); + if (mapping.method().length > 0) { + httpMethod = mapping.method()[0].name(); + } + if (mapping.value().length > 0) { + path += mapping.value()[0]; + } + } + + if (httpMethod == null) { + return null; // Not a REST endpoint + } + + // Extract access limitations + AccessLimitations accessLimitations = extractAccessLimitations(method); + + // Extract parameters + List parameters = extractRestParameters(method); + + return EndpointDescriptor.builder() + .name(method.getName()) + .description("REST endpoint: " + httpMethod + " " + path) // TODO: Extract from JavaDoc or custom annotation + .type("REST") + .httpMethod(httpMethod) + .path(path) + .className(clazz.getName()) + .methodName(method.getName()) + .parameters(parameters) + .accessLimitations(accessLimitations) + .returnType(method.getReturnType()) + .requiresAuthentication(accessLimitations.isHasLimitAccess()) + .build(); + } + + private void scanVerbEndpoints() { + try (ScanResult scanResult = new ClassGraph() + .enableAllInfo() + .acceptPackages("io.sentrius") + .scan()) { + + scanResult.getClassesWithMethodAnnotation(Verb.class.getName()).forEach(classInfo -> { + try { + Class clazz = classInfo.loadClass(); + scanVerbClass(clazz); + } catch (Exception e) { + log.warn("Failed to scan Verb class: {}", classInfo.getName(), e); + } + }); + } + } + + private void scanVerbClass(Class clazz) { + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(Verb.class)) { + EndpointDescriptor descriptor = scanVerbMethod(clazz, method); + if (descriptor != null) { + String key = descriptor.getType() + ":" + descriptor.getName(); + cachedEndpoints.put(key, descriptor); + } + } + } + } + + private EndpointDescriptor scanVerbMethod(Class clazz, Method method) { + Verb verbAnnotation = method.getAnnotation(Verb.class); + + // Extract parameters + List parameters = extractVerbParameters(method, verbAnnotation); + + return EndpointDescriptor.builder() + .name(verbAnnotation.name()) + .description(verbAnnotation.description()) + .type("VERB") + .className(clazz.getName()) + .methodName(method.getName()) + .parameters(parameters) + .returnType(verbAnnotation.returnType()) + .requiresTokenManagement(verbAnnotation.requiresTokenManagement()) + .accessLimitations(AccessLimitations.builder().hasLimitAccess(false).build()) + .metadata(Map.of( + "isAiCallable", verbAnnotation.isAiCallable(), + "outputInterpreter", verbAnnotation.outputInterpreter().getName(), + "inputInterpreter", verbAnnotation.inputInterpreter().getName() + )) + .build(); + } + + private AccessLimitations extractAccessLimitations(Method method) { + LimitAccess limitAccess = method.getAnnotation(LimitAccess.class); + if (limitAccess == null) { + return AccessLimitations.builder().hasLimitAccess(false).build(); + } + + return AccessLimitations.builder() + .hasLimitAccess(true) + .notificationMessage(limitAccess.notificationMessage()) + .allowedIdentityTypes(limitAccess.allowedIdentityTypes()) + .userAccess(limitAccess.userAccess()) + .applicationAccess(limitAccess.applicationAccess()) + .ruleAccess(limitAccess.ruleAccess()) + .sshAccess(limitAccess.sshAccess()) + .systemOperations(limitAccess.systemOperations()) + .ztatAccess(limitAccess.ztatAccess()) + .endpointThreat(limitAccess.endpointThreat()) + .build(); + } + + private List extractRestParameters(Method method) { + List parameters = new ArrayList<>(); + + for (Parameter parameter : method.getParameters()) { + String source = "METHOD_PARAM"; + boolean required = true; + String name = parameter.getName(); + + // Check for Spring Web annotations + if (parameter.isAnnotationPresent(RequestParam.class)) { + RequestParam requestParam = parameter.getAnnotation(RequestParam.class); + source = "QUERY"; + name = !requestParam.value().isEmpty() ? requestParam.value() : + (!requestParam.name().isEmpty() ? requestParam.name() : name); + required = requestParam.required(); + } else if (parameter.isAnnotationPresent(PathVariable.class)) { + PathVariable pathVariable = parameter.getAnnotation(PathVariable.class); + source = "PATH"; + name = !pathVariable.value().isEmpty() ? pathVariable.value() : + (!pathVariable.name().isEmpty() ? pathVariable.name() : name); + required = pathVariable.required(); + } else if (parameter.isAnnotationPresent(RequestBody.class)) { + source = "BODY"; + required = parameter.getAnnotation(RequestBody.class).required(); + } else if (parameter.isAnnotationPresent(RequestHeader.class)) { + RequestHeader requestHeader = parameter.getAnnotation(RequestHeader.class); + source = "HEADER"; + name = !requestHeader.value().isEmpty() ? requestHeader.value() : + (!requestHeader.name().isEmpty() ? requestHeader.name() : name); + required = requestHeader.required(); + } + + parameters.add(ParameterDescriptor.builder() + .name(name) + .type(parameter.getType()) + .required(required) + .source(source) + .build()); + } + + return parameters; + } + + private List extractVerbParameters(Method method, Verb verbAnnotation) { + List parameters = new ArrayList<>(); + + // For Verb methods, parameter descriptions can come from the annotation + String[] paramDescriptions = verbAnnotation.paramDescriptions(); + + Parameter[] methodParams = method.getParameters(); + for (int i = 0; i < methodParams.length; i++) { + Parameter parameter = methodParams[i]; + String description = (i < paramDescriptions.length) ? paramDescriptions[i] : ""; + + parameters.add(ParameterDescriptor.builder() + .name(parameter.getName()) + .description(description) + .type(parameter.getType()) + .required(true) // Assume required for verb parameters + .source("METHOD_PARAM") + .build()); + } + + return parameters; + } +} \ No newline at end of file diff --git a/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java b/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java index 22ab0a63..dde006eb 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java +++ b/core/src/main/java/io/sentrius/sso/core/services/security/JwtUtil.java @@ -1,5 +1,6 @@ package io.sentrius.sso.core.services.security; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Optional; import com.fasterxml.jackson.core.JsonProcessingException; @@ -87,26 +88,33 @@ public static Optional getUserTypeName(ObjectNode jwt) { } - /** - * Extract the 'kid' (Key ID) from a JWT header. - */ - public static String extractKid(String jwt) { - // JWT structure: header.payload.signature + public static String extractKid(String jwt) { try { + // Strip "Bearer " prefix if present + if (jwt.startsWith("Bearer ")) { + jwt = jwt.substring(7); + } String[] parts = jwt.split("\\."); + log.info("JWT parts: header={}, payload={}, signaturePresent={}", + parts.length > 0 ? parts[0] : "null", + parts.length > 1 ? parts[1] : "null", + parts.length == 3); if (parts.length != 3) { throw new IllegalArgumentException("Invalid JWT token format"); } - var part = parts[0].trim(); - String headerJson = new String(Base64.getDecoder().decode(part)); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); var headerNode = JsonUtil.MAPPER.readTree(headerJson); - return headerNode.has("kid") ? headerNode.get("kid").asText() : null; + if (!headerNode.has("kid")) { + throw new RuntimeException("Missing 'kid' in JWT header"); + } + + return headerNode.get("kid").asText(); } catch (Exception e) { - e.printStackTrace(); - log.info("Failed to extract 'kid' from JWT {}", jwt); + log.error("Failed to extract 'kid' from JWT: {}", jwt, e); throw new RuntimeException("Failed to extract 'kid' from JWT", e); } } + } diff --git a/core/src/main/java/io/sentrius/sso/core/services/security/KeycloakService.java b/core/src/main/java/io/sentrius/sso/core/services/security/KeycloakService.java index 9a6f08aa..4ab35d77 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/security/KeycloakService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/security/KeycloakService.java @@ -60,6 +60,11 @@ public Map> getUserAttributes(String userId) { */ public boolean validateJwt(String token) { try { + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + + token = token.trim().replaceAll("\\s+", ""); // remove all whitespace var kid = JwtUtil.extractKid(token); Objects.requireNonNull(kid, "No 'kid' found in JWT header"); var publicKey = keycloak.getPublicKey(kid); @@ -79,6 +84,11 @@ public boolean validateJwt(String token) { * Extract the client ID (agent identity) from a valid JWT. */ public String extractAgentId(String token) { + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + + token = token.trim().replaceAll("\\s+", ""); // remove all whitespace var kid = JwtUtil.extractKid(token); Objects.requireNonNull(kid, "No 'kid' found in JWT header"); var publicKey = keycloak.getPublicKey(kid); @@ -93,6 +103,11 @@ public String extractAgentId(String token) { } public String extractUsername(String token) { + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + + token = token.trim().replaceAll("\\s+", ""); // remove all whitespace var kid = JwtUtil.extractKid(token); Objects.requireNonNull(kid, "No 'kid' found in JWT header"); var publicKey = keycloak.getPublicKey(kid); @@ -119,6 +134,13 @@ public void removeAgentClient(String clientId) { public AgentRegistrationDTO registerAgentClient(AgentRegistrationDTO agent) { ClientsResource clients = keycloak.getKeycloak().realm(realm).clients(); + List existingClients = clients.findByClientId(agent.getAgentName()); + if (!existingClients.isEmpty()) { + String existingClientId = existingClients.get(0).getId(); + log.warn("Client with ID '{}' already exists. Removing before re-registration.", agent.getAgentName()); + clients.get(existingClientId).remove(); + } + // Step 1: Build client representation ClientRepresentation client = new ClientRepresentation(); client.setClientId(agent.getAgentName()); diff --git a/core/src/test/java/io/sentrius/sso/core/services/capabilities/EndpointScanningServiceTest.java b/core/src/test/java/io/sentrius/sso/core/services/capabilities/EndpointScanningServiceTest.java new file mode 100644 index 00000000..6d5e853d --- /dev/null +++ b/core/src/test/java/io/sentrius/sso/core/services/capabilities/EndpointScanningServiceTest.java @@ -0,0 +1,99 @@ +package io.sentrius.sso.core.services.capabilities; + +import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.context.ApplicationContext; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for EndpointScanningService. + * Validates that the service can discover both REST endpoints and Verb methods. + */ +public class EndpointScanningServiceTest { + + @Mock + private ApplicationContext applicationContext; + + private EndpointScanningService endpointScanningService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + endpointScanningService = new EndpointScanningService(applicationContext); + } + + @Test + void testGetAllEndpoints() { + // When + List endpoints = endpointScanningService.getAllEndpoints(); + + // Then + assertNotNull(endpoints); + System.out.println("Found " + endpoints.size() + " endpoints"); + + // Log some information about what was found + long restCount = endpoints.stream().filter(e -> "REST".equals(e.getType())).count(); + long verbCount = endpoints.stream().filter(e -> "VERB".equals(e.getType())).count(); + + System.out.println("Found " + restCount + " REST endpoints and " + verbCount + " VERB endpoints"); + + // Print first few endpoints for inspection + endpoints.stream().limit(10).forEach(endpoint -> { + System.out.println("Endpoint: " + endpoint.getName() + + " (Type: " + endpoint.getType() + + ", Class: " + endpoint.getClassName() + ")"); + }); + + // In a test environment, we might not have full Spring context loaded, + // so the test is mainly to verify the service doesn't crash + // The real validation will be done when the service runs in the full application + } + + @Test + void testRefreshEndpoints() { + // Given - get initial count + List initialEndpoints = endpointScanningService.getAllEndpoints(); + int initialCount = initialEndpoints.size(); + + // When - refresh + endpointScanningService.refreshEndpoints(); + List refreshedEndpoints = endpointScanningService.getAllEndpoints(); + + // Then - should have same count (since we're not loading new classes in test environment) + assertEquals(initialCount, refreshedEndpoints.size(), + "Refresh should maintain endpoint count in test environment"); + } + + @Test + void testEndpointDescriptorStructure() { + // When + List endpoints = endpointScanningService.getAllEndpoints(); + + // Then - verify structure of endpoints + for (EndpointDescriptor endpoint : endpoints) { + assertNotNull(endpoint.getName(), "Endpoint name should not be null"); + assertNotNull(endpoint.getType(), "Endpoint type should not be null"); + assertNotNull(endpoint.getClassName(), "Endpoint class name should not be null"); + assertNotNull(endpoint.getMethodName(), "Endpoint method name should not be null"); + + assertTrue(endpoint.getType().equals("REST") || endpoint.getType().equals("VERB"), + "Endpoint type should be REST or VERB"); + + if ("REST".equals(endpoint.getType())) { + assertNotNull(endpoint.getHttpMethod(), "REST endpoint should have HTTP method"); + assertNotNull(endpoint.getPath(), "REST endpoint should have path"); + } + + if ("VERB".equals(endpoint.getType()) && endpoint.getMetadata() != null) { + assertTrue(endpoint.getMetadata().containsKey("isAiCallable"), + "VERB endpoint should have isAiCallable metadata"); + } + } + } +} \ No newline at end of file diff --git a/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java b/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java index 5a7feac5..888d4cd9 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/model/security/AccessControlAspect.java @@ -7,7 +7,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; -import io.sentrius.sso.config.ApplicationConfig; +import io.sentrius.sso.config.ApplicationEnvironmentConfig; import io.sentrius.sso.core.annotations.LimitAccess; import io.sentrius.sso.core.config.SystemOptions; import io.sentrius.sso.core.dto.ztat.EndpointRequest; @@ -62,7 +62,7 @@ public class AccessControlAspect { private final ZeroTrustRequestService zeroTrustRequestService; private final ATPLPolicyService atplPolicyService; private final AgentService agentService; - private final ApplicationConfig applicationConfig; + private final ApplicationEnvironmentConfig applicationConfig; private final SystemOptions systemOptions; private final ProvenanceKafkaProducer provenanceKafkaProducer; static List allowedEndpoints = new ArrayList<>(); @@ -167,7 +167,9 @@ public void checkLimitAccess(LimitAccess limitAccess) throws SQLException, Gener } - if (isAllowedEndpoint(endpoint)) { + if (imputedAccess(operatingUser, accessAnnotation.applicationAccess(), + ApplicationAccessEnum.CAN_LOG_IN ) || + isAllowedEndpoint(endpoint)) { log.debug("Access Granted to {} at {}", operatingUser, accessAnnotation); return; } else if (null != endpointRequest && containsEndpoint(endpointRequest.getEndpoints(), endpoint)) { @@ -284,6 +286,22 @@ else if (atplPolicyService.allowsEndpoint(policy.get(), endpoint)) { } } + /** + * Returns true if the application access is imputed, meaning that there is only one access type in the + * applicationAccessEnums array and it matches the access parameter. + * @param applicationAccessEnums + * @param access + * @return + */ + private boolean imputedAccess(User operatingUser, ApplicationAccessEnum[] applicationAccessEnums, + ApplicationAccessEnum access ) + throws SQLException, GeneralSecurityException { + if (applicationAccessEnums != null && applicationAccessEnums.length == 1) { + return applicationAccessEnums[0] == access && canAccess(operatingUser, access); + } + return false; + } + private boolean containsEndpoint(List endpoints, String endpoint) { for (String allowedEndpoint : endpoints) { log.debug("Checking if endpoint {} matches {}", endpoint, allowedEndpoint); diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentService.java index e70e06c4..ca397061 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/agents/AgentService.java @@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import io.sentrius.sso.core.dto.AgentCommunicationDTO; @@ -152,9 +153,7 @@ public List getAllAgents(boolean encryptId, List filteredIds, return dtoBuilder.build(); - }) - .toList(); - + }).collect(Collectors.toUnmodifiableList()); } @Async diff --git a/docker/fake-ssh/dev-certs/sentrius-ca.crt b/docker/fake-ssh/dev-certs/sentrius-ca.crt index 2cff46ed..48e05597 100644 --- a/docker/fake-ssh/dev-certs/sentrius-ca.crt +++ b/docker/fake-ssh/dev-certs/sentrius-ca.crt @@ -1 +1,19 @@ -empty file :) \ No newline at end of file +-----BEGIN CERTIFICATE----- +MIIDJTCCAg2gAwIBAgIUDvcfbY2leSeMSnrsrJo2zv0ue/kwDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPc2VudHJpdXMtZGV2LWNhMB4XDTI1MDcwMjIxNDk0MloX +DTI2MDcwMjIxNDk0MlowGjEYMBYGA1UEAwwPc2VudHJpdXMtZGV2LWNhMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0DDoRTDzG6QhQNy9tthyVnFIfBvS +issnqzmpT3XrDdpHT0BIgYIBXWZzQbnhfnM1abCzZtn1ozmzUp84/PJbFYcupjNZ +YUwul0C7BTAm8oN1vhQFbZ6u5iixHUsIbvxNb9IW8Yu003dtP1iXiaMcNZPr9xz7 +INgYigJuoSxtIEuzSBOFNYaXuUfn4r4GIlzF9lDnxeltvQqHTS5j4cdzXdis2e6k +Gy+9OYZZp62WRHWTuhRfOakL1b+voTU8udyIS++mmxXy+AjHlzPuRB8L7wi3HoAM +hBUxCzzJB3+mYNzyOd75bccbiWbMu1ay7WhOxxN2hxWJg+8u05bgAi4EPQIDAQAB +o2MwYTAdBgNVHQ4EFgQU63Fomh1GrbWOavtqFoOhcboMAxMwHwYDVR0jBBgwFoAU +63Fomh1GrbWOavtqFoOhcboMAxMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQELBQADggEBAIu5heYvdV0r33avCMg82txjWvv7mXA5 +8BwU2GUsHqbh/0bS3Sxwc2KRsEh77NcgGo5Lr0gEftTzexGBjCikzhTL1+cWf6Ay +b04NTr7E/EigZlZs/Ceoav5Mw7zElwDhtAr35OoQKTKBUHJgPKUAr5i2Ijwj8HYw +ua/zUKU3RxRiuMTfsZmnzTJEtrTkgMbQN4HNRXTSmVPYNpYhVS+cPM9Xvy5QVaIR +F2RxiywKSSzRY88w2c3sGXjDYs9wmxIWKbjNX51q2ZxwpF9E4c2s48eTjiVS5kVA +/frlToZdVeLORjTtVw24RN4DTqsbOB3SkybylkopF8YjlkvEQNNZZ3c= +-----END CERTIFICATE----- diff --git a/docs/capabilities-api.md b/docs/capabilities-api.md new file mode 100644 index 00000000..51a85cf0 --- /dev/null +++ b/docs/capabilities-api.md @@ -0,0 +1,209 @@ +# Endpoint Capabilities API + +This document describes the new endpoint capabilities API that provides a unified view of all REST endpoints and Verb methods available in the Sentrius system. + +## Overview + +The capabilities API allows AI agents, Python agents, and other systems to dynamically discover what operations are available across the entire Sentrius platform. It scans both: + +- **REST API endpoints** from Spring controllers with `@LimitAccess` annotations +- **Verb methods** from AI agent classes with `@Verb` annotations + +## API Endpoints + +All endpoints are secured with `@LimitAccess` and require appropriate authentication. + +### Get All Endpoints + +``` +GET /api/v1/capabilities/endpoints +``` + +Returns all available endpoints (both REST and Verb) with optional filtering: + +**Query Parameters:** +- `type` (optional): Filter by type (`REST` or `VERB`) +- `requiresAuth` (optional): Filter by authentication requirement (`true` or `false`) + +**Example Response:** +```json +[ + { + "name": "listusers", + "description": "Returns list of users", + "type": "REST", + "httpMethod": "GET", + "path": "/api/v1/users/list", + "className": "io.sentrius.sso.controllers.api.UserApiController", + "methodName": "listusers", + "requiresAuthentication": true, + "accessLimitations": { + "hasLimitAccess": true, + "userAccess": ["CAN_VIEW_USERS"] + }, + "parameters": [...] + }, + { + "name": "assess_ztat_requests", + "description": "Analyzes ztats requests according to the by prompting the LLM", + "type": "VERB", + "className": "io.sentrius.agent.analysis.agents.verbs.AgentVerbs", + "methodName": "analyzeAtatRequests", + "requiresTokenManagement": true, + "metadata": { + "isAiCallable": true, + "outputInterpreter": "...", + "inputInterpreter": "..." + } + } +] +``` + +### Get REST Endpoints Only + +``` +GET /api/v1/capabilities/rest +``` + +Returns only REST API endpoints from Spring controllers. + +### Get Verb Methods Only + +``` +GET /api/v1/capabilities/verbs +``` + +Returns only Verb methods available to AI agents. + +### Refresh Cache + +``` +GET /api/v1/capabilities/refresh +``` + +Forces a refresh of the endpoint cache. Useful during development or after deploying new capabilities. + +**Requires:** `CAN_MANAGE_APPLICATION` access level. + +## Data Model + +### EndpointDescriptor + +| Field | Type | Description | +|-------|------|-------------| +| `name` | String | Endpoint/verb name | +| `description` | String | Human-readable description | +| `type` | String | Either "REST" or "VERB" | +| `httpMethod` | String | HTTP method (GET, POST, etc.) - REST only | +| `path` | String | URL path - REST only | +| `className` | String | Java class containing the method | +| `methodName` | String | Java method name | +| `parameters` | List | Parameter descriptors | +| `accessLimitations` | Object | Access control information | +| `requiresAuthentication` | Boolean | Whether authentication is required | +| `requiresTokenManagement` | Boolean | Whether token management is needed | +| `returnType` | Class | Method return type | +| `metadata` | Map | Additional metadata (mainly for Verbs) | + +### AccessLimitations + +Extracted from `@LimitAccess` annotations: + +| Field | Type | Description | +|-------|------|-------------| +| `hasLimitAccess` | Boolean | Whether @LimitAccess annotation is present | +| `userAccess` | Array | Required user access levels | +| `applicationAccess` | Array | Required application access levels | +| `sshAccess` | Array | Required SSH access levels | +| `allowedIdentityTypes` | Array | Allowed identity types | +| `endpointThreat` | String | Endpoint threat level | + +## Usage Examples + +### For AI Agents + +AI agents can discover available verbs: + +```bash +curl -H "Authorization: Bearer " \ + "/api/v1/capabilities/verbs" +``` + +### For Python Agents + +Python agents can discover all capabilities: + +```python +import requests + +response = requests.get( + "https://sentrius.example.com/api/v1/capabilities/endpoints", + headers={"Authorization": f"Bearer {token}"} +) + +capabilities = response.json() +for cap in capabilities: + print(f"{cap['type']}: {cap['name']} - {cap['description']}") +``` + +### For Dynamic Documentation + +Generate API documentation dynamically: + +```bash +curl -H "Authorization: Bearer " \ + "/api/v1/capabilities/rest?requiresAuth=true" +``` + +## Integration + +### VerbRegistry Integration + +The `VerbRegistry` class now provides additional methods: + +```java +@Autowired +private VerbRegistry verbRegistry; + +// Get all verb descriptors +List allVerbs = verbRegistry.getVerbDescriptors(); + +// Get only AI-callable verbs +List aiVerbs = verbRegistry.getAiCallableVerbDescriptors(); +``` + +### Custom Scanning + +The `EndpointScanningService` can be used directly: + +```java +@Autowired +private EndpointScanningService scanningService; + +// Get all endpoints +List endpoints = scanningService.getAllEndpoints(); + +// Force refresh +scanningService.refreshEndpoints(); +``` + +## Performance + +- Results are cached for performance +- Cache is automatically populated on first request +- Cache can be manually refreshed via the `/refresh` endpoint +- Scanning happens at startup and on-demand + +## Security + +- All endpoints require authentication +- Access limitations from `@LimitAccess` are preserved and exposed +- Endpoint access follows the same security model as the underlying APIs +- The refresh endpoint requires `CAN_MANAGE_APPLICATION` permission + +## Development Notes + +- The scanning covers packages starting with `io.sentrius` +- New controllers and verbs are automatically discovered +- Changes require cache refresh or application restart to be visible +- Comprehensive logging is available for debugging scanning issues \ No newline at end of file diff --git a/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java index a33ad8a6..78393b1a 100644 --- a/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java +++ b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/JiraProxyController.java @@ -8,7 +8,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; -import io.sentrius.sso.config.ApplicationConfig; +import io.sentrius.sso.config.ApplicationEnvironmentConfig; import io.sentrius.sso.core.annotations.LimitAccess; import io.sentrius.sso.core.config.SystemOptions; import io.sentrius.sso.core.controllers.BaseController; @@ -38,7 +38,7 @@ public class JiraProxyController extends BaseController { final KeycloakService keycloakService; final IntegrationSecurityTokenService integrationSecurityTokenService; final RestTemplateBuilder restTemplateBuilder; - final ApplicationConfig applicationConfig; + final ApplicationEnvironmentConfig applicationConfig; Tracer tracer = GlobalOpenTelemetry.getTracer("io.sentrius.sso"); @@ -49,7 +49,7 @@ protected JiraProxyController( KeycloakService keycloakService, IntegrationSecurityTokenService integrationSecurityTokenService, RestTemplateBuilder restTemplateBuilder, - ApplicationConfig applicationConfig + ApplicationEnvironmentConfig applicationConfig ) { super(userService, systemOptions, errorOutputService); this.keycloakService = keycloakService; diff --git a/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/OpenAIProxyController.java b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/OpenAIProxyController.java index 67b65968..4ee6dd07 100644 --- a/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/OpenAIProxyController.java +++ b/integration-proxy/src/main/java/io/sentrius/sso/controllers/api/OpenAIProxyController.java @@ -9,12 +9,10 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; -import io.sentrius.sso.config.ApplicationConfig; -import io.sentrius.sso.core.annotations.LimitAccess; +import io.sentrius.sso.config.ApplicationEnvironmentConfig; import io.sentrius.sso.core.config.SystemOptions; import io.sentrius.sso.core.controllers.BaseController; import io.sentrius.sso.core.integrations.external.ExternalIntegrationDTO; -import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum; import io.sentrius.sso.core.services.ATPLPolicyService; import io.sentrius.sso.core.services.ErrorOutputService; import io.sentrius.sso.core.services.UserService; @@ -60,7 +58,7 @@ public class OpenAIProxyController extends BaseController { final ZeroTrustRequestService ztrService; final IntegrationSecurityTokenService integrationSecurityTokenService; final AgentService agentService; - private final ApplicationConfig applicationConfig; + private final ApplicationEnvironmentConfig applicationConfig; final AgentCommunicationMemoryStore agentCommunicationMemoryStore; final ProvenanceKafkaProducer provenanceKafkaProducer; @@ -72,7 +70,7 @@ protected OpenAIProxyController( SessionTrackingService sessionTrackingService, KeycloakService keycloakService, ATPLPolicyService atplPolicyService, ZeroTrustAccessTokenService ztatService, ZeroTrustRequestService ztrService, IntegrationSecurityTokenService integrationSecurityTokenService, AgentService agentService, - ApplicationConfig applicationConfig, ProvenanceKafkaProducer provenanceKafkaProducer + ApplicationEnvironmentConfig applicationConfig, ProvenanceKafkaProducer provenanceKafkaProducer ) { super(userService, systemOptions, errorOutputService); this.cryptoService = cryptoService; diff --git a/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java b/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java index d0a2a829..f2f3fae7 100644 --- a/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java +++ b/integration-proxy/src/test/java/io/sentrius/sso/controllers/api/JiraProxyControllerTest.java @@ -1,9 +1,7 @@ package io.sentrius.sso.controllers.api; -import io.sentrius.sso.config.ApplicationConfig; +import io.sentrius.sso.config.ApplicationEnvironmentConfig; import io.sentrius.sso.core.config.SystemOptions; -import io.sentrius.sso.core.dto.TicketDTO; -import io.sentrius.sso.core.integrations.external.ExternalIntegrationDTO; import io.sentrius.sso.core.model.security.IntegrationSecurityToken; import io.sentrius.sso.core.model.users.User; import io.sentrius.sso.core.services.ErrorOutputService; @@ -23,11 +21,9 @@ import java.util.Arrays; import java.util.Collections; -import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -52,7 +48,7 @@ class JiraProxyControllerTest { private RestTemplateBuilder restTemplateBuilder; @Mock - private ApplicationConfig applicationConfig; + private ApplicationEnvironmentConfig applicationConfig; @Mock private User mockUser; diff --git a/ops-scripts/local/deploy-helm.sh b/ops-scripts/local/deploy-helm.sh index d05b370e..ecada310 100755 --- a/ops-scripts/local/deploy-helm.sh +++ b/ops-scripts/local/deploy-helm.sh @@ -208,6 +208,7 @@ if [[ -z "$KEYCLOAK_CLIENT_SECRET" ]]; then fi helm upgrade --install sentrius ./sentrius-chart --namespace ${TENANT} \ + --set adminer.enabled=true \ --set tenant=${TENANT} \ --set environment=${ENVIRONMENT} \ --set subdomain="${SUBDOMAIN}" \ diff --git a/sentrius-chart/templates/adminer.yaml b/sentrius-chart/templates/adminer.yaml new file mode 100644 index 00000000..98b60064 --- /dev/null +++ b/sentrius-chart/templates/adminer.yaml @@ -0,0 +1,34 @@ +{{- if .Values.adminer.enabled }} + +apiVersion: v1 +kind: Service +metadata: + name: adminer +spec: + selector: + app: adminer + ports: + - port: 80 + targetPort: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: adminer +spec: + replicas: 1 + selector: + matchLabels: + app: adminer + template: + metadata: + labels: + app: adminer + spec: + containers: + - name: adminer + image: adminer:latest + ports: + - containerPort: 8080 + +{{- end }} \ No newline at end of file diff --git a/sentrius-chart/values.yaml b/sentrius-chart/values.yaml index edaf9f06..b61ccadc 100644 --- a/sentrius-chart/values.yaml +++ b/sentrius-chart/values.yaml @@ -354,4 +354,7 @@ neo4j: resources: {} env: NEO4J_AUTH: "" # To be set via environment variable (e.g., neo4j/your-secure-password) - NEO4J_server_config_strict__validation__enabled: "true" \ No newline at end of file + NEO4J_server_config_strict__validation__enabled: "true" + +adminer: + enabled: false \ No newline at end of file