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
6 changes: 5 additions & 1 deletion .github/docker/config.s3c.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,9 @@
"host": "localhost:6000"
}
],
"enableVeeamRoute": false
"enableVeeamRoute": false,
"serverAccessLogs": {
"enabled": true,
"outputFile": "/logs/server-access.log"
}
}
1 change: 1 addition & 0 deletions .github/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ services:
- S3QUOTA
- QUOTA_ENABLE_INFLIGHTS
- S3_VERSION_ID_ENCODING_TYPE
- S3_ENABLE_SERVER_ACCESS_LOGS=true
env_file:
- creds.env
depends_on:
Expand Down
4 changes: 4 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,9 @@
},
"integrityChecks": {
"objectPutRetention": true
},
"serverAccessLogs": {
"enabled": false,
"outputFile": "/logs/server-access.log"
}
}
27 changes: 26 additions & 1 deletion lib/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,31 @@ function parseIntegrityChecks(config) {
return integrityChecks;
}

function parseServerAccessLogs(config) {
const res = {
enabled: false,
outputFile: '/logs/server-access.log',
};

if (config && config.serverAccessLogs) {
if ('enabled' in config.serverAccessLogs) {
assert(typeof config.serverAccessLogs.enabled === 'boolean');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert(typeof config.serverAccessLogs.enabled === 'boolean');
assert(typeof config.serverAccessLogs.enabled === 'boolean', 'bad config: serverAccessLogs.enabled not boolean');

res.enabled = config.serverAccessLogs.enabled;
}

if ('outputFile' in config.serverAccessLogs) {
assert(typeof config.serverAccessLogs.outputFile === 'string');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert(typeof config.serverAccessLogs.outputFile === 'string');
assert(typeof config.serverAccessLogs.outputFile === 'string', 'bad config: serverAccessLogs.outputFile not string');

res.outputFile = config.serverAccessLogs.outputFile;
}
}

if (process.env.S3_ENABLE_SERVER_ACCESS_LOGS === 'true') {
res.enabled = true;
}

return res;
}

/**
* Reads from a config file and returns the content as a config object
*/
Expand Down Expand Up @@ -1785,7 +1810,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
Expand Down
15 changes: 15 additions & 0 deletions lib/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ const api = {
objectKey: request.objectKey,
});
}
if (request.serverAccessLog) {
request.serverAccessLog.objectKey = request.objectKey;
request.serverAccessLog.analyticsAction = actionLog;
}
let returnTagCount = true;

const validationRes = validateQueryAndHeaders(request, log);
Expand Down Expand Up @@ -219,6 +223,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
Expand All @@ -237,6 +244,10 @@ const api = {
authNames.sessionName = userInfo.getShortid().split(':')[1];
}
log.addDefaultFields(authNames);
if (request.serverAccessLog) {
request.serverAccessLog.analyticsAccountName = authNames.accountName;
request.serverAccessLog.analyticsUserName = authNames.userName;
}
if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') {
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
}
Expand Down Expand Up @@ -264,6 +275,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 });
Expand Down
69 changes: 22 additions & 47 deletions lib/api/bucketDeleteCors.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,62 +17,40 @@ 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);
}

log.trace('deleting cors configuration in metadata');
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);
});
});
}
Expand Down
67 changes: 21 additions & 46 deletions lib/api/bucketDeleteWebsite.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,47 @@
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);
}

log.trace('deleting website configuration in metadata');
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);
});
});
Expand Down
61 changes: 20 additions & 41 deletions lib/api/bucketGetCors.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
});
}
Expand Down
Loading
Loading