Skip to content

fix: dicht twee cross-tenant isolatielekken in OPI#70

Merged
uittenbroekrobbert merged 11 commits into
mainfrom
fix/tenant-isolation-namespace-ownership
May 27, 2026
Merged

fix: dicht twee cross-tenant isolatielekken in OPI#70
uittenbroekrobbert merged 11 commits into
mainfrom
fix/tenant-isolation-namespace-ownership

Conversation

@anneschuth

Copy link
Copy Markdown
Member

Samenvatting

Twee bevestigde, pre-existing cross-tenant isolatielekken in OPI. Beide
staan los van de overige security-PR's en raken elkaars code niet.

VULN 1 - deployment.namespace niet vastgepind op de projectnaam

ProjectManager.get_deployments nam de namespace ongecontroleerd over
uit het project-YAML. Het project-YAML is door de tenant te beheren. Door
namespace: <ander-project> te zetten (met cluster == CLUSTER_MANAGER
om het enige bestaande filter te passeren) liet een tenant OPI de
namespace van een ander project labelen en beheren, en ArgoCD
AppProject/Application-resources richting die slachtoffer-namespace
genereren. get_deployments is het enige knooppunt waar alle consumers
(check_and_create_namespaces, argo_manager) doorheen gaan.

Fix: get_deployments dwingt nu af dat de namespace gelijk is aan de
projectnaam. Ontbreekt de namespace, dan wordt de projectnaam ingevuld
(de bestaande, legitieme default die alle voorbeeldprojecten en fixtures
al gebruiken). Wijkt hij af, dan faalt het project met een duidelijke
Nederlandstalige foutmelding. Bewust fail-closed: geen stille
herschrijving die de werkelijke intentie zou maskeren.

VULN 2 - project-overname via de wizard

De create-flow schreef projects/{naam}.yaml zonder bestaans- of
eigenaarscontrole. Een tenant kon de wizard indienen met de projectnaam
van een andere tenant, waarna diens projectbestand stil werd
overschreven en de eigenaar bij de eerstvolgende herladen wisselde.

Fix: de create-paden controleren nu eerst of het bestand al bestaat en
weigeren bij conflict. process_project_yaml_background krijgt een
is_new_project-vlag; alleen de wizard-create (_start_project_creation)
geeft True door. De update/edit-flows (router_detail_edit,
router.py componentverwijdering) blijven met de default False een
bestaand bestand herschrijven, zodat legitiem bewerken ongewijzigd werkt.

Wijzigingen

  • opi/manager/project_manager.py: namespace-pinning in get_deployments
  • opi/core/simple_background.py: bestaanscontrole op beide create-paden
    • is_new_project-parameter
  • opi/web/router_wizard.py: wizard-create geeft is_new_project=True door
  • tests/test_tenant_isolation_namespace.py: regressietests

Testplan

  • uv run pytest tests/test_tenant_isolation_namespace.py -q - 7 passed
  • Vreemde namespace wordt afgewezen; gelijke/afwezige namespace passeert
  • Create met bestaande projectnaam wordt afgewezen; nieuwe naam passeert
  • Legitieme update van bestaand project schrijft nog steeds
  • uv run ruff check . + ruff format + pyright schoon
  • Volledige suite: 2826 passed, geen regressies

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Drie commits toegevoegd vanuit een diepe review (commits 49a53438, 2ede4d00, b094433e). Twee scope-uitbreidingen + één parking-doc; geen wijziging aan de twee oorspronkelijke VULN-fixes van deze PR.

Wat de PR oorspronkelijk dichtte (✅ ongewijzigd)

  • VULN 1: enforce_namespace_pin in get_deployments (API/wizard-pad)
  • VULN 1 bypass: idem in git_monitor.check_and_create_namespaces (git-monitor-pad)
  • VULN 2: existence-check op wizard-create in simple_background

Onze toevoegingen

1. GAT 1 — handle_create_project mistte de existence-check (49a53438)

task_handlers_project.handle_create_project is geregistreerd voor TaskType.CREATE_PROJECT in server.py:122 en worker_main.py:58. Het schrijft projects/{project_name}.yaml via create_or_update_file zonder bestand-bestaat-check — exact dezelfde kwetsbaarheid die deze PR dichtte in simple_background maar liet open in dit parallelle async-taakpad.

