Skip to content

MILAB-6761: add pl-cli admin delete-user - #1795

Draft
blackcat wants to merge 1 commit into
mainfrom
MILAB-6761_delete-user
Draft

MILAB-6761: add pl-cli admin delete-user#1795
blackcat wants to merge 1 commit into
mainfrom
MILAB-6761_delete-user

Conversation

@blackcat

Copy link
Copy Markdown
Contributor

Ticket: MILAB-6761
Backend PR: milaboratory/pl#2157 (needs to land first — the RPC this drives is new)

Why

Multi-provider auth can hand one person two accounts. The spare showed up in the sharing user picker and its projects kept taking part in deduplication, and no shipped tool removed one.

What

pl-cli admin delete-user <user>, over the new AuthAPI.DeleteUser RPC. When the account still owns projects the command requires an explicit decision instead of picking a default, because both defaults are wrong to assume — one silently destroys data, the other silently hands it to someone:

Flag Effect
--move-projects-to <user> re-attach every project to another user's root, then delete the account
--delete-projects delete the projects along with the account

Passing neither (with projects present) or both is an error. An account that owns nothing has no decision to make and deletes straight through.

The move is a move, not a copy: the same project resources are re-homed, so nothing is duplicated and nothing needs re-verifying — unlike the existing admin copy-project + project delete pattern. This works across roots because the backend permits a reference between differently coloured resources when the caller holds write access to both, which admin credentials do. Details:

  • a name the target already uses is deduplicated (XX (Copy)) rather than overwritten, and two source projects sharing a name do not both keep it;
  • each project is attached to the target before being detached from the source, so it never sits momentarily unreferenced;
  • the whole batch is one transaction — either every project lands in the target root, or none moves and both roots are untouched;
  • the target's project list is created if they never had one, so re-homing to a user who has never opened the app works instead of failing halfway with the account already gone.

Both forms prompt with the affected project list first; --force skips it for scripted runs. --format json emits a machine-readable report (moved/deleted projects, root id, grants revoked, index entries removed).

Supporting changes

  • pl-client: PlClient.deleteUser(login)UserDeletionReport, gRPC-only like listUsers. The report carries the root id as a raw bigint rather than a SignedResourceId — the resource is gone, so it is for the audit line, not for further calls.
  • pl-middle-layer: exports ProjectsResourceType, which a caller writing into another user's root needs (ensureProjectListRid only works on tx.clientRoot).
  • project_ops.ts: moveProjects, listProjectIdentities (listProjects omits the field name a re-home needs), openUserRoot (a missing project list is a legitimate state for a whole-account operation, not an error) and ensureUserProjectList.
  • Unknown-login errors now say User "x" not found on this server. instead of surfacing the backend's user root not found, which reads as a missing resource rather than a missing user.

Proto sync

The DeleteUser messages were applied surgically to lib/node/pl-client/proto/, not via sync-proto.sh. A full resync would have pulled unrelated in-flight backend proto changes (tree since-token work) into this PR. The local copy therefore stays behind pl@main on those messages until someone runs a proper resync — worth doing once the backend PR lands.

Testing

Verified end-to-end against a backend built from the companion branch, with three htpasswd users and an admin:

  • move mode: 2 projects re-homed alice→bob keeping the same resource IDs (NG:0x8f, NG:0x90), the colliding name deduped to Shared Name (Copy), alice gone from admin user-list, bob's own project untouched;
  • purge mode: root deleted, 1 grant revoked, 1 index entry removed, bob unaffected;
  • login freed: alice's next sign-in got root RG:0x93 vs the old RG:0x89 — a fresh identity, which is the point of the ticket;
  • move to a user with no project list (created on demand);
  • guards: both modes, neither mode, self-delete, unknown user, and a declined confirmation prompt leaving the account intact.

types:check, linter:check, formatter:check and build clean on all three touched packages. pl-client's 100 integration tests pass against a live backend.

Pre-existing, not from this change: pl-client's types:check reports 3 errors in src/core/final.ts (SharingOutbox / SharingState / SharedEnvelope missing from the generated resource-type map) — identical with this work stashed, and those types are absent from resource_types.proto in both repos. Separately, the checked-in pf-driver/dist/ imports a pframes-rs-wasm dependency it no longer declares, which breaks running the CLI until pf-driver is rebuilt.

Multi-provider auth can hand one person two accounts. The spare showed up in
the sharing user picker and its projects kept taking part in deduplication,
and no shipped tool removed one — the migration guide said as much.

`admin delete-user <user>` now does, over the new AuthAPI.DeleteUser RPC.
When the account still owns projects it requires an explicit decision rather
than picking a default, because both defaults are wrong to assume — one
silently destroys data, the other silently hands it to someone:

  --move-projects-to <user>  re-attach every project to another user's root,
                             then delete the account
  --delete-projects          delete the projects with the account

The move is a move, not a copy: the same project resources are re-homed, so
nothing is duplicated and nothing needs re-verifying. This works across roots
because the backend allows a reference between differently coloured resources
when the caller holds write access to both. A name the target already uses is
suffixed rather than overwritten, and the target's project list is created if
they never had one. The whole batch is one transaction.

Both forms prompt with the affected project list first; --force skips it.

pl-client gains PlClient.deleteUser (gRPC-only, like listUsers), and
pl-middle-layer exports ProjectsResourceType, which a caller writing into
another user's root needs.
@notion-workspace

Copy link
Copy Markdown

@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fd0ae2c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@platforma-sdk/pl-cli Minor
@milaboratories/pl-client Minor
@milaboratories/pl-middle-layer Patch
@milaboratories/pl-model-backend Patch
@milaboratories/pl-errors Patch
@milaboratories/pl-tree Patch
@milaboratories/pl-drivers Patch
@platforma-sdk/test Patch
@platforma-sdk/tengo-builder Patch
@platforma-sdk/block-tools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
508 1 507 6
View the top 1 failed test(s) by shortest run time
src/core/ll_client.test.ts > wire protocol detection
Stack Traces | 0.0621s run time
Error: RpcError: failed to record user login

Code: INTERNAL
Method: MiLaboratories.PL.API.Platform/Login

Meta:
  content-type: application/grpc
 ❯ UnauthenticatedPlClient.login src/core/unauth_client.ts:155:13
 ❯ getTestClientConf src/test/test_config.ts:157:40
 ❯ src/core/ll_client.test.ts:19:26

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

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