-
-
Notifications
You must be signed in to change notification settings - Fork 721
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
Changes from all commits
d3ea325
095d126
c61d907
9e9396d
2aff955
c830361
622f896
f599f60
8f5907c
52678df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
|
||
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); | ||
} |
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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -29,6 +30,14 @@ export interface PgTransactionConfig { | |
isolationLevel?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'; | ||
accessMode?: 'read only' | 'read write'; | ||
deferrable?: boolean; | ||
rlsConfig?: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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;
};
And I think it is not only related to rls. Like https://www.postgresql.org/docs/current/sql-syntax-calling-funcs.html There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, |
||
set?: { | ||
name: string; | ||
value: string; | ||
isLocal?: boolean; | ||
}[]; | ||
role?: AnyPgRole; | ||
}; | ||
} | ||
|
||
export abstract class PgSession< | ||
|
@@ -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>( | ||
|
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]); | ||
} | ||
} |
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]); | ||
} | ||
} |
There was a problem hiding this comment.
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 & {})
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.existing()
like for views?There was a problem hiding this comment.
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:
Because all it needs is the name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good