Geen huidige caller submit TaskType.CREATE_PROJECT (grep over opi/api/, opi/web/, opi/core/ levert alleen test-fixtures), dus niet exploitable nu. Maar de handler is wel geregistreerd; een toekomstig endpoint dat de task indient zou zonder deze check stilletjes andermans project-file overschrijven. Defense in depth; check is een exacte mirror van wat simple_background doet.

2. GAT 3 — extract_deployment_namespace enforced de pin niet (2ede4d00)

Dezelfde project-YAML wordt op negen plekken rauw uitgelezen via extract_deployment_namespace zonder via get_deployments te gaan:

  • backup_router.py:665, 963, 1311 (3 endpoints)
  • restore_router.py:1167, 1589 (2 endpoints)
  • backup_tasks.py:87, 597, 755 (3 task handlers)
  • router_detail_edit.py:1340 (1 endpoint)

Een tenant die zijn eigen project-YAML aanpast (namespace: victim-project) en daarna een van die endpoints aanroept met zijn eigen API-key krijgt OPI om tegen de victim-namespace te opereren — exact het cross-tenant-primitief dat deze PR dichtte op de andere paden maar hier liet open.

extract_deployment_namespace enforced nu intern dezelfde invariant als enforce_namespace_pin, zonder project_data te muteren:

  • declared matching → return project_name (no-op)
  • missing → default naar project_name
  • declared mismatching → ValueError met dezelfde Nederlandse foutmelding

Alle negen callsites krijgen automatisch de bescherming. Geen caller depend op de oude "return raw declared value"-semantics — ze gebruiken allemaal de waarde voor kubectl-operaties of pod-manifests, wat onder de pin-invariant per definitie project_name is.

3. Parking-doc voor open items (b094433e)

features/futures/tenant-isolation-followups.md documenteert drie items die deze PR niet opgelost heeft:

  • GAT 2 — TOCTOU op concurrent wizard-create. file_exists leest de lokale werkkopie; push_changes retry't met rebase. Twee gelijktijdige creates met dezelfde naam → laatste push wint stilletjes. Vereist twee SSO-sessies binnen seconden, dus laag risico. Drie mogelijke designs geschetst (re-check na rebase, advisory lock, atomic git ops); aanbevolen design (1) als pragmatische fix.
  • Refactor: PR herimplementeert de existence-check inline op drie plekken; git.check_overwrite_project_file (opi/connectors/git.py:1196) doet dit al. Pre-existing duplicatie, prima voor follow-up.
  • UX: ValueError uit de pin-enforcement surfaced als 500 ipv 400. Body is generiek dus geen info-leak; UX-irritatie. Global exception handler aanpassen — kleine follow-up.

Tests

test_tenant_isolation_namespace.py gaat van 8 naar 15 tests, alle 15 groen:

  • 5 nieuwe voor extract_deployment_namespace (matching/missing/mismatch/unknown/multi-deployment)
  • 1 nieuwe voor handle_create_project existence-check
  • 1 wrapper voor de class-headers

Pre-existing observatie (geen actie)

Foutmelding noemt victim-project expliciet. Subagent flagde dit als info-leak; bij heranalyse niet zo: de attacker submitte beide waarden zelf, leert dus niets nieuws. Behouden voor UX (typo-debugging).


Audit: commits direct op de PR-branch gepushed, comment achteraf. Zelfde flow als #68 en #69.

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Vervolg op de eerdere augmentatie-commits: na user-pushback heb ik commit 49a53438 (GAT 1 - existence-check op handle_create_project) gereverteerd en in plaats daarvan de hele CREATE_PROJECT plumbing verwijderd. Commit 6a38e7b7.

Waarom verwijderen ipv defensief patchen

Uitputtende verificatie: geen enkele productiecode submit een CREATE_PROJECT task. Bewijs:

Check Resultaat
create_async_task calls met task_type="create_project" 0 van 19 productie-callsites
Federation/cross-cluster submission 0
Scheduler/cron/startup hook 0
Andere open branches 0 (alle create_project hits waren create_project_file_handler-substring, niet de task type)
Tests die de handler daadwerkelijk uitvoeren 0 (mocked task service of pure string-formatter test)

Plus: task_manager.process_project_background (task_manager.py:897, 215 regels) is óók dead code — geen callers anywhere. Distinct van simple_background.process_project_background (dat WEL het levende wizard-pad is en in deze PR de existence-check kreeg).

Wat de cleanup-commit doet

