Files
windmill/frontend/src/lib/components/workspaceSettings/datatableRoleModals.ts
T
d7d18f00af feat: improve data table roles and grants UX (#11562)
* feat: improve data table roles and grants UX

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep selected bordered badges the same height as unselected ones

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: refuse taking over roles with members and test grant statements

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep grant filter chips while a filter is set

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: check a taken-over role can log in with the new password

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: select several grant filter tags at once

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: explain where schema and table permissions are set

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep the grant create row on one line

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: show grants as a role and its privileges

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: show grants in a table with role, privileges and scope

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: hide the grant scope when it can only be the object itself

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: grant schema usage along with grants inside the schema

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: confirm before closing the roles drawer with unsaved changes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to c4002f777f56d90bb0471164291889850c85179d

This commit updates the EE repository reference after PR #848 was merged in windmill-ee-private.

Previous ee-repo-ref: 7937634a52d04caeb083da94e189705cd00796b3

New ee-repo-ref: c4002f777f56d90bb0471164291889850c85179d

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-08 08:53:23 +02:00

121 lines
4.7 KiB
TypeScript

import { SettingService, type DatatableRoleCluster, type InstanceDatatableRole } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import type { ConfirmationModalHandle } from '../common/confirmationModal/asyncConfirmationModal.svelte'
export function pgInstanceName(cluster: DatatableRoleCluster | undefined): string {
return cluster === 'external_instance'
? 'the external Postgres instance'
: 'the Windmill Postgres instance'
}
function escapeHtml(text: string): string {
return text.replace(
/[&<>"']/g,
(c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!
)
}
/** Reports a failed role creation. One refused because Postgres already has a role of that name
* (a 409) opens the offer to take that role over instead. `confirmationModal` must be closed: the
* offer takes it over. */
export async function handleRoleCreationError(
e: any,
opts: {
name: string
cluster: DatatableRoleCluster | undefined
confirmationModal: ConfirmationModalHandle
onTakenOver: (role: InstanceDatatableRole) => void
}
) {
if (e?.status !== 409) {
sendUserToast(e?.body ?? e?.message ?? String(e), true)
return
}
await offerTakeover(opts)
}
/** A role no data table names any more is still a login on its Postgres instance. A superadmin is
* offered to drop it; anyone else is told to ask one. Resolves to whether it was dropped. */
export async function offerUnusedRoleDrop({
role,
cluster,
superadmin,
confirmationModal
}: {
role: InstanceDatatableRole
cluster: DatatableRoleCluster | undefined
superadmin: boolean
confirmationModal: ConfirmationModalHandle
}): Promise<boolean> {
const name = `<span class="font-mono">${escapeHtml(role.name)}</span>`
const instance = pgInstanceName(cluster)
if (!superadmin) {
await confirmationModal.ask({
title: `Role ${role.name} is no longer used`,
children: `<p>No data table uses the role ${name} any more, but its login still exists in ${instance}. Ask a superadmin to drop it there.</p>`,
confirmationText: 'OK',
type: 'info',
hideCancel: true
})
return false
}
let dropped = false
await confirmationModal.ask({
title: `Drop the role ${role.name}?`,
children: `<div class="flex flex-col gap-2">
<p>No data table uses the role ${name} any more, but its login still exists in ${instance}. Windmill can drop it.</p>
<p>Dropping it <b>deletes the login ${name}</b> from ${instance}: anything still connecting as it, such as a script with <span class="font-mono">-- role ${escapeHtml(role.name)}</span>, fails from then on. Everything it owns in the databases Windmill manages there is handed back to admin, and its grants are revoked. This cannot be undone.</p>
<p>Keep it if you plan to give it to a data table again.</p>
</div>`,
confirmationText: 'Drop role',
type: 'danger',
onConfirmed: async () => {
try {
await SettingService.deleteInstanceDatatableRole({ id: role.id })
dropped = true
sendUserToast(`Dropped the role ${role.name}`)
} catch (e) {
sendUserToast(e?.body ?? e?.message ?? String(e), true)
}
}
})
return dropped
}
async function offerTakeover({
name,
cluster,
confirmationModal,
onTakenOver
}: {
name: string
cluster: DatatableRoleCluster | undefined
confirmationModal: ConfirmationModalHandle
onTakenOver: (role: InstanceDatatableRole) => void
}) {
const role = `<span class="font-mono">${escapeHtml(name)}</span>`
let taken: InstanceDatatableRole | undefined
await confirmationModal.ask({
title: `Let Windmill take over the role ${name}?`,
children: `<div class="flex flex-col gap-2">
<p>A Postgres role named ${role} already exists in ${pgInstanceName(cluster)}, and Windmill did not create it. A data table role logs in with a password only Windmill knows, so Windmill has to manage it to use it.</p>
<p>Taking it over <b>replaces the password of ${role}</b> with a random one and lets it log in. Anything that logs in as ${role} today with its current password, such as another application, a person or another Windmill instance, will be locked out. The old password cannot be restored.</p>
<p>From then on Windmill manages ${role} like a role it created: deleting it in Windmill drops it from Postgres. A role that is a superuser, can create roles or databases, is a member of other roles or has members of its own is refused.</p>
</div>`,
confirmationText: 'Replace password and take over',
type: 'danger',
onConfirmed: async () => {
try {
taken = await SettingService.createInstanceDatatableRole({
requestBody: { name, cluster, take_over: true }
})
} catch (e) {
sendUserToast(e?.body ?? e?.message ?? String(e), true)
}
}
})
if (!taken) return
sendUserToast(`Windmill now manages the role ${name}`)
onTakenOver(taken)
}