feat: namespace plugin permissions and keep them valid when a plugin is off

This commit is contained in:
LukeGus committed 2026-09-22 17:30:00 -05:00
1 parent 8431570ff7
commit cc15559ea1
53 files changed
+31442 -351

No files matched your search

+16
View File
@@ -751,6 +751,22 @@ async function initializeCompleteDatabase(): Promise<void> {
FOREIGN KEY (plugin_id) REFERENCES plugins (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS rbac_known_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
permission TEXT NOT NULL,
plugin_id TEXT,
first_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (permission)
);
CREATE TABLE IF NOT EXISTS rbac_applied_defaults (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role_name TEXT NOT NULL,
permission TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (role_name, permission)
);
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
+54
View File
@@ -2282,3 +2282,57 @@ export const pluginMigrations = mysqlTable(
);
// --- plugins end ---
// --- rbac plugin permissions begin ---
/**
* Every role permission core has ever registered.
*
* Deliberately has no foreign key to `plugins`: the whole point is that a role
* keeps working when the plugin that contributed a permission is disabled or
* uninstalled. Without this, unregistering a group made PUT /rbac/roles/:id
* reject the entire role.
*/
export const rbacKnownPermissions = mysqlTable(
"rbac_known_permissions",
{
id: int("id").autoincrement().primaryKey(),
permission: varchar("permission", { length: 255 }).notNull(),
/** Which plugin contributed it, or null for a core permission. */
pluginId: varchar("plugin_id", { length: 255 }),
firstSeenAt: text("first_seen_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
},
(table) => [
uniqueIndex("idx_rbac_known_permissions_permission").on(table.permission),
],
);
/**
* Which plugin role defaults have already been applied.
*
* Applying a default is a one-time suggestion, so an admin who revokes it does
* not get it handed back on the next restart. This lived in `plugin_storage`,
* which cascades with the plugin, so uninstall-then-reinstall silently re-added
* a permission that had been deliberately removed.
*/
export const rbacAppliedDefaults = mysqlTable(
"rbac_applied_defaults",
{
id: int("id").autoincrement().primaryKey(),
roleName: varchar("role_name", { length: 255 }).notNull(),
permission: varchar("permission", { length: 255 }).notNull(),
appliedAt: text("applied_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
},
(table) => [
uniqueIndex("idx_rbac_applied_defaults_role_permission").on(
table.roleName,
table.permission,
),
],
);
// --- rbac plugin permissions end ---
+54
View File
@@ -2283,3 +2283,57 @@ export const pluginMigrations = pgTable(
);
// --- plugins end ---
// --- rbac plugin permissions begin ---
/**
* Every role permission core has ever registered.
*
* Deliberately has no foreign key to `plugins`: the whole point is that a role
* keeps working when the plugin that contributed a permission is disabled or
* uninstalled. Without this, unregistering a group made PUT /rbac/roles/:id
* reject the entire role.
*/
export const rbacKnownPermissions = pgTable(
"rbac_known_permissions",
{
id: serial("id").primaryKey(),
permission: varchar("permission", { length: 255 }).notNull(),
/** Which plugin contributed it, or null for a core permission. */
pluginId: varchar("plugin_id", { length: 255 }),
firstSeenAt: text("first_seen_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_rbac_known_permissions_permission").on(table.permission),
],
);
/**
* Which plugin role defaults have already been applied.
*
* Applying a default is a one-time suggestion, so an admin who revokes it does
* not get it handed back on the next restart. This lived in `plugin_storage`,
* which cascades with the plugin, so uninstall-then-reinstall silently re-added
* a permission that had been deliberately removed.
*/
export const rbacAppliedDefaults = pgTable(
"rbac_applied_defaults",
{
id: serial("id").primaryKey(),
roleName: varchar("role_name", { length: 255 }).notNull(),
permission: varchar("permission", { length: 255 }).notNull(),
appliedAt: text("applied_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_rbac_applied_defaults_role_permission").on(
table.roleName,
table.permission,
),
],
);
// --- rbac plugin permissions end ---
+54
View File
@@ -2281,3 +2281,57 @@ export const pluginMigrations = sqliteTable(
);
// --- plugins end ---
// --- rbac plugin permissions begin ---
/**
* Every role permission core has ever registered.
*
* Deliberately has no foreign key to `plugins`: the whole point is that a role
* keeps working when the plugin that contributed a permission is disabled or
* uninstalled. Without this, unregistering a group made PUT /rbac/roles/:id
* reject the entire role.
*/
export const rbacKnownPermissions = sqliteTable(
"rbac_known_permissions",
{
id: integer("id").primaryKey({ autoIncrement: true }),
permission: text("permission").notNull(),
/** Which plugin contributed it, or null for a core permission. */
pluginId: text("plugin_id"),
firstSeenAt: text("first_seen_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_rbac_known_permissions_permission").on(table.permission),
],
);
/**
* Which plugin role defaults have already been applied.
*
* Applying a default is a one-time suggestion, so an admin who revokes it does
* not get it handed back on the next restart. This lived in `plugin_storage`,
* which cascades with the plugin, so uninstall-then-reinstall silently re-added
* a permission that had been deliberately removed.
*/
export const rbacAppliedDefaults = sqliteTable(
"rbac_applied_defaults",
{
id: integer("id").primaryKey({ autoIncrement: true }),
roleName: text("role_name").notNull(),
permission: text("permission").notNull(),
appliedAt: text("applied_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_rbac_applied_defaults_role_permission").on(
table.roleName,
table.permission,
),
],
);
// --- rbac plugin permissions end ---