Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standard authorization for backbeat routes #5714

Open
wants to merge 10 commits into
base: development/8.8
Choose a base branch
from
153 changes: 153 additions & 0 deletions docs/BACKBEAT_ROUTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Backbeat routes

Backbeat routes are implemented in `lib/routes/routeBackbeat.js`.

This special router is responsible for handling all the requests that are
related to the Backbeat service. Backbeat may call any of the below APIs to
perform operations on either data or s3 objects (metadata).

These routes follow the same authorization and validation as the S3 routes:

- Authorize the request with support for Implicit Denies from the IAM service.
- Retrieve the bucket and object metadata if applicable.
- Evaluate the S3 Bucket Policies and ACLs before authorizing the request.
- Backbeat routes are only authorized given the right permission, currently,
`objectReplicate` as a unique permission for all these special routes.
- In order to be authorized without S3 Bucket Policy, the caller must be
authorized by the IAM service and the ACLs. Service accounts and accounts
are allowed.
- Finally, evaluate the quotas before allowing the request to proceed.

## List of supported APIs

```plaintext
PUT /_/backbeat/metadata/<bucket name>/<object key>
```

To edit one existing S3 Object's metadata.
williamlardier marked this conversation as resolved.
Show resolved Hide resolved
In the CRR case, this is used to put metadata for new objects.

```plaintext
GET /_/backbeat/metadata/<bucket name>/<object key>?versionId=<version id>
```

To get one existing S3 Object's metadata. Version id can be specified to get
the metadata of a specific version.

```plaintext
PUT /_/backbeat/data/<bucket name>/<object key>
```

To put directly to the storage layer the data for an existing S3 Object.

```plaintext
PUT /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=putobject
```

To put directly to the storage layer the data for an existing S3 Object.
Use case: Zenko Replication.

```plaintext
PUT /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=putpart
```

To put directly to the storage layer the data for an existing S3 Object part.
Use case: Zenko Replication.

```plaintext
DELETE /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=deleteobject
```

To delete the data for an existing S3 Object.
Use case: Zenko Replication.

```plaintext
DELETE /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=abortmpu
```

To abort a multipart upload.
Use case: Zenko Replication.

```plaintext
DELETE /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=deleteobjecttagging
```

To delete the tagging for an existing S3 Object.
Use case: Zenko Replication.

```plaintext
POST /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=initiatempu
```

To initiate a multipart upload.
Use case: Zenko Replication.

```plaintext
POST /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=completempu
```

To complete a multipart upload.
Use case: Zenko Replication.

```plaintext
POST /_/backbeat/multiplebackenddata/<bucket name>/<object key>?operation=puttagging
```

To put the tagging for an existing S3 Object.
Use case: Zenko Replication.

```plaintext
GET /_/backbeat/multiplebackendmetadata/<bucket name>/<object key>
```

To get the metadata for an existing S3 Object. Similar to a S3 HeadObject.
Use case: Cross Region Replication (CRR).

```plaintext
POST /_/backbeat/batchdelete
```

Delete a batch of objects froem the storage layer.
Use case: restored S3 Object expiration.

```plaintext
GET /_/backbeat/lifecycle/<bucket name>?list-type=current
```

To list current S3 Object versions from an S3 Bucket.
Use case: lifecycle listings.

```plaintext
GET /_/backbeat/lifecycle/<bucket name>?list-type=noncurrent
```

To list noncurrent S3 Object versions from an S3 Bucket.
Use case: lifecycle listings.

```plaintext
GET /_/backbeat/lifecycle/<bucket name>?list-type=orphan
```

To list delete markers from an S3 Bucket.
Use case: lifecycle listings.

```plaintext
POST /_/backbeat/index/<bucket name>?operation=add
```

To create an index for a bucket.
Use case: MongoDB backend.

```plaintext
POST /_/backbeat/index/<bucket name>?operation=delete
```

To delete an index for a bucket.
Use case: MongoDB backend.

```plaintext
GET /_/backbeat/index/<bucket name>
```

To get the index for a bucket.
Use case: MongoDB backend.
129 changes: 69 additions & 60 deletions lib/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,66 @@ const monitoringMap = policies.actionMaps.actionMonitoringMapS3;

auth.setHandler(vault);

function checkAuthResults(authResults, apiMethod, log) {
let returnTagCount = true;
const isImplicitDeny = {};
let isOnlyImplicitDeny = true;
if (apiMethod === 'objectGet') {
if (!authResults[0].isAllowed && !authResults[0].isImplicit) {
log.trace('get object authorization denial from Vault');
return errors.AccessDenied;
}
isImplicitDeny[authResults[0].action] = authResults[0].isImplicit;
if (!authResults[1].isAllowed) {
log.trace('get tagging authorization denial ' +
'from Vault');
returnTagCount = false;
}
} else {
for (let i = 0; i < authResults.length; i++) {
isImplicitDeny[authResults[i].action] = true;
if (!authResults[i].isAllowed && !authResults[i].isImplicit) {
// Any explicit deny rejects the current API call
log.trace('authorization denial from Vault');
return errors.AccessDenied;
}
if (authResults[i].isAllowed) {
// If the action is allowed, the result is not implicit
// Deny.
isImplicitDeny[authResults[i].action] = false;
isOnlyImplicitDeny = false;
}
}
}
// These two APIs cannot use ACLs or Bucket Policies, hence, any
// implicit deny from vault must be treated as an explicit deny.
if ((apiMethod === 'bucketPut' || apiMethod === 'serviceGet') && isOnlyImplicitDeny) {
return errors.AccessDenied;
}
return { returnTagCount, isImplicitDeny };
}

