diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/Chart.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/Chart.yaml new file mode 100644 index 00000000..75de8fe6 --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +name: ingress-authz-overlay +description: Authz-aware ingress overlay providing unified path-based routing with centralized authorization for multi-tenant UIs and APIs +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - ingress + - authorization + - multi-tenant + - nginx + - argo +home: https://github.com/calypr/argo-helm +maintainers: + - name: calypr + url: https://github.com/calypr diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/README.md b/helm/argo-stack/overlays/ingress-authz-overlay/README.md new file mode 100644 index 00000000..02a89464 --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/README.md @@ -0,0 +1,65 @@ +# Ingress AuthZ Overlay + +A Helm overlay chart providing unified, path-based ingress with centralized authorization for multi-tenant Argo Stack deployments. + +## Overview + +This overlay provides a **single host, path-based ingress** for all major UIs and APIs: + +| Path | Service | Description | +|------|---------|-------------| +| `/workflows` | Argo Workflows Server | Workflow UI (port 2746) | +| `/applications` | Argo CD Server | GitOps applications UI (port 8080) | +| `/registrations` | GitHub EventSource | Repository registration events (port 12000) | +| `/api` | Calypr API | Platform API service (port 3000) | +| `/tenants` | Calypr Tenants | Tenant portal (port 3001) | + +All endpoints are protected by the `authz-adapter` via NGINX external authentication. + +## Quick Start + +```bash +# Install the overlay +helm upgrade --install ingress-authz-overlay \ + helm/argo-stack/overlays/ingress-authz-overlay \ + --namespace argo-stack \ + --create-namespace + +# With custom host +helm upgrade --install ingress-authz-overlay \ + helm/argo-stack/overlays/ingress-authz-overlay \ + --namespace argo-stack \ + --set ingressAuthzOverlay.host=my-domain.example.com +``` + +## Configuration + +See [`values.yaml`](values.yaml) for all configurable options. + +Key settings: + +```yaml +ingressAuthzOverlay: + enabled: true + host: calypr-demo.ddns.net + tls: + enabled: true + secretName: calypr-demo-tls + clusterIssuer: letsencrypt-prod +``` + +## Documentation + +- [User Guide](docs/authz-ingress-user-guide.md) - Complete installation and configuration guide +- [Acceptance Tests](tests/authz-ingress.feature) - Gherkin-style test scenarios + +## Architecture + +See the [User Guide](docs/authz-ingress-user-guide.md) for architecture diagrams and detailed flow descriptions. + +## Requirements + +- Kubernetes 1.19+ +- Helm 3.x +- NGINX Ingress Controller +- cert-manager (for TLS) diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md b/helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md new file mode 100644 index 00000000..a54d5aed --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md @@ -0,0 +1,367 @@ +# Authz-Aware Ingress Overlay User Guide + +## Overview + +The `ingress-authz-overlay` is a Helm overlay chart that provides a unified, path-based ingress layer for all major UIs and APIs in the Argo Stack. It centralizes authorization through the `authz-adapter` service, ensuring consistent access control across all endpoints. + +## Features + +- **Single Host**: All services exposed on one HTTPS hostname +- **Path-Based Routing**: Clean URL structure (`/workflows`, `/applications`, `/api`, etc.) +- **Centralized Authorization**: All routes protected by `authz-adapter` via NGINX external auth +- **TLS via cert-manager**: Automatic Let's Encrypt certificate management +- **Multi-Tenant Support**: User, email, and group headers passed to backend services +- **Drop-In Deployment**: Simple Helm overlay that can be enabled or disabled per environment + +## Architecture + +```mermaid +sequenceDiagram + participant User + participant Ingress as NGINX Ingress + participant AuthzAdapter as authz-adapter + participant Workflows as Argo Workflows + participant Applications as Argo CD + participant Registrations as Event Source + participant Api as Calypr API + participant Tenants as Calypr Tenants + + User->>Ingress: HTTPS GET /path + Ingress->>AuthzAdapter: auth-url check + AuthzAdapter-->>Ingress: Allow or Deny + alt Allowed + Note over Ingress: Route based on path + Ingress->>Workflows: /workflows... + Ingress->>Applications: /applications... + Ingress->>Registrations: /registrations... + Ingress->>Api: /api... + Ingress->>Tenants: /tenants... + else Denied + Ingress-->>User: Redirect to /tenants/login + end +``` + +## Routes + +| Path | Service | Port | Namespace | Description | +|------|---------|------|-----------|-------------| +| `/workflows` | `argo-stack-argo-workflows-server` | 2746 | `argo-stack` | Argo Workflows UI | +| `/applications` | `argo-stack-argocd-server` | 8080 | `argo-stack` | Argo CD Applications UI | +| `/registrations` | `github-repo-registrations-eventsource-svc` | 12000 | `argo-stack` | GitHub Repo Registration Events | +| `/api` | `calypr-api` | 3000 | `calypr-api` | Calypr API Service | +| `/tenants` | `calypr-tenants` | 3001 | `calypr-tenants` | Calypr Tenant Portal | + +## TLS with Let's Encrypt and cert-manager + +This overlay uses [cert-manager](https://cert-manager.io/) to automatically provision and renew TLS certificates from [Let's Encrypt](https://letsencrypt.org/). + +### How It Works + +```mermaid +sequenceDiagram + participant Ingress as Ingress Resource + participant CM as cert-manager + participant LE as Let's Encrypt + participant DNS as DNS Provider + + Note over Ingress: Created with annotation:
cert-manager.io/cluster-issuer: letsencrypt-prod + Ingress->>CM: Ingress triggers Certificate request + CM->>LE: Request certificate for domain + LE->>CM: ACME challenge (HTTP-01 or DNS-01) + CM->>DNS: Prove domain ownership + DNS-->>LE: Challenge verified + LE-->>CM: Issue certificate + CM->>Ingress: Store cert in TLS Secret + Note over Ingress: HTTPS now available +``` + +### ClusterIssuer: letsencrypt-prod + +The `letsencrypt-prod` ClusterIssuer is a cluster-wide cert-manager resource that defines how to obtain certificates from Let's Encrypt's production API. + +**Prerequisites**: You must create the ClusterIssuer before deploying this overlay: + +```yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + # Let's Encrypt production API endpoint + server: https://acme-v02.api.letsencrypt.org/directory + + # Email for certificate expiration notifications + email: your-email@example.com + + # Secret to store the ACME account private key + privateKeySecretRef: + name: letsencrypt-prod-account-key + + # HTTP-01 challenge solver using ingress + solvers: + - http01: + ingress: + class: nginx +``` + +**Apply the ClusterIssuer**: + +```bash +kubectl apply -f cluster-issuer.yaml +``` + +### Configuration Options + +| Setting | Description | Default | +|---------|-------------|---------| +| `tls.enabled` | Enable TLS for ingress | `true` | +| `tls.secretName` | Name of the TLS Secret (auto-created by cert-manager) | `calypr-demo-tls` | +| `tls.clusterIssuer` | Name of the ClusterIssuer to use | `letsencrypt-prod` | + +### Using letsencrypt-staging (for Testing) + +For testing, use the staging issuer to avoid Let's Encrypt rate limits: + +```yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: your-email@example.com + privateKeySecretRef: + name: letsencrypt-staging-account-key + solvers: + - http01: + ingress: + class: nginx +``` + +Then configure the overlay to use it: + +```yaml +ingressAuthzOverlay: + tls: + clusterIssuer: letsencrypt-staging +``` + +### Verifying Certificate Status + +Check if the certificate was issued successfully: + +```bash +# Check Certificate resource +kubectl get certificate -n argo-stack + +# Check certificate details +kubectl describe certificate -n argo-stack + +# Check the TLS secret +kubectl get secret calypr-demo-tls -n argo-stack +``` + +### Troubleshooting Certificates + +If the certificate is not being issued: + +```bash +# Check cert-manager logs +kubectl logs -n cert-manager -l app=cert-manager + +# Check Certificate status +kubectl describe certificate -n argo-stack + +# Check CertificateRequest +kubectl get certificaterequest -n argo-stack + +# Check ACME challenges +kubectl get challenges -A +``` + +Common issues: +- **Domain not reachable**: Ensure your domain's DNS points to the ingress controller's external IP +- **Rate limited**: Use `letsencrypt-staging` for testing to avoid production rate limits +- **Challenge failed**: Check that port 80 is accessible for HTTP-01 challenges + +## Installation + +### Prerequisites + +- Kubernetes cluster with NGINX Ingress Controller +- cert-manager installed and configured with a ClusterIssuer (e.g., `letsencrypt-prod`) +- Helm 3.x + +### Install the Overlay + +```bash +# Install with default values +helm upgrade --install ingress-authz-overlay \ + helm/argo-stack/overlays/ingress-authz-overlay \ + --namespace argo-stack \ + --create-namespace + +# Install with custom host +helm upgrade --install ingress-authz-overlay \ + helm/argo-stack/overlays/ingress-authz-overlay \ + --namespace argo-stack \ + --set ingressAuthzOverlay.host=my-domain.example.com \ + --set ingressAuthzOverlay.tls.secretName=my-domain-tls +``` + +### Integrate with Parent Chart + +Alternatively, add the values to your main `argo-stack` deployment: + +```bash +helm upgrade --install argo-stack \ + helm/argo-stack \ + --values helm/argo-stack/values.yaml \ + --set ingressAuthzOverlay.enabled=true +``` + +## Configuration + +### Basic Configuration + +```yaml +ingressAuthzOverlay: + enabled: true + host: calypr-demo.ddns.net + tls: + enabled: true + secretName: calypr-demo-tls + clusterIssuer: letsencrypt-prod +``` + +### AuthZ Adapter Configuration + +```yaml +ingressAuthzOverlay: + authzAdapter: + # Disable if authz-adapter is deployed separately + deploy: true + + # Service location + serviceName: authz-adapter + namespace: argo-stack + port: 8080 + path: /check + + # Sign-in redirect URL + signinUrl: https://calypr-demo.ddns.net/tenants/login + + # Headers passed from auth response to backends + responseHeaders: "X-User,X-Email,X-Groups" + + # Environment configuration + env: + fenceBase: "https://calypr-dev.ohsu.edu/user" +``` + +### Custom Routes + +Add or modify routes as needed: + +```yaml +ingressAuthzOverlay: + routes: + # Custom route example + myservice: + enabled: true + namespace: my-namespace + service: my-service + port: 8000 + pathPrefix: /myservice + useRegex: true + rewriteTarget: /$2 +``` + +### Disabling a Route + +```yaml +ingressAuthzOverlay: + routes: + registrations: + enabled: false +``` + +## Authorization Flow + +1. **User Request**: Client sends HTTPS request to the ingress host +2. **External Auth**: NGINX Ingress calls the `authz-adapter` `/check` endpoint +3. **Token Validation**: `authz-adapter` validates the Authorization header against Fence/OIDC +4. **Group Assignment**: User is assigned groups based on their permissions (e.g., `argo-runner`, `argo-viewer`) +5. **Response Headers**: On success, user info headers are added to the request +6. **Routing**: Request is forwarded to the appropriate backend service +7. **Denial**: On failure, user is redirected to the sign-in URL + +### Auth Response Headers + +The following headers are passed to backend services on successful authentication: + +| Header | Description | +|--------|-------------| +| `X-Auth-Request-User` | Username or email of the authenticated user | +| `X-Auth-Request-Email` | Email address of the user | +| `X-Auth-Request-Groups` | Comma-separated list of groups | +| `X-User` | Alias for X-Auth-Request-User | +| `X-Email` | Alias for X-Auth-Request-Email | +| `X-Groups` | Alias for X-Auth-Request-Groups | + +## Troubleshooting + +### Check Ingress Status + +```bash +kubectl get ingress -A -l app.kubernetes.io/name=ingress-authz-overlay +``` + +### Check AuthZ Adapter + +```bash +# Logs +kubectl logs -n argo-stack -l app=authz-adapter + +# Test health endpoint +kubectl port-forward -n argo-stack svc/authz-adapter 8080:8080 & +curl http://localhost:8080/healthz +``` + +### Test Authentication + +```bash +# Should redirect to login +curl -I https://calypr-demo.ddns.net/workflows + +# With valid token (should return 200) +curl -I -H "Authorization: Bearer $TOKEN" https://calypr-demo.ddns.net/workflows +``` + +### Common Issues + +1. **502 Bad Gateway**: AuthZ adapter not reachable + - Check authz-adapter deployment is running + - Verify service selector matches pod labels + +2. **503 Service Unavailable**: Backend service not available + - Check target service exists in the specified namespace + - Verify service port matches configuration + +3. **Redirect Loop**: Auth signin URL misconfigured + - Ensure `/tenants/login` path is accessible + - Check signinUrl matches actual login endpoint + +## Uninstall + +```bash +helm uninstall ingress-authz-overlay -n argo-stack +``` + +## Related Documentation + +- [Argo Stack User Guide](../../docs/user-guide.md) +- [Tenant Onboarding Guide](../../docs/tenant-onboarding.md) +- [Repo Registration Guide](../../docs/repo-registration-guide.md) diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/templates/_helpers.tpl b/helm/argo-stack/overlays/ingress-authz-overlay/templates/_helpers.tpl new file mode 100644 index 00000000..e8f2468d --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/templates/_helpers.tpl @@ -0,0 +1,72 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ingress-authz-overlay.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "ingress-authz-overlay.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ingress-authz-overlay.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ingress-authz-overlay.labels" -}} +helm.sh/chart: {{ include "ingress-authz-overlay.chart" . }} +{{ include "ingress-authz-overlay.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ingress-authz-overlay.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ingress-authz-overlay.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the auth-url for NGINX ingress external auth. +*/}} +{{- define "ingress-authz-overlay.authUrl" -}} +{{- $adapter := .Values.ingressAuthzOverlay.authzAdapter -}} +http://{{ $adapter.serviceName }}.{{ $adapter.namespace }}.svc.cluster.local:{{ $adapter.port }}{{ $adapter.path }} +{{- end }} + +{{/* +Create common ingress annotations for NGINX external auth. +*/}} +{{- define "ingress-authz-overlay.authAnnotations" -}} +nginx.ingress.kubernetes.io/auth-url: {{ include "ingress-authz-overlay.authUrl" . | quote }} +nginx.ingress.kubernetes.io/auth-method: "GET" +nginx.ingress.kubernetes.io/auth-signin: {{ .Values.ingressAuthzOverlay.authzAdapter.signinUrl | quote }} +nginx.ingress.kubernetes.io/auth-response-headers: {{ .Values.ingressAuthzOverlay.authzAdapter.responseHeaders | quote }} +nginx.ingress.kubernetes.io/auth-snippet: | + proxy_set_header Authorization $http_authorization; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Forwarded-Host $host; +{{- end }} diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/templates/authz-adapter.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/templates/authz-adapter.yaml new file mode 100644 index 00000000..194c1ea8 --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/templates/authz-adapter.yaml @@ -0,0 +1,107 @@ +{{/* +AuthZ Adapter Deployment and Service for the ingress-authz-overlay. +The authz-adapter provides external authentication for NGINX Ingress, +validating tokens and returning user/group information. +*/}} +{{- if and .Values.ingressAuthzOverlay.enabled .Values.ingressAuthzOverlay.authzAdapter.deploy }} +{{- $config := .Values.ingressAuthzOverlay }} +{{- $adapter := $config.authzAdapter }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $adapter.serviceName }} + namespace: {{ $adapter.namespace }} + labels: + {{- include "ingress-authz-overlay.labels" . | nindent 4 }} + app.kubernetes.io/component: authz-adapter + app: {{ $adapter.serviceName }} + annotations: + meta.helm.sh/release-name: {{ .Release.Name }} + meta.helm.sh/release-namespace: {{ .Release.Namespace }} +spec: + replicas: {{ $adapter.replicas | default 2 }} + selector: + matchLabels: + app: {{ $adapter.serviceName }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ $adapter.serviceName }} + {{- include "ingress-authz-overlay.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: authz-adapter + spec: + {{- with $adapter.securityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: authz-adapter + image: {{ $adapter.image }} + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + ports: + - name: http + containerPort: {{ $adapter.port }} + protocol: TCP + env: + - name: FENCE_BASE + value: {{ $adapter.env.fenceBase | quote }} + - name: TENANT_LOGIN_PATH + value: {{ $adapter.env.tenantLoginPath | default "/tenants/login" | quote }} + - name: HTTP_TIMEOUT + value: {{ $adapter.env.httpTimeout | default "3.0" | quote }} + {{- if $adapter.env.gitappBaseUrl }} + - name: GITAPP_BASE_URL + value: {{ $adapter.env.gitappBaseUrl | quote }} + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 2 + {{- with $adapter.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $adapter.serviceName }} + namespace: {{ $adapter.namespace }} + labels: + {{- include "ingress-authz-overlay.labels" . | nindent 4 }} + app.kubernetes.io/component: authz-adapter + app: {{ $adapter.serviceName }} + annotations: + meta.helm.sh/release-name: {{ .Release.Name }} + meta.helm.sh/release-namespace: {{ .Release.Namespace }} +spec: + type: ClusterIP + selector: + app: {{ $adapter.serviceName }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: http + port: {{ $adapter.port }} + targetPort: http + protocol: TCP +{{- end }} diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/templates/ingress-authz.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/templates/ingress-authz.yaml new file mode 100644 index 00000000..6f81fae3 --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/templates/ingress-authz.yaml @@ -0,0 +1,63 @@ +{{/* +Ingress resources for each route in the ingress-authz-overlay. +Each route creates a separate Ingress resource in its respective namespace, +all sharing the same host and TLS configuration. +All routes are protected by the authz-adapter via NGINX external auth. +*/}} +{{- if .Values.ingressAuthzOverlay.enabled }} +{{- $root := . }} +{{- $config := .Values.ingressAuthzOverlay }} +{{- range $routeName, $route := $config.routes }} +{{- if $route.enabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-authz-{{ $routeName }} + namespace: {{ $route.namespace }} + labels: + {{- include "ingress-authz-overlay.labels" $root | nindent 4 }} + app.kubernetes.io/component: ingress + ingress-authz-overlay.calypr.io/route: {{ $routeName | quote }} + annotations: + # Helm release tracking + meta.helm.sh/release-name: {{ $root.Release.Name }} + meta.helm.sh/release-namespace: {{ $root.Release.Namespace }} + # NGINX external auth annotations + {{- include "ingress-authz-overlay.authAnnotations" $root | nindent 4 }} + {{- if $config.tls.enabled }} + # Let's Encrypt / cert-manager integration + cert-manager.io/cluster-issuer: {{ $config.tls.clusterIssuer | quote }} + {{- end }} + {{- if $route.useRegex }} + # Path rewriting for subpath support + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: {{ $route.rewriteTarget | default "/$2" }} + {{- end }} +spec: + ingressClassName: {{ $config.ingressClassName | default "nginx" }} + {{- if $config.tls.enabled }} + tls: + - hosts: + - {{ $config.host | quote }} + secretName: {{ $config.tls.secretName | quote }} + {{- end }} + rules: + - host: {{ $config.host | quote }} + http: + paths: + {{- if $route.useRegex }} + - path: {{ $route.pathPrefix }}(/|$)(.*) + pathType: ImplementationSpecific + {{- else }} + - path: {{ $route.pathPrefix }} + pathType: Prefix + {{- end }} + backend: + service: + name: {{ $route.service }} + port: + number: {{ $route.port }} +{{- end }} +{{- end }} +{{- end }} diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/tests/authz-ingress.feature b/helm/argo-stack/overlays/ingress-authz-overlay/tests/authz-ingress.feature new file mode 100644 index 00000000..e84c7a8c --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/tests/authz-ingress.feature @@ -0,0 +1,72 @@ +Feature: Authz ingress overlay + + Background: + Given the ingress-authz-overlay is installed + And the hostname "calypr-demo.ddns.net" resolves to the ingress endpoint + + Scenario: Unauthenticated user is redirected to login + When I send a GET request to "https://calypr-demo.ddns.net/workflows" + Then the response status should be 302 or 303 + And the "Location" header should contain "/tenants/login" + + Scenario: Authenticated user can access workflows + Given I have a valid session recognized by authz-adapter + When I send a GET request to "https://calypr-demo.ddns.net/workflows" + Then the response status should be 200 + + Scenario: All paths are protected by authz-adapter + When I send a GET request to "https://calypr-demo.ddns.net/applications" without credentials + Then I should be redirected to "/tenants/login" + + When I send a GET request to "https://calypr-demo.ddns.net/registrations" without credentials + Then I should be redirected to "/tenants/login" + + When I send a GET request to "https://calypr-demo.ddns.net/api" without credentials + Then I should be redirected to "/tenants/login" + + When I send a GET request to "https://calypr-demo.ddns.net/tenants" without credentials + Then I should be redirected to "/tenants/login" or served only public content as configured + + Scenario: TLS certificate is valid + When I connect to "https://calypr-demo.ddns.net" + Then the TLS certificate should be issued by "Let's Encrypt" + And the certificate subject alt name should include "calypr-demo.ddns.net" + + Scenario: Routing sends requests to the correct services + Given I am authenticated + When I send a GET request to "https://calypr-demo.ddns.net/workflows" + Then the response should contain an HTML title for the workflows UI + + When I send a GET request to "https://calypr-demo.ddns.net/applications" + Then the response should contain an HTML title for the applications UI + + When I send a GET request to "https://calypr-demo.ddns.net/api/health" + Then I should receive a 200 response with a JSON health object from the API + + When I send a GET request to "https://calypr-demo.ddns.net/tenants" + Then I should see the tenant portal landing page or login as configured + + Scenario: Auth response headers are passed to backend + Given I am authenticated with user "test@example.com" in groups "argo-runner,argo-viewer" + When I send a GET request to "https://calypr-demo.ddns.net/api/whoami" + Then the backend should receive header "X-Auth-Request-User" with value "test@example.com" + And the backend should receive header "X-Auth-Request-Groups" with value "argo-runner,argo-viewer" + + Scenario: Path rewriting works correctly + Given I am authenticated + When I send a GET request to "https://calypr-demo.ddns.net/workflows/workflow-details/my-workflow" + Then the Argo Workflows server should receive path "/workflow-details/my-workflow" + + When I send a GET request to "https://calypr-demo.ddns.net/api/v1/users" + Then the Calypr API should receive path "/v1/users" + + Scenario: Health check endpoint is accessible + When I send a GET request to "http://authz-adapter.argo-stack.svc.cluster.local:8080/healthz" + Then the response status should be 200 + And the response body should be "ok" + + Scenario: Multiple simultaneous requests are handled + Given I am authenticated + When I send 10 concurrent GET requests to "https://calypr-demo.ddns.net/workflows" + Then all responses should have status 200 + And the average response time should be less than 500ms diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/values.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/values.yaml new file mode 100644 index 00000000..ed3b71a4 --- /dev/null +++ b/helm/argo-stack/overlays/ingress-authz-overlay/values.yaml @@ -0,0 +1,147 @@ +# ============================================================================ +# Ingress AuthZ Overlay Configuration +# ============================================================================ +# This overlay provides a single host, path-based ingress layer for all +# major UIs and APIs, protected by a centralized authz-adapter. +# +# Usage: +# helm upgrade --install ingress-authz-overlay \ +# helm/argo-stack/overlays/ingress-authz-overlay \ +# --set ingressAuthzOverlay.enabled=true +# ============================================================================ + +ingressAuthzOverlay: + # Enable or disable the overlay + enabled: true + + # ============================================================================ + # Host and TLS Configuration + # ============================================================================ + # Single host for all path-based routes + host: calypr-demo.ddns.net + + # TLS configuration using cert-manager + tls: + enabled: true + secretName: calypr-demo-tls + clusterIssuer: letsencrypt-prod + + # ============================================================================ + # Ingress Controller Configuration + # ============================================================================ + ingressClassName: nginx + + # ============================================================================ + # AuthZ Adapter Configuration + # ============================================================================ + authzAdapter: + # Enable deployment of authz-adapter (set to false if deployed separately) + deploy: true + + # Service discovery settings + serviceName: authz-adapter + namespace: argo-stack + port: 8080 + + # Auth endpoint path + path: /check + + # Sign-in URL for unauthenticated requests + signinUrl: https://calypr-demo.ddns.net/tenants/login + + # Headers to pass back from auth response + responseHeaders: "X-User,X-Email,X-Groups,X-Auth-Request-User,X-Auth-Request-Email,X-Auth-Request-Groups" + + # Container image for authz-adapter + image: ghcr.io/calypr/argo-helm:latest + + # Number of replicas + replicas: 2 + + # Environment configuration for the adapter + env: + # GitApp/Fence base URL for user info + fenceBase: "https://calypr-dev.ohsu.edu/user" + # Tenant login path + tenantLoginPath: "/tenants/login" + # HTTP timeout for auth calls + httpTimeout: "3.0" + + # Resource limits and requests + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + + # Pod security context + securityContext: + runAsNonRoot: true + runAsUser: 1000 + + # ============================================================================ + # Route Definitions + # ============================================================================ + # Each route creates a separate Ingress resource in the specified namespace. + # All routes share the same host and TLS configuration. + # All routes are protected by the authz-adapter via NGINX external auth. + routes: + # Argo Workflows UI + workflows: + enabled: true + namespace: argo-stack + service: argo-stack-argo-workflows-server + port: 2746 + pathPrefix: /workflows + # Use regex path matching for subpaths + useRegex: true + # Rewrite path to remove prefix + rewriteTarget: /$2 + + # Argo CD Applications UI + applications: + enabled: true + namespace: argo-stack + service: argo-stack-argocd-server + port: 8080 + pathPrefix: /applications + useRegex: true + rewriteTarget: /$2 + + # GitHub Repository Registrations EventSource + registrations: + enabled: true + namespace: argo-stack + service: github-repo-registrations-eventsource-svc + port: 12000 + pathPrefix: /registrations + useRegex: true + rewriteTarget: /$2 + + # Calypr API Service + api: + enabled: true + namespace: calypr-api + service: calypr-api + port: 3000 + pathPrefix: /api + useRegex: true + rewriteTarget: /$2 + + # Calypr Tenants Service + tenants: + enabled: true + namespace: calypr-tenants + service: calypr-tenants + port: 3001 + pathPrefix: /tenants + useRegex: true + rewriteTarget: /$2 + # Optional: Allow public access to login endpoint + # Set to true to skip auth for /tenants/login + publicPaths: + - /tenants/login + - /tenants/logout + - /tenants/callback diff --git a/helm/argo-stack/values.yaml b/helm/argo-stack/values.yaml index 4215afd3..76275e6b 100644 --- a/helm/argo-stack/values.yaml +++ b/helm/argo-stack/values.yaml @@ -215,6 +215,59 @@ ingressAuth: authURL: "http://authz-adapter.security.svc.cluster.local:8080/check" passAuthorization: true +# ============================================================================ +# Ingress AuthZ Overlay - Unified Path-Based Routing with Centralized Auth +# ============================================================================ +# Enable this overlay to provide a single host, path-based ingress for all +# major UIs and APIs, protected by the authz-adapter. +# See: helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md +# +# To use the overlay, install it separately: +# helm upgrade --install ingress-authz-overlay \ +# helm/argo-stack/overlays/ingress-authz-overlay \ +# --values helm/argo-stack/values.yaml \ +# --set ingressAuthzOverlay.enabled=true + +ingressAuthzOverlay: + enabled: false + host: calypr-demo.ddns.net + tls: + secretName: calypr-demo-tls + clusterIssuer: letsencrypt-prod + authzAdapter: + serviceName: authz-adapter + namespace: argo-stack + port: 8080 + path: /check + signinUrl: https://calypr-demo.ddns.net/tenants/login + responseHeaders: X-User, X-Email, X-Groups + routes: + workflows: + namespace: argo-stack + service: argo-stack-argo-workflows-server + port: 2746 + pathPrefix: /workflows + applications: + namespace: argo-stack + service: argo-stack-argocd-server + port: 8080 + pathPrefix: /applications + registrations: + namespace: argo-stack + service: github-repo-registrations-eventsource-svc + port: 12000 + pathPrefix: /registrations + api: + namespace: calypr-api + service: calypr-api + port: 3000 + pathPrefix: /api + tenants: + namespace: calypr-tenants + service: calypr-tenants + port: 3001 + pathPrefix: /tenants + # ============================================================================ # Argo CD Applications - Multi-Application Support (REMOVED) # ============================================================================