Repository navigation
feat(admin): manage OpenMapX instances through the admin panel - #692
Draft
Medformatik wants to merge 110 commits into
Draft
Medformatik wants to merge 110 commits into
Medformatik wants to merge 110 commits into
Conversation
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
Comment on lines
+16
to
+49
| scoped.addHook("onRequest", async (request, reply) => { | ||
| const candidate = createHash("sha256") | ||
| .update(request.headers.authorization ?? "") | ||
| .digest(); | ||
| const expected = createHash("sha256").update(`Bearer ${options.apiToken}`).digest(); | ||
| if (!timingSafeEqual(candidate, expected)) | ||
| return reply.code(401).send({ error: "Artifact stream authentication required" }); | ||
| if (options.admissionsOpen?.() === false) | ||
| return reply.code(503).send({ error: "Artifact transfer admission is draining" }); | ||
| try { | ||
| const capability = verifyArtifactStreamCapability( | ||
| options.apiToken, | ||
| String(request.headers["x-openmapx-artifact-capability"] ?? ""), | ||
| ); | ||
| const params = request.params as { stageId: string }; | ||
| if ( | ||
| capability.stageId !== params.stageId || | ||
| capability.direction !== (request.method === "PUT" ? "upload" : "download") | ||
| ) | ||
| throw Error(); | ||
| await options.store.getStage({ | ||
| stageId: capability.stageId, | ||
| ownerId: capability.ownerId, | ||
| purpose: capability.purpose, | ||
| }); | ||
| } catch (error) { | ||
| return reply.code(error instanceof ArtifactTransferError ? error.statusCode : 403).send({ | ||
| error: | ||
| error instanceof ArtifactTransferError | ||
| ? error.message | ||
| : "Artifact stream capability refused", | ||
| }); | ||
| } | ||
| }); |
Comment on lines
+71
to
+125
| async (request, reply) => { | ||
| if ( | ||
| request.headers["content-type"]?.split(";")[0].trim().toLowerCase() !== | ||
| "application/octet-stream" | ||
| ) | ||
| return reply | ||
| .code(415) | ||
| .send({ error: "Artifact upload requires application/octet-stream" }); | ||
| const capability = verifyArtifactStreamCapability( | ||
| options.apiToken, | ||
| String(request.headers["x-openmapx-artifact-capability"]), | ||
| ); | ||
| const abort = new AbortController(); | ||
| const disconnected = () => { | ||
| if (!reply.raw.writableFinished) abort.abort(); | ||
| }; | ||
| request.raw.on("aborted", disconnected); | ||
| reply.raw.on("close", disconnected); | ||
| try { | ||
| const stage = await options.store.getStage({ | ||
| stageId: capability.stageId, | ||
| ownerId: capability.ownerId, | ||
| purpose: capability.purpose, | ||
| }); | ||
| if (stage.bytes > capability.byteCap) | ||
| throw new ArtifactTransferError(413, "Artifact stream exceeds capability limit"); | ||
| const source = request.body as Readable; | ||
| const bounded = async function* () { | ||
| let bytes = 0; | ||
| for await (const chunk of source) { | ||
| bytes += chunk.length; | ||
| if (bytes > capability.byteCap) | ||
| throw new ArtifactTransferError(413, "Artifact stream exceeds capability limit"); | ||
| yield chunk; | ||
| } | ||
| }; | ||
| const contentLength = | ||
| request.headers["content-length"] === undefined | ||
| ? undefined | ||
| : Number(request.headers["content-length"]); | ||
| const result = await options.store.upload({ | ||
| ...capability, | ||
| body: Readable.toWeb(Readable.from(bounded())) as ReadableStream<Uint8Array>, | ||
| contentLength, | ||
| signal: abort.signal, | ||
| }); | ||
| return reply | ||
| .header("Cache-Control", "no-store") | ||
| .header("X-Content-Type-Options", "nosniff") | ||
| .send(result); | ||
| } finally { | ||
| request.raw.off("aborted", disconnected); | ||
| reply.raw.off("close", disconnected); | ||
| } | ||
| }, |
Comment on lines
+130
to
+161
| async (request, reply) => { | ||
| const capability = verifyArtifactStreamCapability( | ||
| options.apiToken, | ||
| String(request.headers["x-openmapx-artifact-capability"]), | ||
| ); | ||
| const abort = new AbortController(); | ||
| const disconnected = () => { | ||
| if (!reply.raw.writableFinished) abort.abort(); | ||
| }; | ||
| reply.raw.on("close", disconnected); | ||
| try { | ||
| await options.store.openDownload( | ||
| { ...capability, signal: abort.signal }, | ||
| async ({ body, headers, signal }) => { | ||
| if (Number(headers.get("Content-Length")) > capability.byteCap) | ||
| throw new ArtifactTransferError(413, "Artifact stream exceeds capability limit"); | ||
| headers.forEach((value, name) => { | ||
| reply.header(name, value); | ||
| }); | ||
| reply.hijack(); | ||
| reply.raw.writeHead(200, Object.fromEntries(headers.entries())); | ||
| await pipeline( | ||
| Readable.fromWeb(body as import("node:stream/web").ReadableStream<Uint8Array>), | ||
| reply.raw, | ||
| { signal }, | ||
| ); | ||
| }, | ||
| ); | ||
| } finally { | ||
| reply.raw.off("close", disconnected); | ||
| } | ||
| }, |
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
The administration panel previously exposed incomplete host operations and left routine instance management to the CLI. This brings supported administration into the UI, with durable jobs and recovery before expanding coverage.
Initial host installation, offline control-plane recovery and destructive whole-stack shutdown remain documented host operations.
Verification
Release and upgrade impact
Product impact: breaking, migration, operator-action
Apply the normal API migrations and upgrade the control services together. Preserve original installation keys and unresolved job records. Private host-control directory prerequisites and reserved community configuration names are documented in the release fragments. Changesets cover the public extension contracts and rebuilt extension CLI.