Skip to content
Merged
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
9 changes: 9 additions & 0 deletions src/api/account_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,15 @@ module.exports = {
},
nsfs_account_config: {
$ref: 'common_api#/definitions/nsfs_account_config'
},
iam_user_policies: {
type: 'array',
items: {
$ref: 'common_api#/definitions/iam_user_policy',
}
},
owner: {
type: 'string'
}
},
},
Expand Down
26 changes: 19 additions & 7 deletions src/endpoint/s3/s3_bucket_policy_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,28 @@ async function _is_object_version_fit(req, predicate, value) {
return res;
}

async function has_bucket_policy_permission(policy, account, method, arn_path, req, disallow_public_access = false) {
async function has_bucket_policy_permission(policy, account, method, arn_path, req,
{ disallow_public_access = false, should_pass_principal = true } = {}) {
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); // No need to disallow in "DENY"
deny_statements, account, method_arr, arn_path, req, {
disallow_public_access: false, // No need to disallow in "DENY"
should_pass_principal
}
);
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, disallow_public_access);
allow_statements, account, method_arr, arn_path, req, {
disallow_public_access,
should_pass_principal
});
if (res_arr_allow.every(item => item)) return 'ALLOW';

// implicit deny
Expand Down Expand Up @@ -217,15 +225,19 @@ 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, disallow_public_access = false) {
async function is_statement_fit_of_method_array(statements, account, method_arr, arn_path, req,
{ disallow_public_access = false, should_pass_principal = true } = {}) {
return Promise.all(method_arr.map(method_permission =>
_is_statements_fit(statements, account, method_permission, arn_path, req, disallow_public_access)));
_is_statements_fit(statements, account, method_permission, arn_path, req, { disallow_public_access, should_pass_principal })));
}

async function _is_statements_fit(statements, account, method, arn_path, req, disallow_public_access = false) {
async function _is_statements_fit(statements, account, method, arn_path, req,
{ disallow_public_access = false, should_pass_principal = true} = {}) {
for (const statement of statements) {
const action_fit = _is_action_fit(method, statement);
const principal_fit = _is_principal_fit(account, statement, disallow_public_access);
// When evaluating IAM user inline policies, should_pass_principal is false since these policies
// don't have a Principal field (the principal is implicitly the user)
const principal_fit = should_pass_principal ? _is_principal_fit(account, statement, disallow_public_access) : true;
const resource_fit = _is_resource_fit(arn_path, statement);
const condition_fit = await _is_condition_fit(statement, req, method);

Expand Down
52 changes: 48 additions & 4 deletions src/endpoint/s3/s3_rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ async function authorize_request(req) {
req.object_sdk.authorize_request_account(req),
// authorize_request_policy(req) is supposed to
// allow owners access unless there is an explicit DENY policy
authorize_request_policy(req)
authorize_request_policy(req),
// authorize_request_iam_policy(req) is for users only
authorize_request_iam_policy(req),
]);
}

Expand Down Expand Up @@ -299,14 +301,17 @@ async function authorize_request_policy(req) {
// we start the permission check on account identifier intentionally
if (account_identifier_id) {
permission_by_id = await s3_bucket_policy_utils.has_bucket_policy_permission(
s3_policy, account_identifier_id, method, arn_path, req, public_access_block?.restrict_public_buckets);
s3_policy, account_identifier_id, method, arn_path, req,
{ disallow_public_access: public_access_block?.restrict_public_buckets }
);
dbg.log3('authorize_request_policy: permission_by_id', permission_by_id);
}
if (permission_by_id === "DENY") throw new S3Error(S3Error.AccessDenied);

if ((!account_identifier_id || permission_by_id !== "DENY") && account.owner === undefined) {
permission_by_name = await s3_bucket_policy_utils.has_bucket_policy_permission(
s3_policy, account_identifier_name, method, arn_path, req, public_access_block?.restrict_public_buckets
s3_policy, account_identifier_name, method, arn_path, req,
{ disallow_public_access: public_access_block?.restrict_public_buckets }
);
dbg.log3('authorize_request_policy: permission_by_name', permission_by_name);
}
Expand All @@ -316,11 +321,50 @@ async function authorize_request_policy(req) {
throw new S3Error(S3Error.AccessDenied);
}

async function authorize_request_iam_policy(req) {
const auth_token = req.object_sdk.get_auth_token();
const is_anonymous = !(auth_token && auth_token.access_key);
if (is_anonymous) return;

const account = req.object_sdk.requesting_account;
const is_iam_user = account.owner !== undefined;
if (!is_iam_user) return; // IAM policy is only on IAM users (account root user is authorized here)

const resource_arn = _get_arn_from_req_path(req);
const method = _get_method_from_req(req);
const iam_policies = account.iam_user_policies || [];
if (iam_policies.length === 0) return;

// parallel policy check
const promises = [];
for (const iam_policy of iam_policies) {
// We are reusing the bucket policy util function as it checks the policy document
const promise = s3_bucket_policy_utils.has_bucket_policy_permission(
iam_policy.policy_document, undefined, method, resource_arn, req,
{ should_pass_principal: false }
);
promises.push(promise);
}
const permission_result = await Promise.all(promises);
let has_allow_permission = false;
for (const permission of permission_result) {
if (permission === "DENY") throw new S3Error(S3Error.AccessDenied);
if (permission === "ALLOW") {
has_allow_permission = true;
}
}
if (has_allow_permission) return;
dbg.log1('authorize_request_iam_policy: user have inline policies but none of them matched the method');
throw new S3Error(S3Error.AccessDenied);
}

async function authorize_anonymous_access(s3_policy, method, arn_path, req, public_access_block) {
if (!s3_policy) throw new S3Error(S3Error.AccessDenied);

const permission = await s3_bucket_policy_utils.has_bucket_policy_permission(
s3_policy, undefined, method, arn_path, req, public_access_block?.restrict_public_buckets);
s3_policy, undefined, method, arn_path, req,
{ disallow_public_access: public_access_block?.restrict_public_buckets }
);
if (permission === "ALLOW") return;

throw new S3Error(S3Error.AccessDenied);
Expand Down
4 changes: 2 additions & 2 deletions src/sdk/bucketspace_fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
action,
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
undefined,
bucket.public_access_block?.restrict_public_buckets,
{ disallow_public_access: bucket.public_access_block?.restrict_public_buckets }
);
if (permission_by_id === "DENY") return false;
// we (currently) allow account identified to be both id and name,
Expand All @@ -963,7 +963,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
action,
`arn:aws:s3:::${bucket.name.unwrap()}${bucket_path}`,
undefined,
bucket.public_access_block?.restrict_public_buckets,
{ disallow_public_access: bucket.public_access_block?.restrict_public_buckets }
);
}
if (permission_by_name === 'DENY') return false;
Expand Down
8 changes: 8 additions & 0 deletions src/server/system_services/account_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,14 @@ function get_account_info(account, include_connection_cache) {
};
info.role_config = account.role_config;
info.force_md5_etag = account.force_md5_etag;

if (account.iam_user_policies) {
info.iam_user_policies = account.iam_user_policies;
}
if (account.owner) {
info.owner = account.owner._id.toString();
}

return info;
}

Expand Down
Loading