feat: share credentials with users and roles, inherit data on account deletion (#1342)

* feat: share credentials with users and roles, inherit data on account deletion

Credentials can be shared at "use" or "manage" level. Recipients get
a copy re-encrypted under their own data key (shared_credential_secrets),
kept in step with the owner's row through the same lifecycle hooks as
shared host secrets. One gate, findUsableCredential(), replaces the
private-namespace lookups so a shared credential works wherever a
private one does. Deleting a user now hands their hosts and credentials
to a successor (the deleting admin by default) instead of revoking
everything they shared.

* fix: harden credential ownership transfer
This commit is contained in:
ZacharyZcR
2026-08-25 04:12:47 +08:00
committed by GitHub
parent 5f55289e00
commit 82143946c7
41 changed files with 30051 additions and 59 deletions
@@ -348,6 +348,54 @@ export class CredentialRepository {
);
}
/**
* Re-keys every credential of one user to another: decrypt under the old
* owner's key, encrypt under the new one, change the owner. Used when an
* account is deleted and its data is inherited rather than dropped.
*/
async transferAllToUser(
fromUserId: string,
toUserId: string,
): Promise<number[]> {
const fromKey = DataCrypto.validateUserAccess(fromUserId);
const toKey = DataCrypto.validateUserAccess(toUserId);
const rows = await this.context.drizzle
.select()
.from(sshCredentials)
.where(eq(sshCredentials.userId, fromUserId));
const moved: number[] = [];
for (const row of rows) {
const plain = DataCrypto.decryptRecord(
"ssh_credentials",
row,
fromUserId,
fromKey,
);
const {
id: _id,
userId: _userId,
...fields
} = plain as Record<string, unknown>;
const encrypted = DataCrypto.encryptRecord(
"ssh_credentials",
{ ...fields, id: row.id },
toUserId,
toKey,
) as Record<string, unknown>;
delete encrypted.id;
await this.context.drizzle
.update(sshCredentials)
.set({
...(encrypted as Partial<NewCredentialRecord>),
userId: toUserId,
})
.where(eq(sshCredentials.id, row.id));
moved.push(row.id);
}
if (moved.length) await this.afterWrite();
return moved;
}
private encryptCredentialRecordForWrite<T extends Record<string, unknown>>(
record: T,
userId: string,