anbaric-cloud-hosting 1.4.0 → 1.6.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 +5 -5
- package/src/hosting/HostingServer.ts +84 -97
- package/src/hosting/Middleware.ts +11 -0
- package/src/hosting/Request.ts +103 -0
- package/src/hosting/RequestHandler.ts +9 -0
- package/src/hosting/Router.ts +22 -319
- package/src/hosting/Server.ts +54 -0
- package/src/hosting/handlers/AppProxyHandler.ts +34 -0
- package/src/hosting/handlers/AppsHandler.ts +57 -0
- package/src/hosting/handlers/AuditsHandler.ts +50 -0
- package/src/hosting/handlers/ConsumersHandler.ts +24 -0
- package/src/hosting/handlers/DocumentsHandler.ts +44 -0
- package/src/hosting/handlers/JobsHandler.ts +60 -0
- package/src/hosting/handlers/PagesHandler.ts +22 -0
- package/src/hosting/handlers/PingHandler.ts +18 -0
- package/src/hosting/handlers/QueueHandler.ts +36 -0
- package/src/hosting/handlers/SecretsHandler.ts +42 -0
- package/src/hosting/handlers/StateMachinesHandler.ts +21 -0
- package/src/hosting/handlers/auth/AuthorizeCliHandler.ts +50 -0
- package/src/hosting/handlers/auth/KeysHandler.ts +36 -0
- package/src/hosting/handlers/auth/WhoamiHandler.ts +18 -0
- package/src/hosting/middleware/AuthenticationMiddleware.ts +52 -0
- package/src/hosting/middleware/SessionMiddleware.ts +21 -0
- package/src/index.ts +20 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {JobPersistence, deserializeJob, serializeJob} from "anbaric-tsapi";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
class JobsHandler implements RequestHandler {
|
|
6
|
+
|
|
7
|
+
constructor(private persistence : JobPersistence) {}
|
|
8
|
+
|
|
9
|
+
async handle(request : Request) : Promise<void> {
|
|
10
|
+
switch (request.subresource) {
|
|
11
|
+
case "properties":
|
|
12
|
+
if (request.id) return this.handleProperties(request, request.id);
|
|
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();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async handleJob(request : Request, id : string) : Promise<void> {
|
|
33
|
+
switch (request.method) {
|
|
34
|
+
case "PUT":
|
|
35
|
+
await this.persistence.save(deserializeJob(await request.body()));
|
|
36
|
+
return request.reply(204);
|
|
37
|
+
case "GET":
|
|
38
|
+
return request.reply(200, serializeJob(await this.persistence.retrieve(id)));
|
|
39
|
+
case "DELETE":
|
|
40
|
+
await this.persistence.delete(id);
|
|
41
|
+
return request.reply(204);
|
|
42
|
+
}
|
|
43
|
+
request.notFound();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async handleCollection(request : Request) : Promise<void> {
|
|
47
|
+
switch (request.method) {
|
|
48
|
+
case "GET": {
|
|
49
|
+
const pageSize = Number(request.query("pageSize") ?? 100);
|
|
50
|
+
const page = Number(request.query("page") ?? 0);
|
|
51
|
+
const jobs = await this.persistence.list(pageSize, page);
|
|
52
|
+
return request.reply(200, jobs.map(serializeJob));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
request.notFound();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { JobsHandler }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {readFile} from "node:fs/promises";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
class PagesHandler implements RequestHandler {
|
|
6
|
+
|
|
7
|
+
async handle(request : Request) : Promise<void> {
|
|
8
|
+
if (request.method === "GET" && !request.id) return this.serve(request);
|
|
9
|
+
request.notFound();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async serve(request : Request) : Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
request.replyHtml(await readFile(new URL("../pages/platform-ui.html", import.meta.url)));
|
|
15
|
+
} catch {
|
|
16
|
+
request.reply(501, { error: "The platform UI has not been built - run npm run build in anbaric-cloud-hosting/ui" });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { PagesHandler }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {Request} from "../Request";
|
|
2
|
+
import {RequestHandler} from "../RequestHandler";
|
|
3
|
+
|
|
4
|
+
class PingHandler implements RequestHandler {
|
|
5
|
+
|
|
6
|
+
constructor(private tenant? : string) {}
|
|
7
|
+
|
|
8
|
+
async handle(request : Request) : Promise<void> {
|
|
9
|
+
switch (request.method) {
|
|
10
|
+
case "GET":
|
|
11
|
+
return request.reply(200, this.tenant ? { status: "ok", tenant: this.tenant } : { status: "ok" });
|
|
12
|
+
}
|
|
13
|
+
request.notFound();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { PingHandler }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {ConfirmableQueue} from "../../queuing/ConfirmableQueue";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
class QueueHandler implements RequestHandler {
|
|
6
|
+
|
|
7
|
+
constructor(private queue : ConfirmableQueue) {}
|
|
8
|
+
|
|
9
|
+
async handle(request : Request) : Promise<void> {
|
|
10
|
+
if (request.method !== "POST" || !request.id || request.subresource) return request.notFound();
|
|
11
|
+
|
|
12
|
+
switch (request.id) {
|
|
13
|
+
case "enqueue": {
|
|
14
|
+
const { jobId, workflowId } = await request.body();
|
|
15
|
+
await this.queue.enqueue(jobId, workflowId);
|
|
16
|
+
return request.reply(204);
|
|
17
|
+
}
|
|
18
|
+
case "schedule": {
|
|
19
|
+
const { jobId, workflowId, due } = await request.body();
|
|
20
|
+
await this.queue.schedule(jobId, workflowId, new Date(due));
|
|
21
|
+
return request.reply(204);
|
|
22
|
+
}
|
|
23
|
+
case "dequeue":
|
|
24
|
+
return request.reply(200, { messages: await this.queue.dequeueSome() });
|
|
25
|
+
case "confirm": {
|
|
26
|
+
const { jobId, workflowId } = await request.body();
|
|
27
|
+
await this.queue.confirm({ jobId, workflowId });
|
|
28
|
+
return request.reply(204);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
request.notFound();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { QueueHandler }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {SecretStore} from "anbaric-tsapi";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
class SecretsHandler implements RequestHandler {
|
|
6
|
+
|
|
7
|
+
constructor(private secretStore : SecretStore) {}
|
|
8
|
+
|
|
9
|
+
async handle(request : Request) : Promise<void> {
|
|
10
|
+
if (request.subresource) return request.notFound();
|
|
11
|
+
if (request.id) return this.handleSecret(request, request.id);
|
|
12
|
+
return this.handleCollection(request);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private async handleSecret(request : Request, name : string) : Promise<void> {
|
|
16
|
+
switch (request.method) {
|
|
17
|
+
case "PUT": {
|
|
18
|
+
const { value } = await request.body();
|
|
19
|
+
if (typeof value !== "string") return request.reply(400, { error: "Expected a body of { value : string }" });
|
|
20
|
+
await this.secretStore.save(name, value);
|
|
21
|
+
return request.reply(204);
|
|
22
|
+
}
|
|
23
|
+
case "GET":
|
|
24
|
+
return request.reply(200, { value: await this.secretStore.retrieve(name) });
|
|
25
|
+
case "DELETE":
|
|
26
|
+
await this.secretStore.delete(name);
|
|
27
|
+
return request.reply(204);
|
|
28
|
+
}
|
|
29
|
+
request.notFound();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async handleCollection(request : Request) : Promise<void> {
|
|
33
|
+
switch (request.method) {
|
|
34
|
+
case "GET":
|
|
35
|
+
return request.reply(200, await this.secretStore.list());
|
|
36
|
+
}
|
|
37
|
+
request.notFound();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { SecretsHandler }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {ConsumerRegistry} from "../../queuing/ConsumerRegistry";
|
|
2
|
+
import {Request} from "../Request";
|
|
3
|
+
import {RequestHandler} from "../RequestHandler";
|
|
4
|
+
|
|
5
|
+
class StateMachinesHandler implements RequestHandler {
|
|
6
|
+
|
|
7
|
+
constructor(private registry : ConsumerRegistry) {}
|
|
8
|
+
|
|
9
|
+
async handle(request : Request) : Promise<void> {
|
|
10
|
+
if (request.id) return request.notFound();
|
|
11
|
+
|
|
12
|
+
switch (request.method) {
|
|
13
|
+
case "GET":
|
|
14
|
+
return request.reply(200, this.registry.list());
|
|
15
|
+
}
|
|
16
|
+
request.notFound();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { StateMachinesHandler }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {CliAuthorizer} from "../../../auth/CliAuthorizer";
|
|
2
|
+
import {User} from "../../../auth/User";
|
|
3
|
+
import {Request} from "../../Request";
|
|
4
|
+
import {RequestHandler} from "../../RequestHandler";
|
|
5
|
+
import {PagesHandler} from "../PagesHandler";
|
|
6
|
+
|
|
7
|
+
class AuthorizeCliHandler implements RequestHandler {
|
|
8
|
+
|
|
9
|
+
constructor(private authorizer : CliAuthorizer, private pages : PagesHandler,
|
|
10
|
+
private tenant? : string) {}
|
|
11
|
+
|
|
12
|
+
async handle(request : Request) : Promise<void> {
|
|
13
|
+
const requestId = request.id;
|
|
14
|
+
if (!requestId) return request.notFound();
|
|
15
|
+
|
|
16
|
+
switch (request.subresource) {
|
|
17
|
+
case "poll":
|
|
18
|
+
if (request.method === "GET") return this.handlePoll(request, requestId);
|
|
19
|
+
break;
|
|
20
|
+
case undefined:
|
|
21
|
+
switch (request.method) {
|
|
22
|
+
case "GET":
|
|
23
|
+
return this.pages.serve(request);
|
|
24
|
+
case "POST":
|
|
25
|
+
return this.handleApprove(request, requestId);
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
request.notFound();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async handlePoll(request : Request, requestId : string) : Promise<void> {
|
|
33
|
+
const keyPair = this.authorizer.collect(requestId);
|
|
34
|
+
if (!keyPair) return request.reply(202, { status: "pending" });
|
|
35
|
+
request.reply(200, keyPair);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async handleApprove(request : Request, requestId : string) : Promise<void> {
|
|
39
|
+
const { clientName } = await request.body();
|
|
40
|
+
if (typeof clientName !== "string" || clientName.trim().length === 0) {
|
|
41
|
+
return request.reply(400, { error: "Expected a body of { clientName : string }" });
|
|
42
|
+
}
|
|
43
|
+
await this.authorizer.approve(requestId, clientName.trim(), request.user ?? new User("local"),
|
|
44
|
+
request.tenant?.id ?? this.tenant);
|
|
45
|
+
request.reply(204);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { AuthorizeCliHandler }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {CliAuthorizer} from "../../../auth/CliAuthorizer";
|
|
2
|
+
import {User} from "../../../auth/User";
|
|
3
|
+
import {Request} from "../../Request";
|
|
4
|
+
import {RequestHandler} from "../../RequestHandler";
|
|
5
|
+
|
|
6
|
+
class KeysHandler implements RequestHandler {
|
|
7
|
+
|
|
8
|
+
constructor(private authorizer : CliAuthorizer) {}
|
|
9
|
+
|
|
10
|
+
async handle(request : Request) : Promise<void> {
|
|
11
|
+
const owner = request.user ?? new User("local");
|
|
12
|
+
|
|
13
|
+
if (request.id) {
|
|
14
|
+
switch (request.method) {
|
|
15
|
+
case "DELETE":
|
|
16
|
+
await this.authorizer.revoke(request.id, owner.id);
|
|
17
|
+
return request.reply(204);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
switch (request.method) {
|
|
21
|
+
case "GET": {
|
|
22
|
+
const keys = await this.authorizer.keysFor(owner.id);
|
|
23
|
+
return request.reply(200, keys.map(key => ({
|
|
24
|
+
id: key.id,
|
|
25
|
+
clientName: key.clientName,
|
|
26
|
+
createdAt: key.createdAt.toISOString(),
|
|
27
|
+
})));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
request.notFound();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { KeysHandler }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {Request} from "../../Request";
|
|
2
|
+
import {RequestHandler} from "../../RequestHandler";
|
|
3
|
+
|
|
4
|
+
class WhoamiHandler implements RequestHandler {
|
|
5
|
+
|
|
6
|
+
async handle(request : Request) : Promise<void> {
|
|
7
|
+
if (request.method === "GET" && !request.id && request.user) {
|
|
8
|
+
return request.reply(200, {
|
|
9
|
+
id: request.user.id,
|
|
10
|
+
roles: request.user.roles.map(role => role.id),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
request.notFound();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { WhoamiHandler }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {Authenticator} from "../../auth/Authenticator";
|
|
2
|
+
import {TokenAuthenticator} from "../../auth/TokenAuthenticator";
|
|
3
|
+
import {Middleware} from "../Middleware";
|
|
4
|
+
import {Request} from "../Request";
|
|
5
|
+
|
|
6
|
+
type OpenRequestPredicate = (request : Request) => boolean;
|
|
7
|
+
|
|
8
|
+
/* Decorates the request with its authenticated user and tenant - bearer
|
|
9
|
+
tokens first, sessions otherwise - and enforces authorization. Requests
|
|
10
|
+
matching the open predicate (ping, the one-time CLI key poll) pass
|
|
11
|
+
through untouched, as does everything when no authenticator is
|
|
12
|
+
configured. */
|
|
13
|
+
class AuthenticationMiddleware implements Middleware {
|
|
14
|
+
|
|
15
|
+
constructor(private authenticator? : Authenticator,
|
|
16
|
+
private tokenAuthenticator? : TokenAuthenticator,
|
|
17
|
+
private isOpen : OpenRequestPredicate = () => false) {}
|
|
18
|
+
|
|
19
|
+
async apply(request : Request) : Promise<boolean> {
|
|
20
|
+
if (this.isOpen(request)) return true;
|
|
21
|
+
|
|
22
|
+
if (this.tokenAuthenticator?.handles(request.raw)) {
|
|
23
|
+
const user = await this.tokenAuthenticator.authenticate(request.raw, request.rawResponse);
|
|
24
|
+
if (!user) return false;
|
|
25
|
+
request.user = user;
|
|
26
|
+
return this.authorized(request);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!this.authenticator) return true;
|
|
30
|
+
|
|
31
|
+
const authenticated = await this.authenticator.authenticate(request.session, request.raw, request.rawResponse);
|
|
32
|
+
if (!authenticated) return false;
|
|
33
|
+
|
|
34
|
+
[request.user, request.tenant] = authenticated;
|
|
35
|
+
return this.authorized(request);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private async authorized(request : Request) : Promise<boolean> {
|
|
39
|
+
if (!this.authenticator || !request.user) return true;
|
|
40
|
+
|
|
41
|
+
const permitted = await this.authenticator.authorize(request.user, request.raw, request.rawResponse);
|
|
42
|
+
if (!permitted) {
|
|
43
|
+
if (!request.handled) request.reply(403, { error: "Not authorized" });
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { AuthenticationMiddleware };
|
|
52
|
+
export type { OpenRequestPredicate };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {SESSION_COOKIE} from "../../auth/Authenticator";
|
|
2
|
+
import {Middleware} from "../Middleware";
|
|
3
|
+
import {Request} from "../Request";
|
|
4
|
+
|
|
5
|
+
class SessionMiddleware implements Middleware {
|
|
6
|
+
|
|
7
|
+
async apply(request : Request) : Promise<boolean> {
|
|
8
|
+
const cookies = String(request.header("cookie") ?? "").split(";");
|
|
9
|
+
for (const cookie of cookies) {
|
|
10
|
+
const [name, ...value] = cookie.trim().split("=");
|
|
11
|
+
if (name === SESSION_COOKIE) {
|
|
12
|
+
request.session = value.join("=");
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { SessionMiddleware }
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,27 @@ export * from "./data-store/PostgresJsonStore";
|
|
|
23
23
|
export * from "./data-store/Schema";
|
|
24
24
|
export * from "./data-store/SecretsManagerSecretStore";
|
|
25
25
|
export * from "./hosting/HostingServer";
|
|
26
|
+
export * from "./hosting/Middleware";
|
|
27
|
+
export * from "./hosting/Request";
|
|
28
|
+
export * from "./hosting/RequestHandler";
|
|
26
29
|
export * from "./hosting/Router";
|
|
30
|
+
export * from "./hosting/Server";
|
|
31
|
+
export * from "./hosting/middleware/AuthenticationMiddleware";
|
|
32
|
+
export * from "./hosting/middleware/SessionMiddleware";
|
|
33
|
+
export * from "./hosting/handlers/AppProxyHandler";
|
|
34
|
+
export * from "./hosting/handlers/AppsHandler";
|
|
35
|
+
export * from "./hosting/handlers/AuditsHandler";
|
|
36
|
+
export * from "./hosting/handlers/ConsumersHandler";
|
|
37
|
+
export * from "./hosting/handlers/DocumentsHandler";
|
|
38
|
+
export * from "./hosting/handlers/JobsHandler";
|
|
39
|
+
export * from "./hosting/handlers/PagesHandler";
|
|
40
|
+
export * from "./hosting/handlers/PingHandler";
|
|
41
|
+
export * from "./hosting/handlers/QueueHandler";
|
|
42
|
+
export * from "./hosting/handlers/SecretsHandler";
|
|
43
|
+
export * from "./hosting/handlers/StateMachinesHandler";
|
|
44
|
+
export * from "./hosting/handlers/auth/AuthorizeCliHandler";
|
|
45
|
+
export * from "./hosting/handlers/auth/KeysHandler";
|
|
46
|
+
export * from "./hosting/handlers/auth/WhoamiHandler";
|
|
27
47
|
export * from "./queuing/ConfirmableQueue";
|
|
28
48
|
export * from "./queuing/ConsumerRegistry";
|
|
29
49
|
export * from "./queuing/Dispatcher";
|