feat(opsserver): add health, audit, cluster health, and durable credential management hardening
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { assertEquals, assertRejects } from 'jsr:@std/assert';
|
||||
import { describe, it } from 'jsr:@std/testing/bdd';
|
||||
import { TypedRequest } from '@api.global/typedrequest';
|
||||
import {
|
||||
createTestContainer,
|
||||
getTestPorts,
|
||||
loginAndGetIdentity,
|
||||
TEST_ACCESS_KEY,
|
||||
} from './helpers/server.helper.ts';
|
||||
import { ObjectStorageContainer } from '../ts/index.ts';
|
||||
import type { IReq_CreateBucket, IReq_ListBuckets } from '../ts_interfaces/requests/buckets.ts';
|
||||
import type {
|
||||
IReq_AddCredential,
|
||||
IReq_GetCredentials,
|
||||
IReq_RemoveCredential,
|
||||
} from '../ts_interfaces/requests/credentials.ts';
|
||||
import type * as interfaces from '../ts_interfaces/index.ts';
|
||||
|
||||
const PORT_INDEX = 8;
|
||||
const ports = getTestPorts(PORT_INDEX);
|
||||
const url = `http://localhost:${ports.uiPort}/typedrequest`;
|
||||
const storageDirectory = `.nogit/testdata-${PORT_INDEX}`;
|
||||
|
||||
const cleanupStorageDirectory = async () => {
|
||||
try {
|
||||
await Deno.remove(storageDirectory, { recursive: true });
|
||||
} catch (error) {
|
||||
if (!(error instanceof Deno.errors.NotFound)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('Credential persistence', { sanitizeResources: false, sanitizeOps: false }, () => {
|
||||
it('persists managed credentials across restart and refreshes the internal client', async () => {
|
||||
await cleanupStorageDirectory();
|
||||
|
||||
let activeContainer: ObjectStorageContainer | null = null;
|
||||
|
||||
const stopContainer = async () => {
|
||||
if (!activeContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await activeContainer.stop();
|
||||
} finally {
|
||||
activeContainer = null;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
activeContainer = createTestContainer(PORT_INDEX);
|
||||
await activeContainer.start();
|
||||
|
||||
let identity: interfaces.data.IIdentity = await loginAndGetIdentity(ports.uiPort);
|
||||
|
||||
const addCredential = new TypedRequest<IReq_AddCredential>(url, 'addCredential');
|
||||
await addCredential.fire({
|
||||
identity,
|
||||
accessKeyId: 'persisted-key',
|
||||
secretAccessKey: 'persisted-secret',
|
||||
});
|
||||
|
||||
const removeCredential = new TypedRequest<IReq_RemoveCredential>(url, 'removeCredential');
|
||||
await removeCredential.fire({ identity, accessKeyId: TEST_ACCESS_KEY });
|
||||
|
||||
const getCredentials = new TypedRequest<IReq_GetCredentials>(url, 'getCredentials');
|
||||
const credentialsBeforeRestart = await getCredentials.fire({ identity });
|
||||
assertEquals(credentialsBeforeRestart.credentials.length, 1);
|
||||
assertEquals(credentialsBeforeRestart.credentials[0].accessKeyId, 'persisted-key');
|
||||
|
||||
const listBuckets = new TypedRequest<IReq_ListBuckets>(url, 'listBuckets');
|
||||
const bucketsBeforeRestart = await listBuckets.fire({ identity });
|
||||
assertEquals(Array.isArray(bucketsBeforeRestart.buckets), true);
|
||||
|
||||
await stopContainer();
|
||||
|
||||
activeContainer = createTestContainer(PORT_INDEX);
|
||||
await activeContainer.start();
|
||||
|
||||
identity = await loginAndGetIdentity(ports.uiPort);
|
||||
|
||||
const credentialsAfterRestart = await getCredentials.fire({ identity });
|
||||
assertEquals(credentialsAfterRestart.credentials.length, 1);
|
||||
assertEquals(credentialsAfterRestart.credentials[0].accessKeyId, 'persisted-key');
|
||||
|
||||
const createBucket = new TypedRequest<IReq_CreateBucket>(url, 'createBucket');
|
||||
await createBucket.fire({ identity, bucketName: 'persisted-creds-bucket' });
|
||||
|
||||
const bucketsAfterRestart = await listBuckets.fire({ identity });
|
||||
assertEquals(
|
||||
bucketsAfterRestart.buckets.some((bucket) => bucket.name === 'persisted-creds-bucket'),
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
await stopContainer();
|
||||
await cleanupStorageDirectory();
|
||||
}
|
||||
});
|
||||
|
||||
it('lets explicit environment credentials override persisted managed credentials', async () => {
|
||||
const portIndex = 10;
|
||||
const envPorts = getTestPorts(portIndex);
|
||||
const envUrl = `http://localhost:${envPorts.uiPort}/typedrequest`;
|
||||
const envStorageDirectory = `.nogit/testdata-${portIndex}`;
|
||||
const previousAccessKey = Deno.env.get('OBJST_ACCESS_KEY');
|
||||
const previousSecretKey = Deno.env.get('OBJST_SECRET_KEY');
|
||||
let container: ObjectStorageContainer | null = null;
|
||||
|
||||
const cleanupEnvStorageDirectory = async () => {
|
||||
try {
|
||||
await Deno.remove(envStorageDirectory, { recursive: true });
|
||||
} catch (error) {
|
||||
if (!(error instanceof Deno.errors.NotFound)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await cleanupEnvStorageDirectory();
|
||||
await Deno.mkdir(`${envStorageDirectory}/.objectstorage`, { recursive: true });
|
||||
await Deno.writeTextFile(
|
||||
`${envStorageDirectory}/.objectstorage/admin-config.json`,
|
||||
JSON.stringify({
|
||||
accessCredentials: [{ accessKeyId: 'persisted-key', secretAccessKey: 'persisted-secret' }],
|
||||
}),
|
||||
);
|
||||
|
||||
Deno.env.set('OBJST_ACCESS_KEY', 'env-key');
|
||||
Deno.env.set('OBJST_SECRET_KEY', 'env-secret');
|
||||
|
||||
container = createTestContainer(portIndex, { storageDirectory: envStorageDirectory });
|
||||
await container.start();
|
||||
|
||||
const identity = await loginAndGetIdentity(envPorts.uiPort);
|
||||
const getCredentials = new TypedRequest<IReq_GetCredentials>(envUrl, 'getCredentials');
|
||||
const response = await getCredentials.fire({ identity });
|
||||
|
||||
assertEquals(response.credentials.length, 1);
|
||||
assertEquals(response.credentials[0].accessKeyId, 'env-key');
|
||||
} finally {
|
||||
if (container) {
|
||||
await container.stop();
|
||||
}
|
||||
if (previousAccessKey === undefined) {
|
||||
Deno.env.delete('OBJST_ACCESS_KEY');
|
||||
} else {
|
||||
Deno.env.set('OBJST_ACCESS_KEY', previousAccessKey);
|
||||
}
|
||||
if (previousSecretKey === undefined) {
|
||||
Deno.env.delete('OBJST_SECRET_KEY');
|
||||
} else {
|
||||
Deno.env.set('OBJST_SECRET_KEY', previousSecretKey);
|
||||
}
|
||||
await cleanupEnvStorageDirectory();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not persist rejected credential replacements', async () => {
|
||||
const portIndex = 11;
|
||||
const rejectPorts = getTestPorts(portIndex);
|
||||
const rejectUrl = `http://localhost:${rejectPorts.uiPort}/typedrequest`;
|
||||
const rejectStorageDirectory = `.nogit/testdata-${portIndex}`;
|
||||
let container: ObjectStorageContainer | null = null;
|
||||
|
||||
const cleanupRejectStorageDirectory = async () => {
|
||||
try {
|
||||
await Deno.remove(rejectStorageDirectory, { recursive: true });
|
||||
} catch (error) {
|
||||
if (!(error instanceof Deno.errors.NotFound)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await cleanupRejectStorageDirectory();
|
||||
container = createTestContainer(portIndex, { storageDirectory: rejectStorageDirectory });
|
||||
await container.start();
|
||||
|
||||
await assertRejects(() =>
|
||||
container!.replaceAccessCredentials([
|
||||
{ accessKeyId: 'duplicate-key', secretAccessKey: 'secret-a' },
|
||||
{ accessKeyId: 'duplicate-key', secretAccessKey: 'secret-b' },
|
||||
])
|
||||
);
|
||||
|
||||
const identity = await loginAndGetIdentity(rejectPorts.uiPort);
|
||||
const getCredentials = new TypedRequest<IReq_GetCredentials>(rejectUrl, 'getCredentials');
|
||||
const response = await getCredentials.fire({ identity });
|
||||
|
||||
assertEquals(response.credentials.length, 1);
|
||||
assertEquals(response.credentials[0].accessKeyId, TEST_ACCESS_KEY);
|
||||
await assertRejects(() =>
|
||||
Deno.readTextFile(`${rejectStorageDirectory}/.objectstorage/admin-config.json`)
|
||||
);
|
||||
} finally {
|
||||
if (container) {
|
||||
await container.stop();
|
||||
}
|
||||
await cleanupRejectStorageDirectory();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user