Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/w/8.4/bugfix/CLDSRV-293/refactor…
Browse files Browse the repository at this point in the history
…_olock_checks' into w/8.5/bugfix/CLDSRV-293/refactor_olock_checks
  • Loading branch information
tmacro committed Nov 11, 2022
2 parents 6ef88fd + af8420f commit 5f94fce
Show file tree
Hide file tree
Showing 5 changed files with 612 additions and 326 deletions.
233 changes: 163 additions & 70 deletions lib/api/apiUtils/object/objectLockHelpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const { errors } = require('arsenal');
const { errors, auth, policies } = require('arsenal');
const moment = require('moment');

const { config } = require('../../../Config');
const vault = require('../../../auth/vault');

/**
* Calculates retain until date for the locked object version
* @param {object} retention - includes days or years retention period
Expand Down Expand Up @@ -43,7 +47,7 @@ function validateHeaders(bucket, headers, log) {
!(objectLockMode && objectLockDate)) {
return errors.InvalidArgument.customizeDescription(
'x-amz-object-lock-retain-until-date and ' +
'x-amz-object-lock-mode must both be supplied'
'x-amz-object-lock-mode must both be supplied',
);
}
const validModes = new Set(['GOVERNANCE', 'COMPLIANCE']);
Expand Down Expand Up @@ -126,101 +130,190 @@ function setObjectLockInformation(headers, md, defaultRetention) {
}

/**
* isObjectLocked - checks whether object is locked or not
* @param {obect} bucket - bucket metadata
* @param {object} objectMD - object metadata
* @param {array} headers - request headers
* @return {boolean} - indicates whether object is locked or not
* Helper class for check object lock state checks
*/
function isObjectLocked(bucket, objectMD, headers) {
if (bucket.isObjectLockEnabled()) {
const objectLegalHold = objectMD.legalHold;
if (objectLegalHold) {
class ObjectLockInfo {
/**
*
* @param {object} retentionInfo - The object lock retention policy
* @param {"GOVERNANCE" | "COMPLIANCE" | null} retentionInfo.mode - Retention policy mode.
* @param {string} retentionInfo.date - Expiration date of retention policy. A string in ISO-8601 format
* @param {bool} retentionInfo.legalHold - Whether a legal hold is enable for the object
*/
constructor(retentionInfo) {
this.mode = retentionInfo.mode || null;
this.date = retentionInfo.date || null;
this.legalHold = retentionInfo.legalHold || false;
}

/**
* ObjectLockInfo.isLocked
* @returns {bool} - Whether the retention policy is active and protecting the object
*/
isLocked() {
if (this.legalHold) {
return true;
}
const retentionMode = objectMD.retentionMode;
const retentionDate = objectMD.retentionDate;
if (!retentionMode || !retentionDate) {
return false;
}
if (retentionMode === 'GOVERNANCE' &&
headers['x-amz-bypass-governance-retention']) {
return false;
}
const objectDate = moment(retentionDate);
const now = moment();
// indicates retain until date has expired
if (now.isSameOrAfter(objectDate)) {

if (!this.mode || !this.date) {
return false;
}
return true;
}
return false;
}


/* objectLockRequiresBypass will return true if the retention info change
* would require a bypass governance flag to be true.
* In order for this to be true the action must be valid as well, so going from
* COMPLIANCE to GOVERNANCE would return false unless it expired.
*/
function objectLockRequiresBypass(objectMD, retentionInfo) {
const { retentionMode: existingMode, retentionDate: existingDateISO } = objectMD;
if (!existingMode) {
return false;
return !this.isExpired();
}

const existingDate = new Date(existingDateISO);
const isExpired = existingDate < Date.now();
const isExtended = new Date(retentionInfo.date) > existingDate;
/**
* ObjectLockInfo.isGovernanceMode
* @returns {bool} - true if retention mode is GOVERNANCE
*/
isGovernanceMode() {
return this.mode === 'GOVERNANCE';
}

if (existingMode === 'GOVERNANCE' && !isExpired) {
if (retentionInfo.mode === 'GOVERNANCE' && isExtended) {
return false;
}
return true;
/**
* ObjectLockInfo.isComplianceMode
* @returns {bool} - True if retention mode is COMPLIANCE
*/
isComplianceMode() {
return this.mode === 'COMPLIANCE';
}

// an invalid retention change or unrelated to bypass
return false;
}
/**
* ObjectLockInfo.isExpired
* @returns {bool} - True if the retention policy has expired
*/
isExpired() {
const now = moment();
return this.date === null || now.isSameOrAfter(this.date);
}

function validateObjectLockUpdate(objectMD, retentionInfo, bypassGovernance) {
const { retentionMode: existingMode, retentionDate: existingDateISO } = objectMD;
if (!existingMode) {
return null;
/**
* ObjectLockInfo.isExtended
* @param {string} timestamp - Timestamp in ISO-8601 format
* @returns {bool} - True if the given timestamp is after the policy expiration date or if no expiration date is set
*/
isExtended(timestamp) {
return timestamp !== undefined && (this.date === null || moment(timestamp).isAfter(this.date));
}

const existingDate = new Date(existingDateISO);
const isExpired = existingDate < Date.now();
const isExtended = new Date(retentionInfo.date) > existingDate;
/**
* ObjectLockInfo.canModifyObject
* @param {bool} hasGovernanceBypass - Whether to bypass governance retention policies
* @returns {bool} - True if the retention policy allows the objects data to be modified (overwritten/deleted)
*/
canModifyObject(hasGovernanceBypass) {
return !this.isLocked() || (this.isGovernanceMode() && !!hasGovernanceBypass);
}

if (existingMode === 'GOVERNANCE' && !isExpired && !bypassGovernance) {
if (retentionInfo.mode === 'GOVERNANCE' && isExtended) {
return null;
/**
* ObjectLockInfo.canModifyPolicy
* @param {object} policyChanges - Proposed changes to the retention policy
* @param {"GOVERNANCE" | "COMPLIANCE" | undefined} policyChanges.mode - Retention policy mode.
* @param {string} policyChanges.date - Expiration date of retention policy. A string in ISO-8601 format
* @param {bool} hasGovernanceBypass - Whether to bypass governance retention policies
* @returns {bool} - True if the changes are allowed to be applied to the retention policy
*/
canModifyPolicy(policyChanges, hasGovernanceBypass) {
// If an object does not have a retention policy or it is expired then all changes are allowed
if (!this.isLocked()) {
return true;
}
return errors.AccessDenied;
}

if (existingMode === 'COMPLIANCE') {
if (retentionInfo.mode === 'GOVERNANCE' && !isExpired) {
return errors.AccessDenied;
// The only allowed change in compliance mode is extending the retention period
if (this.isComplianceMode()) {
if (policyChanges.mode === 'COMPLIANCE' && this.isExtended(policyChanges.date)) {
return true;
}
}

if (!isExtended) {
return errors.AccessDenied;
if (this.isGovernanceMode()) {
// Extensions are always allowed in governance mode
if (policyChanges.mode === 'GOVERNANCE' && this.isExtended(policyChanges.date)) {
return true;
}

// All other changes in governance mode require a bypass
if (hasGovernanceBypass) {
return true;
}
}

return false;
}
}

return null;
/**
*
* @param {object} headers - s3 request headers
* @returns {bool} - True if the headers is present and === "true"
*/
function hasGovernanceBypassHeader(headers) {
const bypassHeader = headers['x-amz-bypass-governance-retention'] || '';
return bypassHeader.toLowerCase() === 'true';
}


/**
* checkUserGovernanceBypass
*
* Checks for the presence of the s3:BypassGovernanceRetention permission for a given user
*
* @param {object} request - Incoming s3 request
* @param {object} authInfo - s3 authentication info
* @param {object} bucketMD - bucket metadata
* @param {string} objectKey - object key
* @param {object} log - Werelogs logger
* @param {function} cb - callback returns errors.AccessDenied if the authorization fails
* @returns {undefined} -
*/
function checkUserGovernanceBypass(request, authInfo, bucketMD, objectKey, log, cb) {
log.trace(
'object in GOVERNANCE mode and is user, checking for attached policies',
{ method: 'checkUserPolicyGovernanceBypass' },
);

const authParams = auth.server.extractParams(request, log, 's3', request.query);
const ip = policies.requestUtils.getClientIp(request, config);
const requestContextParams = {
constantParams: {
headers: request.headers,
query: request.query,
generalResource: bucketMD.getName(),
specificResource: { key: objectKey },
requesterIp: ip,
sslEnabled: request.connection.encrypted,
apiMethod: 'bypassGovernanceRetention',
awsService: 's3',
locationConstraint: bucketMD.getLocationConstraint(),
requesterInfo: authInfo,
signatureVersion: authParams.params.data.signatureVersion,
authType: authParams.params.data.authType,
signatureAge: authParams.params.data.signatureAge,
},
};
return vault.checkPolicies(requestContextParams,
authInfo.getArn(), log, (err, authorizationResults) => {
if (err) {
return cb(err);
}
if (authorizationResults[0].isAllowed !== true) {
log.trace('authorization check failed for user',
{
'method': 'checkUserPolicyGovernanceBypass',
's3:BypassGovernanceRetention': false,
});
return cb(errors.AccessDenied);
}
return cb(null);
});
}

module.exports = {
calculateRetainUntilDate,
compareObjectLockInformation,
setObjectLockInformation,
isObjectLocked,
validateHeaders,
validateObjectLockUpdate,
objectLockRequiresBypass,
hasGovernanceBypassHeader,
checkUserGovernanceBypass,
ObjectLockInfo,
};
48 changes: 43 additions & 5 deletions lib/api/multiObjectDelete.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ const createAndStoreObject = require('./apiUtils/object/createAndStoreObject');
const { metadataGetObject } = require('../metadata/metadataUtils');
const monitoring = require('../utilities/monitoringHandler');
const { config } = require('../Config');
const { isObjectLocked } = require('./apiUtils/object/objectLockHelpers');
const { isRequesterNonAccountUser } = require('./apiUtils/authorization/permissionChecks');
const { hasGovernanceBypassHeader, checkUserGovernanceBypass, ObjectLockInfo }
= require('./apiUtils/object/objectLockHelpers');
const requestUtils = policies.requestUtils;

const versionIdUtils = versioning.VersionID;
Expand Down Expand Up @@ -235,10 +236,6 @@ function getObjMetadataAndDelete(authInfo, canonicalID, request,
successfullyDeleted.push({ entry });
return callback(skipError);
}
if (versionId && isObjectLocked(bucket, objMD, request.headers)) {
log.debug('trying to delete locked object');
return callback(objectLockedError);
}
if (versionId && objMD.location &&
Array.isArray(objMD.location) && objMD.location[0]) {
// we need this information for data deletes to AWS
Expand All @@ -247,6 +244,47 @@ function getObjMetadataAndDelete(authInfo, canonicalID, request,
}
return callback(null, objMD, versionId);
}),
(objMD, versionId, callback) => {
// AWS only returns an object lock error if a version id
// is specified, else continue to create a delete marker
if (!versionId || !bucket.isObjectLockEnabled()) {
return callback(null, null, objMD, versionId);
}
const hasGovernanceBypass = hasGovernanceBypassHeader(request.headers);
if (hasGovernanceBypass && isRequesterNonAccountUser(authInfo)) {
return checkUserGovernanceBypass(request, authInfo, bucket, entry.key, log, error => {
if (error && error.is.AccessDenied) {
log.debug('user does not have BypassGovernanceRetention and object is locked', { error });
return callback(objectLockedError);
}
if (error) {
return callback(error);
}
return callback(null, hasGovernanceBypass, objMD, versionId);
});
}
return callback(null, hasGovernanceBypass, objMD, versionId);
},
(hasGovernanceBypass, objMD, versionId, callback) => {
// AWS only returns an object lock error if a version id
// is specified, else continue to create a delete marker
if (!versionId || !bucket.isObjectLockEnabled()) {
return callback(null, objMD, versionId);
}
const objLockInfo = new ObjectLockInfo({
mode: objMD.retentionMode,
date: objMD.retentionDate,
legalHold: objMD.legalHold || false,
});

// If the object can not be deleted raise an error
if (!objLockInfo.canModifyObject(hasGovernanceBypass)) {
log.debug('trying to delete locked object');
return callback(objectLockedError);
}

return callback(null, objMD, versionId);
},
(objMD, versionId, callback) =>
preprocessingVersioningDelete(bucketName, bucket, objMD,
versionId, log, (err, options) => callback(err, options,
Expand Down
Loading

0 comments on commit 5f94fce

Please sign in to comment.