Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: [Pg] support for row level security #1481

Closed
wants to merge 10 commits into from
2 changes: 2 additions & 0 deletions drizzle-orm/src/aws-data-api/pg/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export class AwsDataApiSession<
await tx.setTransaction(config);
}
try {
await tx.executeRLSConfig(config);

const result = await transaction(tx);
await this.client.send(new CommitTransactionCommand({ ...this.rawQuery, transactionId }));
return result;
Expand Down
2 changes: 2 additions & 0 deletions drizzle-orm/src/neon-serverless/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export class NeonSession<
const tx = new NeonTransaction(this.dialect, session, this.schema);
await tx.execute(sql`begin ${tx.getTransactionConfigSQL(config)}`);
try {
await tx.executeRLSConfig(config);

const result = await transaction(tx);
await tx.execute(sql`commit`);
return result;
Expand Down
2 changes: 2 additions & 0 deletions drizzle-orm/src/node-postgres/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export class NodePgSession<
const tx = new NodePgTransaction(this.dialect, session, this.schema);
await tx.execute(sql`begin${config ? sql` ${tx.getTransactionConfigSQL(config)}` : undefined}`);
try {
await tx.executeRLSConfig(config);

const result = await transaction(tx);
await tx.execute(sql`commit`);
return result;
Expand Down
53 changes: 53 additions & 0 deletions drizzle-orm/src/pg-core/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { entityKind } from '~/entity.ts';
import type { AnyPgTable } from '~/pg-core/table.ts';
import { Policy } from '~/policy.ts';
import type { SQL } from '~/sql/sql.ts';
import type { AnyPgRole } from './role.ts';

export type PgPolicyFor = 'select' | 'insert' | 'update' | 'delete' | 'all';

export type PgPolicyTo = 'public' | 'current_role' | 'current_user' | 'session_user';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Supabase, we have other roles like authenticated, etc.

Maybe the type could be 'public' | 'current_role' | 'current_user' | 'session_user' | (string & {})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you handle that with AnyPgRole, sorry.
But, how can we use existing roles not created by us in the schema?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.existing() like for views?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could inline the pgRole in the config like this:

{ to: [pgRole("my_existing_role")] }

Because all it needs is the name

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good


export type PgPolicyAs = 'permissive' | 'restrictive';

export type PgPolicyConfig = {
as?: PgPolicyAs;
for?: PgPolicyFor;
to?: (AnyPgRole | PgPolicyTo)[];
using?: SQL;
withCheck?: SQL;
};

export const PolicyTable = Symbol.for('drizzle:PolicyTable');

export class PgPolicy<
TName extends string,
TTable extends AnyPgTable,
TConfig extends PgPolicyConfig | undefined,
> extends Policy<TName> {
static readonly [entityKind]: string = 'PgPolicy';

[PolicyTable]: TTable;
config: TConfig;

declare readonly _: {
readonly brand: 'PgPolicy';
readonly name: TName;
readonly table: TTable;
readonly config: TConfig;
};

constructor(name: TName, table: TTable, config: TConfig) {
super(name);
this[PolicyTable] = table;
this.config = config;
}
}

export function pgPolicy<TName extends string, TTable extends AnyPgTable, TConfig extends PgPolicyConfig>(
name: TName,
table: TTable,
config?: TConfig,
): PgPolicy<TName, TTable, TConfig> {
return new PgPolicy(name, table, config as TConfig);
}
46 changes: 46 additions & 0 deletions drizzle-orm/src/pg-core/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { entityKind } from '~/entity.ts';
import { Role } from '~/role.ts';

// Since the create role clause only allow one of these, I guess drizzle-kit will have to generate
// alter role clauses if the user defines more than one of these
export type PgRoleConfig = {
superuser?: boolean;
createDb?: boolean;
createRole?: boolean;
inherit?: boolean;
login?: boolean;
replication?: boolean;
bypassRLS?: boolean;
connectionLimit?: number;
password?: string;
validUntil?: Date;
inRole?: string;
role?: string[]; // Should this be a PgRole[]?
admin?: string[]; // Should this be a PgRole[]?
};

export type AnyPgRole = PgRole<string, PgRoleConfig>;

export class PgRole<TName extends string, TConfig extends PgRoleConfig> extends Role<TName> {
static readonly [entityKind]: string = 'PgRole';

declare readonly _: {
readonly brand: 'PgRole';
readonly name: TName;
readonly config: TConfig;
};

config: TConfig;

constructor(name: TName, config: TConfig) {
super(name);
this.config = config;
}
}

export function pgRole<TName extends string, TConfig extends PgRoleConfig>(
name: TName,
config?: TConfig,
): PgRole<TName, TConfig> {
return new PgRole(name, config ?? {} as TConfig);
}
30 changes: 28 additions & 2 deletions drizzle-orm/src/pg-core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { tracer } from '~/tracing.ts';
import { PgDatabase } from './db.ts';
import type { PgDialect } from './dialect.ts';
import type { SelectedFieldsOrdered } from './query-builders/select.types.ts';
import type { AnyPgRole } from './role.ts';

export interface PreparedQueryConfig {
execute: unknown;
Expand All @@ -29,6 +30,14 @@ export interface PgTransactionConfig {
isolationLevel?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable';
accessMode?: 'read only' | 'read write';
deferrable?: boolean;
rlsConfig?: {
Copy link

@rphlmr rphlmr Dec 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about (suggestion):

set?: {
		configs?: {
			name: string;
			value: string;
			isLocal?: boolean;
		}[];
		role?: AnyPgRole;
	};
  • set because postgres commands start with set or set_
  • configs because of set_config(...)

And I think it is not only related to rls.
We can imagine wanting to use this to set_config, which is later used in a PG function that we called in our transaction.

Like https://www.postgresql.org/docs/current/sql-syntax-calling-funcs.html

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, set_config(...) is not just for policies. I like set: { configs: ..., role: ... } or set_configs: ..., set_role: ....

https://www.postgresql.org/docs/16/functions-admin.html

set?: {
name: string;
value: string;
isLocal?: boolean;
}[];
role?: AnyPgRole;
};
}

export abstract class PgSession<
Expand Down Expand Up @@ -114,8 +123,25 @@ export abstract class PgTransaction<
return sql.raw(chunks.join(' '));
}

setTransaction(config: PgTransactionConfig): Promise<void> {
return this.session.execute(sql`set transaction ${this.getTransactionConfigSQL(config)}`);
setTransaction(config: PgTransactionConfig): Promise<void> | void {
if (config.accessMode || config.deferrable || config.isolationLevel) {
return this.session.execute(sql`set transaction ${this.getTransactionConfigSQL(config)}`);
}
}

async executeRLSConfig(config: PgTransactionConfig | undefined): Promise<void> {
const rlsConfig = config?.rlsConfig;
if (rlsConfig) {
if (rlsConfig.set) {
for (const { name, value, isLocal } of rlsConfig.set) {
await this.session.execute(sql`select set_config(${name}, ${value}, ${isLocal === false ? false : true})`);
}
}

if (rlsConfig.role) {
await this.session.execute(sql`set local role ${rlsConfig.role}`);
}
}
}

abstract override transaction<T>(
Expand Down
10 changes: 10 additions & 0 deletions drizzle-orm/src/pg-core/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export type TableConfig = TableConfigBase<PgColumn>;
/** @internal */
export const InlineForeignKeys = Symbol.for('drizzle:PgInlineForeignKeys');

/** @internal */
export const RLSEnabled = Symbol.for('drizzle:RLSEnabled');

export class PgTable<T extends TableConfig = TableConfig> extends Table<T> {
static readonly [entityKind]: string = 'PgTable';

Expand All @@ -33,9 +36,16 @@ export class PgTable<T extends TableConfig = TableConfig> extends Table<T> {
/**@internal */
[InlineForeignKeys]: ForeignKey[] = [];

[RLSEnabled]: boolean = false;

/** @internal */
override [Table.Symbol.ExtraConfigBuilder]: ((self: Record<string, PgColumn>) => PgTableExtraConfig) | undefined =
undefined;

enableRLS(): this {
this[RLSEnabled] = true;
return this;
}
}

export type AnyPgTable<TPartial extends Partial<TableConfig> = {}> = PgTable<UpdateTableConfig<TableConfig, TPartial>>;
Expand Down
24 changes: 24 additions & 0 deletions drizzle-orm/src/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { entityKind } from '~/entity.ts';
import { SQL, type SQLWrapper } from '~/sql/sql.ts';

export const PolicyName = Symbol.for('drizzle:PolicyName');

export class Policy<
TName extends string,
> implements SQLWrapper {
static readonly [entityKind]: string = 'Policy';

[PolicyName]: TName;

declare readonly _: {
readonly name: TName;
};

constructor(readonly name: TName) {
this[PolicyName] = name;
}

getSQL(): SQL {
return new SQL([this]);
}
}
3 changes: 3 additions & 0 deletions drizzle-orm/src/postgres-js/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ export class PostgresJsSession<
const tx = new PostgresJsTransaction(this.dialect, session, this.schema);
if (config) {
await tx.setTransaction(config);

await tx.executeRLSConfig(config);
}

return transaction(tx);
}) as Promise<T>;
}
Expand Down
24 changes: 24 additions & 0 deletions drizzle-orm/src/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { entityKind } from '~/entity.ts';
import { SQL, type SQLWrapper } from '~/sql/sql.ts';

export type AnyRole = Role<string>;

export const RoleName = Symbol.for('drizzle:RoleName');

export class Role<TName extends string> implements SQLWrapper {
static readonly [entityKind]: string = 'Role';

declare readonly _: {
readonly name: TName;
};

[RoleName]: TName;

constructor(readonly name: TName) {
this[RoleName] = name;
}

getSQL(): SQL {
return new SQL([this]);
}
}
8 changes: 7 additions & 1 deletion drizzle-orm/src/sql/sql.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { entityKind, is } from '~/entity.ts';
import type { SelectedFields } from '~/operations.ts';
import { Policy } from '~/policy.ts';
import { Relation } from '~/relations.ts';
import { Role } from '~/role.ts';
import { Subquery, SubqueryConfig } from '~/subquery.ts';
import { tracer } from '~/tracing.ts';
import { ViewBaseConfig } from '~/view-common.ts';
import type { AnyColumn } from '../column.ts';
import { Column } from '../column.ts';
import { Table } from '../table.ts';
import type { SelectedFields } from '~/operations.ts';

/**
* This class is used to indicate a primitive param value that is used in `sql` tag.
Expand Down Expand Up @@ -149,6 +151,10 @@ export class SQL<T = unknown> implements SQLWrapper {
return { sql: escapeName(chunk.value), params: [] };
}

if (is(chunk, Role) || is(chunk, Policy)) {
return { sql: escapeName(chunk.name), params: [] };
}

if (chunk === undefined) {
return { sql: '', params: [] };
}
Expand Down
2 changes: 2 additions & 0 deletions drizzle-orm/src/vercel-postgres/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export class VercelPgSession<
const tx = new VercelPgTransaction(this.dialect, session, this.schema);
await tx.execute(sql`begin${config ? sql` ${tx.getTransactionConfigSQL(config)}` : undefined}`);
try {
await tx.executeRLSConfig(config);

const result = await transaction(tx);
await tx.execute(sql`commit`);
return result;
Expand Down
Loading