feat(datatables): a permissions drawer on a table

This commit is contained in:
Diego Imbert
2026-08-31 06:52:03 +02:00
parent a8a8812839
commit eb95a95f48
3 changed files with 61 additions and 22 deletions
@@ -363,8 +363,12 @@ fn plan_statements(
}
// Ownership cannot be set ahead of time: an object belongs to
// whoever creates it. Default privileges are what keeps the owner
// in reach of what the other roles create from here on.
for other in other_pg_roles {
// in reach of what the other roles create from here on — which only
// means something for a schema, the thing objects are created in.
for other in other_pg_roles
.iter()
.filter(|_| matches!(target, AclTarget::Schema { .. }))
{
for plural in ["TABLES", "SEQUENCES", "FUNCTIONS"] {
statements.push(format!(
"ALTER DEFAULT PRIVILEGES FOR ROLE {} IN SCHEMA {} GRANT ALL PRIVILEGES ON {} TO {}",
@@ -375,7 +379,7 @@ fn plan_statements(
));
}
}
if existing_objects.is_empty() {
if existing_objects.is_empty() && matches!(target, AclTarget::Schema { .. }) {
warnings.push(format!(
"{} holds no objects yet; only the schema itself changes hands.",
target.label(dbname)
@@ -1053,6 +1057,26 @@ mod tests {
}
}
#[test]
fn a_tables_owner_change_is_only_that_table() {
let plan = plan_statements(
&AclTarget::Table { schema: "analytics".to_string(), table: "orders".to_string() },
&AclChange::SetOwner { role: "analyst".to_string() },
"dt_probe",
"wm_analyst_1",
&["wm_admin".to_string()],
&[],
)
.unwrap();
// Default privileges are about what gets created in a schema, which
// changing one table's owner says nothing about.
assert_eq!(
plan.statements,
[r#"ALTER TABLE "analytics"."orders" OWNER TO "wm_analyst_1""#]
);
assert!(plan.warnings.is_empty());
}
#[test]
fn several_objects_are_revoked_together() {
let plan = plan_statements(
+31 -16
View File
@@ -38,7 +38,7 @@
import PgAclEditor from './datatableAcl/PgAclEditor.svelte'
import { favoriteManager } from './sidebar/FavoriteMenu.svelte'
import DatatableRoleBadge from './DatatableRoleBadge.svelte'
import type { Asset, DataTableTables } from '$lib/gen'
import type { AclTarget, Asset, DataTableTables } from '$lib/gen'
import { ADMIN_DATATABLE_ROLE, type DatatableRowAction } from './dbTypes'
import TextInput from './text_input/TextInput.svelte'
import Checkbox from './common/checkbox/Checkbox.svelte'
@@ -287,8 +287,8 @@
// overrides is what lets the current data table and selected schema — which
// default to open — actually be folded; a plain "expanded" set could never
// close them, since the default would keep winning.
// The schema the permissions drawer is open on, if any.
let schemaPermissions = $state<{ datatable: string | undefined; schema: string } | undefined>(
// What the permissions drawer is open on, if anything: a schema, or a table.
let aclDrawer = $state<{ datatable: string | undefined; target: AclTarget } | undefined>(
undefined
)
@@ -536,6 +536,12 @@
)
let newSchemaName = $state('')
/** What the drawer is about, for its title. */
function aclLabel(target: AclTarget | undefined): string {
if (!target) return ''
return target.kind === 'table' ? `${target.schema}.${target.table}` : (target.schema ?? '')
}
function closeSchemaDialog() {
schemaDialog = undefined
newSchemaName = ''
@@ -727,9 +733,9 @@
displayName: 'Permissions',
icon: KeyRoundIcon,
action: () =>
(schemaPermissions = {
(aclDrawer = {
datatable: root.datatable,
schema: sc.schemaKey
target: { kind: 'schema', schema: sc.schemaKey }
})
},
{
@@ -805,6 +811,19 @@
<DropdownV2
enableFlyTransition
items={() => [
{
displayName: 'Permissions',
icon: KeyRoundIcon,
action: () =>
(aclDrawer = {
datatable: root.datatable,
target: {
kind: 'table',
schema: sc.schemaKey,
table: tableKey
}
})
},
{
displayName: 'Delete table',
icon: Trash2Icon,
@@ -886,20 +905,16 @@
</Splitpanes>
<Portal>
<Drawer open={!!schemaPermissions} size="900px" on:close={() => (schemaPermissions = undefined)}>
<Drawer open={!!aclDrawer} size="900px" on:close={() => (aclDrawer = undefined)}>
<DrawerContent
title="Permissions — {schemaPermissions?.schema ?? ''}"
on:close={() => (schemaPermissions = undefined)}
tooltip="Who owns this schema, and what each role may do in it. Runs against the data table as its admin connection."
title="Permissions — {aclLabel(aclDrawer?.target)}"
on:close={() => (aclDrawer = undefined)}
tooltip="Who owns this, and what each role may do with it. Runs against the data table as its admin connection."
>
{#if schemaPermissions && workspace}
{@const dt = schemaPermissions.datatable ?? currentDatatable}
{#if aclDrawer && workspace}
{@const dt = aclDrawer.datatable ?? currentDatatable}
{#if dt}
<PgAclEditor
{workspace}
datatable={dt}
target={{ kind: 'schema', schema: schemaPermissions.schema }}
/>
<PgAclEditor {workspace} datatable={dt} target={aclDrawer.target} />
{/if}
{/if}
</DrawerContent>
@@ -106,9 +106,9 @@
<div class="flex flex-col gap-0.5">
<span class="text-sm font-semibold text-primary">Owner</span>
<span class="text-xs text-secondary">
The role that owns {target.kind === 'schema' ? 'the schema' : 'the table'} and everything
already in it. Changing it also lets the new owner reach what the other roles create here
later.
{target.kind === 'schema'
? 'The role that owns the schema and everything already in it. Changing it also lets the new owner reach what the other roles create here later.'
: 'The role that owns the table. Its owner may always read and write it, and is who ALTER and DROP answer to.'}
</span>
</div>
<Select