/* eslint-disable no-param-reassign */
function handleAuthorizationResults(request, authorizationResults, apiMethod, returnTagCount, log, callback) {
if (authorizationResults) {
const checkedResults = checkAuthResults(authorizationResults, apiMethod, log);
if (checkedResults instanceof Error) {
return callback(checkedResults);
}
returnTagCount = checkedResults.returnTagCount;
request.actionImplicitDenies = checkedResults.isImplicitDeny;
} else {
// create an object of keys apiMethods with all values to false:
// for backward compatibility, all apiMethods are allowed by default
// thus it is explicitly allowed, so implicit deny is false
request.actionImplicitDenies = request.apiMethods.reduce((acc, curr) => {
acc[curr] = false;
return acc;
}, {});
}
return callback();
}

const api = {
callApiMethod(apiMethod, request, response, log, callback) {
// Attach the apiMethod method to the request, so it can used by monitoring in the server
Expand Down Expand Up @@ -109,7 +168,7 @@ const api = {
objectKey: request.objectKey,
});
}
let returnTagCount = true;
const returnTagCount = true;

const validationRes = validateQueryAndHeaders(request, log);
if (validationRes.error) {
Expand Down Expand Up @@ -152,49 +211,6 @@ const api = {
// eslint-disable-next-line no-param-reassign
request.apiMethods = apiMethods;

function checkAuthResults(authResults) {
let returnTagCount = true;
const isImplicitDeny = {};
let isOnlyImplicitDeny = true;
if (apiMethod === 'objectGet') {
// first item checks s3:GetObject(Version) action
if (!authResults[0].isAllowed && !authResults[0].isImplicit) {
log.trace('get object authorization denial from Vault');
return errors.AccessDenied;
}
// TODO add support for returnTagCount in the bucket policy
// checks
isImplicitDeny[authResults[0].action] = authResults[0].isImplicit;
// second item checks s3:GetObject(Version)Tagging action
if (!authResults[1].isAllowed) {
log.trace('get tagging authorization denial ' +
'from Vault');
returnTagCount = false;
}
} else {
for (let i = 0; i < authResults.length; i++) {
isImplicitDeny[authResults[i].action] = true;
if (!authResults[i].isAllowed && !authResults[i].isImplicit) {
// Any explicit deny rejects the current API call
log.trace('authorization denial from Vault');
return errors.AccessDenied;
}
if (authResults[i].isAllowed) {
// If the action is allowed, the result is not implicit
// Deny.
isImplicitDeny[authResults[i].action] = false;
isOnlyImplicitDeny = false;
}
}
}
// These two APIs cannot use ACLs or Bucket Policies, hence, any
// implicit deny from vault must be treated as an explicit deny.
if ((apiMethod === 'bucketPut' || apiMethod === 'serviceGet') && isOnlyImplicitDeny) {
return errors.AccessDenied;
}
return { returnTagCount, isImplicitDeny };
}

return async.waterfall([
next => auth.server.doAuth(
request, log, (err, userInfo, authorizationResults, streamingV4Params, infos) => {
Expand Down Expand Up @@ -267,27 +283,18 @@ const api = {
return next(null, userInfo, authResultsWithTags, streamingV4Params, infos);
},
),
(userInfo, authorizationResults, streamingV4Params, infos, next) =>
handleAuthorizationResults(request, authorizationResults, apiMethod, returnTagCount, log, err => {
if (err) {
return next(err);
}
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
}),
], (err, userInfo, authorizationResults, streamingV4Params, infos) => {
if (err) {
return callback(err);
}
request.accountQuotas = infos?.accountQuota;
if (authorizationResults) {
const checkedResults = checkAuthResults(authorizationResults);
if (checkedResults instanceof Error) {
return callback(checkedResults);
}
returnTagCount = checkedResults.returnTagCount;
request.actionImplicitDenies = checkedResults.isImplicitDeny;
} else {
// create an object of keys apiMethods with all values to false:
// for backward compatibility, all apiMethods are allowed by default
// thus it is explicitly allowed, so implicit deny is false
request.actionImplicitDenies = apiMethods.reduce((acc, curr) => {
acc[curr] = false;
return acc;
}, {});
}
const methodCallback = (err, ...results) => async.forEachLimit(request.finalizerHooks, 5,
(hook, done) => hook(err, done),
() => callback(err, ...results));
Expand Down Expand Up @@ -372,6 +379,8 @@ const api = {
serviceGet,
websiteGet: website,
websiteHead: website,
checkAuthResults,
handleAuthorizationResults,
};

module.exports = api;
Loading
Loading