@wildo-ai/platform-infra-providers 1.1.2
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/LICENSE +34 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.d.ts +63 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.d.ts.map +1 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.js +110 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.js.map +1 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.d.ts +30 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.d.ts.map +1 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.js +61 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.js.map +1 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts +26 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts.map +1 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js +54 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js.map +1 -0
- package/dist/esm/index.d.ts +88 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +125 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/minio-self/minio-self.infra.module.d.ts +64 -0
- package/dist/esm/minio-self/minio-self.infra.module.d.ts.map +1 -0
- package/dist/esm/minio-self/minio-self.infra.module.js +105 -0
- package/dist/esm/minio-self/minio-self.infra.module.js.map +1 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts +52 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts.map +1 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js +201 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js.map +1 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts +81 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts.map +1 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.js +311 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.js.map +1 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.d.ts +70 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.d.ts.map +1 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.js +328 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.js.map +1 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts +88 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts.map +1 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js +473 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js.map +1 -0
- package/dist/esm/redis-self/redis-self.infra.module.d.ts +89 -0
- package/dist/esm/redis-self/redis-self.infra.module.d.ts.map +1 -0
- package/dist/esm/redis-self/redis-self.infra.module.js +331 -0
- package/dist/esm/redis-self/redis-self.infra.module.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +52 -0
- package/src/aws-s3/__tests__/aws-s3.infra.module.test.ts +67 -0
- package/src/aws-s3/aws-s3.infra.module.ts +120 -0
- package/src/clamav-self/__tests__/clamav-self.infra.module.test.ts +65 -0
- package/src/clamav-self/clamav-self.infra.module.ts +72 -0
- package/src/gotenberg-self/__tests__/gotenberg-self.infra.module.test.ts +65 -0
- package/src/gotenberg-self/gotenberg-self.infra.module.ts +65 -0
- package/src/index.ts +133 -0
- package/src/minio-self/__tests__/minio-self.infra.module.test.ts +70 -0
- package/src/minio-self/minio-self.infra.module.ts +116 -0
- package/src/mongodb-atlas/__tests__/mongodb-atlas.infra.module.test.ts +198 -0
- package/src/mongodb-atlas/mongodb-atlas.infra.module.ts +236 -0
- package/src/mongodb-self/__tests__/mongodb-self.infra.module.test.ts +419 -0
- package/src/mongodb-self/mongodb-self.infra.module.ts +384 -0
- package/src/postgres-self/__tests__/postgres-self.infra.module.test.ts +344 -0
- package/src/postgres-self/postgres-self.infra.module.ts +377 -0
- package/src/rabbitmq-self/__tests__/rabbitmq-self.infra.module.test.ts +499 -0
- package/src/rabbitmq-self/rabbitmq-self.infra.module.ts +531 -0
- package/src/redis-self/__tests__/redis-self.infra.module.test.ts +341 -0
- package/src/redis-self/redis-self.infra.module.ts +372 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `MongodbSelfInfraProviderModule` (Slice 4.B.3 of
|
|
3
|
+
* `infra-provider-planner.md`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
AppCredentialMintingContext,
|
|
9
|
+
IMongoSelfAdminClient,
|
|
10
|
+
InfraCapability,
|
|
11
|
+
InfraPlatformAdminConsumer,
|
|
12
|
+
InfraProvider,
|
|
13
|
+
} from '@wildo-ai/platform-config-lib';
|
|
14
|
+
import { ApplicationRuntimePrincipalKind } from '@wildo-ai/platform-config-lib';
|
|
15
|
+
import type { Mock } from 'vitest';
|
|
16
|
+
|
|
17
|
+
// Derived from the CONTRACT rather than hand-written, so the stub cannot drift from the method it
|
|
18
|
+
// doubles. `ReturnType<typeof vi.fn>` erased both arity and shape, leaving every assertion on the
|
|
19
|
+
// recorded call unchecked.
|
|
20
|
+
type CollectionScopedUpsertCall = Parameters<IMongoSelfAdminClient['upsertCollectionScopedDatabaseUser']>[0];
|
|
21
|
+
type CollectionScopedDropCall = Parameters<IMongoSelfAdminClient['dropCollectionScopedDatabaseUser']>[0];
|
|
22
|
+
|
|
23
|
+
import { MongodbSelfInfraProviderModule, buildMinionMongodbPrivileges } from '../mongodb-self.infra.module';
|
|
24
|
+
|
|
25
|
+
function makeAdminClientStub(): IMongoSelfAdminClient & {
|
|
26
|
+
upsertApplicationDatabaseUserSpy: ReturnType<typeof vi.fn>;
|
|
27
|
+
deleteApplicationDatabaseUserSpy: ReturnType<typeof vi.fn>;
|
|
28
|
+
upsertCollectionScopedDatabaseUserSpy: Mock<[CollectionScopedUpsertCall], Promise<undefined>>;
|
|
29
|
+
dropCollectionScopedDatabaseUserSpy: Mock<[CollectionScopedDropCall], Promise<undefined>>;
|
|
30
|
+
} {
|
|
31
|
+
const upsertApplicationDatabaseUserSpy = vi.fn().mockResolvedValue(undefined);
|
|
32
|
+
const deleteApplicationDatabaseUserSpy = vi.fn().mockResolvedValue(undefined);
|
|
33
|
+
// Declares the one argument production passes, so `mock.calls[0][0]` is a real tuple element.
|
|
34
|
+
// A zero-arg `vi.fn()` types `calls` as `[]`, and every index into it is a TS2493.
|
|
35
|
+
const upsertCollectionScopedDatabaseUserSpy = vi.fn((_options: CollectionScopedUpsertCall) => Promise.resolve(undefined));
|
|
36
|
+
const dropCollectionScopedDatabaseUserSpy = vi.fn((_options: CollectionScopedDropCall) => Promise.resolve(undefined));
|
|
37
|
+
return {
|
|
38
|
+
upsertApplicationDatabaseUser: upsertApplicationDatabaseUserSpy,
|
|
39
|
+
deleteApplicationDatabaseUser: deleteApplicationDatabaseUserSpy,
|
|
40
|
+
upsertCollectionScopedDatabaseUser: upsertCollectionScopedDatabaseUserSpy,
|
|
41
|
+
dropCollectionScopedDatabaseUser: dropCollectionScopedDatabaseUserSpy,
|
|
42
|
+
upsertApplicationDatabaseUserSpy,
|
|
43
|
+
deleteApplicationDatabaseUserSpy,
|
|
44
|
+
upsertCollectionScopedDatabaseUserSpy,
|
|
45
|
+
dropCollectionScopedDatabaseUserSpy,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeMintContext(
|
|
50
|
+
adminClient: IMongoSelfAdminClient,
|
|
51
|
+
overrides: Partial<AppCredentialMintingContext> = {},
|
|
52
|
+
): AppCredentialMintingContext {
|
|
53
|
+
return {
|
|
54
|
+
appSlug: 'wonder-todos',
|
|
55
|
+
applicationNamespace: 'wonder-todos-local',
|
|
56
|
+
wildoApplicationId: '60d21bdf1c4ae5cda1b2c3d4',
|
|
57
|
+
adminClient,
|
|
58
|
+
secretInputs: {
|
|
59
|
+
'database.mongodb.password': 'orchestrator-supplied-password',
|
|
60
|
+
},
|
|
61
|
+
applicationConfigInputs: {
|
|
62
|
+
databaseName: 'wonder_todos',
|
|
63
|
+
adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin?authSource=admin',
|
|
64
|
+
},
|
|
65
|
+
...overrides,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('MongodbSelfInfraProviderModule.plan()', () => {
|
|
70
|
+
const contribution = MongodbSelfInfraProviderModule.plan({
|
|
71
|
+
environment: 'local',
|
|
72
|
+
runtime: 'docker-compose',
|
|
73
|
+
backingServiceKey: 'mongodb',
|
|
74
|
+
config: {},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('declares the right provider + capability triple', () => {
|
|
78
|
+
expect(MongodbSelfInfraProviderModule.provider).toBe(InfraProvider.MONGODB_SELF);
|
|
79
|
+
expect(MongodbSelfInfraProviderModule.supportedCapabilities).toEqual([
|
|
80
|
+
InfraCapability.DOCUMENT_DATABASE,
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('contributes the mongodb compose service + K8s manifest', () => {
|
|
85
|
+
expect(contribution.dockerComposeServices).toEqual(['mongodb']);
|
|
86
|
+
expect(contribution.k8sResources).toEqual([
|
|
87
|
+
'templates/infra/k8s/internal/mongodb/deployment.yaml.hbs',
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('declares platformAdmin (audience-1) for apps-manager + companion', () => {
|
|
92
|
+
expect(contribution.platformAdmin?.secretEnvVars).toEqual(['MONGO_CONNECT_URL']);
|
|
93
|
+
expect(contribution.platformAdmin?.consumers).toEqual([
|
|
94
|
+
InfraPlatformAdminConsumer.APPS_MANAGER,
|
|
95
|
+
InfraPlatformAdminConsumer.COMPANION,
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('declares the same per-app secret blob paths as the Atlas variant', () => {
|
|
100
|
+
expect(contribution.appRuntime?.perAppSecretBlobPaths).toEqual([
|
|
101
|
+
'database.mongodb.username',
|
|
102
|
+
'database.mongodb.password',
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('MongodbSelfInfraProviderModule.mintAppCredentials', () => {
|
|
108
|
+
it('upserts a self-managed Mongo user and returns the per-app SRV URL', async () => {
|
|
109
|
+
const adminClient = makeAdminClientStub();
|
|
110
|
+
const result = await MongodbSelfInfraProviderModule.mintAppCredentials!(
|
|
111
|
+
makeMintContext(adminClient),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(adminClient.upsertApplicationDatabaseUserSpy).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(adminClient.upsertApplicationDatabaseUserSpy).toHaveBeenCalledWith({
|
|
116
|
+
adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin?authSource=admin',
|
|
117
|
+
databaseName: 'wonder_todos',
|
|
118
|
+
username: 'wonder-todos-local_user',
|
|
119
|
+
password: 'orchestrator-supplied-password',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(result.secretBlobValues).toEqual({
|
|
123
|
+
'database.mongodb.username': 'wonder-todos-local_user',
|
|
124
|
+
'database.mongodb.password': 'orchestrator-supplied-password',
|
|
125
|
+
});
|
|
126
|
+
// Per-app URL derived by swapping admin credentials and DB path.
|
|
127
|
+
expect(result.runtimeArtifacts?.connectionString).toContain('wonder-todos-local_user');
|
|
128
|
+
expect(result.runtimeArtifacts?.connectionString).toContain('orchestrator-supplied-password');
|
|
129
|
+
expect(result.runtimeArtifacts?.connectionString).toContain('/wonder_todos');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('fails loud when secretInputs is missing the password', async () => {
|
|
133
|
+
const adminClient = makeAdminClientStub();
|
|
134
|
+
await expect(
|
|
135
|
+
MongodbSelfInfraProviderModule.mintAppCredentials!(
|
|
136
|
+
makeMintContext(adminClient, { secretInputs: {} }),
|
|
137
|
+
),
|
|
138
|
+
).rejects.toThrow(/missing 'database\.mongodb\.password'/);
|
|
139
|
+
expect(adminClient.upsertApplicationDatabaseUserSpy).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('fails loud when applicationNamespace is empty', async () => {
|
|
143
|
+
const adminClient = makeAdminClientStub();
|
|
144
|
+
await expect(
|
|
145
|
+
MongodbSelfInfraProviderModule.mintAppCredentials!(
|
|
146
|
+
makeMintContext(adminClient, { applicationNamespace: '' }),
|
|
147
|
+
),
|
|
148
|
+
).rejects.toThrow(/applicationNamespace is empty/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('fails loud when applicationConfigInputs.databaseName is missing', async () => {
|
|
152
|
+
const adminClient = makeAdminClientStub();
|
|
153
|
+
await expect(
|
|
154
|
+
MongodbSelfInfraProviderModule.mintAppCredentials!(
|
|
155
|
+
makeMintContext(adminClient, {
|
|
156
|
+
applicationConfigInputs: {
|
|
157
|
+
adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin',
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
),
|
|
161
|
+
).rejects.toThrow(/missing 'databaseName'/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('fails loud when applicationConfigInputs.adminConnectUrl is missing', async () => {
|
|
165
|
+
const adminClient = makeAdminClientStub();
|
|
166
|
+
await expect(
|
|
167
|
+
MongodbSelfInfraProviderModule.mintAppCredentials!(
|
|
168
|
+
makeMintContext(adminClient, {
|
|
169
|
+
applicationConfigInputs: { databaseName: 'wonder_todos' },
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
).rejects.toThrow(/missing 'adminConnectUrl'/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('MongodbSelfInfraProviderModule.revokeAppCredentials', () => {
|
|
177
|
+
it('deletes the per-app database user via admin connection', async () => {
|
|
178
|
+
const adminClient = makeAdminClientStub();
|
|
179
|
+
await MongodbSelfInfraProviderModule.revokeAppCredentials!(
|
|
180
|
+
makeMintContext(adminClient),
|
|
181
|
+
);
|
|
182
|
+
expect(adminClient.deleteApplicationDatabaseUserSpy).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(adminClient.deleteApplicationDatabaseUserSpy).toHaveBeenCalledWith({
|
|
184
|
+
adminConnectUrl: 'mongodb://admin:adminpw@mongodb:27017/admin?authSource=admin',
|
|
185
|
+
username: 'wonder-todos-local_user',
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('fails loud when applicationNamespace is empty', async () => {
|
|
190
|
+
const adminClient = makeAdminClientStub();
|
|
191
|
+
await expect(
|
|
192
|
+
MongodbSelfInfraProviderModule.revokeAppCredentials!(
|
|
193
|
+
makeMintContext(adminClient, { applicationNamespace: '' }),
|
|
194
|
+
),
|
|
195
|
+
).rejects.toThrow(/applicationNamespace is empty/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('fails loud when applicationConfigInputs.adminConnectUrl is missing', async () => {
|
|
199
|
+
const adminClient = makeAdminClientStub();
|
|
200
|
+
await expect(
|
|
201
|
+
MongodbSelfInfraProviderModule.revokeAppCredentials!(
|
|
202
|
+
makeMintContext(adminClient, {
|
|
203
|
+
applicationConfigInputs: { databaseName: 'wonder_todos' },
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
206
|
+
).rejects.toThrow(/missing 'adminConnectUrl'/);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('MongodbSelfInfraProviderModule audience-2 absence (D-4)', () => {
|
|
211
|
+
it('does NOT declare mintPlatformServiceCredentials', () => {
|
|
212
|
+
expect(MongodbSelfInfraProviderModule.mintPlatformServiceCredentials).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The MongoDB privilege derivation (Step 3 / R1 / D14).
|
|
218
|
+
*
|
|
219
|
+
* These assertions ARE the security semantics — what "read" and "write" mean
|
|
220
|
+
* once a minion's declaration reaches the database.
|
|
221
|
+
*/
|
|
222
|
+
describe('buildMinionMongodbPrivileges', () => {
|
|
223
|
+
const DB = 'wonder-todos';
|
|
224
|
+
|
|
225
|
+
it('scopes every privilege to ONE named collection', () => {
|
|
226
|
+
// Never the database resource (`collection: ''`), which is Mongo's "every
|
|
227
|
+
// collection here" — that would make `resourceAccess` decorative.
|
|
228
|
+
const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: ['todos'], writeCollections: [] });
|
|
229
|
+
expect(privileges).toEqual([{ resource: { db: DB, collection: 'todos' }, actions: ['find'] }]);
|
|
230
|
+
expect(privileges.every((p) => p.resource.collection.length > 0)).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('grants find for read and NOT find for write', () => {
|
|
234
|
+
// The platform derivation refuses to widen write into read; re-adding `find`
|
|
235
|
+
// here would defeat that decision at the last hop.
|
|
236
|
+
const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: [], writeCollections: ['todos'] });
|
|
237
|
+
expect(privileges[0]!.actions).toEqual(['insert', 'update', 'remove']);
|
|
238
|
+
expect(privileges[0]!.actions).not.toContain('find');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('grants no enumeration action', () => {
|
|
242
|
+
// A minion granted read on one resource must not be able to discover what
|
|
243
|
+
// else exists.
|
|
244
|
+
const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: ['todos'], writeCollections: ['todos'] });
|
|
245
|
+
for (const action of privileges.flatMap((p) => p.actions)) {
|
|
246
|
+
expect(['listCollections', 'listIndexes', 'collStats', 'dbStats', 'listDatabases']).not.toContain(action);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('NEVER emits dbAdmin or readWrite (D14)', () => {
|
|
251
|
+
// The two roles the application backend holds on the whole database.
|
|
252
|
+
// `dbAdmin` is banned outright; `readWrite` is database-WIDE, so granting it
|
|
253
|
+
// would reach every collection regardless of the declaration.
|
|
254
|
+
const serialized = JSON.stringify(buildMinionMongodbPrivileges({
|
|
255
|
+
databaseName: DB, readCollections: ['todos'], writeCollections: ['todos'],
|
|
256
|
+
}));
|
|
257
|
+
expect(serialized).not.toMatch(/dbAdmin|readWrite/);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('keeps read and write as SEPARATE entries for a collection granted both', () => {
|
|
261
|
+
// Mongo unions them identically, but the emitted role then reads back as the
|
|
262
|
+
// declaration that produced it — so an operator inspecting `db.getRole()`
|
|
263
|
+
// can check the grant against the config without re-deriving it.
|
|
264
|
+
const privileges = buildMinionMongodbPrivileges({ databaseName: DB, readCollections: ['todos'], writeCollections: ['todos'] });
|
|
265
|
+
expect(privileges).toHaveLength(2);
|
|
266
|
+
expect(privileges.map((p) => p.actions)).toEqual([['find'], ['insert', 'update', 'remove']]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('produces NO privileges for an empty grant set', () => {
|
|
270
|
+
// Declared-but-empty is a legitimate statement (a principal with no access).
|
|
271
|
+
// It must not degrade into "no privileges specified, therefore everything".
|
|
272
|
+
expect(buildMinionMongodbPrivileges({ databaseName: DB, readCollections: [], writeCollections: [] })).toEqual([]);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('MongodbSelfInfraProviderModule per-principal users (Step 3 / R1 / D14)', () => {
|
|
277
|
+
const MINION = { kind: ApplicationRuntimePrincipalKind.MINION, name: 'marketingScrapper' } as const;
|
|
278
|
+
const baseContext = {
|
|
279
|
+
appSlug: 'wonder-todos',
|
|
280
|
+
applicationNamespace: 'wonder_todos_local',
|
|
281
|
+
wildoApplicationId: '6a5f02014b25d65f9bfd70b2',
|
|
282
|
+
secretInputs: { 'database.mongodb.password': 'orchestrator-pw' },
|
|
283
|
+
applicationConfigInputs: { databaseName: 'wonder-todos', adminConnectUrl: 'mongodb://admin:pw@localhost:27017/admin' },
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
function makeAdmin() {
|
|
287
|
+
return {
|
|
288
|
+
upsertApplicationDatabaseUser: vi.fn(async () => {}),
|
|
289
|
+
upsertCollectionScopedDatabaseUser: vi.fn(async (_options: unknown) => {}),
|
|
290
|
+
deleteApplicationDatabaseUser: vi.fn(async () => {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
it('takes the COLLECTION-SCOPED path for a minion, never the database-wide one', async () => {
|
|
295
|
+
const admin = makeAdmin();
|
|
296
|
+
await MongodbSelfInfraProviderModule.mintAppCredentials!({
|
|
297
|
+
...baseContext, adminClient: admin, principal: MINION,
|
|
298
|
+
resourceGrants: { read: ['todos'], write: ['todos'] },
|
|
299
|
+
} as never);
|
|
300
|
+
|
|
301
|
+
expect(admin.upsertCollectionScopedDatabaseUser).toHaveBeenCalledTimes(1);
|
|
302
|
+
// The wide path grants `readWrite` + `dbAdmin` on the whole database — it
|
|
303
|
+
// must not be reachable for a named principal at all.
|
|
304
|
+
expect(admin.upsertApplicationDatabaseUser).not.toHaveBeenCalled();
|
|
305
|
+
|
|
306
|
+
const call = admin.upsertCollectionScopedDatabaseUser.mock.calls[0]![0] as never as {
|
|
307
|
+
username: string; roleName: string;
|
|
308
|
+
privileges: Array<{ resource: { collection: string }; actions: string[] }>;
|
|
309
|
+
};
|
|
310
|
+
expect(call.username).toBe('wonder_todos_local_minion_marketingScrapper_user');
|
|
311
|
+
expect(call.roleName).toBe('wonder_todos_local_minion_marketingScrapper_user_role');
|
|
312
|
+
expect(call.privileges).toEqual([
|
|
313
|
+
{ resource: { db: 'wonder-todos', collection: 'todos' }, actions: ['find'] },
|
|
314
|
+
{ resource: { db: 'wonder-todos', collection: 'todos' }, actions: ['insert', 'update', 'remove'] },
|
|
315
|
+
]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('leaves the APPLICATION BACKEND on the database-wide path, username unmoved', async () => {
|
|
319
|
+
// Moving it would orphan an existing deployment's user, and the backend
|
|
320
|
+
// legitimately holds the wide grant today.
|
|
321
|
+
const admin = makeAdmin();
|
|
322
|
+
await MongodbSelfInfraProviderModule.mintAppCredentials!({ ...baseContext, adminClient: admin } as never);
|
|
323
|
+
expect(admin.upsertApplicationDatabaseUser).toHaveBeenCalledWith(
|
|
324
|
+
expect.objectContaining({ username: 'wonder_todos_local_user' }),
|
|
325
|
+
);
|
|
326
|
+
expect(admin.upsertCollectionScopedDatabaseUser).not.toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('REFUSES a named principal with no grants rather than falling back to the wide user', async () => {
|
|
330
|
+
// The failure mode this guards: falling back would hand a minion
|
|
331
|
+
// `readWrite` + `dbAdmin` on the whole database through a code path that
|
|
332
|
+
// looks like it is doing the narrow thing. Whether to mint nothing at all
|
|
333
|
+
// is the ORCHESTRATOR's decision (D11/R3), not a default hidden here.
|
|
334
|
+
const admin = makeAdmin();
|
|
335
|
+
await expect(MongodbSelfInfraProviderModule.mintAppCredentials!({
|
|
336
|
+
...baseContext, adminClient: admin, principal: MINION,
|
|
337
|
+
} as never)).rejects.toThrow(/no resourceGrants/);
|
|
338
|
+
expect(admin.upsertApplicationDatabaseUser).not.toHaveBeenCalled();
|
|
339
|
+
expect(admin.upsertCollectionScopedDatabaseUser).not.toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('emits a role with NO privileges for a declared-but-empty grant set', async () => {
|
|
343
|
+
// Legitimate: a principal that declared access and asked for nothing. It
|
|
344
|
+
// must produce a role granting nothing, never "unspecified ⇒ leave it wide".
|
|
345
|
+
const admin = makeAdmin();
|
|
346
|
+
await MongodbSelfInfraProviderModule.mintAppCredentials!({
|
|
347
|
+
...baseContext, adminClient: admin, principal: MINION, resourceGrants: { read: [], write: [] },
|
|
348
|
+
} as never);
|
|
349
|
+
expect((admin.upsertCollectionScopedDatabaseUser.mock.calls[0]![0] as never as { privileges: unknown[] }).privileges)
|
|
350
|
+
.toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Revocation of a NAMED principal (Step 3 / R1).
|
|
357
|
+
*
|
|
358
|
+
* These exist because the defect they pin was SHIPPED: `revokeAppCredentials`
|
|
359
|
+
* derived the username with `buildMongodbUsername(applicationNamespace)`, which
|
|
360
|
+
* returns the APPLICATION BACKEND's name for every input. Retiring one minion
|
|
361
|
+
* therefore left that minion's credential live and dropped the backend's
|
|
362
|
+
* database user — while the mint path a few lines above carried a comment
|
|
363
|
+
* warning about exactly this. The fix landed at one site of the invariant; these
|
|
364
|
+
* pins are the other site.
|
|
365
|
+
*/
|
|
366
|
+
describe('MongodbSelfInfraProviderModule.revokeAppCredentials — named principal', () => {
|
|
367
|
+
const minionPrincipal = {
|
|
368
|
+
kind: ApplicationRuntimePrincipalKind.MINION,
|
|
369
|
+
name: 'marketingScrapper',
|
|
370
|
+
} as AppCredentialMintingContext['principal'];
|
|
371
|
+
|
|
372
|
+
it('never touches the APPLICATION BACKEND user when retiring a minion', async () => {
|
|
373
|
+
const adminClient = makeAdminClientStub();
|
|
374
|
+
await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
|
|
375
|
+
principal: minionPrincipal,
|
|
376
|
+
}));
|
|
377
|
+
// The user-only door is what dropped the backend's user. Asserting it was
|
|
378
|
+
// not called is the pin that reddens on a regression to the old derivation.
|
|
379
|
+
expect(adminClient.deleteApplicationDatabaseUserSpy).not.toHaveBeenCalled();
|
|
380
|
+
expect(adminClient.dropCollectionScopedDatabaseUserSpy).toHaveBeenCalledTimes(1);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('drops the custom ROLE as well as the user', async () => {
|
|
384
|
+
// A role left behind is silently adopted by the next mint of the same name,
|
|
385
|
+
// carrying privileges nobody re-declared.
|
|
386
|
+
const adminClient = makeAdminClientStub();
|
|
387
|
+
await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
|
|
388
|
+
principal: minionPrincipal,
|
|
389
|
+
}));
|
|
390
|
+
const call = adminClient.dropCollectionScopedDatabaseUserSpy.mock.calls[0]![0];
|
|
391
|
+
expect(call.roleName).toBe(`${call.username}_role`);
|
|
392
|
+
expect(call.databaseName).toBe('wonder_todos');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('derives the SAME username on mint and on revoke', async () => {
|
|
396
|
+
// Compared to each other rather than to a literal, so the pin survives a
|
|
397
|
+
// deliberate rename and fails only when the two sides DISAGREE — which is
|
|
398
|
+
// the failure that actually occurred.
|
|
399
|
+
const adminClient = makeAdminClientStub();
|
|
400
|
+
await MongodbSelfInfraProviderModule.mintAppCredentials!(makeMintContext(adminClient, {
|
|
401
|
+
principal: minionPrincipal,
|
|
402
|
+
resourceGrants: { read: ['todos'] as never, write: [] as never },
|
|
403
|
+
}));
|
|
404
|
+
await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
|
|
405
|
+
principal: minionPrincipal,
|
|
406
|
+
}));
|
|
407
|
+
expect(adminClient.dropCollectionScopedDatabaseUserSpy.mock.calls[0]![0].username)
|
|
408
|
+
.toBe(adminClient.upsertCollectionScopedDatabaseUserSpy.mock.calls[0]![0].username);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('still routes the APPLICATION BACKEND through the user-only door', async () => {
|
|
412
|
+
const adminClient = makeAdminClientStub();
|
|
413
|
+
await MongodbSelfInfraProviderModule.revokeAppCredentials!(makeMintContext(adminClient, {
|
|
414
|
+
principal: { kind: ApplicationRuntimePrincipalKind.APPLICATION_BACKEND } as AppCredentialMintingContext['principal'],
|
|
415
|
+
}));
|
|
416
|
+
expect(adminClient.dropCollectionScopedDatabaseUserSpy).not.toHaveBeenCalled();
|
|
417
|
+
expect(adminClient.deleteApplicationDatabaseUserSpy).toHaveBeenCalledTimes(1);
|
|
418
|
+
});
|
|
419
|
+
});
|