From 5bb572907ff0d44021fc89ed0df84bb32016b394 Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Wed, 22 Oct 2025 12:07:01 +0200 Subject: [PATCH 1/6] CLDSRV-750: add Server Access Logs --- config.json | 4 + lib/Config.js | 23 +- lib/api/api.js | 4 + lib/metadata/metadataUtils.js | 19 ++ lib/server.js | 15 ++ lib/utilities/serverAccesssLogger.js | 314 +++++++++++++++++++++++++++ 6 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 lib/utilities/serverAccesssLogger.js diff --git a/config.json b/config.json index d7eee74035..ee2bbc6bf2 100644 --- a/config.json +++ b/config.json @@ -149,5 +149,9 @@ }, "integrityChecks": { "objectPutRetention": true + }, + "serverAccessLogs": { + "enabled": true, + "outputFile": "./logs/server-access.log" } } diff --git a/lib/Config.js b/lib/Config.js index 3dea761b30..88619df38e 100644 --- a/lib/Config.js +++ b/lib/Config.js @@ -574,6 +574,27 @@ function parseIntegrityChecks(config) { return integrityChecks; } +function parseServerAccessLogs(config) { + const res = { + enabled: true, + outputFile: './logs/server-access.log', + }; + + if (config && config.serverAccessLogs) { + if ('enabled' in config.serverAccessLogs) { + assert(typeof config.serverAccessLogs.enabled === 'boolean'); + res.enabled = config.serverAccessLogs.enabled; + } + + if ('outputFile' in config.serverAccessLogs) { + assert(typeof config.serverAccessLogs.outputFile === 'string'); + res.outputFile = config.serverAccessLogs.outputFile; + } + } + + return res; +} + /** * Reads from a config file and returns the content as a config object */ @@ -1785,7 +1806,7 @@ class Config extends EventEmitter { } } this.integrityChecks = parseIntegrityChecks(config); - + this.serverAccessLogs = parseServerAccessLogs(config); /** * S3C-10336: PutObject max size of 5GB is new in 9.5.1 * Provides a way to bypass the new validation if it breaks customer workflows diff --git a/lib/api/api.js b/lib/api/api.js index 6ab4fcc77f..ba9d6eb762 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -264,6 +264,10 @@ const api = { }); request.on('end', () => { + if (request.serverAccessLog) { + request.serverAccessLog.startTurnAroundTime = process.hrtime.bigint(); + } + if (bodyLength > MAX_BODY_LENGTH) { log.error('body length is too long for request type', { bodyLength }); diff --git a/lib/metadata/metadataUtils.js b/lib/metadata/metadataUtils.js index 442af70e0a..58c1308275 100644 --- a/lib/metadata/metadataUtils.js +++ b/lib/metadata/metadataUtils.js @@ -10,6 +10,23 @@ const { onlyOwnerAllowed } = require('../../constants'); const { actionNeedQuotaCheck, actionWithDataDeletion } = require('arsenal/build/lib/policyEvaluator/RequestContext'); const { processBytesToWrite, validateQuotas } = require('../api/apiUtils/quotas/quotaUtils'); +function storeServerAccessLogInfo(request, authInfo, bucket) { + if (request && + request.serverAccessLog && + authInfo && + bucket && + bucket.getBucketLoggingStatus() && + bucket.getBucketLoggingStatus().getLoggingEnabled()) { + /* eslint-disable no-param-reassign */ + request.serverAccessLog.enabled = true; + request.serverAccessLog.bucketOwner = bucket.getOwner(); + request.serverAccessLog.bucketName = bucket.getName(); + request.serverAccessLog.authInfo = authInfo; + request.serverAccessLog.loggingEnabled = bucket.getBucketLoggingStatus().getLoggingEnabled(); + /* eslint-enable no-param-reassign */ + } +} + /** getNullVersionFromMaster - retrieves the null version * metadata via retrieving the master key * @@ -274,6 +291,7 @@ function standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, contentLength, withVersionId, log, err => next(err, bucket, objMD)); }, ], (err, bucket, objMD) => { + storeServerAccessLogInfo(request, authInfo, bucket); if (err) { // still return bucket for cors headers return callback(err, bucket); @@ -296,6 +314,7 @@ function standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, function standardMetadataValidateBucket(params, actionImplicitDenies, log, callback) { const { bucketName } = params; return metadata.getBucket(bucketName, log, (err, bucket) => { + storeServerAccessLogInfo(params.request, params.authInfo, bucket); if (err) { // if some implicit actionImplicitDenies, return AccessDenied before // leaking any state information diff --git a/lib/server.js b/lib/server.js index 89bcd6f906..32eb8d9228 100644 --- a/lib/server.js +++ b/lib/server.js @@ -26,6 +26,7 @@ const { const HttpAgent = require('agentkeepalive'); const QuotaService = require('./utilization/instance'); +const { logServerAccess } = require('./utilities/serverAccesssLogger'); const { parseLC, MultipleBackendGateway } = arsenal.storage.data; const websiteEndpoints = _config.websiteEndpoints; let client = dataWrapper.client; @@ -122,6 +123,20 @@ class S3Server { monitoringClient.httpActiveRequests.inc(); const requestStartTime = process.hrtime.bigint(); + // eslint-disable-next-line no-param-reassign + req.serverAccessLog = { + enabled: false, + startTime: requestStartTime, + }; + // eslint-disable-next-line no-param-reassign + res.serverAccessLog = {}; + + res.on('finish', () => { + // eslint-disable-next-line no-param-reassign + req.serverAccessLog.endTime = process.hrtime.bigint(); + logServerAccess(req.serverAccessLog, req, res); + }); + // disable nagle algorithm req.socket.setNoDelay(); res.on('close', () => { diff --git a/lib/utilities/serverAccesssLogger.js b/lib/utilities/serverAccesssLogger.js new file mode 100644 index 0000000000..746752dd20 --- /dev/null +++ b/lib/utilities/serverAccesssLogger.js @@ -0,0 +1,314 @@ +const { Werelogs } = require('werelogs'); +const { config } = require('../Config'); +const fs = require('fs'); +const path = require('path'); +const logger = require('./logger'); + +const DEFAULT_OUTPUT_FILE = './logs/api-operations.log'; +const SERVER_ACCESS_LOG_FORMAT_VERSION = '0'; + +function createServerAccessLogger() { + if (!config.serverAccessLogs || !config.serverAccessLogs.enabled) { + logger.warn('ServerAccessLogs disabled returning no-op logger'); + return { + info: () => { }, + debug: () => { }, + warn: () => { }, + error: () => { }, + trace: () => { }, + fatal: () => { }, + }; + } + + // Ensure logs directory exists + const outputFile = config.serverAccessLogs.outputFile || DEFAULT_OUTPUT_FILE; + const logDir = path.dirname(outputFile); + + try { + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + } catch (error) { + // Fall back to logger-only logging if directory creation fails + logger.warn('Failed to create ServerAccess log directory, falling back to console logging:', error.message); + + const apiWerelogs = new Werelogs({ + level: config.serverAccessLogs.logLevel || 'info', + dump: config.serverAccessLogs.dumpLevel || 'error', + streams: [ + { level: 'trace', stream: process.stdout } + ] + }); + + return new apiWerelogs.Logger('ServerAccessLogger'); + } + + // Create file stream for API logs + const serverAccessLogStream = fs.createWriteStream(outputFile, { flags: 'a' }); + + // Handle stream errors + serverAccessLogStream.on('error', error => { + logger.error('ServerAccessLogger log file stream error:', error); + }); + + // Create the API-specific Werelogs instance - file output only + const apiWerelogs = new Werelogs({ + level: config.serverAccessLogs.logLevel || 'info', + dump: config.serverAccessLogs.dumpLevel || 'error', + streams: [{ level: 'trace', stream: serverAccessLogStream }] + }); + logger.info('ServerAccessLogger created successfully'); + return new apiWerelogs.Logger('ServerAccessLogger'); +} + +var serverAccessLogger = { + info: () => { }, + debug: () => { }, + warn: () => { }, + error: () => { }, + trace: () => { }, + fatal: () => { }, +}; + + +try { + serverAccessLogger = createServerAccessLogger(); +} catch (error) { + logger.error('Failed to create ServiceAccessLogger, using no-op logger:', error); +} + +function getRemoteIPFromRequest(request) { + let remoteIP = null; + if (request.headers) { + // Check for forwarded IP headers (proxy/load balancer scenarios) + const headerRemoteIP = request.headers['x-forwarded-for'] || + request.headers['x-real-ip'] || + request.headers['x-client-ip'] || + request.headers['cf-connecting-ip']; // Cloudflare + + // x-forwarded-for can contain multiple IPs, take the first one + if (headerRemoteIP && headerRemoteIP.includes(',')) { + remoteIP = headerRemoteIP.split(',')[0].trim(); + } + } + + // Fallback to connection remote address if no forwarded headers + if (!remoteIP) { + const connIP = (request.connection && request.connection.remoteAddress) || + (request.socket && request.socket.remoteAddress) || + (request.ip); + if (connIP) { + remoteIP = connIP; + } + } + + return remoteIP; +} + +function getOperation(req) { + const methodToResType = Object.freeze({ + 'bucketDelete': 'BUCKET', + 'bucketDeleteCors': 'BUCKET', + 'bucketDeleteEncryption': 'BUCKET', + 'bucketDeleteWebsite': 'BUCKET', + 'bucketGet': 'BUCKET', + 'bucketGetACL': 'BUCKET', + 'bucketGetCors': 'BUCKET', + 'bucketGetObjectLock': 'BUCKET', + 'bucketGetVersioning': 'VERSIONING', + 'bucketGetWebsite': 'BUCKET', + 'bucketGetLocation': 'BUCKET', + 'bucketGetEncryption': 'BUCKET', + 'bucketHead': 'BUCKET', + 'bucketPut': 'BUCKET', + 'bucketPutACL': 'BUCKET', + 'bucketPutCors': 'BUCKET', + 'bucketPutVersioning': 'VERSIONING', + 'bucketPutTagging': 'BUCKET', + 'bucketDeleteTagging': 'BUCKET', + 'bucketGetTagging': 'BUCKET', + 'bucketPutWebsite': 'BUCKET', + 'bucketPutReplication': 'BUCKET', + 'bucketGetReplication': 'BUCKET', + 'bucketDeleteReplication': 'BUCKET', + 'bucketDeleteQuota': 'BUCKET', + 'bucketPutLifecycle': 'BUCKET', + 'bucketUpdateQuota': 'BUCKET', + 'bucketGetLifecycle': 'BUCKET', + 'bucketDeleteLifecycle': 'BUCKET', + 'bucketPutPolicy': 'BUCKETPOLICY', + 'bucketGetPolicy': 'BUCKETPOLICY', + 'bucketGetQuota': 'BUCKET', + 'bucketDeletePolicy': 'BUCKETPOLICY', + 'bucketPutObjectLock': 'BUCKET', + 'bucketPutNotification': 'BUCKET', + 'bucketGetNotification': 'BUCKET', + 'bucketPutEncryption': 'BUCKET', + 'bucketPutLogging': 'LOGGING_STATUS', + 'bucketGetLogging': 'LOGGING_STATUS', + // 'corsPreflight': '', + 'completeMultipartUpload': 'OBJECT', + 'initiateMultipartUpload': 'OBJECT', + 'listMultipartUploads': 'OBJECT', + 'listParts': 'OBJECT', + 'metadataSearch': 'OBJECT', + 'multiObjectDelete': 'OBJECT', + 'multipartDelete': 'OBJECT', + 'objectDelete': 'OBJECT', + 'objectDeleteTagging': 'OBJECT', + 'objectGet': 'OBJECT', + 'objectGetACL': 'OBJECT', + 'objectGetLegalHold': 'OBJECT', + 'objectGetRetention': 'OBJECT', + 'objectGetTagging': 'OBJECT', + 'objectCopy': 'OBJECT', + 'objectHead': 'OBJECT', + 'objectPut': 'OBJECT', + 'objectPutACL': 'OBJECT', + 'objectPutLegalHold': 'OBJECT', + 'objectPutTagging': 'OBJECT', + 'objectPutPart': 'OBJECT', + 'objectPutCopyPart': 'OBJECT', + 'objectPutRetention': 'OBJECT', + 'objectRestore': 'OBJECT', + // 'serviceGet': '', + // 'websiteGet': '', + // 'websiteHead': '', + }); + + return `REST.${req.method}.${methodToResType[req.apiMethod] ? methodToResType[req.apiMethod] : 'UNKNOWN'}`; +} + +function getRequester(authInfo) { + const requester = null; + if (authInfo) { + if (authInfo.isRequesterPublicUser && authInfo.isRequesterPublicUser()) { + return requester; // Unauthenticated requests + } else if (authInfo.isRequesterAnIAMUser && authInfo.isRequesterAnIAMUser()) { + // IAM user: include IAM user name and account + const iamUserName = authInfo.getIAMdisplayName ? authInfo.getIAMdisplayName() : ''; + const accountName = authInfo.getAccountDisplayName ? authInfo.getAccountDisplayName() : ''; + return iamUserName && accountName ? `${iamUserName}:${accountName}` : authInfo.getCanonicalID(); + } else if (authInfo.getCanonicalID) { + // Regular user: canonical user ID + return authInfo.getCanonicalID(); + } + } + return requester; +} + +function getURI(request) { + let requestURI = null; + if (request) { + const method = request.method || 'UNKNOWN'; + const url = request.url || request.originalUrl || '/'; + const httpVersion = request.httpVersion || '1.1'; + requestURI = `${method} ${url} HTTP/${httpVersion}`; + } + return requestURI; +} + +function getObjectSize(request, response) { + const objectSizePutMethods = Object.freeze({ + 'objectPut': true, + 'objectPutPart': true, + }); + + const objectSizeGetMethods = Object.freeze({ + 'objectGet': true, + }); + + // If it is a PUT get the Content-Length from the request, if it is a GET get it from the response. + if (request && response && objectSizeGetMethods[request.apiMethod]) { + const len = response.getHeader('Content-Length'); + return len || null; + } + + if (request && objectSizePutMethods[request.apiMethod]) { + const len = request.headers['content-length']; + return len || null; + } + + return null; +} + +function getBytesSent(res, bytesSent) { + if (bytesSent) { + return bytesSent; + } + + if (!res) { + return null; + } + + const len = res.getHeader('Content-Length'); + return len || null; +} + +function calculateTotalTime(startTime, endTime) { + if (!startTime || !endTime) { + return null; + } + + return ((endTime - startTime) / 1_000_000n).toString(); +} + +function calculateTurnAroundTime(startTurnAroundTime, endTurnAroundTime) { + if (!startTurnAroundTime || !endTurnAroundTime) { + return null; + } + + return ((endTurnAroundTime - startTurnAroundTime) / 1_000_000n).toString(); +} + +function logServerAccess(params, req, res) { + const errorCode = res.serverAccessLog.errorCode; + const endTurnAroundTime = res.serverAccessLog.endTurnAroundTime; + const requestID = res.serverAccessLog.requestID; + const bytesSent = res.serverAccessLog.bytesSent; + const authInfo = params.authInfo; + + serverAccessLogger.info('SERVER_ACCESS_LOG', { + // AWS fields. + bucketOwner: params.bucketOwner ? params.bucketOwner : null, + bucket: params.bucketName ? params.bucketName : null, + startTime: params.startTime ? params.startTime.toString() : null, + remoteIP: getRemoteIPFromRequest(req), + requester: getRequester(authInfo), + // eslint-disable-next-line camelcase + req_id: requestID || null, // requestID in AWS + operation: getOperation(req), + requestURI: getURI(req), + HTTPStatus: res.statusCode ? res.statusCode : null, + errorCode: errorCode || null, + bytesSent: getBytesSent(res, bytesSent), + objectSize: getObjectSize(req, res), + totalTime: calculateTotalTime(params.startTime, params.endTime), + turnAroundTime: calculateTurnAroundTime(params.startTurnAroundTime, endTurnAroundTime), + referer: req.headers.referer ? req.headers.referer : null, + userAgent: req.headers['user-agent'] ? req.headers['user-agent'] : null, + versionID: req.query ? req.query.versionId || null : null, // query inserted by arsenal. + hostID: null, // NOT IMPLEMENTED + signatureVersion: authInfo ? authInfo.getAuthVersion() : null, + cipherSuite: req.socket.encrypted ? req.socket.getCipher()['standardName'] : null, + authenticationType: authInfo ? authInfo.getAuthType() : null, + hostHeader: req.headers.host ? req.headers.host : null, + // From https://nodejs.org/api/tls.html#tlssocketgetcipher + tlsVersion: req.socket.encrypted ? req.socket.getCipher()['version'] : null, + accessPointARN: null, // NOT IMPLEMENTED + aclRequired: null, // TODO + + // Scality extra fields. + logFormatVersion: SERVER_ACCESS_LOG_FORMAT_VERSION, + loggingEnabled: params.enabled, + loggingTargetBucket: params.loggingEnabled ? params.loggingEnabled.TargetBucket : null, + loggingTargetPrefix: params.loggingEnabled ? params.loggingEnabled.TargetPrefix : null, + raftSessionID: null, + // eslint-disable-next-line camelcase + aws_access_key_id: authInfo ? authInfo.getAccessKey() : null, + }); +} + +module.exports = { + logServerAccess, +}; From a63c29fadd75a2426c550ca9e5846e14b369626b Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Thu, 23 Oct 2025 19:38:28 +0200 Subject: [PATCH 2/6] tmp use arsenal branch --- package.json | 2 +- yarn.lock | 54 ++++++++++++++++++++++++++-------------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 9434311de9..afa0dac322 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@azure/storage-blob": "^12.28.0", "@hapi/joi": "^17.1.1", - "arsenal": "git+https://github.com/scality/Arsenal#8.2.33", + "arsenal": "git+https://github.com/scality/Arsenal#improvement/ARSN-531-export-server-access-log-fields", "async": "2.6.4", "aws-sdk": "^2.1692.0", "bucketclient": "scality/bucketclient#8.2.5", diff --git a/yarn.lock b/yarn.lock index 0e88a9d91b..bf5e602f1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1431,32 +1431,37 @@ arraybuffer.prototype.slice@^1.0.4: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/Arsenal#8.2.33": - version "8.2.33" - resolved "git+https://github.com/scality/Arsenal#34b7f08652c8eb765551c3422dd6418c0c65a79b" +"arsenal@git+https://github.com/scality/Arsenal#8.2.4": + version "8.2.4" + resolved "git+https://github.com/scality/Arsenal#96ef6a3e26d7528f877300606586759f1da6d0cd" dependencies: - "@azure/identity" "^4.13.0" - "@azure/storage-blob" "^12.28.0" + "@azure/identity" "^4.5.0" + "@azure/storage-blob" "^12.25.0" + "@eslint/plugin-kit" "^0.2.3" "@js-sdsl/ordered-set" "^4.4.2" "@scality/hdclient" "^1.3.1" + "@types/async" "^3.2.24" + "@types/utf8" "^3.0.3" JSONStream "^1.3.5" - agentkeepalive "^4.6.0" + agentkeepalive "^4.5.0" ajv "6.12.3" async "~2.6.4" aws-sdk "^2.1691.0" backo "^1.1.0" base-x "3.0.8" base62 "^2.0.2" - debug "^4.4.3" + bson "^6.8.0" + debug "^4.3.7" + diskusage "^1.2.0" fcntl "github:scality/node-fcntl#0.3.0" httpagent scality/httpagent#1.1.0 - https-proxy-agent "^7.0.6" - ioredis "^5.8.1" + https-proxy-agent "^7.0.5" + ioredis "^5.4.1" ipaddr.js "^2.2.0" - joi "^18.0.1" + joi "^17.13.3" level "~5.0.1" level-sublevel "~6.6.5" - mongodb "^6.20.0" + mongodb "^6.11.0" node-forge "^1.3.1" prom-client "^15.1.3" simple-glob "^0.2.0" @@ -1470,37 +1475,32 @@ arraybuffer.prototype.slice@^1.0.4: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/Arsenal#8.2.4": - version "8.2.4" - resolved "git+https://github.com/scality/Arsenal#96ef6a3e26d7528f877300606586759f1da6d0cd" +"arsenal@git+https://github.com/scality/Arsenal#improvement/ARSN-531-export-server-access-log-fields": + version "8.2.33" + resolved "git+https://github.com/scality/Arsenal#5ee5876f472e3e66679bc29fdbb4764edc5e1365" dependencies: - "@azure/identity" "^4.5.0" - "@azure/storage-blob" "^12.25.0" - "@eslint/plugin-kit" "^0.2.3" + "@azure/identity" "^4.13.0" + "@azure/storage-blob" "^12.28.0" "@js-sdsl/ordered-set" "^4.4.2" "@scality/hdclient" "^1.3.1" - "@types/async" "^3.2.24" - "@types/utf8" "^3.0.3" JSONStream "^1.3.5" - agentkeepalive "^4.5.0" + agentkeepalive "^4.6.0" ajv "6.12.3" async "~2.6.4" aws-sdk "^2.1691.0" backo "^1.1.0" base-x "3.0.8" base62 "^2.0.2" - bson "^6.8.0" - debug "^4.3.7" - diskusage "^1.2.0" + debug "^4.4.3" fcntl "github:scality/node-fcntl#0.3.0" httpagent scality/httpagent#1.1.0 - https-proxy-agent "^7.0.5" - ioredis "^5.4.1" + https-proxy-agent "^7.0.6" + ioredis "^5.8.1" ipaddr.js "^2.2.0" - joi "^17.13.3" + joi "^18.0.1" level "~5.0.1" level-sublevel "~6.6.5" - mongodb "^6.11.0" + mongodb "^6.20.0" node-forge "^1.3.1" prom-client "^15.1.3" simple-glob "^0.2.0" From 1321bca0260ddfcd679f9732009bb8866874ffdd Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Sat, 25 Oct 2025 00:39:43 +0200 Subject: [PATCH 3/6] CLDSRV-750: replace repeated logic with standardMetadataValidateBucket --- lib/api/bucketDeleteCors.js | 69 +++++++++------------------ lib/api/bucketDeleteWebsite.js | 67 +++++++++----------------- lib/api/bucketGetCors.js | 61 ++++++++---------------- lib/api/bucketGetLocation.js | 67 +++++++++----------------- lib/api/bucketGetWebsite.js | 64 +++++++++---------------- lib/api/bucketPutCors.js | 77 ++++++++++++------------------ lib/api/bucketPutWebsite.js | 86 ++++++++++++++-------------------- 7 files changed, 174 insertions(+), 317 deletions(-) diff --git a/lib/api/bucketDeleteCors.js b/lib/api/bucketDeleteCors.js index b08b6c0f13..b0fc2bae68 100644 --- a/lib/api/bucketDeleteCors.js +++ b/lib/api/bucketDeleteCors.js @@ -1,14 +1,11 @@ -const { errors } = require('arsenal'); - -const bucketShield = require('./apiUtils/bucket/bucketShield'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); -const { isBucketAuthorized } = - require('./apiUtils/authorization/permissionChecks'); const metadata = require('../metadata/wrapper'); +const { standardMetadataValidateBucket } = require('../metadata/metadataUtils'); const { pushMetric } = require('../utapi/utilities'); const monitoring = require('../utilities/monitoringHandler'); -const requestType = 'bucketDeleteCors'; +const REQUEST_TYPE = 'bucketDeleteCors'; +const METRICS_ACTION = 'deleteBucketCors'; /** * Bucket Delete CORS - Delete bucket cors configuration @@ -20,44 +17,27 @@ const requestType = 'bucketDeleteCors'; */ function bucketDeleteCors(authInfo, request, log, callback) { const bucketName = request.bucketName; - const canonicalID = authInfo.getCanonicalID(); - - return metadata.getBucket(bucketName, log, (err, bucket) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); + const metadataValParams = { + authInfo, + bucketName, + requestType: REQUEST_TYPE, + request, + }; + + return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { - log.debug('metadata getbucket failed', { error: err }); - monitoring.promMetrics('DELETE', bucketName, 400, - 'deleteBucketCors'); + monitoring.promMetrics('DELETE', bucketName, err.code, METRICS_ACTION); + if (err?.is?.AccessDenied) { + return callback(err, corsHeaders); + } return callback(err); } - if (bucketShield(bucket, requestType)) { - monitoring.promMetrics('DELETE', bucketName, 400, - 'deleteBucketCors'); - return callback(errors.NoSuchBucket); - } - log.trace('found bucket in metadata'); - - if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID, - authInfo, log, request, request.actionImplicitDenies)) { - log.debug('access denied for user on bucket', { - requestType, - method: 'bucketDeleteCors', - }); - monitoring.promMetrics('DELETE', bucketName, 403, - 'deleteBucketCors'); - return callback(errors.AccessDenied, corsHeaders); - } const cors = bucket.getCors(); if (!cors) { - log.trace('no existing cors configuration', { - method: 'bucketDeleteCors', - }); - pushMetric('deleteBucketCors', log, { - authInfo, - bucket: bucketName, - }); + log.trace('no existing cors configuration', { method: REQUEST_TYPE }); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); return callback(null, corsHeaders); } @@ -65,17 +45,12 @@ function bucketDeleteCors(authInfo, request, log, callback) { bucket.setCors(null); return metadata.updateBucket(bucketName, bucket, log, err => { if (err) { - monitoring.promMetrics('DELETE', bucketName, 400, - 'deleteBucketCors'); + monitoring.promMetrics('DELETE', bucketName, err.code, METRICS_ACTION); return callback(err, corsHeaders); } - pushMetric('deleteBucketCors', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics( - 'DELETE', bucketName, '204', 'deleteBucketCors'); - return callback(err, corsHeaders); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); + monitoring.promMetrics('DELETE', bucketName, '204', METRICS_ACTION); + return callback(null , corsHeaders); }); }); } diff --git a/lib/api/bucketDeleteWebsite.js b/lib/api/bucketDeleteWebsite.js index 2f83391871..7d817dd87c 100644 --- a/lib/api/bucketDeleteWebsite.js +++ b/lib/api/bucketDeleteWebsite.js @@ -1,55 +1,35 @@ -const { errors } = require('arsenal'); - -const bucketShield = require('./apiUtils/bucket/bucketShield'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); -const { isBucketAuthorized } = - require('./apiUtils/authorization/permissionChecks'); const metadata = require('../metadata/wrapper'); +const { standardMetadataValidateBucket } = require('../metadata/metadataUtils'); const { pushMetric } = require('../utapi/utilities'); const monitoring = require('../utilities/monitoringHandler'); -const requestType = 'bucketDeleteWebsite'; +const REQUEST_TYPE = 'bucketDeleteWebsite'; +const METRICS_ACTION = 'deleteBucketWebsite'; function bucketDeleteWebsite(authInfo, request, log, callback) { const bucketName = request.bucketName; - const canonicalID = authInfo.getCanonicalID(); - - return metadata.getBucket(bucketName, log, (err, bucket) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); + const metadataValParams = { + authInfo, + bucketName, + requestType: REQUEST_TYPE, + request, + }; + + return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { - log.debug('metadata getbucket failed', { error: err }); - monitoring.promMetrics( - 'DELETE', bucketName, err.code, 'deleteBucketWebsite'); + monitoring.promMetrics('DELETE', bucketName, err.code, REQUEST_TYPE); + if (err?.is?.AccessDenied) { + return callback(err, corsHeaders); + } return callback(err); } - if (bucketShield(bucket, requestType)) { - monitoring.promMetrics( - 'DELETE', bucketName, 404, 'deleteBucketWebsite'); - return callback(errors.NoSuchBucket); - } - log.trace('found bucket in metadata'); - - if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID, - authInfo, log, request, request.actionImplicitDenies)) { - log.debug('access denied for user on bucket', { - requestType, - method: 'bucketDeleteWebsite', - }); - monitoring.promMetrics( - 'DELETE', bucketName, 403, 'deleteBucketWebsite'); - return callback(errors.AccessDenied, corsHeaders); - } const websiteConfig = bucket.getWebsiteConfiguration(); if (!websiteConfig) { - log.trace('no existing website configuration', { - method: 'bucketDeleteWebsite', - }); - pushMetric('deleteBucketWebsite', log, { - authInfo, - bucket: bucketName, - }); + log.trace('no existing website configuration', { method: REQUEST_TYPE }); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); return callback(null, corsHeaders); } @@ -57,16 +37,11 @@ function bucketDeleteWebsite(authInfo, request, log, callback) { bucket.setWebsiteConfiguration(null); return metadata.updateBucket(bucketName, bucket, log, err => { if (err) { - monitoring.promMetrics( - 'DELETE', bucketName, err.code, 'deleteBucketWebsite'); + monitoring.promMetrics('DELETE', bucketName, err.code, METRICS_ACTION); return callback(err, corsHeaders); } - pushMetric('deleteBucketWebsite', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics( - 'DELETE', bucketName, '200', 'deleteBucketWebsite'); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); + monitoring.promMetrics('DELETE', bucketName, '200', METRICS_ACTION); return callback(null, corsHeaders); }); }); diff --git a/lib/api/bucketGetCors.js b/lib/api/bucketGetCors.js index a59b57d451..8c13132fb7 100644 --- a/lib/api/bucketGetCors.js +++ b/lib/api/bucketGetCors.js @@ -1,15 +1,12 @@ const { errors } = require('arsenal'); - -const bucketShield = require('./apiUtils/bucket/bucketShield'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); const { convertToXml } = require('./apiUtils/bucket/bucketCors'); -const { isBucketAuthorized } = - require('./apiUtils/authorization/permissionChecks'); -const metadata = require('../metadata/wrapper'); +const { standardMetadataValidateBucket } = require('../metadata/metadataUtils'); const { pushMetric } = require('../utapi/utilities'); const monitoring = require('../utilities/monitoringHandler'); -const requestType = 'bucketGetCors'; +const REQUEST_TYPE = 'bucketGetCors'; +const METRICS_ACTION = 'getBucketCors'; /** * Bucket Get CORS - Get bucket cors configuration @@ -21,52 +18,34 @@ const requestType = 'bucketGetCors'; */ function bucketGetCors(authInfo, request, log, callback) { const bucketName = request.bucketName; - const canonicalID = authInfo.getCanonicalID(); - - metadata.getBucket(bucketName, log, (err, bucket) => { + const metadataValParams = { + authInfo, + bucketName, + requestType: REQUEST_TYPE, + request, + }; + + return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { - log.debug('metadata getbucket failed', { error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'getBucketCors'); + monitoring.promMetrics('GET', bucketName, err.code, METRICS_ACTION); + if (err?.is?.AccessDenied) { + return callback(err, corsHeaders); + } return callback(err); } - if (bucketShield(bucket, requestType)) { - monitoring.promMetrics( - 'GET', bucketName, 404, 'getBucketCors'); - return callback(errors.NoSuchBucket); - } - log.trace('found bucket in metadata'); - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - - if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID, - authInfo, log, request, request.actionImplicitDenies)) { - log.debug('access denied for user on bucket', { - requestType, - method: 'bucketGetCors', - }); - monitoring.promMetrics( - 'GET', bucketName, 403, 'getBucketCors'); - return callback(errors.AccessDenied, null, corsHeaders); - } const cors = bucket.getCors(); if (!cors) { - log.debug('cors configuration does not exist', { - method: 'bucketGetCors', - }); - monitoring.promMetrics( - 'GET', bucketName, 404, 'getBucketCors'); + log.debug('cors configuration does not exist', { method: REQUEST_TYPE }); + monitoring.promMetrics('GET', bucketName, 404, METRICS_ACTION); return callback(errors.NoSuchCORSConfiguration, null, corsHeaders); } log.trace('converting cors configuration to xml'); const xml = convertToXml(cors); - pushMetric('getBucketCors', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics('GET', bucketName, '200', 'getBucketCors'); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); + monitoring.promMetrics('GET', bucketName, '200', METRICS_ACTION); return callback(null, xml, corsHeaders); }); } diff --git a/lib/api/bucketGetLocation.js b/lib/api/bucketGetLocation.js index 75aac4a29b..83e0f3b601 100644 --- a/lib/api/bucketGetLocation.js +++ b/lib/api/bucketGetLocation.js @@ -1,15 +1,12 @@ -const { errors, s3middleware } = require('arsenal'); - -const bucketShield = require('./apiUtils/bucket/bucketShield'); -const { isBucketAuthorized } = - require('./apiUtils/authorization/permissionChecks'); -const metadata = require('../metadata/wrapper'); +const { s3middleware } = require('arsenal'); +const { standardMetadataValidateBucket } = require('../metadata/metadataUtils'); const { pushMetric } = require('../utapi/utilities'); const escapeForXml = s3middleware.escapeForXml; const collectCorsHeaders = require('../utilities/collectCorsHeaders'); const monitoring = require('../utilities/monitoringHandler'); -const requestType = 'bucketGetLocation'; +const REQUEST_TYPE = 'bucketGetLocation'; +const METRICS_ACTION = 'getBucketLocation'; /** * Bucket Get Location - Get bucket locationConstraint configuration @@ -19,56 +16,38 @@ const requestType = 'bucketGetLocation'; * @param {function} callback - callback to server * @return {undefined} */ - function bucketGetLocation(authInfo, request, log, callback) { const bucketName = request.bucketName; - const canonicalID = authInfo.getCanonicalID(); - - return metadata.getBucket(bucketName, log, (err, bucket) => { + const metadataValParams = { + authInfo, + bucketName, + requestType: request.apiMethod || REQUEST_TYPE, + request, + }; + + return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { - log.debug('metadata getbucket failed', { error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'getBucketLocation'); + monitoring.promMetrics('GET', bucketName, err.code, METRICS_ACTION); + if (err?.is?.AccessDenied) { + return callback(err, corsHeaders); + } return callback(err); } - if (bucketShield(bucket, requestType)) { - monitoring.promMetrics( - 'GET', bucketName, 404, 'getBucketLocation'); - return callback(errors.NoSuchBucket); - } - log.trace('found bucket in metadata'); - - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - - if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID, - authInfo, log, request, request.actionImplicitDenies)) { - log.debug('access denied for account on bucket', { - requestType, - method: 'bucketGetLocation', - }); - monitoring.promMetrics( - 'GET', bucketName, 403, 'getBucketLocation'); - return callback(errors.AccessDenied, null, corsHeaders); - } let locationConstraint = bucket.getLocationConstraint(); if (!locationConstraint || locationConstraint === 'us-east-1') { - // AWS returns empty string if no region has been - // provided or for us-east-1 - // Note: AWS JS SDK sends a request with locationConstraint us-east-1 - // if no locationConstraint provided. + // AWS returns empty string if no region has been + // provided or for us-east-1 + // Note: AWS JS SDK sends a request with locationConstraint us-east-1 + // if no locationConstraint provided. locationConstraint = ''; } const xml = ` ` + `${escapeForXml(locationConstraint)}`; - pushMetric('getBucketLocation', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics( - 'GET', bucketName, '200', 'getBucketLocation'); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); + monitoring.promMetrics('GET', bucketName, '200', METRICS_ACTION); return callback(null, xml, corsHeaders); }); } diff --git a/lib/api/bucketGetWebsite.js b/lib/api/bucketGetWebsite.js index 35093fa457..0f20333c3d 100644 --- a/lib/api/bucketGetWebsite.js +++ b/lib/api/bucketGetWebsite.js @@ -1,15 +1,13 @@ const { errors } = require('arsenal'); -const bucketShield = require('./apiUtils/bucket/bucketShield'); const { convertToXml } = require('./apiUtils/bucket/bucketWebsite'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); -const { isBucketAuthorized } = - require('./apiUtils/authorization/permissionChecks'); -const metadata = require('../metadata/wrapper'); +const { standardMetadataValidateBucket } = require('../metadata/metadataUtils'); const { pushMetric } = require('../utapi/utilities'); const monitoring = require('../utilities/monitoringHandler'); -const requestType = 'bucketGetWebsite'; +const REQUEST_TYPE = 'bucketGetWebsite'; +const METRICS_ACTION = 'getBucketWebsite'; /** * Bucket Get Website - Get bucket website configuration @@ -21,54 +19,34 @@ const requestType = 'bucketGetWebsite'; */ function bucketGetWebsite(authInfo, request, log, callback) { const bucketName = request.bucketName; - const canonicalID = authInfo.getCanonicalID(); - - metadata.getBucket(bucketName, log, (err, bucket) => { + const metadataValParams = { + authInfo, + bucketName, + requestType: request.apiMethod || REQUEST_TYPE, + request, + }; + + return standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { - log.debug('metadata getbucket failed', { error: err }); - monitoring.promMetrics( - 'GET', bucketName, err.code, 'getBucketWebsite'); + monitoring.promMetrics('GET', bucketName, err.code, METRICS_ACTION); + if (err?.is?.AccessDenied) { + return callback(err, corsHeaders); + } return callback(err); } - if (bucketShield(bucket, requestType)) { - monitoring.promMetrics( - 'GET', bucketName, 404, 'getBucketWebsite'); - return callback(errors.NoSuchBucket); - } - log.trace('found bucket in metadata'); - - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); - if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID, - authInfo, log, request, request.actionImplicitDenies)) { - log.debug('access denied for user on bucket', { - requestType, - method: 'bucketGetWebsite', - }); - monitoring.promMetrics( - 'GET', bucketName, 403, 'getBucketWebsite'); - return callback(errors.AccessDenied, null, corsHeaders); - } const websiteConfig = bucket.getWebsiteConfiguration(); if (!websiteConfig) { - log.debug('bucket website configuration does not exist', { - method: 'bucketGetWebsite', - }); - monitoring.promMetrics( - 'GET', bucketName, 404, 'getBucketWebsite'); - return callback(errors.NoSuchWebsiteConfiguration, null, - corsHeaders); + log.debug('bucket website configuration does not exist', { method: REQUEST_TYPE }); + monitoring.promMetrics('GET', bucketName, 404, METRICS_ACTION); + return callback(errors.NoSuchWebsiteConfiguration, null, corsHeaders); } log.trace('converting website configuration to xml'); const xml = convertToXml(websiteConfig); - pushMetric('getBucketWebsite', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics( - 'GET', bucketName, '200', 'getBucketWebsite'); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); + monitoring.promMetrics('GET', bucketName, '200', METRICS_ACTION); return callback(null, xml, corsHeaders); }); } diff --git a/lib/api/bucketPutCors.js b/lib/api/bucketPutCors.js index 03a34430de..bd37a4281d 100644 --- a/lib/api/bucketPutCors.js +++ b/lib/api/bucketPutCors.js @@ -1,16 +1,15 @@ const async = require('async'); const { errors, errorInstances } = require('arsenal'); -const bucketShield = require('./apiUtils/bucket/bucketShield'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); -const { isBucketAuthorized } = - require('./apiUtils/authorization/permissionChecks'); const metadata = require('../metadata/wrapper'); +const { standardMetadataValidateBucket } = require('../metadata/metadataUtils'); const { parseCorsXml } = require('./apiUtils/bucket/bucketCors'); const { pushMetric } = require('../utapi/utilities'); const monitoring = require('../utilities/monitoringHandler'); -const requestType = 'bucketPutCors'; +const REQUEST_TYPE = 'bucketPutCors'; +const METRICS_ACTION = 'putBucketCors'; /** * Bucket Put Cors - Adds cors rules to bucket @@ -22,73 +21,59 @@ const requestType = 'bucketPutCors'; */ function bucketPutCors(authInfo, request, log, callback) { log.debug('processing request', { method: 'bucketPutCors' }); - const { bucketName } = request; - const canonicalID = authInfo.getCanonicalID(); + const bucketName = request.bucketName; + const metadataValParams = { + authInfo, + bucketName, + requestType: request.apiMethod || REQUEST_TYPE, + request, + }; if (!request.post) { log.debug('CORS xml body is missing', - { error: errors.MissingRequestBodyError }); - monitoring.promMetrics('PUT', bucketName, 400, 'putBucketCors'); + { error: errors.MissingRequestBodyError }); + monitoring.promMetrics('PUT', bucketName, 400, METRICS_ACTION); return callback(errors.MissingRequestBodyError); } if (parseInt(request.headers['content-length'], 10) > 65536) { const errMsg = 'The CORS XML document is limited to 64 KB in size.'; log.debug(errMsg, { error: errors.MalformedXML }); - monitoring.promMetrics('PUT', bucketName, 400, 'putBucketCors'); + monitoring.promMetrics('PUT', bucketName, 400, METRICS_ACTION); return callback(errorInstances.MalformedXML.customizeDescription(errMsg)); } return async.waterfall([ - function parseXmlBody(next) { + next => { log.trace('parsing cors rules'); return parseCorsXml(request.post, log, next); }, - function getBucketfromMetadata(rules, next) { - metadata.getBucket(bucketName, log, (err, bucket) => { + (rules, next) => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, + (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { - log.debug('metadata getbucket failed', { error: err }); + monitoring.promMetrics('PUT', bucketName, err.code, METRICS_ACTION); + if (err?.is?.AccessDenied) { + return next(err, corsHeaders); + } return next(err); } - if (bucketShield(bucket, requestType)) { - return next(errors.NoSuchBucket); - } - log.trace('found bucket in metadata'); - // get corsHeaders before CORSConfiguration is updated - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); + return next(null, bucket, rules, corsHeaders); - }); - }, - function validateBucketAuthorization(bucket, rules, corsHeaders, next) { - if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID, - authInfo, log, request, request.actionImplicitDenies)) { - log.debug('access denied for account on bucket', { - requestType, - }); - return next(errors.AccessDenied, corsHeaders); - } - return next(null, bucket, rules, corsHeaders); - }, - function updateBucketMetadata(bucket, rules, corsHeaders, next) { - log.trace('updating bucket cors rules in metadata'); + }), + (bucket, rules, corsHeaders, next) => { bucket.setCors(rules); - metadata.updateBucket(bucketName, bucket, log, err => - next(err, corsHeaders)); + return metadata.updateBucket(bucketName, bucket, log, err => next(err, corsHeaders)); }, ], (err, corsHeaders) => { if (err) { - log.trace('error processing request', { error: err, - method: 'bucketPutCors' }); - monitoring.promMetrics('PUT', bucketName, err.code, - 'putBucketCors'); + log.trace('error processing request', { error: err, method: 'bucketPutCors' }); + monitoring.promMetrics('PUT', bucketName, err.code, METRICS_ACTION); + return callback(err, corsHeaders); } - pushMetric('putBucketCors', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics('PUT', bucketName, '200', 'putBucketCors'); - return callback(err, corsHeaders); + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); + monitoring.promMetrics('PUT', bucketName, '200', METRICS_ACTION); + return callback(null); }); } diff --git a/lib/api/bucketPutWebsite.js b/lib/api/bucketPutWebsite.js index 5b546a743b..b7110a8a11 100644 --- a/lib/api/bucketPutWebsite.js +++ b/lib/api/bucketPutWebsite.js @@ -1,16 +1,15 @@ const async = require('async'); const { errors } = require('arsenal'); -const bucketShield = require('./apiUtils/bucket/bucketShield'); const collectCorsHeaders = require('../utilities/collectCorsHeaders'); -const { isBucketAuthorized } = - require('./apiUtils/authorization/permissionChecks'); const metadata = require('../metadata/wrapper'); +const { standardMetadataValidateBucket } = require('../metadata/metadataUtils'); const { parseWebsiteConfigXml } = require('./apiUtils/bucket/bucketWebsite'); const { pushMetric } = require('../utapi/utilities'); const monitoring = require('../utilities/monitoringHandler'); -const requestType = 'bucketPutWebsite'; +const REQUEST_TYPE = 'bucketPutWebsite'; +const METRICS_ACTION = 'putBucketWebsite'; /** * Bucket Put Website - Create bucket website configuration @@ -21,68 +20,55 @@ const requestType = 'bucketPutWebsite'; * @return {undefined} */ function bucketPutWebsite(authInfo, request, log, callback) { - log.debug('processing request', { method: 'bucketPutWebsite' }); - const { bucketName } = request; - const canonicalID = authInfo.getCanonicalID(); + log.debug('processing request', { method: REQUEST_TYPE }); + const bucketName = request.bucketName; + const metadataValParams = { + authInfo, + bucketName, + requestType: request.apiMethod || REQUEST_TYPE, + request, + }; if (!request.post) { - monitoring.promMetrics( - 'PUT', bucketName, 400, 'putBucketWebsite'); + monitoring.promMetrics('PUT', bucketName, 400, METRICS_ACTION); return callback(errors.MissingRequestBodyError); } + return async.waterfall([ - function parseXmlBody(next) { + next => { log.trace('parsing website configuration'); return parseWebsiteConfigXml(request.post, log, next); }, - function getBucketfromMetadata(config, next) { - metadata.getBucket(bucketName, log, (err, bucket) => { + (config, next) => standardMetadataValidateBucket(metadataValParams, request.actionImplicitDenies, log, + (err, bucket) => { + const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { - log.debug('metadata getbucket failed', { error: err }); + monitoring.promMetrics('PUT', bucketName, err.code, METRICS_ACTION); + if (err?.is?.AccessDenied) { + return next(err, corsHeaders); + } return next(err); } - if (bucketShield(bucket, requestType)) { - return next(errors.NoSuchBucket); - } - log.trace('found bucket in metadata'); - return next(null, bucket, config); - }); - }, - function validateBucketAuthorization(bucket, config, next) { - if (!isBucketAuthorized(bucket, request.apiMethods || requestType, canonicalID, - authInfo, log, request, request.actionImplicitDenies)) { - log.debug('access denied for user on bucket', { - requestType, - method: 'bucketPutWebsite', - }); - return next(errors.AccessDenied, bucket); - } - return next(null, bucket, config); - }, - function updateBucketMetadata(bucket, config, next) { + + return next(null, bucket, config, corsHeaders); + }), + (bucket, config, corsHeaders, next) => { log.trace('updating bucket website configuration in metadata'); bucket.setWebsiteConfiguration(config); - metadata.updateBucket(bucketName, bucket, log, err => { - next(err, bucket); + return metadata.updateBucket(bucketName, bucket, log, err => { + next(err, corsHeaders); }); - }, - ], (err, bucket) => { - const corsHeaders = collectCorsHeaders(request.headers.origin, - request.method, bucket); + } + ], (err, corsHeaders) => { if (err) { - log.trace('error processing request', { error: err, - method: 'bucketPutWebsite' }); - monitoring.promMetrics( - 'PUT', bucketName, err.code, 'putBucketWebsite'); - } else { - pushMetric('putBucketWebsite', log, { - authInfo, - bucket: bucketName, - }); - monitoring.promMetrics( - 'PUT', bucketName, '200', 'putBucketWebsite'); + log.trace('error processing request', { error: err, method: REQUEST_TYPE }); + monitoring.promMetrics('PUT', bucketName, err.code, METRICS_ACTION); + return callback(err, corsHeaders); } - return callback(err, corsHeaders); + + pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); + monitoring.promMetrics('PUT', bucketName, '200', METRICS_ACTION); + return callback(null, corsHeaders); }); } From 883e0c876ff3d56ca70392dbb94681d7bbdf9d2e Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Sat, 25 Oct 2025 00:43:42 +0200 Subject: [PATCH 4/6] fixup! CLDSRV-750: add Server Access Logs --- lib/metadata/metadataUtils.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/metadata/metadataUtils.js b/lib/metadata/metadataUtils.js index 58c1308275..63ecbcdbbc 100644 --- a/lib/metadata/metadataUtils.js +++ b/lib/metadata/metadataUtils.js @@ -11,20 +11,27 @@ const { actionNeedQuotaCheck, actionWithDataDeletion } = require('arsenal/build/ const { processBytesToWrite, validateQuotas } = require('../api/apiUtils/quotas/quotaUtils'); function storeServerAccessLogInfo(request, authInfo, bucket) { - if (request && - request.serverAccessLog && - authInfo && - bucket && - bucket.getBucketLoggingStatus() && - bucket.getBucketLoggingStatus().getLoggingEnabled()) { - /* eslint-disable no-param-reassign */ - request.serverAccessLog.enabled = true; + /* eslint-disable no-param-reassign */ + + if (!request || !request.serverAccessLog) { + return; + } + + if (authInfo) { + request.serverAccessLog.authInfo = authInfo; + } + + if (bucket) { request.serverAccessLog.bucketOwner = bucket.getOwner(); request.serverAccessLog.bucketName = bucket.getName(); - request.serverAccessLog.authInfo = authInfo; + } + + if (bucket && bucket.getBucketLoggingStatus() && bucket.getBucketLoggingStatus().getLoggingEnabled()) { + request.serverAccessLog.enabled = true; request.serverAccessLog.loggingEnabled = bucket.getBucketLoggingStatus().getLoggingEnabled(); - /* eslint-enable no-param-reassign */ } + + /* eslint-enable no-param-reassign */ } /** getNullVersionFromMaster - retrieves the null version From 095e0f7fdf6d8282571b7a2492704768f80a9520 Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Sat, 25 Oct 2025 01:30:30 +0200 Subject: [PATCH 5/6] fixup! CLDSRV-750: replace repeated logic with standardMetadataValidateBucket --- lib/api/bucketPutCors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/bucketPutCors.js b/lib/api/bucketPutCors.js index bd37a4281d..2cc05d25ad 100644 --- a/lib/api/bucketPutCors.js +++ b/lib/api/bucketPutCors.js @@ -73,7 +73,7 @@ function bucketPutCors(authInfo, request, log, callback) { } pushMetric(METRICS_ACTION, log, { authInfo, bucket: bucketName }); monitoring.promMetrics('PUT', bucketName, '200', METRICS_ACTION); - return callback(null); + return callback(null, corsHeaders); }); } From ed7705b45994c0b6691e2b49050262b93304ac59 Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Thu, 30 Oct 2025 17:25:56 +0100 Subject: [PATCH 6/6] fixup! CLDSRV-750: add Server Access Logs --- lib/api/api.js | 3 +++ lib/metadata/metadataUtils.js | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/api/api.js b/lib/api/api.js index ba9d6eb762..1b974e329e 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -219,6 +219,9 @@ const api = { return async.waterfall([ next => auth.server.doAuth( request, log, (err, userInfo, authorizationResults, streamingV4Params, infos) => { + if (request.serverAccessLog) { + request.serverAccessLog.authInfo = userInfo; + } if (err) { // VaultClient returns standard errors, but the route requires // Arsenal errors diff --git a/lib/metadata/metadataUtils.js b/lib/metadata/metadataUtils.js index 63ecbcdbbc..2ceecfcab7 100644 --- a/lib/metadata/metadataUtils.js +++ b/lib/metadata/metadataUtils.js @@ -17,10 +17,6 @@ function storeServerAccessLogInfo(request, authInfo, bucket) { return; } - if (authInfo) { - request.serverAccessLog.authInfo = authInfo; - } - if (bucket) { request.serverAccessLog.bucketOwner = bucket.getOwner(); request.serverAccessLog.bucketName = bucket.getName();