diff --git a/lib/api/api.js b/lib/api/api.js index 195441c3fe..349c29fc97 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -263,6 +263,19 @@ const api = { request.serverAccessLog.analyticsUserName = authNames.userName; } if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') { + const setStartTurnAroundTime = () => { + if (request.serverAccessLog) { + request.serverAccessLog.startTurnAroundTime = process.hrtime.bigint(); + } + }; + // For 0-byte uploads, downstream handlers do not consume + // the request stream, so 'end' never fires. Set + // startTurnAroundTime synchronously in that case. + if (request.headers['content-length'] === '0') { + setStartTurnAroundTime(); + } else { + request.on('end', setStartTurnAroundTime); + } return next(null, userInfo, authorizationResults, streamingV4Params, infos); } // issue 100 Continue to the client diff --git a/lib/utilities/serverAccessLogger.js b/lib/utilities/serverAccessLogger.js index e6869df940..680dab63aa 100644 --- a/lib/utilities/serverAccessLogger.js +++ b/lib/utilities/serverAccessLogger.js @@ -320,16 +320,21 @@ function getOperation(req) { return `REST.${req.method}.${resourceType}`; } +const assumedRoleArnRegex = /^arn:aws:sts::[0-9]{12}:assumed-role\/\S+$/; + function getRequester(authInfo) { const requester = null; if (authInfo) { + const arn = authInfo.getArn ? authInfo.getArn() : null; if (authInfo.isRequesterPublicUser && authInfo.isRequesterPublicUser()) { return requester; // Unauthenticated requests + } else if (arn && assumedRoleArnRegex.test(arn)) { + return arn; } 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(); + // IAM user: emit the IAM ARN (arn:aws:iam:::user/) + // to match the AWS S3 server access log format. Fall back to the + // canonical ID if the ARN is unexpectedly absent. + return arn || authInfo.getCanonicalID(); } else if (authInfo.getCanonicalID) { // Regular user: canonical user ID return authInfo.getCanonicalID(); diff --git a/package.json b/package.json index 90d7f1fb54..8feca11c8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.2.24-1", + "version": "9.2.24-2", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": { diff --git a/tests/unit/api/api.js b/tests/unit/api/api.js index 78626bd651..9d08d33207 100644 --- a/tests/unit/api/api.js +++ b/tests/unit/api/api.js @@ -115,6 +115,40 @@ describe('api.callApiMethod', () => { api.callApiMethod('multipartDelete', request, response, log); }); + ['objectPut', 'objectPutPart'].forEach(method => { + it(`should set startTurnAroundTime on request end for ${method}`, done => { + sandbox.stub(api, method).callsFake( + (userInfo, _request, streamingV4Params, _log, cb) => { + request.on('end', () => { + assert.strictEqual(typeof request.serverAccessLog.startTurnAroundTime, 'bigint'); + cb(); + }); + request.resume(); + }); + request.objectKey = 'testobject'; + request.serverAccessLog = {}; + api.callApiMethod(method, request, response, log, err => { + assert.ifError(err); + done(); + }); + }); + + it(`should set startTurnAroundTime synchronously for 0-byte ${method}`, done => { + sandbox.stub(api, method).callsFake( + (userInfo, _request, streamingV4Params, _log, cb) => { + assert.strictEqual(typeof request.serverAccessLog.startTurnAroundTime, 'bigint'); + cb(); + }); + request.objectKey = 'testobject'; + request.serverAccessLog = {}; + request.headers = Object.assign({}, request.headers, { 'content-length': '0' }); + api.callApiMethod(method, request, response, log, err => { + assert.ifError(err); + done(); + }); + }); + }); + describe('MD5 checksum validation', () => { const methodsWithChecksumValidation = [ 'bucketPutACL', diff --git a/tests/unit/utils/serverAccessLogger.js b/tests/unit/utils/serverAccessLogger.js index a3c3c59d18..6f02034416 100644 --- a/tests/unit/utils/serverAccessLogger.js +++ b/tests/unit/utils/serverAccessLogger.js @@ -269,30 +269,41 @@ describe('serverAccessLogger utility functions', () => { assert.strictEqual(result, null); }); - it('should return IAM user name with account for IAM user', () => { + it('should return IAM ARN for IAM user', () => { + const arn = 'arn:aws:iam::123456789012:user/myuser'; const authInfo = { isRequesterPublicUser: () => false, isRequesterAnIAMUser: () => true, - getIAMdisplayName: () => 'iamUser', - getAccountDisplayName: () => 'accountName', + getArn: () => arn, getCanonicalID: () => 'canonicalID123', }; const result = getRequester(authInfo); - assert.strictEqual(result, 'iamUser:accountName'); + assert.strictEqual(result, arn); }); - it('should return canonical ID for IAM user if display names are missing', () => { + it('should fall back to canonical ID for IAM user when ARN is missing', () => { const authInfo = { isRequesterPublicUser: () => false, isRequesterAnIAMUser: () => true, - getIAMdisplayName: () => '', - getAccountDisplayName: () => 'accountName', + getArn: () => undefined, getCanonicalID: () => 'canonicalID123', }; const result = getRequester(authInfo); assert.strictEqual(result, 'canonicalID123'); }); + it('should return ARN for assumed-role session user', () => { + const arn = 'arn:aws:sts::123456789012:assumed-role/lifecycle-role/backbeat-lifecycle'; + const authInfo = { + isRequesterPublicUser: () => false, + isRequesterAnIAMUser: () => false, + getArn: () => arn, + getCanonicalID: () => 'canonicalID789', + }; + const result = getRequester(authInfo); + assert.strictEqual(result, arn); + }); + it('should return canonical ID for regular user', () => { const authInfo = { isRequesterPublicUser: () => false,