Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# A long random secret used to sign sessions and tokens (min 32 characters)
STACKABLE_COCKPIT_SESSION_SECRET=change-me-to-a-long-random-secret-min-32-chars

# The publicly accessible base URL of this application
# The publicly accessible base URL of this application.
# In development this is derived from the request host (so any free port works,
# e.g. when 5173 is already taken). In production builds this value is used
# directly for the OIDC redirect URI and must match the public origin.
STACKABLE_COCKPIT_BASE_URL=http://localhost:5173

# OIDC discovery URL (Keycloak, Entra ID, or any compliant OIDC provider)
Expand Down Expand Up @@ -35,4 +38,8 @@ STACKABLE_COCKPIT_OIDC_CLIENT_SECRET=your-client-secret
# Feature flags
# STACKABLE_COCKPIT_COMPLETION_ENABLED=false # Disable SQL editor code completion (default: true)
# STACKABLE_COCKPIT_STORAGE_BROWSER_ENABLED=true # Enable S3/HDFS file browser (default: false)
# PUBLIC_STACKABLE_COCKPIT_UPLOAD_CONCURRENCY=3 # Maximum number of concurrent file uploads (default: 3)

# OPA (Open Policy Agent) — admin rights checking
# STACKABLE_COCKPIT_OPA_ENABLED=true # Enable OPA admin checks (default: false)
# STACKABLE_COCKPIT_OPA_URL=http://localhost:8181 # OPA server base URL (required when enabled)
# STACKABLE_COCKPIT_OPA_TIMEOUT=5000 # OPA request timeout in milliseconds (default: 5000)
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ STACKABLE_COCKPIT_SESSION_SECRET=e2e-test-session-secret-that-is-long-enough
STACKABLE_COCKPIT_BASE_URL=http://localhost:4173
STACKABLE_COCKPIT_TRINO_URL=http://localhost:8080
STACKABLE_COCKPIT_STORAGE_BROWSER_ENABLED=true
STACKABLE_COCKPIT_OPA_ENABLED=true
STACKABLE_COCKPIT_OPA_URL=http://localhost:9191
ORIGIN=http://localhost:4173
STACKABLE_COCKPIT_TEXT_PREVIEW_BYTES=262144
STACKABLE_COCKPIT_IMAGE_PREVIEW_BYTES=5242880
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ coverage/
# Helm templates
deploy/helm/
dev/garage/
dev/opa/
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,

// Helm templates use {{ }} syntax that Prettier mangles — disable formatting.
"[yaml]": {
"editor.formatOnSave": false
},

