Nothing subscribed to channel:print_alerts and nothing watched a printer fail, so an operator away from the screen found out when they came back to a ruined plate. Alerts are now rows in printer_alerts, listed at GET /alerts (open=true for what still needs a person) and cleared with POST /alerts/:id/acknowledge, which is operator-only and keeps the first acknowledger on the record. Two sources. Telemetry: a change into error is a failed print if the printer was printing or paused, otherwise a printer error; a print the printer paused itself (how an AMS fault looks) is announced too. Transitions, not levels - a printer sitting in error is one alert, not one per frame - and a connection's first sight of a printer raises nothing, so a gateway that reconnects does not re-announce known faults. AI: the worker's Valkey publications are stored when confidence is at least ALERT_MIN_CONFIDENCE (default 0.6); the worker deliberately does not threshold, so the policy lives here. A bad message costs one message, not the listener. e2e-authz.sh gains the scoping/ack cases; new e2e-alerts.sh drives a stand-in gateway over the real /ws/edge/v1 and the real Valkey channel (checked to fail when the derivation is broken).
189 lines
6.6 KiB
TypeScript
189 lines
6.6 KiB
TypeScript
import { and, eq, inArray } from "drizzle-orm";
|
|
import { ForbiddenError, NotFoundError } from "@continuum/ts-core";
|
|
import { db } from "../db";
|
|
import { farms, orgMembers, printJobs, printerAlerts, printers } from "../db/schema";
|
|
import { roleSatisfies, type OrgRole } from "./roles";
|
|
|
|
/**
|
|
* Authorization for everything except the edge-gateway socket, which
|
|
* authenticates as a farm rather than as a user.
|
|
*
|
|
* One rule, applied at the top of every handler: resolve the thing being
|
|
* acted on back to an organisation, and check the caller's membership role
|
|
* there. user -> org -> farm -> printer, with nothing carrying permissions of
|
|
* its own in between.
|
|
*
|
|
* ## 404 rather than 403 for non-members
|
|
*
|
|
* The `require*` helpers below answer "not found" when the record exists but
|
|
* the caller is not in its organisation. That is deliberate: a 403 would
|
|
* confirm that a given farm id exists and belongs to someone, which is an
|
|
* enumeration oracle over every farm and printer in the system. 403 is kept
|
|
* for the case where the caller *can* see the record and simply is not
|
|
* allowed to do this to it - a viewer trying to move a print head - where
|
|
* saying so is useful and leaks nothing they did not already know.
|
|
*/
|
|
|
|
/** The organisations this user belongs to, with their role in each. */
|
|
export function listMemberships(userId: string) {
|
|
return db
|
|
.select({ orgId: orgMembers.orgId, role: orgMembers.role, joinedAt: orgMembers.createdAt })
|
|
.from(orgMembers)
|
|
.where(eq(orgMembers.userId, userId));
|
|
}
|
|
|
|
/**
|
|
* Subquery of the org ids this user belongs to, for use inside a `where`.
|
|
*
|
|
* A subquery rather than a fetched array of ids so list endpoints stay one
|
|
* round trip, and so a user in many organisations does not build an IN list
|
|
* whose length depends on their membership count.
|
|
*/
|
|
function orgIdsFor(userId: string) {
|
|
return db.select({ id: orgMembers.orgId }).from(orgMembers).where(eq(orgMembers.userId, userId));
|
|
}
|
|
|
|
/** `where` condition limiting `farms` to those the user can see. */
|
|
export function visibleFarms(userId: string) {
|
|
return inArray(farms.orgId, orgIdsFor(userId));
|
|
}
|
|
|
|
/** `where` condition limiting `printers` to those the user can see. */
|
|
export function visiblePrinters(userId: string) {
|
|
return inArray(
|
|
printers.farmId,
|
|
db.select({ id: farms.id }).from(farms).where(visibleFarms(userId)),
|
|
);
|
|
}
|
|
|
|
/** `where` condition limiting `print_jobs` to those the user can see. */
|
|
export function visibleJobs(userId: string) {
|
|
return inArray(
|
|
printJobs.farmId,
|
|
db.select({ id: farms.id }).from(farms).where(visibleFarms(userId)),
|
|
);
|
|
}
|
|
|
|
/** `where` condition limiting `printer_alerts` to those the user can see. */
|
|
export function visibleAlerts(userId: string) {
|
|
return inArray(
|
|
printerAlerts.farmId,
|
|
db.select({ id: farms.id }).from(farms).where(visibleFarms(userId)),
|
|
);
|
|
}
|
|
|
|
function check(role: OrgRole | null, minimum: OrgRole, missing: string, action: string): OrgRole {
|
|
if (role === null) throw new NotFoundError(missing);
|
|
if (!roleSatisfies(role, minimum)) {
|
|
throw new ForbiddenError(`${action} requires the '${minimum}' role; you are '${role}'`);
|
|
}
|
|
return role;
|
|
}
|
|
|
|
/** Asserts the caller holds at least `minimum` in this organisation. */
|
|
export async function requireOrg(
|
|
userId: string,
|
|
orgId: string,
|
|
minimum: OrgRole,
|
|
action = "this operation",
|
|
): Promise<OrgRole> {
|
|
const [row] = await db
|
|
.select({ role: orgMembers.role })
|
|
.from(orgMembers)
|
|
.where(and(eq(orgMembers.orgId, orgId), eq(orgMembers.userId, userId)))
|
|
.limit(1);
|
|
return check(row?.role ?? null, minimum, `Organisation ${orgId} not found`, action);
|
|
}
|
|
|
|
/**
|
|
* Asserts the caller holds at least `minimum` in the farm's organisation.
|
|
*
|
|
* A left join so that "no such farm" and "a farm you are not a member of"
|
|
* run the same query and return the same 404 - see the note above.
|
|
*/
|
|
export async function requireFarm(
|
|
userId: string,
|
|
farmId: string,
|
|
minimum: OrgRole,
|
|
action = "this operation",
|
|
): Promise<{ orgId: string; role: OrgRole }> {
|
|
const [row] = await db
|
|
.select({ orgId: farms.orgId, role: orgMembers.role })
|
|
.from(farms)
|
|
.leftJoin(
|
|
orgMembers,
|
|
and(eq(orgMembers.orgId, farms.orgId), eq(orgMembers.userId, userId)),
|
|
)
|
|
.where(eq(farms.id, farmId))
|
|
.limit(1);
|
|
|
|
const role = check(row?.role ?? null, minimum, `Farm ${farmId} not found`, action);
|
|
return { orgId: row!.orgId, role };
|
|
}
|
|
|
|
/** As `requireFarm`, resolved through the printer's farm. */
|
|
export async function requirePrinter(
|
|
userId: string,
|
|
printerId: string,
|
|
minimum: OrgRole,
|
|
action = "this operation",
|
|
): Promise<{ farmId: string; orgId: string; role: OrgRole }> {
|
|
const [row] = await db
|
|
.select({ farmId: printers.farmId, orgId: farms.orgId, role: orgMembers.role })
|
|
.from(printers)
|
|
.innerJoin(farms, eq(farms.id, printers.farmId))
|
|
.leftJoin(
|
|
orgMembers,
|
|
and(eq(orgMembers.orgId, farms.orgId), eq(orgMembers.userId, userId)),
|
|
)
|
|
.where(eq(printers.id, printerId))
|
|
.limit(1);
|
|
|
|
const role = check(row?.role ?? null, minimum, `Printer ${printerId} not found`, action);
|
|
return { farmId: row!.farmId, orgId: row!.orgId, role };
|
|
}
|
|
|
|
/** As `requireFarm`, resolved through the job's farm. */
|
|
export async function requireJob(
|
|
userId: string,
|
|
jobId: string,
|
|
minimum: OrgRole,
|
|
action = "this operation",
|
|
): Promise<{ farmId: string; orgId: string; role: OrgRole }> {
|
|
const [row] = await db
|
|
.select({ farmId: printJobs.farmId, orgId: farms.orgId, role: orgMembers.role })
|
|
.from(printJobs)
|
|
.innerJoin(farms, eq(farms.id, printJobs.farmId))
|
|
.leftJoin(
|
|
orgMembers,
|
|
and(eq(orgMembers.orgId, farms.orgId), eq(orgMembers.userId, userId)),
|
|
)
|
|
.where(eq(printJobs.id, jobId))
|
|
.limit(1);
|
|
|
|
const role = check(row?.role ?? null, minimum, `Print job ${jobId} not found`, action);
|
|
return { farmId: row!.farmId, orgId: row!.orgId, role };
|
|
}
|
|
|
|
/** As `requireFarm`, resolved through the alert's farm. */
|
|
export async function requireAlert(
|
|
userId: string,
|
|
alertId: string,
|
|
minimum: OrgRole,
|
|
action = "this operation",
|
|
): Promise<{ farmId: string; orgId: string; role: OrgRole }> {
|
|
const [row] = await db
|
|
.select({ farmId: printerAlerts.farmId, orgId: farms.orgId, role: orgMembers.role })
|
|
.from(printerAlerts)
|
|
.innerJoin(farms, eq(farms.id, printerAlerts.farmId))
|
|
.leftJoin(
|
|
orgMembers,
|
|
and(eq(orgMembers.orgId, farms.orgId), eq(orgMembers.userId, userId)),
|
|
)
|
|
.where(eq(printerAlerts.id, alertId))
|
|
.limit(1);
|
|
|
|
const role = check(row?.role ?? null, minimum, `Alert ${alertId} not found`, action);
|
|
return { farmId: row!.farmId, orgId: row!.orgId, role };
|
|
}
|