Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/api/apiUtils/authorization/prepareRequestContexts.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,23 @@ function prepareRequestContexts(apiMethod, request, sourceBucket,
if (requestedAttributes.filter(attr => attr != 'RestoreStatus').length > 0) {
requestContexts.push(generateRequestContext('listObjectsV2OptionalAttributes'));
}
} else if (apiMethodAfterVersionCheck === 'objectGetAttributes') {
if (request.headers['x-amz-version-id']) {
requestContexts.push(
generateRequestContext('objectGetVersion'),
generateRequestContext('objectGetVersionAttributes'),
);
} else {
requestContexts.push(
generateRequestContext('objectGet'),
generateRequestContext('objectGetAttributes'),
);
}

const attributes = request.headers['x-amz-object-attributes']?.split(',') ?? [];
if (attributes.some(attr => attr.trim().toLowerCase().startsWith('x-amz-meta-'))) {
requestContexts.push(generateRequestContext('objectGetAttributesCustom'));
}
} else {
const requestContext =
generateRequestContext(apiMethodAfterVersionCheck);
Expand Down
117 changes: 117 additions & 0 deletions lib/api/apiUtils/object/objectAttributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const { errorInstances } = require('arsenal');
const { getPartCountFromMd5 } = require('./partInfo');

/**
* Parse and validate attribute headers from a request.
* @param {object} headers - Request headers object
* @param {string} headerName - Name of the header to parse (e.g., 'x-amz-object-attributes')
* @param {Set<string>} supportedAttributes - Set of valid attribute names
* @returns {Set<string>} - set of requested attribute names
* @throws {arsenal.errors.InvalidRequest} When header is required but missing/empty
* @throws {arsenal.errors.InvalidArgument} When an invalid attribute name is specified
* @example
* // Input headers:
* { 'headerName': 'ETag, ObjectSize, x-amz-meta-custom' }
*
* // Parsed result:
* ['ETag', 'ObjectSize', 'x-amz-meta-custom']
*/
function parseAttributesHeaders(headers, headerName, supportedAttributes) {
const result = new Set();

const rawValue = headers[headerName];
if (rawValue === null || rawValue === undefined) {
return result;
}

for (const rawAttr of rawValue.split(',')) {
let attr = rawAttr.trim();

if (!supportedAttributes.has(attr)) {
attr = attr.toLowerCase();
}

if (!attr.startsWith('x-amz-meta-') && !supportedAttributes.has(attr)) {
throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.');
}

result.add(attr);
}

return result;
}

/**
* buildAttributesXml - Builds XML reponse for requested object attributes
* @param {Object} objectMD - The internal metadata object for the file/object.
* @param {string} [objectMD.content-md5] - The MD5 hash used for the ETag.
* @param {string} [objectMD.x-amz-storage-class] - The storage tier of the object.
* @param {number} [objectMD.content-length] - The size of the object in bytes.
* @param {Object} [objectMD.restoreStatus] - Information regarding the restoration of archived objects.
* @param {boolean} [objectMD.restoreStatus.inProgress] - Whether a restore is currently active.
* @param {string} [objectMD.restoreStatus.expiryDate] - The date after which the restored copy expires.
* @param {Object.<string, any>} userMetadata - Key-value pairs of user-defined metadata.
* @param {string[]} requestedAttrs - A list of specific attributes to include in the output.
* Supports 'ETag', 'ObjectParts', 'StorageClass', 'ObjectSize',
* 'RestoreStatus', and 'x-amz-meta-*' for all user metadata.
* @returns {string[]} An array of XML strings representing the requested attributes.
*/
function buildAttributesXml(objectMD, userMetadata, requestedAttrs) {
const xml = [];

const customAttributes = new Set();
for (const attribute of requestedAttrs) {
switch (attribute) {
case 'ETag':
xml.push(`<ETag>${objectMD['content-md5']}</ETag>`);
break;
case 'ObjectParts': {
const partCount = getPartCountFromMd5(objectMD);
if (partCount) {
xml.push(
'<ObjectParts>',
`<PartsCount>${partCount}</PartsCount>`,
'</ObjectParts>',
);
}
break;
}
case 'StorageClass':
xml.push(`<StorageClass>${objectMD['x-amz-storage-class']}</StorageClass>`);
break;
case 'ObjectSize':
xml.push(`<ObjectSize>${objectMD['content-length']}</ObjectSize>`);
break;
case 'RestoreStatus':
xml.push('<RestoreStatus>');
xml.push(`<IsRestoreInProgress>${!!objectMD.restoreStatus?.inProgress}</IsRestoreInProgress>`);

if (objectMD.restoreStatus?.expiryDate) {
xml.push(`<RestoreExpiryDate>${objectMD.restoreStatus?.expiryDate}</RestoreExpiryDate>`);
}

xml.push('</RestoreStatus>');
break;
case 'x-amz-meta-*':
for (const key of Object.keys(userMetadata)) {
customAttributes.add(key);
}
break;
default:
if (userMetadata[attribute]) {
customAttributes.add(attribute);
}
}
}

for (const key of customAttributes) {
xml.push(`<${key}>${userMetadata[key]}</${key}>`);
}

return xml;
}

module.exports = {
parseAttributesHeaders,
buildAttributesXml,
};
25 changes: 0 additions & 25 deletions lib/api/apiUtils/object/parseAttributesHeader.js

This file was deleted.

56 changes: 10 additions & 46 deletions lib/api/bucketGet.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const { pushMetric } = require('../utapi/utilities');
const versionIdUtils = versioning.VersionID;
const monitoring = require('../utilities/monitoringHandler');
const { generateToken, decryptToken } = require('../api/apiUtils/object/continueToken');
const { parseAttributesHeaders, buildAttributesXml } = require('./apiUtils/object/objectAttributes');

const OPTIONAL_ATTRIBUTES = new Set(['RestoreStatus']);

const xmlParamsToSkipUrlEncoding = new Set(['ContinuationToken', 'NextContinuationToken']);

Expand Down Expand Up @@ -150,7 +153,7 @@ function processVersions(bucketName, listParams, list) {
`<ID>${v.Owner.ID}</ID>`,
`<DisplayName>${v.Owner.DisplayName}</DisplayName>`,
'</Owner>',
...processOptionalAttributes(v, listParams.optionalAttributes),
...buildAttributesXml(v, v.userMetadata, listParams.optionalAttributes),
`<StorageClass>${v.StorageClass}</StorageClass>`,
v.IsDeleteMarker ? '</DeleteMarker>' : '</Version>'
);
Expand Down Expand Up @@ -231,7 +234,7 @@ function processMasterVersions(bucketName, listParams, list) {
);
}