// Route .svelte formatting through the Svelte extension so it applies the
// Svelte-aware Prettier plugin (configured in .prettierrc).
"[svelte]": {
Expand Down
8 changes: 8 additions & 0 deletions TECH_DEBT.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ All server-side query state (progress, rows, status) is held in a module-level `

---

### Bookmark tools embedded via unsandboxed iframe

**File:** `src/routes/(app)/bookmark/[id]/+page.svelte`

Bookmarks are embedded as full-page iframes without a `sandbox` attribute, so the embedded tool can run scripts, navigate the top frame, and read cookies in its own origin context. A `sandbox` attribute would break legitimate tools that need scripts/forms, and most external services will refuse framing anyway via `X-Frame-Options`/CSP. Acceptable for the current stage; long-term, consider a configurable sandbox policy per bookmark and validation of the URL scheme (http/https only).

---

### Single-file download limit

**File:** `src/lib/storage/download.ts`, `src/lib/components/storage/FileExplorer.svelte`
Expand Down
7 changes: 7 additions & 0 deletions dev/opa/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
apiVersion: v2
name: opa
description: Open Policy Agent for local dev and E2E testing
type: application
version: 0.1.0
appVersion: '1.16.2'
18 changes: 18 additions & 0 deletions dev/opa/policies/admin.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package stackable

default admin = false

# Admin if user ID is in the hardcoded admin list
admin if {
admin_users[input.user.id]
}

# Admin if user email ends with the admin domain
admin if {
endswith(input.user.email, "@admin.example.com")
}

admin_users := {
"admin-user-id-1": true,
"admin-user-id-2": true,
}
9 changes: 9 additions & 0 deletions dev/opa/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: opa-policies
namespace: default
data:
admin.rego: |-
{{- .Files.Get "policies/admin.rego" | nindent 4 }}
60 changes: 60 additions & 0 deletions dev/opa/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: opa
namespace: default
labels:
app: opa
spec:
replicas: 1
selector:
matchLabels:
app: opa
template:
metadata:
labels:
app: opa
spec:
containers:
- name: opa
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- run
- --server
- --addr
- 0.0.0.0:8181
- '{{ .Values.policyMountPath }}/admin.rego'
ports:
- name: http
containerPort: 8181
protocol: TCP
readinessProbe:
httpGet:
path: /health
port: 8181
initialDelaySeconds: 2
periodSeconds: 3
failureThreshold: 10
livenessProbe:
httpGet:
path: /health
port: 8181
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- name: policies
mountPath: '{{ .Values.policyMountPath }}'
readOnly: true
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
volumes:
- name: policies
configMap:
name: opa-policies
15 changes: 15 additions & 0 deletions dev/opa/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
apiVersion: v1
kind: Service
metadata:
name: opa
namespace: default
spec:
type: NodePort
selector:
app: opa
ports:
- name: http
port: 8181
targetPort: 8181
nodePort: {{ .Values.nodePort }}
19 changes: 19 additions & 0 deletions dev/opa/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
image:
repository: openpolicyagent/opa
tag: 1.16.2
pullPolicy: IfNotPresent

# NodePort for the OPA HTTP API.
nodePort: 30181

# Path inside the container where policies are loaded from.
policyMountPath: /policies

resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
34 changes: 31 additions & 3 deletions dev/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ if [[ "$SKIP_GARAGE" == false ]]; then
--timeout 60s
fi

# ------------------------------------------------------------------
# 5c. Deploy OPA (via Helm)
# ------------------------------------------------------------------
echo ""
echo "Deploying OPA..."
helm upgrade --install opa "$SCRIPT_DIR/opa" \
--namespace default \
--wait \
--timeout 60s

# On some local Kubernetes distributions (e.g. Rancher Desktop k3s), the node's
# InternalIP is not reachable from the host network, but NodePorts are exposed
# on localhost. Probe both and use the first reachable URL.
Expand Down Expand Up @@ -171,11 +181,12 @@ else

create_user() {
local username=$1 password=$2 first=$3 last=$4
local email=${5:-"$username@example.com"}
echo "Creating user '$username'..."
kcadm create users \
-r stackable \
-s username="$username" \
-s email="$username@example.com" \
-s email="$email" \
-s firstName="$first" \
-s lastName="$last" \
-s enabled=true
Expand All @@ -185,7 +196,7 @@ else
--new-password "$password"
}

create_user alice alicealice Alice Example
create_user alice alicealice Alice Example alice@admin.example.com
create_user bob bobbob Bob Example

echo "Fetching client secret..."
Expand Down Expand Up @@ -260,6 +271,17 @@ if [[ "$SKIP_TRINO" == false ]]; then
TRINO_BASE_URL="${TRINO_BASE_URL:-https://${NODE_IP}:${TRINO_PORT}}"
fi

# Probe OPA reachability (same pattern as Keycloak/Trino).
OPA_NODE_PORT=30181
OPA_BASE_URL=""
for base in "http://${NODE_IP}:${OPA_NODE_PORT}" "http://127.0.0.1:${OPA_NODE_PORT}" "http://localhost:${OPA_NODE_PORT}"; do
if curl -sf --max-time 2 "${base}/health" >/dev/null 2>&1; then
OPA_BASE_URL="$base"
break
fi
done
OPA_BASE_URL="${OPA_BASE_URL:-http://${NODE_IP}:${OPA_NODE_PORT}}"

if [[ "$SKIP_TRINO" == false ]]; then
cat > "$ENV_FILE" <<EOF
STACKABLE_COCKPIT_OIDC_DISCOVERY_URL=${KEYCLOAK_BASE_URL}/realms/stackable/.well-known/openid-configuration
Expand All @@ -273,6 +295,8 @@ STACKABLE_COCKPIT_TRINO_AUTH_USERNAME=stackable-cockpit
STACKABLE_COCKPIT_TRINO_AUTH_PASSWORD=stackable-cockpit-dev
STACKABLE_COCKPIT_TRINO_TLS_INSECURE=true
STACKABLE_COCKPIT_STORAGE_BROWSER_ENABLED=true
STACKABLE_COCKPIT_OPA_ENABLED=true
STACKABLE_COCKPIT_OPA_URL=${OPA_BASE_URL}
STACKABLE_COCKPIT_TEXT_PREVIEW_BYTES=262144
STACKABLE_COCKPIT_IMAGE_PREVIEW_BYTES=5242880
STACKABLE_COCKPIT_PDF_PREVIEW_BYTES=26214400
Expand All @@ -292,6 +316,8 @@ STACKABLE_COCKPIT_OIDC_CLIENT_SECRET=${SECRET}
STACKABLE_COCKPIT_SESSION_SECRET=${SESSION_SECRET}
STACKABLE_COCKPIT_BASE_URL=http://localhost:5173
STACKABLE_COCKPIT_STORAGE_BROWSER_ENABLED=true
STACKABLE_COCKPIT_OPA_ENABLED=true
STACKABLE_COCKPIT_OPA_URL=${OPA_BASE_URL}
STACKABLE_COCKPIT_TEXT_PREVIEW_BYTES=262144
STACKABLE_COCKPIT_IMAGE_PREVIEW_BYTES=5242880
STACKABLE_COCKPIT_PDF_PREVIEW_BYTES=26214400
Expand Down Expand Up @@ -339,6 +365,8 @@ echo ""
echo "Keycloak: http://${NODE_IP}:30080"
echo " Admin: admin / admin"
echo ""
echo "OPA: ${OPA_BASE_URL}"
echo ""
if [[ "$SKIP_TRINO" == false ]]; then
echo "Trino endpoint: https://${NODE_IP}:${TRINO_PORT}"
echo ""
Expand All @@ -354,5 +382,5 @@ if [[ "$SKIP_GARAGE" == false ]]; then
echo ""
fi
echo "Test users (OIDC):"
echo " alice / alicealice"
echo " alice / alicealice (admin)"
echo " bob / bobbob"
12 changes: 12 additions & 0 deletions e2e/auth/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test as setup, expect } from '@playwright/test';
import path from 'path';
import { waitForHydration } from '../support/helpers.js';
import { ISSUER_URL, PROFILE_COOKIE } from '../support/mock-oidc-server.js';

// Each Playwright project runs its own auth setup, producing a unique
// session. The mock OIDC server issues a different `sub` per token so
Expand All @@ -10,6 +11,17 @@ import { waitForHydration } from '../support/helpers.js';
setup('authenticate via mock OIDC', async ({ page }, testInfo) => {
const authFile = path.join(import.meta.dirname, `../.auth/user-${testInfo.project.name}.json`);

// Projects whose name contains "admin" (e.g. setup-admin) log in with an
// admin profile so the mock OPA server grants them admin rights.
const isAdmin = testInfo.project.name.includes('admin');
await page.context().addCookies([
{
name: PROFILE_COOKIE,
value: isAdmin ? 'admin' : 'regular',
url: ISSUER_URL
}
]);

// Navigate to the app — auth guard redirects to /auth/login
await page.goto('/');
await expect(page).toHaveURL(/\/auth\/login/);
Expand Down
Loading