6a38e7b7 remove dead CREATE_PROJECT task path - consolidate to one create-flow (-489 / +15 regels):

  • opi/core/task_handlers_project.py: handle_create_project weg, import time orphan weg
  • opi/core/task_manager.py: process_project_background weg (215 regels), import GitConnector + import settings orphans weg
  • opi/server.py + worker_main.py: imports + register_handler(TaskType.CREATE_PROJECT, ...) weg
  • opi/core/async_task_service.py: CREATE_PROJECT enum entry weg
  • opi/api/task_models.py: CreateProjectResult model + mapping in TASK_RESULT_MODELS weg
  • tests/test_task_helpers.py: "create_project" in parametrize weg + vervangen in default-payload test
  • tests/test_tenant_isolation_namespace.py: TestHandleCreateProjectExistenceCheck weg (test bewaakte nu-verwijderde dode code)
  • tests/test_task_manager.py: stale comment opgeruimd

Resultaat: één authoritative project-creation pad — simple_background.process_project_background met de existence-check uit deze PR. Geen parallelle codepaden meer waar een toekomstige refactor de cross-tenant invariant kan vergeten.

Consistent met de lijn van PR #72 (fix/remove-dead-git-rce): dode code met latente security-risks verwijderen ipv defensief patchen.

Pre-existing test-hang op deze branch (niet door deze cleanup)

Tijdens de bredere test-sweep hangt tests/test_logs_websocket_router.py::TestStreamDeploymentLogs::test_stream_deployment_logs_not_connected op unittest.IsolatedAsyncioTestCase._tearDownAsyncioRunner. Onafhankelijk geverifieerd:

Conclusie: de hang is geïntroduceerd door één van PR #70's eigen commits (eb07ef9a of c192a9c3), niet door onze augmentatie. Eigen follow-up — vermoedelijk een asyncio-task die niet netjes wordt gecancelled in de tear-down van een websocket-test. Niet blokkerend voor merge (test slaagt functioneel, alleen teardown blijft hangen — pytest-timeout of een proper await task.cancel() in de test zou volstaan).

Audit-status

Voor deze PR zijn nu vier commits van mij toegevoegd:

  • 2ede4d00extract_deployment_namespace pin (blijft)
  • b094433e — tests + future-doc voor GAT 1/2/3 (blijft; tests bijgewerkt)
  • 6a38e7b7 — CREATE_PROJECT dead-code removal (deze)
  • 49a53438 — feitelijk gereverteerd door 6a38e7b7 (handler is gewoon weg)

Future-doc features/futures/tenant-isolation-followups.md blijft relevant: GAT 2 (TOCTOU op concurrent create), refactor naar git.check_overwrite_project_file, ValueError → 400 UX. Plus nu de pre-existing teardown-hang.

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Vijfde commit toegevoegd (2794a403): tangentieel maar nodig voor betrouwbare pre-push CI.

Wat het probleem was

tests/test_logs_websocket_router.py::TestStreamDeploymentLogs::test_stream_deployment_logs_not_connected hing in unittest.IsolatedAsyncioTestCase._tearDownAsyncioRunner op machines zonder werkende kubectl (ook de pre-push hook in sommige situaties). Niet veroorzaakt door deze PR — onafhankelijk geverifieerd door op de pristine PR-branch (zonder onze deletions) dezelfde hang te reproduceren — maar het blokkeerde wel onze push-cycli.

Root cause

KubectlConnector.__init__ (opi/connectors/kubectl.py:77-78) spawnt een achtergrond-retry-task wanneer de connection-test faalt:

if not KubectlConnector.isConnected:
    self._retry_task = asyncio.create_task(self._connection_retry())

De broer-test (test_stream_deployment_logs_success, regels 458-461) cancelt deze task expliciet vóór z'n assertions. De not_connected-test was dat vergeten. Bij teardown wacht unittest's _cancel_all_tasks op de retry-coroutine die nooit afsluit → indefiniete hang.

Op een dev-machine met werkende kubectl-context fired isConnected=True na init → geen retry-task → geen hang. Daarom werkte de eerdere PR #70 push, en daarom is de hang vandaag tijdens m'n brede sweep opgekomen (worktree-env zonder kubectl-context).

De fix

Twee regels, gekopieerd uit de zuster-test:

connector = KubectlConnector()
if connector._retry_task:
    connector._retry_task.cancel()
    connector._retry_task = None

De test blijft kubectl-env-onafhankelijk (de patch.object(KubectlConnector, "isConnected", False) forceert het no-connection-pad ongeacht echte kubectl-status), dus géén requires_infra/slow/sandbox marker nodig.

