147 lines
6.0 KiB
TypeScript
147 lines
6.0 KiB
TypeScript
|
|
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,
|
||
|
|
} from './helpers/server.helper.ts';
|
||
|
|
import { ObjectStorageContainer } from '../ts/index.ts';
|
||
|
|
import type { IReq_AdminLogout, IReq_VerifyIdentity } from '../ts_interfaces/requests/admin.ts';
|
||
|
|
import type { IReq_CreateBucket } from '../ts_interfaces/requests/buckets.ts';
|
||
|
|
import type { IReq_AddCredential } from '../ts_interfaces/requests/credentials.ts';
|
||
|
|
import type { IReq_ListAuditEntries } from '../ts_interfaces/requests/audit.ts';
|
||
|
|
|
||
|
|
const PORT_INDEX = 12;
|
||
|
|
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('Enterprise hardening', { sanitizeResources: false, sanitizeOps: false }, () => {
|
||
|
|
it('refuses default admin credentials on persistent production storage', async () => {
|
||
|
|
const previousAllowInsecureDefaults = Deno.env.get('OBJST_ALLOW_INSECURE_DEFAULTS');
|
||
|
|
const previousAccessKey = Deno.env.get('OBJST_ACCESS_KEY');
|
||
|
|
const previousSecretKey = Deno.env.get('OBJST_SECRET_KEY');
|
||
|
|
const previousAdminPassword = Deno.env.get('OBJST_ADMIN_PASSWORD');
|
||
|
|
const previousStorageDir = Deno.env.get('OBJST_STORAGE_DIR');
|
||
|
|
try {
|
||
|
|
Deno.env.delete('OBJST_ALLOW_INSECURE_DEFAULTS');
|
||
|
|
Deno.env.delete('OBJST_ACCESS_KEY');
|
||
|
|
Deno.env.delete('OBJST_SECRET_KEY');
|
||
|
|
Deno.env.delete('OBJST_ADMIN_PASSWORD');
|
||
|
|
Deno.env.delete('OBJST_STORAGE_DIR');
|
||
|
|
const container = new ObjectStorageContainer();
|
||
|
|
await assertRejects(() => container.start(), Error, 'Refusing to start with default admin credentials');
|
||
|
|
} finally {
|
||
|
|
if (previousAllowInsecureDefaults === undefined) {
|
||
|
|
Deno.env.delete('OBJST_ALLOW_INSECURE_DEFAULTS');
|
||
|
|
} else {
|
||
|
|
Deno.env.set('OBJST_ALLOW_INSECURE_DEFAULTS', previousAllowInsecureDefaults);
|
||
|
|
}
|
||
|
|
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);
|
||
|
|
if (previousAdminPassword === undefined) Deno.env.delete('OBJST_ADMIN_PASSWORD');
|
||
|
|
else Deno.env.set('OBJST_ADMIN_PASSWORD', previousAdminPassword);
|
||
|
|
if (previousStorageDir === undefined) Deno.env.delete('OBJST_STORAGE_DIR');
|
||
|
|
else Deno.env.set('OBJST_STORAGE_DIR', previousStorageDir);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('exposes health endpoints and writes audit entries for privileged actions', async () => {
|
||
|
|
await cleanupStorageDirectory();
|
||
|
|
let container: ObjectStorageContainer | null = null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
container = createTestContainer(PORT_INDEX, { storageDirectory });
|
||
|
|
await container.start();
|
||
|
|
|
||
|
|
const live = await fetch(`http://localhost:${ports.uiPort}/livez`);
|
||
|
|
assertEquals(live.status, 200);
|
||
|
|
assertEquals((await live.json()).status, 'alive');
|
||
|
|
|
||
|
|
const ready = await fetch(`http://localhost:${ports.uiPort}/readyz`);
|
||
|
|
assertEquals(ready.status, 200);
|
||
|
|
assertEquals((await ready.json()).status, 'ready');
|
||
|
|
|
||
|
|
const health = await fetch(`http://localhost:${ports.uiPort}/healthz`);
|
||
|
|
assertEquals(health.status, 200);
|
||
|
|
assertEquals((await health.json()).ok, true);
|
||
|
|
|
||
|
|
const metrics = await fetch(`http://localhost:${ports.uiPort}/metrics`);
|
||
|
|
assertEquals(metrics.status, 200);
|
||
|
|
assertEquals((await metrics.text()).includes('objectstorage_ready 1'), true);
|
||
|
|
|
||
|
|
const identity = await loginAndGetIdentity(ports.uiPort);
|
||
|
|
const createBucket = new TypedRequest<IReq_CreateBucket>(url, 'createBucket');
|
||
|
|
await createBucket.fire({ identity, bucketName: 'enterprise-audit-bucket' });
|
||
|
|
|
||
|
|
const addCredential = new TypedRequest<IReq_AddCredential>(url, 'addCredential');
|
||
|
|
await addCredential.fire({
|
||
|
|
identity,
|
||
|
|
accessKeyId: 'enterprise-key',
|
||
|
|
secretAccessKey: 'enterprise-secret',
|
||
|
|
});
|
||
|
|
|
||
|
|
const adminConfigInfo = await Deno.stat(
|
||
|
|
`${storageDirectory}/.objectstorage/admin-config.json`,
|
||
|
|
);
|
||
|
|
if (adminConfigInfo.mode !== null) {
|
||
|
|
assertEquals(adminConfigInfo.mode & 0o777, 0o600);
|
||
|
|
}
|
||
|
|
|
||
|
|
const listAuditEntries = new TypedRequest<IReq_ListAuditEntries>(url, 'listAuditEntries');
|
||
|
|
const auditResponse = await listAuditEntries.fire({ identity, limit: 10 });
|
||
|
|
const actions = auditResponse.entries.map((entry) => entry.action);
|
||
|
|
|
||
|
|
assertEquals(actions.includes('admin.login'), true);
|
||
|
|
assertEquals(actions.includes('bucket.create'), true);
|
||
|
|
assertEquals(actions.includes('credential.add'), true);
|
||
|
|
} finally {
|
||
|
|
if (container) {
|
||
|
|
await container.stop();
|
||
|
|
}
|
||
|
|
await cleanupStorageDirectory();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('revokes admin identities on logout', async () => {
|
||
|
|
await cleanupStorageDirectory();
|
||
|
|
let container: ObjectStorageContainer | null = null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
container = createTestContainer(PORT_INDEX, { storageDirectory });
|
||
|
|
await container.start();
|
||
|
|
|
||
|
|
const identity = await loginAndGetIdentity(ports.uiPort);
|
||
|
|
const logout = new TypedRequest<IReq_AdminLogout>(url, 'adminLogout');
|
||
|
|
await logout.fire({ identity });
|
||
|
|
|
||
|
|
const verifyIdentity = new TypedRequest<IReq_VerifyIdentity>(url, 'verifyIdentity');
|
||
|
|
const verification = await verifyIdentity.fire({ identity });
|
||
|
|
assertEquals(verification.valid, false);
|
||
|
|
|
||
|
|
const createBucket = new TypedRequest<IReq_CreateBucket>(url, 'createBucket');
|
||
|
|
await assertRejects(() =>
|
||
|
|
createBucket.fire({ identity, bucketName: 'revoked-token-bucket' })
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
if (container) {
|
||
|
|
await container.stop();
|
||
|
|
}
|
||
|
|
await cleanupStorageDirectory();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|