anbaric-cloud-hosting 1.32.0 → 1.33.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 -7
- package/src/app-management/DockerBuildLayer.ts +1 -0
- package/src/app-management/FargateBuildLayer.ts +1 -0
- package/src/data-store/EntitlementStore.ts +41 -0
- package/src/data-store/InMemoryEntitlementStore.ts +55 -0
- package/src/data-store/PostgresEntitlementStore.ts +81 -0
- package/src/data-store/Schema.ts +32 -0
- package/src/hosting/HostingServer.ts +7 -1
- package/src/hosting/handlers/EntitlementsAdminHandler.ts +52 -0
- package/src/hosting/handlers/EntitlementsHandler.ts +39 -0
- package/src/index.ts +5 -0
- package/src/main.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.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",
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
"@aws-sdk/client-s3": "^3.1110.0",
|
|
19
19
|
"@aws-sdk/client-secrets-manager": "^3.700.0",
|
|
20
20
|
"@aws-sdk/client-servicediscovery": "^3.1110.0",
|
|
21
|
-
"anbaric-data-store": "^1.
|
|
22
|
-
"anbaric-plugins": "^1.
|
|
23
|
-
"anbaric-tsapi": "^1.
|
|
24
|
-
"anbaric-web": "^1.
|
|
21
|
+
"anbaric-data-store": "^1.33.0",
|
|
22
|
+
"anbaric-plugins": "^1.33.0",
|
|
23
|
+
"anbaric-tsapi": "^1.33.0",
|
|
24
|
+
"anbaric-web": "^1.33.0",
|
|
25
25
|
"esbuild": "^0.28.2",
|
|
26
26
|
"pg": "^8.16.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^26.2.0",
|
|
30
30
|
"@types/pg": "^8.15.0",
|
|
31
|
-
"anbaric-impl-cloud": "^1.
|
|
32
|
-
"anbaric-state-machine": "^1.
|
|
31
|
+
"anbaric-impl-cloud": "^1.33.0",
|
|
32
|
+
"anbaric-state-machine": "^1.33.0",
|
|
33
33
|
"tsx": "^4.20.0",
|
|
34
34
|
"typescript": "^7.0.2"
|
|
35
35
|
},
|
|
@@ -107,6 +107,7 @@ class DockerBuildLayer extends BaseBuildLayer {
|
|
|
107
107
|
"--env", "ANBARIC_SECRET_STORE_TYPE=cloud",
|
|
108
108
|
"--env", "ANBARIC_AUDITOR_TYPE=cloud",
|
|
109
109
|
"--env", "ANBARIC_SESSION_RESOLVER_TYPE=cloud",
|
|
110
|
+
"--env", "ANBARIC_ENTITLEMENTS_TYPE=cloud",
|
|
110
111
|
"--env", "ANBARIC_NOTIFIER_TYPE=cloud",
|
|
111
112
|
"--env", `ANBARIC_CONSUMER_PORT=${deployment.consumerPort}`,
|
|
112
113
|
"--env", `ANBARIC_CONSUMER_URL=http://${container}:${deployment.consumerPort}`,
|
|
@@ -197,6 +197,7 @@ class FargateBuildLayer extends BaseBuildLayer {
|
|
|
197
197
|
{ name: "ANBARIC_SECRET_STORE_TYPE", value: "cloud" },
|
|
198
198
|
{ name: "ANBARIC_AUDITOR_TYPE", value: "cloud" },
|
|
199
199
|
{ name: "ANBARIC_SESSION_RESOLVER_TYPE", value: "cloud" },
|
|
200
|
+
{ name: "ANBARIC_ENTITLEMENTS_TYPE", value: "cloud" },
|
|
200
201
|
{ name: "ANBARIC_NOTIFIER_TYPE", value: "cloud" },
|
|
201
202
|
{ name: "ANBARIC_CONSUMER_PORT", value: String(deployment.consumerPort) },
|
|
202
203
|
{ name: "ANBARIC_CONSUMER_URL", value: `http://${deployment.appHost}:${deployment.consumerPort}` },
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {Entitlements} from "anbaric-tsapi";
|
|
2
|
+
|
|
3
|
+
/* An appId of null means global: a global definition is one no app owns
|
|
4
|
+
(console-created), a global grant satisfies the check from any app. */
|
|
5
|
+
type EntitlementDefinition = {
|
|
6
|
+
|
|
7
|
+
appId : string | null,
|
|
8
|
+
entitlementId : string,
|
|
9
|
+
notes : string,
|
|
10
|
+
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type EntitlementGrant = {
|
|
14
|
+
|
|
15
|
+
id : string,
|
|
16
|
+
userId : string,
|
|
17
|
+
appId : string | null,
|
|
18
|
+
entitlementId : string,
|
|
19
|
+
notes : string,
|
|
20
|
+
grantedAt : string,
|
|
21
|
+
grantedBy : string,
|
|
22
|
+
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/* The platform's view of entitlements: what apps can do (register, has) plus
|
|
26
|
+
the management the console performs on their behalf. */
|
|
27
|
+
interface EntitlementStore extends Entitlements {
|
|
28
|
+
|
|
29
|
+
defineGlobal(entitlementId : string, notes : string) : Promise<void>;
|
|
30
|
+
|
|
31
|
+
listDefinitions() : Promise<Array<EntitlementDefinition>>;
|
|
32
|
+
|
|
33
|
+
grant(userId : string, appId : string | null, entitlementId : string, notes : string, grantedBy : string) : Promise<EntitlementGrant>;
|
|
34
|
+
|
|
35
|
+
revoke(grantId : string) : Promise<boolean>;
|
|
36
|
+
|
|
37
|
+
listGrants(userId? : string) : Promise<Array<EntitlementGrant>>;
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type { EntitlementStore, EntitlementDefinition, EntitlementGrant }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {randomUUID} from "node:crypto";
|
|
2
|
+
import {EntitlementDefinition, EntitlementGrant, EntitlementStore} from "./EntitlementStore";
|
|
3
|
+
|
|
4
|
+
class InMemoryEntitlementStore implements EntitlementStore {
|
|
5
|
+
|
|
6
|
+
private definitions = new Map<string, EntitlementDefinition>();
|
|
7
|
+
private grants = new Map<string, EntitlementGrant>();
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.definitions.set(this.key(null, "access"), { appId: null, entitlementId: "access", notes: "Global access" });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async register(appId : string, entitlementId : string, notes : string = "") : Promise<void> {
|
|
14
|
+
this.definitions.set(this.key(appId, entitlementId), { appId, entitlementId, notes });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async has(appId : string, userId : string, entitlementId : string) : Promise<boolean> {
|
|
18
|
+
return Array.from(this.grants.values()).some(grant =>
|
|
19
|
+
grant.userId === userId && grant.entitlementId === entitlementId &&
|
|
20
|
+
(grant.appId === null || grant.appId === appId));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async defineGlobal(entitlementId : string, notes : string) : Promise<void> {
|
|
24
|
+
this.definitions.set(this.key(null, entitlementId), { appId: null, entitlementId, notes });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async listDefinitions() : Promise<Array<EntitlementDefinition>> {
|
|
28
|
+
return Array.from(this.definitions.values()).map(definition => ({ ...definition }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async grant(userId : string, appId : string | null, entitlementId : string, notes : string, grantedBy : string) : Promise<EntitlementGrant> {
|
|
32
|
+
const grant : EntitlementGrant = {
|
|
33
|
+
id: randomUUID(), userId, appId, entitlementId, notes, grantedBy, grantedAt: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
this.grants.set(grant.id, grant);
|
|
36
|
+
return { ...grant };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async revoke(grantId : string) : Promise<boolean> {
|
|
40
|
+
return this.grants.delete(grantId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async listGrants(userId? : string) : Promise<Array<EntitlementGrant>> {
|
|
44
|
+
return Array.from(this.grants.values())
|
|
45
|
+
.filter(grant => userId === undefined || grant.userId === userId)
|
|
46
|
+
.map(grant => ({ ...grant }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private key(appId : string | null, entitlementId : string) : string {
|
|
50
|
+
return `${appId ?? ""}/${entitlementId}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { InMemoryEntitlementStore }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {randomUUID} from "node:crypto";
|
|
2
|
+
import {Pool} from "pg";
|
|
3
|
+
import {EntitlementDefinition, EntitlementGrant, EntitlementStore} from "./EntitlementStore";
|
|
4
|
+
|
|
5
|
+
class PostgresEntitlementStore implements EntitlementStore {
|
|
6
|
+
|
|
7
|
+
constructor(private pool : Pool) {}
|
|
8
|
+
|
|
9
|
+
async register(appId : string, entitlementId : string, notes : string = "") : Promise<void> {
|
|
10
|
+
await this.upsertDefinition(appId, entitlementId, notes);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async has(appId : string, userId : string, entitlementId : string) : Promise<boolean> {
|
|
14
|
+
const result = await this.pool.query(
|
|
15
|
+
`SELECT 1 FROM entitlement_grants
|
|
16
|
+
WHERE user_id = $1 AND entitlement_id = $2 AND (app_id = $3 OR app_id IS NULL)
|
|
17
|
+
LIMIT 1`,
|
|
18
|
+
[userId, entitlementId, appId]);
|
|
19
|
+
return (result.rowCount ?? 0) > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async defineGlobal(entitlementId : string, notes : string) : Promise<void> {
|
|
23
|
+
await this.upsertDefinition(null, entitlementId, notes);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async listDefinitions() : Promise<Array<EntitlementDefinition>> {
|
|
27
|
+
const result = await this.pool.query(
|
|
28
|
+
`SELECT app_id, entitlement_id, notes FROM entitlements
|
|
29
|
+
ORDER BY app_id NULLS FIRST, entitlement_id`);
|
|
30
|
+
return result.rows.map(row => ({ appId: row.app_id, entitlementId: row.entitlement_id, notes: row.notes }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async grant(userId : string, appId : string | null, entitlementId : string, notes : string, grantedBy : string) : Promise<EntitlementGrant> {
|
|
34
|
+
const id = randomUUID();
|
|
35
|
+
const result = await this.pool.query(
|
|
36
|
+
`INSERT INTO entitlement_grants (id, user_id, app_id, entitlement_id, notes, granted_by)
|
|
37
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
38
|
+
RETURNING granted_at`,
|
|
39
|
+
[id, userId, appId, entitlementId, notes, grantedBy]);
|
|
40
|
+
return { id, userId, appId, entitlementId, notes, grantedBy, grantedAt: this.iso(result.rows[0]?.granted_at) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async revoke(grantId : string) : Promise<boolean> {
|
|
44
|
+
const result = await this.pool.query("DELETE FROM entitlement_grants WHERE id = $1", [grantId]);
|
|
45
|
+
return (result.rowCount ?? 0) > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async listGrants(userId? : string) : Promise<Array<EntitlementGrant>> {
|
|
49
|
+
const result = userId === undefined
|
|
50
|
+
? await this.pool.query(
|
|
51
|
+
`SELECT id, user_id, app_id, entitlement_id, notes, granted_at, granted_by
|
|
52
|
+
FROM entitlement_grants ORDER BY granted_at DESC`)
|
|
53
|
+
: await this.pool.query(
|
|
54
|
+
`SELECT id, user_id, app_id, entitlement_id, notes, granted_at, granted_by
|
|
55
|
+
FROM entitlement_grants WHERE user_id = $1 ORDER BY granted_at DESC`, [userId]);
|
|
56
|
+
return result.rows.map(row => ({
|
|
57
|
+
id: row.id,
|
|
58
|
+
userId: row.user_id,
|
|
59
|
+
appId: row.app_id,
|
|
60
|
+
entitlementId: row.entitlement_id,
|
|
61
|
+
notes: row.notes,
|
|
62
|
+
grantedAt: this.iso(row.granted_at),
|
|
63
|
+
grantedBy: row.granted_by,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async upsertDefinition(appId : string | null, entitlementId : string, notes : string) : Promise<void> {
|
|
68
|
+
await this.pool.query(
|
|
69
|
+
`INSERT INTO entitlements (app_id, entitlement_id, notes)
|
|
70
|
+
VALUES ($1, $2, $3)
|
|
71
|
+
ON CONFLICT (COALESCE(app_id, ''), entitlement_id) DO UPDATE SET notes = EXCLUDED.notes`,
|
|
72
|
+
[appId, entitlementId, notes]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private iso(value : unknown) : string {
|
|
76
|
+
return value instanceof Date ? value.toISOString() : String(value ?? new Date().toISOString());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { PostgresEntitlementStore }
|
package/src/data-store/Schema.ts
CHANGED
|
@@ -112,6 +112,38 @@ const ensureSchema = async (pool : Pool) : Promise<void> => {
|
|
|
112
112
|
`);
|
|
113
113
|
await pool.query("ALTER TABLE app_docs ADD COLUMN IF NOT EXISTS parent_slug TEXT");
|
|
114
114
|
await pool.query("ALTER TABLE app_docs ADD COLUMN IF NOT EXISTS position INTEGER NOT NULL DEFAULT 0");
|
|
115
|
+
await pool.query(`
|
|
116
|
+
CREATE TABLE IF NOT EXISTS entitlements (
|
|
117
|
+
app_id TEXT,
|
|
118
|
+
entitlement_id TEXT NOT NULL,
|
|
119
|
+
notes TEXT NOT NULL DEFAULT '',
|
|
120
|
+
registered_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
121
|
+
)
|
|
122
|
+
`);
|
|
123
|
+
await pool.query(`
|
|
124
|
+
CREATE UNIQUE INDEX IF NOT EXISTS entitlements_key
|
|
125
|
+
ON entitlements (COALESCE(app_id, ''), entitlement_id)
|
|
126
|
+
`);
|
|
127
|
+
await pool.query(`
|
|
128
|
+
CREATE TABLE IF NOT EXISTS entitlement_grants (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
user_id TEXT NOT NULL,
|
|
131
|
+
app_id TEXT,
|
|
132
|
+
entitlement_id TEXT NOT NULL,
|
|
133
|
+
notes TEXT NOT NULL DEFAULT '',
|
|
134
|
+
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
135
|
+
granted_by TEXT NOT NULL
|
|
136
|
+
)
|
|
137
|
+
`);
|
|
138
|
+
await pool.query(`
|
|
139
|
+
CREATE INDEX IF NOT EXISTS entitlement_grants_lookup
|
|
140
|
+
ON entitlement_grants (user_id, entitlement_id)
|
|
141
|
+
`);
|
|
142
|
+
await pool.query(`
|
|
143
|
+
INSERT INTO entitlements (app_id, entitlement_id, notes)
|
|
144
|
+
VALUES (NULL, 'access', 'Global access')
|
|
145
|
+
ON CONFLICT (COALESCE(app_id, ''), entitlement_id) DO NOTHING
|
|
146
|
+
`);
|
|
115
147
|
await pool.query("CREATE SCHEMA IF NOT EXISTS anbaric_system");
|
|
116
148
|
await pool.query(`
|
|
117
149
|
CREATE TABLE IF NOT EXISTS anbaric_system.cli_keys (
|
|
@@ -16,6 +16,9 @@ import {SessionsHandler} from "./handlers/auth/SessionsHandler";
|
|
|
16
16
|
import {WhoamiHandler} from "./handlers/auth/WhoamiHandler";
|
|
17
17
|
import {ConsumersHandler} from "./handlers/ConsumersHandler";
|
|
18
18
|
import {DocumentsHandler} from "./handlers/DocumentsHandler";
|
|
19
|
+
import {EntitlementsAdminHandler} from "./handlers/EntitlementsAdminHandler";
|
|
20
|
+
import {EntitlementsHandler} from "./handlers/EntitlementsHandler";
|
|
21
|
+
import {EntitlementStore} from "../data-store/EntitlementStore";
|
|
19
22
|
import {JobsHandler} from "./handlers/JobsHandler";
|
|
20
23
|
import {FaviconHandler} from "./handlers/FaviconHandler";
|
|
21
24
|
import {JobRunSchedulesHandler} from "./handlers/JobRunSchedulesHandler";
|
|
@@ -58,7 +61,8 @@ class HostingServer {
|
|
|
58
61
|
auditRecords? : AuditRecordStore,
|
|
59
62
|
plugins : Array<LoadedPlugin> = [],
|
|
60
63
|
jobRunSchedules? : JobRunSchedulePersistence,
|
|
61
|
-
notifier? : Notifier
|
|
64
|
+
notifier? : Notifier,
|
|
65
|
+
entitlements? : EntitlementStore) {
|
|
62
66
|
const pages = new PagesHandler();
|
|
63
67
|
const ping = new PingHandler(tenant);
|
|
64
68
|
const jobs = new JobsHandler(persistence);
|
|
@@ -87,6 +91,7 @@ class HostingServer {
|
|
|
87
91
|
if (audits) publicRouter.registerApi("audits", audits);
|
|
88
92
|
if (jobRunSchedules) publicRouter.registerApi("job-run-schedules", new JobRunSchedulesHandler(jobRunSchedules));
|
|
89
93
|
if (notifier) publicRouter.registerApi("notifications", new NotificationsHandler(notifier));
|
|
94
|
+
if (entitlements) publicRouter.registerApi("entitlements", new EntitlementsAdminHandler(entitlements));
|
|
90
95
|
if (cliAuthorizer) {
|
|
91
96
|
publicRouter.register("authorize-cli", new AuthorizeCliHandler(cliAuthorizer, pages, tenant));
|
|
92
97
|
publicRouter.registerApi("keys", new KeysHandler(cliAuthorizer));
|
|
@@ -111,6 +116,7 @@ class HostingServer {
|
|
|
111
116
|
if (audits) internalRouter.registerApi("audits", audits);
|
|
112
117
|
if (jobRunSchedules) internalRouter.registerApi("job-run-schedules", new JobRunSchedulesHandler(jobRunSchedules));
|
|
113
118
|
if (notifier) internalRouter.registerApi("notifications", new NotificationsHandler(notifier));
|
|
119
|
+
if (entitlements) internalRouter.registerApi("entitlements", new EntitlementsHandler(entitlements));
|
|
114
120
|
|
|
115
121
|
// The favicon is requested by the browser before anyone has signed in,
|
|
116
122
|
// and by deployed apps' pages, so gating it behind a session would send
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {EntitlementStore} from "../../data-store/EntitlementStore";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
/* The console-facing side, served on the authenticated public entry point:
|
|
6
|
+
list definitions, create global ones, and grant or revoke entitlements to
|
|
7
|
+
users. Grants record who made them from the signed-in user. */
|
|
8
|
+
class EntitlementsAdminHandler implements RequestHandler {
|
|
9
|
+
|
|
10
|
+
constructor(private store : EntitlementStore) {}
|
|
11
|
+
|
|
12
|
+
async handle(request : Request) : Promise<void> {
|
|
13
|
+
const { method, id, subresource } = request;
|
|
14
|
+
|
|
15
|
+
if (method === "GET" && ! id) return request.reply(200, await this.store.listDefinitions());
|
|
16
|
+
if (method === "POST" && id === "definitions" && ! subresource) return this.defineGlobal(request);
|
|
17
|
+
if (method === "GET" && id === "grants" && ! subresource) {
|
|
18
|
+
return request.reply(200, await this.store.listGrants(request.query("userId")));
|
|
19
|
+
}
|
|
20
|
+
if (method === "POST" && id === "grants" && ! subresource) return this.grant(request);
|
|
21
|
+
if (method === "DELETE" && id === "grants" && subresource) return this.revoke(request, subresource);
|
|
22
|
+
request.notFound();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async defineGlobal(request : Request) : Promise<void> {
|
|
26
|
+
const body = await request.body() as { entitlementId? : string, notes? : string } | undefined;
|
|
27
|
+
if (! body?.entitlementId) return request.reply(400, { error: "entitlementId is required" });
|
|
28
|
+
|
|
29
|
+
await this.store.defineGlobal(body.entitlementId, body.notes ?? "");
|
|
30
|
+
request.reply(204);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async grant(request : Request) : Promise<void> {
|
|
34
|
+
const body = await request.body() as
|
|
35
|
+
{ userId? : string, appId? : string | null, entitlementId? : string, notes? : string } | undefined;
|
|
36
|
+
if (! body?.userId || ! body.entitlementId) {
|
|
37
|
+
return request.reply(400, { error: "userId and entitlementId are required" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const grantedBy = request.user?.id ?? "system";
|
|
41
|
+
const grant = await this.store.grant(body.userId, body.appId || null, body.entitlementId, body.notes ?? "", grantedBy);
|
|
42
|
+
request.reply(201, grant);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async revoke(request : Request, grantId : string) : Promise<void> {
|
|
46
|
+
const removed = await this.store.revoke(grantId);
|
|
47
|
+
removed ? request.reply(204) : request.notFound();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { EntitlementsAdminHandler }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {Entitlements} from "anbaric-tsapi";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
/* The app-facing side, served on the internal entry point: register an
|
|
6
|
+
entitlement and check whether a user holds it. Both are scoped to the
|
|
7
|
+
calling app (the ambient app header), so an app can neither define a global
|
|
8
|
+
entitlement nor check as another app. */
|
|
9
|
+
class EntitlementsHandler implements RequestHandler {
|
|
10
|
+
|
|
11
|
+
constructor(private entitlements : Entitlements) {}
|
|
12
|
+
|
|
13
|
+
async handle(request : Request) : Promise<void> {
|
|
14
|
+
const appId = request.appId;
|
|
15
|
+
if (! appId) return request.reply(400, { error: "Missing the x-anbaric-app header" });
|
|
16
|
+
|
|
17
|
+
if (request.method === "POST" && ! request.id) return this.register(request, appId);
|
|
18
|
+
if (request.method === "GET" && request.id && request.subresource === "check") return this.check(request, appId);
|
|
19
|
+
request.notFound();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async register(request : Request, appId : string) : Promise<void> {
|
|
23
|
+
const body = await request.body() as { entitlementId? : string, notes? : string } | undefined;
|
|
24
|
+
if (! body?.entitlementId) return request.reply(400, { error: "entitlementId is required" });
|
|
25
|
+
|
|
26
|
+
await this.entitlements.register(appId, body.entitlementId, body.notes ?? "");
|
|
27
|
+
request.reply(204);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async check(request : Request, appId : string) : Promise<void> {
|
|
31
|
+
const userId = request.query("userId");
|
|
32
|
+
if (! userId) return request.reply(400, { error: "userId is required" });
|
|
33
|
+
|
|
34
|
+
request.reply(200, { has: await this.entitlements.has(appId, userId, request.id!) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { EntitlementsHandler }
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,11 @@ export * from "./data-store/PostgresJobRunSchedulePersistence";
|
|
|
27
27
|
export * from "./data-store/AppDocsStore";
|
|
28
28
|
export * from "./data-store/PostgresAppDocsStore";
|
|
29
29
|
export * from "./data-store/InMemoryAppDocsStore";
|
|
30
|
+
export * from "./data-store/EntitlementStore";
|
|
31
|
+
export * from "./data-store/PostgresEntitlementStore";
|
|
32
|
+
export * from "./data-store/InMemoryEntitlementStore";
|
|
33
|
+
export * from "./hosting/handlers/EntitlementsHandler";
|
|
34
|
+
export * from "./hosting/handlers/EntitlementsAdminHandler";
|
|
30
35
|
export * from "./docs/DocModel";
|
|
31
36
|
export * from "./docs/GatewayDocModel";
|
|
32
37
|
export * from "./docs/DocGenerator";
|
package/src/main.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {FargateBuildLayer} from "./app-management/FargateBuildLayer";
|
|
|
20
20
|
import {DocGenerator} from "./docs/DocGenerator";
|
|
21
21
|
import {GatewayDocModel} from "./docs/GatewayDocModel";
|
|
22
22
|
import {PostgresAppDocsStore} from "./data-store/PostgresAppDocsStore";
|
|
23
|
+
import {PostgresEntitlementStore} from "./data-store/PostgresEntitlementStore";
|
|
23
24
|
import {ConsumerRegistry} from "./queuing/ConsumerRegistry";
|
|
24
25
|
import {Dispatcher} from "./queuing/Dispatcher";
|
|
25
26
|
import {HostingServer} from "./hosting/HostingServer";
|
|
@@ -94,7 +95,8 @@ const plugins = await new PluginLoader().load(process.env.ANBARIC_PLUGINS ?? "an
|
|
|
94
95
|
const server = new HostingServer(new PostgresJobPersistence(pool), queue, registry, buildLayer,
|
|
95
96
|
(appId, collection) => new PostgresJsonStore(pool, appId, collection), secretStoreFor, authenticator, cliAuthorizer,
|
|
96
97
|
tokenAuthenticator, process.env.ANBARIC_TENANT, new PostgresAuditRecordStore(pool), plugins,
|
|
97
|
-
new PostgresJobRunSchedulePersistence(pool), await loadNotifier(process.env.ANBARIC_NOTIFIER_MODULE)
|
|
98
|
+
new PostgresJobRunSchedulePersistence(pool), await loadNotifier(process.env.ANBARIC_NOTIFIER_MODULE),
|
|
99
|
+
new PostgresEntitlementStore(pool));
|
|
98
100
|
const port = await server.listen(hostingPort);
|
|
99
101
|
const internal = await server.listenInternal(internalPort);
|
|
100
102
|
|