mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
fix(db): Renumber workflow review migration to 1784000000048
Master now uses 0047 for OAuth grant scope backfill. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
615b0a0707
commit
df9aa6efe4
|
|
@ -84,7 +84,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
|
|||
| [public.oauth_authorization_codes](public.oauth_authorization_codes.md) | 13 | | BASE TABLE |
|
||||
| [public.oauth_clients](public.oauth_clients.md) | 9 | | BASE TABLE |
|
||||
| [public.oauth_refresh_tokens](public.oauth_refresh_tokens.md) | 7 | | BASE TABLE |
|
||||
| [public.oauth_user_consents](public.oauth_user_consents.md) | 4 | | BASE TABLE |
|
||||
| [public.oauth_user_consents](public.oauth_user_consents.md) | 5 | | BASE TABLE |
|
||||
| [public.processed_data](public.processed_data.md) | 5 | | BASE TABLE |
|
||||
| [public.project](public.project.md) | 9 | | BASE TABLE |
|
||||
| [public.project_relation](public.project_relation.md) | 5 | | BASE TABLE |
|
||||
|
|
@ -1061,6 +1061,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
integer id
|
||||
json scope
|
||||
uuid userId FK
|
||||
}
|
||||
"public.processed_data" {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
integer id
|
||||
json scope
|
||||
uuid userId FK
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
| clientId | varchar | | false | | [public.oauth_clients](public.oauth_clients.md) | |
|
||||
| grantedAt | bigint | | false | | | Unix timestamp in milliseconds |
|
||||
| id | integer | | false | | | |
|
||||
| scope | json | | false | | | OAuth scopes granted on the consent screen |
|
||||
| userId | uuid | | false | | [public.user](public.user.md) | |
|
||||
|
||||
## Constraints
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
| oauth_user_consents_clientId_not_null | n | NOT NULL "clientId" |
|
||||
| oauth_user_consents_grantedAt_not_null | n | NOT NULL "grantedAt" |
|
||||
| oauth_user_consents_id_not_null | n | NOT NULL id |
|
||||
| oauth_user_consents_scope_not_null | n | NOT NULL scope |
|
||||
| oauth_user_consents_userId_not_null | n | NOT NULL "userId" |
|
||||
|
||||
## Indexes
|
||||
|
|
@ -41,6 +43,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
integer id
|
||||
json scope
|
||||
uuid userId FK
|
||||
}
|
||||
"public.oauth_clients" {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
integer id
|
||||
json scope
|
||||
uuid userId FK
|
||||
}
|
||||
"public.project" {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
|
|||
| [oauth_authorization_codes](oauth_authorization_codes.md) | 13 | | table |
|
||||
| [oauth_clients](oauth_clients.md) | 9 | | table |
|
||||
| [oauth_refresh_tokens](oauth_refresh_tokens.md) | 7 | | table |
|
||||
| [oauth_user_consents](oauth_user_consents.md) | 4 | | table |
|
||||
| [oauth_user_consents](oauth_user_consents.md) | 5 | | table |
|
||||
| [processed_data](processed_data.md) | 5 | | table |
|
||||
| [project](project.md) | 9 | | table |
|
||||
| [project_relation](project_relation.md) | 5 | | table |
|
||||
|
|
@ -1048,6 +1048,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
INTEGER id
|
||||
TEXT scope
|
||||
varchar userId FK
|
||||
}
|
||||
"processed_data" {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
INTEGER id
|
||||
TEXT scope
|
||||
varchar userId FK
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<summary><strong>Table Definition</strong></summary>
|
||||
|
||||
```sql
|
||||
CREATE TABLE "oauth_user_consents" ("id" integer PRIMARY KEY NOT NULL, "userId" varchar NOT NULL, "clientId" varchar NOT NULL, "grantedAt" bigint NOT NULL, CONSTRAINT "UQ_083721d99ce8db4033e2958ebb4" UNIQUE ("userId", "clientId"), CONSTRAINT "FK_a651acea2f6c97f8c4514935486" FOREIGN KEY ("clientId") REFERENCES "oauth_clients" ("id") ON DELETE CASCADE, CONSTRAINT "FK_21e6c3c2d78a097478fae6aaefa" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE)
|
||||
CREATE TABLE "oauth_user_consents" ("id" integer PRIMARY KEY NOT NULL, "userId" varchar NOT NULL, "clientId" varchar NOT NULL, "grantedAt" bigint NOT NULL, "scope" text NOT NULL, CONSTRAINT "UQ_083721d99ce8db4033e2958ebb4" UNIQUE ("userId", "clientId"), CONSTRAINT "FK_a651acea2f6c97f8c4514935486" FOREIGN KEY ("clientId") REFERENCES "oauth_clients" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_21e6c3c2d78a097478fae6aaefa" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
|
@ -18,6 +18,7 @@ CREATE TABLE "oauth_user_consents" ("id" integer PRIMARY KEY NOT NULL, "userId"
|
|||
| clientId | varchar | | false | | [oauth_clients](oauth_clients.md) | |
|
||||
| grantedAt | bigint | | false | | | |
|
||||
| id | INTEGER | | false | | | |
|
||||
| scope | TEXT | | false | | | |
|
||||
| userId | varchar | | false | | [user](user.md) | |
|
||||
|
||||
## Constraints
|
||||
|
|
@ -47,6 +48,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
INTEGER id
|
||||
TEXT scope
|
||||
varchar userId FK
|
||||
}
|
||||
"oauth_clients" {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ erDiagram
|
|||
varchar clientId FK
|
||||
bigint grantedAt
|
||||
INTEGER id
|
||||
TEXT scope
|
||||
varchar userId FK
|
||||
}
|
||||
"project" {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
import type { MigrationContext, IrreversibleMigration } from '../migration-types';
|
||||
|
||||
// Backfill value written to the `scope` column by migration 1784000000026 for
|
||||
// grants that predate scoping. No runtime code ever wrote this value, so it
|
||||
// provably identifies a grant made under the old full-delegation contract.
|
||||
const LEGACY_SENTINEL = ['tool:listWorkflows', 'tool:getWorkflowDetails'];
|
||||
|
||||
// The instance MCP scope set at the moment scoping shipped. Pre-scoping grants
|
||||
// were full user delegations, so they get exactly this frozen set — scopes
|
||||
// added in later releases require a fresh consent. Inlined because migrations
|
||||
// are snapshots and must not import runtime code.
|
||||
const PRE_SCOPING_FULL_SCOPES = [
|
||||
'workflow:read',
|
||||
'workflow:write',
|
||||
'workflow:execute',
|
||||
'execution:read',
|
||||
'credential:read',
|
||||
'dataTable:read',
|
||||
'dataTable:write',
|
||||
'project:read',
|
||||
'tag:read',
|
||||
];
|
||||
|
||||
/**
|
||||
* Moves OAuth grants that predate scoping onto explicit scopes so no runtime
|
||||
* grandfathering logic is needed:
|
||||
* - `oauth_user_consents` gets a NOT NULL `scope` column, with existing
|
||||
* consents backfilled to the frozen launch scope set;
|
||||
* - sentinel-scoped refresh tokens and authorization codes get the same set;
|
||||
* - all access tokens are deleted. They are 1-hour JWTs whose `scope` claim
|
||||
* cannot be rewritten; deleting the rows makes verification fail so clients
|
||||
* transparently refresh, and the refreshed tokens carry a real scope claim.
|
||||
*/
|
||||
export class BackfillPreScopingOAuthGrantScopes1784000000047 implements IrreversibleMigration {
|
||||
async up(context: MigrationContext) {
|
||||
await this.addConsentScopeColumn(context);
|
||||
await this.rewriteSentinelRows(context, 'oauth_refresh_tokens', 'token');
|
||||
await this.rewriteSentinelRows(context, 'oauth_authorization_codes', 'code');
|
||||
|
||||
const accessTokens = context.escape.tableName('oauth_access_tokens');
|
||||
await context.runQuery(`DELETE FROM ${accessTokens}`);
|
||||
}
|
||||
|
||||
private async addConsentScopeColumn({
|
||||
schemaBuilder: { addColumns, addNotNull, column },
|
||||
escape,
|
||||
runQuery,
|
||||
}: MigrationContext) {
|
||||
await addColumns(
|
||||
'oauth_user_consents',
|
||||
[column('scope').json.comment('OAuth scopes granted on the consent screen')],
|
||||
{ recreatesOnSqlite: true },
|
||||
);
|
||||
|
||||
const table = escape.tableName('oauth_user_consents');
|
||||
const scopeColumn = escape.columnName('scope');
|
||||
await runQuery(`UPDATE ${table} SET ${scopeColumn} = :scope WHERE ${scopeColumn} IS NULL`, {
|
||||
scope: JSON.stringify(PRE_SCOPING_FULL_SCOPES),
|
||||
});
|
||||
|
||||
// No default on purpose: an insert that forgets the scope must fail
|
||||
// instead of silently granting anything.
|
||||
await addNotNull('oauth_user_consents', 'scope', { recreatesOnSqlite: true });
|
||||
}
|
||||
|
||||
private async rewriteSentinelRows(
|
||||
{ escape, runQuery, runInBatches, parseJson, logger, migrationName }: MigrationContext,
|
||||
tableName: string,
|
||||
primaryKey: string,
|
||||
) {
|
||||
const table = escape.tableName(tableName);
|
||||
const scopeColumn = escape.columnName('scope');
|
||||
const keyColumn = escape.columnName(primaryKey);
|
||||
|
||||
let rewritten = 0;
|
||||
// JSON equality is not portable SQL, so rows are compared in JS. The
|
||||
// ORDER BY keeps LIMIT/OFFSET pages stable while rows are updated mid-scan.
|
||||
await runInBatches<{ key: string; scope: string | string[] }>(
|
||||
`SELECT ${keyColumn} AS key, ${scopeColumn} AS scope FROM ${table} ORDER BY ${keyColumn}`,
|
||||
async (rows) => {
|
||||
for (const row of rows) {
|
||||
const scopes = parseJson<string[]>(row.scope);
|
||||
const isSentinel =
|
||||
Array.isArray(scopes) &&
|
||||
scopes.length === LEGACY_SENTINEL.length &&
|
||||
LEGACY_SENTINEL.every((scope, i) => scopes[i] === scope);
|
||||
if (!isSentinel) continue;
|
||||
|
||||
await runQuery(`UPDATE ${table} SET ${scopeColumn} = :scope WHERE ${keyColumn} = :key`, {
|
||||
scope: JSON.stringify(PRE_SCOPING_FULL_SCOPES),
|
||||
key: row.key,
|
||||
});
|
||||
rewritten++;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
logger.info(`[${migrationName}] Rewrote ${rewritten} pre-scoping grants in ${tableName}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ const UQ_WORKFLOW_REQUEST_WORKFLOW = 'workflow_review_request_workflow_request_w
|
|||
const UQ_WORKFLOW_REQUEST_REVIEWERS = 'workflow_review_request_reviewers_request_user';
|
||||
const UQ_WORKFLOW_REQUEST_AUTHORS = 'workflow_review_request_authors_request_user';
|
||||
|
||||
export class CreateWorkflowReviewRequestTables1784000000047 implements ReversibleMigration {
|
||||
export class CreateWorkflowReviewRequestTables1784000000048 implements ReversibleMigration {
|
||||
async up(context: MigrationContext) {
|
||||
await this.createRequestTable(context);
|
||||
await this.createWorkflowChildTable(context);
|
||||
|
|
@ -220,7 +220,8 @@ import { CreateSchedulerTables1784000000042 } from '../common/1784000000042-Crea
|
|||
import { AddPartialIndexForGlobalCredentials1784000000044 } from '../common/1784000000044-AddPartialIndexForGlobalCredentials';
|
||||
import { AddRecurringCronScheduleKind1784000000045 } from '../common/1784000000045-AddRecurringCronScheduleKind';
|
||||
import { CreateInstanceAiEventsTable1784000000046 } from '../common/1784000000046-CreateInstanceAiEventsTable';
|
||||
import { CreateWorkflowReviewRequestTables1784000000047 } from '../common/1784000000047-CreateWorkflowReviewRequestTables';
|
||||
import { BackfillPreScopingOAuthGrantScopes1784000000047 } from '../common/1784000000047-BackfillPreScopingOAuthGrantScopes';
|
||||
import { CreateWorkflowReviewRequestTables1784000000048 } from '../common/1784000000048-CreateWorkflowReviewRequestTables';
|
||||
import type { Migration } from '../migration-types';
|
||||
|
||||
export const postgresMigrations: Migration[] = [
|
||||
|
|
@ -446,5 +447,6 @@ export const postgresMigrations: Migration[] = [
|
|||
AddPartialIndexForGlobalCredentials1784000000044,
|
||||
AddRecurringCronScheduleKind1784000000045,
|
||||
CreateInstanceAiEventsTable1784000000046,
|
||||
CreateWorkflowReviewRequestTables1784000000047,
|
||||
BackfillPreScopingOAuthGrantScopes1784000000047,
|
||||
CreateWorkflowReviewRequestTables1784000000048,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -212,7 +212,8 @@ import { AddUsedPrivateCredentialsToExecutionEntity1784000000041 } from '../comm
|
|||
import { CreateSchedulerTables1784000000042 } from '../common/1784000000042-CreateSchedulerTables';
|
||||
import { AddPartialIndexForGlobalCredentials1784000000044 } from '../common/1784000000044-AddPartialIndexForGlobalCredentials';
|
||||
import { CreateInstanceAiEventsTable1784000000046 } from '../common/1784000000046-CreateInstanceAiEventsTable';
|
||||
import { CreateWorkflowReviewRequestTables1784000000047 } from '../common/1784000000047-CreateWorkflowReviewRequestTables';
|
||||
import { BackfillPreScopingOAuthGrantScopes1784000000047 } from '../common/1784000000047-BackfillPreScopingOAuthGrantScopes';
|
||||
import { CreateWorkflowReviewRequestTables1784000000048 } from '../common/1784000000048-CreateWorkflowReviewRequestTables';
|
||||
|
||||
const sqliteMigrations: Migration[] = [
|
||||
InitialMigration1588102412422,
|
||||
|
|
@ -428,7 +429,8 @@ const sqliteMigrations: Migration[] = [
|
|||
AddPartialIndexForGlobalCredentials1784000000044,
|
||||
AddRecurringCronScheduleKind1784000000045,
|
||||
CreateInstanceAiEventsTable1784000000046,
|
||||
CreateWorkflowReviewRequestTables1784000000047,
|
||||
BackfillPreScopingOAuthGrantScopes1784000000047,
|
||||
CreateWorkflowReviewRequestTables1784000000048,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { DataSource } from '@n8n/typeorm';
|
|||
import { generateNanoId } from '@n8n/utils/generate-nano-id';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const MIGRATION_NAME = 'CreateWorkflowReviewRequestTables1784000000047';
|
||||
const MIGRATION_NAME = 'CreateWorkflowReviewRequestTables1784000000048';
|
||||
|
||||
describe('CreateWorkflowReviewRequestTables Migration', () => {
|
||||
let dataSource: DataSource;
|
||||
Loading…
Reference in New Issue
Block a user