This repository has been archived by the owner on Apr 2, 2024. It is now read-only.
generated from AMRC-FactoryPlus/acs-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8bc9c1d
commit d5d1b90
Showing
8 changed files
with
3,280 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
ARG utility_prefix=ghcr.io/amrc-factoryplus/utilities | ||
ARG utility_ver=v1.0.6 | ||
|
||
FROM ${utility_prefix}-build:${utility_ver} AS build | ||
|
||
# Install the node application on the build container where we can | ||
# compile the native modules. | ||
RUN install -d -o node -g node /home/node/app | ||
WORKDIR /home/node/app | ||
USER node | ||
COPY package*.json ./ | ||
RUN npm install --save=false | ||
COPY . . | ||
|
||
FROM ${utility_prefix}-run:${utility_ver} | ||
|
||
# Copy across from the build container. | ||
WORKDIR /home/node/app | ||
COPY --from=build --chown=root:root /home/node/app ./ | ||
|
||
USER node | ||
CMD npm start |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
> **Note** | ||
> The AMRC Connectivity Stack is an open-source implementation of the AMRC's [Factory+ Framework](https://factoryplus.app.amrc.co.uk/). | ||
# ACS Command Escalation Service | ||
|
||
The [COMPONENT] component of the AMRC Connectivity Stack. | ||
> The [AMRC Connectivity Stack (ACS)](https://github.com/AMRC-FactoryPlus/amrc-connectivity-stack) is an open-source implementation of the AMRC's [Factory+ Framework](https://factoryplus.app.amrc.co.uk). | ||
This `acs-cmdesc` service satisfies the **Command Escalation** component of the Factory+ framework and provides a service that handles Command Escalation requests on behalf of clients. As per Factory+, this service manages escalation requests, authenticating the client, verifying the request is authorised, and actually transmitting the CMD to the device. | ||
|
||
For more information about the Command Escalation component of Factory+ see the [specification](https://factoryplus.app.amrc.co.uk) or for an example of how to deploy this service see the [AMRC Connectivity Stack repository](https://github.com/AMRC-FactoryPlus/amrc-connectivity-stack). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* Factory+ / AMRC Connectivity Stack (ACS) Command Escalation component | ||
* Main entrypoint | ||
* Copyright 2022 AMRC | ||
*/ | ||
|
||
import { ServiceClient, WebAPI, pkgVersion } from "@amrc-factoryplus/utilities"; | ||
|
||
import ApiV1 from "../lib/api_v1.js"; | ||
import CmdEscD from "../lib/cmdescd.js"; | ||
import MqttCli from "../lib/mqttcli.js"; | ||
|
||
const Service_Cmdesc = "78ea7071-24ac-4916-8351-aa3e549d8ccd"; | ||
|
||
const Version = pkgVersion(import.meta); | ||
const Device_UUID = process.env.DEVICE_UUID; | ||
|
||
const fplus = await new ServiceClient({ | ||
root_principal: process.env.ROOT_PRINCIPAL, | ||
directory_url: process.env.DIRECTORY_URL, | ||
}).init(); | ||
|
||
const cmdesc = await new CmdEscD({ | ||
fplus, | ||
}).init(); | ||
|
||
const mqtt = await new MqttCli({ | ||
fplus, | ||
sparkplug_address: process.env.SPARKPLUG_ADDRESS, | ||
device_uuid: Device_UUID, | ||
service: Service_Cmdesc, | ||
http_url: process.env.HTTP_API_URL, | ||
}).init(); | ||
|
||
const v1 = await new ApiV1({ | ||
cmdesc, | ||
}).init(); | ||
|
||
const web = await new WebAPI({ | ||
ping: { | ||
version: Version, | ||
service: Service_Cmdesc, | ||
device: Device_UUID, | ||
}, | ||
realm: process.env.REALM, | ||
hostname: process.env.HOSTNAME, | ||
keytab: process.env.SERVER_KEYTAB, | ||
http_port: process.env.PORT, | ||
max_age: process.env.CACHE_MAX_AGE, | ||
|
||
routes: app => { | ||
app.use("/v1", v1.routes); | ||
}, | ||
}).init(); | ||
|
||
mqtt.set_cmdesc(cmdesc); | ||
cmdesc.set_mqtt(mqtt); | ||
mqtt.run(); | ||
web.run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Factory+ / AMRC Connectivity Stack (ACS) Command Escalation component | ||
* v1 HTTP API | ||
* Copyright 2022 AMRC | ||
*/ | ||
|
||
import express from "express"; | ||
import typeis from "type-is"; | ||
|
||
import {Address} from "@amrc-factoryplus/utilities"; | ||
|
||
export default class ApiV1 { | ||
constructor(opts) { | ||
this.cmdesc = opts.cmdesc; | ||
|
||
this.routes = express.Router(); | ||
} | ||
|
||
async init() { | ||
this.setup_routes(); | ||
return this; | ||
} | ||
|
||
setup_routes() { | ||
const api = this.routes; | ||
|
||
api.post("/address/:group/:node", this.by_address.bind(this)); | ||
api.post("/address/:group/:node/:device", this.by_address.bind(this)); | ||
} | ||
|
||
async by_address(req, res) { | ||
if (!typeis(req, "application/json")) | ||
return res.status(415).end(); | ||
|
||
const to = new Address( | ||
req.params.group, req.params.node, req.params.device); | ||
|
||
const st = await this.cmdesc.execute_command( | ||
req.auth, to, req.body); | ||
res.status(st).end(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
/* | ||
* Factory+ / AMRC Connectivity Stack (ACS) Command Escalation component | ||
* MQTT client | ||
* Copyright 2022 AMRC | ||
*/ | ||
|
||
import {Address, Debug, UUIDs,} from "@amrc-factoryplus/utilities"; | ||
|
||
const CCL_Perms = "9584ee09-a35a-4278-bc13-21a8be1f007c"; | ||
const CCL_Template = "60e99f28-67fe-4344-a6ab-b1edb8b8e810"; | ||
|
||
const debug = new Debug(); | ||
|
||
export default class CmdEscD { | ||
constructor(opts) { | ||
this.fplus = opts.fplus; | ||
} | ||
|
||
async init() { | ||
return this; | ||
} | ||
|
||
set_mqtt(m) { | ||
this.mqtt = m; | ||
} | ||
|
||
async find_principal_for_address(from) { | ||
if (from.isDevice()) { | ||
debug.log("cmdesc", "Cannot authorise request from a Device"); | ||
return; | ||
} | ||
|
||
const res = await this.fplus.fetch({ | ||
service: UUIDs.Service.Registry, | ||
url: `/v1/app/${UUIDs.App.SparkplugAddress}/search`, | ||
query: { | ||
group_id: JSON.stringify(from.group), | ||
node_id: JSON.stringify(from.node), | ||
}, | ||
}); | ||
if (!res.ok) { | ||
debug.log("cmdesc", `Failed to look up address ${from}: ${res.status}`); | ||
return; | ||
} | ||
const json = await res.json(); | ||
|
||
switch (json.length) { | ||
case 0: | ||
debug.log("cmdesc", `No UUID found for address ${from}`); | ||
return; | ||
case 1: | ||
break; | ||
default: | ||
debug.log("cmdesc", `More than one UUID found for address ${from}`); | ||
return; | ||
} | ||
|
||
debug.log("cmdesc", `Request from ${from} = ${json[0]}`); | ||
return json[0]; | ||
} | ||
|
||
async fetch_acl(princ, by_uuid) { | ||
const res = await this.fplus.fetch({ | ||
service: UUIDs.Service.Authentication, | ||
url: "/authz/acl", | ||
query: { | ||
principal: princ, | ||
permission: CCL_Perms, | ||
"by-uuid": !!by_uuid, | ||
}, | ||
}); | ||
if (!res.ok) { | ||
debug.log("acl", `Can't get ACL for ${princ}: ${res.status}`); | ||
return []; | ||
} | ||
|
||
return await res.json(); | ||
} | ||
|
||
async expand_acl(princ) { | ||
let acl; | ||
if (princ instanceof Address) { | ||
const uuid = await this.find_principal_for_address(princ); | ||
if (!uuid) return []; | ||
acl = await this.fetch_acl(uuid, true); | ||
} else { | ||
acl = await this.fetch_acl(princ, false); | ||
} | ||
|
||
/* Fetch the CDB entries we need. Don't fetch any entry more | ||
* than once, the HTTP caching logic can't return a cached | ||
* result for a request still in flight. */ | ||
const perms = new Map(acl.map(a => [a.permission, null])); | ||
const targs = new Map( | ||
/* We don't need to look up the wildcard address. */ | ||
acl.filter(a => a.target != UUIDs.Null) | ||
.map(a => [a.target, null])); | ||
|
||
await Promise.all([ | ||
...[...perms.keys()].map(perm => | ||
this.fplus.fetch_configdb(CCL_Template, perm) | ||
.then(tmpl => perms.set(perm, tmpl))), | ||
|
||
...[...targs.keys()].map(targ => | ||
this.fplus.fetch_configdb(UUIDs.App.SparkplugAddress, targ) | ||
.then(a => a | ||
? new Address(a.group_id, a.node_id, a.device_id) | ||
: null) | ||
.then(addr => targs.set(targ, addr))), | ||
]); | ||
|
||
const res_targ = t => t == UUIDs.Null ? true : targs.get(t); | ||
|
||
return acl.flatMap(ace => { | ||
const tags = perms.get(ace.permission); | ||
const address = res_targ(ace.target); | ||
return tags && address ? [{tags, address}] : []; | ||
}); | ||
} | ||
|
||
/* Potential return values: | ||
* 200: OK | ||
* 403: Forbidden | ||
* 404: Metric does not exist | ||
* 409: Wrong type / metric otherwise can't be set | ||
* 503: Device offline / not responding | ||
* | ||
* from can be an Address or a Kerberos principal string. | ||
* to must be an Address. | ||
* cmd is { name, type?, value } | ||
*/ | ||
async execute_command(from, to, cmd) { | ||
const log = stat => { | ||
debug.log("cmdesc", `${stat}: ${from} -> ${to}[${cmd.name} = ${cmd.value}]`); | ||
return stat; | ||
}; | ||
|
||
/* We can only give root rights if a type is explicitly | ||
* supplied. Otherwise we don't know what type to send. */ | ||
const is_root = | ||
typeof (from) == "string" && | ||
"type" in cmd && | ||
from == this.fplus.root_principal; | ||
|
||
if (!is_root) { | ||
const acl = await this.expand_acl(from); | ||
debug.log("acl", "ACL for %s: %o", from, acl); | ||
|
||
const tag = acl | ||
.find(ace => ace.address === true || | ||
ace.address.matches(to)) | ||
?.tags | ||
?.find(t => t.name == cmd.name); | ||
if (!tag) return log(403); | ||
|
||
if (cmd.type == undefined) { | ||
cmd.type = tag.type ?? "Boolean"; | ||
} else if (cmd.type != tag.type) { | ||
return log(409); | ||
} | ||
} | ||
|
||
this.mqtt.publish(to, "CMD", [cmd]); | ||
return log(200); | ||
} | ||
} |
Oops, something went wrong.