This layer provides a Kubernetes-native control plane for collaborative, data-driven workspaces.
It combines a FastAPI backend (workspace_api) with a Quasar/Vue frontend (workspace_ui) to deliver a seamless management layer on top of Kubernetes Custom Resources.
At its core, the Workspace API & UI Layer orchestrates three pillars:
- Memberships — define who belongs to a workspace and what role they hold.
- Storage — provision buckets, attach credentials, and manage access grants between workspaces.
- Interactive Sessions — track and control whether interactive workspace sessions are running (always-on) or can be started (on-demand), respectively.
By building on Kubernetes CRDs (Storage, Datalab), the API exposes a clean HTTP/JSON interface and an optional single-page UI to manage these resources without needing to interact directly with kubectl. This makes it equally suited for operators (who want Kubernetes-level control) and end users (who just need to join a workspace, get storage, and start analyzing data).
Kubernetes-native: The Workspace API sits on top of two CRDs — Storage and Datalab — and reads and patches them to present a unified “Workspace” view (including storage, memberships, and session state). It applies changes through standard REST calls, simplifying access and abstracting away the low-level details of the CRDs and Kubernetes.
See: Storage CRD · Datalab CRD
As the Workspace API runs directly on Kubernetes, the ServiceAccount executing it requires minimal RBAC (Role-Based Access Control) permissions to operate on resources such as secrets, storages, and datalabs.
These permissions allow the service to list, watch, and modify Custom Resources within its namespace and to read their CRD definitions.
Both a Role and ClusterRole are automatically provisioned through the Helm chart.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["pkg.internal"]
resources: ["storages", "datalabs"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
rules:
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get"]
resourceNames: ["storages.pkg.internal", "datalabs.pkg.internal"]These permissions enable the API to synchronize resource state and discover CRD schemas while maintaining namespace isolation and least privilege.
There are three modes that define how the API initializes the default Datalab session for a newly created team:
-
Disabled (
SESSION_MODE=off) — The API creates theDatalabresource without declaring a default session. The workspace is primarily used to provision storage buckets and manage bucket access. Operators can still manage sessions directly on the correspondingDatalabCustom Resource. -
Always On (
SESSION_MODE=on) — The API declares the default Datalab session with statestartedduring workspace provisioning. Operators can manually patch theDatalabresource in the cluster to stop or restart the session if required. -
On Demand (
SESSION_MODE=auto) — The API declares the default Datalab session with statestopped. The Workspace UI exposes a Datalab link that starts the session by patching it tostartedwhen a team needs access. Operators can define external policies for automatic shutdowns of sessions (for example, every day at 8 p.m. or every Friday night). When a team needs access again, they can relaunch the session via the Datalab link in the Workspace UI.
Note:
The Workspace UI can manage multiple sessions per team. By default, up to three sessions may be declared; operators can change this with MAX_SESSIONS.
- Workspace API —
workspace_api/(FastAPI backend) - Workspace UI —
workspace_ui/(Quasar/Vue app; built assets placed inworkspace_ui/dist/)- Luigi Shell — provides the micro frontend navigation and layout
- Management app — a single-page application (SPA) embedded via Luigi as a view, using Quasar.js/Vue
.
├── workspace_api/ # Python FastAPI backend
└── workspace_ui/ # Luigi shell + Vue frontend views
├── luigi-shell/
│ ├── ui.html # Luigi shell template (rendered by FastAPI)
│ ├── logo.svg # Main logo
│ ├── icons/ # favicon.ico
│ └── standalone/ # Luigi shell with statically defined workspace data
├── management/ # Quasar App
│ ├── index.html # Vue app entry point (used inside Luigi iframe)
│ └── dist/ # Built Quasar App
└── dist/ # Combined built UI code, served as static content- Python 3.12 (e.g., via pyenv)
- uv for Python deps
- Node.js 20.x + npm for the frontend
- Docker (optional)
-
Backend setup (from repo root):
pyenv local 3.12.11 python --version # should be 3.12.11 uv lock --python python uv sync --python python --extra dev uv run pre-commit install
-
Frontend setup (from repo root):
cd workspace_ui ./build_dist.sh cd ..
KUBECONFIG=~/.kube/config-eoepca-demo uv run uvicorn workspace_api:app --reload --host=0.0.0.0 --port=8181 --log-level=infoThe API will be at http://localhost:8181.
Run the Quasar/Vite dev server (default: http://localhost:9000):
From the workspace_ui/management folder:
npm run devThen in another terminal, from the workspace_api/ folder:
KUBECONFIG=~/.kube/config-eoepca-demo UI_MODE="ui" FRONTEND_URL="http://localhost:9000" uv run uvicorn workspace_api:app --reload --host=0.0.0.0 --port=8181 --log-level=infoOpen http://localhost:8181/workspaces/<YOUR_WS_NAME> in a browser (sends Accept: text/html) to load the UI via the dev server.
Build the SPA into workspace_ui/dist/ and let the backend serve it as static content:
From the workspace_ui/ folder:
./build_dist.shKUBECONFIG=~/.kube/config-eoepca-demo UI_MODE="ui" uv run uvicorn workspace_api:app --reload --host=0.0.0.0 --port=8181 --log-level=infoThe Docker image (below) builds both the API and the UI and copies
workspace_ui/dist/into the container.
Python (from workspace_api/):
uv run ruff check .
uv run ruff format .
uv run mypy .Management Frontend (from workspace_ui/management):
npm run lintRun all pre-commit hooks from repo root:
uv run pre-commit run --all-filesBackend tests live in workspace_api/tests/:
cd workspace_api
uv run pytestWatch mode:
uv run pytest-watcher tests --nowInstalled via the backend dev extra:
- mypy – static typing
- ruff – linting & formatting
- pytest / pytest-watcher – testing
- pre-commit – git hooks
- ipdb – debugger
Run via uv run <tool> from workspace_api/.
The Workspace API uses a gateway-centric authentication model. Authentication is enforced upstream by an API gateway (for example APISIX) using OpenID Connect. The gateway validates the access token signature, issuer, expiration, and other token policy. Only authenticated requests are forwarded to the backend.
The backend does not re-validate tokens cryptographically. It treats the forwarded token as trusted input, requires the decoded aud claim to contain AUTH_AUDIENCE (default workspace-api), and extracts a minimal identity and authorization context, which is attached to each request via request.state.user. It does not enforce azp or client_id itself.
Backend JWT handling checks only the forwarded token shape and authorization claims:
Authorizationheader exists and usesBearer <token>.- JWT payload can be decoded.
audis either a string equal toAUTH_AUDIENCEor a list containing it.resource_accessis mapped to permissions:workspace-api:admin-> wildcard admin<workspace>:ws_admin-> admin permissions<workspace>:ws_access-> read/session visibility permissions<workspace>:ws_api-> bucket credential visibility only
The gateway remains responsible for signature validation, issuer validation, expiration/nbf, token policy, client trust, and OIDC/JWKS handling. The backend does not enforce azp or client_id; resource_access still says what the caller may do.
External roles are normalized into explicit permissions:
-
ws_accessVIEW_BUCKET_CREDENTIALSVIEW_MEMBERSVIEW_BUCKETSVIEW_STORESVIEW_SESSIONS
-
ws_apiVIEW_BUCKET_CREDENTIALS
-
ws_admin- all
ws_accesspermissions, plus: MANAGE_MEMBERSMANAGE_BUCKETSMANAGE_STORESMANAGE_SESSIONS
- all
Authorization decisions are based exclusively on these permissions, not on raw roles.
The ws_api role is intended for workspace-local machine/API access, for example a Keycloak client-credentials token minted from a provider-datalab-generated confidential workspace client. It currently grants only bucket credential visibility and deliberately does not grant session, member, bucket, or store management permissions.
Workspace API does not evaluate Keycloak role-scope mappings. It only authorizes the roles present in the token's resource_access claim, so a client-credentials token is treated as machine/API access only when it carries ws_api.
The platform-wide workspace-api client remains separate: its admin role grants wildcard workspace administration. A token requested through a workspace client such as ws-bob is accepted by the backend only when its audience is valid for Workspace API; a token intended only for the workspace runtime must not be forwarded to this backend.
When AUTH_MODE=no, authentication is disabled. The backend injects a synthetic user context with username Default and wildcard workspace permissions granting full access.
Human Workspace API/UI access normally uses a token requested through the workspace-api client. The token can contain workspace-local roles for the workspaces the user may access:
{
"aud": ["workspace-api"],
"azp": "workspace-api",
"sub": "user-id-alice",
"preferred_username": "alice",
"resource_access": {
"ws-alice": {
"roles": ["ws_admin"]
},
"ws-bob": {
"roles": ["ws_access"]
},
"ws-ci": {
"roles": ["ws_api"]
}
}
}Workspace-local automation can use a client-credentials token requested through the confidential workspace client, provided the token audience is valid for Workspace API. The request authenticates the ws-bob client itself; preferred_username is added by Keycloak from the service-account user and is usually service-account-ws-bob.
curl -sS -X POST \
"https://<keycloak>/realms/<realm>/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=ws-bob" \
--data-urlencode "client_secret=<client-secret>"{
"aud": ["workspace-api"],
"azp": "ws-bob",
"sub": "<keycloak-service-account-user-id>",
"preferred_username": "service-account-ws-bob",
"resource_access": {
"ws-bob": {
"roles": ["ws_api"]
}
}
}For development or testing purposes, similar payloads can be encoded as JWTs and passed via the Authorization header as Bearer tokens when AUTH_MODE=gateway.
Environment variables used by the backend (besides KUBECONFIG for Kubernetes access):
| Variable | Default | What it does |
|---|---|---|
PREFIX_FOR_NAME |
Prefix applied to user-facing names to build K8s object names (e.g. ws to get ws-alice for alice). |
|
PROVIDER_ENVIRONMENT |
datalab |
EnvironmentConfig name selected for new provider-storage and provider-datalab XRs. The API writes this value to storages.pkg.internal/environment on Storage and datalabs.pkg.internal/environment on Datalab. |
USE_VCLUSTER |
false |
Whether to provision an isolated vcluster for each datalab session (true) or run in separate namespace on host cluster (false). |
SESSION_MODE |
on |
Initial default-session mode for newly created Datalabs: on declares it as started, auto declares it as stopped for UI launch, and off does not declare it. |
MAX_SESSIONS |
3 |
Maximum number of Datalab sessions that can be declared for a workspace. |
DISABLE_DOCKER_REGISTRY |
false |
By default, each datalab gets an in-session Docker registry. |
DISABLE_STORES |
false |
Disable creation and display of all Datalab store types. |
DISABLED_STORE_TYPES |
Comma- or semicolon-separated store types to disable even when their backing CRDs are installed. Accepted aliases include postgres, qdrant, redis, and mongodb. |
|
ENDPOINT |
from AWS_ENDPOINT_URL |
S3 endpoint URL used when falling back to environment-based config. |
REGION |
from AWS_REGION or AWS_DEFAULT_REGION |
S3 region used when falling back to environment-based config. |
UI_MODE |
no |
Set to ui to enable templated HTML shell and SPA embedding. |
FRONTEND_URL |
/ui/management |
Base path (prod) or absolute URL (dev server like http://localhost:9000) for the SPA. |
AUTH_MODE |
gateway |
Authentication mode gateway expects a validated Authorization: Bearer <access_token> header to be forwarded by an upstream gateway, no disables authentication entirely (for local development only). |
AUTH_AUDIENCE |
workspace-api |
Required JWT aud value when AUTH_MODE=gateway. The decoded aud may be a string or list, but it must contain this value. |
AUTH_DEBUG |
false |
Enable verbose authentication and workspace debug logging. |
The Workspace UI only offers store types that the cluster can currently reconcile. A store type is available when:
- the
DatalabCRD exposes the matching spec field, - the backing operator CRD is installed in the cluster, and
- the store type is not disabled through
DISABLE_STORESorDISABLED_STORE_TYPES.
The current store type mapping is:
| Store type | Datalab field | Required backing CRD |
|---|---|---|
| Database (Postgres) | spec.databases |
postgresclusters.postgres-operator.crunchydata.com |
| Vector store (Qdrant) | spec.vectorStores |
qdrantclusters.qdrant.io |
| Cache (Redis) | spec.cacheStores |
redis.redis.redis.opstreelabs.in |
| Document store (MongoDB) | spec.documentStores |
mongodbcommunity.mongodbcommunity.mongodb.com |
If a backing CRD is not installed, the UI hides that store type and the API rejects attempts to create it. Manual changes made directly to a Datalab XR are not blocked by the Workspace API; in that case Crossplane reports reconciliation failures on the Datalab status if the required operator CRD is missing.
Build the combined image (Python 3.12.11 + built UI) from repo root:
docker build . -t workspace-api:latest --build-arg VERSION=$(git rev-parse --short HEAD)Run it, e.g.
docker run --rm \
-p 8181:8181 \
-e GUNICORN_WORKERS=2 \
-e UI_MODE=ui \
-e PREFIX_FOR_NAME=ws \
-e AWS_REGION=eoepca-demo \
-e AWS_ENDPOINT_URL=https://minio.develop.eoepca.org \
-e KUBECONFIG=/kube/config \
--mount type=bind,src=$HOME/.kube/config-eoepca-demo,dst=/kube/config,readonly \
workspace-api:latestApache 2.0 (Apache License Version 2.0, January 2004) https://www.apache.org/licenses/LICENSE-2.0