Skip to content

Commit 0251b1b

Browse files
committed
CLDSRV-781: Add ratelimiting post MD fetch
1 parent 437cf8b commit 0251b1b

File tree

1 file changed

+91
-9
lines changed

1 file changed

+91
-9
lines changed

lib/metadata/metadataUtils.js

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const bucketShield = require('../api/apiUtils/bucket/bucketShield');
99
const { onlyOwnerAllowed } = require('../../constants');
1010
const { actionNeedQuotaCheck, actionWithDataDeletion } = require('arsenal/build/lib/policyEvaluator/RequestContext');
1111
const { processBytesToWrite, validateQuotas } = require('../api/apiUtils/quotas/quotaUtils');
12+
const { config } = require('../Config');
13+
const {
14+
extractAndCacheRateLimitConfig,
15+
checkRateLimitWithConfig,
16+
} = require('../api/apiUtils/rateLimit/helpers');
1217

1318
/** getNullVersionFromMaster - retrieves the null version
1419
* metadata via retrieving the master key
@@ -169,6 +174,65 @@ function validateBucket(bucket, params, log, actionImplicitDenies = {}) {
169174
}
170175
return null;
171176
}
177+
178+
/**
179+
* Check rate limiting if not already checked
180+
*
181+
* Extracts rate limit config from bucket metadata, caches it, and enforces limit.
182+
* Calls callback with error if rate limited, null if allowed or no rate limiting.
183+
*
184+
* @param {object} bucket - Bucket metadata object
185+
* @param {string} bucketName - Bucket name
186+
* @param {object} request - Request object with rateLimitAlreadyChecked tracker
187+
* @param {object} log - Logger instance
188+
* @param {function} callback - Callback(err) - err if rate limited, null if allowed
189+
* @returns {undefined}
190+
*/
191+
function checkRateLimitIfNeeded(bucket, bucketName, request, log, callback) {
192+
// Skip if already checked or not enabled
193+
if (request.rateLimitAlreadyChecked || !config.rateLimiting?.enabled) {
194+
return process.nextTick(callback, null);
195+
}
196+
197+
// Extract rate limit config from bucket metadata and cache it
198+
const rateLimitConfig = extractAndCacheRateLimitConfig(bucket, bucketName, log);
199+
200+
// No rate limiting configured
201+
if (!rateLimitConfig) {
202+
// eslint-disable-next-line no-param-reassign
203+
request.rateLimitAlreadyChecked = true;
204+
return process.nextTick(callback, null);
205+
}
206+
207+
// Check rate limit with GCRA
208+
return checkRateLimitWithConfig(
209+
bucketName,
210+
rateLimitConfig,
211+
log,
212+
(rateLimitErr, rateLimited) => {
213+
if (rateLimitErr) {
214+
log.error('Rate limit check error in metadata validation', {
215+
error: rateLimitErr,
216+
});
217+
}
218+
219+
if (rateLimited) {
220+
log.addDefaultFields({
221+
rateLimited: true,
222+
rateLimitSource: rateLimitConfig.source,
223+
});
224+
// eslint-disable-next-line no-param-reassign
225+
request.rateLimitAlreadyChecked = true;
226+
return callback(config.rateLimiting.error);
227+
}
228+
229+
// Allowed - set tracker and continue
230+
// eslint-disable-next-line no-param-reassign
231+
request.rateLimitAlreadyChecked = true;
232+
return callback(null);
233+
}
234+
);
235+
}
172236
/** standardMetadataValidateBucketAndObj - retrieve bucket and object md from metadata
173237
* and check if user is authorized to access them.
174238
* @param {object} params - function parameters
@@ -222,12 +286,21 @@ function standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log,
222286
if (validationError) {
223287
return next(validationError, bucket);
224288
}
225-
const objMD = getResult.obj ? JSON.parse(getResult.obj) : undefined;
226-
if (!objMD && versionId === 'null') {
227-
return getNullVersionFromMaster(bucketName, objectKey, log,
228-
(err, nullVer) => next(err, bucket, nullVer));
229-
}
230-
return next(null, bucket, objMD);
289+
290+
// Rate limiting check if not already done in api.js
291+
return checkRateLimitIfNeeded(bucket, bucketName, request, log, err => {
292+
if (err) {
293+
return next(err, bucket);
294+
}
295+
296+
// Continue with object metadata processing
297+
const objMD = getResult.obj ? JSON.parse(getResult.obj) : undefined;
298+
if (!objMD && versionId === 'null') {
299+
return getNullVersionFromMaster(bucketName, objectKey, log,
300+
(err, nullVer) => next(err, bucket, nullVer));
301+
}
302+
return next(null, bucket, objMD);
303+
});
231304
},
232305
(bucket, objMD, next) => {
233306
const objMetadata = objMD;
@@ -294,7 +367,7 @@ function standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log,
294367
* @return {undefined} - and call callback with params err, bucket md
295368
*/
296369
function standardMetadataValidateBucket(params, actionImplicitDenies, log, callback) {
297-
const { bucketName } = params;
370+
const { bucketName, request } = params;
298371
return metadata.getBucket(bucketName, log, (err, bucket) => {
299372
if (err) {
300373
// if some implicit actionImplicitDenies, return AccessDenied before
@@ -305,8 +378,17 @@ function standardMetadataValidateBucket(params, actionImplicitDenies, log, callb
305378
log.debug('metadata getbucket failed', { error: err });
306379
return callback(err);
307380
}
308-
const validationError = validateBucket(bucket, params, log, actionImplicitDenies);
309-
return callback(validationError, bucket);
381+
382+
// Rate limiting check if not already done in api.js
383+
return checkRateLimitIfNeeded(bucket, bucketName, request, log, err => {
384+
if (err) {
385+
return callback(err);
386+
}
387+
388+
// Continue with validation
389+
const validationError = validateBucket(bucket, params, log, actionImplicitDenies);
390+
return callback(validationError, bucket);
391+
});
310392
});
311393
}
312394

0 commit comments

Comments
 (0)