@decohere/data-typeorm 0.0.1-dev.126 → 0.0.1-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -24
- package/src/bootstrap-policy.ts +84 -0
- package/src/bootstrap.ts +76 -0
- package/src/data-source.factory.ts +51 -0
- package/src/database-options.ts +10 -0
- package/src/dialect-map.ts +12 -0
- package/{es6/src/inject-data-source.js → src/inject-data-source.ts} +15 -9
- package/src/scripts/user.ts +234 -0
- package/src/server-manager.ts +300 -0
- package/src/sql-pretty.transport.ts +29 -0
- package/src/type-orm.module.ts +155 -0
- package/src/type-orm.provider.ts +37 -0
- package/src/type-orm.repository.ts +33 -0
- package/src/typeorm-scribe.logger.ts +55 -0
- package/src/typeorm.extensions.ts +42 -0
- package/src/unit-of-work.ts +52 -0
- package/tests/bootstrap-schemas.docker.spec.ts +145 -0
- package/tests/bootstrap.spec.ts +156 -0
- package/tests/db-least-privilege.pentest.spec.ts +125 -0
- package/tests/postgres-container.ts +139 -0
- package/tests/prepare-database.docker.spec.ts +96 -0
- package/tests/server-manager.bootstrap.spec.ts +72 -0
- package/tests/trusted-dev-postgres.spec.ts +212 -0
- package/tests/type-orm.module.spec.ts +123 -0
- package/tests/typeorm.lifecycle.spec.ts +97 -0
- package/tests/unit-of-work.spec.ts +54 -0
- package/tests/user-scripts.pentest.spec.ts +42 -0
- package/LICENSE +0 -15
- package/commonjs/src/bootstrap-policy.js +0 -69
- package/commonjs/src/bootstrap.js +0 -142
- package/commonjs/src/data-source.factory.js +0 -42
- package/commonjs/src/database-options.js +0 -4
- package/commonjs/src/dialect-map.js +0 -26
- package/commonjs/src/index.js +0 -24
- package/commonjs/src/inject-data-source.js +0 -33
- package/commonjs/src/scripts/user.js +0 -259
- package/commonjs/src/server-manager.js +0 -294
- package/commonjs/src/sql-pretty.transport.js +0 -35
- package/commonjs/src/type-orm.module.js +0 -115
- package/commonjs/src/type-orm.provider.js +0 -36
- package/commonjs/src/type-orm.repository.js +0 -30
- package/commonjs/src/typeorm-scribe.logger.js +0 -70
- package/commonjs/src/typeorm.extensions.js +0 -60
- package/commonjs/src/unit-of-work.js +0 -49
- package/es6/src/bootstrap-policy.js +0 -55
- package/es6/src/bootstrap.js +0 -139
- package/es6/src/data-source.factory.js +0 -27
- package/es6/src/database-options.js +0 -1
- package/es6/src/dialect-map.js +0 -8
- package/es6/src/scripts/user.js +0 -209
- package/es6/src/server-manager.js +0 -284
- package/es6/src/sql-pretty.transport.js +0 -20
- package/es6/src/type-orm.module.js +0 -105
- package/es6/src/type-orm.provider.js +0 -26
- package/es6/src/type-orm.repository.js +0 -20
- package/es6/src/typeorm-scribe.logger.js +0 -60
- package/es6/src/typeorm.extensions.js +0 -50
- package/es6/src/unit-of-work.js +0 -39
- package/types/src/bootstrap-policy.d.ts +0 -19
- package/types/src/bootstrap.d.ts +0 -16
- package/types/src/data-source.factory.d.ts +0 -17
- package/types/src/database-options.d.ts +0 -9
- package/types/src/dialect-map.d.ts +0 -8
- package/types/src/index.d.ts +0 -7
- package/types/src/inject-data-source.d.ts +0 -14
- package/types/src/scripts/user.d.ts +0 -56
- package/types/src/server-manager.d.ts +0 -39
- package/types/src/sql-pretty.transport.d.ts +0 -2
- package/types/src/type-orm.module.d.ts +0 -17
- package/types/src/type-orm.provider.d.ts +0 -14
- package/types/src/type-orm.repository.d.ts +0 -12
- package/types/src/typeorm-scribe.logger.d.ts +0 -9
- package/types/src/typeorm.extensions.d.ts +0 -11
- package/types/src/unit-of-work.d.ts +0 -20
- /package/{es6/src/index.js → src/index.ts} +0 -0
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "TypeOrmUnitOfWork", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return TypeOrmUnitOfWork;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
class TypeOrmUnitOfWork {
|
|
12
|
-
dataSource;
|
|
13
|
-
queryRunner;
|
|
14
|
-
constructor(dataSource){
|
|
15
|
-
this.dataSource = dataSource;
|
|
16
|
-
this.queryRunner = this.dataSource.createQueryRunner();
|
|
17
|
-
}
|
|
18
|
-
get entityManager() {
|
|
19
|
-
return this.queryRunner.manager;
|
|
20
|
-
}
|
|
21
|
-
getRepository(entity) {
|
|
22
|
-
return this.entityManager.getRepository(entity);
|
|
23
|
-
}
|
|
24
|
-
async beginTransaction() {
|
|
25
|
-
return this.queryRunner.startTransaction();
|
|
26
|
-
}
|
|
27
|
-
async commit() {
|
|
28
|
-
return this.queryRunner.commitTransaction();
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Hand the connection back to the pool, rolling back first if a transaction is still open
|
|
32
|
-
* (e.g. the request failed before commit). Otherwise the next user of that pooled
|
|
33
|
-
* connection would inherit the half-finished transaction.
|
|
34
|
-
*/ async [Symbol.asyncDispose]() {
|
|
35
|
-
if (this.queryRunner.isReleased) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
if (this.queryRunner.isTransactionActive) {
|
|
40
|
-
await this.queryRunner.rollbackTransaction();
|
|
41
|
-
}
|
|
42
|
-
} finally{
|
|
43
|
-
await this.queryRunner.release();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/** Prefer `await using` / async disposal; this starts the same cleanup without waiting. */ [Symbol.dispose]() {
|
|
47
|
-
void this[Symbol.asyncDispose]();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { DataConfigurationError } from "@decohere/data";
|
|
2
|
-
/** True when NODE_ENV says development. Only then may a data source declare `auth: trust`. */ export function isDevelopmentEnv(nodeEnv = process.env.NODE_ENV) {
|
|
3
|
-
const env = nodeEnv?.toLowerCase() ?? "";
|
|
4
|
-
return env === "development" || env === "dev";
|
|
5
|
-
}
|
|
6
|
-
function isTrusted(info) {
|
|
7
|
-
return info.auth === "trust";
|
|
8
|
-
}
|
|
9
|
-
function has(value) {
|
|
10
|
-
return typeof value === "string" && value.length > 0;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Check how a data source signs in, in every environment. A server that needs no password is
|
|
14
|
-
* declared with `auth: trust`, never inferred from a missing password: a password left out by
|
|
15
|
-
* mistake would otherwise look like a trusted server and fail later, less clearly.
|
|
16
|
-
*/ export function assertDataSourceAuth(info, nodeEnv = process.env.NODE_ENV) {
|
|
17
|
-
const auth = info.auth ?? "password";
|
|
18
|
-
if (auth !== "password" && auth !== "trust") {
|
|
19
|
-
throw new DataConfigurationError(`Data source "${info.name}": auth must be "password" or "trust" (got ${JSON.stringify(auth)})`);
|
|
20
|
-
}
|
|
21
|
-
if (auth === "trust") {
|
|
22
|
-
if (!isDevelopmentEnv(nodeEnv)) {
|
|
23
|
-
throw new DataConfigurationError(`Data source "${info.name}" sets auth: trust, which is allowed only in development (NODE_ENV is ${JSON.stringify(nodeEnv ?? "")}). A server that trusts every connection belongs on a developer's machine.`);
|
|
24
|
-
}
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (!has(info.password)) {
|
|
28
|
-
throw new DataConfigurationError(`Data source "${info.name}" has no password. Set password, or auth: trust for a local server that trusts every connection.`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Decide whether to bootstrap a data source (create its database, roles and schema).
|
|
33
|
-
*
|
|
34
|
-
* - `bootstrap: false` → never
|
|
35
|
-
* - `bootstrap: true` → yes (requires adminUser, and adminPassword unless `auth: trust`)
|
|
36
|
-
* - omitted → only in development, when those admin credentials are present
|
|
37
|
-
*/ export function shouldBootstrapDatabase(info, nodeEnv = process.env.NODE_ENV) {
|
|
38
|
-
if (info.bootstrap === false) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
const hasAdmin = has(info.adminUser) && (isTrusted(info) || has(info.adminPassword));
|
|
42
|
-
if (info.bootstrap === true) {
|
|
43
|
-
if (!hasAdmin) {
|
|
44
|
-
throw new DataConfigurationError(`Data source "${info.name}": bootstrap requires adminUser and adminPassword (adminPassword may be left out only with auth: trust)`);
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
return isDevelopmentEnv(nodeEnv) && hasAdmin;
|
|
49
|
-
}
|
|
50
|
-
/** A data source that will be bootstrapped must have every password the bootstrap needs. */ export function assertBootstrapCredentials(info) {
|
|
51
|
-
if (isTrusted(info)) return;
|
|
52
|
-
if (has(info.migrateUsername) && !has(info.migratePassword)) {
|
|
53
|
-
throw new DataConfigurationError(`Data source "${info.name}" has migrateUsername but no migratePassword. Set migratePassword, or auth: trust for a local server that trusts every connection.`);
|
|
54
|
-
}
|
|
55
|
-
}
|
package/es6/src/bootstrap.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
function _ts_add_disposable_resource(env, value, async) {
|
|
2
|
-
if (value !== null && value !== void 0) {
|
|
3
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
-
var dispose, inner;
|
|
5
|
-
if (async) {
|
|
6
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
-
dispose = value[Symbol.asyncDispose];
|
|
8
|
-
}
|
|
9
|
-
if (dispose === void 0) {
|
|
10
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
-
dispose = value[Symbol.dispose];
|
|
12
|
-
if (async) inner = dispose;
|
|
13
|
-
}
|
|
14
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
-
if (inner) dispose = function() {
|
|
16
|
-
try {
|
|
17
|
-
inner.call(this);
|
|
18
|
-
} catch (e) {
|
|
19
|
-
return Promise.reject(e);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
env.stack.push({
|
|
23
|
-
value: value,
|
|
24
|
-
dispose: dispose,
|
|
25
|
-
async: async
|
|
26
|
-
});
|
|
27
|
-
} else if (async) {
|
|
28
|
-
env.stack.push({
|
|
29
|
-
async: true
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
function _ts_dispose_resources(env) {
|
|
35
|
-
var _SuppressedError = typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
|
|
36
|
-
var e = new Error(message);
|
|
37
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
38
|
-
};
|
|
39
|
-
return (_ts_dispose_resources = function _ts_dispose_resources(env) {
|
|
40
|
-
function fail(e) {
|
|
41
|
-
env.error = env.hasError ? new _SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
42
|
-
env.hasError = true;
|
|
43
|
-
}
|
|
44
|
-
var r, s = 0;
|
|
45
|
-
function next() {
|
|
46
|
-
while(r = env.stack.pop()){
|
|
47
|
-
try {
|
|
48
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
49
|
-
if (r.dispose) {
|
|
50
|
-
var result = r.dispose.call(r.value);
|
|
51
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
|
|
52
|
-
fail(e);
|
|
53
|
-
return next();
|
|
54
|
-
});
|
|
55
|
-
} else s |= 1;
|
|
56
|
-
} catch (e) {
|
|
57
|
-
fail(e);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
61
|
-
if (env.hasError) throw env.error;
|
|
62
|
-
}
|
|
63
|
-
return next();
|
|
64
|
-
})(env);
|
|
65
|
-
}
|
|
66
|
-
import { DataConfigurationError } from "@decohere/data";
|
|
67
|
-
import { assertBootstrapCredentials, assertDataSourceAuth, shouldBootstrapDatabase } from "./bootstrap-policy.js";
|
|
68
|
-
import { PostgresServerManager } from "./server-manager.js";
|
|
69
|
-
/** What every data source on one server and database must agree on. */ const SHARED_FIELDS = [
|
|
70
|
-
"auth",
|
|
71
|
-
"adminUser",
|
|
72
|
-
"migrateUsername"
|
|
73
|
-
];
|
|
74
|
-
/**
|
|
75
|
-
* Check every data source's auth, then create the databases, roles and schemas of those that
|
|
76
|
-
* bootstrap. Data sources on the same server and database are bootstrapped once, one after the
|
|
77
|
-
* other: in parallel they would race to create the same database and roles. Running it again
|
|
78
|
-
* changes nothing.
|
|
79
|
-
*
|
|
80
|
-
* For runners and scripts that open their own `DataSource`; `withTypeOrm` calls it for you.
|
|
81
|
-
*/ export async function bootstrapDataSources(dataSources, options) {
|
|
82
|
-
const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV;
|
|
83
|
-
for (const info of dataSources)assertDataSourceAuth(info, nodeEnv);
|
|
84
|
-
const toBootstrap = dataSources.filter((info)=>shouldBootstrapDatabase(info, nodeEnv));
|
|
85
|
-
for (const info of toBootstrap)assertBootstrapCredentials(info);
|
|
86
|
-
for (const group of byServerAndDatabase(toBootstrap)){
|
|
87
|
-
const env = {
|
|
88
|
-
stack: [],
|
|
89
|
-
error: void 0,
|
|
90
|
-
hasError: false
|
|
91
|
-
};
|
|
92
|
-
try {
|
|
93
|
-
const [first] = group;
|
|
94
|
-
const server = _ts_add_disposable_resource(env, new PostgresServerManager(options.applicationName, first), true);
|
|
95
|
-
await server.connect();
|
|
96
|
-
await server.initializeDatabase(first.database);
|
|
97
|
-
for (const info of group){
|
|
98
|
-
await server.provisionRoles({
|
|
99
|
-
database: info.database,
|
|
100
|
-
appUsername: info.username,
|
|
101
|
-
appPassword: info.password,
|
|
102
|
-
migrateUsername: info.migrateUsername,
|
|
103
|
-
migratePassword: info.migratePassword,
|
|
104
|
-
allowAppDdl: info.synchronize === true,
|
|
105
|
-
schema: info.schema
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
} catch (e) {
|
|
109
|
-
env.error = e;
|
|
110
|
-
env.hasError = true;
|
|
111
|
-
} finally{
|
|
112
|
-
const result = _ts_dispose_resources(env);
|
|
113
|
-
if (result) await result;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function byServerAndDatabase(dataSources) {
|
|
118
|
-
const groups = new Map();
|
|
119
|
-
for (const info of dataSources){
|
|
120
|
-
const key = `${info.host}:${info.port}/${info.database}`;
|
|
121
|
-
const group = groups.get(key);
|
|
122
|
-
if (group == null) {
|
|
123
|
-
groups.set(key, [
|
|
124
|
-
info
|
|
125
|
-
]);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const [first] = group;
|
|
129
|
-
for (const field of SHARED_FIELDS){
|
|
130
|
-
if (info[field] !== first[field]) {
|
|
131
|
-
throw new DataConfigurationError(`Data sources "${first.name}" and "${info.name}" share database ${key} but set different ${field} (${JSON.stringify(first[field])} and ${JSON.stringify(info[field])})`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
group.push(info);
|
|
135
|
-
}
|
|
136
|
-
return [
|
|
137
|
-
...groups.values()
|
|
138
|
-
];
|
|
139
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { DataSource } from "typeorm";
|
|
2
|
-
import { ProviderMap } from "./dialect-map.js";
|
|
3
|
-
/** Fields used only for bootstrap / privilege provisioning — not TypeORM connection options. */ function omitProvisionFields(options) {
|
|
4
|
-
const { adminUser: _adminUser, adminPassword: _adminPassword, migrateUsername: _migrateUsername, migratePassword: _migratePassword, bootstrap: _bootstrap, auth: _auth, seed: _seed, dialect: _dialect, name: _name, ...rest } = options;
|
|
5
|
-
return rest;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* A DataSource the container can close: TypeORM's has `destroy()` but no `Symbol.asyncDispose`,
|
|
9
|
-
* so disposing the app's container used to leave its connections open.
|
|
10
|
-
*/ export class ManagedDataSource extends DataSource {
|
|
11
|
-
async [Symbol.asyncDispose]() {
|
|
12
|
-
if (this.isInitialized) await this.destroy();
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export class DataSourceFactory {
|
|
16
|
-
static create(options, logger) {
|
|
17
|
-
const { applicationName } = options;
|
|
18
|
-
const { provider } = options;
|
|
19
|
-
const type = ProviderMap[provider];
|
|
20
|
-
return new ManagedDataSource({
|
|
21
|
-
type,
|
|
22
|
-
logger,
|
|
23
|
-
applicationName,
|
|
24
|
-
...omitProvisionFields(options)
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/es6/src/dialect-map.js
DELETED
package/es6/src/scripts/user.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/** Postgres identifier: letters, digits, underscore; must start with letter or underscore. */ const SAFE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
-
export function assertSafeSqlIdent(value, label) {
|
|
3
|
-
if (typeof value !== "string" || value.length === 0 || !SAFE_IDENT.test(value)) {
|
|
4
|
-
throw new Error(`Invalid ${label}: must match ${SAFE_IDENT} (got ${JSON.stringify(value)})`);
|
|
5
|
-
}
|
|
6
|
-
return value;
|
|
7
|
-
}
|
|
8
|
-
/** Double-quote a validated identifier for use in DDL. */ export function quoteIdent(value, label = "identifier") {
|
|
9
|
-
const safe = assertSafeSqlIdent(value, label);
|
|
10
|
-
return `"${safe.replaceAll('"', '""')}"`;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Single-quote a string literal for DDL that cannot take bind parameters
|
|
14
|
-
* (e.g. CREATE USER … PASSWORD — Postgres rejects PASSWORD $1).
|
|
15
|
-
*/ export function quoteLiteral(value) {
|
|
16
|
-
return `'${value.replaceAll("'", "''")}'`;
|
|
17
|
-
}
|
|
18
|
-
/** Existence check — username bound as $1 (never concatenated). */ export function roleExistsQuery(username) {
|
|
19
|
-
assertSafeSqlIdent(username, "username");
|
|
20
|
-
return {
|
|
21
|
-
text: `SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = $1`,
|
|
22
|
-
values: [
|
|
23
|
-
username
|
|
24
|
-
]
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* CREATE USER with password as an escaped SQL literal.
|
|
29
|
-
* Username is validated + quoted as an identifier (cannot be parameterized in DDL).
|
|
30
|
-
* Password cannot use bind params in CREATE USER … PASSWORD (Postgres syntax error).
|
|
31
|
-
*
|
|
32
|
-
* `password` omitted (undefined) creates a role with no password, for a local dev server that
|
|
33
|
-
* trusts every connection. An empty string is still an error: that is a typo, not a choice.
|
|
34
|
-
* Callers decide when no password is allowed (see PostgresServerManager).
|
|
35
|
-
*/ export function createUserQuery(username, password) {
|
|
36
|
-
const userIdent = quoteIdent(username, "username");
|
|
37
|
-
const attributes = "NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE";
|
|
38
|
-
if (password === undefined) {
|
|
39
|
-
return {
|
|
40
|
-
text: `CREATE USER ${userIdent} WITH LOGIN ${attributes}`,
|
|
41
|
-
values: []
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (typeof password !== "string" || password.length === 0) {
|
|
45
|
-
throw new Error("Invalid password: must be a non-empty string");
|
|
46
|
-
}
|
|
47
|
-
const passwordLiteral = quoteLiteral(password);
|
|
48
|
-
return {
|
|
49
|
-
text: `CREATE USER ${userIdent} WITH LOGIN PASSWORD ${passwordLiteral} ${attributes}`,
|
|
50
|
-
values: []
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/** CONNECT only — production runtime app role. */ export function grantConnectQuery(username, database) {
|
|
54
|
-
const userIdent = quoteIdent(username, "username");
|
|
55
|
-
const dbIdent = quoteIdent(database, "database");
|
|
56
|
-
return {
|
|
57
|
-
text: `GRANT CONNECT ON DATABASE ${dbIdent} TO ${userIdent}`,
|
|
58
|
-
values: []
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
export function grantSchemaUsageQuery(username, schema = "public") {
|
|
62
|
-
const userIdent = quoteIdent(username, "username");
|
|
63
|
-
const schemaIdent = quoteIdent(schema, "schema");
|
|
64
|
-
return {
|
|
65
|
-
text: `GRANT USAGE ON SCHEMA ${schemaIdent} TO ${userIdent}`,
|
|
66
|
-
values: []
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/** Needed for TypeORM synchronize (dev only). */ export function grantSchemaCreateQuery(username, schema = "public") {
|
|
70
|
-
const userIdent = quoteIdent(username, "username");
|
|
71
|
-
const schemaIdent = quoteIdent(schema, "schema");
|
|
72
|
-
return {
|
|
73
|
-
text: `GRANT CREATE ON SCHEMA ${schemaIdent} TO ${userIdent}`,
|
|
74
|
-
values: []
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
/** The schema, owned by `owner`. Unchanged when it already exists. */ export function createSchemaQuery(schema, owner) {
|
|
78
|
-
const schemaIdent = quoteIdent(schema, "schema");
|
|
79
|
-
const ownerIdent = quoteIdent(owner, "owner");
|
|
80
|
-
return {
|
|
81
|
-
text: `CREATE SCHEMA IF NOT EXISTS ${schemaIdent} AUTHORIZATION ${ownerIdent}`,
|
|
82
|
-
values: []
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Whether a role already holds a privilege. Postgres checks that the grantor may grant even when
|
|
87
|
-
* the grant would change nothing, so bootstrap asks first and grants only what is missing.
|
|
88
|
-
*/ export function hasDatabasePrivilegeQuery(username, database, privilege) {
|
|
89
|
-
assertSafeSqlIdent(username, "username");
|
|
90
|
-
assertSafeSqlIdent(database, "database");
|
|
91
|
-
return {
|
|
92
|
-
text: `SELECT has_database_privilege($1, $2, $3) AS granted`,
|
|
93
|
-
values: [
|
|
94
|
-
username,
|
|
95
|
-
database,
|
|
96
|
-
privilege
|
|
97
|
-
]
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
export function hasSchemaPrivilegeQuery(username, schema, privilege) {
|
|
101
|
-
assertSafeSqlIdent(username, "username");
|
|
102
|
-
assertSafeSqlIdent(schema, "schema");
|
|
103
|
-
return {
|
|
104
|
-
text: `SELECT has_schema_privilege($1, $2, $3) AS granted`,
|
|
105
|
-
values: [
|
|
106
|
-
username,
|
|
107
|
-
schema,
|
|
108
|
-
privilege
|
|
109
|
-
]
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
/** Whether `member` already has the privileges of `role` (inherited, or as a superuser). */ export function hasRolePrivilegesQuery(member, role) {
|
|
113
|
-
assertSafeSqlIdent(member, "member");
|
|
114
|
-
assertSafeSqlIdent(role, "role");
|
|
115
|
-
return {
|
|
116
|
-
text: `SELECT pg_has_role($1, $2, 'USAGE') AS granted`,
|
|
117
|
-
values: [
|
|
118
|
-
member,
|
|
119
|
-
role
|
|
120
|
-
]
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
export function databaseOwnerQuery(database) {
|
|
124
|
-
assertSafeSqlIdent(database, "database");
|
|
125
|
-
return {
|
|
126
|
-
text: `SELECT pg_get_userbyid(datdba) AS owner FROM pg_catalog.pg_database WHERE datname = $1`,
|
|
127
|
-
values: [
|
|
128
|
-
database
|
|
129
|
-
]
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
export function grantSchemaPrivilegesQuery(username, schema = "public") {
|
|
133
|
-
const userIdent = quoteIdent(username, "username");
|
|
134
|
-
const schemaIdent = quoteIdent(schema, "schema");
|
|
135
|
-
return {
|
|
136
|
-
text: `GRANT ALL ON SCHEMA ${schemaIdent} TO ${userIdent}`,
|
|
137
|
-
values: []
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
export function alterDatabaseOwnerQuery(database, username) {
|
|
141
|
-
const dbIdent = quoteIdent(database, "database");
|
|
142
|
-
const userIdent = quoteIdent(username, "username");
|
|
143
|
-
return {
|
|
144
|
-
text: `ALTER DATABASE ${dbIdent} OWNER TO ${userIdent}`,
|
|
145
|
-
values: []
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Postgres requires the session user to be a member of the target role to
|
|
150
|
-
* `ALTER DATABASE … OWNER TO` / `ALTER DEFAULT PRIVILEGES FOR ROLE`.
|
|
151
|
-
*/ export function grantRoleMembershipQuery(member, role) {
|
|
152
|
-
const memberIdent = quoteIdent(member, "member");
|
|
153
|
-
const roleIdent = quoteIdent(role, "role");
|
|
154
|
-
return {
|
|
155
|
-
text: `GRANT ${roleIdent} TO ${memberIdent}`,
|
|
156
|
-
values: []
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
export function revokeRoleMembershipQuery(member, role) {
|
|
160
|
-
const memberIdent = quoteIdent(member, "member");
|
|
161
|
-
const roleIdent = quoteIdent(role, "role");
|
|
162
|
-
return {
|
|
163
|
-
text: `REVOKE ${roleIdent} FROM ${memberIdent}`,
|
|
164
|
-
values: []
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Future tables/sequences created by the migration role are usable by the app role (DML only).
|
|
169
|
-
*/ export function defaultPrivilegesForAppQueries(migrateUsername, appUsername, schema = "public") {
|
|
170
|
-
const migrateIdent = quoteIdent(migrateUsername, "migrateUsername");
|
|
171
|
-
const appIdent = quoteIdent(appUsername, "username");
|
|
172
|
-
const schemaIdent = quoteIdent(schema, "schema");
|
|
173
|
-
return [
|
|
174
|
-
{
|
|
175
|
-
text: `ALTER DEFAULT PRIVILEGES FOR ROLE ${migrateIdent} IN SCHEMA ${schemaIdent} GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${appIdent}`,
|
|
176
|
-
values: []
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
text: `ALTER DEFAULT PRIVILEGES FOR ROLE ${migrateIdent} IN SCHEMA ${schemaIdent} GRANT USAGE, SELECT ON SEQUENCES TO ${appIdent}`,
|
|
180
|
-
values: []
|
|
181
|
-
}
|
|
182
|
-
];
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* @deprecated Prefer discrete query helpers + PostgresServerManager.provisionRoles.
|
|
186
|
-
* Kept for call-site / pen-test migration: emits create + CONNECT + USAGE (not ALL PRIVILEGES).
|
|
187
|
-
*/ const createOwnerUser = (username, password, database)=>{
|
|
188
|
-
quoteIdent(username, "username");
|
|
189
|
-
quoteIdent(database, "database");
|
|
190
|
-
if (typeof password !== "string" || password.length === 0) {
|
|
191
|
-
throw new Error("Invalid password: must be a non-empty string");
|
|
192
|
-
}
|
|
193
|
-
const create = createUserQuery(username, password);
|
|
194
|
-
const connect = grantConnectQuery(username, database);
|
|
195
|
-
const usage = grantSchemaUsageQuery(username);
|
|
196
|
-
return {
|
|
197
|
-
text: [
|
|
198
|
-
create.text,
|
|
199
|
-
connect.text,
|
|
200
|
-
usage.text
|
|
201
|
-
].join(";\n"),
|
|
202
|
-
values: []
|
|
203
|
-
};
|
|
204
|
-
};
|
|
205
|
-
const userExists = (username)=>roleExistsQuery(username);
|
|
206
|
-
export const userScripts = {
|
|
207
|
-
createOwnerUser,
|
|
208
|
-
userExists
|
|
209
|
-
};
|