diff --git a/lib/api/api.js b/lib/api/api.js index 349c29fc97..a11446085c 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -81,6 +81,8 @@ const { isRequesterASessionUser } = require('./apiUtils/authorization/permission const checkHttpHeadersSize = require('./apiUtils/object/checkHttpHeadersSize'); const constants = require('../../constants'); const { config } = require('../Config.js'); +const metadata = require('../metadata/wrapper'); +const collectCorsHeaders = require('../utilities/collectCorsHeaders'); const { validateMethodChecksumNoChunking } = require('./apiUtils/integrity/validateChecksums'); const { getRateLimitFromCache, @@ -92,6 +94,88 @@ const monitoringMap = policies.actionMaps.actionMonitoringMapS3; auth.setHandler(vault); +// Detect CORS headers the handler already attached to its callback args. +// Callback arity varies per route ((err, corsHeaders), (err, xml, +// corsHeaders), (err, dataGetInfo, resMetaHeaders, range), ...) so we +// scan the args instead of relying on a fixed position. +function hasCorsHeaders(rest) { + for (const arg of rest) { + if (!arg || typeof arg !== 'object' || Array.isArray(arg) + || Buffer.isBuffer(arg)) { + continue; + } + if ('access-control-allow-origin' in arg) { + return true; + } + } + return false; +} + +// Wrap the API callback so that, on error, we look up the bucket's CORS +// configuration and set the matching Access-Control-* headers directly on +// the HTTP response. This is needed because auth / pre-handler errors +// return before the API handler retrieves the bucket (where the existing +// collectCorsHeaders call lives), so the route-level error response path +// otherwise sends no CORS headers - breaking cross-origin browser clients. +// We only pay the extra metadata lookup when an Origin header is present, +// matching AWS behavior and the existing contract of collectCorsHeaders. +function wrapCallbackWithErrorCorsHeaders(request, response, log, callback) { + function applyHeadersFromBucket(bucket) { + if (!bucket || response.headersSent) { + return; + } + const headers = collectCorsHeaders( + request.headers.origin, request.method, bucket); + Object.keys(headers).forEach(key => { + if (headers[key] === undefined) { + return; + } + try { + response.setHeader(key, headers[key]); + } catch (e) { + log.debug('could not set cors header on error', { + header: key, error: e.message, + }); + } + }); + } + + return (err, ...rest) => { + if (!err || !request.headers || !request.headers.origin + || !request.bucketName) { + return callback(err, ...rest); + } + // Fast path: most post-auth failures come back with corsHeaders + // already computed by the handler. The route will forward them + // to responseXMLBody/responseNoBody, so no extra lookup is needed. + if (hasCorsHeaders(rest)) { + return callback(err, ...rest); + } + // Reuse bucket cached on the request by standardMetadataValidate* + // to avoid a redundant metadata.getBucket call. + if (request._loadedBucket) { + applyHeadersFromBucket(request._loadedBucket); + return callback(err, ...rest); + } + // Fallback: bucket was never loaded (pre-handler failure like + // auth.server.doAuth denial, header-size check, copy-source parse + // error). Look the bucket up so we can still reply with CORS + // headers. + return metadata.getBucket(request.bucketName, log, (mdErr, bucket) => { + if (mdErr) { + log.warn('could not fetch bucket CORS config for error ' + + 'response', { + bucketName: request.bucketName, + error: mdErr.code || mdErr.message, + }); + return callback(err, ...rest); + } + applyHeadersFromBucket(bucket); + return callback(err, ...rest); + }); + }; +} + function checkAuthResults(authResults, apiMethod, log) { let returnTagCount = true; const isImplicitDeny = {}; @@ -158,6 +242,8 @@ const api = { callApiMethod(apiMethod, request, response, log, callback) { // Attach the apiMethod method to the request, so it can used by monitoring in the server request.apiMethod = apiMethod; + callback = wrapCallbackWithErrorCorsHeaders( + request, response, log, callback); // Array of end of API callbacks, used to perform some logic // at the end of an API. request.finalizerHooks = []; diff --git a/lib/api/bucketGetCors.js b/lib/api/bucketGetCors.js index 8c13132fb7..482ffdda62 100644 --- a/lib/api/bucketGetCors.js +++ b/lib/api/bucketGetCors.js @@ -29,10 +29,7 @@ function bucketGetCors(authInfo, request, log, callback) { const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { monitoring.promMetrics('GET', bucketName, err.code, METRICS_ACTION); - if (err?.is?.AccessDenied) { - return callback(err, corsHeaders); - } - return callback(err); + return callback(err, null, corsHeaders); } const cors = bucket.getCors(); diff --git a/lib/api/bucketGetLocation.js b/lib/api/bucketGetLocation.js index 83e0f3b601..31d8b6dfec 100644 --- a/lib/api/bucketGetLocation.js +++ b/lib/api/bucketGetLocation.js @@ -29,10 +29,7 @@ function bucketGetLocation(authInfo, request, log, callback) { const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { monitoring.promMetrics('GET', bucketName, err.code, METRICS_ACTION); - if (err?.is?.AccessDenied) { - return callback(err, corsHeaders); - } - return callback(err); + return callback(err, null, corsHeaders); } let locationConstraint = bucket.getLocationConstraint(); diff --git a/lib/api/bucketGetWebsite.js b/lib/api/bucketGetWebsite.js index 0f20333c3d..49f12683f5 100644 --- a/lib/api/bucketGetWebsite.js +++ b/lib/api/bucketGetWebsite.js @@ -30,10 +30,7 @@ function bucketGetWebsite(authInfo, request, log, callback) { const corsHeaders = collectCorsHeaders(request.headers.origin, request.method, bucket); if (err) { monitoring.promMetrics('GET', bucketName, err.code, METRICS_ACTION); - if (err?.is?.AccessDenied) { - return callback(err, corsHeaders); - } - return callback(err); + return callback(err, null, corsHeaders); } const websiteConfig = bucket.getWebsiteConfiguration(); diff --git a/lib/metadata/metadataUtils.js b/lib/metadata/metadataUtils.js index c8588f9859..0974b52e65 100644 --- a/lib/metadata/metadataUtils.js +++ b/lib/metadata/metadataUtils.js @@ -431,6 +431,11 @@ function standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, }, ], (err, bucket, objMD, raftSessionId) => { storeServerAccessLogInfo(request, bucket, raftSessionId, params.serverAccessLogOptions); + if (bucket && request) { + // Cache the loaded bucket so downstream code can reuse it + // without re-fetching from metadata. + request._loadedBucket = bucket; + } if (err) { // still return bucket for cors headers return callback(err, bucket); @@ -454,6 +459,11 @@ function standardMetadataValidateBucket(params, actionImplicitDenies, log, callb const { bucketName, request } = params; return metadata.getBucket(bucketName, log, (err, bucket, raftSessionId) => { storeServerAccessLogInfo(params.request, bucket, raftSessionId); + if (bucket && request) { + // Cache the loaded bucket so downstream code can reuse it + // without re-fetching from metadata. + request._loadedBucket = bucket; + } if (err) { // if some implicit actionImplicitDenies, return AccessDenied before // leaking any state information diff --git a/lib/utilities/collectCorsHeaders.js b/lib/utilities/collectCorsHeaders.js index 508bbe55ab..286e510953 100644 --- a/lib/utilities/collectCorsHeaders.js +++ b/lib/utilities/collectCorsHeaders.js @@ -9,15 +9,11 @@ const { findCorsRule, generateCorsResHeaders } = * @return {object} - object containing CORS headers */ function collectCorsHeaders(origin, httpMethod, bucket) { - // NOTE: Because collecting CORS headers requires making a call to - // metadata to retrieve the bucket's CORS configuration, we opt not to - // return the CORS headers if the request encounters an error before - // the api method retrieves the bucket from metadata (an example - // being if a request is not properly authenticated). This is a slight - // deviation from AWS compatibility, but has the benefit of avoiding - // additional backend calls for an invalid request. Also, we anticipate - // that the preflight OPTIONS route will serve most client needs regarding - // CORS. + // Returns {} when no bucket is supplied; this function does not itself + // fetch metadata. Callers that only have a bucketName (e.g. auth + // failures that happen before the API handler loads the bucket) should + // look it up themselves - see wrapCallbackWithErrorCorsHeaders in + // lib/api/api.js. if (!origin || !bucket) { return {}; } diff --git a/tests/functional/aws-node-sdk/test/object/corsErrorHeaders.js b/tests/functional/aws-node-sdk/test/object/corsErrorHeaders.js new file mode 100644 index 0000000000..8c2dd1b216 --- /dev/null +++ b/tests/functional/aws-node-sdk/test/object/corsErrorHeaders.js @@ -0,0 +1,152 @@ +const { S3 } = require('aws-sdk'); +const assert = require('assert'); +const async = require('async'); + +const getConfig = require('../support/config'); +const { methodRequest, generateCorsParams } = + require('../../lib/utility/cors-util'); + +const config = getConfig('default', { signatureVersion: 'v4' }); +const s3 = new S3(config); + +const bucket = 'corserrorheadertest'; +const objectKey = 'objectKey'; +const allowedOrigin = 'http://www.allowed.test'; +const vary = 'Origin, Access-Control-Request-Headers, ' + + 'Access-Control-Request-Method'; + +const expectedCorsHeaders = { + 'access-control-allow-origin': allowedOrigin, + 'access-control-allow-methods': 'GET, PUT, POST, DELETE, HEAD', + 'access-control-allow-credentials': 'true', + vary, +}; + +const corsParams = generateCorsParams(bucket, { + allowedMethods: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'], + allowedOrigins: [allowedOrigin], + allowedHeaders: ['*'], +}); + +// Raw unauthenticated requests - they always return 403. +// Each spec describes (method, path, query) against the bucket. +const unauthenticatedRequests = [ + { description: 'GET bucket (list objects)', + method: 'GET', query: null, objectKey: null }, + { description: 'HEAD bucket', + method: 'HEAD', query: null, objectKey: null }, + { description: 'DELETE bucket', + method: 'DELETE', query: null, objectKey: null }, + { description: 'GET bucket ACL', + method: 'GET', query: 'acl', objectKey: null }, + { description: 'GET bucket CORS', + method: 'GET', query: 'cors', objectKey: null }, + { description: 'GET bucket versioning', + method: 'GET', query: 'versioning', objectKey: null }, + { description: 'GET bucket website', + method: 'GET', query: 'website', objectKey: null }, + { description: 'GET bucket tagging', + method: 'GET', query: 'tagging', objectKey: null }, + { description: 'GET object', + method: 'GET', query: null, objectKey }, + { description: 'HEAD object', + method: 'HEAD', query: null, objectKey }, + { description: 'PUT object', + method: 'PUT', query: null, objectKey }, + { description: 'DELETE object', + method: 'DELETE', query: null, objectKey }, + { description: 'GET bucket uploads (list multipart uploads)', + method: 'GET', query: 'uploads', objectKey: null }, + // GET bucket policy and POST multi-delete are not covered here: the + // first returns 405 (method rejected pre-auth), the second returns 400 + // (missing XML body fails validation pre-auth). Neither reaches the + // 403 path. Both are exercised via the unit test that stubs auth + // failure directly. +]; + +function _waitForAWS(callback, err) { + if (err) { + return setTimeout(() => callback(err), 500); + } + return setTimeout(() => callback(), 500); +} + +describe('CORS headers on 403 responses when bucket has CORS configured', () => { + before(done => async.series([ + cb => s3.createBucket({ Bucket: bucket }, err => _waitForAWS(cb, err)), + cb => s3.putBucketCors(corsParams, err => _waitForAWS(cb, err)), + ], done)); + + after(done => s3.deleteBucket({ Bucket: bucket }, + err => _waitForAWS(done, err))); + + unauthenticatedRequests.forEach(spec => { + it(`returns CORS headers on 403 for ${spec.description} ` + + 'when Origin matches a rule', done => { + methodRequest({ + method: spec.method, + bucket, + objectKey: spec.objectKey, + query: spec.query, + headers: { origin: allowedOrigin }, + // Use numeric status: HEAD responses have no body, and some + // endpoints (bucket policy, multi-delete) can fail with a + // non-AccessDenied body before auth even runs. We only care + // about the 403 status and the CORS headers here. + code: 403, + headersResponse: expectedCorsHeaders, + }, done); + }); + }); + + it('omits CORS headers on 403 when Origin does not match any rule', + done => { + methodRequest({ + method: 'GET', + bucket, + query: null, + objectKey: null, + headers: { origin: 'http://not-allowed.test' }, + code: 403, + // headersResponse unset -> cors-util asserts CORS headers + // are NOT present. + }, done); + }); + + it('omits CORS headers on 403 when no Origin header is sent', + done => { + methodRequest({ + method: 'GET', + bucket, + query: null, + objectKey: null, + headers: {}, + code: 403, + }, done); + }); +}); + +describe('CORS headers on 200 responses (regression guard)', () => { + before(done => async.series([ + cb => s3.createBucket({ Bucket: bucket }, err => _waitForAWS(cb, err)), + cb => s3.putBucketCors(corsParams, err => _waitForAWS(cb, err)), + ], done)); + + after(done => s3.deleteBucket({ Bucket: bucket }, + err => _waitForAWS(done, err))); + + it('returns CORS headers on a successful list objects (200)', done => { + const request = s3.listObjects({ Bucket: bucket }); + request.on('build', () => { + request.httpRequest.headers.origin = allowedOrigin; + }); + request.on('success', response => { + const h = response.httpResponse.headers; + assert.strictEqual(h['access-control-allow-origin'], + allowedOrigin); + done(); + }); + request.on('error', err => done(err)); + request.send(); + }); +}); diff --git a/tests/unit/api/corsErrorHeaders.js b/tests/unit/api/corsErrorHeaders.js new file mode 100644 index 0000000000..7d22947c70 --- /dev/null +++ b/tests/unit/api/corsErrorHeaders.js @@ -0,0 +1,333 @@ +const assert = require('assert'); +const sinon = require('sinon'); +const { errors, auth } = require('arsenal'); + +const api = require('../../../lib/api/api'); +const { bucketGet } = require('../../../lib/api/bucketGet'); +const bucketGetCors = require('../../../lib/api/bucketGetCors'); +const { bucketPut } = require('../../../lib/api/bucketPut'); +const bucketPutCors = require('../../../lib/api/bucketPutCors'); +const metadata = require('../../../lib/metadata/wrapper'); +const DummyRequest = require('../DummyRequest'); +const { + CorsConfigTester, + DummyRequestLogger, + cleanup, + makeAuthInfo, +} = require('../helpers'); + +const endpoints = [ + { apiMethod: 'bucketGet', httpMethod: 'GET', url: '/', query: {} }, + { apiMethod: 'bucketHead', httpMethod: 'HEAD', url: '/', query: {} }, + { apiMethod: 'bucketDelete', httpMethod: 'DELETE', url: '/', query: {} }, + { apiMethod: 'bucketGetACL', httpMethod: 'GET', url: '/?acl', + query: { acl: '' } }, + { apiMethod: 'bucketGetCors', httpMethod: 'GET', url: '/?cors', + query: { cors: '' } }, + { apiMethod: 'bucketGetLifecycle', httpMethod: 'GET', url: '/?lifecycle', + query: { lifecycle: '' } }, + { apiMethod: 'bucketGetReplication', httpMethod: 'GET', + url: '/?replication', query: { replication: '' } }, + { apiMethod: 'bucketGetPolicy', httpMethod: 'GET', url: '/?policy', + query: { policy: '' } }, + { apiMethod: 'bucketGetVersioning', httpMethod: 'GET', url: '/?versioning', + query: { versioning: '' } }, + { apiMethod: 'bucketGetWebsite', httpMethod: 'GET', url: '/?website', + query: { website: '' } }, + { apiMethod: 'bucketGetTagging', httpMethod: 'GET', url: '/?tagging', + query: { tagging: '' } }, + { apiMethod: 'bucketGetEncryption', httpMethod: 'GET', url: '/?encryption', + query: { encryption: '' } }, + { apiMethod: 'bucketGetNotification', httpMethod: 'GET', + url: '/?notification', query: { notification: '' } }, + { apiMethod: 'bucketGetObjectLock', httpMethod: 'GET', + url: '/?object-lock', query: { 'object-lock': '' } }, + { apiMethod: 'bucketGetLocation', httpMethod: 'GET', url: '/?location', + query: { location: '' } }, + { apiMethod: 'objectGet', httpMethod: 'GET', url: '/obj', query: {}, + objectKey: 'obj' }, + { apiMethod: 'objectHead', httpMethod: 'HEAD', url: '/obj', query: {}, + objectKey: 'obj' }, + { apiMethod: 'objectDelete', httpMethod: 'DELETE', url: '/obj', query: {}, + objectKey: 'obj' }, + { apiMethod: 'listMultipartUploads', httpMethod: 'GET', url: '/?uploads', + query: { uploads: '' } }, +]; + +const bucketName = 'corserrorheaderstest'; +const authInfo = makeAuthInfo('accessKey1'); +const origin = 'http://foo.test'; + +function setupBucketWithCors(done) { + cleanup(); + const putBucketReq = { + bucketName, + headers: { host: `${bucketName}.s3.amazonaws.com` }, + url: '/', + actionImplicitDenies: false, + }; + // Single rule allowing all tested HTTP methods from the test Origin. + // Using CorsConfigTester's default rules would leave HEAD/DELETE + // uncovered for our origin and mask the thing we want to assert. + const corsUtil = new CorsConfigTester({ + allowedMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'], + allowedOrigins: [origin], + allowedHeaders: ['*'], + }); + const putCorsReq = corsUtil.createBucketCorsRequest('PUT', bucketName); + const log = new DummyRequestLogger(); + return bucketPut(authInfo, putBucketReq, log, err => { + if (err) { + return done(err); + } + return bucketPutCors(authInfo, putCorsReq, log, done); + }); +} + +function buildRequest(spec) { + // DummyRequest is an http.IncomingMessage stream that emits 'end' + // synchronously. We need that because callApiMethod's waterfall + // waits for the request body on non-objectPut paths. + return new DummyRequest({ + bucketName, + objectKey: spec.objectKey, + headers: { + host: `${bucketName}.s3.amazonaws.com`, + origin, + }, + url: spec.url, + query: spec.query, + method: spec.httpMethod, + }, Buffer.alloc(0)); +} + +function buildResponseSpy(sandbox) { + const headers = {}; + return { + headers, + setHeader: sandbox.spy((k, v) => { headers[k.toLowerCase()] = v; }), + getHeader: k => headers[k.toLowerCase()], + }; +} + +function buildLog(sandbox) { + return { + addDefaultFields: sandbox.stub(), + trace: sandbox.stub(), + debug: sandbox.stub(), + info: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + fatal: sandbox.stub(), + end() { return this; }, + }; +} + +describe('CORS headers on 403 auth failures (api.callApiMethod)', () => { + let sandbox; + + before(done => setupBucketWithCors(done)); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + const authServer = { + doAuth: sandbox.stub().callsArgWith(2, errors.AccessDenied), + }; + sandbox.stub(auth, 'server').value(authServer); + }); + + afterEach(() => sandbox.restore()); + + endpoints.forEach(spec => { + it(`attaches CORS headers to 403 response for ${spec.apiMethod}`, + done => { + const request = buildRequest(spec); + const response = buildResponseSpy(sandbox); + const log = buildLog(sandbox); + + api.callApiMethod(spec.apiMethod, request, response, log, + err => { + assert(err, 'expected an error'); + assert(err.is && err.is.AccessDenied, + `expected AccessDenied, got ${err.code}`); + // Either the callback surfaces CORS headers in one of + // its trailing args OR they have been set directly on + // the HTTP response. We assert on the response since + // that is what the HTTP transport ultimately sends. + const allowOrigin = response.getHeader( + 'access-control-allow-origin'); + assert(allowOrigin, + 'access-control-allow-origin missing from 403 ' + + `response for ${spec.apiMethod}`); + assert(response.getHeader( + 'access-control-allow-methods'), + 'access-control-allow-methods missing'); + done(); + }); + }); + }); + + it('does not attach CORS headers when Origin header is absent', + done => { + const request = buildRequest({ + apiMethod: 'bucketGet', httpMethod: 'GET', + url: '/', query: {}, + }); + delete request.headers.origin; + const response = buildResponseSpy(sandbox); + const log = buildLog(sandbox); + + api.callApiMethod('bucketGet', request, response, log, err => { + assert(err && err.is.AccessDenied); + assert.strictEqual( + response.getHeader('access-control-allow-origin'), + undefined); + done(); + }); + }); + + it('does not attach CORS headers when origin does not match any rule', + done => { + const request = buildRequest({ + apiMethod: 'bucketGet', httpMethod: 'GET', + url: '/', query: {}, + }); + request.headers.origin = 'http://not-allowed.test'; + // The bucket's CORS config allows any method from foo.test + // plus GET from *. Use PUT from a different origin so neither + // condition matches. + request.method = 'PUT'; + const response = buildResponseSpy(sandbox); + const log = buildLog(sandbox); + + api.callApiMethod('bucketGet', request, response, log, err => { + assert(err && err.is.AccessDenied); + assert.strictEqual( + response.getHeader('access-control-allow-origin'), + undefined); + done(); + }); + }); +}); + +describe('CORS headers on 403 via handler (fast path)', () => { + let sandbox; + + before(done => setupBucketWithCors(done)); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + // Auth succeeds as accessKey2 so the handler runs and then + // denies at its own ACL check (bucket is owned by accessKey1). + const otherAuth = makeAuthInfo('accessKey2'); + const authServer = { + doAuth: sandbox.stub().callsArgWith(2, null, otherAuth, + [{ isAllowed: true, isImplicit: false }], null, {}), + }; + sandbox.stub(auth, 'server').value(authServer); + }); + + afterEach(() => sandbox.restore()); + + it('forwards handler-provided corsHeaders without setting headers ' + + 'on the response directly', done => { + const request = buildRequest({ + apiMethod: 'bucketGet', httpMethod: 'GET', + url: '/', query: {}, + }); + const response = buildResponseSpy(sandbox); + const log = buildLog(sandbox); + + api.callApiMethod('bucketGet', request, response, log, + (err, xml, corsHeaders) => { + assert(err, 'expected an error'); + assert(err.is && err.is.AccessDenied, + `expected AccessDenied, got ${err.code}`); + assert(corsHeaders, + 'handler should have supplied corsHeaders'); + assert.strictEqual( + corsHeaders['access-control-allow-origin'], origin); + // Fast path: wrapper forwards corsHeaders via the callback + // instead of setting them on the response directly. + assert.strictEqual( + response.getHeader('access-control-allow-origin'), + undefined); + done(); + }); + }); + + it('reuses the bucket cached on the request instead of re-fetching', + done => { + const getBucketSpy = sandbox.spy(metadata, 'getBucket'); + const request = buildRequest({ + apiMethod: 'bucketHead', httpMethod: 'HEAD', + url: '/', query: {}, + }); + // Origin that matches no CORS rule -> collectCorsHeaders in + // the handler returns {} -> fast path misses -> wrapper has + // to decide between the cache and a fallback getBucket. + request.headers.origin = 'http://not-allowed.test'; + const response = buildResponseSpy(sandbox); + const log = buildLog(sandbox); + + api.callApiMethod('bucketHead', request, response, log, err => { + assert(err && err.is.AccessDenied); + // The handler's own standardMetadataValidateBucket makes one + // getBucket call. Without the cache reuse, the wrapper would + // make a second call. Assert the total stays at 1. + assert.strictEqual(getBucketSpy.callCount, 1, + 'expected 1 metadata.getBucket call, got ' + + `${getBucketSpy.callCount}`); + done(); + }); + }); +}); + +describe('CORS headers on 200 successful responses (per-handler)', () => { + before(done => setupBucketWithCors(done)); + + it('bucketGet returns corsHeaders to callback on 200', done => { + const log = new DummyRequestLogger(); + const request = { + bucketName, + headers: { + host: `${bucketName}.s3.amazonaws.com`, + origin, + }, + method: 'GET', + url: '/', + query: {}, + actionImplicitDenies: false, + }; + bucketGet(authInfo, request, log, (err, xml, corsHeaders) => { + assert.ifError(err); + assert(corsHeaders, + 'expected corsHeaders to be set on successful bucketGet'); + assert(corsHeaders['access-control-allow-origin'], + 'expected access-control-allow-origin on 200'); + done(); + }); + }); + + it('bucketGetCors returns corsHeaders to callback on 200', done => { + const log = new DummyRequestLogger(); + const request = { + bucketName, + headers: { + host: `${bucketName}.s3.amazonaws.com`, + origin, + }, + method: 'GET', + url: '/?cors', + query: { cors: '' }, + actionImplicitDenies: false, + }; + bucketGetCors(authInfo, request, log, (err, xml, corsHeaders) => { + assert.ifError(err); + assert(corsHeaders + && corsHeaders['access-control-allow-origin'], + 'expected access-control-allow-origin on 200'); + done(); + }); + }); +});