diff --git a/src/helpers/copy.js b/src/helpers/copy.js
index 39fd4eb2..d984278f 100644
--- a/src/helpers/copy.js
+++ b/src/helpers/copy.js
@@ -19,6 +19,11 @@ const BAD_CONTENT_TYPE_ERROR = {
status: 400,
};
+const CROSS_ORG_ERROR = {
+ body: JSON.stringify({ error: 'Destination must be in the same org as the source.' }),
+ status: 400,
+};
+
export default async function copyHelper(req, daCtx) {
let formData;
try {
@@ -32,7 +37,12 @@ export default async function copyHelper(req, daCtx) {
const continuationToken = formData.get('continuation-token');
const lower = fullDest.slice(1).toLowerCase();
const sanitized = lower.endsWith('/') ? lower.slice(0, -1) : lower;
- const destination = sanitized.split('/').slice(1).join('/');
+
+ // Reject cross-org destinations
+ const [destOrg, ...destParts] = sanitized.split('/');
+ if (destOrg !== daCtx.org) return { error: CROSS_ORG_ERROR };
+
+ const destination = destParts.join('/');
const source = daCtx.key;
return { source, destination, continuationToken };
}
diff --git a/src/helpers/move.js b/src/helpers/move.js
index 62604770..8f3c6f01 100644
--- a/src/helpers/move.js
+++ b/src/helpers/move.js
@@ -20,6 +20,11 @@ const NO_PARENT_ERROR = {
status: 400,
};
+const CROSS_ORG_ERROR = {
+ body: JSON.stringify({ error: 'Destination must be in the same org as the source.' }),
+ status: 400,
+};
+
export default async function moveHelper(req, daCtx) {
try {
const formData = await req.formData();
@@ -28,7 +33,12 @@ export default async function moveHelper(req, daCtx) {
if (!fullDest) return { error: NO_DEST_ERROR };
const lower = fullDest.slice(1).toLowerCase();
const sanitized = lower.endsWith('/') ? lower.slice(0, -1) : lower;
- let destination = sanitized.split('/').slice(1).join('/');
+
+ // Reject cross-org destinations
+ const [destOrg, ...destParts] = sanitized.split('/');
+ if (destOrg !== daCtx.org) return { error: CROSS_ORG_ERROR };
+
+ let destination = destParts.join('/');
const source = daCtx.key;
// Ensure destination is not child of source
diff --git a/test/it/it-tests.js b/test/it/it-tests.js
index f37b988d..55883f39 100644
--- a/test/it/it-tests.js
+++ b/test/it/it-tests.js
@@ -451,6 +451,75 @@ export default (ctx) => describe('Integration Tests: it tests', function () {
assert.ok(fileNames.includes('page2'), 'Should list page2');
});
+ it('[super user] should copy a page within the org', async () => {
+ const {
+ serverUrl, org, repo, superUser,
+ } = ctx;
+ const formData = new FormData();
+ formData.append('destination', `/${org}/${repo}/test-folder/page1-copy.html`);
+
+ let resp = await fetch(`${serverUrl}/copy/${org}/${repo}/test-folder/page1.html`, {
+ method: 'POST',
+ body: formData,
+ headers: { Authorization: `Bearer ${superUser.accessToken}` },
+ });
+ assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status} - user: ${superUser.email}`);
+
+ // validate the copy exists
+ resp = await fetch(`${serverUrl}/source/${org}/${repo}/test-folder/page1-copy.html`, {
+ headers: { Authorization: `Bearer ${superUser.accessToken}` },
+ });
+ assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`);
+ const body = await resp.text();
+ assert.strictEqual(body, '
Page 1
');
+ });
+
+ it('[super user] cannot copy a page to another org', async () => {
+ const {
+ serverUrl, org, repo, superUser,
+ } = ctx;
+ const formData = new FormData();
+ formData.append('destination', `/other-${org}/${repo}/test-folder/page1-xorg.html`);
+
+ let resp = await fetch(`${serverUrl}/copy/${org}/${repo}/test-folder/page1.html`, {
+ method: 'POST',
+ body: formData,
+ headers: { Authorization: `Bearer ${superUser.accessToken}` },
+ });
+ assert.strictEqual(resp.status, 400, `Expected 400 Bad Request, got ${resp.status} - user: ${superUser.email}`);
+ const body = await resp.json();
+ assert.match(body.error, /same org/i, `Expected cross-org error, got ${body.error}`);
+
+ // validate no phantom copy was re-anchored into the source org
+ resp = await fetch(`${serverUrl}/source/${org}/${repo}/test-folder/page1-xorg.html`, {
+ headers: { Authorization: `Bearer ${superUser.accessToken}` },
+ });
+ assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status} - user: ${superUser.email}`);
+ });
+
+ it('[super user] cannot move a page to another org', async () => {
+ const {
+ serverUrl, org, repo, superUser,
+ } = ctx;
+ const formData = new FormData();
+ formData.append('destination', `/other-${org}/${repo}/test-folder/page1-moved.html`);
+
+ let resp = await fetch(`${serverUrl}/move/${org}/${repo}/test-folder/page1-copy.html`, {
+ method: 'POST',
+ body: formData,
+ headers: { Authorization: `Bearer ${superUser.accessToken}` },
+ });
+ assert.strictEqual(resp.status, 400, `Expected 400 Bad Request, got ${resp.status} - user: ${superUser.email}`);
+ const body = await resp.json();
+ assert.match(body.error, /same org/i, `Expected cross-org error, got ${body.error}`);
+
+ // validate the source was not touched
+ resp = await fetch(`${serverUrl}/source/${org}/${repo}/test-folder/page1-copy.html`, {
+ headers: { Authorization: `Bearer ${superUser.accessToken}` },
+ });
+ assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`);
+ });
+
it('[anonymous] cannot delete an object', async () => {
const {
serverUrl, org, repo, key,
diff --git a/test/routes/copy.test.js b/test/routes/copy.test.js
index ff25ed9f..97d54d90 100644
--- a/test/routes/copy.test.js
+++ b/test/routes/copy.test.js
@@ -44,11 +44,11 @@ describe('Copy Route', () => {
formData: () => formdata,
};
- const resp = await copyHandler({ req, env: {}, daCtx: { key: 'my/src.html' } });
+ const resp = await copyHandler({ req, env: {}, daCtx: { org: 'myorg', key: 'my/src.html' } });
assert.strictEqual(403, resp.status);
assert.strictEqual(copyCalled.length, 0);
- const resp2 = await copyHandler({ req, env: {}, daCtx: { key: 'my/src2.html' } });
+ const resp2 = await copyHandler({ req, env: {}, daCtx: { org: 'myorg', key: 'my/src2.html' } });
assert.strictEqual(403, resp2.status);
assert.strictEqual(copyCalled.length, 0);
@@ -58,11 +58,11 @@ describe('Copy Route', () => {
formData: () => formdata2,
};
- const resp3 = await copyHandler({ req: req2, env: {}, daCtx: { key: 'my/src.html' } });
+ const resp3 = await copyHandler({ req: req2, env: {}, daCtx: { org: 'myorg', key: 'my/src.html' } });
assert.strictEqual(403, resp3.status);
assert.strictEqual(copyCalled.length, 0);
- const resp4 = await copyHandler({ req: req2, env: {}, daCtx: { key: 'my/src2.html' } });
+ const resp4 = await copyHandler({ req: req2, env: {}, daCtx: { org: 'myorg', key: 'my/src2.html' } });
assert.strictEqual(200, resp4.status);
assert.strictEqual(copyCalled.length, 1);
assert.strictEqual('my/src2.html', copyCalled[0].d.source);
@@ -99,6 +99,63 @@ describe('Copy Route', () => {
assert.match(body.error, /Content-Type/i);
});
+ it('Test copyHandler returns 400 when destination org differs from request org', async () => {
+ const copyCalled = [];
+ const copyObject = (e, c, d, m) => {
+ copyCalled.push({
+ e, c, d, m,
+ });
+ return { status: 200 };
+ };
+
+ const copyHandler = await esmock('../../src/routes/copy.js', {
+ '../../src/storage/object/copy.js': {
+ default: copyObject,
+ },
+ '../../src/utils/auth.js': { hasPermission: () => true },
+ });
+
+ const formdata = new Map();
+ formdata.set('destination', '/otherorg/my/dest.html');
+ const req = {
+ formData: () => formdata,
+ };
+
+ const resp = await copyHandler({ req, env: {}, daCtx: { org: 'myorg', key: 'my/src.html' } });
+ assert.strictEqual(resp.status, 400);
+ assert.strictEqual(copyCalled.length, 0);
+ const body = JSON.parse(resp.body);
+ assert.match(body.error, /same org/i);
+ });
+
+ it('Test copyHandler accepts a same-org destination regardless of case', async () => {
+ const copyCalled = [];
+ const copyObject = (e, c, d, m) => {
+ copyCalled.push({
+ e, c, d, m,
+ });
+ return { status: 200 };
+ };
+
+ const copyHandler = await esmock('../../src/routes/copy.js', {
+ '../../src/storage/object/copy.js': {
+ default: copyObject,
+ },
+ '../../src/utils/auth.js': { hasPermission: () => true },
+ });
+
+ const formdata = new Map();
+ formdata.set('destination', '/MyOrg/my/dest.html');
+ const req = {
+ formData: () => formdata,
+ };
+
+ const resp = await copyHandler({ req, env: {}, daCtx: { org: 'myorg', key: 'my/src.html' } });
+ assert.strictEqual(resp.status, 200);
+ assert.strictEqual(copyCalled.length, 1);
+ assert.strictEqual(copyCalled[0].d.destination, 'my/dest.html');
+ });
+
it('Test copyHandler - no destination provided', async () => {
const copyCalled = [];
const copyObject = (e, c, d, m) => {
diff --git a/test/routes/move.test.js b/test/routes/move.test.js
index 9f0b58fd..5cf8d096 100644
--- a/test/routes/move.test.js
+++ b/test/routes/move.test.js
@@ -44,11 +44,11 @@ describe('Move Route', () => {
formData: () => formdata,
};
- const resp = await moveRoute({ req, env: {}, daCtx: { key: 'abc.html' } });
+ const resp = await moveRoute({ req, env: {}, daCtx: { org: 'someorg', key: 'abc.html' } });
assert.strictEqual(403, resp.status);
assert.strictEqual(0, moCalled.length);
- const resp2 = await moveRoute({ req, env: {}, daCtx: { key: 'zzz.html' } });
+ const resp2 = await moveRoute({ req, env: {}, daCtx: { org: 'someorg', key: 'zzz.html' } });
assert.strictEqual(403, resp2.status);
assert.strictEqual(0, moCalled.length);
@@ -58,13 +58,69 @@ describe('Move Route', () => {
formData: () => formdata2,
};
- const resp3 = await moveRoute({ req: req2, env: {}, daCtx: { key: 'abc.html' } });
+ const resp3 = await moveRoute({ req: req2, env: {}, daCtx: { org: 'someorg', key: 'abc.html' } });
assert.strictEqual(403, resp3.status);
assert.strictEqual(0, moCalled.length);
- await moveRoute({ req: req2, env: {}, daCtx: { key: 'zzz.html' } });
+ await moveRoute({ req: req2, env: {}, daCtx: { org: 'someorg', key: 'zzz.html' } });
assert.strictEqual(1, moCalled.length);
assert.strictEqual('zzz.html', moCalled[0].d.source);
assert.strictEqual('someotherdest', moCalled[0].d.destination);
});
+
+ it('Test moveRoute returns 400 when destination org differs from request org', async () => {
+ const moCalled = [];
+ const moveObject = (e, c, d) => {
+ moCalled.push({ e, c, d });
+ };
+
+ const moveRoute = await esmock('../../src/routes/move.js', {
+ '../../src/storage/object/move.js': {
+ default: moveObject,
+ },
+ '../../src/utils/auth.js': {
+ hasPermission: () => true,
+ },
+ });
+
+ const formdata = new Map();
+ formdata.set('destination', '/otherorg/somedest/');
+ const req = {
+ formData: () => formdata,
+ };
+
+ const resp = await moveRoute({ req, env: {}, daCtx: { org: 'someorg', key: 'abc.html' } });
+ assert.strictEqual(resp.status, 400);
+ assert.strictEqual(0, moCalled.length);
+ const body = JSON.parse(resp.body);
+ assert.match(body.error, /same org/i);
+ });
+
+ it('Test moveRoute accepts a same-org destination regardless of case', async () => {
+ const moCalled = [];
+ const moveObject = (e, c, d) => {
+ moCalled.push({ e, c, d });
+ return { status: 204 };
+ };
+
+ const moveRoute = await esmock('../../src/routes/move.js', {
+ '../../src/storage/object/move.js': {
+ default: moveObject,
+ },
+ '../../src/utils/auth.js': {
+ hasPermission: () => true,
+ },
+ });
+
+ const formdata = new Map();
+ formdata.set('destination', '/SomeOrg/somedest/');
+ const req = {
+ formData: () => formdata,
+ };
+
+ const resp = await moveRoute({ req, env: {}, daCtx: { org: 'someorg', key: 'abc.html' } });
+ assert.strictEqual(resp.status, 204);
+ assert.strictEqual(1, moCalled.length);
+ assert.strictEqual('somedest', moCalled[0].d.destination);
+ });
});