Geverifieerd: tests/test_logs_websocket_router.py::TestStreamDeploymentLogs draait nu in 20.91s, beide tests groen.

Bredere observatie (geen actie op deze PR)

KubectlConnector is een singleton met side-effects in __init__ (background asyncio task). Tests die de connector instantiëren zonder die task te cancellen krijgen kapotte teardowns op machines zonder kubectl. tests/test_kubectl_connector.py doet het netter via with patch("opi.connectors.kubectl.asyncio.create_task", new=MagicMock()) (regels 26-27) — die patroon zou ideaal in een fixture of in __init__ zelf moeten zitten (e.g., KubectlConnector(_skip_background=True) voor testbaarheid). Eigen test-hygiene PR waardig; tests/test_rate_limiter.py en tests/test_resource_history.py hebben dezelfde val (niet vandaag fataal omdat de singleton vaak al gecached is, maar latent).

Samenvatting van wat er nu op PR #70 staat

2794a403 (mij)         test(kubectl): cancel retry-task in not_connected test
6a38e7b7 (mij)         remove dead CREATE_PROJECT task path - consolidate to one create-flow
b094433e (mij)         test+docs(tenant): regression tests for GAT 1/3 + parking-doc
2ede4d00 (mij)         fix(tenant): pin namespace inside extract_deployment_namespace
49a53438 (mij)         [effectief teruggedraaid door 6a38e7b7 — handler is verwijderd]
c192a9c3 (anneschuth)  fix: dicht namespace-pin bypass via git-monitor pad
eb07ef9a (anneschuth)  fix: dicht twee cross-tenant isolatielekken in OPI

Alle review-bevindingen geadresseerd: VULN 1+2 (origineel), GAT 1 (verwijderd), GAT 3 (helper-pin), plus deze tangentiële test-fix.

anneschuth and others added 7 commits May 27, 2026 15:26
VULN 1 - deployment.namespace niet vastgepind op projectnaam:
get_deployments nam de namespace ongecontroleerd over uit het
project-YAML. Een tenant kon namespace: <ander-project> zetten
(met cluster == CLUSTER_MANAGER om het enige filter te passeren)
waardoor OPI de namespace van een ander project labelde en
beheerde, plus ArgoCD AppProject/Application richting die namespace
genereerde. get_deployments dwingt nu af dat de namespace gelijk is
aan de projectnaam: ontbreekt hij dan wordt de projectnaam ingevuld
(de bestaande, legitieme default), wijkt hij af dan faalt het project
met een duidelijke foutmelding. Bewust fail-closed, geen stille
herschrijving die de intentie zou maskeren.

VULN 2 - project-overname via de wizard:
de create-flow schreef projects/{naam}.yaml zonder bestaans- of
eigenaarscontrole, waardoor het projectbestand van een andere tenant
stil werd overschreven en de eigenaar bij herladen wisselde. De
create-paden controleren nu eerst of het bestand al bestaat en
weigeren in dat geval (process_project_yaml_background krijgt een
is_new_project-vlag; alleen de wizard-create geeft True door). De
update/edit-flows blijven ongewijzigd een bestaand bestand
herschrijven.

Beide lekken waren pre-existing en staan los van andere PR's.

Tests: tests/test_tenant_isolation_namespace.py - red/green voor
beide fixes, inclusief de legitieme default- en update-gevallen.
De namespace-pin uit de vorige commit zat alleen in
ProjectManager.get_deployments. Het git-monitor pad
(check_and_create_namespaces in git_monitor.py) leest het
projectbestand rechtstreeks en ging niet door dat chokepoint,
waardoor een tenant die een projectbestand direct naar git commit
nog steeds een namespace van een andere tenant kon laten aanmaken
en labelen.

De pin-logica staat nu in een gedeelde enforce_namespace_pin()
in project_manager.py, aangeroepen vanuit zowel get_deployments
als het git-monitor pad. Het git-monitor pad faalt gesloten: een
afwijkende namespace wordt geweigerd en gelogd zonder iets aan te
maken, zonder de polling-loop te laten crashen.

Regressietest toegevoegd die bewijst dat het git-monitor pad nu
ook gepind is (foreign namespace geweigerd voordat kubectl wordt
geraakt; afwezige namespace valt terug op de projectnaam).
… task)

