@homeflare/alchemy 0.44.0 → 0.46.0
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/dist/postgres/fake-role-sql.d.ts +2 -1
- package/dist/postgres/fake-role-sql.d.ts.map +1 -1
- package/dist/postgres/fake-sql.d.ts +3 -3
- package/dist/postgres/fake-sql.d.ts.map +1 -1
- package/dist/postgres/index.d.ts +1 -1
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +117 -94
- package/dist/postgres/index.js.map +11 -11
- package/dist/postgres/role-attrs.d.ts +7 -4
- package/dist/postgres/role-attrs.d.ts.map +1 -1
- package/dist/postgres/role-diff.d.ts +2 -2
- package/dist/postgres/role-diff.d.ts.map +1 -1
- package/dist/postgres/role-errors.d.ts +13 -7
- package/dist/postgres/role-errors.d.ts.map +1 -1
- package/dist/postgres/role-membership-sql.d.ts +30 -27
- package/dist/postgres/role-membership-sql.d.ts.map +1 -1
- package/dist/postgres/role-provider.d.ts.map +1 -1
- package/dist/postgres/role-scram.d.ts +2 -0
- package/dist/postgres/role-scram.d.ts.map +1 -1
- package/dist/postgres/role-secrets.d.ts +8 -0
- package/dist/postgres/role-secrets.d.ts.map +1 -1
- package/dist/postgres/role-sql.d.ts.map +1 -1
- package/dist/postgres/role.d.ts +2 -2
- package/dist/postgres/role.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
- package/dist/proxmox/index.d.ts +1 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +283 -130
- package/dist/proxmox/index.js.map +31 -27
- package/dist/proxmox/qemu-dns.d.ts +2 -0
- package/dist/proxmox/qemu-dns.d.ts.map +1 -0
- package/dist/proxmox/qemu-errors.d.ts +1 -0
- package/dist/proxmox/qemu-errors.d.ts.map +1 -1
- package/dist/proxmox/qemu-form.d.ts.map +1 -1
- package/dist/proxmox/qemu-judge.d.ts.map +1 -1
- package/dist/proxmox/qemu-lifecycle.d.ts +15 -10
- package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu-props.d.ts +10 -0
- package/dist/proxmox/qemu-props.d.ts.map +1 -1
- package/dist/proxmox/qemu-resize.d.ts +10 -0
- package/dist/proxmox/qemu-resize.d.ts.map +1 -0
- package/dist/proxmox/qemu-size.d.ts +20 -0
- package/dist/proxmox/qemu-size.d.ts.map +1 -0
- package/dist/proxmox/qemu-spec.d.ts +25 -0
- package/dist/proxmox/qemu-spec.d.ts.map +1 -0
- package/dist/verify/fake-engine.d.ts +4 -2
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/postgres-role.md +18 -6
- package/docs/proxmox-qemu-resize.md +62 -0
- package/package.json +1 -1
- package/src/ceph/ceph-argv.test.ts +1 -1
- package/src/launchd/sudo-allowlist.test.ts +2 -2
- package/src/linux/sudo-allowlist.test.ts +3 -3
- package/src/linux/sudo-runner.test.ts +2 -2
- package/src/postgres/fake-role-sql.ts +26 -6
- package/src/postgres/fake-sql.ts +10 -5
- package/src/postgres/index.ts +1 -0
- package/src/postgres/role-attrs.ts +7 -4
- package/src/postgres/role-diff.ts +7 -3
- package/src/postgres/role-errors.ts +22 -14
- package/src/postgres/role-guard.test.ts +3 -3
- package/src/postgres/role-identity.test.ts +14 -2
- package/src/postgres/role-lifecycle.test.ts +2 -2
- package/src/postgres/role-membership-options.test.ts +73 -0
- package/src/postgres/role-membership-repair.test.ts +153 -0
- package/src/postgres/role-membership-sql.ts +88 -63
- package/src/postgres/role-password-ascii.test.ts +53 -0
- package/src/postgres/role-password-transports.test.ts +123 -0
- package/src/postgres/role-privilege.test.ts +37 -10
- package/src/postgres/role-provenance.test.ts +1 -1
- package/src/postgres/role-provider.ts +3 -2
- package/src/postgres/role-refusals.test.ts +0 -25
- package/src/postgres/role-scram.ts +2 -0
- package/src/postgres/role-secrets.ts +15 -0
- package/src/postgres/role-sql.ts +3 -1
- package/src/postgres/role.ts +9 -1
- package/src/postgres/schema-socket.test.ts +1 -0
- package/src/proxmox/generated/constraints/index.ts +1 -1
- package/src/proxmox/generated/constraints/pve-access.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +6 -1
- package/src/proxmox/generated/constraints/pve-nodes-storage.ts +1 -1
- package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
- package/src/proxmox/index.ts +1 -0
- package/src/proxmox/qemu-dns.test.ts +73 -0
- package/src/proxmox/qemu-dns.ts +32 -0
- package/src/proxmox/qemu-errors.ts +1 -0
- package/src/proxmox/qemu-form.ts +6 -3
- package/src/proxmox/qemu-judge.ts +2 -1
- package/src/proxmox/qemu-lifecycle.ts +28 -47
- package/src/proxmox/qemu-props.ts +14 -2
- package/src/proxmox/qemu-resize-lock.test.ts +121 -0
- package/src/proxmox/qemu-resize-recovery.test.ts +81 -0
- package/src/proxmox/qemu-resize.test.ts +199 -0
- package/src/proxmox/qemu-resize.ts +86 -0
- package/src/proxmox/qemu-size.test.ts +64 -0
- package/src/proxmox/qemu-size.ts +95 -0
- package/src/proxmox/qemu-spec.ts +44 -0
- package/src/proxmox/qemu-volume.ts +5 -5
- package/src/verify/fake-engine.ts +6 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import { makeFakeSql } from './fake-sql.ts';
|
|
4
|
+
import { diffPostgresRole } from './role-diff.ts';
|
|
5
|
+
import { reconcileWithClient } from './role.ts';
|
|
6
|
+
import { seal } from '../secrets/write-only.ts';
|
|
7
|
+
import type { PostgresRoleAttributes } from './role-attrs.ts';
|
|
8
|
+
|
|
9
|
+
const props = {
|
|
10
|
+
name: 'ascii-seat',
|
|
11
|
+
login: true,
|
|
12
|
+
inherit: true,
|
|
13
|
+
connectionLimit: -1,
|
|
14
|
+
password: { fromEnv: 'TEST_PASSWORD' },
|
|
15
|
+
};
|
|
16
|
+
const live: PostgresRoleAttributes = {
|
|
17
|
+
...props,
|
|
18
|
+
oid: 42,
|
|
19
|
+
validUntil: null,
|
|
20
|
+
memberOf: [],
|
|
21
|
+
passwordSeal: '',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Synthetic values cover SASLprep mapping, normalization and invalid Unicode; never credentials.
|
|
25
|
+
test.each(['soft\u00adhyphen', 'caf\u00e9', '\u2168', '\ud800'])(
|
|
26
|
+
'non-ASCII case %# refuses create, adopt, sealed reconcile and diff without exposing input',
|
|
27
|
+
async (password) => {
|
|
28
|
+
const env = { TEST_PASSWORD: password };
|
|
29
|
+
const passwordSeal = seal({ password }, 'test-salt');
|
|
30
|
+
for (const exists of [false, true]) {
|
|
31
|
+
const fake = makeFakeSql({ roleRows: exists ? [live] : [] });
|
|
32
|
+
for (const previous of ['', passwordSeal]) {
|
|
33
|
+
const error = await Effect.runPromise(
|
|
34
|
+
reconcileWithClient(fake, props, env, previous).pipe(
|
|
35
|
+
Effect.catchTag('PostgresRolePasswordNonAsciiRefused', Effect.succeed),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
expect(error).toMatchObject({
|
|
39
|
+
_tag: 'PostgresRolePasswordNonAsciiRefused',
|
|
40
|
+
role: props.name,
|
|
41
|
+
});
|
|
42
|
+
expect(JSON.stringify(error)).not.toContain(password);
|
|
43
|
+
expect(fake.statements).toEqual([]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const error = await Effect.runPromise(
|
|
47
|
+
diffPostgresRole(props, { ...live, passwordSeal }, env).pipe(
|
|
48
|
+
Effect.catchTag('PostgresRolePasswordNonAsciiRefused', Effect.succeed),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
expect(error).toHaveProperty('_tag', 'PostgresRolePasswordNonAsciiRefused');
|
|
52
|
+
},
|
|
53
|
+
);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Capture the actual Effect SQL spans and psql stdin, not just the builder's output. */
|
|
2
|
+
import { expect, test } from 'bun:test';
|
|
3
|
+
import * as PgClient from '@effect/sql-pg/PgClient';
|
|
4
|
+
import * as Effect from 'effect/Effect';
|
|
5
|
+
import * as Stream from 'effect/Stream';
|
|
6
|
+
import * as Tracer from 'effect/Tracer';
|
|
7
|
+
import * as Statement from 'effect/unstable/sql/Statement';
|
|
8
|
+
import type { Connection } from 'effect/unstable/sql/SqlConnection';
|
|
9
|
+
import type { PgExecutor } from './database-sql.ts';
|
|
10
|
+
import { makeFakeSql } from './fake-sql.ts';
|
|
11
|
+
import { type PsqlRunner, makePsqlExecutor } from './psql-executor.ts';
|
|
12
|
+
import { reconcileWithClient } from './role.ts';
|
|
13
|
+
|
|
14
|
+
const props = {
|
|
15
|
+
name: 'transport-seat',
|
|
16
|
+
login: true,
|
|
17
|
+
inherit: true,
|
|
18
|
+
connectionLimit: -1,
|
|
19
|
+
password: { fromEnv: 'TEST_PASSWORD' },
|
|
20
|
+
};
|
|
21
|
+
// Synthetic test input, deliberately recognizable if it escapes into captured telemetry.
|
|
22
|
+
const password = 'kit336-plaintext-canary:must-never-reach-transport!';
|
|
23
|
+
const env = { TEST_PASSWORD: password };
|
|
24
|
+
const live = { ...props, oid: 42, validUntil: null, memberOf: [], passwordSeal: '' };
|
|
25
|
+
|
|
26
|
+
test('SQL tracer captures only a verifier on create, rotate and failed ALTER', async () => {
|
|
27
|
+
for (const exists of [false, true]) {
|
|
28
|
+
for (const fail of [false, true]) {
|
|
29
|
+
const fake = makeFakeSql({
|
|
30
|
+
roleRows: exists ? [live] : [],
|
|
31
|
+
...(fail ? { failNext: 'ALTER ROLE' } : {}),
|
|
32
|
+
});
|
|
33
|
+
const spans: Tracer.Span[] = [];
|
|
34
|
+
const tracer = Tracer.make({
|
|
35
|
+
span: (options) => {
|
|
36
|
+
const span = Tracer.nativeTracer.span(options);
|
|
37
|
+
spans.push(span);
|
|
38
|
+
return span;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const execute: Connection['execute'] = (sql, params) => fake.unsafe(sql, params);
|
|
42
|
+
const connection: Connection = {
|
|
43
|
+
execute,
|
|
44
|
+
executeUnprepared: execute,
|
|
45
|
+
executeRaw: (sql, params) => fake.unsafe(sql, params),
|
|
46
|
+
executeValues: () => Effect.succeed([]),
|
|
47
|
+
executeValuesUnprepared: () => Effect.succeed([]),
|
|
48
|
+
executeStream: () => Stream.empty,
|
|
49
|
+
};
|
|
50
|
+
// Use the installed SQL Statement and PG compiler: db.query.text is set by Effect,
|
|
51
|
+
// while only the connection beneath it is fake. A hand-written span would prove nothing.
|
|
52
|
+
const sql = Statement.make(
|
|
53
|
+
Effect.succeed(connection),
|
|
54
|
+
PgClient.makeCompiler(),
|
|
55
|
+
[],
|
|
56
|
+
undefined,
|
|
57
|
+
);
|
|
58
|
+
const pg: PgExecutor = {
|
|
59
|
+
unsafe: (text, params) => sql.unsafe(text, params),
|
|
60
|
+
transaction: (texts) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
for (const text of texts) yield* sql.unsafe(text);
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
const result = await Effect.runPromise(
|
|
66
|
+
Effect.exit(reconcileWithClient(pg, props, env, exists ? 'stale-seal' : '')).pipe(
|
|
67
|
+
Effect.provideService(Tracer.Tracer, tracer),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
expect(result._tag).toBe(fail ? 'Failure' : 'Success');
|
|
71
|
+
const attributes = spans.map((span) => Object.fromEntries(span.attributes));
|
|
72
|
+
const passwordQueries = attributes
|
|
73
|
+
.map((a) => a['db.query.text'])
|
|
74
|
+
.filter((text): text is string => typeof text === 'string' && text.includes('PASSWORD'));
|
|
75
|
+
expect(passwordQueries).toHaveLength(1);
|
|
76
|
+
expect(passwordQueries[0]).toContain("PASSWORD E'SCRAM-SHA-256$");
|
|
77
|
+
expect(JSON.stringify({ attributes, result })).not.toContain(password);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('psql runner receives only a verifier; echoed failure and state hold no plain value', async () => {
|
|
83
|
+
for (const exists of [false, true]) {
|
|
84
|
+
for (const fail of [false, true]) {
|
|
85
|
+
const calls: Parameters<PsqlRunner>[0][] = [];
|
|
86
|
+
let created = exists;
|
|
87
|
+
const run: PsqlRunner = (call) => {
|
|
88
|
+
calls.push(call);
|
|
89
|
+
if (call.stdin.includes('PASSWORD') && fail) {
|
|
90
|
+
return Promise.resolve({
|
|
91
|
+
code: 3,
|
|
92
|
+
stdout: '',
|
|
93
|
+
stderr: `ERROR: 42501: refused\nSTATEMENT: ${call.stdin}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (call.stdin.startsWith('BEGIN')) created = true;
|
|
97
|
+
return Promise.resolve({
|
|
98
|
+
code: 0,
|
|
99
|
+
stderr: '',
|
|
100
|
+
stdout: call.stdin.includes('FROM pg_roles r')
|
|
101
|
+
? JSON.stringify(created ? [live] : [])
|
|
102
|
+
: '[]',
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const result = await Effect.runPromise(
|
|
106
|
+
Effect.exit(
|
|
107
|
+
reconcileWithClient(
|
|
108
|
+
makePsqlExecutor(run, { database: 'postgres', username: 'postgres' }),
|
|
109
|
+
props,
|
|
110
|
+
env,
|
|
111
|
+
exists ? 'stale-seal' : '',
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
expect(result._tag).toBe(fail ? 'Failure' : 'Success');
|
|
116
|
+
const writes = calls.filter((call) => call.stdin.includes('PASSWORD'));
|
|
117
|
+
expect(writes).toHaveLength(1);
|
|
118
|
+
expect(writes[0]?.stdin).toContain("PASSWORD E'SCRAM-SHA-256$");
|
|
119
|
+
expect(JSON.stringify({ calls, result })).not.toContain(password);
|
|
120
|
+
expect(JSON.stringify(result)).not.toContain('SCRAM-SHA-256$');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -11,6 +11,7 @@ import * as Effect from 'effect/Effect';
|
|
|
11
11
|
import { makeFakeSql } from './fake-sql.ts';
|
|
12
12
|
import { PostgresRolePrivilegedRefused } from './role-errors.ts';
|
|
13
13
|
import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
|
|
14
|
+
import { diffPostgresRole } from './role-diff.ts';
|
|
14
15
|
import { reconcileWithClient } from './role.ts';
|
|
15
16
|
|
|
16
17
|
const run = <A, E>(eff: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(eff);
|
|
@@ -57,24 +58,50 @@ describe('adopted privilege flags', () => {
|
|
|
57
58
|
fake.membershipOptions.set('seat-widget\0hf_agent\0postgres', { admin: true, set: true });
|
|
58
59
|
await run(reconcileWithClient(fake, baseProps));
|
|
59
60
|
const texts = fake.statements.map((s) => s.text);
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Option revocation keeps the membership, while RESTRICT protects dependent grants.
|
|
62
|
+
expect(texts).toContain(
|
|
63
|
+
'REVOKE ADMIN OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "postgres" RESTRICT',
|
|
64
|
+
);
|
|
65
|
+
expect(texts).toContain(
|
|
66
|
+
'REVOKE SET OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "postgres" RESTRICT',
|
|
67
|
+
);
|
|
68
|
+
expect(texts.some((text) => text.startsWith('GRANT'))).toBe(false);
|
|
64
69
|
expect(fake.memberships.has('seat-widget\0hf_agent\0postgres')).toBe(true);
|
|
65
70
|
expect(fake.membershipOptions.has('seat-widget\0hf_agent\0postgres')).toBe(false);
|
|
66
71
|
});
|
|
67
72
|
|
|
68
|
-
test("another grantor's
|
|
73
|
+
test("another grantor's options are revoked while its membership remains", async () => {
|
|
69
74
|
const fake = makeFakeSql({ roleRows: [liveRole()], roles: ['hf_agent'] });
|
|
70
75
|
fake.memberships.add('seat-widget\0hf_agent\0other');
|
|
71
76
|
fake.membershipOptions.set('seat-widget\0hf_agent\0other', { admin: true, set: true });
|
|
72
77
|
await run(reconcileWithClient(fake, baseProps));
|
|
73
78
|
const texts = fake.statements.map((s) => s.text);
|
|
74
|
-
expect(texts).toContain(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
-
|
|
79
|
+
expect(texts).toContain(
|
|
80
|
+
'REVOKE ADMIN OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "other" RESTRICT',
|
|
81
|
+
);
|
|
82
|
+
expect(texts).toContain(
|
|
83
|
+
'REVOKE SET OPTION FOR "hf_agent" FROM "seat-widget" GRANTED BY "other" RESTRICT',
|
|
84
|
+
);
|
|
85
|
+
expect(fake.memberships.has('seat-widget\0hf_agent\0other')).toBe(true);
|
|
86
|
+
expect(fake.memberships.has('seat-widget\0hf_agent\0postgres')).toBe(false);
|
|
87
|
+
expect(fake.membershipOptions.has('seat-widget\0hf_agent\0other')).toBe(false);
|
|
79
88
|
});
|
|
80
89
|
});
|
|
90
|
+
|
|
91
|
+
test.each(['superuser', 'createrole', 'createdb', 'replication', 'bypassrls'] as const)(
|
|
92
|
+
'live %s plans update and reconcile refuses before writing',
|
|
93
|
+
async (flag) => {
|
|
94
|
+
const live = liveRole({ [flag]: true });
|
|
95
|
+
expect(await run(diffPostgresRole(baseProps, liveRole(), {}, { found: live }))).toEqual({
|
|
96
|
+
action: 'update',
|
|
97
|
+
});
|
|
98
|
+
const fake = makeFakeSql({ roleRows: [live] });
|
|
99
|
+
const refused = await run(
|
|
100
|
+
reconcileWithClient(fake, baseProps, {}).pipe(
|
|
101
|
+
Effect.catchTag('PostgresRolePrivilegedRefused', (error) => Effect.succeed(error.flags)),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
expect(refused).toContain(flag.toUpperCase());
|
|
105
|
+
expect(fake.statements.every((s) => s.text.trimStart().startsWith('SELECT'))).toBe(true);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
@@ -94,7 +94,7 @@ const EXCLUDED: Readonly<Record<string, string>> = {
|
|
|
94
94
|
'the create-time membership shorthand; this family writes memberships with one explicit GRANT per parent instead, so the re-read stays authoritative',
|
|
95
95
|
ROLE: 'grants the listed roles membership in the NEW role — the inverse of memberOf; membership is declared in one direction only',
|
|
96
96
|
ADMIN:
|
|
97
|
-
'grants membership with ADMIN OPTION; a live
|
|
97
|
+
'grants membership with ADMIN OPTION; a live admin_option is revoked by grantor with RESTRICT, preserving membership and refusing dependent grants, and the seat GRANT says SET FALSE',
|
|
98
98
|
SYSID:
|
|
99
99
|
'ignored by the server since PostgreSQL 8.1 (the docs keep it only for compatibility); this family never assigns oids',
|
|
100
100
|
};
|
|
@@ -14,7 +14,7 @@ import { PostgresRole } from './role.ts';
|
|
|
14
14
|
import { readRole, reconcileWithClient, refuseAtPlan } from './role.ts';
|
|
15
15
|
import { diffPostgresRole } from './role-diff.ts';
|
|
16
16
|
import { PostgresRoleIdentityRefused } from './role-errors.ts';
|
|
17
|
-
import { buildDropRoleSql, declarationMatches } from './role-sql.ts';
|
|
17
|
+
import { buildDropRoleSql, declarationMatches, storedAttributes } from './role-sql.ts';
|
|
18
18
|
import { withPg } from './connection.ts';
|
|
19
19
|
|
|
20
20
|
export const postgresRoleHandlers = PostgresRole.Provider.of({
|
|
@@ -40,7 +40,8 @@ export const postgresRoleHandlers = PostgresRole.Provider.of({
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
const passwordSeal = output?.passwordSeal ?? '';
|
|
43
|
-
|
|
43
|
+
// Read and reconcile must expose the same shape to Alchemy's drift comparison.
|
|
44
|
+
const found = { ...storedAttributes(live), passwordSeal };
|
|
44
45
|
return yield* ownedRead(
|
|
45
46
|
{ fqn, instanceId, output },
|
|
46
47
|
found,
|
|
@@ -99,31 +99,6 @@ describe('diff', () => {
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
test('ADMIN or SET on the live read answers update; stored membership rows do not', async () => {
|
|
103
|
-
const declared = { ...base, memberOf: ['hf_agent'] as const };
|
|
104
|
-
const stored = { ...output, memberOf: ['hf_agent'] as const };
|
|
105
|
-
const row = (admin: boolean, set: boolean) => ({
|
|
106
|
-
found: {
|
|
107
|
-
...stored,
|
|
108
|
-
memberships: [{ parent: 'hf_agent', grantor: 'postgres', admin, set }],
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
expect(await run(diffPostgresRole(declared, stored, {}, row(true, true)))).toEqual({
|
|
112
|
-
action: 'update',
|
|
113
|
-
});
|
|
114
|
-
expect(await run(diffPostgresRole(declared, stored, {}, row(false, true)))).toEqual({
|
|
115
|
-
action: 'update',
|
|
116
|
-
});
|
|
117
|
-
expect(await run(diffPostgresRole(declared, stored, {}, row(false, false)))).toEqual({
|
|
118
|
-
action: 'noop',
|
|
119
|
-
});
|
|
120
|
-
const stuffed = {
|
|
121
|
-
...stored,
|
|
122
|
-
memberships: [{ parent: 'hf_agent', grantor: 'postgres', admin: true, set: true }],
|
|
123
|
-
};
|
|
124
|
-
expect(await run(diffPostgresRole(declared, stuffed))).toEqual({ action: 'noop' });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
102
|
test('a rotated password answers update even with no other drift; a matching or unreadable one does not', async () => {
|
|
128
103
|
const declared = { ...base, password: { fromEnv: 'PG_SEAT_ROLE_PASSWORD' } as const };
|
|
129
104
|
const stale = seal({ password: 'old' }, 'fixed-salt');
|
|
@@ -28,6 +28,8 @@ const b64 = (value: Buffer): string => value.toString('base64');
|
|
|
28
28
|
/**
|
|
29
29
|
* `SCRAM-SHA-256$4096:<salt>$<StoredKey>:<ServerKey>` for one password. The salt is fresh per
|
|
30
30
|
* call (same as `psql \password`); tests pass one to make the output deterministic.
|
|
31
|
+
* Callers first run `assertPasswordAscii`: raw non-ASCII may differ from PostgreSQL's
|
|
32
|
+
* SASLprep bytes (https://www.postgresql.org/docs/16/sasl-authentication.html).
|
|
31
33
|
*/
|
|
32
34
|
export const scramSha256Verifier = (password: string, salt?: Buffer): string => {
|
|
33
35
|
const bytes = salt ?? randomBytes(SALT_BYTES);
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* written — a plan never re-sends a secret that already matches, and never stores one.
|
|
12
12
|
*/
|
|
13
13
|
import * as Redacted from 'effect/Redacted';
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import { PostgresRolePasswordNonAsciiRefused } from './role-errors.ts';
|
|
14
16
|
import { type Environment, resolveAll, seal, sealMatches } from '../secrets/write-only.ts';
|
|
15
17
|
import type { PostgresRoleProps } from './role-attrs.ts';
|
|
16
18
|
|
|
@@ -22,6 +24,19 @@ export interface ResolvedPassword {
|
|
|
22
24
|
readonly value: Redacted.Redacted<string> | undefined;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** ⛔ PostgreSQL applies SASLprep before SCRAM, with a raw-byte fallback for prohibited input:
|
|
28
|
+
* https://www.postgresql.org/docs/16/sasl-authentication.html#SASL-SCRAM-SHA-256
|
|
29
|
+
* Non-ASCII can normalize to different bytes (e.g. soft hyphen disappears). Refuse before
|
|
30
|
+
* any write, even with a matching old seal, until we have a vendor-compatible normalizer.
|
|
31
|
+
* ASCII is unchanged, including prohibited ASCII controls via PostgreSQL's fallback. */
|
|
32
|
+
export const assertPasswordAscii = (
|
|
33
|
+
role: string,
|
|
34
|
+
resolved: ResolvedPassword,
|
|
35
|
+
): Effect.Effect<void, PostgresRolePasswordNonAsciiRefused> =>
|
|
36
|
+
resolved.value !== undefined && /[\u0080-\uffff]/.test(Redacted.value(resolved.value))
|
|
37
|
+
? Effect.fail(new PostgresRolePasswordNonAsciiRefused({ role }))
|
|
38
|
+
: Effect.void;
|
|
39
|
+
|
|
25
40
|
export const resolvePassword = (
|
|
26
41
|
props: PostgresRoleProps,
|
|
27
42
|
env: Environment = process.env,
|
package/src/postgres/role-sql.ts
CHANGED
|
@@ -181,7 +181,9 @@ export const declarationMatches = (
|
|
|
181
181
|
(props.validUntil === undefined || sameValidUntil(props.validUntil, live.validUntil)) &&
|
|
182
182
|
(wanted === undefined || wanted === found) &&
|
|
183
183
|
privilegedFlags(live).length === 0 &&
|
|
184
|
-
(live.memberships ?? []).every(
|
|
184
|
+
(live.memberships ?? []).every(
|
|
185
|
+
(row) => !row.admin && !row.set && (!row.inherit || props.inherit),
|
|
186
|
+
)
|
|
185
187
|
);
|
|
186
188
|
};
|
|
187
189
|
|
package/src/postgres/role.ts
CHANGED
|
@@ -33,7 +33,12 @@ import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
|
33
33
|
import { type Environment } from '../secrets/write-only.ts';
|
|
34
34
|
import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
|
|
35
35
|
import { nameByteRefusal, validUntilRefusal } from './role-attrs.ts';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
assertPasswordAscii,
|
|
38
|
+
passwordMatchesSeal,
|
|
39
|
+
resolvePassword,
|
|
40
|
+
sealPassword,
|
|
41
|
+
} from './role-secrets.ts';
|
|
37
42
|
import { scramSha256Verifier } from './role-scram.ts';
|
|
38
43
|
import {
|
|
39
44
|
buildAlterRoleSql,
|
|
@@ -52,6 +57,7 @@ import {
|
|
|
52
57
|
PostgresRoleNameRefused,
|
|
53
58
|
type PostgresRoleParentMissing,
|
|
54
59
|
PostgresRolePasswordEnvUnsetError,
|
|
60
|
+
type PostgresRolePasswordNonAsciiRefused,
|
|
55
61
|
PostgresRolePrivilegedRefused,
|
|
56
62
|
PostgresRoleValidUntilRefused,
|
|
57
63
|
} from './role-errors.ts';
|
|
@@ -93,6 +99,7 @@ export const reconcileWithClient = (
|
|
|
93
99
|
): Effect.Effect<
|
|
94
100
|
PostgresRoleAttributes,
|
|
95
101
|
| PostgresRolePasswordEnvUnsetError
|
|
102
|
+
| PostgresRolePasswordNonAsciiRefused
|
|
96
103
|
| PostgresRolePrivilegedRefused
|
|
97
104
|
| PostgresRoleCreateVanished
|
|
98
105
|
| PostgresRoleMembershipUnrepaired
|
|
@@ -102,6 +109,7 @@ export const reconcileWithClient = (
|
|
|
102
109
|
> =>
|
|
103
110
|
Effect.gen(function* () {
|
|
104
111
|
const resolved = resolvePassword(props, env);
|
|
112
|
+
yield* assertPasswordAscii(props.name, resolved);
|
|
105
113
|
const observed = yield* readRoleWithClient(pg, props.name);
|
|
106
114
|
if (observed === undefined) {
|
|
107
115
|
// A create that declares a password must hold it: a LOGIN role with no password set, or
|
|
@@ -49,6 +49,7 @@ const fakeClient = {
|
|
|
49
49
|
|
|
50
50
|
// The pool factory is provided to this Effect only; other suites keep the real driver.
|
|
51
51
|
const socket = Layer.merge(
|
|
52
|
+
// tmp-allow: unix socket directory the driver joins a socket name onto, not a directory this test creates
|
|
52
53
|
postgresConnection({ host: '/tmp', database: 'postgres', username: 'postgres' }),
|
|
53
54
|
Layer.succeed(PostgresPool, (config) => {
|
|
54
55
|
pools.push(config);
|
|
@@ -54,4 +54,4 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
|
|
|
54
54
|
* ★ A DIGEST OF THE DATA, NOT OF THE FILE TEXT, so reformatting is not a stale generation while
|
|
55
55
|
* changing a 128 to a 129 by hand is. `tests/schema-manifest.test.ts` recomputes it.
|
|
56
56
|
*/
|
|
57
|
-
export const PROXMOX_CONSTRAINTS_DIGEST = '
|
|
57
|
+
export const PROXMOX_CONSTRAINTS_DIGEST = 'cbf63b7bac0500ef';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -122,4 +122,9 @@ export const PVE_NODES_QEMU_CONSTRAINTS: Readonly<Record<string, EndpointConstra
|
|
|
122
122
|
"vmstatestorage": {"format":"pve-storage-id","type":"string"},
|
|
123
123
|
"watchdog": {"format":"pve-qm-watchdog","type":"string"},
|
|
124
124
|
},
|
|
125
|
+
"pve:PUT /nodes/{node}/qemu/{vmid}/resize": {
|
|
126
|
+
"digest": {"maxLength":40,"type":"string"},
|
|
127
|
+
"disk": {"enum":["ide0","ide1","ide2","ide3","scsi0","scsi1","scsi2","scsi3","scsi4","scsi5","scsi6","scsi7","scsi8","scsi9","scsi10","scsi11","scsi12","scsi13","scsi14","scsi15","scsi16","scsi17","scsi18","scsi19","scsi20","scsi21","scsi22","scsi23","scsi24","scsi25","scsi26","scsi27","scsi28","scsi29","scsi30","virtio0","virtio1","virtio2","virtio3","virtio4","virtio5","virtio6","virtio7","virtio8","virtio9","virtio10","virtio11","virtio12","virtio13","virtio14","virtio15","sata0","sata1","sata2","sata3","sata4","sata5","efidisk0","tpmstate0"],"required":true,"type":"string"},
|
|
128
|
+
"size": {"pattern":"^\\+?\\d+(\\.\\d+)?[KMGT]?\\n?$","patternSource":"\\+?\\d+(\\.\\d+)?[KMGT]?","required":true,"type":"string"},
|
|
129
|
+
},
|
|
125
130
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
package/src/proxmox/index.ts
CHANGED
|
@@ -113,6 +113,7 @@ export {
|
|
|
113
113
|
export { provisionBootstrap } from './provision-bootstrap.ts';
|
|
114
114
|
export { type DeclareProvisionOptions, declareProvisionBaseline } from './provision-declare.ts';
|
|
115
115
|
export { ProxmoxVm, ProxmoxVmProvider, type VmAttributes, type VmProps } from './qemu.ts';
|
|
116
|
+
export { QemuDiskResizeRefused } from './qemu-size.ts';
|
|
116
117
|
export {
|
|
117
118
|
ProxmoxStorageDownload,
|
|
118
119
|
ProxmoxStorageDownloadProvider,
|