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

PublicAccessBlock Support #8571

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
59 changes: 59 additions & 0 deletions src/api/bucket_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,65 @@ module.exports = {
system: ['admin', 'user']
}
},

get_public_access_block: {
method: 'GET',
params: {
type: 'object',
required: ['bucket_name'],
properties: {
bucket_name: {
$ref: 'common_api#/definitions/bucket_name'
}
}
},
reply: {
type: 'object',
properties: {
public_access_block: {
$ref: 'common_api#/definitions/public_access_block'
}
}
},
auth: {
system: ['admin', 'user']
}
},

put_public_access_block: {
method: 'PUT',
params: {
type: 'object',
required: ['bucket_name', 'public_access_block'],
properties: {
bucket_name: {
$ref: 'common_api#/definitions/bucket_name'
},
public_access_block: {
$ref: 'common_api#/definitions/public_access_block'
},
},
},
auth: {
system: ['admin', 'user']
}
},

delete_public_access_block: {
method: 'DELETE',
params: {
type: 'object',
required: ['bucket_name'],
properties: {
bucket_name: {
$ref: 'common_api#/definitions/bucket_name'
},
},
},
auth: {
system: ['admin', 'user']
}
},
},

definitions: {
Expand Down
9 changes: 9 additions & 0 deletions src/api/common_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,15 @@ module.exports = {
}
}
}
},
public_access_block: {
type: 'object',
properties: {
block_public_acls: { type: 'boolean' },
ignore_public_acls: { type: 'boolean' },
block_public_policy: { type: 'boolean' },
restrict_public_buckets: { type: 'boolean' },
},
}
}
};
3 changes: 3 additions & 0 deletions src/endpoint/s3/ops/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports.delete_bucket_policy = require('./s3_delete_bucket_policy');
exports.delete_bucket_replication = require('./s3_delete_bucket_replication');
exports.delete_bucket_tagging = require('./s3_delete_bucket_tagging');
exports.delete_bucket_website = require('./s3_delete_bucket_website');
exports.delete_bucket_public_access_block = require('./s3_delete_bucket_public_access_block');
exports.delete_object = require('./s3_delete_object');
exports.delete_object_tagging = require('./s3_delete_object_tagging');
exports.delete_object_uploadId = require('./s3_delete_object_uploadId');
Expand All @@ -37,6 +38,7 @@ exports.get_bucket_uploads = require('./s3_get_bucket_uploads');
exports.get_bucket_versioning = require('./s3_get_bucket_versioning');
exports.get_bucket_versions = require('./s3_get_bucket_versions');
exports.get_bucket_website = require('./s3_get_bucket_website');
exports.get_bucket_public_access_block = require('./s3_get_bucket_public_access_block');
exports.get_object = require('./s3_get_object');
exports.get_object_acl = require('./s3_get_object_acl');
exports.get_object_attributes = require('./s3_get_object_attributes');
Expand Down Expand Up @@ -71,6 +73,7 @@ exports.put_bucket_requestPayment = require('./s3_put_bucket_requestPayment');
exports.put_bucket_tagging = require('./s3_put_bucket_tagging');
exports.put_bucket_versioning = require('./s3_put_bucket_versioning');
exports.put_bucket_website = require('./s3_put_bucket_website');
exports.put_bucket_public_access_block = require('./s3_put_bucket_public_access_block');
exports.put_object = require('./s3_put_object');
exports.put_object_acl = require('./s3_put_object_acl');
exports.put_object_legal_hold = require('./s3_put_object_legal_hold');
Expand Down
20 changes: 20 additions & 0 deletions src/endpoint/s3/ops/s3_delete_bucket_public_access_block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Copyright (C) 2024 NooBaa */
'use strict';

/**
* https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeletePublicAccessBlock.html
*/
async function delete_public_access_block(req) {
await req.object_sdk.delete_public_access_block({ name: req.params.bucket });
}

module.exports = {
handler: delete_public_access_block,
body: {
type: 'empty',
},
reply: {
type: 'empty',
},
};

