Skip to content
Merged
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
114 changes: 114 additions & 0 deletions lib/api/apiUtils/object/objectAttributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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.
* @param {string[]} xml - The string array acting as the output buffer/collector.
* @returns {void} - this function does not return a value, it mutates the `xml` param.
*/
function buildAttributesXml(objectMD, userMetadata, requestedAttrs, 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}>`);
}
}

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

This file was deleted.

60 changes: 14 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,11 @@ 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, xml),

xml.push(
`<StorageClass>${v.StorageClass}</StorageClass>`,
v.IsDeleteMarker ? '</DeleteMarker>' : '</Version>'
);
Expand Down Expand Up @@ -231,7 +238,7 @@ function processMasterVersions(bucketName, listParams, list) {
);
}

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

return xml.push(
`<StorageClass>${v.StorageClass}</StorageClass>`,
Expand All @@ -246,41 +253,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 +293,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
Loading
Loading