diff --git a/.gitignore b/.gitignore index e61ed431..bb361de1 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,9 @@ api/node_modules/ # Ignore generated frontend assets api/src/main/resources/static/node/ -api/node \ No newline at end of file +api/node + +.generated/ +# Ignore Generated keys if they exist +docker/dev-certs/sentrius-ca.crt +docker/dev-certs/sentrius-ca.key \ No newline at end of file diff --git a/.local.env b/.local.env index afdec487..12966b43 100644 --- a/.local.env +++ b/.local.env @@ -1,7 +1,8 @@ -SENTRIUS_VERSION=1.1.110 -SENTRIUS_SSH_VERSION=1.1.19 -SENTRIUS_KEYCLOAK_VERSION=1.1.31 -SENTRIUS_AGENT_VERSION=1.1.19 -SENTRIUS_AI_AGENT_VERSION=1.1.34 -LLMPROXY_VERSION=1.0.22 -LAUNCHER_VERSION=1.0.30 +SENTRIUS_VERSION=1.1.158 +SENTRIUS_SSH_VERSION=1.1.32 +SENTRIUS_KEYCLOAK_VERSION=1.1.44 +SENTRIUS_AGENT_VERSION=1.1.31 +SENTRIUS_AI_AGENT_VERSION=1.1.50 +LLMPROXY_VERSION=1.0.40 +LAUNCHER_VERSION=1.0.47 +AGENTPROXY_VERSION=1.0.58 \ No newline at end of file diff --git a/.local.env.bak b/.local.env.bak index ee39e85f..12966b43 100644 --- a/.local.env.bak +++ b/.local.env.bak @@ -1,7 +1,8 @@ -SENTRIUS_VERSION=1.1.110 -SENTRIUS_SSH_VERSION=1.1.19 -SENTRIUS_KEYCLOAK_VERSION=1.1.30 -SENTRIUS_AGENT_VERSION=1.1.19 -SENTRIUS_AI_AGENT_VERSION=1.1.34 -LLMPROXY_VERSION=1.0.22 -LAUNCHER_VERSION=1.0.30 +SENTRIUS_VERSION=1.1.158 +SENTRIUS_SSH_VERSION=1.1.32 +SENTRIUS_KEYCLOAK_VERSION=1.1.44 +SENTRIUS_AGENT_VERSION=1.1.31 +SENTRIUS_AI_AGENT_VERSION=1.1.50 +LLMPROXY_VERSION=1.0.40 +LAUNCHER_VERSION=1.0.47 +AGENTPROXY_VERSION=1.0.58 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cc27c73c..00000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# Use an OpenJDK image as the base -FROM openjdk:17-jdk-slim - -# Set working directory -WORKDIR /app - -# Copy the pre-built API JAR into the container -COPY api/target/sentrius-api-1.0.0-SNAPSHOT.jar /app/sentrius.jar -COPY docker/sentrius/exampleInstallWithTypes.yml /app/exampleInstallWithTypes.yml -COPY docker/sentrius/demoInstaller.yml /app/demoInstaller.yml - -# Expose the port the app runs on -EXPOSE 8080 - -RUN apt-get update && apt-get install -y curl - - -# Command to run the app -CMD ["java", "-jar", "/app/sentrius.jar", "--spring.config.location=/config/api-application.properties", "--dynamic.properties.path=/config/dynamic.properties"] diff --git a/README.md b/README.md index 226050ff..ddc00e61 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ Run the Helm deployment script to deploy Sentrius to your local Kubernetes clust ./ops-scripts/local/deploy-helm.sh - +## If Not using TLS You may wish to forward ports so you can access the services locally. The following commands will forward the necessary ports for the core and api modules: kubectl port-forward -n dev service/sentrius-sentrius 8080:8080 kubectl port-forward -n dev service/sentrius-keycloak 8081:8081 @@ -127,6 +127,15 @@ This will require that you either change the hostnames in the deploy-helm script 127.0.0.1 sentrius-sentrius 127.0.0.1 sentrius-keycloak +## If Using TLS +The deploy script will automatically install cert-manager and create self-signed certificates for the services. You can access the services via: + + https://sentrius-dev.local + https://keycloak-dev.local + +Add these to /etc/hosts file pointing to your minikube or local cluster IP. + + There is a GCP deployment that is hasn't been tested in some time. You can find it in the ops-scripts/gcp directory. You will need to ensure you link to your GKE cluster and have the necessary permissions to deploy resources. @@ -230,20 +239,39 @@ Sentrius provides comprehensive Helm charts for Kubernetes deployment across mul # Build all images ./build-images.sh --all --no-cache -# Deploy to local Kubernetes cluster +# Deploy to local Kubernetes cluster (HTTP) ./ops-scripts/local/deploy-helm.sh -# Forward ports for local access +# OR deploy with TLS enabled for secure transport +./ops-scripts/local/deploy-helm.sh --tls + +# OR deploy with TLS and auto-install cert-manager +./ops-scripts/local/deploy-helm.sh --tls --install-cert-manager + +# Forward ports for local access (HTTP deployment) kubectl port-forward -n dev service/sentrius-sentrius 8080:8080 kubectl port-forward -n dev service/sentrius-keycloak 8081:8081 ``` -Add to `/etc/hosts` for local development: +**For HTTP deployment**, add to `/etc/hosts`: ``` 127.0.0.1 sentrius-sentrius 127.0.0.1 sentrius-keycloak ``` +**For TLS deployment**, add to `/etc/hosts`: +``` +127.0.0.1 sentrius-dev.local +127.0.0.1 keycloak-dev.local +``` + +**TLS Requirements:** +- cert-manager must be installed in your cluster. You can: + - Install manually: `kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml` + - Use auto-install flag: `./ops-scripts/local/deploy-helm.sh --tls --install-cert-manager` +- Access via: `https://sentrius-dev.local` and `https://keycloak-dev.local` +- Self-signed certificates will be automatically generated + #### GCP/GKE Deployment ```bash @@ -277,6 +305,20 @@ ingress: networking.gke.io/managed-certificates: wildcard-cert ``` +**TLS/SSL Configuration:** +```yaml +certificates: + enabled: true # Enable certificate generation + issuer: "letsencrypt-prod" # For AWS/Azure (cert-manager) + +# For local development with self-signed certificates: +environment: local +certificates: + enabled: true +ingress: + tlsEnabled: true +``` + **Agent Configuration:** ```yaml sentriusagent: diff --git a/agent-launcher/src/main/java/io/sentrius/agent/launcher/service/PodLauncherService.java b/agent-launcher/src/main/java/io/sentrius/agent/launcher/service/PodLauncherService.java index 92f0e8f4..e4e38638 100644 --- a/agent-launcher/src/main/java/io/sentrius/agent/launcher/service/PodLauncherService.java +++ b/agent-launcher/src/main/java/io/sentrius/agent/launcher/service/PodLauncherService.java @@ -1,5 +1,6 @@ package io.sentrius.agent.launcher.service; +import io.kubernetes.client.custom.IntOrString; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.apis.CoreV1Api; @@ -71,8 +72,8 @@ public V1Pod launchAgentPod(String agentId, String callbackUrl) throws Exception )) .resources(new V1ResourceRequirements() .limits(Map.of( - "cpu", Quantity.fromString("500m"), - "memory", Quantity.fromString("512Mi") + "cpu", Quantity.fromString("1000m"), + "memory", Quantity.fromString("1Gi") ))) .volumeMounts(List.of( new V1VolumeMount() @@ -91,6 +92,26 @@ public V1Pod launchAgentPod(String agentId, String callbackUrl) throws Exception ))); pod.getSpec().setOverhead(null); - return coreV1Api.createNamespacedPod(agentNamespace, pod).execute(); + var createdPod = coreV1Api.createNamespacedPod(agentNamespace, pod).execute(); + + // Create corresponding service for WebSocket routing + V1Service service = new V1Service() + .metadata(new V1ObjectMeta() + .name("sentrius-agent-" + agentId) + .labels(Map.of("agentId", agentId))) + .spec(new V1ServiceSpec() + .selector(Map.of("agentId", agentId)) + .ports(List.of(new V1ServicePort() + .protocol("TCP") + .port(8090) + .targetPort(new IntOrString(8090)) + )) + .type("ClusterIP") + ); + + log.info("Created service pod: {} and service {}", createdPod, service); + coreV1Api.createNamespacedService(agentNamespace, service).execute(); + + return createdPod; } } diff --git a/agent-proxy/Gruntfile.js b/agent-proxy/Gruntfile.js new file mode 100644 index 00000000..589a78fa --- /dev/null +++ b/agent-proxy/Gruntfile.js @@ -0,0 +1,241 @@ +module.exports = function(grunt) { + grunt.initConfig({ + node: './node_modules', + dest: './src/main/resources/static/node', + destJs: '<%= dest %>/js', + destCss: '<%= dest %>/css', + destFonts: '<%= dest %>/fonts', + clean: { + build: { + src: ['<%= dest %>'] + } + }, + mkdir: { + all: { + options: { + create: ['<%= destCss %>/jquery-ui/images', '<%= destJs %>/jquery-ui/widgets'] + }, + }, + }, + copy: { + main: { + files: [ + { + expand: true, + flatten: true, + src: [ + '<%= node %>/jointjs/dist/joint.js', + ], + dest: '<%= destJs %>/jointjs/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/jointjs/dist/joint.css', + ], + dest: '<%= destCss %>/jointjs/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/chart.js/dist/chart.js', + '<%= node %>/chart.js/dist/chart.umd.js', + ], + dest: '<%= destJs %>/chart.js/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/sockjs-client/dist/sockjs.js', + ], + dest: '<%= destJs %>/sockjs-client/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/google-protobuf/google-protobuf.js', + ], + dest: '<%= destJs %>/google-protobuf/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/datatables/media/css/jquery.dataTables.css', + ], + dest: '<%= destCss %>/datatables/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/datatables/media/images/*', + ], + dest: '<%= destCss %>/images/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/datatables/media/js/jquery.dataTables.js', + ], + dest: '<%= destJs %>/datatables/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/gridstack/dist/gridstack.css', + '<%= node %>/gridstack/dist/gridstack-extra.min.css' + ], + dest: '<%= destCss %>/gridstack/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/gridstack/dist/gridstack-all.js' + ], + dest: '<%= destJs %>/gridstack/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/bootstrap/dist/css/bootstrap*min*', + '<%= node %>/xterm/css/xterm.*', + '<%= node %>/jquery-cron/dist/jquery-cron.css' + ], + dest: '<%= destCss %>/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: [ + '<%= node %>/@fortawesome/fontawesome-free/css/*.css' + ], + dest: '<%= destCss %>/font-awesome/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/@fortawesome/fontawesome-free/webfonts/*', + ], + dest: '<%= dest %>/css/webfonts/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/jquery-ui/themes/base/*'], + dest: '<%= destCss %>/jquery-ui/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/jquery-ui/themes/base/images/*'], + dest: '<%= destCss %>/jquery-ui/images', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/jquery/dist/jquery.min.*', + '<%= node %>/@popperjs/core/dist/umd/popper.min.js', + '<%= node %>/@popperjs/core/dist/umd/popper.min.js.map', + '<%= node %>/bootstrap/dist/js/bootstrap.min.*', + '<%= node %>/bootstrap5-tags/tags.js', + '<%= node %>/floatthead/dist/jquery.floatThead.min.*', + '<%= node %>/xterm/lib/xterm.*', + '<%= node %>/jquery-cron/dist/jquery-cron-min.*', + '<%= node %>/xterm-addon-fit/lib/xterm-addon-fit.*', + '<%= node %>/xterm-addon-search/lib/xterm-addon-search.*', + '<%= node %>/tooltip/dist/Tooltip.min.js', + '<%= node %>/rrule/dist/es5/rrule.js', + ], + dest: '<%= destJs %>/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/@fullcalendar/rrule/index.global.min.js'], + dest: '<%= destJs %>/fullcalendar/rrule/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/@fullcalendar/interaction/index.global.min.js'], + dest: '<%= destJs %>/fullcalendar/interaction/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/@fullcalendar/daygrid/index.global.min.js'], + dest: '<%= destJs %>/fullcalendar/daygrid/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/@fullcalendar/core/index.global.min.js'], + dest: '<%= destJs %>/fullcalendar/core/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/@fullcalendar/timegrid/index.global.min.js'], + dest: '<%= destJs %>/fullcalendar/timegrid/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/jquery-ui/ui/*.*'], + dest: '<%= destJs %>/jquery-ui/', + filter: 'isFile' + }, + { + expand: true, + flatten: true, + src: ['<%= node %>/jquery-ui/ui/widgets/draggable.*', + '<%= node %>/jquery-ui/ui/widgets/droppable.*', + '<%= node %>/jquery-ui/ui/widgets/datepicker.*', + '<%= node %>/jquery-ui/ui/widgets/resizable.*', + '<%= node %>/jquery-ui/ui/widgets/selectable.*', + '<%= node %>/jquery-ui/ui/widgets/sortable.*', + '<%= node %>/jquery-ui/ui/widgets/mouse.*' + ], + dest: '<%= destJs %>/jquery-ui/widgets', + filter: 'isFile' + } + ] + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-mkdir'); + grunt.loadNpmTasks('grunt-contrib-copy'); + + grunt.registerTask('default', ['clean','mkdir','copy']); +}; \ No newline at end of file diff --git a/agent-proxy/JIRA_PROXY_API.md b/agent-proxy/JIRA_PROXY_API.md new file mode 100644 index 00000000..cd1958f4 --- /dev/null +++ b/agent-proxy/JIRA_PROXY_API.md @@ -0,0 +1,163 @@ +# JIRA Proxy API Documentation + +The JIRA Proxy Controller provides a secure interface to interact with JIRA instances through the Sentrius platform. It mirrors key JIRA REST API endpoints while maintaining the platform's authentication and authorization mechanisms. + +## Overview + +The JIRA proxy is implemented in the `integration-proxy` module and provides authenticated access to JIRA functionality for agents and compliance tools. It follows the same security patterns as the existing OpenAI proxy. + +## Authentication + +All endpoints require: +- Valid JWT token in the `Authorization` header (format: `Bearer `) +- User must have `CAN_LOG_IN` application access +- At least one JIRA integration must be configured in the system + +## Endpoints + +### 1. Search Issues + +**GET** `/api/v1/jira/rest/api/3/search` + +Search for JIRA issues using JQL or simple text queries. + +**Parameters:** +- `jql` (optional): JIRA Query Language string +- `query` (optional): Simple text search query + +**Example:** +```bash +curl -X GET \ + "https://your-instance/api/v1/jira/rest/api/3/search?query=bug" \ + -H "Authorization: Bearer " +``` + +**Response:** Array of TicketDTO objects containing issue information. + +### 2. Get Issue + +**GET** `/api/v1/jira/rest/api/3/issue/{issueKey}` + +Retrieve information about a specific JIRA issue. + +**Parameters:** +- `issueKey` (path): JIRA issue key (e.g., "PROJECT-123") + +**Example:** +```bash +curl -X GET \ + "https://your-instance/api/v1/jira/rest/api/3/issue/PROJECT-123" \ + -H "Authorization: Bearer " +``` + +**Response:** Issue status information. + +### 3. Add Comment + +**POST** `/api/v1/jira/rest/api/3/issue/{issueKey}/comment` + +Add a comment to a JIRA issue. + +**Parameters:** +- `issueKey` (path): JIRA issue key +- Request body: Comment object with `text` or `body` field + +**Example:** +```bash +curl -X POST \ + "https://your-instance/api/v1/jira/rest/api/3/issue/PROJECT-123/comment" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"text": "This is a comment from the compliance agent"}' +``` + +**Response:** Success/failure message. + +### 4. Assign Issue + +**PUT** `/api/v1/jira/rest/api/3/issue/{issueKey}/assignee` + +Assign a JIRA issue to a user. + +**Parameters:** +- `issueKey` (path): JIRA issue key +- Request body: Assignee object with `accountId` field + +**Example:** +```bash +curl -X PUT \ + "https://your-instance/api/v1/jira/rest/api/3/issue/PROJECT-123/assignee" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"accountId": "user-account-id"}' +``` + +**Response:** HTTP 204 (No Content) on success. + +## Configuration + +### JIRA Integration Setup + +Before using the proxy, ensure a JIRA integration is configured: + +1. Use the existing `/api/v1/integrations/jira/add` endpoint to add JIRA integration +2. Provide required fields: `baseUrl`, `username`, `apiToken` + +### Security Model + +The proxy uses the existing security infrastructure: +- JWT validation through Keycloak +- User authentication via `BaseController.getOperatingUser()` +- Access control through `@LimitAccess` annotations +- OpenTelemetry tracing for monitoring + +## Implementation Details + +### Error Handling + +- **401 Unauthorized**: Invalid or missing JWT token +- **404 Not Found**: No JIRA integration configured +- **400 Bad Request**: Missing required parameters +- **500 Internal Server Error**: JIRA operation failed + +### Integration Token Selection + +Currently, the proxy uses the first available JIRA integration found for the connection type "jira". In production environments, you may want to extend this to allow users to specify which integration to use. + +### Tracing + +All operations are traced using OpenTelemetry with the tracer name `io.sentrius.sso`. Trace spans include: +- Operation type (search, get-issue, add-comment, assign-issue) +- Query parameters +- Result counts +- Success/failure status + +## Future Enhancements + +1. **Multi-integration Support**: Allow specifying which JIRA instance to use +2. **Enhanced JQL Support**: Full JQL query validation and optimization +3. **Bulk Operations**: Support for bulk issue updates and assignments +4. **Webhook Support**: Real-time notifications from JIRA +5. **Custom Field Support**: Access to JIRA custom fields +6. **Project-specific Operations**: Project creation, configuration management + +## Usage with Compliance Agents + +This proxy is designed to support compliance agents that need to: +- Search for compliance-related issues +- Create comments with compliance findings +- Assign issues to appropriate team members +- Track compliance status across JIRA projects + +Example agent workflow: +1. Search for open compliance issues: `GET /api/v1/jira/rest/api/3/search?jql=project = COMPLIANCE AND status = Open` +2. Add compliance assessment: `POST /api/v1/jira/rest/api/3/issue/COMPLIANCE-123/comment` +3. Assign for remediation: `PUT /api/v1/jira/rest/api/3/issue/COMPLIANCE-123/assignee` + +## Testing + +Comprehensive test coverage is provided in `JiraProxyControllerTest.java`, including: +- Authentication validation +- Authorization checks +- Error handling scenarios +- Request/response validation \ No newline at end of file diff --git a/agent-proxy/dynamic.properties b/agent-proxy/dynamic.properties new file mode 100644 index 00000000..bf3b6e39 --- /dev/null +++ b/agent-proxy/dynamic.properties @@ -0,0 +1,28 @@ +#Thu Nov 28 06:34:19 EST 2024 +auditorClass=io.sentrius.sso.automation.auditing.AccessTokenAuditor +twopartyapproval.option.LOCKING_SYSTEMS=true +requireProfileForLogin=true +maxJitDurationMs=1440000 +sshEnabled=true +systemLogoName=Sentrius +AccessTokenAuditor.rule.4=io.sentrius.sso.automation.auditing.rules.OpenAISessionRule;Malicious AI Monitoring +AccessTokenAuditor.rule.5=io.sentrius.sso.automation.auditing.rules.TwoPartyAIMonitor;AI Second Party Monitor +AccessTokenAuditor.rule.6=io.sentrius.sso.automation.auditing.rules.SudoApproval;Sudo Approval +allowProxies=true +AccessTokenAuditor.rule.2=io.sentrius.sso.automation.auditing.rules.DeletePrevention;Delete Prevention +AccessTokenAuditor.rule.3=io.sentrius.sso.automation.auditing.rules.TwoPartySessionRule;Require Second Party Monitoring +AccessTokenAuditor.rule.0=io.sentrius.sso.automation.auditing.rules.CommandEvaluator;Restricted Commands +terminalsInNewTab=false +auditFlushIntervalMs=5000 +AccessTokenAuditor.rule.1=io.sentrius.sso.automation.auditing.rules.AllowedCommandsRule;Approved Commands +knownHostsPath=/home/marc/.ssh/known_hosts +systemLogoPathLarge=/images/sentrius_large.jpg +maxJitUses=1 +systemLogoPathSmall=/images/sentrius_small.png +enableInternalAudit=true +twopartyapproval.require.explanation.LOCKING_SYSTEMS=false +canApproveOwnJITs=false +allowUploadSystemConfiguration = true +yamlConfigurationPath=/mnt/ExtraDrive/repos/Sentrius/docker/sentrius/demoInstaller.yml +# defines the policy mapping for the java agents +agents.trust.policy.mapping.1=java-agents:/mnt/ExtraDrive/repos/Sentrius/docker/sentrius/java-agents.yaml \ No newline at end of file diff --git a/agent-proxy/package.json b/agent-proxy/package.json new file mode 100644 index 00000000..b2976d2c --- /dev/null +++ b/agent-proxy/package.json @@ -0,0 +1,45 @@ +{ + "name": "secureshellops", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/phrocker/SSO.git" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "latest", + "@fullcalendar/core": "^6.1.12", + "@fullcalendar/daygrid": "^6.1.12", + "@fullcalendar/interaction": "^6.1.12", + "@fullcalendar/rrule": "^6.1.12", + "@fullcalendar/timegrid": "^6.1.12", + "@popperjs/core": "^2.11.8", + "@stomp/stompjs": "^7.0.0", + "bootstrap": "^5.3.1", + "bootstrap5-tags": "latest", + "browserify": "^17.0.1", + "datatables": "^1.10.18", + "floatthead": "^2.2.5", + "google-protobuf": "^3.21.4", + "gridstack": "^11.0.1", + "jquery": "^3.7.0", + "jquery-cron": "latest", + "jquery-ui": "^1.13.2", + "rrule": "^2.8.1", + "sockjs-client": "^1.6.1", + "tooltip": "^1.6.1", + "xterm": "5.3.0", + "xterm-addon-fit": "^0.7.0", + "xterm-addon-search": "^0.13.0", + "chart.js": "^4.4.6", + "jointjs": "^3.7.7", + "lodash": "^4.17.21", + "backbone": "^1.6.0" + }, + "devDependencies": { + "grunt": "^1.6.1", + "grunt-cli": "^1.4.3", + "grunt-contrib-clean": "^2.0.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-mkdir": "^1.1.0" + } +} diff --git a/agent-proxy/pom.xml b/agent-proxy/pom.xml new file mode 100644 index 00000000..037a857a --- /dev/null +++ b/agent-proxy/pom.xml @@ -0,0 +1,292 @@ + + 4.0.0 + + + io.sentrius + sentrius + 1.0.0-SNAPSHOT + + + sentrius-agent-proxy + + + + UTF-8 + 17 + 17 + + + + + + + io.sentrius + sentrius-core + 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + + + io.sentrius + sentrius-dataplane + 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + + + io.sentrius + provenance-core + 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + + + io.sentrius + llm-dataplane + 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + provided + + + io.micrometer + micrometer-observation + compile + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-devtools + true + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-test + test + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.flywaydb + flyway-core + + + + com.h2database + h2 + runtime + + + com.google.protobuf + protobuf-java + + + org.postgresql + postgresql + + + org.flywaydb + flyway-database-postgresql + + + org.mockito + mockito-junit-jupiter + test + + + org.junit.jupiter + junit-jupiter + test + + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + + + io.opentelemetry.instrumentation + opentelemetry-spring-boot-starter + + + + + io.opentelemetry + opentelemetry-exporter-otlp + + + + + + + + com.github.eirslett + frontend-maven-plugin + 1.13.4 + + + install node and npm + + install-node-and-npm + + generate-resources + + + npm install + + npm + + generate-resources + + clean-install + + + + grunt build + + grunt + + generate-resources + + + + v16.13.1 + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + repackage + + + + + + maven-compiler-plugin + + + + maven-clean-plugin + + + maven-resources-plugin + + + maven-surefire-plugin + + + maven-jar-plugin + + + maven-install-plugin + + + maven-deploy-plugin + + + maven-site-plugin + + + maven-project-info-reports-plugin + + + + + diff --git a/agent-proxy/src/main/java/io/sentrius/sso/AgentProxyApplication.java b/agent-proxy/src/main/java/io/sentrius/sso/AgentProxyApplication.java new file mode 100644 index 00000000..654b69d7 --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/AgentProxyApplication.java @@ -0,0 +1,16 @@ +package io.sentrius.sso; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication(scanBasePackages = {"io.sentrius.sso", "org.springframework.security.oauth2.jwt"}) +//@ComponentScan(basePackages = {"io.sentrius.sso"}) +@EnableJpaRepositories(basePackages = {"io.sentrius.sso.core.data", "io.sentrius.sso.core.repository"}) +@EntityScan(basePackages = "io.sentrius.sso.core.model") // Replace with your actual entity package +public class AgentProxyApplication { + public static void main(String[] args) { + SpringApplication.run(AgentProxyApplication.class, args); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/sentrius/sso/websocket/AgentHandshakeInterceptor.java b/agent-proxy/src/main/java/io/sentrius/sso/config/AgentHandshakeInterceptor.java similarity index 100% rename from api/src/main/java/io/sentrius/sso/websocket/AgentHandshakeInterceptor.java rename to agent-proxy/src/main/java/io/sentrius/sso/config/AgentHandshakeInterceptor.java diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/AgentWebSocketProxyHandler.java b/agent-proxy/src/main/java/io/sentrius/sso/config/AgentWebSocketProxyHandler.java new file mode 100644 index 00000000..ecbcfa91 --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/AgentWebSocketProxyHandler.java @@ -0,0 +1,178 @@ +package io.sentrius.sso.config; + +import java.net.URI; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import io.sentrius.sso.core.services.security.ZeroTrustAccessTokenService; +import io.sentrius.sso.locator.KubernetesAgentLocator; +import io.sentrius.sso.service.ActiveWebSocketSessionManager; +import lombok.RequiredArgsConstructor; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.NettyDataBuffer; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketSession; +import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; + +import io.sentrius.sso.core.services.security.CryptoService; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Component +@Slf4j +@RequiredArgsConstructor +public class AgentWebSocketProxyHandler implements WebSocketHandler { + + private final KubernetesAgentLocator agentLocator; + private final ActiveWebSocketSessionManager sessionManager; + private final ZeroTrustAccessTokenService ztatService; + + @Override + public Mono handle(WebSocketSession clientSession) { + try { + URI uri = clientSession.getHandshakeInfo().getUri(); + var queryParams = parseQueryParams(uri); + + String agentHost = queryParams.get("phost"); + if (agentHost.startsWith("wss://")) { + agentHost = agentHost.replace("wss://", "ws://"); + } + String sessionId = queryParams.get("sessionId"); + sessionId = sessionId.replace(" ","+"); + String chatGroupId = queryParams.get("chatGroupId"); + chatGroupId = chatGroupId.replace(" ","+"); + String ztat = queryParams.get("ztat"); + String ztatForChat = queryParams.get("jwt"); + + + if (ztatForChat != null && !ztatForChat.isEmpty()) { + log.info("ZTAT for chat: {}", ztatForChat); + if ( !ztatService.isOpsActive(ztatForChat) ){ + log.info("Invalid ZTAT token for sessionId: {}, ztat: {}", sessionId, ztatForChat); + //return Mono.error(new RuntimeException("Invalid ZTAT token for sessionId: " + sessionId)); + } + ztatService.incremenOpsUses(ztatForChat); + + } else { + log.info("Invalid ZTAT token for sessionId: {}", sessionId); + return Mono.error(new RuntimeException("Invalid ZTAT token") ); + } + log.info("Handling WebSocket connection for host: {}, sessionId: {}, chatGroupId: {}, ztat: {}", + agentHost, sessionId, chatGroupId, ztat); + + URI agentUri = agentLocator.resolveWebSocketUri(agentHost, sessionId, chatGroupId, ztat); + + log.info("Resolved agent URI: {}", agentUri); + + ReactorNettyWebSocketClient proxyClient = new ReactorNettyWebSocketClient(); + + sessionManager.register(sessionId, clientSession); + String finalSessionId = sessionId; + + return proxyClient.execute(agentUri, agentSession -> { + log.info("Proxy client connected to agent"); + + Mono clientToAgent = clientSession.receive() + .doOnSubscribe(s -> log.info("client -> agent subscribed")) + .doOnNext(m -> log.debug("client -> agent: message type {}", m.getType())) + .flatMap(webSocketMessage -> { + if (webSocketMessage.getType() == WebSocketMessage.Type.TEXT) { + return Mono.just(agentSession.textMessage(webSocketMessage.getPayloadAsText())); + } else { + log.warn("Client sent a BINARY message to agent. Agent expects TEXT. Converting to Base64 Text."); + return DataBufferUtils.join(Mono.just(webSocketMessage.getPayload())) + .map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + return agentSession.textMessage(Base64.getEncoder().encodeToString(bytes)); + }); + } + }) + .as(agentSession::send) + .doOnSuccess(aVoid -> log.info("client -> agent completed gracefully")) // Corrected for Mono + .doOnError(e -> log.error("Error in client -> agent stream", e)) + .onErrorResume(e -> { + log.error("Client to agent stream error, closing client session.", e); + return clientSession.close().then(Mono.empty()); + }) + .doFinally(sig -> log.info("Client to agent stream finalized: {}", sig)); + +// Stream from agent to client (Agent -> Proxy -> Client) + Mono agentToClient = agentSession.receive() + .doOnSubscribe(s -> log.info("agent -> client subscribed")) + .doOnNext(m -> log.debug("agent -> client: message type {}", m.getType())) + .flatMap(webSocketMessage -> { + if (webSocketMessage.getType() == WebSocketMessage.Type.TEXT) { + return Mono.just(clientSession.textMessage(webSocketMessage.getPayloadAsText())); + } else { + log.warn("Agent sent a BINARY message to client. Client expects TEXT. Converting to Base64 Text."); + return DataBufferUtils.join(Mono.just(webSocketMessage.getPayload())) + .map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + return clientSession.textMessage(Base64.getEncoder().encodeToString(bytes)); + }); + } + }) + .as(clientSession::send) + .doOnSuccess(aVoid -> log.info("agent -> client completed gracefully")) // Corrected for Mono + .doOnError(e -> log.error("Error in agent -> client stream", e)) + .onErrorResume(e -> { + log.error("Agent to client stream error, closing agent session.", e); + return agentSession.close().then(Mono.empty()); + }) + .doFinally(sig -> log.info("Agent to client stream finalized: {}", sig)); + + return Mono.when(clientToAgent, agentToClient) + .doOnTerminate(() -> log.info("WebSocket proxy connection terminated (client and agent streams completed/cancelled)")) + .doOnError(e -> log.error("Overall proxy connection failed", e)) + .doFinally(sig -> { + sessionManager.unregister(finalSessionId); + log.info("WebSocket proxy stream closed completely: {}. Final session ID: {}", sig, finalSessionId); + }); + } + ).doOnError(e -> log.error("Failed to establish proxy connection", e)); + + + } catch (Exception ex) { + ex.printStackTrace(); + log.info("WebSocket handshake failed: {}", ex.getMessage()); + return Mono.error(new RuntimeException("WebSocket handshake failed", ex)); + } + } + + + private Map parseQueryParams(URI uri) { + Map queryMap = new HashMap<>(); + String query = uri.getQuery(); + if (query != null) { + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + if (idx > 0 && idx < pair.length() - 1) { + queryMap.put( + decode(pair.substring(0, idx)), + decode(pair.substring(idx + 1)) + ); + } + } + } + return queryMap; + } + + private String decode(String value) { + try { + return java.net.URLDecoder.decode(value, java.nio.charset.StandardCharsets.UTF_8.name()); + } catch (Exception e) { + return ""; + } + } + +} \ No newline at end of file diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/AsyncConfig.java b/agent-proxy/src/main/java/io/sentrius/sso/config/AsyncConfig.java new file mode 100644 index 00000000..67cc9637 --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/AsyncConfig.java @@ -0,0 +1,44 @@ +package io.sentrius.sso.config; + +import io.sentrius.sso.core.services.TerminalService; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Slf4j +@Configuration +@EnableAsync +public class AsyncConfig { + + private ThreadPoolTaskExecutor executor; + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(15); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("SentriusTask-"); + executor.initialize(); + return executor; + } + + @PreDestroy + public void shutdownExecutor() { + if (executor != null) { + executor.shutdown(); + } + log.info("Shutting down executor"); + // Call shutdown on SshListenerService to close streams + terminalService.shutdown(); + } + + @Autowired + private TerminalService terminalService; +} diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/GlobalExceptionHandler.java b/agent-proxy/src/main/java/io/sentrius/sso/config/GlobalExceptionHandler.java new file mode 100644 index 00000000..1787ebef --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/GlobalExceptionHandler.java @@ -0,0 +1,60 @@ +package io.sentrius.sso.config; + +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.sentrius.sso.core.services.ErrorOutputService; +import io.sentrius.sso.core.utils.JsonUtil; +import io.sentrius.sso.core.utils.ZTATUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.server.ResponseStatusException; + +@ControllerAdvice +@RequiredArgsConstructor +public class GlobalExceptionHandler { + + final ErrorOutputService errorOutputService; + + public static String createErrorHash(StackTraceElement[] trace, String t) { + StringBuilder sb = new StringBuilder(); + for (StackTraceElement element : trace) { + sb.append(element.toString()); + } + sb.append(t); + return ZTATUtils.getCommandHash(sb.toString()); + } + + @ExceptionHandler(Throwable.class) + public ResponseEntity handleAllExceptions(Throwable ex) { + ex.printStackTrace(); + + if (ex instanceof ResponseStatusException rse) { + HttpStatus status = (HttpStatus) rse.getStatusCode(); + Map error = new HashMap<>(); + error.put("status", status.value()); + error.put("error", status.getReasonPhrase()); + try{ + + ObjectNode node = (ObjectNode) JsonUtil.MAPPER.readTree(rse.getReason()); + error.put("message", node); + }catch(Exception e){ + error.put("message", rse.getReason()); + } + + return new ResponseEntity<>(error, status); + } + + // Default fallback + Map fallback = new HashMap<>(); + fallback.put("status", 500); + fallback.put("error", "Internal Server Error"); + fallback.put("message", ex.getMessage()); + return new ResponseEntity<>(fallback, HttpStatus.INTERNAL_SERVER_ERROR); + } + + +} \ No newline at end of file diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/HttpsRedirectConfig.java b/agent-proxy/src/main/java/io/sentrius/sso/config/HttpsRedirectConfig.java new file mode 100644 index 00000000..4ffb391d --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/HttpsRedirectConfig.java @@ -0,0 +1,29 @@ +package io.sentrius.sso.config; + +import java.net.URI; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.server.WebFilter; + +@Configuration +public class HttpsRedirectConfig { + + @Value("${https.redirect.enabled:true}") // Default is true + private boolean httpsRedirectEnabled; + + @Bean + public WebFilter httpsRedirectFilter() { + return (exchange, chain) -> { + if (httpsRedirectEnabled && + exchange.getRequest().getHeaders().containsKey("X-Forwarded-Proto") && + "http".equals(exchange.getRequest().getHeaders().getFirst("X-Forwarded-Proto"))) { + URI httpsUri = exchange.getRequest() + .getURI() + .resolve(exchange.getRequest().getURI().toString().replace("http://", "https://")); + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + }; + } +} \ No newline at end of file diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/SchedulingConfig.java b/agent-proxy/src/main/java/io/sentrius/sso/config/SchedulingConfig.java new file mode 100644 index 00000000..8b0da8b3 --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/SchedulingConfig.java @@ -0,0 +1,9 @@ +package io.sentrius.sso.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulingConfig { +} \ No newline at end of file diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java b/agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java new file mode 100644 index 00000000..4595af51 --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java @@ -0,0 +1,81 @@ +package io.sentrius.sso.config; + +import java.util.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.server.resource.authentication.*; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import reactor.core.publisher.Flux; + +@Slf4j +@Configuration +@EnableWebFluxSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + + @Value("${https.required:false}") + private boolean httpsRequired; + + @Value("${agent.api.url:http://localhost:8080}") + private String agentApiUrl; + + @Bean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .pathMatchers("/actuator/**", "/api/v1/agents/ws").permitAll() + .anyExchange().authenticated() + ) + .csrf(Customizer.withDefaults()) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwtSpec -> jwtSpec.jwtAuthenticationConverter(grantedAuthoritiesExtractor())) + ) + .cors(Customizer.withDefaults()); + + if (httpsRequired) { + http.redirectToHttps(Customizer.withDefaults()); + } + + return http.build(); + } + + private ReactiveJwtAuthenticationConverter grantedAuthoritiesExtractor() { + ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter(); + + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + Collection authorities = new JwtGrantedAuthoritiesConverter().convert(jwt); + log.info("JWT Claims: {}", jwt.getClaims()); + + String username = jwt.getClaimAsString("preferred_username"); + String email = jwt.getClaimAsString("email"); + + return Flux.fromIterable(authorities); + }); + + return converter; + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of(agentApiUrl)); + config.setAllowedMethods(List.of("GET", "POST", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/WebSocketRouteConfig.java b/agent-proxy/src/main/java/io/sentrius/sso/config/WebSocketRouteConfig.java new file mode 100644 index 00000000..064b7ddc --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/WebSocketRouteConfig.java @@ -0,0 +1,28 @@ +package io.sentrius.sso.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; +import java.util.Map; + +@Configuration +@RequiredArgsConstructor +public class WebSocketRouteConfig { + + private final AgentWebSocketProxyHandler agentWebSocketProxyHandler; + + @Bean + public HandlerMapping webSocketMapping() { + return new SimpleUrlHandlerMapping(Map.of( + "/api/v1/agents/ws", agentWebSocketProxyHandler + ), -1); // -1 means high priority + } + + @Bean + public WebSocketHandlerAdapter webSocketHandlerAdapter() { + return new WebSocketHandlerAdapter(); + } +} diff --git a/api/src/main/java/io/sentrius/sso/locator/KubernetesAgentLocator.java b/agent-proxy/src/main/java/io/sentrius/sso/locator/KubernetesAgentLocator.java similarity index 100% rename from api/src/main/java/io/sentrius/sso/locator/KubernetesAgentLocator.java rename to agent-proxy/src/main/java/io/sentrius/sso/locator/KubernetesAgentLocator.java diff --git a/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java b/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java new file mode 100644 index 00000000..d2173ae2 --- /dev/null +++ b/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java @@ -0,0 +1,23 @@ +package io.sentrius.sso.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.socket.WebSocketSession; + +@Component +public class ActiveWebSocketSessionManager { + private final Map sessions = new ConcurrentHashMap<>(); + + public void register(String sessionId, WebSocketSession session) { + sessions.put(sessionId, session); + } + + public void unregister(String sessionId) { + sessions.remove(sessionId); + } + + public WebSocketSession get(String sessionId) { + return sessions.get(sessionId); + } +} diff --git a/agent-proxy/src/main/resources/application.properties b/agent-proxy/src/main/resources/application.properties new file mode 100644 index 00000000..d174b384 --- /dev/null +++ b/agent-proxy/src/main/resources/application.properties @@ -0,0 +1,101 @@ +keystore.file=sso.jceks +keystore.password=${KEYSTORE_PASSWORD:keystorepassword} + +keystore.alias=KEYBOX-ENCRYPTION_KEY +keystore.algorithm=AES + +spring.main.web-application-type=servlet +spring.thymeleaf.enabled=true +spring.freemarker.enabled=false + +#flyway configuration +spring.flyway.enabled=true +#spring.flyway.locations=classpath:db/postgres/ # Ensure this path matches your project structure +spring.flyway.baseline-on-migrate=true + +# Thymeleaf settings +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html + +## h2 database + +spring.datasource.url=jdbc:postgresql://home.guard.local:5432/sentrius +spring.datasource.username=postgres +spring.datasource.password=${DATABASE_PASSWORD:password} +spring.datasource.driver-class-name=org.postgresql.Driver + +# Connection pool settings +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.minimum-idle=5 +spring.datasource.hikari.idle-timeout=30000 +spring.datasource.hikari.max-lifetime=1800000 + +# Hibernate settings (optional, for JPA) +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +#spring.datasource.url=jdbc:h2:mem:testdb +#spring.datasource.url=jdbc:h2:file:~/data/testdb +#spring.datasource.driver-class-name=org.h2.Driver +#spring.datasource.username=sa +#spring.datasource.password=${DATABASE_PASSWORD:password} +#spring.jpa.hibernate.ddl-auto=update + + +## Logging + +logging.level.org.springframework.web=INFO +logging.level.org.springframework.security=INFO +logging.level.io.dataguardians=DEBUG + +logging.level.org.thymeleaf=INFO + +spring.thymeleaf.servlet.produce-partial-output-while-processing=false + +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB + + +#Jira integration +#jira.base-url=https://dataguardians-team.atlassian.net/ +#jira.api-token= +server.error.whitelabel.enabled=false + + + +keycloak.realm=sentrius +keycloak.base-url=${KEYCLOAK_BASE_URL:http://localhost:8180} +spring.security.oauth2.client.registration.keycloak.client-secret=${KEYCLOAK_SECRET:defaultSecret} + +spring.security.oauth2.client.registration.keycloak.client-id=sentrius-api +spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.keycloak.redirect-uri=${BASE_URL:http://localhost:8080}/login/oauth2/code/keycloak +spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email + +spring.security.oauth2.resourceserver.jwt.issuer-uri=${KEYCLOAK_BASE_URL:http://localhost:8180}/realms/sentrius +spring.security.oauth2.client.provider.keycloak.issuer-uri=${KEYCLOAK_BASE_URL:http://localhost:8180}/realms/sentrius + +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=always +### change for production environments +#https.required=${HTTP_REQUIRED:true} +server.port=8084 +agent.api.url=http://localhost:8080/ +agent.open.ai.endpoint=http://localhost:8084/ +agent.ai.config=agent-config.yaml +otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://home.guard.local:4317} +otel.traces.exporter=otlp +otel.exporter.otlp.protocol=grpc +otel.metrics.exporter=none +otel.logs.exporter=none +otel.resource.attributes.service.name=integration-proxy +otel.traces.sampler=always_on +otel.exporter.otlp.timeout=10s + + +provenance.kafka.topic=sentrius-provenance +spring.kafka.bootstrap-servers=home.guard.local:9092 +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer + +# Optional: trust package to avoid class cast issues with JSON +spring.kafka.producer.properties.spring.json.trusted.packages=io.sentrius.* diff --git a/agent-proxy/src/main/resources/java-agents.yaml b/agent-proxy/src/main/resources/java-agents.yaml new file mode 100644 index 00000000..4a6af4e7 --- /dev/null +++ b/agent-proxy/src/main/resources/java-agents.yaml @@ -0,0 +1,48 @@ +policy_id: agent-log-access-openai +version: v1 +description: > + Allows an agent to pull terminal logs and access OpenAI credentials. + +trust_score: + minimum: 80 + marginal_threshold: 50 + +capabilities: + - id: terminal-log-access + name: Pull Terminal Logs + endpoints: + - path: /api/v1/terminal/logs + methods: [GET] + actions: + allow: true + purpose: Agent is permitted to retrieve historical terminal logs. + + - id: openai-credential-access + name: OpenAI Credential Access + endpoints: + - path: /api/v1/openai/credentials + methods: [GET] + actions: + allow: true + purpose: Agent is permitted to access credentials for OpenAI model use. + +actions: + on_success: allow + on_marginal: require_ztat + on_failure: deny + +ztat: + provider: ztat-service.internal + ttl: 10m + approved_issuers: + - sentrius + key_binding: RSA2048 + approval_required: true + +behavior: + minimum_positive_runs: 5 + max_incidents: 0 + incident_types: + denylist: + - escalation + - tamper diff --git a/agent-proxy/src/main/resources/sentriussql b/agent-proxy/src/main/resources/sentriussql new file mode 100644 index 00000000..7b584069 --- /dev/null +++ b/agent-proxy/src/main/resources/sentriussql @@ -0,0 +1 @@ +--INSERT INTO host_systems (host_system_id, display_name) VALUES (-1, 'Sentrius'); \ No newline at end of file diff --git a/agent-proxy/src/test/resources/configs/application.properties b/agent-proxy/src/test/resources/configs/application.properties new file mode 100644 index 00000000..f3392b37 --- /dev/null +++ b/agent-proxy/src/test/resources/configs/application.properties @@ -0,0 +1,57 @@ +keystore.file=sso.jceks +keystore.password=keystorepassword + +keystore.alias=KEYBOX-ENCRYPTION_KEY +keystore.algorithm=AES + +spring.main.web-application-type=servlet +spring.thymeleaf.enabled=true +spring.freemarker.enabled=false + +#flyway configuration +spring.flyway.enabled=true +#spring.flyway.locations=classpath:db/postgres/ # Ensure this path matches your project structure +spring.flyway.baseline-on-migrate=true + +# Thymeleaf settings +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html + +## h2 database + +spring.datasource.url=jdbc:postgresql://home.guard.local:5432/sentrius +spring.datasource.username=postgres +spring.datasource.password=password +spring.datasource.driver-class-name=org.postgresql.Driver + +# Connection pool settings +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.minimum-idle=5 +spring.datasource.hikari.idle-timeout=30000 +spring.datasource.hikari.max-lifetime=1800000 + +# Hibernate settings (optional, for JPA) +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +#spring.datasource.url=jdbc:h2:mem:testdb +#spring.datasource.url=jdbc:h2:file:~/data/testdb +#spring.datasource.driver-class-name=org.h2.Driver +#spring.datasource.username=sa +#spring.datasource.password=password +spring.jpa.hibernate.ddl-auto=update + + +## Logging + +logging.level.org.springframework.web=INFO +logging.level.org.springframework.security=INFO +logging.level.io.sentrius=DEBUG + +logging.level.org.thymeleaf=INFO + +spring.thymeleaf.servlet.produce-partial-output-while-processing=false + +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB + +keycloak.realm=sentrius-api \ No newline at end of file diff --git a/agent-proxy/src/test/resources/configs/exampleInstall.yml b/agent-proxy/src/test/resources/configs/exampleInstall.yml new file mode 100644 index 00000000..5b1c7725 --- /dev/null +++ b/agent-proxy/src/test/resources/configs/exampleInstall.yml @@ -0,0 +1,9 @@ +users: + - username: test + name: firstname lastname + +systems: + - displayName: host + sshUser: root + port: 22 + authorizedKeys: ~/.ssh/authorized_keys \ No newline at end of file diff --git a/agent-proxy/src/test/resources/configs/exampleInstallWithTypes.yml b/agent-proxy/src/test/resources/configs/exampleInstallWithTypes.yml new file mode 100644 index 00000000..f418360e --- /dev/null +++ b/agent-proxy/src/test/resources/configs/exampleInstallWithTypes.yml @@ -0,0 +1,88 @@ +userTypes: + - userTypeName: testType + systemAccess: CAN_MANAGE_SYSTEMS + ruleAccess: CAN_DEL_RULES + +users: + - username: test + userId: 6c1980d6-63e5-49e5-bd88-435cb07c9e7f + name: firstname + password: test + authorizationType: + userTypeName: testType + hostGroups: + - displayName: name + - displayName: testGroup + +systems: + - displayName: host + sshUser: marc + port: 22 + host: localhost + authorizedKeys: ~/.ssh/authorized_keys + +## Define groups of users who are assigned to systems +## also entails the configuration that is applied to group +## Some users may not have access to all systems in the group +## or may have restricted accesses to systems. +managementGroups: + - displayName: testGroup + description: test group + systems: + - host + configuration: + configurationName: testConfig + terminalsLocked: false + allowSudo: false + +## Define Automation used within the platform + +systemKeyConfigurations: + - keyConfigurationName: testKey + #can also include paths. Note that this private key should not be used + ## for production purposes. + #pathToPrivateKey: /home/user/.ssh/id_rsa + #pathToPublicKey: /home/user/.ssh/id_rsa.pub + privateKey: | + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCfQpOIo/ + +tvZqi8Yg9rbBEAAAAEAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQDmanENTLBj + xp7ZwbNoNaIU9nl7dIPAm0yyVAKPs3d3GSw6VEAxQIEAbPxygxlQta5YZ6oLKVIA7oUadg + /r7fWo095Ah9IPDvZOgV1Z0LVM/qGSBLFFMIZqyeA+N28M9LfO2mG2vLrvWRv1jbCKUVOg + nWiTisU78ubV26zScTelJh/UQE4bAkdRfs+YfJMvkNm1LpoHIKaaKtSgTrf912L/cIPbW3 + sM5Vi6j7mR0/Ya2+q+uacpTPL4EMRmF8Fg6/F/OcqIjUcsr5FtI6owXu2GWuIeud28DqHV + xXqEZ6ZYR2/J5Y/fOEoTpSJ2fNwvCl1fm2NM8a+Ndngokb40zsn8lDwfslEefRcZfPrDEe + 97s+kmP5ed5s/xpyVAy3YgAF21HUWqTu4GrS34cUqmEZEQb4xTrsNoJ94nQrmEFOlPKKCx + CsNH3Gj4/RiNLxIBKdwoEVOk/S3yHh2U11ngjQEzVwK0n5CbAGik5UKPQ++k1b2gi3Fbth + M58MJgltc/k9MAAAWQIC946mMvCfH+nFtWQwvczqqpT+O2IhosryvLjmOOZECrBCDv2Dgp + 4kUajUSMes4hFgzqYQZtbjs2v3ul8qhGP0BuPrI2oTPA+8/anF/wDoeyxtRE8dRFMjMHy2 + I6/1pQDuHp626qTd6SVa+LzVfxjVjuLJpIWx2fnTPF/TfrzPOE2it3fwfXzjjFBzRDg0jT + seRZF+Wh/yhFCIdwKYA3C2mJAZR13N1H7xFTNr44hAWLEVZ289ix0ltWY4gi3krOqwYn2g + vNyGWz/k+snqjjR2cg7I1eNCsEzRZn1i0HMYlkggB+g+YwmOG4cnFP6RZU1ZK3/SbA5aMB + QzvSyJZPLIsZxdkdb5Z15AbVN2nhszS0egxGWc7rgi//7ftF9jVL7Oz52ADZY29xowcKF+ + hDAfbXXgVJX9+gTVIqwQkgl260+6uv0szQIABoHkvbaf8c+1WlkmR13EcoHHkfqNSlqXNy + Cx3nZ7BasEipx0Rw2WhNV+B6rZ/CW005GwRfwmdo+hkwELvShBOesyD8JJB11M9qHOhG+h + ieQnhXbsmUE83KI1MTUSq3iEtrhiHa+R2mRqUSgPW8AT306HqzritisVAow/GxgcHSeZ5d + i2ofwNU7YatePfOBEB3F/MsBC9alF+yEZUOSXnyB2omCSwMp50pn2XMKg3B8iZxK54QBdd + don9zNf3smP0HZC+w44mgiMwFTf7CfTbGXo1u3DNCDMcaOvq3dBawvTVzCvMAiELnF7WgL + s7NTDFRn43xXEplIvmUz8rdik4XPaL3srCPPS27H+q6WkFBOrFggK4YzvmliDTpAINK4Xf + k7y4+NabpV1mRKGayrkXcXgG4gkkhEr5zwQHBbXVAyZxOEVgLtA0P+2tL7HW9nM6WN4EF/ + A3bF7wuj8ntVByQqnGC/+8ALolJQ3LKJGbnrJgx9a3AMcMd3G0pkwIDEUPWNoyWhhuaj5H + yQLoaNb8xOD9p4LTGixsoMI1CiJCXWJFVMZ+iM8CKWYqNwXZyiULuvx3Qo6Dz0VaZAzMj+ + bOR9rKfzraOqrg0Wcn9znMDDitAJ61CKi1oks/DZ0+OI+k4YaW2z2IywBuGo/h4xxUe33F + R4WPl0XKGmHKerv5iOaLM+4JDJxVudphWBgU63kG4PGqTFqgbdZL48kqO714GzWVENSCPm + gMwWR6pcZ4Bu1SlDYwkPLPpUi3z8/xawrbszfeDL/di0dxKQVR8LmaErKh9iMZNJEyQLCd + NpUuiqYcdPK968xImWjQi5QPou/R2XTwD/CN3P4chjTQTdVkkgxDhMv78b4GyxawH2H6HZ + 5zItiC6kESXa7dSqhvlm6YLLypeGs1qYJyNLuwzyjrHQFCMIVpCK9H8zJmv9cQ1je7xfnq + rKHin47ujda3F/nbdeX7OfZRF1VxV6XtB/gdPLaaUJeNdxIsCGdl/qU6ENS1yy5vAMqRmi + eszOAqlHkomlSb46OGyIe7iiBYnUAiggUOuHf5+sc9DkBofPo0Ikv0H0gjTIFMmbOfuP4k + IlgKE/KtXuqdZeAH8dUYof0qZVnl+ihIbniJBzxMKhog4yoymJrDea/K6c+j9RDTHfb1Ht + fVvLoq/Rx8kaJaCQ/Uou+c9FSEJnPXvrXhXDCgTQgq6NBpKmvahnzcrwlX3ZLqSmSl3UDx + JoEfkmB24pHL5zlkeuqcbVmS2Wpm1OfFq3fk8Gv0orFph6AnUvtM7e1nPhqqo6g9V1zdqD + GZRUwuyhrj9QJlcUJ5NwXZ+10GNg2rqu3C0zPJbAVb8cjivc+plwDK6vbtLpsL6YtVs2km + Ze4KLFjKvirOtrEUcDcoYnF5M8sddInz2o/sntiWDQookn662OOUXPR4rRbC8tD/EsXOKl + 3LOzbzv5dTxnMe4TjoOct1zbsGU= + -----END OPENSSH PRIVATE KEY----- + publicKey: | + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDmanENTLBjxp7ZwbNoNaIU9nl7dIPAm0yyVAKPs3d3GSw6VEAxQIEAbPxygxlQta5YZ6oLKVIA7oUadg/r7fWo095Ah9IPDvZOgV1Z0LVM/qGSBLFFMIZqyeA+N28M9LfO2mG2vLrvWRv1jbCKUVOgnWiTisU78ubV26zScTelJh/UQE4bAkdRfs+YfJMvkNm1LpoHIKaaKtSgTrf912L/cIPbW3sM5Vi6j7mR0/Ya2+q+uacpTPL4EMRmF8Fg6/F/OcqIjUcsr5FtI6owXu2GWuIeud28DqHVxXqEZ6ZYR2/J5Y/fOEoTpSJ2fNwvCl1fm2NM8a+Ndngokb40zsn8lDwfslEefRcZfPrDEe97s+kmP5ed5s/xpyVAy3YgAF21HUWqTu4GrS34cUqmEZEQb4xTrsNoJ94nQrmEFOlPKKCxCsNH3Gj4/RiNLxIBKdwoEVOk/S3yHh2U11ngjQEzVwK0n5CbAGik5UKPQ++k1b2gi3FbthM58MJgltc/k9M= user@public-key + privateKeyPassphrase: password diff --git a/agent-proxy/src/test/resources/configs/exampleWrongInstall.yml b/agent-proxy/src/test/resources/configs/exampleWrongInstall.yml new file mode 100644 index 00000000..5b492604 --- /dev/null +++ b/agent-proxy/src/test/resources/configs/exampleWrongInstall.yml @@ -0,0 +1,7 @@ +employees: + - id: 1 + name: user + age: 30 + position: Software Engineer + address: + street: "street" diff --git a/agent-proxy/src/test/resources/configs/priv_key b/agent-proxy/src/test/resources/configs/priv_key new file mode 100644 index 00000000..a3e41268 --- /dev/null +++ b/agent-proxy/src/test/resources/configs/priv_key @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCfQpOIo/ ++tvZqi8Yg9rbBEAAAAEAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQDmanENTLBj +xp7ZwbNoNaIU9nl7dIPAm0yyVAKPs3d3GSw6VEAxQIEAbPxygxlQta5YZ6oLKVIA7oUadg +/r7fWo095Ah9IPDvZOgV1Z0LVM/qGSBLFFMIZqyeA+N28M9LfO2mG2vLrvWRv1jbCKUVOg +nWiTisU78ubV26zScTelJh/UQE4bAkdRfs+YfJMvkNm1LpoHIKaaKtSgTrf912L/cIPbW3 +sM5Vi6j7mR0/Ya2+q+uacpTPL4EMRmF8Fg6/F/OcqIjUcsr5FtI6owXu2GWuIeud28DqHV +xXqEZ6ZYR2/J5Y/fOEoTpSJ2fNwvCl1fm2NM8a+Ndngokb40zsn8lDwfslEefRcZfPrDEe +97s+kmP5ed5s/xpyVAy3YgAF21HUWqTu4GrS34cUqmEZEQb4xTrsNoJ94nQrmEFOlPKKCx +CsNH3Gj4/RiNLxIBKdwoEVOk/S3yHh2U11ngjQEzVwK0n5CbAGik5UKPQ++k1b2gi3Fbth +M58MJgltc/k9MAAAWQIC946mMvCfH+nFtWQwvczqqpT+O2IhosryvLjmOOZECrBCDv2Dgp +4kUajUSMes4hFgzqYQZtbjs2v3ul8qhGP0BuPrI2oTPA+8/anF/wDoeyxtRE8dRFMjMHy2 +I6/1pQDuHp626qTd6SVa+LzVfxjVjuLJpIWx2fnTPF/TfrzPOE2it3fwfXzjjFBzRDg0jT +seRZF+Wh/yhFCIdwKYA3C2mJAZR13N1H7xFTNr44hAWLEVZ289ix0ltWY4gi3krOqwYn2g +vNyGWz/k+snqjjR2cg7I1eNCsEzRZn1i0HMYlkggB+g+YwmOG4cnFP6RZU1ZK3/SbA5aMB +QzvSyJZPLIsZxdkdb5Z15AbVN2nhszS0egxGWc7rgi//7ftF9jVL7Oz52ADZY29xowcKF+ +hDAfbXXgVJX9+gTVIqwQkgl260+6uv0szQIABoHkvbaf8c+1WlkmR13EcoHHkfqNSlqXNy +Cx3nZ7BasEipx0Rw2WhNV+B6rZ/CW005GwRfwmdo+hkwELvShBOesyD8JJB11M9qHOhG+h +ieQnhXbsmUE83KI1MTUSq3iEtrhiHa+R2mRqUSgPW8AT306HqzritisVAow/GxgcHSeZ5d +i2ofwNU7YatePfOBEB3F/MsBC9alF+yEZUOSXnyB2omCSwMp50pn2XMKg3B8iZxK54QBdd +don9zNf3smP0HZC+w44mgiMwFTf7CfTbGXo1u3DNCDMcaOvq3dBawvTVzCvMAiELnF7WgL +s7NTDFRn43xXEplIvmUz8rdik4XPaL3srCPPS27H+q6WkFBOrFggK4YzvmliDTpAINK4Xf +k7y4+NabpV1mRKGayrkXcXgG4gkkhEr5zwQHBbXVAyZxOEVgLtA0P+2tL7HW9nM6WN4EF/ +A3bF7wuj8ntVByQqnGC/+8ALolJQ3LKJGbnrJgx9a3AMcMd3G0pkwIDEUPWNoyWhhuaj5H +yQLoaNb8xOD9p4LTGixsoMI1CiJCXWJFVMZ+iM8CKWYqNwXZyiULuvx3Qo6Dz0VaZAzMj+ +bOR9rKfzraOqrg0Wcn9znMDDitAJ61CKi1oks/DZ0+OI+k4YaW2z2IywBuGo/h4xxUe33F +R4WPl0XKGmHKerv5iOaLM+4JDJxVudphWBgU63kG4PGqTFqgbdZL48kqO714GzWVENSCPm +gMwWR6pcZ4Bu1SlDYwkPLPpUi3z8/xawrbszfeDL/di0dxKQVR8LmaErKh9iMZNJEyQLCd +NpUuiqYcdPK968xImWjQi5QPou/R2XTwD/CN3P4chjTQTdVkkgxDhMv78b4GyxawH2H6HZ +5zItiC6kESXa7dSqhvlm6YLLypeGs1qYJyNLuwzyjrHQFCMIVpCK9H8zJmv9cQ1je7xfnq +rKHin47ujda3F/nbdeX7OfZRF1VxV6XtB/gdPLaaUJeNdxIsCGdl/qU6ENS1yy5vAMqRmi +eszOAqlHkomlSb46OGyIe7iiBYnUAiggUOuHf5+sc9DkBofPo0Ikv0H0gjTIFMmbOfuP4k +IlgKE/KtXuqdZeAH8dUYof0qZVnl+ihIbniJBzxMKhog4yoymJrDea/K6c+j9RDTHfb1Ht +fVvLoq/Rx8kaJaCQ/Uou+c9FSEJnPXvrXhXDCgTQgq6NBpKmvahnzcrwlX3ZLqSmSl3UDx +JoEfkmB24pHL5zlkeuqcbVmS2Wpm1OfFq3fk8Gv0orFph6AnUvtM7e1nPhqqo6g9V1zdqD +GZRUwuyhrj9QJlcUJ5NwXZ+10GNg2rqu3C0zPJbAVb8cjivc+plwDK6vbtLpsL6YtVs2km +Ze4KLFjKvirOtrEUcDcoYnF5M8sddInz2o/sntiWDQookn662OOUXPR4rRbC8tD/EsXOKl +3LOzbzv5dTxnMe4TjoOct1zbsGU= +-----END OPENSSH PRIVATE KEY----- diff --git a/agent-proxy/src/test/resources/configs/priv_key.pub b/agent-proxy/src/test/resources/configs/priv_key.pub new file mode 100644 index 00000000..be451863 --- /dev/null +++ b/agent-proxy/src/test/resources/configs/priv_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDmanENTLBjxp7ZwbNoNaIU9nl7dIPAm0yyVAKPs3d3GSw6VEAxQIEAbPxygxlQta5YZ6oLKVIA7oUadg/r7fWo095Ah9IPDvZOgV1Z0LVM/qGSBLFFMIZqyeA+N28M9LfO2mG2vLrvWRv1jbCKUVOgnWiTisU78ubV26zScTelJh/UQE4bAkdRfs+YfJMvkNm1LpoHIKaaKtSgTrf912L/cIPbW3sM5Vi6j7mR0/Ya2+q+uacpTPL4EMRmF8Fg6/F/OcqIjUcsr5FtI6owXu2GWuIeud28DqHVxXqEZ6ZYR2/J5Y/fOEoTpSJ2fNwvCl1fm2NM8a+Ndngokb40zsn8lDwfslEefRcZfPrDEe97s+kmP5ed5s/xpyVAy3YgAF21HUWqTu4GrS34cUqmEZEQb4xTrsNoJ94nQrmEFOlPKKCxCsNH3Gj4/RiNLxIBKdwoEVOk/S3yHh2U11ngjQEzVwK0n5CbAGik5UKPQ++k1b2gi3FbthM58MJgltc/k9M= marc@pop-os diff --git a/agent-proxy/sso.jceks b/agent-proxy/sso.jceks new file mode 100644 index 00000000..afb6d9ef Binary files /dev/null and b/agent-proxy/sso.jceks differ diff --git a/agent-proxy/test-jira-proxy.sh b/agent-proxy/test-jira-proxy.sh new file mode 100755 index 00000000..63c27107 --- /dev/null +++ b/agent-proxy/test-jira-proxy.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# JIRA Proxy API Test Script +# This script demonstrates how to interact with the JIRA proxy endpoints + +# Configuration +BASE_URL="http://localhost:8080" +JWT_TOKEN="your-jwt-token-here" + +echo "Testing JIRA Proxy API Endpoints" +echo "=================================" + +# Test 1: Search for issues +echo -e "\n1. Testing search endpoint..." +curl -X GET \ + "${BASE_URL}/api/v1/jira/rest/api/3/search?query=test" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -H "Content-Type: application/json" \ + --silent --show-error || echo "Search test failed (expected if no auth)" + +# Test 2: Get specific issue +echo -e "\n2. Testing get issue endpoint..." +curl -X GET \ + "${BASE_URL}/api/v1/jira/rest/api/3/issue/TEST-123" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -H "Content-Type: application/json" \ + --silent --show-error || echo "Get issue test failed (expected if no auth)" + +# Test 3: Add comment +echo -e "\n3. Testing add comment endpoint..." +curl -X POST \ + "${BASE_URL}/api/v1/jira/rest/api/3/issue/TEST-123/comment" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"text": "Test comment from compliance agent"}' \ + --silent --show-error || echo "Add comment test failed (expected if no auth)" + +# Test 4: Assign issue +echo -e "\n4. Testing assign issue endpoint..." +curl -X PUT \ + "${BASE_URL}/api/v1/jira/rest/api/3/issue/TEST-123/assignee" \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"accountId": "test-user-id"}' \ + --silent --show-error || echo "Assign issue test failed (expected if no auth)" + +echo -e "\n\nAPI endpoint testing completed." +echo "Note: These tests will fail with authentication errors unless you have:" +echo "1. A running instance of the integration-proxy service" +echo "2. A valid JWT token" +echo "3. A configured JIRA integration" + +# Test with invalid token to verify security +echo -e "\nTesting security with invalid token..." +curl -X GET \ + "${BASE_URL}/api/v1/jira/rest/api/3/search?query=test" \ + -H "Authorization: Bearer invalid-token" \ + -H "Content-Type: application/json" \ + --include --silent --show-error | head -1 + +echo -e "\nExpected: HTTP 401 Unauthorized for invalid token" \ No newline at end of file diff --git a/ai-agent/src/main/java/io/sentrius/agent/analysis/agents/verbs/ChatVerbs.java b/ai-agent/src/main/java/io/sentrius/agent/analysis/agents/verbs/ChatVerbs.java index ed003168..992768d9 100644 --- a/ai-agent/src/main/java/io/sentrius/agent/analysis/agents/verbs/ChatVerbs.java +++ b/ai-agent/src/main/java/io/sentrius/agent/analysis/agents/verbs/ChatVerbs.java @@ -2,6 +2,9 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.core.JsonParser; @@ -67,7 +70,7 @@ public TerminalResponse interpretUserData( } String terminalResponse = new String(terminalHelperStream.readAllBytes()); - InputStream is = getClass().getClassLoader().getResourceAsStream(agentConfigFile); + InputStream is = getStream(agentConfigFile); if (is == null) { throw new RuntimeException(agentConfigFile + " not found on classpath"); } @@ -113,7 +116,7 @@ public TerminalResponse interpretUserData( } String terminalResponse = new String(terminalHelperStream.readAllBytes()); - InputStream is = getClass().getClassLoader().getResourceAsStream(agentConfigFile); + InputStream is = getStream(agentConfigFile); if (is == null) { throw new RuntimeException(agentConfigFile + " not found on classpath"); } @@ -172,7 +175,7 @@ public TerminalResponse interpret_plan_response( } String terminalResponse = new String(terminalHelperStream.readAllBytes()); - InputStream is = getClass().getClassLoader().getResourceAsStream(agentConfigFile); + InputStream is = getStream(agentConfigFile); if (is == null) { throw new RuntimeException(agentConfigFile + " not found on classpath"); } @@ -214,4 +217,16 @@ public TerminalResponse interpret_plan_response( } return null; } + + private InputStream getStream(String requestedPath) throws IOException { + Path path = Paths.get(requestedPath); // 🔁 Replace with your actual path + + if (!Files.exists(path)) { + throw new RuntimeException("File not found at path: " + path.toAbsolutePath()); + } + + return Files.newInputStream(path); + + } + } diff --git a/api/src/main/java/io/sentrius/sso/ApiApplication.java b/api/src/main/java/io/sentrius/sso/ApiApplication.java index 8bccc137..ed42a965 100644 --- a/api/src/main/java/io/sentrius/sso/ApiApplication.java +++ b/api/src/main/java/io/sentrius/sso/ApiApplication.java @@ -12,6 +12,13 @@ @EntityScan(basePackages = "io.sentrius.sso.core.model") // Replace with your actual entity package public class ApiApplication { public static void main(String[] args) { - SpringApplication.run(ApiApplication.class, args); + + String user = System.getenv("SPRING_DATASOURCE_USERNAME"); + String pass = System.getenv("SPRING_DATASOURCE_PASSWORD"); + + System.out.println("🔐 SPRING_DATASOURCE_USERNAME = " + user); + System.out.println("🔐 SPRING_DATASOURCE_PASSWORD2 = " + pass); + + SpringApplication.run(ApiApplication.class, args); } } \ No newline at end of file diff --git a/api/src/main/java/io/sentrius/sso/config/AppConfig.java b/api/src/main/java/io/sentrius/sso/config/AppConfig.java index 09466f46..601f040c 100644 --- a/api/src/main/java/io/sentrius/sso/config/AppConfig.java +++ b/api/src/main/java/io/sentrius/sso/config/AppConfig.java @@ -1,10 +1,15 @@ package io.sentrius.sso.config; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration +@Getter @EnableAspectJAutoProxy public class AppConfig { // Your configuration beans + @Value("${agentproxy.externalUrl:}") // Defaults to empty string if not set + private String agentProxyExternalUrl; } diff --git a/api/src/main/java/io/sentrius/sso/controllers/api/AgentApiController.java b/api/src/main/java/io/sentrius/sso/controllers/api/AgentApiController.java index bb1a0ef8..4a5c20fe 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/api/AgentApiController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/api/AgentApiController.java @@ -26,6 +26,7 @@ import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum; import io.sentrius.sso.core.model.sessions.SessionLog; import io.sentrius.sso.core.model.users.User; +import io.sentrius.sso.core.model.zt.OpsZeroTrustAcessTokenRequest; import io.sentrius.sso.core.model.zt.RequestCommunicationLink; import io.sentrius.sso.core.model.zt.ZeroTrustAccessTokenReason; import io.sentrius.sso.core.services.ATPLPolicyService; @@ -38,6 +39,7 @@ import io.sentrius.sso.core.services.security.ZeroTrustAccessTokenService; import io.sentrius.sso.core.services.security.ZeroTrustRequestService; import io.sentrius.sso.core.services.terminal.SessionTrackingService; +import io.sentrius.sso.core.utils.ZTATUtils; import io.sentrius.sso.provenance.ProvenanceEvent; import io.sentrius.sso.provenance.kafka.ProvenanceKafkaProducer; import jakarta.servlet.http.HttpServletRequest; @@ -73,6 +75,7 @@ public class AgentApiController extends BaseController { final ZeroTrustRequestService ztrService; final AgentService agentService; final ProvenanceKafkaProducer provenanceKafkaProducer; + private final ZeroTrustRequestService ztatRequestService; public AgentApiController( UserService userService, @@ -82,7 +85,7 @@ public AgentApiController( CryptoService cryptoService, SessionTrackingService sessionTrackingService, KeycloakService keycloakService, ATPLPolicyService atplPolicyService, ZeroTrustAccessTokenService ztatService, ZeroTrustRequestService ztrService, AgentService agentService, - ProvenanceKafkaProducer provenanceKafkaProducer + ProvenanceKafkaProducer provenanceKafkaProducer, ZeroTrustRequestService ztatRequestService ) { super(userService, systemOptions, errorOutputService); this.auditService = auditService; @@ -94,6 +97,7 @@ public AgentApiController( this.ztrService = ztrService; this.agentService = agentService; this.provenanceKafkaProducer = provenanceKafkaProducer; + this.ztatRequestService = ztatRequestService; } public SessionLog createSession(@RequestParam String username, @RequestParam String ipAddress) { @@ -241,6 +245,36 @@ public ResponseEntity submitProvenance( + } + + @PostMapping("/connect") + @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) + public ResponseEntity createAgentChatRequest( + @RequestParam(name="session_id") String sessionId, + HttpServletRequest request, HttpServletResponse response) throws Exception { + + + var operatingUser = getOperatingUser(request, response ); + + ZeroTrustAccessTokenReason reason = ZeroTrustAccessTokenReason.builder() + .commandNeed("chat_with_agent") + .reasonIdentifier(UUID.randomUUID().toString()) + .build(); + var command = "chat_with_agent"; + var opsRequest = + OpsZeroTrustAcessTokenRequest.builder() + .commandHash(ZTATUtils.getCommandHash(command)) + .command(command).user(operatingUser).ztatReason(reason).build(); + + var ztatRequest = ztatRequestService.createOpsTATRequest(opsRequest); + + + // Approve the request if the agent has an active policy ( and it is known and allowed ). + var admin = createOrGetSystemAdmin(); + var approval = ztatService.approveOpsAccessToken(ztatRequest, admin); + + // return the ztat token to the agent + return ResponseEntity.ok(Map.of("ztat_token", approval.getToken().toString())); } diff --git a/api/src/main/java/io/sentrius/sso/controllers/api/AgentBootstrapController.java b/api/src/main/java/io/sentrius/sso/controllers/api/AgentBootstrapController.java index 7bb2e9cf..b1ae6b6c 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/api/AgentBootstrapController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/api/AgentBootstrapController.java @@ -161,7 +161,7 @@ public ResponseEntity launchPod( zeroTrustClientService.callAuthenticatedPostOnApi(sentriusLauncherService, "agent/launcher/create", registrationDTO); // bootstrap with a default policy - return ResponseEntity.ok("{status: 'success'}"); + return ResponseEntity.ok("{\"status\": \"success\"}"); } diff --git a/api/src/main/java/io/sentrius/sso/controllers/api/ChatApiController.java b/api/src/main/java/io/sentrius/sso/controllers/api/ChatApiController.java index cfb0b520..58388fcd 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/api/ChatApiController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/api/ChatApiController.java @@ -6,14 +6,19 @@ import java.security.Signature; import java.time.ZoneOffset; import java.util.Base64; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import io.sentrius.sso.config.AppConfig; +import io.sentrius.sso.core.annotations.LimitAccess; import io.sentrius.sso.core.config.SystemOptions; import io.sentrius.sso.core.controllers.BaseController; import io.sentrius.sso.core.dto.AgentDTO; import io.sentrius.sso.core.dto.ztat.UserTokenDTO; import io.sentrius.sso.core.dto.ztat.UserTokenResponse; import io.sentrius.sso.core.dto.ztat.ZtatChallengeRequest; +import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum; import io.sentrius.sso.core.services.UserService; import io.sentrius.sso.core.services.agents.AgentService; import io.sentrius.sso.core.services.security.CryptoService; @@ -47,6 +52,7 @@ public class ChatApiController extends BaseController { final ChatLogRepository chatLogRepository; final AgentService agentService; final ZtatTokenService tokenService; + final AppConfig appConfig; public ChatApiController( UserService userService, @@ -54,7 +60,7 @@ public ChatApiController( ErrorOutputService errorOutputService, AuditService auditService, CryptoService cryptoService, SessionTrackingService sessionTrackingService, ChatLogRepository chatLogRepository, - AgentService agentService, ZtatTokenService tokenService + AgentService agentService, ZtatTokenService tokenService, AppConfig appConfig ) { super(userService, systemOptions, errorOutputService); this.auditService = auditService; @@ -63,12 +69,28 @@ public ChatApiController( this.chatLogRepository = chatLogRepository; this.agentService = agentService; this.tokenService = tokenService; + this.appConfig = appConfig; } public SessionLog createSession(@RequestParam String username, @RequestParam String ipAddress) { return auditService.createSession(username, ipAddress); } + @GetMapping("/config") + @LimitAccess(applicationAccess = ApplicationAccessEnum.CAN_LOG_IN) + public Map getChatConfig() { + Map config = new HashMap<>(); + var agentProxyUrl = appConfig.getAgentProxyExternalUrl(); + if (agentProxyUrl != null ) { + config.put("agentProxyUrl", agentProxyUrl); + if (agentProxyUrl.startsWith("http")) { + var wssUrl = agentProxyUrl.replace("http", "ws"); + config.put("agentProxyWsUrl", wssUrl); + } + } + return config; + } + @GetMapping("/history") public ResponseEntity> getChatHistory( HttpServletRequest request, diff --git a/api/src/main/java/io/sentrius/sso/controllers/api/IntegrationApiController.java b/api/src/main/java/io/sentrius/sso/controllers/api/IntegrationApiController.java index 9cab0bf6..5d6f9b43 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/api/IntegrationApiController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/api/IntegrationApiController.java @@ -88,7 +88,7 @@ public ResponseEntity addOpenaiIntegration(HttpServletRe token = integrationService.save(token); // excludes the access token - return ResponseEntity.ok(new ExternalIntegrationDTO(token)); + return ResponseEntity.ok(new ExternalIntegrationDTO(token,false )); } @PostMapping("/jira/delete") diff --git a/api/src/main/java/io/sentrius/sso/controllers/view/DashboardController.java b/api/src/main/java/io/sentrius/sso/controllers/view/DashboardController.java index 7c90f2f4..83a9ab60 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/view/DashboardController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/view/DashboardController.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import io.sentrius.sso.config.AppConfig; import io.sentrius.sso.core.config.SystemOptions; import io.sentrius.sso.core.controllers.BaseController; import io.sentrius.sso.core.dto.AgentDTO; @@ -13,6 +14,7 @@ import io.sentrius.sso.core.services.UserService; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -27,10 +29,15 @@ public class DashboardController extends BaseController { @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri}") private String issuerUri; + @Autowired + final AppConfig appConfig; + protected DashboardController( UserService userService, SystemOptions systemOptions, - ErrorOutputService errorOutputService) { + ErrorOutputService errorOutputService, AppConfig appConfig + ) { super(userService, systemOptions, errorOutputService); + this.appConfig = appConfig; } diff --git a/api/src/main/java/io/sentrius/sso/websocket/AgentWebSocketProxyHandler.java b/api/src/main/java/io/sentrius/sso/websocket/AgentWebSocketProxyHandler.java deleted file mode 100644 index 6cefa16e..00000000 --- a/api/src/main/java/io/sentrius/sso/websocket/AgentWebSocketProxyHandler.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.sentrius.sso.websocket; - -import java.net.URI; -import java.security.GeneralSecurityException; - -import io.sentrius.sso.locator.KubernetesAgentLocator; -import lombok.RequiredArgsConstructor; - -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.socket.WebSocketHandler; -import org.springframework.web.reactive.socket.WebSocketMessage; -import org.springframework.web.reactive.socket.WebSocketSession; -import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; - -import io.sentrius.sso.core.services.security.CryptoService; -import lombok.extern.slf4j.Slf4j; -import reactor.core.publisher.Mono; - -@Component -@Slf4j -@RequiredArgsConstructor -public class AgentWebSocketProxyHandler implements WebSocketHandler { - -private final KubernetesAgentLocator agentLocator; -private final CryptoService cryptoService; - -@Override -public Mono handle(WebSocketSession clientSession) { - try { - String host = (String) clientSession.getAttributes().get("host"); - var decryptedHost = cryptoService.decrypt(host); // Ensure host is decrypted if necessary - String sessionId = (String) clientSession.getAttributes().get("sessionId"); - String chatGroupId = (String) clientSession.getAttributes().get("chatGroupId"); - String ztat = (String) clientSession.getAttributes().get("ztat"); - log.info("Handling WebSocket connection for host: {}, sessionId: {}, chatGroupId: {}, ztat: {}", - decryptedHost, sessionId, chatGroupId, ztat); - URI agentUri = agentLocator.resolveWebSocketUri(decryptedHost, sessionId, chatGroupId, ztat); - - ReactorNettyWebSocketClient proxyClient = new ReactorNettyWebSocketClient(); - - return proxyClient.execute(agentUri, agentSession -> { - // Forward messages from client to agent - Mono clientToAgent = clientSession.receive() - .map(WebSocketMessage::getPayload) - .map(dataBuffer -> agentSession.binaryMessage(factory -> dataBuffer)) - .as(agentSession::send); - - // Forward messages from agent to client - Mono agentToClient = agentSession.receive() - .map(WebSocketMessage::getPayload) - .map(dataBuffer -> clientSession.binaryMessage(factory -> dataBuffer)) - .as(clientSession::send); - - // Run both directions in parallel, complete when both are done - return Mono.zip(clientToAgent, agentToClient).then(); - }); - } catch (GeneralSecurityException ex) { - throw new RuntimeException("Failed to decrypt host", ex); - } -} -} \ No newline at end of file diff --git a/api/src/main/java/io/sentrius/sso/websocket/WebSocketConfig.java b/api/src/main/java/io/sentrius/sso/websocket/WebSocketConfig.java index 961000d1..031f4a89 100644 --- a/api/src/main/java/io/sentrius/sso/websocket/WebSocketConfig.java +++ b/api/src/main/java/io/sentrius/sso/websocket/WebSocketConfig.java @@ -14,7 +14,6 @@ public class WebSocketConfig implements WebSocketConfigurer { private final TerminalWSHandler customWebSocketHandler; private final AuditSocketHandler auditSocketHandler; private final ChatWSHandler chatWSHandler; - private final AgentWebSocketProxyHandler agentProxyHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(customWebSocketHandler, "/api/v1/ssh/terminal/subscribe") diff --git a/api/src/main/java/io/sentrius/sso/websocket/WebSocketRouteConfig.java b/api/src/main/java/io/sentrius/sso/websocket/WebSocketRouteConfig.java deleted file mode 100644 index 62b554aa..00000000 --- a/api/src/main/java/io/sentrius/sso/websocket/WebSocketRouteConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.sentrius.sso.websocket; - -import java.util.HashMap; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.reactive.socket.WebSocketHandler; -import org.springframework.web.reactive.socket.server.WebSocketService; -import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; -import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; -import org.springframework.web.socket.server.support.WebSocketHandlerMapping; - -@Configuration -@RequiredArgsConstructor -public class WebSocketRouteConfig { - - private final AgentWebSocketProxyHandler agentWebSocketProxyHandler; - - @Bean - public WebSocketHandlerMapping webSocketMapping() { - Map map = new HashMap<>(); - map.put("/api/v1/agents/ws/{host}/{sessionId}/{chatGroupId}/{ztat}", agentWebSocketProxyHandler); - - WebSocketHandlerMapping mapping = new WebSocketHandlerMapping(); - mapping.setUrlMap(map); - mapping.setOrder(-1); // Ensure it's picked up early - return mapping; - } - - @Bean - public WebSocketService webSocketService() { - return new HandshakeWebSocketService(new ReactorNettyRequestUpgradeStrategy()); - } -} diff --git a/api/src/main/resources/static/js/add_agent.js b/api/src/main/resources/static/js/add_agent.js index e8d90b4f..52ebf32b 100644 --- a/api/src/main/resources/static/js/add_agent.js +++ b/api/src/main/resources/static/js/add_agent.js @@ -26,21 +26,16 @@ document.addEventListener('DOMContentLoaded', function () { // Optionally close the modal const modalElement = document.getElementById('agentFormModal'); const modal = bootstrap.Modal.getInstance(modalElement); - $("#alertTop").text("User added successfully").show().delay(3000).fadeOut(); + $("#alertTop").text("Agent created").show().delay(3000).fadeOut(); $("#alertTopError").hide(); if (modal) { modal.hide(); } - countUsers(); - const userTable = document.getElementById("user-table"); - if (userTable){ - $('#user-table').DataTable().ajax.reload(null, false); - } }) .catch((error) => { $("#alertTop").hide(); - $("#alertTopError").text("User Not Added").show().delay(3000).fadeOut(); -cd }); + $("#alertTopError").text("Agent not created").show().delay(3000).fadeOut(); + }); }); } }); diff --git a/api/src/main/resources/static/js/chat.js b/api/src/main/resources/static/js/chat.js index 2f57b3c2..ba4450c1 100644 --- a/api/src/main/resources/static/js/chat.js +++ b/api/src/main/resources/static/js/chat.js @@ -32,6 +32,10 @@ class ChatSession { } async connect() { + + const config = await fetch("/api/v1/chat/config").then(r => r.json()); + + console.log("Connecting to chat server at:", this.agentHost); const protocol = location.protocol === "https:" ? "wss" : "ws"; const phost = this.agentHost.replace(/^(https?:\/\/)?/, `${protocol}://`); console.log("Connecting to chat server at:", phost); @@ -70,12 +74,46 @@ class ChatSession { const { jwt } = await ztatResponse.json(); + let ztatForChat = {}; + try { + const response = await fetch( + "/api/v1/agent/connect?session_id=" + this.sessionId, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": csrfToken, + "ZTAT_TOKEN": jwt, + }, + credentials: "same-origin" + } + ); + + if (!response.ok) { + const body = await response.text(); + throw new Error(`Request failed: ${response.status} ${response.statusText}\n${body}`); + } + + ztatForChat = await response.json(); // or .text() depending on response + console.log("ZTAT received:", ztatForChat); + if (!ztatForChat.ztat_token) { + console.error("Failed to retrieve ZTAT"); + return; + } + + + } catch (error) { + console.error("Failed to fetch ZTAT:", error); + } + + // Step 3: Open WebSocket with ZTAT token //const uri = `${phost}/api/v1/chat/attach/subscribe?sessionId=${encodeURIComponent(this.sessionId)}&chatGroupId=${this.chatGroupId}&ztat=${encodeURIComponent(jwt)}`; //const uri = `/api/v1/agents/ws/${encodeURIComponent(phost)}/${encodeURIComponent(this.sessionId)}/${encodeURIComponent(this.chatGroupId)}/${encodeURIComponent(jwt)}`; - const uri = `/api/v1/agents/ws?phost=${encodeURIComponent(phost)}&sessionId=${encodeURIComponent(this.sessionId)}&chatGroupId=${encodeURIComponent(this.chatGroupId)}&jwt=${encodeURIComponent(jwt)}`; + + const uri = config.agentProxyWsUrl + `/api/v1/agents/ws?phost=${encodeURIComponent(phost)}&sessionId=${encodeURIComponent(this.sessionId)}&chatGroupId=${encodeURIComponent(this.chatGroupId)}&ztat=${encodeURIComponent(jwt)}&jwt=${encodeURIComponent(ztatForChat.ztat_token)}`; console.log("Connecting to chat server with ZTAT at:", uri); this.connection = new WebSocket(uri); diff --git a/api/src/main/resources/templates/fragments/chat.html b/api/src/main/resources/templates/fragments/chat.html index c6d0036e..a4204ad1 100644 --- a/api/src/main/resources/templates/fragments/chat.html +++ b/api/src/main/resources/templates/fragments/chat.html @@ -15,9 +15,9 @@ chatFocus = true; enableSessionChat(); } else { - + chatContainer.style.display = (chatContainer.style.display === "none" || chatContainer.style.display === "") ? "flex" : "none"; } - //chatContainer.style.display = (chatContainer.style.display === "none" || chatContainer.style.display === "") ? "flex" : "none"; + // } @@ -471,18 +519,30 @@
AI Admin Operations
Launch Agent - Usage Patterns + + + Usage Patterns
-
+ +
diff --git a/api/src/main/resources/templates/sso/ssh/sso.html b/api/src/main/resources/templates/sso/ssh/sso.html index a13054d6..5e859c91 100755 --- a/api/src/main/resources/templates/sso/ssh/sso.html +++ b/api/src/main/resources/templates/sso/ssh/sso.html @@ -88,7 +88,7 @@ chatFocus = true; enableSessionChat(); } else { - + chatContainer.style.display = (chatContainer.style.display === "none" || chatContainer.style.display === "") ? "flex" : "none"; } //chatContainer.style.display = (chatContainer.style.display === "none" || chatContainer.style.display === "") ? "flex" : "none"; } diff --git a/core/src/main/java/io/sentrius/sso/config/KeycloakConfig.java b/core/src/main/java/io/sentrius/sso/config/KeycloakConfig.java index f09bceca..74da4987 100644 --- a/core/src/main/java/io/sentrius/sso/config/KeycloakConfig.java +++ b/core/src/main/java/io/sentrius/sso/config/KeycloakConfig.java @@ -8,6 +8,8 @@ import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.KeycloakBuilder; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -17,6 +19,7 @@ @Configuration @Slf4j @Getter +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class KeycloakConfig { @Value("${keycloak.base-url}") diff --git a/core/src/main/java/io/sentrius/sso/config/KeycloakManager.java b/core/src/main/java/io/sentrius/sso/config/KeycloakManager.java index adb4f99a..b906fd79 100644 --- a/core/src/main/java/io/sentrius/sso/config/KeycloakManager.java +++ b/core/src/main/java/io/sentrius/sso/config/KeycloakManager.java @@ -10,11 +10,13 @@ import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.KeycloakBuilder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @Builder @Getter @Setter @Slf4j +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class KeycloakManager { private Keycloak keycloak; diff --git a/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java b/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java index 5183ebae..712dc624 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/agents/AgentClientService.java @@ -21,11 +21,13 @@ import io.sentrius.sso.provenance.ProvenanceEvent; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Slf4j @Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class AgentClientService { final ZeroTrustClientService zeroTrustClientService; diff --git a/core/src/main/java/io/sentrius/sso/core/services/agents/LLMService.java b/core/src/main/java/io/sentrius/sso/core/services/agents/LLMService.java index 02b884f2..1dc53045 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/agents/LLMService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/agents/LLMService.java @@ -3,9 +3,11 @@ import io.sentrius.sso.core.dto.ztat.TokenDTO; import io.sentrius.sso.core.exceptions.ZtatException; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Service; @Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class LLMService { final ZeroTrustClientService zeroTrustClientService; diff --git a/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java b/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java index bfac577d..77bd2f72 100644 --- a/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java +++ b/core/src/main/java/io/sentrius/sso/core/services/agents/ZeroTrustClientService.java @@ -18,6 +18,7 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; @@ -26,6 +27,7 @@ @Slf4j @Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class ZeroTrustClientService { private final KeycloakService keycloakService; 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 f454d135..9a6f08aa 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 @@ -13,6 +13,7 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; @@ -20,6 +21,7 @@ @Slf4j @Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class KeycloakService { private final KeycloakManager keycloak; diff --git a/dataplane/src/main/java/io/sentrius/sso/config/KeycloakAuthSuccessHandler.java b/dataplane/src/main/java/io/sentrius/sso/config/KeycloakAuthSuccessHandler.java index b952aef5..f92d5875 100644 --- a/dataplane/src/main/java/io/sentrius/sso/config/KeycloakAuthSuccessHandler.java +++ b/dataplane/src/main/java/io/sentrius/sso/config/KeycloakAuthSuccessHandler.java @@ -13,12 +13,14 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.keycloak.admin.client.Keycloak; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; @Component @Slf4j +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class KeycloakAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final Keycloak keycloak; diff --git a/dataplane/src/main/java/io/sentrius/sso/config/security/KeycloakUserSyncFilter.java b/dataplane/src/main/java/io/sentrius/sso/config/security/KeycloakUserSyncFilter.java index 356204b4..a57852e5 100644 --- a/dataplane/src/main/java/io/sentrius/sso/config/security/KeycloakUserSyncFilter.java +++ b/dataplane/src/main/java/io/sentrius/sso/config/security/KeycloakUserSyncFilter.java @@ -10,12 +10,14 @@ import jakarta.servlet.ServletResponse; import lombok.extern.slf4j.Slf4j; import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @Component @Slf4j +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class KeycloakUserSyncFilter implements Filter { private final KeycloakService keycloakService; diff --git a/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java b/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java index 35543597..ac4ea59a 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/controllers/BaseController.java @@ -14,12 +14,14 @@ import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpStatus; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.http.ResponseEntity; import org.springframework.security.core.parameters.P; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestParam; @Slf4j +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public abstract class BaseController { diff --git a/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/TicketService.java b/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/TicketService.java index aebafbe9..86c4f8db 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/TicketService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/integrations/ticketing/TicketService.java @@ -7,11 +7,13 @@ import io.sentrius.sso.core.model.users.User; import io.sentrius.sso.core.services.security.IntegrationSecurityTokenService; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.stereotype.Service; @Slf4j @Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class TicketService { 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 fc126e25..e0751fd4 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 @@ -35,6 +35,7 @@ import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @@ -52,6 +53,7 @@ @Component @Slf4j @RequiredArgsConstructor +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class AccessControlAspect { private final UserService userService; diff --git a/dataplane/src/main/java/io/sentrius/sso/core/security/CustomAuthenticationSuccessHandler.java b/dataplane/src/main/java/io/sentrius/sso/core/security/CustomAuthenticationSuccessHandler.java index bb56e0b4..5d1b3e03 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/security/CustomAuthenticationSuccessHandler.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/security/CustomAuthenticationSuccessHandler.java @@ -2,6 +2,7 @@ import io.sentrius.sso.core.services.UserService; import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; @@ -14,6 +15,7 @@ @Component @RequiredArgsConstructor +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final UserService userService; diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/CustomUserDetailsService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/CustomUserDetailsService.java index 0cdd1af7..579d0dd0 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/CustomUserDetailsService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/CustomUserDetailsService.java @@ -6,6 +6,7 @@ import io.sentrius.sso.core.security.CustomUserDetails; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -13,6 +14,7 @@ @Slf4j @Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; // Assuming you have a UserRepository to retrieve user data diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/UserAttributeSyncService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/UserAttributeSyncService.java index e401ee13..05e9e6ee 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/UserAttributeSyncService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/UserAttributeSyncService.java @@ -5,6 +5,7 @@ import io.sentrius.sso.core.repository.UserTypeRepository; import io.sentrius.sso.core.services.security.KeycloakService; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; @@ -12,6 +13,7 @@ @Service @Slf4j +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class UserAttributeSyncService { private final KeycloakService keycloakService; diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java index 00fae487..40a9261d 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/UserService.java @@ -31,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; import org.hibernate.Hibernate; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Service; /** @@ -40,6 +41,7 @@ @Slf4j @Service @RequiredArgsConstructor +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class UserService { @Value("${keycloak.realm}") 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 6f6c7cf8..d994331b 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 @@ -32,6 +32,7 @@ import io.sentrius.sso.provenance.ProvenanceEvent; import io.sentrius.sso.provenance.kafka.ProvenanceKafkaProducer; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpEntity; @@ -46,6 +47,7 @@ @Service @Slf4j +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class AgentService { private final AgentCommunicationRepository agentCommunicationRepository; @@ -129,13 +131,8 @@ public List getAllAgents(boolean encryptId, List filteredIds, dtoBuilder.agentName(heartbeat.getAgentName()); var callback = callbackUrls.get(heartbeat.getAgentId()); if (callback != null) { - try { - - var encryptedCallback = cryptoService.encrypt(callback); // Ensure callback is decrypted - dtoBuilder.agentCallback(encryptedCallback); - } catch (GeneralSecurityException e) { - throw new RuntimeException("Error encrypting callback URL", e); - } + + dtoBuilder.agentCallback(callback); } } if (encryptId){ diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/security/IntegrationSecurityTokenService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/security/IntegrationSecurityTokenService.java index 467a3fd9..48247c9b 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/security/IntegrationSecurityTokenService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/security/IntegrationSecurityTokenService.java @@ -3,6 +3,7 @@ import io.sentrius.sso.core.model.security.IntegrationSecurityToken; import io.sentrius.sso.core.repository.IntegrationSecurityTokenRepository; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,6 +12,7 @@ import java.util.List; import java.util.Optional; +@Slf4j @Service public class IntegrationSecurityTokenService { @@ -26,12 +28,9 @@ public IntegrationSecurityTokenService(IntegrationSecurityTokenRepository reposi @Transactional(readOnly = true) public List findAll() { return repository.findAll().stream().map(token -> { - try { - // decrypt the connecting info - token.setConnectionInfo(cryptoService.decrypt(token.getConnectionInfo())); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } + // decrypt the connecting info + //token.setConnectionInfo(cryptoService.decrypt(token.getConnectionInfo())); + token.setConnectionInfo(token.getConnectionInfo()); return token; }).toList(); } @@ -40,24 +39,20 @@ public List findAll() { public Optional findById(Long id) { var token = repository.findById(id); if (token.isPresent()) { - try { - IntegrationSecurityToken unmanaged = IntegrationSecurityToken.builder() - .id(token.get().getId()) - .connectionType(token.get().getConnectionType()) - .connectionInfo(cryptoService.decrypt(token.get().getConnectionInfo())) - .build(); - // decrypt the connecting info - return Optional.of(unmanaged); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } + IntegrationSecurityToken unmanaged = IntegrationSecurityToken.builder() + .id(token.get().getId()) + .connectionType(token.get().getConnectionType()) + .connectionInfo(token.get().getConnectionInfo()) + .build(); + // decrypt the connecting info + return Optional.of(unmanaged); } return token; } @Transactional public IntegrationSecurityToken save(IntegrationSecurityToken token) throws GeneralSecurityException { - token.setConnectionInfo( cryptoService.encrypt(token.getConnectionInfo())); + // token.setConnectionInfo( cryptoService.encrypt(token.getConnectionInfo())); return repository.save(token); } @@ -69,17 +64,15 @@ public void deleteById(Long id) { @Transactional(readOnly = true) public List findByConnectionType(String connectionType) { return repository.findByConnectionType(connectionType).stream().map(token -> { - try { - // decrypt the connecting info - IntegrationSecurityToken unmanaged = IntegrationSecurityToken.builder() - .id(token.getId()) - .connectionType(token.getConnectionType()) - .connectionInfo(cryptoService.decrypt(token.getConnectionInfo())) - .build(); - return unmanaged; - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } + // decrypt the connecting info + log.info("IntegrationSecurityTokenService.findByConnectionType: {}", token); + IntegrationSecurityToken unmanaged = IntegrationSecurityToken.builder() + .id(token.getId()) + .connectionType(token.getConnectionType()) + .connectionInfo(token.getConnectionInfo()) + // .connectionInfo(cryptoService.decrypt(token.getConnectionInfo())) + .build(); + return unmanaged; }).toList(); } } diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustAccessTokenService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustAccessTokenService.java index 2459add2..907f5762 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustAccessTokenService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustAccessTokenService.java @@ -300,4 +300,37 @@ public ZeroTrustAccessTokenRequest getZtatRequest(Long ztatId) { public void addCommunicationLink(RequestCommunicationLink link) { ztatRequestService.addCommunicationLink(link); } + + public boolean isOpsActive(String ztat) { + var status = ztatRequestService.getOpsTokenStatus(ztat); + if (status.isPresent()) { + var lastUpdated = null != status.get().getZtatRequest().getLastUpdated() ? + status.get().getZtatRequest().getLastUpdated().getTime() : System.currentTimeMillis(); + var currentTime = System.currentTimeMillis(); + if (systemOptions.getMaxJitUses() > 0 + && status.get().getUses() >= systemOptions.getMaxJitUses()) { + log.info("JIT request has reached max uses: " + ztat); + return false; + } else if ((currentTime - lastUpdated) > systemOptions.getMaxJitDurationMs()) { + log.info("JIT request has exceeded time: " + status); + return false; + } else { + return true; + } + }else { + log.info("{} Not present", ztat); + return false; + } + + } + + public boolean incremenOpsUses(String ztat) { + var status = ztatRequestService.getOpsTokenStatus(ztat); + if (status.isPresent()) { + log.info("incrementing uses for ops ztat: " + ztat); + ztatRequestService.incrementAccessTokenUses(status.get()); + return true; + } + return false; + } } diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustRequestService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustRequestService.java index 29fe90e0..bbc95b09 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustRequestService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/security/ZeroTrustRequestService.java @@ -83,6 +83,10 @@ public OpsZeroTrustAcessTokenRequest getOpsAccessTokenRequestById(Long ztatId) { @Transactional public OpsZeroTrustAcessTokenRequest createOpsTATRequest(OpsZeroTrustAcessTokenRequest ztatRequest) { try { + if (ztatRequest.getZtatReason() != null && ztatRequest.getZtatReason().getId() == null) { + // save the reason if it is new + ztatRequest.setZtatReason(ztatReasonRepository.save(ztatRequest.getZtatReason())); + } OpsZeroTrustAcessTokenRequest savedRequest = opsJITRequestRepository.save(ztatRequest); log.info("JITRequest created: {}", savedRequest); return savedRequest; diff --git a/dataplane/src/main/java/io/sentrius/sso/core/utils/ScriptCronTask.java b/dataplane/src/main/java/io/sentrius/sso/core/utils/ScriptCronTask.java index 91905dd8..66c7126e 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/utils/ScriptCronTask.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/utils/ScriptCronTask.java @@ -11,10 +11,12 @@ import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Component; @Slf4j @Component +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class ScriptCronTask implements Job { @Autowired diff --git a/docker/agent-proxy/Dockerfile b/docker/agent-proxy/Dockerfile new file mode 100644 index 00000000..8585f2ae --- /dev/null +++ b/docker/agent-proxy/Dockerfile @@ -0,0 +1,39 @@ +# Use an OpenJDK image as the base +FROM openjdk:17-jdk-slim + +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} + + +# Set working directory +WORKDIR /app + +# Copy the pre-built API JAR into the container +COPY agentproxy.jar /app/agentproxy.jar + + +COPY dev-certs/sentrius-ca.crt /tmp/sentrius-ca.crt + +RUN if [ "$INCLUDE_DEV_CERTS" = "true" ] && [ -f /tmp/sentrius-ca.crt ]; then \ + echo "Importing dev CA cert..." && \ + keytool -import -noprompt -trustcacerts \ + -alias sentrius-local-ca \ + -file /tmp/sentrius-ca.crt \ + -keystore "$JAVA_HOME/lib/security/cacerts" \ + -storepass changeit ; \ + else \ + echo "Skipping cert import"; \ + fi + + +# Expose the port the app runs on +EXPOSE 8080 + +RUN apt-get update && apt-get install -y curl + + +# Command to run the app +CMD ["java", "-jar", "/app/agentproxy.jar", "--spring.config.location=/config/agentproxy-application.properties"] diff --git a/docker/agent-proxy/dev-certs/sentrius-ca.crt b/docker/agent-proxy/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/agent-proxy/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/dev-certs/empty-sentrius-ca.crt b/docker/dev-certs/empty-sentrius-ca.crt new file mode 100644 index 00000000..2cff46ed --- /dev/null +++ b/docker/dev-certs/empty-sentrius-ca.crt @@ -0,0 +1 @@ +empty file :) \ No newline at end of file diff --git a/docker/fake-ssh/dev-certs/sentrius-ca.crt b/docker/fake-ssh/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/fake-ssh/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/integrationproxy/Dockerfile b/docker/integrationproxy/Dockerfile index 4a8774f2..a74b0745 100644 --- a/docker/integrationproxy/Dockerfile +++ b/docker/integrationproxy/Dockerfile @@ -1,12 +1,34 @@ # Use an OpenJDK image as the base FROM openjdk:17-jdk-slim +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} + + # Set working directory WORKDIR /app # Copy the pre-built API JAR into the container COPY llmproxy.jar /app/llmproxy.jar + +COPY dev-certs/sentrius-ca.crt /tmp/sentrius-ca.crt + +RUN if [ "$INCLUDE_DEV_CERTS" = "true" ] && [ -f /tmp/sentrius-ca.crt ]; then \ + echo "Importing dev CA cert..." && \ + keytool -import -noprompt -trustcacerts \ + -alias sentrius-local-ca \ + -file /tmp/sentrius-ca.crt \ + -keystore "$JAVA_HOME/lib/security/cacerts" \ + -storepass changeit ; \ + else \ + echo "Skipping cert import"; \ + fi + + # Expose the port the app runs on EXPOSE 8080 diff --git a/docker/integrationproxy/dev-certs/sentrius-ca.crt b/docker/integrationproxy/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/integrationproxy/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/keycloak/Dockerfile b/docker/keycloak/Dockerfile index 13199433..743eac81 100644 --- a/docker/keycloak/Dockerfile +++ b/docker/keycloak/Dockerfile @@ -1,14 +1,22 @@ FROM quay.io/keycloak/keycloak:24.0.1 as builder +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} + # Enable health and metrics support ENV KC_HEALTH_ENABLED=true ENV KC_METRICS_ENABLED=true - # Configure a database vendor ENV KC_DB=postgres WORKDIR /opt/keycloak +# Copy certs if needed + + RUN /opt/keycloak/bin/kc.sh build FROM quay.io/keycloak/keycloak:24.0.1 diff --git a/docker/keycloak/dev-certs/sentrius-ca.crt b/docker/keycloak/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/keycloak/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/keycloak/process-realm-template.sh b/docker/keycloak/process-realm-template.sh index a88c326d..7369ecc4 100644 --- a/docker/keycloak/process-realm-template.sh +++ b/docker/keycloak/process-realm-template.sh @@ -28,6 +28,7 @@ echo " Output: $REALM_OUTPUT" if command -v openssl >/dev/null 2>&1; then # Use openssl if available export SENTRIUS_API_CLIENT_SECRET="${SENTRIUS_API_CLIENT_SECRET:-default-api-secret-$(openssl rand -hex 16)}" + export SENTRIUS_APROXY_CLIENT_SECRET="${SENTRIUS_APROXY_CLIENT_SECRET:-default-api-secret-$(openssl rand -hex 16)}" export SENTRIUS_LAUNCHER_CLIENT_SECRET="${SENTRIUS_LAUNCHER_CLIENT_SECRET:-default-launcher-secret-$(openssl rand -hex 16)}" export JAVA_AGENTS_CLIENT_SECRET="${JAVA_AGENTS_CLIENT_SECRET:-default-agents-secret-$(openssl rand -hex 16)}" export AI_AGENT_ASSESSOR_CLIENT_SECRET="${AI_AGENT_ASSESSOR_CLIENT_SECRET:-default-assessor-secret-$(openssl rand -hex 16)}" @@ -35,6 +36,7 @@ else # Fallback to simple random generation using date and process ID RAND_SUFFIX=$(date +%s%N | cut -b1-13)$$ export SENTRIUS_API_CLIENT_SECRET="${SENTRIUS_API_CLIENT_SECRET:-default-api-secret-${RAND_SUFFIX}}" + export SENTRIUS_APROXY_CLIENT_SECRET="${SENTRIUS_APROXY_CLIENT_SECRET:-default-api-secret-${RAND_SUFFIX}}" export SENTRIUS_LAUNCHER_CLIENT_SECRET="${SENTRIUS_LAUNCHER_CLIENT_SECRET:-default-launcher-secret-${RAND_SUFFIX}a}" export JAVA_AGENTS_CLIENT_SECRET="${JAVA_AGENTS_CLIENT_SECRET:-default-agents-secret-${RAND_SUFFIX}b}" export AI_AGENT_ASSESSOR_CLIENT_SECRET="${AI_AGENT_ASSESSOR_CLIENT_SECRET:-default-assessor-secret-${RAND_SUFFIX}c}" @@ -45,11 +47,12 @@ fi #export ROOT_URL="${ROOT_URL:-http://localhost:8080}" # set in helm chart #export REDIRECT_URIS="${REDIRECT_URIS:-http://localhost:8080}" -export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID:-}" +export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID:google-oauth-sentrius}" export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET:-}" echo "Substituting environment variables in realm template..." echo " SENTRIUS_API_CLIENT_SECRET: ${SENTRIUS_API_CLIENT_SECRET:0:8}..." +echo " SENTRIUS_APROXY_CLIENT_SECRET: ${SENTRIUS_APROXY_CLIENT_SECRET:0:8}..." echo " SENTRIUS_LAUNCHER_CLIENT_SECRET: ${SENTRIUS_LAUNCHER_CLIENT_SECRET:0:8}..." echo " JAVA_AGENTS_CLIENT_SECRET: ${JAVA_AGENTS_CLIENT_SECRET:0:8}..." echo " AI_AGENT_ASSESSOR_CLIENT_SECRET: ${AI_AGENT_ASSESSOR_CLIENT_SECRET:0:8}..." @@ -57,6 +60,7 @@ echo " AI_AGENT_ASSESSOR_CLIENT_SECRET: ${AI_AGENT_ASSESSOR_CLIENT_SECRET:0:8}. # Use sed to replace environment variables (since envsubst may not be available) # Replace ${VAR} with actual values sed -e "s|\${SENTRIUS_API_CLIENT_SECRET}|${SENTRIUS_API_CLIENT_SECRET}|g" \ + -e "s|\${SENTRIUS_APROXY_CLIENT_SECRET}|${SENTRIUS_APROXY_CLIENT_SECRET}|g" \ -e "s|\${SENTRIUS_LAUNCHER_CLIENT_SECRET}|${SENTRIUS_LAUNCHER_CLIENT_SECRET}|g" \ -e "s|\${JAVA_AGENTS_CLIENT_SECRET}|${JAVA_AGENTS_CLIENT_SECRET}|g" \ -e "s|\${AI_AGENT_ASSESSOR_CLIENT_SECRET}|${AI_AGENT_ASSESSOR_CLIENT_SECRET}|g" \ diff --git a/docker/keycloak/realms/sentrius-realm.json.template b/docker/keycloak/realms/sentrius-realm.json.template index 9bf8da64..ae86dc63 100644 --- a/docker/keycloak/realms/sentrius-realm.json.template +++ b/docker/keycloak/realms/sentrius-realm.json.template @@ -36,6 +36,40 @@ } ] }, + { + "clientId": "sentrius-agent-proxy", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "${SENTRIUS_APROXY_CLIENT_SECRET}", + "rootUrl": "${ROOT_URL}", + "baseUrl": "${ROOT_URL}", + "serviceAccountsEnabled": true, + "redirectUris": [ + "${REDIRECT_URIS}/*" + ], + "protocol": "openid-connect", + "attributes": { + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true" + }, + "protocolMappers": [ + { + "name": "userType", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "userType", + "jsonType.label": "String", + "user.attribute": "userType" + } + } + ] + }, { "clientId": "sentrius-launcher-service", "enabled": true, diff --git a/docker/keycloak/startup.sh b/docker/keycloak/startup.sh index 7dce0e2b..26a3c262 100644 --- a/docker/keycloak/startup.sh +++ b/docker/keycloak/startup.sh @@ -5,4 +5,4 @@ echo "Starting Keycloak with dynamic realm processing..." /opt/keycloak/bin/process-realm-template.sh # Start Keycloak with the processed realm -exec /opt/keycloak/bin/kc.sh start-dev --proxy=passthrough --import-realm --import-realm-overwrite=true --health-enabled=true \ No newline at end of file +exec /opt/keycloak/bin/kc.sh start-dev --proxy=edge --import-realm --import-realm-overwrite=true --health-enabled=true \ No newline at end of file diff --git a/docker/sentrius-agent-proxy/dev-certs/sentrius-ca.crt b/docker/sentrius-agent-proxy/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/sentrius-agent-proxy/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/sentrius-agent/Dockerfile b/docker/sentrius-agent/Dockerfile index d620ffec..3d6ce1ac 100644 --- a/docker/sentrius-agent/Dockerfile +++ b/docker/sentrius-agent/Dockerfile @@ -1,9 +1,29 @@ # Use an OpenJDK image as the base FROM openjdk:17-jdk-slim +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} + # Set working directory WORKDIR /app +# Conditionally import cert +COPY dev-certs/sentrius-ca.crt /tmp/sentrius-ca.crt + +RUN if [ "$INCLUDE_DEV_CERTS" = "true" ] && [ -f /tmp/sentrius-ca.crt ]; then \ + echo "Importing dev CA cert..." && \ + keytool -import -noprompt -trustcacerts \ + -alias sentrius-local-ca \ + -file /tmp/sentrius-ca.crt \ + -keystore "$JAVA_HOME/lib/security/cacerts" \ + -storepass changeit ; \ + else \ + echo "Skipping cert import"; \ + fi + # Copy the pre-built API JAR into the container COPY agent.jar /app/agent.jar diff --git a/docker/sentrius-agent/dev-certs/sentrius-ca.crt b/docker/sentrius-agent/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/sentrius-agent/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/sentrius-ai-agent/Dockerfile b/docker/sentrius-ai-agent/Dockerfile index 131353e1..a0162a53 100644 --- a/docker/sentrius-ai-agent/Dockerfile +++ b/docker/sentrius-ai-agent/Dockerfile @@ -1,9 +1,27 @@ # Use an OpenJDK image as the base FROM openjdk:17-jdk-slim +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} # Set working directory WORKDIR /app +COPY dev-certs/sentrius-ca.crt /tmp/sentrius-ca.crt + +RUN if [ "$INCLUDE_DEV_CERTS" = "true" ] && [ -f /tmp/sentrius-ca.crt ]; then \ + echo "Importing dev CA cert..." && \ + keytool -import -noprompt -trustcacerts \ + -alias sentrius-local-ca \ + -file /tmp/sentrius-ca.crt \ + -keystore "$JAVA_HOME/lib/security/cacerts" \ + -storepass changeit ; \ + else \ + echo "Skipping cert import"; \ + fi + # Copy the pre-built API JAR into the container COPY agent.jar /app/agent.jar diff --git a/docker/sentrius-ai-agent/dev-certs/sentrius-ca.crt b/docker/sentrius-ai-agent/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/sentrius-ai-agent/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/sentrius-launchable-agent/Dockerfile b/docker/sentrius-launchable-agent/Dockerfile index e2a44cbb..0eeb7240 100644 --- a/docker/sentrius-launchable-agent/Dockerfile +++ b/docker/sentrius-launchable-agent/Dockerfile @@ -1,9 +1,28 @@ # Use an OpenJDK image as the base FROM openjdk:17-jdk-slim +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} + # Set working directory WORKDIR /app +COPY dev-certs/sentrius-ca.crt /tmp/sentrius-ca.crt + +RUN if [ "$INCLUDE_DEV_CERTS" = "true" ] && [ -f /tmp/sentrius-ca.crt ]; then \ + echo "Importing dev CA cert..." && \ + keytool -import -noprompt -trustcacerts \ + -alias sentrius-local-ca \ + -file /tmp/sentrius-ca.crt \ + -keystore "$JAVA_HOME/lib/security/cacerts" \ + -storepass changeit ; \ + else \ + echo "Skipping cert import"; \ + fi + # Copy the pre-built API JAR into the container COPY agent.jar /app/agent.jar diff --git a/docker/sentrius-launchable-agent/dev-certs/sentrius-ca.crt b/docker/sentrius-launchable-agent/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/sentrius-launchable-agent/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/sentrius-launcher-service/Dockerfile b/docker/sentrius-launcher-service/Dockerfile index 3e09a92c..c904b798 100644 --- a/docker/sentrius-launcher-service/Dockerfile +++ b/docker/sentrius-launcher-service/Dockerfile @@ -1,9 +1,27 @@ # Use an OpenJDK image as the base FROM openjdk:17-jdk-slim +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} + # Set working directory WORKDIR /app +COPY dev-certs/sentrius-ca.crt /tmp/sentrius-ca.crt + +RUN if [ "$INCLUDE_DEV_CERTS" = "true" ] && [ -f /tmp/sentrius-ca.crt ]; then \ + echo "Importing dev CA cert..." && \ + keytool -import -noprompt -trustcacerts \ + -alias sentrius-local-ca \ + -file /tmp/sentrius-ca.crt \ + -keystore "$JAVA_HOME/lib/security/cacerts" \ + -storepass changeit ; \ + else \ + echo "Skipping cert import"; \ + fi # Copy the pre-built API JAR into the container COPY launcher.jar /app/launcher.jar diff --git a/docker/sentrius-launcher-service/dev-certs/sentrius-ca.crt b/docker/sentrius-launcher-service/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/sentrius-launcher-service/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/docker/sentrius/Dockerfile b/docker/sentrius/Dockerfile new file mode 100644 index 00000000..54e3db34 --- /dev/null +++ b/docker/sentrius/Dockerfile @@ -0,0 +1,38 @@ +# Use an OpenJDK image as the base +FROM openjdk:17-jdk-slim + +# Declare the argument +ARG INCLUDE_DEV_CERTS=false + +# Set environment so you can use in RUN +ENV INCLUDE_DEV_CERTS=${INCLUDE_DEV_CERTS} + +# Set working directory +WORKDIR /app + +COPY dev-certs/sentrius-ca.crt /tmp/sentrius-ca.crt + +RUN if [ "$INCLUDE_DEV_CERTS" = "true" ] && [ -f /tmp/sentrius-ca.crt ]; then \ + echo "Importing dev CA cert..." && \ + keytool -import -noprompt -trustcacerts \ + -alias sentrius-local-ca \ + -file /tmp/sentrius-ca.crt \ + -keystore "$JAVA_HOME/lib/security/cacerts" \ + -storepass changeit ; \ + else \ + echo "Skipping cert import"; \ + fi + +# Copy the pre-built API JAR into the container +COPY sentrius.jar /app/sentrius.jar +COPY exampleInstallWithTypes.yml /app/exampleInstallWithTypes.yml +COPY demoInstaller.yml /app/demoInstaller.yml + +# Expose the port the app runs on +EXPOSE 8080 + +RUN apt-get update && apt-get install -y curl + + +# Command to run the app +CMD ["java", "-jar", "/app/sentrius.jar", "--spring.config.location=/config/api-application.properties", "--dynamic.properties.path=/config/dynamic.properties"] diff --git a/docker/sentrius/dev-certs/sentrius-ca.crt b/docker/sentrius/dev-certs/sentrius-ca.crt new file mode 100644 index 00000000..48e05597 --- /dev/null +++ b/docker/sentrius/dev-certs/sentrius-ca.crt @@ -0,0 +1,19 @@ +-----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/helm-testing.md b/docs/helm-testing.md index 5338a10a..63b4f912 100644 --- a/docs/helm-testing.md +++ b/docs/helm-testing.md @@ -63,7 +63,10 @@ helm lint sentrius-chart-launcher # Test template rendering helm template test sentrius-chart-launcher --dry-run -helm template test sentrius-chart --set environment=local --set ingress.tlsEnabled=false --dry-run +helm template test sentrius-chart --set environment=local --dry-run + +# Test with TLS enabled +helm template test sentrius-chart --set environment=local --set ingress.tlsEnabled=true --set certificates.enabled=true --dry-run # Test with custom values helm template test sentrius-chart-launcher \ @@ -74,21 +77,21 @@ helm template test sentrius-chart-launcher \ ## Known Issues -### Sentrius Chart Ingress Template +### ~~Sentrius Chart Ingress Template~~ (FIXED) + +~~The main `sentrius-chart` has a known issue with the ingress template that causes linting failures. This is a YAML parsing issue in the conditional annotations section. The CI/CD pipeline handles this gracefully:~~ -The main `sentrius-chart` has a known issue with the ingress template that causes linting failures. This is a YAML parsing issue in the conditional annotations section. The CI/CD pipeline handles this gracefully: +**UPDATE**: The ingress template YAML parsing issues have been resolved. The chart now passes linting and supports TLS configuration properly. -- Identifies the issue during linting -- Continues testing other charts -- Provides warnings rather than failing the entire pipeline +### Previous Workarounds (No Longer Needed) -### Workarounds +~~Until the ingress template is fixed, you can:~~ -Until the ingress template is fixed, you can: +1. ~~Use the `sentrius-chart-launcher` which works correctly~~ +2. ~~Test `sentrius-chart` with `ingress.tlsEnabled=false`~~ +3. ~~Use the local deployment scripts which work around the issue~~ -1. Use the `sentrius-chart-launcher` which works correctly -2. Test `sentrius-chart` with `ingress.tlsEnabled=false` -3. Use the local deployment scripts which work around the issue +**All charts now work correctly with TLS enabled or disabled.** ## Chart Testing Best Practices 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 bde0f88f..c3bc851c 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 @@ -90,7 +90,7 @@ protected OpenAIProxyController( @PostMapping("/completions") // require a registered user with an active ztat - @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) + //@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) public ResponseEntity chat(@RequestHeader("Authorization") String token, @RequestHeader("communication_id") String communicationId, HttpServletRequest request, HttpServletResponse response, @@ -113,10 +113,13 @@ public ResponseEntity chat(@RequestHeader("Authorization") String token, if (null == operatingUser) { log.warn("No operating user found for agent: {}", agentId); var username = keycloakService.extractUsername(compactJwt); + log.info("Extracted username from JWT: {}", username); operatingUser = userService.getUserByUsername(username); } + log.info("Operating user: {}", operatingUser); + // we've reached this point, so we can assume the user is allowed to access OpenAI var openAiToken = @@ -197,7 +200,7 @@ public ResponseEntity chat(@RequestHeader("Authorization") String token, @PostMapping("/justify") // require a registered user with an active ztat - @LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) + //@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN}) public ResponseEntity justify(@RequestHeader("Authorization") String token, @RequestHeader("communication_id") String communicationId, HttpServletRequest request, HttpServletResponse response, diff --git a/llm-dataplane/src/main/java/io/sentrius/sso/genai/spring/ai/AgentCommunicationMemoryStore.java b/llm-dataplane/src/main/java/io/sentrius/sso/genai/spring/ai/AgentCommunicationMemoryStore.java index fb616641..c8b22c68 100644 --- a/llm-dataplane/src/main/java/io/sentrius/sso/genai/spring/ai/AgentCommunicationMemoryStore.java +++ b/llm-dataplane/src/main/java/io/sentrius/sso/genai/spring/ai/AgentCommunicationMemoryStore.java @@ -10,11 +10,13 @@ import io.sentrius.sso.core.utils.JsonUtil; import io.sentrius.sso.genai.Message; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.stereotype.Service; @Slf4j @Service +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class AgentCommunicationMemoryStore { private final AgentService service; diff --git a/ops-scripts/base/base.sh b/ops-scripts/base/base.sh new file mode 100644 index 00000000..e157d1e1 --- /dev/null +++ b/ops-scripts/base/base.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +update_env_var() { + local key=$1 + local value=$2 + if grep -q "^$key=" "$ENV_FILE"; then + sed -i "s/^$key=.*/$key=$value/" "$ENV_FILE" + else + echo "$key=$value" >> "$ENV_FILE" + fi +} + +increment_patch_version() { + version=$1 + major=$(echo "$version" | cut -d. -f1) + minor=$(echo "$version" | cut -d. -f2) + patch=$(echo "$version" | cut -d. -f3) + echo "$major.$minor.$((patch + 1))" +} \ No newline at end of file diff --git a/build-images.sh b/ops-scripts/base/build-images.sh similarity index 65% rename from build-images.sh rename to ops-scripts/base/build-images.sh index 75c782b8..1e22afb1 100755 --- a/build-images.sh +++ b/ops-scripts/base/build-images.sh @@ -1,8 +1,13 @@ #!/bin/bash +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +source ${SCRIPT_DIR}/base.sh + # --- Set default mode --- ENV_TARGET="local" # default mode NO_CACHE=false +INCLUDE_DEV_CERTS=false # --- Parse the environment target (local | gcp) --- if [[ "$1" == "local" || "$1" == "gcp" ]]; then @@ -20,23 +25,24 @@ if [[ "$ENV_TARGET" == "local" ]]; then eval $(minikube -p minikube docker-env) fi -# --- Helpers --- -update_env_var() { - local key=$1 - local value=$2 - if grep -q "^$key=" "$ENV_FILE"; then - sed -i "s/^$key=.*/$key=$value/" "$ENV_FILE" +prepare_docker_context() { + local context_dir=$1 + + if $INCLUDE_DEV_CERTS; then + echo "Including dev certificates in Docker context..." + mkdir -p "$context_dir/dev-certs" + cp "$context_dir/../dev-certs/sentrius-ca.crt" "$context_dir/dev-certs/" else - echo "$key=$value" >> "$ENV_FILE" + echo "Excluding dev certificates from Docker context..." + rm -rf "$context_dir/dev-certs" + mkdir -p "$context_dir/dev-certs" + cp "$context_dir/../dev-certs/empty-sentrius-ca.crt" "$context_dir/dev-certs/sentrius-ca.crt" fi } -increment_patch_version() { - version=$1 - major=$(echo "$version" | cut -d. -f1) - minor=$(echo "$version" | cut -d. -f2) - patch=$(echo "$version" | cut -d. -f3) - echo "$major.$minor.$((patch + 1))" +cleanup_docker_context() { + local context_dir=$1 + #rm -rf "$context_dir/dev-certs" } build_image() { @@ -45,15 +51,22 @@ build_image() { local context_dir=$3 echo "Building $name:$version..." + prepare_docker_context "$context_dir" + + BUILD_ARGS=() + if $INCLUDE_DEV_CERTS; then + BUILD_ARGS+=(--build-arg INCLUDE_DEV_CERTS=true) + fi if $NO_CACHE; then - docker build --no-cache -t "$name:$version" "$context_dir" + docker build --no-cache "${BUILD_ARGS[@]}" -t "$name:$version" "$context_dir" else - docker build -t "$name:$version" "$context_dir" + docker build "${BUILD_ARGS[@]}" -t "$name:$version" "$context_dir" fi if [ $? -ne 0 ]; then echo "❌ Failed to build $name" + cleanup_docker_context "$context_dir" exit 1 fi @@ -66,6 +79,8 @@ build_image() { docker tag "$name:$version" "$name:latest" echo "✅ Built locally: $name:$version" fi + + cleanup_docker_context "$context_dir" } # --- Flags --- @@ -76,6 +91,7 @@ update_sentrius_agent=false update_sentrius_ai_agent=false update_integrationproxy=false update_launcher=false +update_agent_proxy=false while [[ "$#" -gt 0 ]]; do case $1 in @@ -86,8 +102,10 @@ while [[ "$#" -gt 0 ]]; do --sentrius-ai-agent) update_sentrius_ai_agent=true ;; --sentrius-launcher-service) update_launcher=true ;; --sentrius-integration-proxy) update_integrationproxy=true ;; - --all) update_sentrius=true; update_sentrius_ssh=true; update_sentrius_keycloak=true; update_sentrius_agent=true; update_sentrius_ai_agent=true; update_integrationproxy=true; update_launcher=true ;; + --sentrius-agent-proxy) update_agent_proxy=true ;; + --all) update_sentrius=true; update_sentrius_ssh=true; update_sentrius_keycloak=true; update_sentrius_agent=true; update_sentrius_ai_agent=true; update_integrationproxy=true; update_launcher=true; update_agent_proxy=true; ;; --no-cache) NO_CACHE=true ;; + --include-dev-certs) INCLUDE_DEV_CERTS=true ;; *) echo "Unknown flag: $1"; exit 1 ;; esac shift @@ -101,27 +119,29 @@ fi # --- Build Steps --- if $update_sentrius; then + cp api/target/sentrius-api-*.jar docker/sentrius/sentrius.jar SENTRIUS_VERSION=$(increment_patch_version $SENTRIUS_VERSION) - build_image "sentrius" "$SENTRIUS_VERSION" "." + build_image "sentrius" "$SENTRIUS_VERSION" "${SCRIPT_DIR}/../../docker/sentrius/" + rm docker/sentrius/sentrius.jar update_env_var "SENTRIUS_VERSION" "$SENTRIUS_VERSION" fi if $update_sentrius_ssh; then SENTRIUS_SSH_VERSION=$(increment_patch_version $SENTRIUS_SSH_VERSION) - build_image "sentrius-ssh" "$SENTRIUS_SSH_VERSION" "./docker/fake-ssh" + build_image "sentrius-ssh" "$SENTRIUS_SSH_VERSION" "${SCRIPT_DIR}/../../docker/fake-ssh" update_env_var "SENTRIUS_SSH_VERSION" "$SENTRIUS_SSH_VERSION" fi if $update_sentrius_keycloak; then SENTRIUS_KEYCLOAK_VERSION=$(increment_patch_version $SENTRIUS_KEYCLOAK_VERSION) - build_image "sentrius-keycloak" "$SENTRIUS_KEYCLOAK_VERSION" "./docker/keycloak" + build_image "sentrius-keycloak" "$SENTRIUS_KEYCLOAK_VERSION" "${SCRIPT_DIR}/../../docker/keycloak" update_env_var "SENTRIUS_KEYCLOAK_VERSION" "$SENTRIUS_KEYCLOAK_VERSION" fi if $update_sentrius_agent; then cp analytics/target/analytics-*.jar docker/sentrius-agent/agent.jar SENTRIUS_AGENT_VERSION=$(increment_patch_version $SENTRIUS_AGENT_VERSION) - build_image "sentrius-agent" "$SENTRIUS_AGENT_VERSION" "./docker/sentrius-agent" + build_image "sentrius-agent" "$SENTRIUS_AGENT_VERSION" "${SCRIPT_DIR}/../../docker/sentrius-agent" rm docker/sentrius-agent/agent.jar update_env_var "SENTRIUS_AGENT_VERSION" "$SENTRIUS_AGENT_VERSION" fi @@ -129,19 +149,19 @@ fi if $update_sentrius_ai_agent; then cp ai-agent/target/ai-agent-*.jar docker/sentrius-ai-agent/agent.jar SENTRIUS_AI_AGENT_VERSION=$(increment_patch_version $SENTRIUS_AI_AGENT_VERSION) - build_image "sentrius-ai-agent" "$SENTRIUS_AI_AGENT_VERSION" "./docker/sentrius-ai-agent" + build_image "sentrius-ai-agent" "$SENTRIUS_AI_AGENT_VERSION" "${SCRIPT_DIR}/../../docker/sentrius-ai-agent" rm docker/sentrius-ai-agent/agent.jar update_env_var "SENTRIUS_AI_AGENT_VERSION" "$SENTRIUS_AI_AGENT_VERSION" cp ai-agent/target/ai-agent-*.jar docker/sentrius-launchable-agent/agent.jar - build_image "sentrius-launchable-agent" "$SENTRIUS_AI_AGENT_VERSION" "./docker/sentrius-launchable-agent" + build_image "sentrius-launchable-agent" "$SENTRIUS_AI_AGENT_VERSION" "${SCRIPT_DIR}/../../docker/sentrius-launchable-agent" rm docker/sentrius-launchable-agent/agent.jar fi if $update_integrationproxy; then cp integration-proxy/target/sentrius-integration-proxy-*.jar docker/integrationproxy/llmproxy.jar LLMPROXY_VERSION=$(increment_patch_version $LLMPROXY_VERSION) - build_image "sentrius-integration-proxy" "$LLMPROXY_VERSION" "./docker/integrationproxy" + build_image "sentrius-integration-proxy" "$LLMPROXY_VERSION" "${SCRIPT_DIR}/../../docker/integrationproxy" rm docker/integrationproxy/llmproxy.jar update_env_var "LLMPROXY_VERSION" "$LLMPROXY_VERSION" fi @@ -149,7 +169,15 @@ fi if $update_launcher; then cp agent-launcher/target/agent-launcher-*.jar docker/sentrius-launcher-service/launcher.jar LAUNCHER_VERSION=$(increment_patch_version $LAUNCHER_VERSION) - build_image "sentrius-launcher-service" "$LAUNCHER_VERSION" "./docker/sentrius-launcher-service" + build_image "sentrius-launcher-service" "$LAUNCHER_VERSION" "${SCRIPT_DIR}/../../docker/sentrius-launcher-service" rm docker/sentrius-launcher-service/launcher.jar update_env_var "LAUNCHER_VERSION" "$LAUNCHER_VERSION" +fi + +if $update_agent_proxy; then + cp agent-proxy/target/sentrius-agent-proxy-*.jar docker/agent-proxy/agentproxy.jar + AGENTPROXY_VERSION=$(increment_patch_version $AGENTPROXY_VERSION) + build_image "sentrius-agent-proxy" "$AGENTPROXY_VERSION" "${SCRIPT_DIR}/../../docker/agent-proxy" + rm docker/agent-proxy/agentproxy.jar + update_env_var "AGENTPROXY_VERSION" "$AGENTPROXY_VERSION" fi \ No newline at end of file diff --git a/ops-scripts/local/deploy-helm.sh b/ops-scripts/local/deploy-helm.sh index 285d7755..86e760f1 100755 --- a/ops-scripts/local/deploy-helm.sh +++ b/ops-scripts/local/deploy-helm.sh @@ -2,16 +2,158 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) - source ${SCRIPT_DIR}/base.sh +source ${SCRIPT_DIR}/../base/base.sh source ${SCRIPT_DIR}/../../.local.env - +CERT_DIR="${SCRIPT_DIR}/../../docker/dev-certs" +CERT_FILE="${CERT_DIR}/sentrius-ca.crt" +KEY_FILE="${CERT_DIR}/sentrius-ca.key" TENANT=dev +ENABLE_TLS=false +INSTALL_CERT_MANAGER=false +ENV_TARGET="local" # default mode +CERT_DIR="${SCRIPT_DIR}/../../docker/dev-certs" + +# --- Load and back up environment file --- +ENV_FILE="${SCRIPT_DIR}/../../.$ENV_TARGET.env" +source "$ENV_FILE" +cp "$ENV_FILE" "$ENV_FILE.bak" + + +GENERATED_ENV_PATH="${SCRIPT_DIR}/../../.generated.env" +if [[ -f "$GENERATED_ENV_PATH" ]]; then + source "$GENERATED_ENV_PATH" +fi + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --tls) + ENABLE_TLS=true + shift + ;; + --install-cert-manager) + INSTALL_CERT_MANAGER=true + shift + ;; + --tenant) + TENANT="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--tls] [--install-cert-manager] [--tenant TENANT_NAME]" + echo " --tls: Enable TLS/SSL for secure transport" + echo " --install-cert-manager: Automatically install cert-manager if not present" + echo " --tenant: Specify tenant name (default: dev)" + exit 1 + ;; + esac +done + if [[ -z "$TENANT" ]]; then - echo "Must provide first argument for tenant name" 1>&2 + echo "Must provide tenant name" 1>&2 exit 1 fi + +if [[ "$ENABLE_TLS" == "true" ]]; then + if [[ ! -f "$CERT_FILE" || ! -f "$KEY_FILE" ]]; then + echo "🔧 Generating dev TLS certificate..." + openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout "$KEY_FILE" \ + -out "$CERT_FILE" \ + -days 365 \ + -subj "/CN=sentrius-dev-ca" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -addext "keyUsage=critical,keyCertSign,cRLSign" + + + echo "Creating dev CA secret in cluster" + kubectl -n cert-manager delete secret sentrius-dev-ca 2>/dev/null || true + kubectl -n cert-manager create secret tls sentrius-dev-ca \ + --cert="$CERT_DIR/sentrius-ca.crt" \ + --key="$CERT_DIR/sentrius-ca.key" + + echo "Rebuilding docker images with dev certs included" + + ${SCRIPT_DIR}/../base/build-images.sh --all --include-dev-certs + + else + echo "✅ Dev cert already exists at $CERT_FILE" + + + echo "Creating dev CA secret in cluster" + kubectl -n cert-manager delete secret sentrius-dev-ca 2>/dev/null || true + kubectl -n cert-manager create secret tls sentrius-dev-ca \ + --cert="$CERT_DIR/sentrius-ca.crt" \ + --key="$CERT_DIR/sentrius-ca.key" + fi +fi + + +# Function to check if cert-manager is installed and ready +check_cert_manager() { + echo "Checking if cert-manager is installed..." + + # Check if cert-manager deployments are present +if ! kubectl get deployment cert-manager -n cert-manager >/dev/null 2>&1 || \ + ! kubectl get deployment cert-manager-webhook -n cert-manager >/dev/null 2>&1 || \ + ! kubectl get deployment cert-manager-cainjector -n cert-manager >/dev/null 2>&1; then + if [[ "$INSTALL_CERT_MANAGER" == "true" ]]; then + echo "cert-manager components not found. Installing via Helm..." + helm repo add jetstack https://charts.jetstack.io + helm repo update + helm upgrade --install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set installCRDs=true + if [[ $? -ne 0 ]]; then + echo "ERROR: Failed to install cert-manager with Helm" + exit 1 + fi + echo "Waiting for cert-manager to be ready..." + kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=cert-manager -n cert-manager --timeout=300s + kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=webhook -n cert-manager --timeout=300s + else + echo "ERROR: cert-manager is not fully installed in your cluster." + echo "You can install it manually or rerun this script with --install-cert-manager --tls" + exit 1 + fi +fi + +} + +# Configure TLS settings +if [[ "$ENABLE_TLS" == "true" ]]; then + echo "Deploying with TLS enabled..." + check_cert_manager + SUBDOMAIN="sentrius-${TENANT}.local" + APROXY_SUBDOMAIN="agentproxy-${TENANT}.local" + KEYCLOAK_SUBDOMAIN="keycloak-${TENANT}.local" + KEYCLOAK_HOSTNAME=${KEYCLOAK_SUBDOMAIN} + KEYCLOAK_DOMAIN="https://${KEYCLOAK_SUBDOMAIN}" + KEYCLOAK_INTERNAL_DOMAIN="https://${KEYCLOAK_SUBDOMAIN}" + SENTRIUS_DOMAIN="https://${SUBDOMAIN}" + APROXY_DOMAIN="https://${APROXY_SUBDOMAIN}" + CERTIFICATES_ENABLED="true" + INGRESS_TLS_ENABLED="true" + ENVIRONMENT="local" +else + echo "Deploying with HTTP (no TLS)..." + SUBDOMAIN="sentrius-sentrius" + APROXY_SUBDOMAIN="sentrius-agentproxy" + KEYCLOAK_SUBDOMAIN="sentrius-keycloak" + KEYCLOAK_HOSTNAME="sentrius-keycloak:8081" + KEYCLOAK_DOMAIN="http://sentrius-keycloak:8081" + KEYCLOAK_INTERNAL_DOMAIN="http://sentrius-keycloak:8081" + APROXY_DOMAIN="http://sentrius-agentproxy:8080" + SENTRIUS_DOMAIN="http://sentrius-sentrius:8080" + CERTIFICATES_ENABLED="false" + INGRESS_TLS_ENABLED="false" + ENVIRONMENT="local" +fi + # Check if namespace exists kubectl get namespace ${TENANT} >/dev/null 2>&1 if [[ $? -ne 0 ]]; then @@ -30,10 +172,7 @@ fi # --set sentrius-bad-ssh.image.pullPolicy="Never" \ # Load any previously generated password from .generated.env -GENERATED_ENV_PATH="${SCRIPT_DIR}/../../.generated.env" -if [[ -f "$GENERATED_ENV_PATH" ]]; then - source "$GENERATED_ENV_PATH" -fi + # Generate Keycloak DB password if not set and secret doesn't exist if [[ -z "$KEYCLOAK_DB_PASSWORD" ]]; then @@ -50,21 +189,71 @@ if [[ -z "$KEYCLOAK_DB_PASSWORD" ]]; then echo "⚠️ No existing secret found; generating new Keycloak DB password..." KEYCLOAK_DB_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24) - # Persist it to .generated.env so it doesn't change between runs - echo "KEYCLOAK_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}" > "$GENERATED_ENV_PATH" fi fi +# Generate Keycloak client secret if not already present +if [[ -z "$KEYCLOAK_CLIENT_SECRET" ]]; then + echo "🔎 Checking if keycloak secret already exists..." + if kubectl get secret "${TENANT}-keycloak-secrets" --namespace "${TENANT}" >/dev/null 2>&1; then + echo "✅ Found existing keycloak secret; extracting client secret..." + KEYCLOAK_CLIENT_SECRET=$(kubectl get secret "${TENANT}-keycloak-secrets" --namespace "${TENANT}" -o jsonpath="{.data.client-secret}" | base64 --decode) + else + echo "⚠️ No existing secret found; generating new Keycloak client secret..." + KEYCLOAK_CLIENT_SECRET=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32) + fi +fi + +# Generate Keycloak admin password if not already present +if [[ -z "$KEYCLOAK_ADMIN_PASSWORD" ]]; then + echo "🔎 Checking if keycloak secret already exists..." + if kubectl get secret "${TENANT}-keycloak-secrets" --namespace "${TENANT}" >/dev/null 2>&1; then + echo "✅ Found existing keycloak secret; extracting admin password..." + KEYCLOAK_ADMIN_PASSWORD=$(kubectl get secret "${TENANT}-keycloak-secrets" --namespace "${TENANT}" -o jsonpath="{.data.admin-password}" | base64 --decode) + else + echo "⚠️ No existing secret found; generating new Keycloak admin password..." + KEYCLOAK_ADMIN_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24) + fi +fi + +if [[ -z "$DB_PASSWORD" ]]; then + DB_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32) +fi + +if [[ -z "$KEYSTORE_PASSWORD" ]]; then + KEYSTORE_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32) +fi + + +# Save them to .generated.env so they persist across runs +cat < "$GENERATED_ENV_PATH" +KEYCLOAK_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD} +KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET} +KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} +DB_PASSWORD=${DB_PASSWORD} +KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD} +EOF + helm upgrade --install sentrius ./sentrius-chart --namespace ${TENANT} \ --set tenant=${TENANT} \ - --set subdomain="sentrius-sentrius" \ - --set keycloakSubdomain="sentrius-keycloak" \ - --set keycloakHostname="sentrius-keycloak:8081" \ - --set keycloakDomain="http://sentrius-keycloak:8081" \ - --set sentriusDomain="http://sentrius-sentrius:8080" \ + --set environment=${ENVIRONMENT} \ + --set subdomain="${SUBDOMAIN}" \ + --set agentproxySubdomain="${APROXY_SUBDOMAIN}" \ + --set keycloakSubdomain="${KEYCLOAK_SUBDOMAIN}" \ + --set keycloakHostname="${KEYCLOAK_HOSTNAME}" \ + --set keycloakDomain="${KEYCLOAK_DOMAIN}" \ + --set keycloakInternalDomain="${KEYCLOAK_INTERNAL_DOMAIN}" \ + --set sentriusDomain="${SENTRIUS_DOMAIN}" \ + --set secrets.db.password="${DB_PASSWORD}" \ + --set secrets.db.keystorePassword="${KEYSTORE_PASSWORD}" \ + --set agentproxyDomain="${APROXY_DOMAIN}" \ + --set certificates.enabled=${CERTIFICATES_ENABLED} \ + --set ingress.tlsEnabled=${INGRESS_TLS_ENABLED} \ --set launcherFQDN=sentrius-agents-launcherservice.${TENANT}-agents.svc.cluster.local \ --set integrationproxy.image.repository="sentrius-integration-proxy" \ + --set agentproxy.image.pullPolicy="Never" \ + --set agentproxy.image.tag=${AGENTPROXY_VERSION} \ --set integrationproxy.image.pullPolicy="Never" \ --set sentrius.image.repository="sentrius" \ --set keycloak.db.password="${KEYCLOAK_DB_PASSWORD}" \ @@ -89,13 +278,19 @@ helm upgrade --install sentrius-agents ./sentrius-chart-launcher --namespace ${T --set keycloakFQDN=sentrius-keycloak.${TENANT}.svc.cluster.local \ --set sentriusFQDN=sentrius-sentrius.${TENANT}.svc.cluster.local \ --set integrationproxyFQDN=sentrius-integrationproxy.${TENANT}.svc.cluster.local \ - --set subdomain="sentrius-sentrius" \ - --set keycloakSubdomain="sentrius-keycloak" \ - --set keycloakHostname="sentrius-keycloak:8081" \ - --set keycloakDomain="http://sentrius-keycloak:8081" \ - --set sentriusDomain="http://sentrius-sentrius:8080" \ + --set agentproxyFQDN=sentrius-llmproxy.${TENANT}.svc.cluster.local \ + --set subdomain="${SUBDOMAIN}" \ + --set agentproxySubdomain="${APROXY_SUBDOMAIN}" \ + --set agentproxyDomain="${APROXY_DOMAIN}" \ + --set keycloakSubdomain="${KEYCLOAK_SUBDOMAIN}" \ + --set keycloakHostname="${KEYCLOAK_HOSTNAME}" \ + --set keycloakDomain="${KEYCLOAK_DOMAIN}" \ + --set keycloakInternalDomain="${KEYCLOAK_INTERNAL_DOMAIN}" \ + --set sentriusDomain="${SENTRIUS_DOMAIN}" \ --set integrationproxy.image.repository="sentrius-integration-proxy" \ - --set integrationproxy.image.pullPolicy="IfNotPresent" \ + --set integrationproxy.image.pullPolicy="Never" \ + --set secrets.db.password="${DB_PASSWORD}" \ + --set secrets.db.keystorePassword="${KEYSTORE_PASSWORD}" \ --set sentrius.image.repository="sentrius" \ --set sentrius.image.pullPolicy="Never" \ --set keycloak.image.pullPolicy="Never" \ diff --git a/pom.xml b/pom.xml index ad1a3c1c..1d7472e8 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ llm-dataplane provenance-ingestor api + agent-proxy integration-proxy analytics ai-agent diff --git a/sentrius-chart-launcher/templates/agentproxy-alias-service.yaml b/sentrius-chart-launcher/templates/agentproxy-alias-service.yaml new file mode 100644 index 00000000..e90b8fcc --- /dev/null +++ b/sentrius-chart-launcher/templates/agentproxy-alias-service.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Service +metadata: + name: sentrius-agentproxy + namespace: dev-agents +spec: + type: ExternalName + externalName: {{ .Values.agentProxyFQDN }} \ No newline at end of file diff --git a/sentrius-chart-launcher/templates/configmap.yaml b/sentrius-chart-launcher/templates/configmap.yaml index 41c918d2..169431fd 100644 --- a/sentrius-chart-launcher/templates/configmap.yaml +++ b/sentrius-chart-launcher/templates/configmap.yaml @@ -33,7 +33,7 @@ data: spring.servlet.multipart.max-request-size=10MB server.error.whitelabel.enabled=false keycloak.realm=sentrius - keycloak.base-url={{ .Values.keycloakDomain }} + keycloak.base-url={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }} agent.api.url={{ .Values.sentriusDomain }} # Keycloak configuration spring.security.oauth2.client.registration.keycloak.client-id={{ .Values.launcherservice.oauth2.client_id }} @@ -41,8 +41,8 @@ data: spring.security.oauth2.client.registration.keycloak.authorization-grant-type={{ .Values.launcherservice.oauth2.authorization_grant_type }} spring.security.oauth2.client.registration.keycloak.redirect-uri={{ .Values.sentriusDomain }}/login/oauth2/code/keycloak spring.security.oauth2.client.registration.keycloak.scope={{ .Values.launcherservice.oauth2.scope }} - spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius - spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius # otel.traces.exporter=none otel.metrics.exporter=none otel.logs.exporter=none @@ -133,4 +133,6 @@ data: agents.ai.chat.agent.enabled=true sentrius.agent.callback.format.url=http://sentrius-agent-%s.%s.svc.cluster.local:8090 agent.api.url={{ .Values.sentriusDomain }} - agent.open.ai.endpoint=http://sentrius-llmproxy:8084/ \ No newline at end of file + agent.open.ai.endpoint=http://sentrius-integrationproxy:8080/ + agent.listen.websocket=true + server.port=8090 \ No newline at end of file diff --git a/sentrius-chart-launcher/templates/llm-proxy-alias-service.yaml b/sentrius-chart-launcher/templates/llm-proxy-alias-service.yaml index b7295b27..b7d36f41 100644 --- a/sentrius-chart-launcher/templates/llm-proxy-alias-service.yaml +++ b/sentrius-chart-launcher/templates/llm-proxy-alias-service.yaml @@ -1,8 +1,8 @@ apiVersion: v1 kind: Service metadata: - name: sentrius-llmproxy + name: sentrius-integrationproxy namespace: dev-agents spec: type: ExternalName - externalName: {{ .Values.sentriusFQDN }} \ No newline at end of file + externalName: {{ .Values.integrationproxyFQDN }} \ No newline at end of file diff --git a/sentrius-chart-launcher/templates/role.yaml b/sentrius-chart-launcher/templates/role.yaml index 6f8a1e1f..9c79f9d7 100644 --- a/sentrius-chart-launcher/templates/role.yaml +++ b/sentrius-chart-launcher/templates/role.yaml @@ -5,5 +5,5 @@ metadata: namespace: {{ .Values.tenant }} rules: - apiGroups: [""] - resources: ["pods"] + resources: ["pods" , "services"] verbs: ["create", "get", "list", "watch"] \ No newline at end of file diff --git a/sentrius-chart-launcher/values.yaml b/sentrius-chart-launcher/values.yaml index 755e95a0..71b6fca7 100644 --- a/sentrius-chart-launcher/values.yaml +++ b/sentrius-chart-launcher/values.yaml @@ -5,16 +5,18 @@ namespace: default environment: "gke" # Can be "gke", "aws", "azure", "local" tenant: sentrius-demo -sentriusNamespace: "{{ .Values.tenant }}" +sentriusNamespace: "sentrius-demo" baseRelease: sentrius-demo -subdomain: "{{ .Values.tenant }}.sentrius.cloud" -keycloakSubdomain: keycloak.{{ .Values.subdomain }} -keycloakHostname: "{{ .Values.keycloakSubdomain }}" -keycloakDomain: https://{{ .Values.keycloakSubdomain }} -sentriusDomain: https://{{ .Values.subdomain }} +subdomain: "sentrius-demo.sentrius.cloud" +keycloakSubdomain: "keycloak.sentrius-demo.sentrius.cloud" +keycloakHostname: "keycloak.sentrius-demo.sentrius.cloud" +keycloakDomain: https://keycloak.sentrius-demo.sentrius.cloud +keycloakInternalDomain: http://sentrius-keycloak:8081 # Internal cluster communication +sentriusDomain: https://sentrius-demo.sentrius.cloud keycloakFQDN: sentrius-keycloak.dev.svc.cluster.local sentriusFQDN: sentrius-sentrius.dev.svc.cluster.local -llmProxyFQDN: sentrius-llmproxy.dev.svc.cluster.local +integrationproxyFQDN: sentrius-integrationproxy.dev.svc.cluster.local +agentProxyFQDN: sentrius-agentproxy.dev.svc.cluster.local certificates: enabled: false # Disable certs for local; enable for cloud @@ -221,6 +223,12 @@ ingress: alb.ingress.kubernetes.io/ssl-redirect: "443" local: # Local environment annotations (e.g., Minikube) kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/use-forwarded-headers: "true" + nginx.ingress.kubernetes.io/enable-websocket: "true" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" diff --git a/sentrius-chart/templates/agentproxy-deployment.yaml b/sentrius-chart/templates/agentproxy-deployment.yaml new file mode 100644 index 00000000..f452eaf8 --- /dev/null +++ b/sentrius-chart/templates/agentproxy-deployment.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-agentproxy + labels: + {{- include "sentrius.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: agentproxy + template: + metadata: + labels: + app: agentproxy + spec: + initContainers: + - name: wait-for-postgres + image: busybox + command: [ 'sh', '-c', 'until nc -z {{ .Release.Name }}-sentrius 8080; do echo waiting for postgres; sleep 2; + done;' ] + containers: + - name: agentproxy + image: "{{ .Values.agentproxy.image.repository }}:{{ .Values.agentproxy.image.tag }}" + imagePullPolicy: {{ .Values.agentproxy.image.pullPolicy }} + ports: + - containerPort: {{ .Values.agentproxy.port }} + {{- if not (eq .Values.environment "gke") }} + readinessProbe: + httpGet: + path: {{ .Values.healthCheck.readinessProbe.path }} + port: {{ .Values.healthCheck.readinessProbe.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: {{ .Values.healthCheck.livenessProbe.path }} + port: {{ .Values.healthCheck.livenessProbe.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + {{- end }} + volumeMounts: + - name: config-volume + mountPath: /config + env: + - name: KEYSTORE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-db-secret + key: keystore-password + - name: KEYCLOAK_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-oauth2-secrets + key: agentproxy-client-secret + + volumes: + - name: config-volume + configMap: + name: {{ .Release.Name }}-config diff --git a/sentrius-chart/templates/agentproxy-service.yaml b/sentrius-chart/templates/agentproxy-service.yaml new file mode 100644 index 00000000..b036aacd --- /dev/null +++ b/sentrius-chart/templates/agentproxy-service.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-agentproxy + namespace: {{ .Values.tenant }} + annotations: + {{- if eq .Values.environment "gke" }} + cloud.google.com/backend-config: '{"default": "sentrius-backend-config"}' + {{- else if eq .Values.environment "aws" }} + {{- range $key, $value := .Values.sentrius.annotations.aws }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- else if eq .Values.environment "azure" }} + {{- range $key, $value := .Values.sentrius.annotations.azure }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} + labels: + app: agentproxy +spec: + type: {{ .Values.sentrius.serviceType }} + ports: + - name: http + port: {{ .Values.sentrius.port }} + targetPort: {{ .Values.sentrius.port }} # Port used inside the container + {{- if eq .Values.sentrius.serviceType "NodePort" }} + nodePort: {{ .Values.agentproxy.nodePort | default 30080 }} + {{- end }} + selector: + app: agentproxy \ No newline at end of file diff --git a/sentrius-chart/templates/ai-agent-deployment.yaml b/sentrius-chart/templates/ai-agent-deployment.yaml index a3717c1a..851c4928 100644 --- a/sentrius-chart/templates/ai-agent-deployment.yaml +++ b/sentrius-chart/templates/ai-agent-deployment.yaml @@ -32,7 +32,7 @@ spec: valueFrom: secretKeyRef: name: {{ .Release.Name }}-oauth2-secrets - key: sentriusaiagent-client-secret + key: ai-agent-assessor-client-secret volumes: - name: config-volume configMap: diff --git a/sentrius-chart/templates/configmap.yaml b/sentrius-chart/templates/configmap.yaml index 69f50e38..2a7119cc 100644 --- a/sentrius-chart/templates/configmap.yaml +++ b/sentrius-chart/templates/configmap.yaml @@ -19,6 +19,60 @@ data: } ] } + agentproxy-application.properties: | + keystore.file=sso.jceks + keystore.password=${KEYSTORE_PASSWORD} + keystore.alias=KEYBOX-ENCRYPTION_KEY + spring.thymeleaf.enabled=true + spring.freemarker.enabled=false + #flyway configuration + spring.main.web-application-type=reactive + spring.flyway.enabled=false + logging.level.org.springframework.web=INFO + logging.level.org.springframework.security=INFO + logging.level.io.sentrius=DEBUG + logging.level.org.thymeleaf=INFO + spring.thymeleaf.servlet.produce-partial-output-while-processing=false + spring.servlet.multipart.enabled=true + spring.servlet.multipart.max-file-size=10MB + spring.servlet.multipart.max-request-size=10MB + server.error.whitelabel.enabled=false + dynamic.properties.path=/config/dynamic.properties + keycloak.realm=sentrius + keycloak.base-url={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }} + agent.api.url={{ .Values.sentriusDomain }} + # Keycloak configuration + spring.security.oauth2.client.registration.keycloak.client-id={{ .Values.agentproxy.oauth2.client_id }} + spring.security.oauth2.client.registration.keycloak.client-secret=${KEYCLOAK_CLIENT_SECRET} + spring.security.oauth2.client.registration.keycloak.authorization-grant-type={{ .Values.sentriusagent.oauth2.authorization_grant_type }} + #spring.security.oauth2.client.registration.keycloak.redirect-uri={{ .Values.sentriusDomain }}/login/oauth2/code/keycloak + #spring.security.oauth2.client.registration.keycloak.scope={{ .Values.sentriusagent.oauth2.scope }} + spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius + # OTEL settings + otel.traces.exporter=otlp + otel.metrics.exporter=none + otel.logs.exporter=none + otel.exporter.otlp.endpoint=http://sentrius-jaeger:4317 + otel.resource.attributes.service.name=integration-proxy + otel.traces.sampler=always_on + otel.exporter.otlp.timeout=10s + otel.exporter.otlp.protocol=grpc + provenance.kafka.topic=sentrius-provenance + # Serialization + spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer + spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer + spring.kafka.producer.properties.spring.json.trusted.packages=io.sentrius.* + # Reliability + spring.kafka.producer.retries=5 + spring.kafka.producer.acks=all + # Timeout tuning + spring.kafka.producer.request-timeout-ms=10000 + spring.kafka.producer.delivery-timeout-ms=30000 + spring.kafka.properties.max.block.ms=500 + spring.kafka.properties.metadata.max.age.ms=10000 + spring.kafka.properties.retry.backoff.ms=1000 + spring.kafka.bootstrap-servers=sentrius-kafka:9092 llmproxy-application.properties: | keystore.file=sso.jceks keystore.password=${KEYSTORE_PASSWORD} @@ -28,7 +82,22 @@ data: spring.freemarker.enabled=false #flyway configuration spring.flyway.enabled=false - #spring.datasource.url=jdbc:h2:mem:testdb + spring.datasource.url=jdbc:postgresql://sentrius-postgres:5432/sentrius + spring.datasource.username=${SPRING_DATASOURCE_USERNAME} + spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} + spring.datasource.driver-class-name=org.postgresql.Driver + # Connection pool settings + spring.datasource.hikari.maximum-pool-size=10 + spring.datasource.hikari.minimum-idle=5 + spring.datasource.hikari.idle-timeout=30000 + spring.datasource.hikari.max-lifetime=1800000 + # Hibernate settings (optional, for JPA) + spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + # Disable automatic schema generation in production + spring.jpa.hibernate.ddl-auto=none + # Ensure this path matches your project structure + #spring.flyway.locations=classpath:db/migration/ + spring.flyway.baseline-on-migrate=true logging.level.org.springframework.web=INFO logging.level.org.springframework.security=INFO logging.level.io.sentrius=DEBUG @@ -40,7 +109,7 @@ data: server.error.whitelabel.enabled=false dynamic.properties.path=/config/dynamic.properties keycloak.realm=sentrius - keycloak.base-url={{ .Values.keycloakDomain }} + keycloak.base-url={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }} agent.api.url={{ .Values.sentriusDomain }} # Keycloak configuration spring.security.oauth2.client.registration.keycloak.client-id={{ .Values.sentriusagent.oauth2.client_id }} @@ -48,11 +117,12 @@ data: spring.security.oauth2.client.registration.keycloak.authorization-grant-type={{ .Values.sentriusagent.oauth2.authorization_grant_type }} spring.security.oauth2.client.registration.keycloak.redirect-uri={{ .Values.sentriusDomain }}/login/oauth2/code/keycloak spring.security.oauth2.client.registration.keycloak.scope={{ .Values.sentriusagent.oauth2.scope }} - spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius - spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius # OTEL settings otel.traces.exporter=otlp otel.metrics.exporter=none + otel.logs.exporter=none otel.exporter.otlp.endpoint=http://sentrius-jaeger:4317 otel.resource.attributes.service.name=integration-proxy otel.traces.sampler=always_on @@ -74,14 +144,13 @@ data: spring.kafka.properties.max.block.ms=500 spring.kafka.properties.metadata.max.age.ms=10000 spring.kafka.properties.retry.backoff.ms=1000 - bootstrap-servers=sentrius-kafka:9092: + spring.kafka.bootstrap-servers=sentrius-kafka:9092 ai-agent-application.properties: | spring.main.web-application-type=servlet spring.thymeleaf.enabled=true spring.freemarker.enabled=false #flyway configuration spring.flyway.enabled=false - #spring.datasource.url=jdbc:h2:mem:testdb logging.level.org.springframework.web=INFO logging.level.org.springframework.security=INFO logging.level.io.sentrius=DEBUG @@ -93,16 +162,16 @@ data: server.error.whitelabel.enabled=false dynamic.properties.path=/config/dynamic.properties keycloak.realm=sentrius - keycloak.base-url={{ .Values.keycloakDomain }} + keycloak.base-url={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }} agent.api.url={{ .Values.sentriusDomain }} # Keycloak configuration - spring.security.oauth2.client.registration.keycloak.client-id={{ .Values.sentriusagent.oauth2.client_id }} + spring.security.oauth2.client.registration.keycloak.client-id={{ .Values.sentriusaiagent.oauth2.client_id }} spring.security.oauth2.client.registration.keycloak.client-secret=${KEYCLOAK_CLIENT_SECRET} spring.security.oauth2.client.registration.keycloak.authorization-grant-type={{ .Values.sentriusagent.oauth2.authorization_grant_type }} spring.security.oauth2.client.registration.keycloak.redirect-uri={{ .Values.sentriusDomain }}/login/oauth2/code/keycloak spring.security.oauth2.client.registration.keycloak.scope={{ .Values.sentriusagent.oauth2.scope }} - spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius - spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius agents.ai.registered.agent.enabled=true # OTEL settings otel.traces.exporter=otlp @@ -132,7 +201,7 @@ data: spring.kafka.properties.max.block.ms=500 spring.kafka.properties.metadata.max.age.ms=10000 spring.kafka.properties.retry.backoff.ms=1000 - bootstrap-servers=sentrius-kafka:9092: + spring.kafka.bootstrap-servers=sentrius-kafka:9092 analysis-agent-application.properties: | keystore.file=sso.jceks @@ -163,7 +232,6 @@ data: # Thymeleaf settings spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html - #spring.datasource.url=jdbc:h2:mem:testdb logging.level.org.springframework.web=INFO logging.level.org.springframework.security=INFO logging.level.io.sentrius=DEBUG @@ -175,7 +243,7 @@ data: server.error.whitelabel.enabled=false dynamic.properties.path=/config/dynamic.properties keycloak.realm=sentrius - keycloak.base-url={{ .Values.keycloakDomain }} + keycloak.base-url={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }} agent.api.url={{ .Values.sentriusDomain }} # Keycloak configuration spring.security.oauth2.client.registration.keycloak.client-id={{ .Values.sentriusagent.oauth2.client_id }} @@ -183,8 +251,8 @@ data: spring.security.oauth2.client.registration.keycloak.authorization-grant-type={{ .Values.sentriusagent.oauth2.authorization_grant_type }} spring.security.oauth2.client.registration.keycloak.redirect-uri={{ .Values.sentriusDomain }}/login/oauth2/code/keycloak spring.security.oauth2.client.registration.keycloak.scope={{ .Values.sentriusagent.oauth2.scope }} - spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius - spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius agents.session-analytics.enabled=true # OTEL settings otel.traces.exporter=otlp @@ -203,7 +271,7 @@ data: # Reliability spring.kafka.producer.retries=5 spring.kafka.producer.acks=all - bootstrap-servers=sentrius-kafka:9092: + spring.kafka.bootstrap-servers=sentrius-kafka:9092 # Timeout tuning spring.kafka.producer.request-timeout-ms=10000 @@ -212,6 +280,7 @@ data: spring.kafka.properties.metadata.max.age.ms=10000 spring.kafka.properties.retry.backoff.ms=1000 api-application.properties: | + org.springframework.context.ApplicationListener=your.package.DbEnvPrinter keystore.file=sso.jceks keystore.password=${KEYSTORE_PASSWORD} keystore.alias=KEYBOX-ENCRYPTION_KEY @@ -240,7 +309,6 @@ data: # Thymeleaf settings spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html - #spring.datasource.url=jdbc:h2:mem:testdb logging.level.org.springframework.web=INFO logging.level.org.springframework.security=INFO logging.level.io.sentrius=DEBUG @@ -252,7 +320,7 @@ data: server.error.whitelabel.enabled=false dynamic.properties.path=/config/dynamic.properties keycloak.realm=sentrius - keycloak.base-url={{ .Values.keycloakDomain }} + keycloak.base-url={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }} management.endpoints.web.exposure.include=health management.endpoint.health.show-details=always # Keycloak configuration @@ -261,8 +329,8 @@ data: spring.security.oauth2.client.registration.keycloak.authorization-grant-type={{ .Values.sentrius.oauth2.authorization_grant_type }} spring.security.oauth2.client.registration.keycloak.redirect-uri={{ .Values.sentriusDomain }}/login/oauth2/code/keycloak spring.security.oauth2.client.registration.keycloak.scope={{ .Values.sentrius.oauth2.scope }} - spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius - spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.resourceserver.jwt.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius + spring.security.oauth2.client.provider.keycloak.issuer-uri={{ .Values.keycloakInternalDomain | default .Values.keycloakDomain }}/realms/sentrius server.forward-headers-strategy=framework https.redirect.enabled=true https.required={{ .Values.certificates.enabled }} @@ -294,6 +362,7 @@ data: sentrius.agent.launcher.service=http://sentrius-agents-launcherservice:8080/ sentrius.agent.register.bootstrap.allow=true sentrius.agent.bootstrap.policy=default-policy.yaml + agentproxy.externalUrl={{ .Values.agentproxyDomain }} default-policy.yaml: | --- version: "v0" diff --git a/sentrius-chart/templates/ingress.yaml b/sentrius-chart/templates/ingress.yaml index 7514ae2e..626d674c 100644 --- a/sentrius-chart/templates/ingress.yaml +++ b/sentrius-chart/templates/ingress.yaml @@ -5,10 +5,19 @@ metadata: name: managed-cert-ingress-{{ .Values.tenant }} namespace: {{ .Values.tenant }} annotations: - kubernetes.io/ingress.class: {{ .Values.ingress.class }} + {{- toYaml .Values.ingress.annotationSets.local | nindent 4 }} + nginx.ingress.kubernetes.io/ssl-redirect: "{{ .Values.certificates.enabled }}" + nginx.ingress.kubernetes.io/force-ssl-redirect: "{{ .Values.certificates.enabled }}" spec: + {{- if .Values.ingress.tlsEnabled }} + tls: + - hosts: + - "{{ .Values.keycloakSubdomain }}" + - "{{ .Values.subdomain }}" + secretName: wildcard-cert-{{ .Values.tenant }} + {{- end }} rules: - - host: {{ .Values.keycloakSubdomain }} + - host: "{{ .Values.keycloakSubdomain }}" http: paths: - path: / @@ -18,7 +27,7 @@ spec: name: {{ .Release.Name }}-keycloak port: number: 8081 - - host: {{ .Values.tenant }}.sentrius.cloud + - host: "{{ .Values.subdomain }}" http: paths: - path: / @@ -28,4 +37,14 @@ spec: name: {{ .Release.Name }}-sentrius port: number: 8080 -{{- end }} \ No newline at end of file + - host: "{{ .Values.agentproxySubdomain }}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ .Release.Name }}-agentproxy + port: + number: 8080 +{{- end }} diff --git a/sentrius-chart/templates/keycloak-deployment.yaml b/sentrius-chart/templates/keycloak-deployment.yaml index b814cdae..7dd29ba0 100644 --- a/sentrius-chart/templates/keycloak-deployment.yaml +++ b/sentrius-chart/templates/keycloak-deployment.yaml @@ -80,10 +80,12 @@ spec: value: "true" - name: KC_HOSTNAME_STRICT_HTTPS value: "false" + - name: KEYCLOAK_FRONTEND_URL + value: {{ .Values.keycloakDomain }} - name: KC_HTTP_ENABLED value: "true" - name: GOOGLE_CLIENT_ID - value: {{ .Values.keycloak.clientId }} + value: {{ .Values.keycloak.google.clientId }} - name: GOOGLE_CLIENT_SECRET valueFrom: secretKeyRef: diff --git a/sentrius-chart/templates/keycloak-secrets.yaml b/sentrius-chart/templates/keycloak-secrets.yaml index fb4333ec..36a60733 100644 --- a/sentrius-chart/templates/keycloak-secrets.yaml +++ b/sentrius-chart/templates/keycloak-secrets.yaml @@ -1,23 +1,13 @@ -{{- include "keycloak.requireDbPassword" . }} - +{{- $secretName := printf "%s-keycloak-secrets" .Release.Name }} +{{- $existing := (lookup "v1" "Secret" .Release.Namespace $secretName) }} +{{- if not $existing }} apiVersion: v1 kind: Secret metadata: - name: {{ .Release.Name }}-keycloak-secrets + name: {{ $secretName }} type: Opaque data: - # Keycloak Admin Password - {{- if .Values.keycloak.adminPassword }} - admin-password: {{ .Values.keycloak.adminPassword | b64enc }} - {{- else }} - admin-password: {{ randAlphaNum 24 | b64enc }} - {{- end }} - - # Keycloak Client Secret - {{- if .Values.keycloak.clientSecret }} - client-secret: {{ .Values.keycloak.clientSecret | b64enc }} - {{- else }} - client-secret: {{ randAlphaNum 32 | b64enc }} - {{- end }} - # Keycloak Database Password - db-password: {{ .Values.keycloak.db.password | b64enc }} \ No newline at end of file + admin-password: {{ (.Values.keycloak.adminPassword | default (randAlphaNum 24)) | b64enc }} + client-secret: {{ (.Values.keycloak.clientSecret | default (randAlphaNum 32)) | b64enc }} + db-password: {{ (.Values.keycloak.db.password | default (randAlphaNum 32)) | b64enc }} +{{- end }} diff --git a/sentrius-chart/templates/managed-cert.yaml b/sentrius-chart/templates/managed-cert.yaml index 64ee2e2b..0def9ba9 100644 --- a/sentrius-chart/templates/managed-cert.yaml +++ b/sentrius-chart/templates/managed-cert.yaml @@ -1,6 +1,7 @@ {{- if and (ne .Values.environment "local") (.Values.certificates.enabled) }} --- {{- if eq .Values.environment "gke" }} +# GKE Managed Certificate apiVersion: networking.gke.io/v1 kind: ManagedCertificate metadata: @@ -8,8 +9,10 @@ metadata: spec: domains: - "{{ .Values.subdomain }}" - - "keycloak.{{ .Values.subdomain }}" + - "{{ .Values.keycloakSubdomain }}" + - "{{ .Values.agentproxySubdomain }}" {{- else if or (eq .Values.environment "aws") (eq .Values.environment "azure") }} +# Cert-Manager Certificate for AWS or Azure apiVersion: cert-manager.io/v1 kind: Certificate metadata: @@ -19,9 +22,36 @@ spec: issuerRef: name: {{ .Values.certificates.issuer }} kind: ClusterIssuer - commonName: "{{ .Values.tenant }}.sentrius.cloud" dnsNames: - - "{{ .Values.tenant }}.sentrius.cloud" - - "keycloak.{{ .Values.tenant }}.sentrius.cloud" + - "{{ .Values.subdomain }}" + - "{{ .Values.keycloakSubdomain }}" + - "{{ .Values.agentproxySubdomain }}" {{- end }} +{{- else if and (eq .Values.environment "local") (.Values.certificates.enabled) }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: dev-ca-issuer +spec: + ca: + secretName: sentrius-dev-ca +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: wildcard-cert-{{ .Values.tenant }} + namespace: {{ .Values.tenant }} +spec: + secretName: wildcard-cert-{{ .Values.tenant }} + issuerRef: + name: dev-ca-issuer + kind: ClusterIssuer + dnsNames: + - "{{ .Values.subdomain }}" + - "{{ .Values.keycloakSubdomain }}" + - "{{ .Values.agentproxySubdomain }}" + subject: + organizations: + - sentrius-local {{- end }} diff --git a/sentrius-chart/templates/oauth2-secrets.yaml b/sentrius-chart/templates/oauth2-secrets.yaml index f9af42c5..3e089080 100644 --- a/sentrius-chart/templates/oauth2-secrets.yaml +++ b/sentrius-chart/templates/oauth2-secrets.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-oauth2-secrets + annotations: + "helm.sh/resource-policy": keep type: Opaque data: # Sentrius OAuth2 Client Secret @@ -10,56 +12,63 @@ data: {{- else }} sentrius-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + # Integration Proxy OAuth2 Client Secret {{- if .Values.integrationproxy.oauth2.client_secret }} integrationproxy-client-secret: {{ .Values.integrationproxy.oauth2.client_secret | b64enc }} {{- else }} integrationproxy-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + # Sentrius Agent OAuth2 Client Secret {{- if .Values.sentriusagent.oauth2.client_secret }} sentriusagent-client-secret: {{ .Values.sentriusagent.oauth2.client_secret | b64enc }} {{- else }} sentriusagent-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + # Sentrius AI Agent OAuth2 Client Secret {{- if .Values.sentriusaiagent.oauth2.client_secret }} sentriusaiagent-client-secret: {{ .Values.sentriusaiagent.oauth2.client_secret | b64enc }} {{- else }} sentriusaiagent-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + # Launcher Service OAuth2 Client Secret {{- if .Values.launcherservice.oauth2.client_secret }} launcherservice-client-secret: {{ .Values.launcherservice.oauth2.client_secret | b64enc }} {{- else }} launcherservice-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + # Keycloak Realm Client Secrets - These are used by Keycloak realm configuration {{- if .Values.keycloak.realm.clients.sentriusApi.client_secret }} sentrius-api-client-secret: {{ .Values.keycloak.realm.clients.sentriusApi.client_secret | b64enc }} {{- else }} sentrius-api-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + {{- if .Values.keycloak.realm.clients.sentriusLauncher.client_secret }} sentrius-launcher-service-client-secret: {{ .Values.keycloak.realm.clients.sentriusLauncher.client_secret | b64enc }} {{- else }} sentrius-launcher-service-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + {{- if .Values.keycloak.realm.clients.javaAgents.client_secret }} java-agents-client-secret: {{ .Values.keycloak.realm.clients.javaAgents.client_secret | b64enc }} {{- else }} java-agents-client-secret: {{ randAlphaNum 32 | b64enc }} {{- end }} - + {{- if .Values.keycloak.realm.clients.aiAgentAssessor.client_secret }} ai-agent-assessor-client-secret: {{ .Values.keycloak.realm.clients.aiAgentAssessor.client_secret | b64enc }} {{- else }} ai-agent-assessor-client-secret: {{ randAlphaNum 32 | b64enc }} - {{- end }} \ No newline at end of file + {{- end }} + + # Agent Proxy OAuth2 Client Secret + {{- if .Values.agentproxy.oauth2.client_secret }} + agentproxy-client-secret: {{ .Values.agentproxy.oauth2.client_secret | b64enc }} + {{- else }} + agentproxy-client-secret: {{ randAlphaNum 32 | b64enc }} + {{- end }} diff --git a/sentrius-chart/templates/secret.yaml b/sentrius-chart/templates/secret.yaml index 86fc4061..9f491a5a 100644 --- a/sentrius-chart/templates/secret.yaml +++ b/sentrius-chart/templates/secret.yaml @@ -2,20 +2,10 @@ apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-db-secret + annotations: + "helm.sh/resource-policy": keep type: Opaque data: - {{- if .Values.secrets.db.username }} - db-username: {{ .Values.secrets.db.username | b64enc }} - {{- else }} - db-username: {{ "admin" | b64enc }} - {{- end }} - {{- if .Values.secrets.db.password }} - db-password: {{ .Values.secrets.db.password | b64enc }} - {{- else }} - db-password: {{ randAlphaNum 32 | b64enc }} - {{- end }} - {{- if .Values.secrets.db.keystorePassword }} - keystore-password: {{ .Values.secrets.db.keystorePassword | b64enc }} - {{- else }} - keystore-password: {{ randAlphaNum 24 | b64enc }} - {{- end }} + db-username: {{ (.Values.secrets.db.username | default "admin") | b64enc }} + db-password: {{ (.Values.secrets.db.password | default (randAlphaNum 32)) | b64enc }} + keystore-password: {{ (.Values.secrets.db.keystorePassword | default (randAlphaNum 24)) | b64enc }} diff --git a/sentrius-chart/templates/selfsigned-issuer.yaml b/sentrius-chart/templates/selfsigned-issuer.yaml new file mode 100644 index 00000000..df27a6b0 --- /dev/null +++ b/sentrius-chart/templates/selfsigned-issuer.yaml @@ -0,0 +1,9 @@ +{{- if and (eq .Values.environment "local") (.Values.certificates.enabled) }} +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: {} +{{- end }} \ No newline at end of file diff --git a/sentrius-chart/values.yaml b/sentrius-chart/values.yaml index 76cb3a4d..6b407018 100644 --- a/sentrius-chart/values.yaml +++ b/sentrius-chart/values.yaml @@ -9,8 +9,11 @@ subdomain: "{{ .Values.tenant }}.sentrius.cloud" keycloakSubdomain: "keycloak.{{ .Values.tenant }}.sentrius.cloud" keycloakHostname: "keycloak.{{ .Values.tenant }}.sentrius.cloud" keycloakDomain: "https://keycloak.{{ .Values.tenant }}.sentrius.cloud" +keycloakInternalDomain: http://sentrius-keycloak:8081 # Internal cluster communication sentriusDomain: "https://{{ .Values.tenant }}.sentrius.cloud" launcherFQDN: sentrius-launcher-service.dev.svc.cluster.local +agentproxySubdomain: "agentproxy.{{ .Values.tenant }}.sentrius.cloud" +agentproxyDomain: "https://agentproxy.{{ .Values.tenant }}.sentrius.cloud" certificates: @@ -70,6 +73,30 @@ integrationproxy: azure: service.beta.kubernetes.io/azure-load-balancer-internal: "true" +agentproxy: + image: + repository: sentrius-agent-proxy + tag: tag + pullPolicy: IfNotPresent + port: 8080 + serviceType: ClusterIP + ssh: + port: 22 + resources: {} + oauth2: + client_id: sentrius-agent-proxy + client_secret: "" # To be set via environment variable or external secret + authorization_grant_type: authorization_code + redirect_uri: http://{{ .Values.subdomain }}/login/oauth2/code/keycloak + scope: openid,profile,email + issuer_uri: http://keycloak.{{ .Values.subdomain }}/realms/sentrius + annotations: + gke: + cloud.google.com/backend-config: '{"default": "sentrius-backend-config"}' + aws: + service.beta.kubernetes.io/aws-load-balancer-type: "nlb" + azure: + service.beta.kubernetes.io/azure-load-balancer-internal: "true" sentriusagent: service: @@ -108,7 +135,7 @@ sentriusaiagent: pullPolicy: IfNotPresent port: 8080 oauth2: - client_id: java-agents + client_id: ai-agent-assessor client_secret: "" # To be set via environment variable or external secret authorization_grant_type: authorization_code redirect-uri: http://{{ .Values.subdomain }}/login/oauth2/code/keycloak @@ -186,6 +213,8 @@ keycloak: serviceType: ClusterIP port: 8081 clientId: sentrius-api + google: + clientId: google-sentrius-api clientSecret: "" # To be set via environment variable or external secret db: image: postgres:15 @@ -218,6 +247,10 @@ ingress: class: "nginx" # Default for local; override for GKE/AWS tlsEnabled: true # Enable TLS when supported annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/backend-protocol: HTTP + # Environment-specific annotation sets (use via --set-file or values override) + annotationSets: gke: # GKE-specific annotations kubernetes.io/ingress.class: gce networking.gke.io/managed-certificates: wildcard-cert @@ -229,6 +262,8 @@ ingress: alb.ingress.kubernetes.io/ssl-redirect: "443" local: # Local environment annotations (e.g., Minikube) kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/use-forwarded-headers: "true"