Skip to content

Commit

Permalink
Evaluate the BypassGovernance permission during API authz
Browse files Browse the repository at this point in the history
Previously, we were dynamically calling the internal authorization
API to check if the identity, when not an account, has the
permission to bypass the governance lock configuration. This
call sent request contexts with information we already have
when authorizing the API, except the type of identity.

This commit introduces an optimization by including the
Bypass action, when the header is set, directly when the api
is authorized. If the identity is an account, the overhead is
lower than doing another API call: no policy are evaluated.

Both PutObjectRetention and DeleteObject API calls are updated
accordingly.
  • Loading branch information
williamlardier committed Nov 19, 2024
1 parent d11b522 commit 58a4187
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 38 deletions.
28 changes: 28 additions & 0 deletions lib/api/apiUtils/authorization/prepareRequestContexts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { policies } = require('arsenal');
const { config } = require('../../../Config');
const { hasGovernanceBypassHeader } = require('../object/objectLockHelpers');

const { RequestContext, requestUtils } = policies;
let apiMethodAfterVersionCheck;
Expand Down Expand Up @@ -193,13 +194,28 @@ function prepareRequestContexts(apiMethod, request, sourceBucket,
const putObjectLockRequestContext =
generateRequestContext('objectPutRetention');
requestContexts.push(putObjectLockRequestContext);
if (hasGovernanceBypassHeader(request.headers)) {
const checkUserGovernanceBypassRequestContext =
generateRequestContext('bypassGovernanceRetention');
requestContexts.push(checkUserGovernanceBypassRequestContext);
}
}
if (request.headers['x-amz-version-id']) {
const putObjectVersionRequestContext =
generateRequestContext('objectPutTaggingVersion');
requestContexts.push(putObjectVersionRequestContext);
}
}
} else if (apiMethodAfterVersionCheck === 'objectPutRetention' ||
apiMethodAfterVersionCheck === 'objectPutRetentionVersion') {
const putRetentionRequestContext =
generateRequestContext(apiMethodAfterVersionCheck);
requestContexts.push(putRetentionRequestContext);
if (hasGovernanceBypassHeader(request.headers)) {
const checkUserGovernanceBypassRequestContext =
generateRequestContext('bypassGovernanceRetention');
requestContexts.push(checkUserGovernanceBypassRequestContext);
}
} else if (apiMethodAfterVersionCheck === 'initiateMultipartUpload' ||
apiMethodAfterVersionCheck === 'objectPutPart' ||
apiMethodAfterVersionCheck === 'completeMultipartUpload'
Expand Down Expand Up @@ -232,11 +248,23 @@ function prepareRequestContexts(apiMethod, request, sourceBucket,
generateRequestContext('objectPutTaggingVersion');
requestContexts.push(putObjectVersionRequestContext);
}
// AWS only returns an object lock error if a version id
// is specified, else continue to create a delete marker
} else if (sourceVersionId && apiMethodAfterVersionCheck === 'objectDeleteVersion') {
const deleteRequestContext =
generateRequestContext(apiMethodAfterVersionCheck);
requestContexts.push(deleteRequestContext);
if (hasGovernanceBypassHeader(request.headers)) {
const checkUserGovernanceBypassRequestContext =
generateRequestContext('bypassGovernanceRetention');
requestContexts.push(checkUserGovernanceBypassRequestContext);
}
} else {
const requestContext =
generateRequestContext(apiMethodAfterVersionCheck);
requestContexts.push(requestContext);
}

return requestContexts;
}

Expand Down
24 changes: 3 additions & 21 deletions lib/api/objectDelete.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ const { decodeVersionId, preprocessingVersioningDelete }
= require('./apiUtils/object/versioning');
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
const monitoring = require('../utilities/monitoringHandler');
const { hasGovernanceBypassHeader, checkUserGovernanceBypass, ObjectLockInfo }
const { hasGovernanceBypassHeader, ObjectLockInfo }
= require('./apiUtils/object/objectLockHelpers');
const { isRequesterNonAccountUser } = require('./apiUtils/authorization/permissionChecks');
const { config } = require('../Config');
const { _bucketRequiresOplogUpdate } = require('./apiUtils/object/deleteObject');

Expand Down Expand Up @@ -50,6 +49,7 @@ function objectDeleteInternal(authInfo, request, log, isExpiration, cb) {
return cb(decodedVidResult);
}
const reqVersionId = decodedVidResult;
const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers);

