Skip to content

Commit

Permalink
Added an unlocking endpoint for admins to fix failed uploads.
Browse files Browse the repository at this point in the history
  • Loading branch information
LTLA committed May 20, 2024
1 parent 5f58f49 commit 3170de4
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "gypsum",
"version": "1.0.1",
"version": "1.0.2",
"type": "module",
"description": "A small ArtifactDB API on Cloudflare Workers",
"main": "src/index.js",
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as auth from "./utils/permissions.js";
import * as http from "./utils/http.js";
import * as s3 from "./utils/s3.js";
import * as read from "./read.js";
import * as lock from "./lock.js";

const router = IttyRouter();

Expand Down Expand Up @@ -84,6 +85,8 @@ router.post("/refresh/latest/:project/:asset", version.refreshLatestVersionHandl

router.post("/refresh/usage/:project", quota.refreshQuotaUsageHandler);

router.delete("/unlock/:project", lock.unlockProjectHandler);

/*** Download ***/

router.head("/file/:key", read.headFileHandler);
Expand Down
10 changes: 10 additions & 0 deletions src/lock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as auth from "./utils/permissions.js";
import * as lock from "./utils/lock.js";

export async function unlockProjectHandler(request, env, nonblockers) {
let token = auth.extractBearerToken(request);
await auth.checkAdminPermissions(token, env, nonblockers);
let project = decodeURIComponent(request.params.project);
await lock.unlockProject(project, env);
return new Response(null, { status: 200 });
}
16 changes: 16 additions & 0 deletions swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,22 @@
}
},

"/unlock/{project}": {
"delete": {
"summary": "Unlock a project. This is occasionally necessary, e.g., due to an incomplete upload that was not properly aborted.",
"parameters": [
{ "$ref": "#/components/parameters/project" }
],
"security": [ { "admin": [] } ],
"responses": {
"200": { "description": "Successful removal of the lock. This is reported even if `project` does not exist or has no lock." },
"401": { "$ref": "#/components/responses/401" },
"403": { "$ref": "#/components/responses/403" }
},
"tags": [ "Refresh" ]
}
},

"/file/{key}": {
"get": {
"summary": "Download the file with the specified key in the bucket. This is a REST-based replacement for `get-object` from the AWS S3 API.",
Expand Down
48 changes: 48 additions & 0 deletions tests/lock.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as lock from "../src/utils/lock.js";
import * as lckh from "../src/lock.js";
import * as gh from "../src/utils/github.js";
import * as setup from "./setup.js";
import * as pkeys from "../src/utils/internal.js";

beforeAll(async () => {
const env = getMiniflareBindings();
await setup.simpleMockProject(env);
let rigging = gh.enableTestRigging();
setup.mockGitHubIdentities(rigging);
})

test("unlockProjectHandler works correctly", async () => {
const env = getMiniflareBindings();

let req = new Request("http://localhost", {
method: "DELETE",
headers: { "Content-Type": "application/json" }
});
req.headers.set("Authorization", "Bearer " + setup.mockTokenAdmin);
req.params = { project: "test" };

// Unlocking works if it's not locked.
expect((await lckh.unlockProjectHandler(req, env, [])).status).toBe(200);
expect(await env.BOUND_BUCKET.head(pkeys.lock("test"))).toBeNull()

// Unlocking works if it's locked.
await lock.lockProject("test", "whee", "bar", "SESSION_KEY", env)
expect((await lckh.unlockProjectHandler(req, env, [])).status).toBe(200);
expect(await env.BOUND_BUCKET.head(pkeys.lock("test"))).toBeNull()

// Unlocking works if the project doesn't even exist.
req.params = { project: "test-does-not-exist" };
expect((await lckh.unlockProjectHandler(req, env, [])).status).toBe(200);
})

test("unlockProjectHandler works correctly if user is not authorized", async () => {
const env = getMiniflareBindings();
let req = new Request("http://localhost", {
method: "DELETE",
headers: { "Content-Type": "application/json" }
});
req.params = { project: "test" };
req.headers.set("Authorization", "Bearer " + setup.mockTokenUser);
await setup.expectError(lckh.unlockProjectHandler(req, env, []), "not an administrator");
})

0 comments on commit 3170de4

Please sign in to comment.