Skip to content

Commit

Permalink
Merge branch 'bugfix/CLDSRV-584/backbeat-api' into tmp/octopus/w/7.70…
Browse files Browse the repository at this point in the history
…/bugfix/CLDSRV-584/backbeat-api
  • Loading branch information
bert-e committed Dec 20, 2024
2 parents 9244f1a + 0ec3584 commit d11eab0
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 31 deletions.
5 changes: 4 additions & 1 deletion lib/routes/routeBackbeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,10 @@ function routeBackbeat(clientIP, request, response, log) {
[request.query.operation](request, response, log, next);
}
const versioningConfig = bucketInfo.getVersioningConfiguration();
if (!versioningConfig || versioningConfig.Status !== 'Enabled') {
// The following makes sure that only replication destination-related operations
// target buckets with versioning enabled.
const isVersioningRequired = request.headers['x-scal-versioning-required'] === 'true';
if (isVersioningRequired && (!versioningConfig || versioningConfig.Status !== 'Enabled')) {
log.debug('bucket versioning is not enabled', {
method: request.method,
bucketName: request.bucketName,
Expand Down
128 changes: 98 additions & 30 deletions tests/functional/raw-node/test/routes/routeBackbeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const TEST_BUCKET = 'backbeatbucket';
const TEST_ENCRYPTED_BUCKET = 'backbeatbucket-encrypted';
const TEST_KEY = 'fookey';
const NONVERSIONED_BUCKET = 'backbeatbucket-non-versioned';
const VERSION_SUSPENDED_BUCKET = 'backbeatbucket-version-suspended';
const BUCKET_FOR_NULL_VERSION = 'backbeatbucket-null-version';

const testArn = 'aws::iam:123456789012:user/bart';
Expand Down Expand Up @@ -129,7 +130,8 @@ describeSkipIfAWS('backbeat routes', () => {
bucketUtil = new BucketUtility(
'default', { signatureVersion: 'v4' });
s3 = bucketUtil.s3;
bucketUtil.emptyManyIfExists([TEST_BUCKET, TEST_ENCRYPTED_BUCKET, NONVERSIONED_BUCKET])
bucketUtil.emptyManyIfExists([TEST_BUCKET, TEST_ENCRYPTED_BUCKET, NONVERSIONED_BUCKET,
VERSION_SUSPENDED_BUCKET])
.then(() => s3.createBucket({ Bucket: TEST_BUCKET }).promise())
.then(() => s3.putBucketVersioning(
{
Expand All @@ -139,6 +141,12 @@ describeSkipIfAWS('backbeat routes', () => {
.then(() => s3.createBucket({
Bucket: NONVERSIONED_BUCKET,
}).promise())
.then(() => s3.createBucket({ Bucket: VERSION_SUSPENDED_BUCKET }).promise())
.then(() => s3.putBucketVersioning(
{
Bucket: VERSION_SUSPENDED_BUCKET,
VersioningConfiguration: { Status: 'Suspended' },
}).promise())
.then(() => s3.createBucket({ Bucket: TEST_ENCRYPTED_BUCKET }).promise())
.then(() => s3.putBucketVersioning(
{
Expand Down Expand Up @@ -172,6 +180,8 @@ describeSkipIfAWS('backbeat routes', () => {
.then(() => s3.deleteBucket({ Bucket: TEST_ENCRYPTED_BUCKET }).promise())
.then(() =>
s3.deleteBucket({ Bucket: NONVERSIONED_BUCKET }).promise())
.then(() =>
s3.deleteBucket({ Bucket: VERSION_SUSPENDED_BUCKET }).promise())
.then(() => done(), err => done(err))
);

Expand Down Expand Up @@ -1508,37 +1518,95 @@ describeSkipIfAWS('backbeat routes', () => {
});
});

it('should refuse PUT data if bucket is not versioned',
done => makeBackbeatRequest({
method: 'PUT', bucket: NONVERSIONED_BUCKET,
objectKey: testKey, resourceType: 'data',
queryObj: { v2: '' },
headers: {
'content-length': testData.length,
'x-scal-canonical-id': testArn,
const testCases = [
{
description: 'bucket is version suspended',
bucket: VERSION_SUSPENDED_BUCKET,
},
authCredentials: backbeatAuthCredentials,
requestBody: testData,
},
err => {
assert.strictEqual(err.code, 'InvalidBucketState');
done();
}));

it('should refuse PUT metadata if bucket is not versioned',
done => makeBackbeatRequest({
method: 'PUT', bucket: NONVERSIONED_BUCKET,
objectKey: testKey, resourceType: 'metadata',
queryObj: {
versionId: versionIdUtils.encode(testMd.versionId),
{
description: 'bucket is not versioned',
bucket: NONVERSIONED_BUCKET,
},
authCredentials: backbeatAuthCredentials,
requestBody: JSON.stringify(testMd),
},
err => {
assert.strictEqual(err.code, 'InvalidBucketState');
done();
}));
];

testCases.forEach(({ description, bucket }) => {
it(`should PUT metadata and data if ${description} and x-scal-versioning-required is not set`, done => {
async.waterfall([next => {
makeBackbeatRequest({
method: 'PUT', bucket,
objectKey: 'test-updatemd-key',
resourceType: 'data',
queryObj: { v2: '' },
headers: {
'content-length': testData.length,
'x-scal-canonical-id': testArn,
},
authCredentials: backbeatAuthCredentials,
requestBody: testData,
}, next);
}, (response, next) => {
assert.strictEqual(response.statusCode, 200);
const newMd = getMetadataToPut(response);
makeBackbeatRequest({
method: 'PUT', bucket,
objectKey: 'test-updatemd-key',
resourceType: 'metadata',
queryObj: {
versionId: versionIdUtils.encode(testMd.versionId),
},
authCredentials: backbeatAuthCredentials,
requestBody: JSON.stringify(newMd),
}, next);
}], err => {
assert.ifError(err);
done();
});
});
});

testCases.forEach(({ description, bucket }) => {
it(`should refuse PUT data if ${description} and x-scal-versioning-required is true`, done => {
makeBackbeatRequest({
method: 'PUT',
bucket,
objectKey: testKey,
resourceType: 'data',
queryObj: { v2: '' },
headers: {
'content-length': testData.length,
'x-scal-canonical-id': testArn,
'x-scal-versioning-required': 'true',
},
authCredentials: backbeatAuthCredentials,
requestBody: testData,
}, err => {
assert.strictEqual(err.code, 'InvalidBucketState');
done();
});
});
});

testCases.forEach(({ description, bucket }) => {
it(`should refuse PUT metadata if ${description} and x-scal-versioning-required is true`, done => {
makeBackbeatRequest({
method: 'PUT',
bucket,
objectKey: testKey,
resourceType: 'metadata',
queryObj: {
versionId: versionIdUtils.encode(testMd.versionId),
},
headers: {
'x-scal-versioning-required': 'true',
},
authCredentials: backbeatAuthCredentials,
requestBody: JSON.stringify(testMd),
}, err => {
assert.strictEqual(err.code, 'InvalidBucketState');
done();
});
});
});

it('should refuse PUT data if no x-scal-canonical-id header ' +
'is provided', done => makeBackbeatRequest({
Expand Down
185 changes: 185 additions & 0 deletions tests/unit/routes/routeBackbeat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
const assert = require('assert');
const sinon = require('sinon');
const metadataUtils = require('../../../lib/metadata/metadataUtils');
const storeObject = require('../../../lib/api/apiUtils/object/storeObject');
const metadata = require('../../../lib/metadata/wrapper');
const { DummyRequestLogger } = require('../helpers');
const DummyRequest = require('../DummyRequest');

const log = new DummyRequestLogger();

function prepareDummyRequest(headers = {}) {
const request = new DummyRequest({
hostname: 'localhost',
method: 'PUT',
url: '/_/backbeat/metadata/bucket0/key0',
port: 80,
headers,
socket: {
remoteAddress: '0.0.0.0',
},
}, '{"replicationInfo":"{}"}');
return request;
}

describe('routeBackbeat', () => {
let mockResponse;
let mockRequest;
let sandbox;
let endPromise;
let resolveEnd;
let routeBackbeat;

beforeEach(() => {
sandbox = sinon.createSandbox();

// create a Promise that resolves when response.end is called
endPromise = new Promise((resolve) => { resolveEnd = resolve; });

mockResponse = {
statusCode: null,
body: null,
setHeader: () => {},
writeHead: sandbox.spy(statusCode => {
mockResponse.statusCode = statusCode;
}),
end: sandbox.spy((body, encoding, callback) => {
mockResponse.body = JSON.parse(body);
if (callback) callback();
resolveEnd(); // Resolve the Promise when end is called
}),
};

mockRequest = prepareDummyRequest();

sandbox.stub(metadataUtils, 'standardMetadataValidateBucketAndObj');
sandbox.stub(storeObject, 'dataStore');

// Clear require cache for routeBackbeat to make sure fresh module with stubbed dependencies
delete require.cache[require.resolve('../../../lib/routes/routeBackbeat')];
routeBackbeat = require('../../../lib/routes/routeBackbeat');
});

afterEach(() => {
sandbox.restore();
});

const rejectionTests = [
{
description: 'should reject CRR destination (putData) requests when versioning is disabled',
method: 'PUT',
url: '/_/backbeat/data/bucket0/key0',
},
{
description: 'should reject CRR destination (putMetadata) requests when versioning is disabled',
method: 'PUT',
url: '/_/backbeat/metadata/bucket0/key0',
},
];

rejectionTests.forEach(({ description, method, url }) => {
it(description, async () => {
mockRequest.method = method;
mockRequest.url = url;
mockRequest.headers = {
'x-scal-versioning-required': 'true',
};
metadataUtils.standardMetadataValidateBucketAndObj.callsFake((params, denies, log, callback) => {
const bucketInfo = {
getVersioningConfiguration: () => ({ Status: 'Disabled' }),
};
const objMd = {};
callback(null, bucketInfo, objMd);
});

routeBackbeat('127.0.0.1', mockRequest, mockResponse, log);

void await endPromise;

assert.strictEqual(mockResponse.statusCode, 409);
assert.strictEqual(mockResponse.body.code, 'InvalidBucketState');
});
});

it('should allow non-CRR destination (getMetadata) requests regardless of versioning', async () => {
mockRequest.method = 'GET';

metadataUtils.standardMetadataValidateBucketAndObj.callsFake((params, denies, log, callback) => {
const bucketInfo = {
getVersioningConfiguration: () => ({ Status: 'Disabled' }),
};
const objMd = {};
callback(null, bucketInfo, objMd);
});

routeBackbeat('127.0.0.1', mockRequest, mockResponse, log);

void await endPromise;

assert.strictEqual(mockResponse.statusCode, 200);
assert.deepStrictEqual(mockResponse.body, { Body: '{}' });
});

it('should allow CRR destination requests (putMetadata) when versioning is enabled', async () => {
mockRequest.method = 'PUT';
mockRequest.url = '/_/backbeat/metadata/bucket0/key0';
mockRequest.headers = {
'x-scal-versioning-required': 'true',
};
mockRequest.destroy = () => {};

sandbox.stub(metadata, 'putObjectMD').callsFake((bucketName, objectKey, omVal, options, logParam, cb) => {
cb(null, {});
});

metadataUtils.standardMetadataValidateBucketAndObj.callsFake((params, denies, log, callback) => {
const bucketInfo = {
getVersioningConfiguration: () => ({ Status: 'Enabled' }),
isVersioningEnabled: () => true,
};
const objMd = {};
callback(null, bucketInfo, objMd);
});

routeBackbeat('127.0.0.1', mockRequest, mockResponse, log);

void await endPromise;

assert.strictEqual(mockResponse.statusCode, 200);
assert.deepStrictEqual(mockResponse.body, {});
});

it('should allow CRR destination requests (putData) when versioning is enabled', async () => {
const md5 = '1234';
mockRequest.method = 'PUT';
mockRequest.url = '/_/backbeat/data/bucket0/key0';
mockRequest.headers = {
'x-scal-canonical-id': 'id',
'content-md5': md5,
'content-length': '0',
'x-scal-versioning-required': 'true',
};
mockRequest.destroy = () => {};

metadataUtils.standardMetadataValidateBucketAndObj.callsFake((params, denies, log, callback) => {
const bucketInfo = {
getVersioningConfiguration: () => ({ Status: 'Enabled' }),
isVersioningEnabled: () => true,
getLocationConstraint: () => undefined,
};
const objMd = {};
callback(null, bucketInfo, objMd);
});
storeObject.dataStore.callsFake((objectContext, cipherBundle, stream, size,
streamingV4Params, backendInfo, log, callback) => {
callback(null, {}, md5);
});

routeBackbeat('127.0.0.1', mockRequest, mockResponse, log);

void await endPromise;

assert.strictEqual(mockResponse.statusCode, 200);
assert.deepStrictEqual(mockResponse.body, [{}]);
});
});

0 comments on commit d11eab0

Please sign in to comment.