Add internal vs hackathon Jupyter spaces - #88
Conversation
Split shared Labs by group (hard isolation between trees; visual hide of others' private project dirs inside a Lab) and keep GUI/API lists on the same tenant boundary.
There was a problem hiding this comment.
Pull request overview
This PR introduces two-tenant isolation (internal vs hackathon) across the GUI/API and JupyterHub, so projects/nodes are scoped to the caller’s tenant and the frontend opens Jupyter using a tenant-derived Hub user + viewer token (instead of hardcoded user1). It also adds a tenant-scoped node governance pipeline (private → submitted → approved/public) with reviewer permissions and auditing.
Changes:
- Add tenant scoping for workflow projects (DB
tenantcolumn, tenant-filtered querysets/permissions, tenant-aware filesystem roots). - Add Jupyter “viewer token” session APIs + a JupyterLab contents filter extension to visually hide other users’ private project directories in the shared Lab, and update the frontend to open tenant-correct Jupyter URLs.
- Add tenant-scoped node governance (status, reviewer group, endpoints, audit log) and expose status/actions in the frontend palette.
Reviewed changes
Copilot reviewed 47 out of 49 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| gui/workflow_frontend/src/views/home/type.ts | Adds Tenant type and optional tenant field on Project. |
| gui/workflow_frontend/src/views/home/homeView.tsx | Switches Jupyter URL construction to openJupyterTree() (tenant/session-driven). |
| gui/workflow_frontend/src/views/home/components/projectSelector.tsx | Switches Jupyter open behavior to openJupyterTree() and standardizes project path usage. |
| gui/workflow_frontend/src/views/home/components/nodeDetailModal.tsx | Switches to tenant-based Jupyter open helper (but currently leaves dead/unused code). |
| gui/workflow_frontend/src/views/home/components/jupyterModal.tsx | Uses backend session + viewer token to build the Jupyter tree URL for iframe/new-tab. |
| gui/workflow_frontend/src/views/home/components/calculationNode.tsx | Uses openJupyterTree() for node file navigation in Jupyter. |
| gui/workflow_frontend/src/views/box/boxView.tsx | Adds node governance actions/status badges and uses tenant-based Jupyter open helper. |
| gui/workflow_frontend/src/hooks/useUploadedNodes.ts | Extends uploaded-node response/types with tenant/reviewer/governance fields. |
| gui/workflow_frontend/src/api/jupyterTenant.ts | New API module: caches Jupyter session, builds tree URLs, adds viewer token query param. |
| gui/workflow_backend/env.template | Documents fallback Jupyter execution user configuration. |
| gui/workflow_backend/django-project/tests/test_tenants.py | New tests for tenant isolation and viewer-token visible-path allowlisting. |
| gui/workflow_backend/django-project/tests/test_node_governance.py | New tests for tenant-scoped node governance and reviewer permissions. |
| gui/workflow_backend/django-project/tests/test_jupyter_listing_filter.py | New pure unit tests for the Jupyter contents filter logic. |
| gui/workflow_backend/django-project/neuroworkflow/PRIVACY_NOTICE.md | Adds user-facing explanation that the Jupyter hide is visual-only within a tenant space. |
| gui/workflow_backend/django-project/neuroworkflow/jupyterhub_config.py | Adds two-space volume mapping + tenant env + enables contents filter extension; tightens sudo default. |
| gui/workflow_backend/django-project/neuroworkflow/jupyter_tenant_filter.py | New Jupyter server extension: filters directory listing and blocks contents GET outside allowlist. |
| gui/workflow_backend/django-project/neuroworkflow/jupyter_server_config.py | Enables the Jupyter server extension in both Labs. |
| gui/workflow_backend/django-project/codes-hackathon/README.md | Documents hackathon Lab filesystem tree and cutover expectations. |
| gui/workflow_backend/django-project/codes-hackathon/projects/.gitkeep | Ensures hackathon projects directory exists in repo. |
| gui/workflow_backend/django-project/codes-hackathon/nodes/.gitkeep | Ensures hackathon nodes directory exists in repo. |
| gui/workflow_backend/django-project/app/workflow/views.py | Enforces tenant-scoped querysets and uses tenant Hub user for Jupyter URLs / execution. |
| gui/workflow_backend/django-project/app/workflow/viewer_tokens.py | Adds signed short-lived viewer tokens that encode user+tenant+hub user. |
| gui/workflow_backend/django-project/app/workflow/urls.py | Adds Jupyter session and visible-paths endpoints. |
| gui/workflow_backend/django-project/app/workflow/serializers.py | Exposes tenant on projects (read-only). |
| gui/workflow_backend/django-project/app/workflow/permissions.py | Adds tenant isolation checks + a tenant-scoped project queryset helper. |
| gui/workflow_backend/django-project/app/workflow/path_utils.py | Makes project/nodes filesystem roots tenant-aware; propagates tenant through path guards. |
| gui/workflow_backend/django-project/app/workflow/models.py | Adds tenant column to FlowProject. |
| gui/workflow_backend/django-project/app/workflow/migrations/0005_flowproject_tenant.py | Migration for FlowProject tenant + ensures tenant/reviewer groups exist. |
| gui/workflow_backend/django-project/app/workflow/jupyter_views.py | New backend APIs for session+visible paths, supporting viewer token + Keycloak auth. |
| gui/workflow_backend/django-project/app/workflow/jupyter_execution_service.py | Changes default execution user to internal and maps legacy user1 → internal. |
| gui/workflow_backend/django-project/app/workflow/jupyter_auth.py | Adds DRF authentication backend for “Viewer ” signed viewer tokens. |
| gui/workflow_backend/django-project/app/workflow/execution/remote_slurm_executor.py | Copies tenant-correct project/nodes directories when staging remote runs. |
| gui/workflow_backend/django-project/app/workflow/execution/local_executor.py | Resolves project directory via project model/tenant-aware helpers where possible. |
| gui/workflow_backend/django-project/app/tenants.py | New tenant model (groups → tenant), hub user mapping, reviewer checks, login sync from Keycloak claims. |
| gui/workflow_backend/django-project/app/box/views.py | Makes uploads tenant-scoped; adds node governance endpoints + tenant/reviewer info in list response. |
| gui/workflow_backend/django-project/app/box/urls.py | Registers node governance/reviewer/audit endpoints. |
| gui/workflow_backend/django-project/app/box/services/python_file_service.py | Makes node duplicate checks tenant-scoped and initializes governance/audit fields. |
| gui/workflow_backend/django-project/app/box/serializers.py | Exposes governance fields (tenant/status/review_comment). |
| gui/workflow_backend/django-project/app/box/models.py | Adds tenant + status + review metadata to nodes; adds NodeAuditLog model; adjusts uniqueness constraint. |
| gui/workflow_backend/django-project/app/box/migrations/0006_pythonfile_tenant_governance.py | Migration for node tenant/governance fields + NodeAuditLog + backfill rules. |
| gui/workflow_backend/django-project/app/box/governance.py | New governance logic with reviewer rules and audit logging. |
| gui/workflow_backend/django-project/app/auth/authViews.py | Adds tenant/hub user/reviewer flags + notice to user profile response. |
| gui/workflow_backend/django-project/app/auth/authentication.py | Syncs user tenant group membership from Keycloak token claims at login. |
| gui/README.md | Documents JupyterHub allowed users env var. |
| gui/env.template | Adds two-space JupyterHub env documentation. |
| gui/docker-compose.yml | Mounts hackathon code tree; sets default execution user; exposes JupyterHub limits/env. |
| gui/docker-compose.prod.yml | Updates allowed users default + sets default resource limits for Hub in prod. |
| gui/.env.example | Documents new two-space JupyterHub env knobs. |
| deployment/JUPYTER_TWO_SPACES.md | Adds operator runbook for two shared Labs, cutover steps, and tenant/governance behavior. |
Suppressed comments (1)
gui/workflow_backend/django-project/app/box/services/python_file_service.py:57
- When an identical file hash already exists, this code overwrites that existing PythonFile record and reassigns uploaded_by. This lets a different user (or a user upload matching catalog content) take over/replace someone else’s node by uploading identical content, which is a cross-user data integrity/security issue.
if existing_file is not None:
python_file = PythonFile.objects.get(id=existing_file.id)
python_file.name = name
python_file.description = description or ""
python_file.category = category
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
IzBrain67
left a comment
There was a problem hiding this comment.
Hi @kirillmitrofanov, thanks a lot for this PR — the two-space design is a big step forward, and I really appreciate how honestly the visual-only nature of the in-Lab hiding is documented (the PRIVACY_NOTICE is a nice touch).
I ran an automated review over the diff (Claude Code) and am sharing the results for reference — please treat them as input rather than change requests, and feel free to push back on anything that doesn't match your intent or the actual runtime setup. The ones that map to changed lines are inline comments; a few relate to code outside the diff, so I'm listing those here:
Notes on code outside the diff
app/box/views.py:328(code-save PUT / parameter update) —_can_modify_python_fileonly checks ownership, so it seems an owner can rewrite an approved/PUBLIC node's content after review: the status stays PUBLIC, the shared tenant nodes tree is rewritten via_update_nodes_folder_file, and no re-review or audit entry happens. A status downgrade toprivate(or reviewer-only edits) on content change might be worth considering so the governance pipeline isn't one-shot.app/box/views.py:402(bulk copy) and:557(copy-by-filename) — the copiedPythonFiledoesn't settenant/status, so a hackathon user's copy defaults totenant="internal"and never appears in their list (and can't be managed afterwards — GET/PUT/DELETE 404). The duplicate-name checks around:506/:535also look tenant-unscoped, so internal file names can block or rename hackathon copies.app/box/views.py:1478(NodeCategoryListView) and:1540(category color update) — these still usePath(settings.MEDIA_ROOT)rather thannodes_root(tenant), so hackathon requests read and write the internalcodes/nodestree (the siblingUploadedNodesViewwas converted; these two seem to have been missed).
There are also a handful of smaller cleanup-level notes (dead code, duplicated exception boilerplate, a few lines over black's 88-char limit) — happy to share those separately if useful.
None of this is meant to block — take whatever is useful. Thanks again!
…vernance exits. Copy the viewer token from the Lab URL onto a cookie, wrap async contents get, and stop stealing /checkpoints. Tighten claim matching, keep owners able to see their own rows, and block self-approve plus hash-based ownership takeover.
|
Addressed the Copilot and @IzBrain67 review threads in 0944089. Lab filter: copy Tests: |
Summary
internal/hackathon) with separate host trees (codes/vscodes-hackathon/). Isolation between groups is filesystem-level; hiding other private project folders inside a Lab is visual only (kernel/terminal still see the mount).nw-internal/nw-hackathonKeycloak groups) so GUI lists do not leak the other tenant’s public projects or nodes.private → submitted → approved → public,node-reviewersgroup, audit log, tenant-scoped palette.user1/ password).Operator runbook:
deployment/JUPYTER_TWO_SPACES.md.Not in this PR: live Hub recreate, production DB migrate, copying catalog nodes into the hackathon tree. Those are an explicit cutover.
Test plan
pytesttest_tenants.pytest_node_governance.pytest_jupyter_listing_filter.pytest_visibility.py(already passing in an ephemeral container)lshas no internal UUIDslsstill sees them (expected)