39 changes: 39 additions & 0 deletions src/endpoint/s3/ops/s3_get_bucket_public_access_block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* Copyright (C) 2024 NooBaa */
'use strict';

/**
* https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetPublicAccessBlock.html
*/
async function get_public_access_block(req) {
const reply = await req.object_sdk.get_public_access_block({ name: req.params.bucket });
if (!reply.public_access_block) {
return {
PublicAccessBlockConfiguration: {
BlockPublicAcls: false,
IgnorePublicAcls: false,
BlockPublicPolicy: false,
RestrictPublicBuckets: false,
}
};
}

return {
PublicAccessBlockConfiguration: {
BlockPublicAcls: Boolean(reply.public_access_block.block_public_acls),
IgnorePublicAcls: Boolean(reply.public_access_block.ignore_public_acls),
BlockPublicPolicy: Boolean(reply.public_access_block.block_public_policy),
RestrictPublicBuckets: Boolean(reply.public_access_block.restrict_public_buckets),
}
};
}

module.exports = {
handler: get_public_access_block,
body: {
type: 'empty'
},
reply: {
type: 'xml',
},
};

25 changes: 25 additions & 0 deletions src/endpoint/s3/ops/s3_put_bucket_public_access_block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* Copyright (C) 2024 NooBaa */
'use strict';

const s3_utils = require('../s3_utils');

/**
* https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutPublicAccessBlock.html
* @param {*} req
* @param {*} res
*/
async function put_public_access_block(req, res) {
const public_access_block = s3_utils.parse_body_public_access_block(req);
await req.object_sdk.put_public_access_block({ name: req.params.bucket, public_access_block });
}

module.exports = {
handler: put_public_access_block,
body: {
type: 'xml',
},
reply: {
type: 'empty',
},
};