const valParams = {
authInfo,
Expand Down Expand Up @@ -101,25 +101,7 @@ function objectDeleteInternal(authInfo, request, log, isExpiration, cb) {
return next(null, bucketMD, objMD);
});
},
function checkGovernanceBypassHeader(bucketMD, objectMD, next) {
// AWS only returns an object lock error if a version id
// is specified, else continue to create a delete marker
if (!reqVersionId) {
return next(null, null, bucketMD, objectMD);
}
const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers);
if (hasGovernanceBypass && isRequesterNonAccountUser(authInfo)) {
return checkUserGovernanceBypass(request, authInfo, bucketMD, objectKey, log, err => {
if (err) {
log.debug('user does not have BypassGovernanceRetention and object is locked');
return next(err, bucketMD);
}
return next(null, hasGovernanceBypass, bucketMD, objectMD);
});
}
return next(null, hasGovernanceBypass, bucketMD, objectMD);
},
function evaluateObjectLockPolicy(hasGovernanceBypass, bucketMD, objectMD, next) {
function evaluateObjectLockPolicy(bucketMD, objectMD, next) {
// AWS only returns an object lock error if a version id
// is specified, else continue to create a delete marker
if (!reqVersionId) {
Expand Down
20 changes: 3 additions & 17 deletions lib/api/objectPutRetention.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ const { errors, s3middleware } = require('arsenal');

const { decodeVersionId, getVersionIdResHeader, getVersionSpecificMetadataOptions } =
require('./apiUtils/object/versioning');
const { ObjectLockInfo, checkUserGovernanceBypass, hasGovernanceBypassHeader } =
const { ObjectLockInfo, hasGovernanceBypassHeader } =
require('./apiUtils/object/objectLockHelpers');
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
const { pushMetric } = require('../utapi/utilities');
const getReplicationInfo = require('./apiUtils/object/getReplicationInfo');
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
const metadata = require('../metadata/wrapper');
const { isRequesterNonAccountUser } = require('./apiUtils/authorization/permissionChecks');
const { config } = require('../Config');

const { parseRetentionXml } = s3middleware.retention;
Expand Down Expand Up @@ -49,6 +48,8 @@ function objectPutRetention(authInfo, request, log, callback) {
request,
};

const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers);

return async.waterfall([
next => {
log.trace('parsing retention information');
Expand Down Expand Up @@ -94,21 +95,6 @@ function objectPutRetention(authInfo, request, log, callback) {
return next(null, bucket, retentionInfo, objectMD);
}),
(bucket, retentionInfo, objectMD, next) => {
const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers);
if (hasGovernanceBypass && isRequesterNonAccountUser(authInfo)) {
return checkUserGovernanceBypass(request, authInfo, bucket, objectKey, log, err => {
if (err) {
if (err.is.AccessDenied) {
log.debug('user does not have BypassGovernanceRetention and object is locked');
}
return next(err, bucket);
}
return next(null, bucket, retentionInfo, hasGovernanceBypass, objectMD);
});
}
return next(null, bucket, retentionInfo, hasGovernanceBypass, objectMD);
},
(bucket, retentionInfo, hasGovernanceBypass, objectMD, next) => {
const objLockInfo = new ObjectLockInfo({
mode: objectMD.retentionMode,
date: objectMD.retentionDate,
Expand Down
161 changes: 161 additions & 0 deletions tests/unit/api/apiUtils/authorization/prepareRequestContexts.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,167 @@ describe('prepareRequestContexts', () => {
assert.strictEqual(results[2].getAction(), 'scality:GetObjectArchiveInfo');
});

it('should return s3:PutObjectRetention with header x-amz-object-lock-mode', () => {
const apiMethod = 'objectPut';
const request = makeRequest({
'x-amz-object-lock-mode': 'GOVERNANCE',
'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId);

assert.strictEqual(results.length, 2);
const expectedAction1 = 's3:PutObject';
const expectedAction2 = 's3:PutObjectRetention';
assert.strictEqual(results[0].getAction(), expectedAction1);
assert.strictEqual(results[1].getAction(), expectedAction2);
});

it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPut ' +
'with header x-amz-bypass-governance-retention', () => {
const apiMethod = 'objectPut';
const request = makeRequest({
'x-amz-object-lock-mode': 'GOVERNANCE',
'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z',
'x-amz-bypass-governance-retention': 'true',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId);

assert.strictEqual(results.length, 3);
const expectedAction1 = 's3:PutObject';
const expectedAction2 = 's3:PutObjectRetention';
const expectedAction3 = 's3:BypassGovernanceRetention';
assert.strictEqual(results[0].getAction(), expectedAction1);
assert.strictEqual(results[1].getAction(), expectedAction2);
assert.strictEqual(results[2].getAction(), expectedAction3);
});

it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPut ' +
'with header x-amz-bypass-governance-retention with version id specified', () => {
const apiMethod = 'objectPut';
const request = makeRequest({
'x-amz-object-lock-mode': 'GOVERNANCE',
'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z',
'x-amz-bypass-governance-retention': 'true',
}, {
versionId: 'vid1',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId);

assert.strictEqual(results.length, 3);
const expectedAction1 = 's3:PutObject';
const expectedAction2 = 's3:PutObjectRetention';
const expectedAction3 = 's3:BypassGovernanceRetention';
assert.strictEqual(results[0].getAction(), expectedAction1);
assert.strictEqual(results[1].getAction(), expectedAction2);
assert.strictEqual(results[2].getAction(), expectedAction3);
});

it('should return s3:PutObjectRetention with header x-amz-object-lock-mode for objectPutRetention action', () => {
const apiMethod = 'objectPutRetention';
const request = makeRequest({
'x-amz-object-lock-mode': 'GOVERNANCE',
'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId);

assert.strictEqual(results.length, 1);
const expectedAction = 's3:PutObjectRetention';
assert.strictEqual(results[0].getAction(), expectedAction);
});

it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPutRetention ' +
'with header x-amz-bypass-governance-retention', () => {
const apiMethod = 'objectPutRetention';
const request = makeRequest({
'x-amz-object-lock-mode': 'GOVERNANCE',
'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z',
'x-amz-bypass-governance-retention': 'true',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId);

assert.strictEqual(results.length, 2);
const expectedAction1 = 's3:PutObjectRetention';
const expectedAction2 = 's3:BypassGovernanceRetention';
assert.strictEqual(results[0].getAction(), expectedAction1);
assert.strictEqual(results[1].getAction(), expectedAction2);
});

it('should return s3:PutObjectRetention and s3:BypassGovernanceRetention for objectPutRetention ' +
'with header x-amz-bypass-governance-retention with version id specified', () => {
const apiMethod = 'objectPutRetention';
const request = makeRequest({
'x-amz-object-lock-mode': 'GOVERNANCE',
'x-amz-object-lock-retain-until-date': '2021-12-31T23:59:59.000Z',
'x-amz-bypass-governance-retention': 'true',
}, {
versionId: 'vid1',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId);

assert.strictEqual(results.length, 2);
const expectedAction1 = 's3:PutObjectRetention';
const expectedAction2 = 's3:BypassGovernanceRetention';
assert.strictEqual(results[0].getAction(), expectedAction1);
assert.strictEqual(results[1].getAction(), expectedAction2);
});

it('should return s3:DeleteObject for objectDelete method', () => {
const apiMethod = 'objectDelete';
const request = makeRequest();
const results = prepareRequestContexts(apiMethod, request, sourceBucket,
sourceObject, sourceVersionId);

assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].getAction(), 's3:DeleteObject');
});

it('should return s3:DeleteObjectVersion for objectDelete method with version id specified', () => {
const apiMethod = 'objectDelete';
const request = makeRequest({}, {
versionId: 'vid1',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket,
sourceObject, sourceVersionId);

assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].getAction(), 's3:DeleteObjectVersion');
});

// Now it shuld include the bypass header if set
it('should return s3:DeleteObjectVersion and s3:BypassGovernanceRetention for objectDelete method ' +
'with version id specified and x-amz-bypass-governance-retention header', () => {
const apiMethod = 'objectDelete';
const request = makeRequest({
'x-amz-bypass-governance-retention': 'true',
}, {
versionId: 'vid1',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket,
sourceObject, sourceVersionId);

assert.strictEqual(results.length, 2);
const expectedAction1 = 's3:DeleteObjectVersion';
const expectedAction2 = 's3:BypassGovernanceRetention';
assert.strictEqual(results[0].getAction(), expectedAction1);
assert.strictEqual(results[1].getAction(), expectedAction2);
});

// When there is no version ID, AWS does not return any error if the object
// is locked, but creates a delete marker
it('should only return s3:DeleteObject for objectDelete method ' +
'with x-amz-bypass-governance-retention header and no version id', () => {
const apiMethod = 'objectDelete';
const request = makeRequest({
'x-amz-bypass-governance-retention': 'true',
});
const results = prepareRequestContexts(apiMethod, request, sourceBucket,
sourceObject, sourceVersionId);

assert.strictEqual(results.length, 1);
const expectedAction = 's3:DeleteObject';
assert.strictEqual(results[0].getAction(), expectedAction);
});

['initiateMultipartUpload', 'objectPutPart', 'completeMultipartUpload'].forEach(apiMethod => {
it(`should return s3:PutObjectVersion request context action for ${apiMethod} method ` +
'with x-scal-s3-version-id header', () => {
Expand Down

0 comments on commit 58a4187

Please sign in to comment.