xml.push(...processOptionalAttributes(v, listParams.optionalAttributes));
xml.push(...buildAttributesXml(v, v.userMetadata, listParams.optionalAttributes));

return xml.push(
`<StorageClass>${v.StorageClass}</StorageClass>`,
Expand All @@ -246,41 +249,6 @@ function processMasterVersions(bucketName, listParams, list) {
return xml.join('');
}

function processOptionalAttributes(item, optionalAttributes) {
const xml = [];
const userMetadata = new Set();

for (const attribute of optionalAttributes) {
switch (attribute) {
case 'RestoreStatus':
xml.push('<RestoreStatus>');
xml.push(`<IsRestoreInProgress>${!!item.restoreStatus?.inProgress}</IsRestoreInProgress>`);

if (item.restoreStatus?.expiryDate) {
xml.push(`<RestoreExpiryDate>${item.restoreStatus?.expiryDate}</RestoreExpiryDate>`);
}

xml.push('</RestoreStatus>');
break;
case 'x-amz-meta-*':
for (const key of Object.keys(item.userMetadata)) {
userMetadata.add(key);
}
break;
default:
if (item.userMetadata?.[attribute]) {
userMetadata.add(attribute);
}
}
}

for (const key of userMetadata) {
xml.push(`<${key}>${item.userMetadata[key]}</${key}>`);
}

return xml;
}

function handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, list, log) {
// eslint-disable-next-line no-param-reassign
listParams.maxKeys = requestMaxKeys;
Expand Down Expand Up @@ -321,15 +289,11 @@ async function bucketGet(authInfo, request, log, callback) {
const bucketName = request.bucketName;
const v2 = params['list-type'];

const optionalAttributes =
request.headers['x-amz-optional-object-attributes']
?.split(',')
.map(attr => attr.trim())
.map(attr => attr !== 'RestoreStatus' ? attr.toLowerCase() : attr)
?? [];
if (optionalAttributes.some(attr => !attr.startsWith('x-amz-meta-') && attr != 'RestoreStatus')) {
throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified');
}
const optionalAttributes = parseAttributesHeaders(
request.headers,
'x-amz-optional-object-attributes',
OPTIONAL_ATTRIBUTES,
);

if (v2 !== undefined && Number.parseInt(v2, 10) !== 2) {
throw errorInstances.InvalidArgument.customizeDescription('Invalid List Type specified in Request');
Expand Down
2 changes: 1 addition & 1 deletion lib/api/metadataSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function handleResult(listParams, requestMaxKeys, encoding, authInfo,
// eslint-disable-next-line no-param-reassign
listParams.encoding = encoding;
// eslint-disable-next-line no-param-reassign
listParams.optionalAttributes = [];
listParams.optionalAttributes = new Set();
let res;
if (listParams.listingType === 'DelimiterVersions') {
res = processVersions(bucketName, listParams, list);
Expand Down
64 changes: 32 additions & 32 deletions lib/api/objectGetAttributes.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const { promisify } = require('util');
const { errors } = require('arsenal');
const { errors, errorInstances } = require('arsenal');
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
const parseAttributesHeaders = require('./apiUtils/object/parseAttributesHeader');
const { decodeVersionId, getVersionIdResHeader } = require('./apiUtils/object/versioning');
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
const { pushMetric } = require('../utapi/utilities');
const { getPartCountFromMd5 } = require('./apiUtils/object/partInfo');
const { supportedGetObjectAttributes } = require('../../constants');
const { parseAttributesHeaders, buildAttributesXml } = require('./apiUtils/object/objectAttributes');

const checkExpectedBucketOwnerPromise = promisify(checkExpectedBucketOwner);
const validateBucketAndObj = promisify(standardMetadataValidateBucketAndObj);
Expand All @@ -20,39 +20,32 @@ const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes';
* @returns {string} XML response
*/
function buildXmlResponse(objMD, requestedAttrs) {
const xml = [];
xml.push(
const userMetadata = getUserMetadata(objMD);

return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<GetObjectAttributesResponse>',
);

for (const attribute of requestedAttrs) {
switch (attribute) {
case 'ETag':
xml.push(`<ETag>${objMD['content-md5']}</ETag>`);
break;
case 'ObjectParts': {
const partCount = getPartCountFromMd5(objMD);
if (partCount) {
xml.push(
'<ObjectParts>',
`<PartsCount>${partCount}</PartsCount>`,
'</ObjectParts>',
);
}
break;
}
case 'StorageClass':
xml.push(`<StorageClass>${objMD['x-amz-storage-class']}</StorageClass>`);
break;
case 'ObjectSize':
xml.push(`<ObjectSize>${objMD['content-length']}</ObjectSize>`);
break;
...buildAttributesXml(objMD, userMetadata, requestedAttrs),
'</GetObjectAttributesResponse>',
].join('');
}


/**
* getUserMetadata - Retrieves all object user metadata
* @param {object} objMD - object metadata
* @returns {object} - user metadata key-value pairs
*/
function getUserMetadata(objMD) {
const metadata = {};

for (const key in objMD) {
if (key.startsWith('x-amz-meta-') && objMD[key] !== undefined) {
metadata[key] = objMD[key];
}
}

xml.push('</GetObjectAttributesResponse>');
return xml.join('');
return metadata;
}

/**
Expand Down Expand Up @@ -141,7 +134,14 @@ async function objectGetAttributes(authInfo, request, log, callback) {
throw err;
}

const requestedAttrs = parseAttributesHeaders(headers);
const attrHeader = headers['x-amz-object-attributes'];
if (attrHeader === undefined) {
throw errorInstances.InvalidRequest.customizeDescription(
'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty',
);
}

const requestedAttrs = parseAttributesHeaders(headers, 'x-amz-object-attributes', supportedGetObjectAttributes, true);

if (requestedAttrs.has('Checksum')) {
log.debug('Checksum attribute requested but not implemented', {
Expand Down
2 changes: 1 addition & 1 deletion lib/routes/veeam/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function buildXMLResponse(request, arrayOfFiles, versioned = false) {
prefix: validPath,
maxKeys: parsedQs['max-keys'] || 1000,
delimiter: '/',
optionalAttributes: [],
optionalAttributes: new Set(),
};
const list = {
IsTruncated: false,
Expand Down
1 change: 1 addition & 0 deletions lib/utilities/serverAccessLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ const methodToResType = Object.freeze({
'objectDelete': 'OBJECT',
'objectDeleteTagging': 'TAGGING',
'objectGet': 'OBJECT',
'objectGetAttributes': 'OBJECT',
'objectGetACL': 'ACL',
'objectGetLegalHold': 'LEGALHOLD',
'objectGetRetention': 'OBJECT_LOCK_RETENTION',
Expand Down
Loading
Loading