Skip to content

Commit dfe7321

Browse files
Bypass bucket policies on internal endpoint
Currently, bucket policies affect also internal processes (backbeat...), which can allow end users to "break" some features (replication, lifecycle...). The topic was discussed in [1], and the short term solution is to use some sort of "internal" cloudserver, which would not evaluate bucket policies. This is addressed by this commit, which adds the ability to create an "internal" endpoint on a separate port, where bucket policies are ignored. [1] https://scality.atlassian.net/wiki/spaces/OS/pages/2895347722/Authorizing+Internal+Services+S3+Operations Issue: CLDSRV-650
1 parent cc605e2 commit dfe7321

File tree

4 files changed

+132
-12
lines changed

4 files changed

+132
-12
lines changed

lib/api/apiUtils/authorization/permissionChecks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ function processBucketPolicy(requestType, bucket, canonicalID, arn, bucketOwner,
358358
request, aclPermission, results, actionImplicitDenies) {
359359
const bucketPolicy = bucket.getBucketPolicy();
360360
let processedResult = results[requestType];
361-
if (!bucketPolicy) {
361+
if (!bucketPolicy || request?.bypassUserBucketPolicies) {
362362
processedResult = actionImplicitDenies[requestType] === false && aclPermission;
363363
} else {
364364
const bucketPolicyPermission = checkBucketPolicy(bucketPolicy, requestType, canonicalID, arn,

lib/server.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const {
2626

2727
const HttpAgent = require('agentkeepalive');
2828
const QuotaService = require('./quotas/quotas');
29-
const routes = arsenal.s3routes.routes;
3029
const { parseLC, MultipleBackendGateway } = arsenal.storage.data;
3130
const websiteEndpoints = _config.websiteEndpoints;
3231
let client = dataWrapper.client;
@@ -95,11 +94,29 @@ class S3Server {
9594
this.started = false;
9695
}
9796

97+
/**
98+
* Route requests on 'internal s3' port
99+
* Same as routeRequest, but ignoring user's bucket policy. This should be used only for
100+
* backbeat and other internal/system processes that must not be affected by user's bucket
101+
* policy.
102+
* Note that this is not a temporary measure: eventually this should be improved to better
103+
* identify and isolate internal/system calls vs user calls, so that "system" bucket policies
104+
* may be applied to system requests, and the existing "user" bucket policies apply only to
105+
* user requests.
106+
* @param {http.IncomingMessage} req - http request object
107+
* @param {http.ServerResponse} res - http response object
108+
* @returns {undefined}
109+
*/
110+
internalRouteRequest(req, res) {
111+
req.bypassUserBucketPolicies = true; // eslint-disable-line no-param-reassign
112+
return this.routeRequest(req, res);
113+
}
114+
98115
/**
99116
* Route requests on 's3' port
100117
* @param {http.IncomingMessage} req - http request object
101118
* @param {http.ServerResponse} res - http response object
102-
* @returns {void}
119+
* @returns {undefined}
103120
*/
104121
routeRequest(req, res) {
105122
monitoringClient.httpActiveRequests.inc();
@@ -153,14 +170,14 @@ class S3Server {
153170
vault,
154171
},
155172
};
156-
routes(req, res, params, logger, this.config);
173+
arsenal.s3routes.routes(req, res, params, logger, this.config);
157174
}
158175

159176
/**
160177
* Route requests on 'admin' port
161178
* @param {http.IncomingMessage} req - http request object
162179
* @param {http.ServerResponse} res - http response object
163-
* @returns {void}
180+
* @returns {undefined}
164181
*/
165182
routeAdminRequest(req, res) {
166183
// use proxied hostname if needed
@@ -207,7 +224,7 @@ class S3Server {
207224
* @param {http.RequestListener} listener - Callback to handle requests
208225
* @param {number} port - Port to listen on
209226
* @param {string | undefined} ipAddress - IpAddress to listen on
210-
* @returns {void}
227+
* @returns {undefined}
211228
*/
212229
_startServer(listener, port, ipAddress) {
213230
// Todo: http.globalAgent.maxSockets, http.globalAgent.maxFreeSockets
@@ -318,10 +335,10 @@ class S3Server {
318335
// Start internal API server(s)
319336
if (this.config.internalListenOn.length > 0) {
320337
this.config.internalListenOn.forEach(item => {
321-
this._startServer(this.routeRequest, item.port, item.ip);
338+
this._startServer(this.internalRouteRequest, item.port, item.ip);
322339
});
323340
} else if (this.config.internalPort) {
324-
this._startServer(this.routeRequest, this.config.internalPort);
341+
this._startServer(this.internalRouteRequest, this.config.internalPort);
325342
}
326343

327344
// Start metrics server(s)

tests/unit/api/bucketPolicyAuth.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,34 @@ describe('bucket policy authorization', () => {
346346
assert.equal(allowed, false);
347347
done();
348348
});
349+
350+
it('should bypass bucket policy when request.bypassUserBucketPolicies is true', function () {
351+
// Create a request with the bypassUserBucketPolicies flag initially not set
352+
const request = {
353+
bucketName,
354+
socket: {},
355+
};
356+
357+
// Add a policy to explicitely deny access
358+
const newPolicy = this.test.basePolicy;
359+
newPolicy.Statement[0] = {
360+
Effect: 'Deny',
361+
Principal: { CanonicalUser: [bucketOwnerCanonicalId] },
362+
Resource: `arn:aws:s3:::${bucketName}`,
363+
Action: 's3:*',
364+
};
365+
bucket.setBucketPolicy(newPolicy);
366+
367+
// Check that the policy denies access, as expected
368+
assert.ok(!isBucketAuthorized(bucket, bucAction,
369+
bucketOwnerCanonicalId, user1AuthInfo, log, request));
370+
371+
// But with bypassUserBucketPolicies set to true, it should still be authorized
372+
// based on ACL permissions (which we mock to return true)
373+
request.bypassUserBucketPolicies = true;
374+
assert.ok(isBucketAuthorized(bucket, bucAction,
375+
bucketOwnerCanonicalId, user1AuthInfo, log, request));
376+
});
349377
});
350378

351379
describe('isObjAuthorized with no policy set', () => {
@@ -427,6 +455,35 @@ describe('bucket policy authorization', () => {
427455
assert.equal(allowed, false);
428456
done();
429457
});
458+
459+
it('should bypass bucket policy when request.bypassUserBucketPolicies is true', function () {
460+
// Create a request with the bypassUserBucketPolicies flag initially not set
461+
const request = {
462+
bucketName,
463+
socket: {},
464+
};
465+
466+
// Add a policy to explicitely deny access
467+
const newPolicy = this.test.basePolicy;
468+
newPolicy.Statement[0] = {
469+
Effect: 'Deny',
470+
Principal: { CanonicalUser: [bucketOwnerCanonicalId] },
471+
Resource: `arn:aws:s3:::${bucketName}`,
472+
Action: 's3:*',
473+
};
474+
bucket.setBucketPolicy(newPolicy);
475+
476+
// Check that the policy denies access, as expected
477+
assert.ok(!isObjAuthorized(bucket, object, 'objectGet',
478+
bucketOwnerCanonicalId, user1AuthInfo, log, request));
479+
480+
// But with bypassUserBucketPolicies set to true, it should still be authorized
481+
// based on ACL permissions (which we mock to return true)
482+
request.bypassUserBucketPolicies = true;
483+
assert.ok(isObjAuthorized(bucket, object, 'objectGet',
484+
bucketOwnerCanonicalId, user1AuthInfo, log, request));
485+
});
486+
430487
it('should deny access to non-object owner if two statements apply ' +
431488
'to principal but one denies access', function itFn(done) {
432489
const newPolicy = this.test.basePolicy;

tests/unit/server.js

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const assert = require('assert');
44
const sinon = require('sinon');
5+
const arsenal = require('arsenal');
56
const logger = require('../../lib/utilities/logger');
67
const { config: defaultConfig } = require('../../lib/Config');
78
const { S3Server } = require('../../lib/server');
@@ -83,7 +84,7 @@ describe('S3Server', () => {
8384
await waitReady();
8485

8586
assert.strictEqual(startServerStub.callCount, 2);
86-
assert(startServerStub.calledWith(server.routeRequest, 9000));
87+
assert(startServerStub.calledWith(server.internalRouteRequest, 9000));
8788
});
8889

8990
it('should start internal API servers from internalListenOn array', async () => {
@@ -98,8 +99,8 @@ describe('S3Server', () => {
9899
await waitReady();
99100

100101
assert.strictEqual(startServerStub.callCount, 3);
101-
assert(startServerStub.calledWith(server.routeRequest, 9000, '127.0.0.1'));
102-
assert(startServerStub.calledWith(server.routeRequest, 9001, '0.0.0.0'));
102+
assert(startServerStub.calledWith(server.internalRouteRequest, 9000, '127.0.0.1'));
103+
assert(startServerStub.calledWith(server.internalRouteRequest, 9001, '0.0.0.0'));
103104
assert(startServerStub.calledWith(server.routeAdminRequest));
104105
assert.strictEqual(startServerStub.neverCalledWith(server.routeRequest, 9999), true);
105106
});
@@ -143,8 +144,53 @@ describe('S3Server', () => {
143144

144145
assert.strictEqual(startServerStub.callCount, 3);
145146
assert(startServerStub.calledWith(server.routeRequest, 8000));
146-
assert(startServerStub.calledWith(server.routeRequest, 9000));
147+
assert(startServerStub.calledWith(server.internalRouteRequest, 9000));
147148
assert(startServerStub.calledWith(server.routeAdminRequest, 8002));
148149
});
149150
});
151+
152+
describe('internalRouteRequest', () => {
153+
const resp = {
154+
on: () => { },
155+
setHeader: () => { },
156+
writeHead: () => { },
157+
end: () => { },
158+
};
159+
160+
let req;
161+
162+
beforeEach(() => {
163+
req = {
164+
headers: {},
165+
socket: {
166+
setNoDelay: () => { },
167+
},
168+
url: 'http://localhost:8000',
169+
};
170+
});
171+
172+
afterEach(() => {
173+
sinon.restore();
174+
});
175+
176+
it('should bypass bucket policy for internal requests', () => {
177+
const routesMock = sinon.stub().callsFake(req => {
178+
assert(req.bypassUserBucketPolicies);
179+
});
180+
sinon.stub(arsenal.s3routes, 'routes').value(routesMock);
181+
182+
server.internalRouteRequest(req, resp);
183+
sinon.assert.calledOnce(routesMock);
184+
});
185+
186+
it('should bypass bucket policy for routes requests', () => {
187+
const routesMock = sinon.stub().callsFake(req => {
188+
assert(!req.bypassUserBucketPolicies);
189+
});
190+
sinon.stub(arsenal.s3routes, 'routes').value(routesMock);
191+
192+
server.routeRequest(req, resp);
193+
sinon.assert.calledOnce(routesMock);
194+
});
195+
});
150196
});

0 commit comments

Comments
 (0)