anbaric-cloud-hosting 1.6.0 → 1.8.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/package.json +7 -5
- package/src/app-management/FargateBuildLayer.ts +4 -0
- package/src/auditing/AuditRecordStore.ts +4 -3
- package/src/auditing/InMemoryAuditRecordStore.ts +3 -2
- package/src/data-store/PostgresAuditRecordStore.ts +40 -14
- package/src/data-store/PostgresJobPersistence.ts +15 -24
- package/src/data-store/PostgresJsonStore.ts +9 -7
- package/src/data-store/Schema.ts +46 -17
- package/src/data-store/SecretsManagerSecretStore.ts +10 -7
- package/src/hosting/HostingServer.ts +11 -1
- package/src/hosting/Request.ts +5 -0
- package/src/hosting/handlers/AuditsHandler.ts +12 -9
- package/src/hosting/handlers/DocumentsHandler.ts +5 -5
- package/src/hosting/handlers/JobsHandler.ts +8 -25
- package/src/hosting/handlers/PluginsHandler.ts +52 -0
- package/src/hosting/handlers/SecretsHandler.ts +5 -5
- package/src/hosting/pages/platform-ui.html +4 -4
- package/src/index.ts +5 -0
- package/src/main.ts +6 -1
- package/src/plugins/PageDirectory.ts +35 -0
- package/src/plugins/Plugin.ts +39 -0
- package/src/plugins/PluginBundler.ts +75 -0
- package/src/plugins/PluginLoader.ts +72 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "chris@anbaric.ai",
|
|
@@ -17,15 +17,17 @@
|
|
|
17
17
|
"@aws-sdk/client-s3": "^3.1110.0",
|
|
18
18
|
"@aws-sdk/client-secrets-manager": "^3.700.0",
|
|
19
19
|
"@aws-sdk/client-servicediscovery": "^3.1110.0",
|
|
20
|
-
"anbaric-data-store": "^1.
|
|
21
|
-
"anbaric-
|
|
20
|
+
"anbaric-data-store": "^1.8.0",
|
|
21
|
+
"anbaric-plugins": "^1.8.0",
|
|
22
|
+
"anbaric-tsapi": "^1.8.0",
|
|
23
|
+
"esbuild": "^0.28.2",
|
|
22
24
|
"pg": "^8.16.0"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
27
|
"@types/node": "^26.2.0",
|
|
26
28
|
"@types/pg": "^8.15.0",
|
|
27
|
-
"anbaric-impl-cloud": "^1.
|
|
28
|
-
"anbaric-state-machine": "^1.
|
|
29
|
+
"anbaric-impl-cloud": "^1.8.0",
|
|
30
|
+
"anbaric-state-machine": "^1.8.0",
|
|
29
31
|
"tsx": "^4.20.0",
|
|
30
32
|
"typescript": "^7.0.2"
|
|
31
33
|
},
|
|
@@ -23,6 +23,8 @@ type FargateBuildLayerOptions = {
|
|
|
23
23
|
appExecutionRoleArn : string,
|
|
24
24
|
appsLogGroup : string,
|
|
25
25
|
platformUrl : string,
|
|
26
|
+
servicesUrl? : string,
|
|
27
|
+
servicesApiKey? : string,
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
type AwsClients = {
|
|
@@ -151,6 +153,8 @@ class FargateBuildLayer extends BaseBuildLayer {
|
|
|
151
153
|
{ name: "ANBARIC_AUDITOR_TYPE", value: "cloud" },
|
|
152
154
|
{ name: "ANBARIC_CONSUMER_PORT", value: String(deployment.consumerPort) },
|
|
153
155
|
{ name: "ANBARIC_CONSUMER_URL", value: `http://${deployment.appHost}:${deployment.consumerPort}` },
|
|
156
|
+
...(this.options.servicesUrl ? [{ name: "ANBARIC_SERVICES_URL", value: this.options.servicesUrl }] : []),
|
|
157
|
+
...(this.options.servicesApiKey ? [{ name: "ANBARIC_SERVICES_API_KEY", value: this.options.servicesApiKey }] : []),
|
|
154
158
|
],
|
|
155
159
|
logConfiguration: {
|
|
156
160
|
logDriver: "awslogs",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {AuditInteraction, AuditRecord} from "anbaric-tsapi";
|
|
2
2
|
|
|
3
3
|
type AuditFilter = {
|
|
4
|
-
|
|
4
|
+
resourceType? : string,
|
|
5
|
+
resourceId? : string,
|
|
5
6
|
actorId? : string,
|
|
6
|
-
|
|
7
|
+
interaction? : AuditInteraction,
|
|
7
8
|
search? : string,
|
|
8
9
|
pageSize? : number,
|
|
9
10
|
page? : number,
|
|
@@ -15,9 +15,10 @@ class InMemoryAuditRecordStore implements AuditRecordStore {
|
|
|
15
15
|
const page = filter.page ?? 0;
|
|
16
16
|
|
|
17
17
|
return this.records
|
|
18
|
-
.filter(record => !filter.
|
|
18
|
+
.filter(record => !filter.resourceType || record.resourceType === filter.resourceType)
|
|
19
|
+
.filter(record => !filter.resourceId || record.resourceId === filter.resourceId)
|
|
19
20
|
.filter(record => !filter.actorId || record.actorId === filter.actorId)
|
|
20
|
-
.filter(record => !filter.
|
|
21
|
+
.filter(record => !filter.interaction || record.interaction.includes(filter.interaction))
|
|
21
22
|
.filter(record => !filter.search ||
|
|
22
23
|
record.description.toLowerCase().includes(filter.search.toLowerCase()))
|
|
23
24
|
.reverse()
|
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import {AuditRecord} from "anbaric-tsapi";
|
|
1
|
+
import {AuditInteraction, AuditRecord} from "anbaric-tsapi";
|
|
2
2
|
import {Pool} from "pg";
|
|
3
3
|
import {AuditFilter, AuditRecordStore} from "../auditing/AuditRecordStore";
|
|
4
4
|
|
|
5
|
+
const DEFAULT_WRITE_MASK : Array<AuditInteraction> = [
|
|
6
|
+
AuditInteraction.CREATE, AuditInteraction.UPDATE_PROPERTIES, AuditInteraction.CHANGE_STATE, AuditInteraction.DELETE,
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/* Persists audit records, subject to a write mask: only the interactions in
|
|
10
|
+
the mask are stored, so high-volume reads can be audited at the call site
|
|
11
|
+
yet kept out of the durable trail. The mask defaults to every write and is
|
|
12
|
+
overridable via ANBARIC_AUDIT_INTERACTIONS. */
|
|
5
13
|
class PostgresAuditRecordStore implements AuditRecordStore {
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
private writeMask : Set<AuditInteraction>;
|
|
16
|
+
|
|
17
|
+
constructor(private pool : Pool, writeMask : Set<AuditInteraction> = PostgresAuditRecordStore.maskFromEnvironment()) {
|
|
18
|
+
this.writeMask = writeMask;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static maskFromEnvironment() : Set<AuditInteraction> {
|
|
22
|
+
const configured = process.env.ANBARIC_AUDIT_INTERACTIONS;
|
|
23
|
+
if (!configured) return new Set(DEFAULT_WRITE_MASK);
|
|
24
|
+
return new Set(configured.split(",").map(entry => entry.trim()).filter(Boolean) as Array<AuditInteraction>);
|
|
25
|
+
}
|
|
8
26
|
|
|
9
27
|
async save(record : AuditRecord) : Promise<void> {
|
|
28
|
+
if (!record.interaction.some(interaction => this.writeMask.has(interaction))) return;
|
|
29
|
+
|
|
10
30
|
await this.pool.query(
|
|
11
|
-
`INSERT INTO anbaric_system.audit_records
|
|
12
|
-
|
|
13
|
-
|
|
31
|
+
`INSERT INTO anbaric_system.audit_records
|
|
32
|
+
(resource_type, resource_id, actor_id, actor_type, interaction, description, details)
|
|
33
|
+
VALUES ($1, $2, $3, $4, $5::anbaric_system.audit_interaction[], $6, $7)`,
|
|
34
|
+
[record.resourceType, record.resourceId, record.actorId, record.actorType, record.interaction,
|
|
14
35
|
record.description, JSON.stringify(record.details ?? null)],
|
|
15
36
|
);
|
|
16
37
|
}
|
|
@@ -19,17 +40,21 @@ class PostgresAuditRecordStore implements AuditRecordStore {
|
|
|
19
40
|
const conditions : Array<string> = [];
|
|
20
41
|
const parameters : Array<any> = [];
|
|
21
42
|
|
|
22
|
-
if (filter.
|
|
23
|
-
parameters.push(filter.
|
|
24
|
-
conditions.push(`
|
|
43
|
+
if (filter.resourceType) {
|
|
44
|
+
parameters.push(filter.resourceType);
|
|
45
|
+
conditions.push(`resource_type = $${parameters.length}`);
|
|
46
|
+
}
|
|
47
|
+
if (filter.resourceId) {
|
|
48
|
+
parameters.push(filter.resourceId);
|
|
49
|
+
conditions.push(`resource_id = $${parameters.length}`);
|
|
25
50
|
}
|
|
26
51
|
if (filter.actorId) {
|
|
27
52
|
parameters.push(filter.actorId);
|
|
28
53
|
conditions.push(`actor_id = $${parameters.length}`);
|
|
29
54
|
}
|
|
30
|
-
if (filter.
|
|
31
|
-
parameters.push(filter.
|
|
32
|
-
conditions.push(
|
|
55
|
+
if (filter.interaction) {
|
|
56
|
+
parameters.push(filter.interaction);
|
|
57
|
+
conditions.push(`$${parameters.length}::anbaric_system.audit_interaction = ANY(interaction)`);
|
|
33
58
|
}
|
|
34
59
|
if (filter.search) {
|
|
35
60
|
parameters.push(`%${filter.search}%`);
|
|
@@ -43,7 +68,7 @@ class PostgresAuditRecordStore implements AuditRecordStore {
|
|
|
43
68
|
const offset = `OFFSET $${parameters.length}`;
|
|
44
69
|
|
|
45
70
|
const result = await this.pool.query(
|
|
46
|
-
`SELECT id,
|
|
71
|
+
`SELECT id, resource_type, resource_id, actor_id, actor_type, interaction, description, details, at
|
|
47
72
|
FROM anbaric_system.audit_records ${where}
|
|
48
73
|
ORDER BY at DESC, id DESC ${limit} ${offset}`,
|
|
49
74
|
parameters,
|
|
@@ -51,10 +76,11 @@ class PostgresAuditRecordStore implements AuditRecordStore {
|
|
|
51
76
|
|
|
52
77
|
return result.rows.map(row => ({
|
|
53
78
|
id: String(row.id),
|
|
54
|
-
|
|
79
|
+
resourceType: row.resource_type,
|
|
80
|
+
resourceId: row.resource_id,
|
|
55
81
|
actorId: row.actor_id,
|
|
56
82
|
actorType: row.actor_type,
|
|
57
|
-
|
|
83
|
+
interaction: row.interaction,
|
|
58
84
|
description: row.description,
|
|
59
85
|
details: row.details,
|
|
60
86
|
at: row.at.toISOString(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Job, JobPersistence,
|
|
1
|
+
import {Auditor, Job, JobPersistence, NoOpAuditor, deserializeJob} from "anbaric-tsapi";
|
|
2
2
|
import {Pool} from "pg";
|
|
3
3
|
|
|
4
4
|
type JobRow = {
|
|
@@ -9,28 +9,28 @@ type JobRow = {
|
|
|
9
9
|
started_at : Date,
|
|
10
10
|
started_by : string,
|
|
11
11
|
last_updated : Date,
|
|
12
|
-
transitions : Array<JobTransition>,
|
|
13
12
|
};
|
|
14
13
|
|
|
15
|
-
const JOB_COLUMNS = "id, state, properties, workflow_id, started_at, started_by, last_updated
|
|
14
|
+
const JOB_COLUMNS = "id, state, properties, workflow_id, started_at, started_by, last_updated";
|
|
16
15
|
|
|
17
|
-
class PostgresJobPersistence
|
|
16
|
+
class PostgresJobPersistence extends JobPersistence {
|
|
18
17
|
|
|
19
|
-
constructor(private pool : Pool) {
|
|
18
|
+
constructor(private pool : Pool, auditor : Auditor = new NoOpAuditor()) {
|
|
19
|
+
super(auditor);
|
|
20
|
+
}
|
|
20
21
|
|
|
21
|
-
async
|
|
22
|
+
protected async saveInternal(job : Job) : Promise<void> {
|
|
22
23
|
await this.pool.query(
|
|
23
|
-
`INSERT INTO jobs (id, state, properties, workflow_id, started_at, started_by, last_updated
|
|
24
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7
|
|
24
|
+
`INSERT INTO jobs (id, state, properties, workflow_id, started_at, started_by, last_updated)
|
|
25
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
25
26
|
ON CONFLICT (id) DO UPDATE SET state = EXCLUDED.state, properties = EXCLUDED.properties,
|
|
26
|
-
workflow_id = EXCLUDED.workflow_id, last_updated = EXCLUDED.last_updated
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
job.startedAt, job.startedBy, job.lastUpdated, JSON.stringify(job.transitions)],
|
|
27
|
+
workflow_id = EXCLUDED.workflow_id, last_updated = EXCLUDED.last_updated`,
|
|
28
|
+
[job.id, job.state, Object.fromEntries(job.properties), job.workflowId,
|
|
29
|
+
job.startedAt, job.startedBy, job.lastUpdated],
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
async
|
|
33
|
+
protected async retrieveInternal(id : string) : Promise<Job> {
|
|
34
34
|
const result = await this.pool.query(
|
|
35
35
|
`SELECT ${JOB_COLUMNS} FROM jobs WHERE id = $1`,
|
|
36
36
|
[id],
|
|
@@ -40,11 +40,11 @@ class PostgresJobPersistence implements JobPersistence {
|
|
|
40
40
|
return this.deserializeRow(result.rows[0]);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async
|
|
43
|
+
protected async deleteInternal(id : string) : Promise<void> {
|
|
44
44
|
await this.pool.query("DELETE FROM jobs WHERE id = $1", [id]);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async
|
|
47
|
+
protected async listInternal(pageSize : number = 100, page : number = 0) : Promise<Array<Job>> {
|
|
48
48
|
const result = await this.pool.query(
|
|
49
49
|
`SELECT ${JOB_COLUMNS} FROM jobs ORDER BY inserted_at LIMIT $1 OFFSET $2`,
|
|
50
50
|
[pageSize, page * pageSize],
|
|
@@ -60,18 +60,9 @@ class PostgresJobPersistence implements JobPersistence {
|
|
|
60
60
|
startedAt: row.started_at.toISOString(),
|
|
61
61
|
startedBy: row.started_by,
|
|
62
62
|
lastUpdated: row.last_updated.toISOString(),
|
|
63
|
-
transitions: row.transitions,
|
|
64
63
|
});
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
async updateProperties(id : string, properties : Map<string, any>) : Promise<void> {
|
|
68
|
-
const result = await this.pool.query(
|
|
69
|
-
"UPDATE jobs SET properties = properties || $2::jsonb, last_updated = now() WHERE id = $1",
|
|
70
|
-
[id, Object.fromEntries(properties)],
|
|
71
|
-
);
|
|
72
|
-
if (result.rowCount === 0) throw new Error(`No job found with id "${id}"`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
66
|
}
|
|
76
67
|
|
|
77
68
|
export { PostgresJobPersistence }
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {JsonStore} from "anbaric-tsapi";
|
|
1
|
+
import {Auditor, JsonStore, NoOpAuditor} from "anbaric-tsapi";
|
|
2
2
|
import {Pool} from "pg";
|
|
3
3
|
|
|
4
|
-
class PostgresJsonStore
|
|
4
|
+
class PostgresJsonStore extends JsonStore {
|
|
5
5
|
|
|
6
|
-
constructor(private pool : Pool,
|
|
6
|
+
constructor(private pool : Pool, collection : string, auditor : Auditor = new NoOpAuditor()) {
|
|
7
|
+
super(auditor, collection);
|
|
8
|
+
}
|
|
7
9
|
|
|
8
|
-
async
|
|
10
|
+
protected async saveInternal(id : string, document : any) : Promise<void> {
|
|
9
11
|
await this.pool.query(
|
|
10
12
|
`INSERT INTO documents (collection, id, document) VALUES ($1, $2, $3)
|
|
11
13
|
ON CONFLICT (collection, id) DO UPDATE SET document = EXCLUDED.document`,
|
|
@@ -13,7 +15,7 @@ class PostgresJsonStore implements JsonStore {
|
|
|
13
15
|
);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
async
|
|
18
|
+
protected async retrieveInternal(id : string) : Promise<any> {
|
|
17
19
|
const result = await this.pool.query(
|
|
18
20
|
"SELECT document FROM documents WHERE collection = $1 AND id = $2",
|
|
19
21
|
[this.collection, id],
|
|
@@ -23,11 +25,11 @@ class PostgresJsonStore implements JsonStore {
|
|
|
23
25
|
return result.rows[0].document;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
async
|
|
28
|
+
protected async deleteInternal(id : string) : Promise<void> {
|
|
27
29
|
await this.pool.query("DELETE FROM documents WHERE collection = $1 AND id = $2", [this.collection, id]);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
async
|
|
32
|
+
protected async listInternal(pageSize : number = 100, page : number = 0) : Promise<Array<any>> {
|
|
31
33
|
const result = await this.pool.query(
|
|
32
34
|
"SELECT document FROM documents WHERE collection = $1 ORDER BY inserted_at LIMIT $2 OFFSET $3",
|
|
33
35
|
[this.collection, pageSize, page * pageSize],
|
package/src/data-store/Schema.ts
CHANGED
|
@@ -54,30 +54,59 @@ const ensureSchema = async (pool : Pool) : Promise<void> => {
|
|
|
54
54
|
await pool.query("ALTER TABLE anbaric_system.cli_keys ADD COLUMN IF NOT EXISTS tenant TEXT");
|
|
55
55
|
await pool.query(`
|
|
56
56
|
DO $$ BEGIN
|
|
57
|
-
CREATE TYPE anbaric_system.
|
|
57
|
+
CREATE TYPE anbaric_system.audit_interaction AS ENUM
|
|
58
|
+
('CREATE', 'UPDATE_PROPERTIES', 'CHANGE_STATE', 'DELETE', 'READ', 'LIST');
|
|
58
59
|
EXCEPTION WHEN duplicate_object THEN null;
|
|
59
60
|
END $$
|
|
60
61
|
`);
|
|
61
62
|
await pool.query(`
|
|
62
63
|
CREATE TABLE IF NOT EXISTS anbaric_system.audit_records (
|
|
63
|
-
id
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
65
|
+
resource_type TEXT NOT NULL,
|
|
66
|
+
resource_id TEXT NOT NULL,
|
|
67
|
+
actor_id TEXT NOT NULL,
|
|
68
|
+
actor_type TEXT NOT NULL,
|
|
69
|
+
interaction anbaric_system.audit_interaction[] NOT NULL,
|
|
70
|
+
description TEXT NOT NULL,
|
|
71
|
+
details JSONB,
|
|
72
|
+
at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
71
73
|
)
|
|
72
74
|
`);
|
|
73
|
-
await pool.query("ALTER TABLE anbaric_system.audit_records ADD COLUMN IF NOT EXISTS
|
|
74
|
-
await pool.query("
|
|
75
|
-
await pool.query("
|
|
76
|
-
await pool.query(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
await pool.query("ALTER TABLE anbaric_system.audit_records ADD COLUMN IF NOT EXISTS resource_type TEXT");
|
|
76
|
+
await pool.query("ALTER TABLE anbaric_system.audit_records ADD COLUMN IF NOT EXISTS resource_id TEXT");
|
|
77
|
+
await pool.query("ALTER TABLE anbaric_system.audit_records ADD COLUMN IF NOT EXISTS interaction anbaric_system.audit_interaction[]");
|
|
78
|
+
await pool.query(`
|
|
79
|
+
DO $$ BEGIN
|
|
80
|
+
IF EXISTS (SELECT 1 FROM information_schema.columns
|
|
81
|
+
WHERE table_schema = 'anbaric_system' AND table_name = 'audit_records'
|
|
82
|
+
AND column_name = 'interaction' AND data_type <> 'ARRAY') THEN
|
|
83
|
+
ALTER TABLE anbaric_system.audit_records
|
|
84
|
+
ALTER COLUMN interaction TYPE anbaric_system.audit_interaction[] USING ARRAY[interaction];
|
|
85
|
+
END IF;
|
|
86
|
+
END $$
|
|
87
|
+
`);
|
|
88
|
+
await pool.query(`
|
|
89
|
+
DO $$ BEGIN
|
|
90
|
+
IF EXISTS (SELECT 1 FROM information_schema.columns
|
|
91
|
+
WHERE table_schema = 'anbaric_system' AND table_name = 'audit_records'
|
|
92
|
+
AND column_name = 'job_id') THEN
|
|
93
|
+
UPDATE anbaric_system.audit_records
|
|
94
|
+
SET resource_type = COALESCE(resource_type, 'job'),
|
|
95
|
+
resource_id = COALESCE(resource_id, job_id),
|
|
96
|
+
interaction = COALESCE(interaction, ARRAY[change::text::anbaric_system.audit_interaction])
|
|
97
|
+
WHERE resource_id IS NULL OR interaction IS NULL;
|
|
98
|
+
ALTER TABLE anbaric_system.audit_records DROP COLUMN job_id;
|
|
99
|
+
ALTER TABLE anbaric_system.audit_records DROP COLUMN change;
|
|
100
|
+
END IF;
|
|
101
|
+
END $$
|
|
102
|
+
`);
|
|
103
|
+
await pool.query("UPDATE anbaric_system.audit_records SET resource_type = 'job' WHERE resource_type IS NULL");
|
|
104
|
+
await pool.query("UPDATE anbaric_system.audit_records SET resource_id = 'unknown' WHERE resource_id IS NULL");
|
|
105
|
+
await pool.query("UPDATE anbaric_system.audit_records SET interaction = ARRAY['UPDATE_PROPERTIES']::anbaric_system.audit_interaction[] WHERE interaction IS NULL");
|
|
106
|
+
await pool.query("ALTER TABLE anbaric_system.audit_records ALTER COLUMN resource_type SET NOT NULL");
|
|
107
|
+
await pool.query("ALTER TABLE anbaric_system.audit_records ALTER COLUMN resource_id SET NOT NULL");
|
|
108
|
+
await pool.query("ALTER TABLE anbaric_system.audit_records ALTER COLUMN interaction SET NOT NULL");
|
|
109
|
+
await pool.query("CREATE INDEX IF NOT EXISTS audit_records_resource ON anbaric_system.audit_records (resource_type, resource_id)");
|
|
81
110
|
};
|
|
82
111
|
|
|
83
112
|
export { ensureSchema }
|
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
ResourceNotFoundException,
|
|
9
9
|
SecretsManagerClient,
|
|
10
10
|
} from "@aws-sdk/client-secrets-manager";
|
|
11
|
-
import {SecretStore} from "anbaric-tsapi";
|
|
11
|
+
import {Auditor, NoOpAuditor, SecretStore} from "anbaric-tsapi";
|
|
12
12
|
|
|
13
|
-
class SecretsManagerSecretStore
|
|
13
|
+
class SecretsManagerSecretStore extends SecretStore {
|
|
14
14
|
|
|
15
|
-
constructor(private client : SecretsManagerClient, private prefix : string = "anbaric/"
|
|
15
|
+
constructor(private client : SecretsManagerClient, private prefix : string = "anbaric/",
|
|
16
|
+
auditor : Auditor = new NoOpAuditor()) {
|
|
17
|
+
super(auditor);
|
|
18
|
+
}
|
|
16
19
|
|
|
17
|
-
async
|
|
20
|
+
protected async saveInternal(name : string, value : string) : Promise<void> {
|
|
18
21
|
try {
|
|
19
22
|
await this.client.send(new CreateSecretCommand({ Name: this.prefix + name, SecretString: value }));
|
|
20
23
|
} catch (error) {
|
|
@@ -23,7 +26,7 @@ class SecretsManagerSecretStore implements SecretStore {
|
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
async
|
|
29
|
+
protected async retrieveInternal(name : string) : Promise<string> {
|
|
27
30
|
try {
|
|
28
31
|
const secret = await this.client.send(new GetSecretValueCommand({ SecretId: this.prefix + name }));
|
|
29
32
|
return secret.SecretString ?? "";
|
|
@@ -33,7 +36,7 @@ class SecretsManagerSecretStore implements SecretStore {
|
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
async
|
|
39
|
+
protected async deleteInternal(name : string) : Promise<void> {
|
|
37
40
|
try {
|
|
38
41
|
await this.client.send(new DeleteSecretCommand({ SecretId: this.prefix + name, ForceDeleteWithoutRecovery: true }));
|
|
39
42
|
} catch (error) {
|
|
@@ -41,7 +44,7 @@ class SecretsManagerSecretStore implements SecretStore {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
async
|
|
47
|
+
protected async listInternal() : Promise<Array<string>> {
|
|
45
48
|
const result = await this.client.send(new ListSecretsCommand({
|
|
46
49
|
Filters: [{ Key: "name", Values: [this.prefix] }],
|
|
47
50
|
}));
|
|
@@ -20,8 +20,11 @@ import {PingHandler} from "./handlers/PingHandler";
|
|
|
20
20
|
import {QueueHandler} from "./handlers/QueueHandler";
|
|
21
21
|
import {SecretsHandler} from "./handlers/SecretsHandler";
|
|
22
22
|
import {StateMachinesHandler} from "./handlers/StateMachinesHandler";
|
|
23
|
+
import {PluginsHandler} from "./handlers/PluginsHandler";
|
|
23
24
|
import {AuthenticationMiddleware} from "./middleware/AuthenticationMiddleware";
|
|
24
25
|
import {SessionMiddleware} from "./middleware/SessionMiddleware";
|
|
26
|
+
import {PageDirectory} from "../plugins/PageDirectory";
|
|
27
|
+
import {LoadedPlugin} from "../plugins/Plugin";
|
|
25
28
|
import {Request} from "./Request";
|
|
26
29
|
import {Router} from "./Router";
|
|
27
30
|
import {Server} from "./Server";
|
|
@@ -47,7 +50,8 @@ class HostingServer {
|
|
|
47
50
|
cliAuthorizer? : CliAuthorizer,
|
|
48
51
|
tokenAuthenticator? : TokenAuthenticator,
|
|
49
52
|
tenant? : string,
|
|
50
|
-
auditRecords? : AuditRecordStore
|
|
53
|
+
auditRecords? : AuditRecordStore,
|
|
54
|
+
plugins : Array<LoadedPlugin> = []) {
|
|
51
55
|
const pages = new PagesHandler();
|
|
52
56
|
const ping = new PingHandler(tenant);
|
|
53
57
|
const jobs = new JobsHandler(persistence);
|
|
@@ -60,6 +64,12 @@ class HostingServer {
|
|
|
60
64
|
|
|
61
65
|
const publicRouter = new Router();
|
|
62
66
|
publicRouter.registerRoot(pages);
|
|
67
|
+
if (plugins.length > 0) {
|
|
68
|
+
for (const segment of new PageDirectory(plugins).topLevelSegments) {
|
|
69
|
+
publicRouter.register(segment, pages);
|
|
70
|
+
}
|
|
71
|
+
publicRouter.register("plugins", new PluginsHandler(plugins));
|
|
72
|
+
}
|
|
63
73
|
publicRouter.register("ping", ping);
|
|
64
74
|
publicRouter.register("audit", pages);
|
|
65
75
|
publicRouter.register("whoami", new WhoamiHandler());
|
package/src/hosting/Request.ts
CHANGED
|
@@ -94,6 +94,11 @@ class Request {
|
|
|
94
94
|
this.response.end(page);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
replyJavaScript(script : string) : void {
|
|
98
|
+
this.response.writeHead(200, { "content-type": "text/javascript" });
|
|
99
|
+
this.response.end(script);
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
notFound() : void {
|
|
98
103
|
this.reply(404, { error: "Not found" });
|
|
99
104
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {AuditInteraction} from "anbaric-tsapi";
|
|
2
2
|
import {AuditRecordStore} from "../../auditing/AuditRecordStore";
|
|
3
3
|
import {Request} from "../Request";
|
|
4
4
|
import {RequestHandler} from "../RequestHandler";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const AUDIT_INTERACTIONS = new Set(["CREATE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE", "READ", "LIST"]);
|
|
7
7
|
|
|
8
8
|
class AuditsHandler implements RequestHandler {
|
|
9
9
|
|
|
@@ -23,21 +23,24 @@ class AuditsHandler implements RequestHandler {
|
|
|
23
23
|
|
|
24
24
|
private async handleRecord(request : Request) : Promise<void> {
|
|
25
25
|
const record = await request.body();
|
|
26
|
-
if (typeof record?.
|
|
27
|
-
|| typeof record?.
|
|
28
|
-
||
|
|
29
|
-
|
|
26
|
+
if (typeof record?.resourceType !== "string" || typeof record?.resourceId !== "string"
|
|
27
|
+
|| typeof record?.description !== "string" || typeof record?.actorId !== "string"
|
|
28
|
+
|| typeof record?.actorType !== "string"
|
|
29
|
+
|| !Array.isArray(record?.interaction) || record.interaction.length === 0
|
|
30
|
+
|| !record.interaction.every((interaction : unknown) => typeof interaction === "string" && AUDIT_INTERACTIONS.has(interaction))) {
|
|
31
|
+
return request.reply(400, { error: "Expected a body of { resourceType, resourceId, actorId, actorType, interaction: [...], description, ... }" });
|
|
30
32
|
}
|
|
31
33
|
await this.auditRecords.save(record);
|
|
32
34
|
request.reply(204);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
private async handleQuery(request : Request) : Promise<void> {
|
|
36
|
-
const
|
|
38
|
+
const interaction = request.query("interaction");
|
|
37
39
|
const records = await this.auditRecords.list({
|
|
38
|
-
|
|
40
|
+
resourceType: request.query("resourceType"),
|
|
41
|
+
resourceId: request.query("resourceId"),
|
|
39
42
|
actorId: request.query("actorId"),
|
|
40
|
-
|
|
43
|
+
interaction: interaction && AUDIT_INTERACTIONS.has(interaction) ? interaction as AuditInteraction : undefined,
|
|
41
44
|
search: request.query("search"),
|
|
42
45
|
pageSize: request.query("pageSize") === undefined ? undefined : Number(request.query("pageSize")),
|
|
43
46
|
page: request.query("page") === undefined ? undefined : Number(request.query("page")),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {JsonStore} from "anbaric-tsapi";
|
|
1
|
+
import {JsonStore, SystemActor} from "anbaric-tsapi";
|
|
2
2
|
import {Request} from "../Request";
|
|
3
3
|
import {RequestHandler} from "../RequestHandler";
|
|
4
4
|
|
|
@@ -17,12 +17,12 @@ class DocumentsHandler implements RequestHandler {
|
|
|
17
17
|
private async handleDocument(request : Request, store : JsonStore, documentId : string) : Promise<void> {
|
|
18
18
|
switch (request.method) {
|
|
19
19
|
case "PUT":
|
|
20
|
-
await store.
|
|
20
|
+
await store.create(SystemActor.actor, documentId, await request.body());
|
|
21
21
|
return request.reply(204);
|
|
22
22
|
case "GET":
|
|
23
|
-
return request.reply(200, await store.retrieve(documentId));
|
|
23
|
+
return request.reply(200, await store.retrieve(documentId, SystemActor.actor));
|
|
24
24
|
case "DELETE":
|
|
25
|
-
await store.delete(documentId);
|
|
25
|
+
await store.delete(documentId, SystemActor.actor);
|
|
26
26
|
return request.reply(204);
|
|
27
27
|
}
|
|
28
28
|
request.notFound();
|
|
@@ -33,7 +33,7 @@ class DocumentsHandler implements RequestHandler {
|
|
|
33
33
|
case "GET": {
|
|
34
34
|
const pageSize = Number(request.query("pageSize") ?? 100);
|
|
35
35
|
const page = Number(request.query("page") ?? 0);
|
|
36
|
-
return request.reply(200, await store.list(pageSize, page));
|
|
36
|
+
return request.reply(200, await store.list(SystemActor.actor, pageSize, page));
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
request.notFound();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {JobPersistence, deserializeJob, serializeJob} from "anbaric-tsapi";
|
|
1
|
+
import {JobPersistence, SystemActor, deserializeJob, serializeJob} from "anbaric-tsapi";
|
|
2
2
|
import {Request} from "../Request";
|
|
3
3
|
import {RequestHandler} from "../RequestHandler";
|
|
4
4
|
|
|
@@ -7,37 +7,20 @@ class JobsHandler implements RequestHandler {
|
|
|
7
7
|
constructor(private persistence : JobPersistence) {}
|
|
8
8
|
|
|
9
9
|
async handle(request : Request) : Promise<void> {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
break;
|
|
14
|
-
case undefined:
|
|
15
|
-
if (request.id) return this.handleJob(request, request.id);
|
|
16
|
-
return this.handleCollection(request);
|
|
17
|
-
}
|
|
18
|
-
request.notFound();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
private async handleProperties(request : Request, id : string) : Promise<void> {
|
|
22
|
-
switch (request.method) {
|
|
23
|
-
case "PATCH": {
|
|
24
|
-
const properties = new Map<string, any>(Object.entries(await request.body()));
|
|
25
|
-
await this.persistence.updateProperties(id, properties);
|
|
26
|
-
return request.reply(204);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
request.notFound();
|
|
10
|
+
if (request.subresource) return request.notFound();
|
|
11
|
+
if (request.id) return this.handleJob(request, request.id);
|
|
12
|
+
return this.handleCollection(request);
|
|
30
13
|
}
|
|
31
14
|
|
|
32
15
|
private async handleJob(request : Request, id : string) : Promise<void> {
|
|
33
16
|
switch (request.method) {
|
|
34
17
|
case "PUT":
|
|
35
|
-
await this.persistence.
|
|
18
|
+
await this.persistence.create(SystemActor.actor, deserializeJob(await request.body()));
|
|
36
19
|
return request.reply(204);
|
|
37
20
|
case "GET":
|
|
38
|
-
return request.reply(200, serializeJob(await this.persistence.retrieve(id)));
|
|
21
|
+
return request.reply(200, serializeJob(await this.persistence.retrieve(id, SystemActor.actor)));
|
|
39
22
|
case "DELETE":
|
|
40
|
-
await this.persistence.delete(id);
|
|
23
|
+
await this.persistence.delete(id, SystemActor.actor);
|
|
41
24
|
return request.reply(204);
|
|
42
25
|
}
|
|
43
26
|
request.notFound();
|
|
@@ -48,7 +31,7 @@ class JobsHandler implements RequestHandler {
|
|
|
48
31
|
case "GET": {
|
|
49
32
|
const pageSize = Number(request.query("pageSize") ?? 100);
|
|
50
33
|
const page = Number(request.query("page") ?? 0);
|
|
51
|
-
const jobs = await this.persistence.list(pageSize, page);
|
|
34
|
+
const jobs = await this.persistence.list(SystemActor.actor, pageSize, page);
|
|
52
35
|
return request.reply(200, jobs.map(serializeJob));
|
|
53
36
|
}
|
|
54
37
|
}
|