A withdrawn consumer keeps its data, in every provider that holds some (hq issue 241)
mssql disables the login, mongodb takes the user's roles, minio revokes the key and keeps the bucket, mailu disables the mailbox, gitea prohibits the login instead of purging the user and their repositories, umami keeps the website. Each provider's create already enables what this locks.
This commit is contained in:
@@ -564,6 +564,16 @@ export class GiteaAdmin {
|
||||
GiteaAdmin.fail(`/teams/${found.id}/members/${username}`, member);
|
||||
}
|
||||
|
||||
/** Withdraw a user and keep everything they own: login prohibited, which ensureUser undoes. */
|
||||
async prohibitLogin(username: string): Promise<void> {
|
||||
const res = await this.request(`/admin/users/${encodeURIComponent(username)}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ login_name: username, prohibit_login: true }),
|
||||
});
|
||||
if (res.status === 200 || res.status === 404) return;
|
||||
GiteaAdmin.fail(`/admin/users/${username}`, res);
|
||||
}
|
||||
|
||||
/** Delete a user, purging what they own. A 404 means the mesh already withdrew them — success, not
|
||||
* an error, so a re-run of remove is safe. */
|
||||
async deleteUser(username: string): Promise<void> {
|
||||
|
||||
@@ -54,7 +54,8 @@ runProvisioner("npm-package-registry", {
|
||||
},
|
||||
|
||||
async remove(p: { as: string }): Promise<void> {
|
||||
await gitea.deleteUser(p.as);
|
||||
// Login prohibited, never deleted (novox/hq issue 241: a withdrawal never destroys a consumer's data — on 2026-10-04 a misread grants file withdrew every consumer at once): deleting purges every repository the user owns.
|
||||
await gitea.prohibitLogin(p.as);
|
||||
},
|
||||
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
|
||||
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
|
||||
|
||||
@@ -140,6 +140,11 @@ export class MailuClient {
|
||||
await this.api("PATCH", `/user/${encodeURIComponent(email)}`, { raw_password: password, enabled: true });
|
||||
}
|
||||
|
||||
/** Withdraw a mailbox and keep its mail: disabled, which applyProvisioned undoes. */
|
||||
async disableUser(email: string): Promise<void> {
|
||||
await this.api("PATCH", `/user/${encodeURIComponent(email)}`, { enabled: false });
|
||||
}
|
||||
|
||||
async deleteUser(email: string): Promise<void> {
|
||||
await this.api("DELETE", `/user/${encodeURIComponent(email)}`);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ runProvisioner("smtp", {
|
||||
// exists, and left otherwise — a mailbox holding mail is the one thing a background loop
|
||||
// must not guess about (this module's own events file says the same). Withdrawal of a
|
||||
// named-account consumer is an operator action until the harness carries values here.
|
||||
await mailu.deleteUser(`${p.as}@${domain()}`).catch(() => {});
|
||||
// Disabled, never deleted (novox/hq issue 241: a withdrawal never destroys a consumer's data — on 2026-10-04 a misread grants file withdrew every consumer at once): a mailbox holding mail is the one thing a background loop must
|
||||
// not destroy. applyProvisioned enables it again when the consumer returns.
|
||||
await mailu.disableUser(`${p.as}@${domain()}`).catch(() => {});
|
||||
},
|
||||
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
|
||||
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
|
||||
|
||||
@@ -47,15 +47,10 @@ runProvisioner("s3-bucket", {
|
||||
async remove(p: { as: string; derived: Readonly<Record<string, unknown>> }): Promise<void> {
|
||||
const bucket = bucketNamed(p.derived);
|
||||
|
||||
// Revoking the key is what cuts the consumer's access. The bucket is emptied-then-dropped only if
|
||||
// empty; a bucket that still holds objects is left for an operator rather than erroring on every
|
||||
// reconcile tick — access is already gone, and silently deleting a consumer's data would be worse.
|
||||
// Revoking the key is what cuts the consumer's access, and the bucket is kept, empty or not
|
||||
// (novox/hq issue 241: a withdrawal never destroys a consumer's data — on 2026-10-04 a misread grants file withdrew every consumer at once). A bucket is removed by a person, never by this loop.
|
||||
try { await minio.removeAccessKey(p.as); } catch { /* already gone */ }
|
||||
try {
|
||||
await minio.removeBucket(bucket);
|
||||
} catch (err) {
|
||||
console.error(`[minio] bucket ${bucket} not removed (likely non-empty), access revoked: ${err}`);
|
||||
}
|
||||
console.error(`[minio] ${p.as} withdrawn: access key revoked, bucket ${bucket} kept`);
|
||||
|
||||
await announce("bucket.removed", { bucket, accessKey: p.as });
|
||||
},
|
||||
|
||||
@@ -125,6 +125,18 @@ export class MongoClient {
|
||||
|
||||
/** Drop a database and its owning user, idempotently. Dropping the database evicts its data; the
|
||||
* user is removed first so a re-grant of the same login starts clean. */
|
||||
/** Withdraw a consumer and keep its database: the user keeps its name and loses every role. */
|
||||
async lockUser(database: string, user: string): Promise<void> {
|
||||
await this.admin(async (client) => {
|
||||
const target = client.db(database);
|
||||
try {
|
||||
await target.command({ updateUser: user, roles: [] });
|
||||
} catch (err) {
|
||||
if (!(err instanceof MongoServerError && err.code === 11)) throw err; // 11: UserNotFound
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async dropDatabaseAndUser(database: string, user: string): Promise<void> {
|
||||
await this.admin(async (client) => {
|
||||
const target = client.db(database);
|
||||
|
||||
@@ -41,8 +41,9 @@ runProvisioner("mongodb-database", {
|
||||
},
|
||||
|
||||
async remove(p: { as: string }): Promise<void> {
|
||||
await mongo.dropDatabaseAndUser(p.as, p.as);
|
||||
await announce("database.deprovisioned", { database: p.as });
|
||||
// Locked, never dropped (novox/hq issue 241: a withdrawal never destroys a consumer's data — on 2026-10-04 a misread grants file withdrew every consumer at once). create gives the roles back.
|
||||
await mongo.lockUser(p.as, p.as);
|
||||
await announce("database.deprovisioned", { database: p.as, kept: "true" });
|
||||
},
|
||||
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
|
||||
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
|
||||
|
||||
@@ -256,6 +256,14 @@ export class MssqlClient {
|
||||
}
|
||||
|
||||
/** Drop a database and its login, idempotently, after evicting live connections. */
|
||||
/** Withdraw a consumer and keep its database: its login is disabled, which create undoes. */
|
||||
async disableLogin(login: string): Promise<void> {
|
||||
const logins = await this.query(
|
||||
`SELECT 1 AS ok FROM sys.server_principals WHERE name = ${literal(login)}`,
|
||||
);
|
||||
if (logins.length > 0) await this.exec(`ALTER LOGIN ${ident(login)} DISABLE`);
|
||||
}
|
||||
|
||||
async dropDatabaseAndLogin(database: string, login: string): Promise<void> {
|
||||
const dbs = await this.query(
|
||||
`SELECT 1 AS ok FROM sys.databases WHERE name = ${literal(database)}`,
|
||||
|
||||
@@ -41,8 +41,9 @@ runProvisioner("mssql-database", {
|
||||
},
|
||||
|
||||
async remove(p: { as: string }): Promise<void> {
|
||||
await mssql.dropDatabaseAndLogin(p.as, p.as);
|
||||
await announce("database.deprovisioned", { database: p.as });
|
||||
// Disabled, never dropped (novox/hq issue 241: a withdrawal never destroys a consumer's data — on 2026-10-04 a misread grants file withdrew every consumer at once). create enables the login again.
|
||||
await mssql.disableLogin(p.as);
|
||||
await announce("database.deprovisioned", { database: p.as, kept: "true" });
|
||||
},
|
||||
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
|
||||
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
|
||||
|
||||
@@ -31,9 +31,7 @@ runProvisioner("analytics", {
|
||||
},
|
||||
|
||||
async remove(p: { as: string }): Promise<void> {
|
||||
const token = await umami.getToken();
|
||||
// Keyed on the mesh-derived login, the one identity the harness carries into removal.
|
||||
const site = await umami.findWebsite(token, p.as);
|
||||
if (site) await umami.deleteWebsite(token, site.id);
|
||||
// The website and its analytics are kept (novox/hq issue 241: a withdrawal never destroys a consumer's data — on 2026-10-04 a misread grants file withdrew every consumer at once); a person deletes a site, never this loop.
|
||||
console.error(`[umami] ${p.as} withdrawn: website and its analytics kept`);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user