54 changes: 46 additions & 8 deletions src/endpoint/s3/s3_bucket_policy_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const OP_NAME_TO_ACTION = Object.freeze({
delete_bucket_replication: { regular: "s3:PutReplicationConfiguration" },
delete_bucket_tagging: { regular: "s3:PutBucketTagging" },
delete_bucket_website: { regular: "s3:DeleteBucketWebsite" },
delete_bucket_public_access_block: { regular: "s3:PutBucketPublicAccessBlock" },
delete_bucket: { regular: "s3:DeleteBucket" },
delete_object_tagging: { regular: "s3:DeleteObjectTagging", versioned: "s3:DeleteObjectVersionTagging" },
delete_object_uploadId: { regular: "s3:AbortMultipartUpload" },
Expand All @@ -43,6 +44,7 @@ const OP_NAME_TO_ACTION = Object.freeze({
get_bucket_versions: { regular: "s3:ListBucketVersions" },
get_bucket_website: { regular: "s3:GetBucketWebsite" },
get_bucket_object_lock: { regular: "s3:GetBucketObjectLockConfiguration" },
get_bucket_public_access_block: { regular: "s3:GetBucketPublicAccessBlock" },
get_bucket: { regular: "s3:ListBucket" },
get_object_acl: { regular: "s3:GetObjectAcl" },
get_object_attributes: { regular: ["s3:GetObject", "s3:GetObjectAttributes"], versioned: ["s3:GetObjectVersion", "s3:GetObjectVersionAttributes"] }, // Notice - special case
Expand Down Expand Up @@ -80,6 +82,7 @@ const OP_NAME_TO_ACTION = Object.freeze({
put_bucket_versioning: { regular: "s3:PutBucketVersioning" },
put_bucket_website: { regular: "s3:PutBucketWebsite" },
put_bucket_object_lock: { regular: "s3:PutBucketObjectLockConfiguration" },
put_bucket_public_access_block: { regular: "s3:PutBucketPublicAccessBlock" },
put_bucket: { regular: "s3:CreateBucket" },
put_object_acl: { regular: "s3:PutObjectAcl" },
put_object_tagging: { regular: "s3:PutObjectTagging", versioned: "s3:PutObjectVersionTagging" },
Expand Down Expand Up @@ -137,18 +140,20 @@ async function _is_object_tag_fit(req, predicate, value) {
return res;
}

async function has_bucket_policy_permission(policy, account, method, arn_path, req) {
async function has_bucket_policy_permission(policy, account, method, arn_path, req, disallow_public_access = false) {
const [allow_statements, deny_statements] = _.partition(policy.Statement, statement => statement.Effect === 'Allow');

// the case where the permission is an array started in op get_object_attributes
const method_arr = Array.isArray(method) ? method : [method];

// look for explicit denies
const res_arr_deny = await is_statement_fit_of_method_array(deny_statements, account, method_arr, arn_path, req);
const res_arr_deny = await is_statement_fit_of_method_array(
deny_statements, account, method_arr, arn_path, req, disallow_public_access);
if (res_arr_deny.every(item => item)) return 'DENY';

// look for explicit allows
const res_arr_allow = await is_statement_fit_of_method_array(allow_statements, account, method_arr, arn_path, req);
const res_arr_allow = await is_statement_fit_of_method_array(
allow_statements, account, method_arr, arn_path, req, disallow_public_access);
if (res_arr_allow.every(item => item)) return 'ALLOW';

// implicit deny
Expand All @@ -168,14 +173,19 @@ function _is_action_fit(method, statement) {
return statement.Action ? action_fit : !action_fit;
}

function _is_principal_fit(account, statement) {
function _is_principal_fit(account, statement, ignore_anon_principal = false) {
let statement_principal = statement.Principal || statement.NotPrincipal;

let principal_fit = false;
statement_principal = statement_principal.AWS ? statement_principal.AWS : statement_principal;
for (const principal of _.flatten([statement_principal])) {
dbg.log1('bucket_policy: ', statement.Principal ? 'Principal' : 'NotPrincipal', ' fit?', principal, account);
if ((principal.unwrap() === '*') || (principal.unwrap() === account)) {
if (ignore_anon_principal && principal.unwrap() === '*' && statement.Principal) {
// Ignore the "fit" if ignore_anon_principal is requested
continue;
}

principal_fit = true;
break;
}
Expand All @@ -198,15 +208,15 @@ function _is_resource_fit(arn_path, statement) {
return statement.Resource ? resource_fit : !resource_fit;
}

async function is_statement_fit_of_method_array(statements, account, method_arr, arn_path, req) {
async function is_statement_fit_of_method_array(statements, account, method_arr, arn_path, req, disallow_public_access = false) {
return Promise.all(method_arr.map(method_permission =>
_is_statements_fit(statements, account, method_permission, arn_path, req)));
_is_statements_fit(statements, account, method_permission, arn_path, req, disallow_public_access)));
}

async function _is_statements_fit(statements, account, method, arn_path, req) {
async function _is_statements_fit(statements, account, method, arn_path, req, disallow_public_access = false) {
for (const statement of statements) {
const action_fit = _is_action_fit(method, statement);
const principal_fit = _is_principal_fit(account, statement);
const principal_fit = _is_principal_fit(account, statement, disallow_public_access);
const resource_fit = _is_resource_fit(arn_path, statement);
const condition_fit = await _is_condition_fit(statement, req, method);

Expand Down Expand Up @@ -295,6 +305,34 @@ async function validate_s3_policy(policy, bucket_name, get_account_handler) {
}
}

/**
* allows_public_access returns true if a policy will allow public access
* to a resource
*
* NOTE: It assumes that the given policy has already been validated
* @param {*} policy
* @returns {boolean}
*/
function allows_public_access(policy) {
for (const statement of policy.Statement) {
if (statement.Effect === 'Deny') continue;

const statement_principal = statement.Principal;
if (statement_principal.AWS) {
for (const principal of _.flatten([statement_principal.AWS])) {
if (typeof principal === 'string' ? principal === '*' : principal.unwrap() === '*') {
return true;
}
}
} else if (typeof statement_principal === 'string' ? statement_principal === '*' : statement_principal.unwrap() === '*') {
return true;
}
}

return false;
}

exports.OP_NAME_TO_ACTION = OP_NAME_TO_ACTION;
exports.has_bucket_policy_permission = has_bucket_policy_permission;
exports.validate_s3_policy = validate_s3_policy;
exports.allows_public_access = allows_public_access;
10 changes: 7 additions & 3 deletions src/endpoint/s3/s3_rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const BUCKET_SUB_RESOURCES = Object.freeze({
'encryption': 'encryption',
'object-lock': 'object_lock',
'legal-hold': 'legal_hold',
'retention': 'retention'
'retention': 'retention',
'publicAccessBlock': 'public_access_block'
});

const OBJECT_SUB_RESOURCES = Object.freeze({
Expand Down Expand Up @@ -277,19 +278,22 @@ async function authorize_request_policy(req) {
if (is_owner || is_iam_account_and_same_root_account_owner) return;
throw new S3Error(S3Error.AccessDenied);
}

const public_access_block_cfg = await req.object_sdk.get_public_access_block({ name: req.params.bucket });
let permission;
// In NC, we allow principal to be:
// 1. account name (for backwards compatibility)
// 2. account id
// we start the permission check on account identifier intentionally
if (account_identifier_id) {
permission = await s3_bucket_policy_utils.has_bucket_policy_permission(
s3_policy, account_identifier_id, method, arn_path, req);
s3_policy, account_identifier_id, method, arn_path, req, public_access_block_cfg?.public_access_block?.restrict_public_buckets);
}

if ((!account_identifier_id || permission === "IMPLICIT_DENY") && account.owner === undefined) {
permission = await s3_bucket_policy_utils.has_bucket_policy_permission(
s3_policy, account_identifier_name, method, arn_path, req);
s3_policy, account_identifier_name, method, arn_path, req, public_access_block_cfg?.public_access_block?.restrict_public_buckets
);
}

if (permission === "DENY") throw new S3Error(S3Error.AccessDenied);
Expand Down
20 changes: 20 additions & 0 deletions src/endpoint/s3/s3_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,25 @@ function key_marker_to_cont_tok(key_marker, objects_arr, is_truncated) {
return Buffer.from(j).toString('base64');
}

function parse_body_public_access_block(req) {
const parsed = {};

const access_cfg = req.body.PublicAccessBlockConfiguration;
if (!access_cfg) throw new S3Error(S3Error.MalformedXML);

if (access_cfg.BlockPublicAcls || access_cfg.IgnorePublicAcls) {
throw new S3Error(S3Error.AccessControlListNotSupported);
}
if (access_cfg.BlockPublicPolicy) {
parsed.block_public_policy = access_cfg.BlockPublicPolicy?.[0].toLowerCase?.() === 'true';
}
if (access_cfg.RestrictPublicBuckets) {
parsed.restrict_public_buckets = access_cfg.RestrictPublicBuckets?.[0].toLowerCase?.() === 'true';
}

return parsed;
}

exports.STORAGE_CLASS_STANDARD = STORAGE_CLASS_STANDARD;
exports.STORAGE_CLASS_GLACIER = STORAGE_CLASS_GLACIER;
exports.STORAGE_CLASS_GLACIER_IR = STORAGE_CLASS_GLACIER_IR;
Expand Down Expand Up @@ -828,5 +847,6 @@ exports.set_response_supported_storage_classes = set_response_supported_storage_
exports.cont_tok_to_key_marker = cont_tok_to_key_marker;
exports.key_marker_to_cont_tok = key_marker_to_cont_tok;
exports.parse_sse_c = parse_sse_c;
exports.parse_body_public_access_block = parse_body_public_access_block;
exports.OBJECT_ATTRIBUTES = OBJECT_ATTRIBUTES;
exports.OBJECT_ATTRIBUTES_UNSUPPORTED = OBJECT_ATTRIBUTES_UNSUPPORTED;
Loading
Loading