handle_create_project is registered for TaskType.CREATE_PROJECT in
server.py:122 and worker_main.py:58. It writes projects/{project_name}.yaml
via create_or_update_file without checking whether the file already exists
- same vulnerability that PR 70 fixed in simple_background but not in this
parallel async-task path.

Today no caller in the codebase submits TaskType.CREATE_PROJECT (grep over
opi/api/, opi/web/, opi/core/ confirms only test fixtures use the string),
so not exploitable now. But the handler is wired as a task type the
worker will accept, so a future endpoint added without thinking about the
existence check would silently overwrite victim projects. Defense in
depth.

The check mirrors simple_background.process_project_background exactly:
file_exists before create_or_update_file, fail-closed with a Dutch error
message and a failed task result. No functional difference for the
already-correct case (file does not exist).
PR 70 enforces the namespace pin in two places:
- ProjectManager.get_deployments (API/wizard path)
- git_monitor.check_and_create_namespaces (git-monitor path)

But the same project YAML is read raw via extract_deployment_namespace
from nine other callsites that do not go through get_deployments:
- backup_router.py:665, 963, 1311 (3 endpoints)
- restore_router.py:1167, 1589 (2 endpoints)
- backup_tasks.py:87, 597, 755 (3 task handlers)
- router_detail_edit.py:1340 (1 endpoint)

A tenant who edits his own project YAML to set namespace=victim-project
and then calls one of those endpoints with his own API key gets OPI to
operate against the victim namespace - the exact cross-tenant primitive
that PR 70 closes on the other paths but leaves open here.

extract_deployment_namespace now enforces the same invariant as
enforce_namespace_pin internally, without mutating project_data:
- declared namespace matching project name -> return project name (no-op)
- missing namespace -> default to project name
- declared namespace mismatching project name -> raise ValueError with
  the same Dutch error message shape as enforce_namespace_pin

Callers that depend on the helper's old contract (returning the raw
declared value) would be the issue, but all nine callsites use the
result for kubectl operations against the deployment's namespace -
which under the pin invariant is always the project name. None benefit
from getting an attacker-controlled value.

Followup: callers do not catch ValueError; FastAPI converts it to a 500
Internal Server Error instead of a 400 Bad Request. Tracked in
features/futures/tenant-isolation-followups.md - body is generic so no
info-leak, just suboptimal UX.
…pens

Seven additional tests pinning the augmentations from the previous two
commits:

- TestExtractDeploymentNamespacePinned (5 tests): declared-matching ->
  project name, missing -> project name, mismatched -> ValueError,
  unknown deployment -> None, multi-deployment scope-isolation.
- TestHandleCreateProjectExistenceCheck (1 test): create_project task
  for an already-existing project name fails fast and does not call
  create_or_update_file.

features/futures/tenant-isolation-followups.md parks the three items
this PR review surfaced but did not address in code:

1. GAT 2 - TOCTOU on concurrent wizard-create with three possible
   designs.
2. Refactor: reuse git.check_overwrite_project_file instead of inline
   duplication in three places.
3. UX: ValueError from the new pin enforcement currently surfaces as 500
   instead of 400.

Plus status-table mapping all review findings against this PR.
…onnected

Tangential to PR70's tenant-isolation scope but necessary for reliable
pre-push CI: when kubectl is unavailable in the test environment,
KubectlConnector.__init__ (opi/connectors/kubectl.py:77-78) spawns a
background retry task. unittest.IsolatedAsyncioTestCase's teardown waits
for all async tasks to be cancelled cleanly before exiting; the retry
coroutine doesn't terminate, so _cancel_all_tasks hangs indefinitely.

The sibling test test_stream_deployment_logs_success already cancels the
retry task (lines 458-461). The not-connected test forgot to. This was
masked when kubectl IS available on the developer machine (no retry task
spawned, no hang) but reliably bites on machines without kubectl.

Pattern copied verbatim from the sibling test. Two-line fix. The test
remains kubectl-environment-independent (patch.object(KubectlConnector,
'isConnected', False) forces the no-connection path regardless of real
kubectl status), so no CI marker needed.

Verified: tests/test_logs_websocket_router.py::TestStreamDeploymentLogs
runs cleanly in 20.91s with --timeout=20 (both tests pass).
@uittenbroekrobbert uittenbroekrobbert force-pushed the fix/tenant-isolation-namespace-ownership branch from 2794a40 to 9949629 Compare May 27, 2026 13:55
@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Update — rebased op main + review-aanpassingen

Tijdens review zijn een paar dingen aangepast voordat het naar main kan.

Rebase + conflict-resolutie

handle_create_project in task_handlers_project.py is op main de v2-task-handler voor project-create geworden (terwijl de PR werd gereviewd). Commit 6a38e7b7 ("remove dead CREATE_PROJECT task path") gaat ervan uit dat die handler dood is — niet meer waar, applying ervan zou de hele wizard-create flow breken. Commit gedropped tijdens rebase.

De existence-check die commit 49a53438 in dat handler heeft toegevoegd is wél behouden — dat is precies de live save-path die VULN 2 daadwerkelijk afdekt.

Conflict in router_wizard.py (BackgroundTask(process_project_yaml_background, ...) toevoeging) ook gedropped: main's wizard gebruikt nu create_async_task (v2 task systeem) zodat de background-werk via het v2-pad loopt, niet meer via een aparte BackgroundTask response-attribuut.

features/futures/tenant-isolation-followups.md getrimd

Was 95 regels met een uitgebreide ✅/⏳-status-tabel (PR-historie, niet toekomstig werk) en een rechtvaardigings-sectie over error-message info-leak (geen follow-up). Nu 38 regels — alleen de drie echte open items:

  1. TOCTOU race op concurrente wizard-create (lage waarschijnlijkheid, real). Aanbevolen fix: na rebase nogmaals file_exists checken.
  2. Hergebruik check_overwrite_project_file helper — pre-existing duplicatie van het file-exists patroon.
  3. ValueError → 400 UX-fix voor de 9 extract_deployment_namespace call-sites; nu wordt het een opake 500.

Comment-trim

De code-comments en docstrings waren wat lang voor wat ze beschrijven. Getrimd waar mogelijk, zonder context te verliezen:

  • enforce_namespace_pin docstring: 23 → 7 regels
  • extract_deployment_namespace docstring: 30 → 6 regels
  • process_project_yaml_background docstring: 16 → 8 regels
  • Inline guards in simple_background + task_handlers_project: van 5-6 regel block → 3 zelf-staande regels (TLDR maar cold-readable)
  • git_monitor ValueError-catch: 4 → 3 regels (project + niet-matching namespace expliciet benoemd)
  • git_monitor pin-call: 5 → 2 regels (legt uit waarom de helper hier nogmaals nodig is)
  • get_deployments Raises: 6 → 2 regels (verwijst naar enforce_namespace_pin ipv duplicatie)

Verificatie

  • CI: MERGEABLE / UNSTABLE — enige rode check is Python dependency audit (pre-existing starlette CVE, raakt elke open PR; aparte upgrade-PR nodig)
  • Pre-push pytest equivalent: 3434 passed, 31 deselected
  • Tenant-isolatie regressietests: 15/15 groen
  • 0 commits behind main, 11 ahead
  • Geen leftover artifacts van de gedropte commit (task_manager.py, worker_main.py, test_task_manager.py zijn ongewijzigd t.o.v. main)

@uittenbroekrobbert uittenbroekrobbert merged commit 0a074b4 into main May 27, 2026
19 of 20 checks passed
@uittenbroekrobbert uittenbroekrobbert deleted the fix/tenant-isolation-namespace-ownership branch May 27, 2026 15:22
uittenbroekrobbert added a commit that referenced this pull request May 28, 2026
handle_create_project's file_exists check (PR #70) fired on every dispatch
of the create_project task, but four callers use it:

  - router_wizard.py:1944  (real create -- check must fire)
  - router_detail_edit.py:493  (modal edit -- legitimate overwrite)
  - router_subdomain_admin.py:360  (subdomain admin)
  - router.py:498  (component delete)

The three update flows hit the check and got 'project bestaat al' on every
edit. Fix: payload['is_new_project'] flag, default False, only the wizard
create sets True. Existence check + commit message ('Create' vs 'Update')
key off it.

database_manager: 'Creating database resources' -> 'Database klaarmaken'
as a Dutch + neutral label that fits both create and update. Full
create/update label split is tracked in issue #112.
uittenbroekrobbert added a commit that referenced this pull request May 28, 2026
… flag

PR #71 made handle_create_project's existence check conditional on
payload['is_new_project'] == True (so edit/update flows that reuse the
create_project task aren't blocked). The existing test from PR #70 still
ran without the flag and expected the block to fire -- now silently passes
through. Add the flag, and add a complementary test that an edit-flow
payload (no flag) DOES overwrite an existing file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants