Compare commits

...

2 Commits

6 changed files with 1019 additions and 253 deletions

View File

@ -467,6 +467,10 @@
"code": 400, "code": 400,
"description": "The request was rejected because an invalid or out-of-range value was supplied for an input parameter." "description": "The request was rejected because an invalid or out-of-range value was supplied for an input parameter."
}, },
"MalformedPolicy": {
"code": 400,
"description": "Policies must be valid JSON and the first byte must be '{'"
},
"_comment": "-------------- Special non-AWS S3 errors --------------", "_comment": "-------------- Special non-AWS S3 errors --------------",
"MPUinProgress": { "MPUinProgress": {
"code": 409, "code": 409,

View File

@ -2,11 +2,13 @@
const Ajv = require('ajv'); const Ajv = require('ajv');
const userPolicySchema = require('./userPolicySchema'); const userPolicySchema = require('./userPolicySchema');
const resourcePolicySchema = require('./resourcePolicySchema');
const errors = require('../errors'); const errors = require('../errors');
const ajValidate = new Ajv({ allErrors: true }); const ajValidate = new Ajv({ allErrors: true });
// compiles schema to functions and caches them for all cases // compiles schema to functions and caches them for all cases
const userPolicyValidate = ajValidate.compile(userPolicySchema); const userPolicyValidate = ajValidate.compile(userPolicySchema);
const resourcePolicyValidate = ajValidate.compile(resourcePolicySchema);
const errDict = { const errDict = {
required: { required: {
@ -24,33 +26,39 @@ const errDict = {
}; };
// parse ajv errors and return early with the first relevant error // parse ajv errors and return early with the first relevant error
function _parseErrors(ajvErrors) { function _parseErrors(ajvErrors, policyType) {
// deep copy is needed as we have to assign custom error description let parsedErr;
const parsedErr = Object.assign({}, errors.MalformedPolicyDocument); if (policyType === 'user') {
parsedErr.description = 'Syntax errors in policy.'; // deep copy is needed as we have to assign custom error description
parsedErr = Object.assign({}, errors.MalformedPolicyDocument);
parsedErr.description = 'Syntax errors in policy.';
}
if (policyType === 'resource') {
parsedErr = Object.assign({}, errors.MalformedPolicy);
}
ajvErrors.some(err => { ajvErrors.some(err => {
const resource = err.dataPath; const resource = err.dataPath;
const field = err.params ? err.params.missingProperty : undefined; const field = err.params ? err.params.missingProperty : undefined;
const errType = err.keyword; const errType = err.keyword;
if (errType === 'type' && (resource === '.Statement' || if (errType === 'type' && (resource === '.Statement' ||
resource === '.Statement.Resource' || resource.includes('.Resource') ||
resource === '.Statement.NotResource')) { resource.includes('.NotResource'))) {
// skip this as this doesn't have enough error context // skip this as this doesn't have enough error context
return false; return false;
} }
if (err.keyword === 'required' && field && errDict.required[field]) { if (err.keyword === 'required' && field && errDict.required[field]) {
parsedErr.description = errDict.required[field]; parsedErr.description = errDict.required[field];
} else if (err.keyword === 'pattern' && } else if (err.keyword === 'pattern' &&
(resource === '.Statement.Action' || (resource.includes('.Action') ||
resource === '.Statement.NotAction')) { resource.includes('.NotAction'))) {
parsedErr.description = errDict.pattern.Action; parsedErr.description = errDict.pattern.Action;
} else if (err.keyword === 'pattern' && } else if (err.keyword === 'pattern' &&
(resource === '.Statement.Resource' || (resource.includes('.Resource') ||
resource === '.Statement.NotResource')) { resource.includes('.NotResource'))) {
parsedErr.description = errDict.pattern.Resource; parsedErr.description = errDict.pattern.Resource;
} else if (err.keyword === 'minItems' && } else if (err.keyword === 'minItems' &&
(resource === '.Statement.Resource' || (resource.includes('.Resource') ||
resource === '.Statement.NotResource')) { resource.includes('.NotResource'))) {
parsedErr.description = errDict.minItems.Resource; parsedErr.description = errDict.minItems.Resource;
} }
return true; return true;
@ -77,12 +85,24 @@ function _validatePolicy(type, policy) {
} }
userPolicyValidate(parseRes); userPolicyValidate(parseRes);
if (userPolicyValidate.errors) { if (userPolicyValidate.errors) {
return { error: _parseErrors(userPolicyValidate.errors), return { error: _parseErrors(userPolicyValidate.errors, 'user'),
valid: false }; valid: false };
} }
return { error: null, valid: true }; return { error: null, valid: true };
} }
// TODO: add support for resource policies if (type === 'resource') {
const parseRes = _safeJSONParse(policy);
if (parseRes instanceof Error) {
return { error: Object.assign({}, errors.MalformedPolicy),
valid: false };
}
resourcePolicyValidate(parseRes);
if (resourcePolicyValidate.errors) {
return { error: _parseErrors(resourcePolicyValidate.errors,
'resource'), valid: false };
}
return { error: null, valid: true };
}
return { error: errors.NotImplemented, valid: false }; return { error: errors.NotImplemented, valid: false };
} }
/** /**

View File

@ -0,0 +1,477 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "AWS Bucket Policy schema.",
"description": "This schema describes a bucket policy per AWS policy grammar rules",
"definitions": {
"principalService": {
"type": "object",
"properties": {
"Service": {
"type": "string",
"enum": [
"backbeat"
]
}
},
"additionalProperties": false
},
"principalCanonicalUser": {
"type": "object",
"properties": {
"CanonicalUser": {
"type": "string",
"pattern": "^[0-9a-z]{64}$"
}
},
"additionalProperties": false
},
"principalAnonymous": {
"type": "string",
"pattern": "^\\*$"
},
"principalAWSAccountID": {
"type": "string",
"pattern": "^[0-9]{12}$"
},
"principalAWSAccountArn": {
"type": "string",
"pattern": "^arn:aws:iam::[0-9]{12}:root$"
},
"principalAWSUserArn": {
"type": "string",
"pattern": "^arn:aws:iam::[0-9]{12}:user/[\\w+=,.@ -]{1,64}$"
},
"principalAWSRoleArn": {
"type": "string",
"pattern": "^arn:aws:iam::[0-9]{12}:role/[\\w+=,.@ -]{1,64}$"
},
"principalAWSItem": {
"type": "object",
"properties": {
"AWS": {
"oneOf": [
{ "$ref": "#/definitions/principalAWSAccountID" },
{ "$ref": "#/definitions/principalAnonymous" },
{ "$ref": "#/definitions/principalAWSAccountArn" },
{ "$ref": "#/definitions/principalAWSUserArn" },
{ "$ref": "#/definitions/principalAWSRoleArn" },
{
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/principalAWSAccountID"
}
},
{
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/principalAWSAccountArn"
}
},
{
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/principalAWSRoleArn"
}
},
{
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/principalAWSUserArn"
}
}
]
}
},
"additionalProperties": false
},
"principalItem": {
"oneOf": [
{ "$ref": "#/definitions/principalAWSItem" },
{ "$ref": "#/definitions/principalAnonymous" },
{ "$ref": "#/definitions/principalService" },
{ "$ref": "#/definitions/principalCanonicalUser" }
]
},
"actionItem": {
"type": "string",
"pattern": "^[^*:]+:([^:])+|^\\*$"
},
"resourceItem": {
"type": "string",
"pattern": "^\\*|arn:(aws|scality)(:(\\*{1}|[a-z0-9\\*\\-]{2,})*?){3}:((?!\\$\\{\\}).)*?$"
},
"conditionKeys" : {
"properties": {
"aws:CurrentTime": {},
"aws:EpochTime": {},
"aws:MultiFactorAuthAge": {},
"aws:MultiFactorAuthPresent": {},
"aws:PrincipalArn": {},
"aws:PrincipalOrgId": {},
"aws:PrincipalTag/${TagKey}": {},
"aws:PrincipalType": {},
"aws:Referer": {},
"aws:RequestTag/${TagKey}": {},
"aws:RequestedRegion": {},
"aws:SecureTransport": {},
"aws:SourceAccount": {},
"aws:SourceArn": {},
"aws:SourceIp": {},
"aws:SourceVpc": {},
"aws:SourceVpce": {},
"aws:TagKeys": {},
"aws:TokenIssueTime": {},
"aws:UserAgent": {},
"aws:userid": {},
"aws:username": {},
"s3:ExistingJobOperation": {},
"s3:ExistingJobPriority": {},
"s3:ExistingObjectTag/<key>": {},
"s3:JobSuspendedCause": {},
"s3:LocationConstraint": {},
"s3:RequestJobOperation": {},
"s3:RequestJobPriority": {},
"s3:RequestObjectTag/<key>": {},
"s3:RequestObjectTagKeys": {},
"s3:VersionId": {},
"s3:authtype": {},
"s3:delimiter": {},
"s3:locationconstraint": {},
"s3:max-keys": {},
"s3:object-lock-legal-hold": {},
"s3:object-lock-mode": {},
"s3:object-lock-remaining-retention-days": {},
"s3:object-lock-retain-until-date": {},
"s3:prefix": {},
"s3:signatureage": {},
"s3:signatureversion": {},
"s3:versionid": {},
"s3:x-amz-acl": {},
"s3:x-amz-content-sha256": {},
"s3:x-amz-copy-source": {},
"s3:x-amz-grant-full-control": {},
"s3:x-amz-grant-read": {},
"s3:x-amz-grant-read-acp": {},
"s3:x-amz-grant-write": {},
"s3:x-amz-grant-write-acp": {},
"s3:x-amz-metadata-directive": {},
"s3:x-amz-server-side-encryption": {},
"s3:x-amz-server-side-encryption-aws-kms-key-id": {},
"s3:x-amz-storage-class": {},
"s3:x-amz-website-redirect-location": {}
},
"additionalProperties": false
},
"conditions": {
"type": "object",
"properties": {
"ArnEquals": {
"type": "object"
},
"ArnEqualsIfExists": {
"type": "object"
},
"ArnLike": {
"type": "object"
},
"ArnLikeIfExists": {
"type": "object"
},
"ArnNotEquals": {
"type": "object"
},
"ArnNotEqualsIfExists": {
"type": "object"
},
"ArnNotLike": {
"type": "object"
},
"ArnNotLikeIfExists": {
"type": "object"
},
"BinaryEquals": {
"type": "object"
},
"BinaryEqualsIfExists": {
"type": "object"
},
"BinaryNotEquals": {
"type": "object"
},
"BinaryNotEqualsIfExists": {
"type": "object"
},
"Bool": {
"type": "object"
},
"BoolIfExists": {
"type": "object"
},
"DateEquals": {
"type": "object"
},
"DateEqualsIfExists": {
"type": "object"
},
"DateGreaterThan": {
"type": "object"
},
"DateGreaterThanEquals": {
"type": "object"
},
"DateGreaterThanEqualsIfExists": {
"type": "object"
},
"DateGreaterThanIfExists": {
"type": "object"
},
"DateLessThan": {
"type": "object"
},
"DateLessThanEquals": {
"type": "object"
},
"DateLessThanEqualsIfExists": {
"type": "object"
},
"DateLessThanIfExists": {
"type": "object"
},
"DateNotEquals": {
"type": "object"
},
"DateNotEqualsIfExists": {
"type": "object"
},
"IpAddress": {
"type": "object"
},
"IpAddressIfExists": {
"type": "object"
},
"NotIpAddress": {
"type": "object"
},
"NotIpAddressIfExists": {
"type": "object"
},
"Null": {
"type": "object"
},
"NumericEquals": {
"type": "object"
},
"NumericEqualsIfExists": {
"type": "object"
},
"NumericGreaterThan": {
"type": "object"
},
"NumericGreaterThanEquals": {
"type": "object"
},
"NumericGreaterThanEqualsIfExists": {
"type": "object"
},
"NumericGreaterThanIfExists": {
"type": "object"
},
"NumericLessThan": {
"type": "object"
},
"NumericLessThanEquals": {
"type": "object"
},
"NumericLessThanEqualsIfExists": {
"type": "object"
},
"NumericLessThanIfExists": {
"type": "object"
},
"NumericNotEquals": {
"type": "object"
},
"NumericNotEqualsIfExists": {
"type": "object"
},
"StringEquals": {
"type": "object"
},
"StringEqualsIfExists": {
"type": "object"
},
"StringEqualsIgnoreCase": {
"type": "object"
},
"StringEqualsIgnoreCaseIfExists": {
"type": "object"
},
"StringLike": {
"type": "object"
},
"StringLikeIfExists": {
"type": "object"
},
"StringNotEquals": {
"type": "object"
},
"StringNotEqualsIfExists": {
"type": "object"
},
"StringNotEqualsIgnoreCase": {
"type": "object"
},
"StringNotEqualsIgnoreCaseIfExists": {
"type": "object"
},
"StringNotLike": {
"type": "object"
},
"StringNotLikeIfExists": {
"type": "object"
}
},
"additionalProperties": false
}
},
"properties": {
"Version": {
"type": "string",
"enum": [
"2012-10-17"
]
},
"Statement": {
"oneOf": [
{
"type": [
"array"
],
"minItems": 1,
"items": {
"type": "object",
"properties": {
"Sid": {
"type": "string",
"pattern": "^[a-zA-Z0-9]+$"
},
"Action": {
"oneOf": [
{
"$ref": "#/definitions/actionItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/actionItem"
}
}
]
},
"Effect": {
"type": "string",
"enum": [
"Allow",
"Deny"
]
},
"Principal": {
"$ref": "#/definitions/principalItem"
},
"Resource": {
"oneOf": [
{
"$ref": "#/definitions/resourceItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/resourceItem"
},
"minItems": 1
}
]
},
"Condition": {
"$ref": "#/definitions/conditions"
}
},
"required": [
"Action",
"Effect",
"Principal",
"Resource"
]
}
},
{
"type": [
"object"
],
"properties": {
"Sid": {
"type": "string",
"pattern": "^[a-zA-Z0-9]+$"
},
"Action": {
"oneOf": [
{
"$ref": "#/definitions/actionItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/actionItem"
}
}
]
},
"Effect": {
"type": "string",
"enum": [
"Allow",
"Deny"
]
},
"Principal": {
"$ref": "#/definitions/principalItem"
},
"Resource": {
"oneOf": [
{
"$ref": "#/definitions/resourceItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/resourceItem"
},
"minItems": 1
}
]
},
"Condition": {
"$ref": "#/definitions/conditions"
}
},
"required": [
"Action",
"Effect",
"Resource",
"Principal"
]
}
]
}
},
"required": [
"Version",
"Statement"
],
"additionalProperties": false
}

View File

@ -1,7 +1,7 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object", "type": "object",
"title": "AWS Policy schema.", "title": "AWS IAM Policy schema.",
"description": "This schema describes a user policy per AWS policy grammar rules", "description": "This schema describes a user policy per AWS policy grammar rules",
"definitions": { "definitions": {
"principalService": { "principalService": {

View File

@ -32,6 +32,9 @@ const _actionMap = {
bucketPutLifecycle: 's3:PutLifecycleConfiguration', bucketPutLifecycle: 's3:PutLifecycleConfiguration',
bucketGetLifecycle: 's3:GetLifecycleConfiguration', bucketGetLifecycle: 's3:GetLifecycleConfiguration',
bucketDeleteLifecycle: 's3:DeleteLifecycleConfiguration', bucketDeleteLifecycle: 's3:DeleteLifecycleConfiguration',
bucketPutPolicy: 's3:PutBucketPolicy',
bucketGetPolicy: 's3:GetBucketPolicy',
bucketDeletePolicy: 's3:DeleteBucketPolicy',
completeMultipartUpload: 's3:PutObject', completeMultipartUpload: 's3:PutObject',
initiateMultipartUpload: 's3:PutObject', initiateMultipartUpload: 's3:PutObject',
listMultipartUploads: 's3:ListBucketMultipartUploads', listMultipartUploads: 's3:ListBucketMultipartUploads',

View File

@ -4,8 +4,9 @@ const assert = require('assert');
const policyValidator = require('../../../lib/policy/policyValidator'); const policyValidator = require('../../../lib/policy/policyValidator');
const errors = require('../../../lib/errors'); const errors = require('../../../lib/errors');
const validateUserPolicy = policyValidator.validateUserPolicy; const validateUserPolicy = policyValidator.validateUserPolicy;
const validateResourcePolicy = policyValidator.validateResourcePolicy;
const successRes = { error: null, valid: true }; const successRes = { error: null, valid: true };
const samplePolicy = { const sampleUserPolicy = {
Version: '2012-10-17', Version: '2012-10-17',
Statement: { Statement: {
Sid: 'FooBar1234', Sid: 'FooBar1234',
@ -15,6 +16,19 @@ const samplePolicy = {
Condition: { NumericLessThanEquals: { 's3:max-keys': '10' } }, Condition: { NumericLessThanEquals: { 's3:max-keys': '10' } },
}, },
}; };
const sampleResourcePolicy = {
Version: '2012-10-17',
Statement: [
{
Sid: 'ResourcePolicy1',
Effect: 'Allow',
Action: 's3:ListBucket',
Resource: 'arn:aws:s3:::example-bucket',
Condition: { StringLike: { 's3:prefix': 'foo' } },
Principal: '*',
},
],
};
const errDict = { const errDict = {
required: { required: {
@ -30,45 +44,84 @@ const errDict = {
Resource: 'Policy statement must contain resources.', Resource: 'Policy statement must contain resources.',
}, },
}; };
let policy;
function failRes(errDescription) { function failRes(policyType, errDescription) {
const error = Object.assign({}, errors.MalformedPolicyDocument); let error;
if (policyType === 'user') {
error = Object.assign({}, errors.MalformedPolicyDocument);
}
if (policyType === 'resource') {
error = Object.assign({}, errors.MalformedPolicy);
}
error.description = errDescription || error.description; error.description = errDescription || error.description;
return { error, valid: false }; return { error, valid: false };
} }
function check(input, expected) { function check(input, expected, policyType) {
const result = validateUserPolicy(JSON.stringify(input)); let result;
if (policyType === 'user') {
result = validateUserPolicy(JSON.stringify(input));
}
if (policyType === 'resource') {
result = validateResourcePolicy(JSON.stringify(input));
}
assert.deepStrictEqual(result, expected); assert.deepStrictEqual(result, expected);
} }
let userPolicy;
let resourcePolicy;
const user = 'user';
const resource = 'resource';
beforeEach(() => { beforeEach(() => {
policy = JSON.parse(JSON.stringify(samplePolicy)); userPolicy = JSON.parse(JSON.stringify(sampleUserPolicy));
resourcePolicy = JSON.parse(JSON.stringify(sampleResourcePolicy));
}); });
describe('Policies validation - Invalid JSON', () => { describe('Policies validation - Invalid JSON', () => {
it('should return error for invalid JSON', () => { it('should return error for invalid user policy JSON', () => {
const result = validateUserPolicy('{"Version":"2012-10-17",' + const result = validateUserPolicy('{"Version":"2012-10-17",' +
'"Statement":{"Effect":"Allow""Action":"s3:PutObject",' + '"Statement":{"Effect":"Allow""Action":"s3:PutObject",' +
'"Resource":"arn:aws:s3*"}}'); '"Resource":"arn:aws:s3*"}}');
assert.deepStrictEqual(result, failRes()); assert.deepStrictEqual(result, failRes(user));
});
it('should return error for invaild resource policy JSON', () => {
const result = validateResourcePolicy('{"Version":"2012-10-17",' +
'"Statement":{"Effect":"Allow""Action":"s3:PutObject",' +
'"Resource":"arn:aws:s3*"}}');
assert.deepStrictEqual(result, failRes(resource));
}); });
}); });
describe('Policies validation - Version', () => { describe('Policies validation - Version', () => {
it('should validate with version date 2012-10-17', () => { it('should validate user policy with version date 2012-10-17', () => {
check(policy, successRes); check(userPolicy, successRes, user);
}); });
it('should return error for other dates', () => { it('should validate resource policy with version date 2012-10-17', () => {
policy.Version = '2012-11-17'; check(resourcePolicy, successRes, 'resource');
check(policy, failRes());
}); });
it('should return error if Version field is missing', () => { it('user policy should return error for other dates', () => {
policy.Version = undefined; userPolicy.Version = '2012-11-17';
check(policy, failRes(errDict.required.Version)); check(userPolicy, failRes(user), user);
});
it('resource policy should return error for other dates', () => {
resourcePolicy.Version = '2012-11-17';
check(resourcePolicy, failRes(resource), resource);
});
it('should return error if Version field in user policy is missing', () => {
userPolicy.Version = undefined;
check(userPolicy, failRes(user, errDict.required.Version), user);
});
it('should return error if Version field in resource policy is missing',
() => {
resourcePolicy.Version = undefined;
check(resourcePolicy, failRes(resource, errDict.required.Version),
resource);
}); });
}); });
@ -77,20 +130,24 @@ describe('Policies validation - Principal', () => {
{ {
name: 'an account id', name: 'an account id',
value: { AWS: '111111111111' }, value: { AWS: '111111111111' },
policyType: [user, resource],
}, },
{ {
name: 'anonymous user AWS form', name: 'anonymous user AWS form',
value: { AWS: '*' }, value: { AWS: '*' },
policyType: [user, resource],
}, },
{ {
name: 'an account arn', name: 'an account arn',
value: { AWS: 'arn:aws:iam::111111111111:root' }, value: { AWS: 'arn:aws:iam::111111111111:root' },
policyType: [user, resource],
}, },
{ {
name: 'multiple account id', name: 'multiple account id',
value: { value: {
AWS: ['111111111111', '111111111112'], AWS: ['111111111111', '111111111112'],
}, },
policyType: [user, resource],
}, },
{ {
name: 'multiple account arn', name: 'multiple account arn',
@ -100,14 +157,17 @@ describe('Policies validation - Principal', () => {
'arn:aws:iam::111111111112:root', 'arn:aws:iam::111111111112:root',
], ],
}, },
policyType: [user, resource],
}, },
{ {
name: 'anonymous user as string', name: 'anonymous user as string',
value: '*', value: '*',
policyType: [user, resource],
}, },
{ {
name: 'user arn', name: 'user arn',
value: { AWS: 'arn:aws:iam::111111111111:user/alex' }, value: { AWS: 'arn:aws:iam::111111111111:user/alex' },
policyType: [user, resource],
}, },
{ {
name: 'multiple user arns', name: 'multiple user arns',
@ -117,12 +177,14 @@ describe('Policies validation - Principal', () => {
'arn:aws:iam::111111111111:user/thibault', 'arn:aws:iam::111111111111:user/thibault',
], ],
}, },
policyType: [user, resource],
}, },
{ {
name: 'role arn', name: 'role arn',
value: { value: {
AWS: 'arn:aws:iam::111111111111:role/dev', AWS: 'arn:aws:iam::111111111111:role/dev',
}, },
policyType: [user, resource],
}, },
{ {
name: 'multiple role arn', name: 'multiple role arn',
@ -132,6 +194,7 @@ describe('Policies validation - Principal', () => {
'arn:aws:iam::111111111111:role/prod', 'arn:aws:iam::111111111111:role/prod',
], ],
}, },
policyType: [user, resource],
}, },
{ {
name: 'saml provider', name: 'saml provider',
@ -139,57 +202,84 @@ describe('Policies validation - Principal', () => {
Federated: Federated:
'arn:aws:iam::111111111111:saml-provider/mysamlprovider', 'arn:aws:iam::111111111111:saml-provider/mysamlprovider',
}, },
policyType: [user],
}, },
{ {
name: 'with backbeat service', name: 'with backbeat service',
value: { Service: 'backbeat' }, value: { Service: 'backbeat' },
policyType: [user, resource],
},
{
name: 'with canonical user id',
value: { CanonicalUser:
'1examplecanonicalid12345678909876' +
'54321qwerty12345asdfg67890z1x2c' },
policyType: [resource],
}, },
].forEach(test => { ].forEach(test => {
it(`should allow principal field with ${test.name}`, () => { if (test.policyType.includes(user)) {
policy.Statement.Principal = test.value; it(`should allow user policy principal field with ${test.name}`,
delete policy.Statement.Resource; () => {
check(policy, successRes); userPolicy.Statement.Principal = test.value;
}); delete userPolicy.Statement.Resource;
check(userPolicy, successRes, user);
});
it(`shoud allow notPrincipal field with ${test.name}`, () => { it(`should allow user policy notPrincipal field with ${test.name}`,
policy.Statement.NotPrincipal = test.value; () => {
delete policy.Statement.Resource; userPolicy.Statement.NotPrincipal = test.value;
check(policy, successRes); delete userPolicy.Statement.Resource;
}); check(userPolicy, successRes, user);
});
}
if (test.policyType.includes(resource)) {
it(`should allow resource policy principal field with ${test.name}`,
() => {
resourcePolicy.Statement[0].Principal = test.value;
check(resourcePolicy, successRes, resource);
});
}
}); });
[ [
{ {
name: 'wrong format account id', name: 'wrong format account id',
value: { AWS: '11111111111z' }, value: { AWS: '11111111111z' },
policyType: [user, resource],
}, },
{ {
name: 'empty string', name: 'empty string',
value: '', value: '',
policyType: [user, resource],
}, },
{ {
name: 'anonymous user federated form', name: 'anonymous user federated form',
value: { federated: '*' }, value: { federated: '*' },
policyType: [user, resource],
}, },
{ {
name: 'wildcard in ressource', name: 'wildcard in resource',
value: { AWS: 'arn:aws:iam::111111111111:user/*' }, value: { AWS: 'arn:aws:iam::111111111111:user/*' },
policyType: [user, resource],
}, },
{ {
name: 'a malformed account arn', name: 'a malformed account arn',
value: { AWS: 'arn:aws:iam::111111111111:' }, value: { AWS: 'arn:aws:iam::111111111111:' },
policyType: [user, resource],
}, },
{ {
name: 'multiple malformed account id', name: 'multiple malformed account id',
value: { value: {
AWS: ['1111111111z1', '1111z1111112'], AWS: ['1111111111z1', '1111z1111112'],
}, },
policyType: [user, resource],
}, },
{ {
name: 'multiple anonymous', name: 'multiple anonymous',
value: { value: {
AWS: ['*', '*'], AWS: ['*', '*'],
}, },
policyType: [user, resource],
}, },
{ {
name: 'multiple malformed account arn', name: 'multiple malformed account arn',
@ -199,18 +289,22 @@ describe('Policies validation - Principal', () => {
'arn:aws:iam::111111111112:', 'arn:aws:iam::111111111112:',
], ],
}, },
policyType: [user, resource],
}, },
{ {
name: 'account id as a string', name: 'account id as a string',
value: '111111111111', value: '111111111111',
policyType: [user, resource],
}, },
{ {
name: 'account arn as a string', name: 'account arn as a string',
value: 'arn:aws:iam::111111111111:root', value: 'arn:aws:iam::111111111111:root',
policyType: [user, resource],
}, },
{ {
name: 'user arn as a string', name: 'user arn as a string',
value: 'arn:aws:iam::111111111111:user/alex', value: 'arn:aws:iam::111111111111:user/alex',
policyType: [user, resource],
}, },
{ {
name: 'multiple malformed user arns', name: 'multiple malformed user arns',
@ -220,12 +314,14 @@ describe('Policies validation - Principal', () => {
'arn:aws:iam::111111111111:user/', 'arn:aws:iam::111111111111:user/',
], ],
}, },
policyType: [user, resource],
}, },
{ {
name: 'malformed role arn', name: 'malformed role arn',
value: { value: {
AWS: 'arn:aws:iam::111111111111:role/', AWS: 'arn:aws:iam::111111111111:role/',
}, },
policyType: [user, resource],
}, },
{ {
name: 'multiple malformed role arn', name: 'multiple malformed role arn',
@ -235,36 +331,84 @@ describe('Policies validation - Principal', () => {
'arn:aws:iam::11111111z111:role/prod', 'arn:aws:iam::11111111z111:role/prod',
], ],
}, },
policyType: [user, resource],
}, },
{ {
name: 'saml provider as a string', name: 'saml provider as a string',
value: 'arn:aws:iam::111111111111:saml-provider/mysamlprovider', value: 'arn:aws:iam::111111111111:saml-provider/mysamlprovider',
policyType: [user],
}, },
{ {
name: 'with other service than backbeat', name: 'with other service than backbeat',
value: { Service: 'non-existent-service' }, value: { Service: 'non-existent-service' },
policyType: [user, resource],
},
{
name: 'invalid canonical user',
value: { CanonicalUser:
'12345invalid-canonical-id$$$//098' +
'7654321poiu1q2w3e4r5t6y7u8i9o0p' },
policyType: [resource],
}, },
].forEach(test => { ].forEach(test => {
it(`should fail with ${test.name}`, () => { if (test.policyType.includes(user)) {
policy.Statement.Principal = test.value; it(`user policy should fail with ${test.name}`, () => {
delete policy.Statement.Resource; userPolicy.Statement.Principal = test.value;
check(policy, failRes()); delete userPolicy.Statement.Resource;
}); check(userPolicy, failRes(user), user);
});
}
if (test.policyType.includes(resource)) {
it(`resource policy should fail with ${test.name}`, () => {
resourcePolicy.Statement[0].Principal = test.value;
check(resourcePolicy, failRes(resource), resource);
});
}
}); });
it('should not allow Resource field', () => { it('should not allow Resource field', () => {
policy.Statement.Principal = '*'; userPolicy.Statement.Principal = '*';
check(policy, failRes()); check(userPolicy, failRes(user), user);
}); });
}); });
describe('Policies validation - Statement', () => { describe('Policies validation - Statement', () => {
it('should succeed for a valid object', () => { [
check(policy, successRes); {
name: 'should return error for undefined',
value: undefined,
},
{
name: 'should return an error for an empty list',
value: [],
},
{
name: 'should return an error for an empty object',
value: {},
errMessage: errDict.required.Action,
},
].forEach(test => {
it(`user policy ${test.name}`, () => {
userPolicy.Statement = test.value;
check(userPolicy, failRes(user, test.errMessage), user);
});
it(`resource policy ${test.name}`, () => {
resourcePolicy.Statement = test.value;
check(resourcePolicy, failRes(resource, test.errMessage), resource);
});
}); });
it('should succeed for a valid array', () => { it('user policy should succeed for a valid object', () => {
policy.Statement = [ check(userPolicy, successRes, user);
});
it('resource policy should succeed for a valid object', () => {
check(resourcePolicy, successRes, resource);
});
it('user policy should succeed for a valid object', () => {
userPolicy.Statement = [
{ {
Effect: 'Allow', Effect: 'Allow',
Action: 's3:PutObject', Action: 's3:PutObject',
@ -276,255 +420,373 @@ describe('Policies validation - Statement', () => {
Resource: 'arn:aws:s3:::my_bucket/uploads/widgetco/*', Resource: 'arn:aws:s3:::my_bucket/uploads/widgetco/*',
}, },
]; ];
check(policy, successRes); check(userPolicy, successRes, user);
}); });
it('should return an error for undefined', () => { it('resource policy should succeed for a valid object', () => {
policy.Statement = undefined; resourcePolicy.Statement = [
check(policy, failRes()); {
Effect: 'Allow',
Action: 's3:PutObject',
Resource: 'arn:aws:s3:::my_bucket/uploads/widgetco/*',
Principal: '*',
},
{
Effect: 'Deny',
Action: 's3:DeleteObject',
Resource: 'arn:aws:s3:::my_bucket/uploads/widgetco/*',
Principal: '*',
},
];
check(resourcePolicy, successRes, resource);
}); });
it('should return an error for an empty list', () => { [
policy.Statement = []; {
check(policy, failRes()); name: 'should return error for missing a required field - Action',
}); toDelete: ['Action'],
expected: 'fail',
errMessage: errDict.required.Action,
},
{
name: 'should return error for missing a required field - Effect',
toDelete: ['Effect'],
expected: 'fail',
},
{
name: 'should return error for missing required field - Resource',
toDelete: ['Resource'],
expected: 'fail',
},
{
name: 'should return error for missing multiple required fields',
toDelete: ['Effect', 'Resource'],
expected: 'fail',
},
{
name: 'should succeed w optional fields missing - Sid, Condition',
toDelete: ['Sid', 'Condition'],
expected: successRes,
},
].forEach(test => {
it(`user policy ${test.name}`, () => {
test.toDelete.forEach(p => delete userPolicy.Statement[p]);
if (test.expected === 'fail') {
check(userPolicy, failRes(user, test.errMessage), user);
} else {
check(userPolicy, test.expected, user);
}
});
it('should return an error for an empty object', () => { it(`resource policy ${test.name}`, () => {
policy.Statement = {}; test.toDelete.forEach(p => delete resourcePolicy.Statement[0][p]);
check(policy, failRes(errDict.required.Action)); if (test.expected === 'fail') {
}); check(resourcePolicy, failRes(resource, test.errMessage),
resource);
it('should return an error for missing a required field - Action', () => { } else {
delete policy.Statement.Action; check(resourcePolicy, test.expected, resource);
check(policy, failRes(errDict.required.Action)); }
}); });
it('should return an error for missing a required field - Effect', () => {
delete policy.Statement.Effect;
check(policy, failRes());
});
it('should return an error for missing a required field - Resource', () => {
delete policy.Statement.Resource;
check(policy, failRes());
});
it('should return an error for missing multiple required fields', () => {
delete policy.Statement.Effect;
delete policy.Statement.Resource;
check(policy, failRes());
});
it('should succeed with optional fields missing - Sid, Condition', () => {
delete policy.Statement.Sid;
delete policy.Statement.Condition;
check(policy, successRes);
}); });
}); });
describe('Policies validation - Statement::Sid_block', () => { describe('Policies validation - Statement::Sid_block', () => {
it('should succeed if Sid is any alphanumeric string', () => { it('user policy should succeed if Sid is any alphanumeric string', () => {
check(policy, successRes); check(userPolicy, successRes, user);
}); });
it('should fail if Sid is not a valid format', () => { it('resource policy should succeed if Sid is any alphanumeric string',
policy.Statement.Sid = 'foo bar()'; () => {
check(policy, failRes()); check(resourcePolicy, successRes, resource);
}); });
it('should fail if Sid is not a string', () => { it('user policy should fail if Sid is not a valid format', () => {
policy.Statement.Sid = 1234; userPolicy.Statement.Sid = 'foo bar()';
check(policy, failRes()); check(userPolicy, failRes(user), user);
});
it('resource policy should fail if Sid is not a valid format', () => {
resourcePolicy.Statement[0].Sid = 'foo bar()';
check(resourcePolicy, failRes(resource), resource);
});
it('user policy should fail if Sid is not a string', () => {
userPolicy.Statement.Sid = 1234;
check(userPolicy, failRes(user), user);
});
it('resource policy should fail if Sid is not a string', () => {
resourcePolicy.Statement[0].Sid = 1234;
check(resourcePolicy, failRes(resource), resource);
}); });
}); });
describe('Policies validation - Statement::Effect_block', () => { describe('Policies validation - Statement::Effect_block', () => {
it('should succeed for Allow', () => { it('user policy should succeed for Allow', () => {
check(policy, successRes); check(userPolicy, successRes, user);
}); });
it('should succeed for Deny', () => { it('resource policy should succeed for Allow', () => {
policy.Statement.Effect = 'Deny'; check(resourcePolicy, successRes, resource);
check(policy, successRes);
}); });
it('should fail for strings other than Allow/Deny', () => { it('user policy should succeed for Deny', () => {
policy.Statement.Effect = 'Reject'; userPolicy.Statement.Effect = 'Deny';
check(policy, failRes()); check(userPolicy, successRes, user);
}); });
it('should fail if Effect is not a string', () => { it('resource policy should succeed for Deny', () => {
policy.Statement.Effect = 1; resourcePolicy.Statement[0].Effect = 'Deny';
check(policy, failRes()); check(resourcePolicy, successRes, resource);
});
it('user policy should fail for strings other than Allow/Deny', () => {
userPolicy.Statement.Effect = 'Reject';
check(userPolicy, failRes(user), user);
});
it('resource policy should fail for strings other than Allow/Deny', () => {
resourcePolicy.Statement[0].Effect = 'Reject';
check(resourcePolicy, failRes(resource), resource);
});
it('user policy should fail if Effect is not a string', () => {
userPolicy.Statement.Effect = 1;
check(userPolicy, failRes(user), user);
});
it('resource policy should fail if Effect is not a string', () => {
resourcePolicy.Statement[0].Effect = 1;
check(resourcePolicy, failRes(resource), resource);
}); });
}); });
describe('Policies validation - Statement::Action_block/' + const actionTests = [
{
name: 'should succeed for foo:bar',
value: 'foo:bar',
expected: successRes,
},
{
name: 'should succeed for foo:*',
value: 'foo:*',
expected: successRes,
},
{
name: 'should succeed for *',
value: '*',
expected: successRes,
},
{
name: 'should fail for **',
value: '**',
expected: 'fail',
errMessage: errDict.pattern.Action,
},
{
name: 'should fail for foobar',
value: 'foobar',
expected: 'fail',
errMessage: errDict.pattern.Action,
},
];
describe('User policies validation - Statement::Action_block/' +
'Statement::NotAction_block', () => { 'Statement::NotAction_block', () => {
beforeEach(() => { beforeEach(() => {
policy.Statement.Action = undefined; userPolicy.Statement.Action = undefined;
policy.Statement.NotAction = undefined; userPolicy.Statement.NotAction = undefined;
}); });
it('should succeed for foo:bar', () => { actionTests.forEach(test => {
policy.Statement.Action = 'foo:bar'; it(`${test.name}`, () => {
check(policy, successRes); userPolicy.Statement.Action = test.value;
if (test.expected === 'fail') {
check(userPolicy, failRes(user, test.errMessage), user);
} else {
check(userPolicy, test.expected, user);
}
policy.Statement.Action = undefined; userPolicy.Statement.Action = undefined;
policy.Statement.NotAction = 'foo:bar'; userPolicy.Statement.NotAction = test.value;
check(policy, successRes); if (test.expected === 'fail') {
}); check(userPolicy, failRes(user, test.errMessage), user);
} else {
it('should succeed for foo:*', () => { check(userPolicy, test.expected, user);
policy.Statement.Action = 'foo:*'; }
check(policy, successRes); });
policy.Statement.Action = undefined;
policy.Statement.NotAction = 'foo:*';
check(policy, successRes);
});
it('should succeed for *', () => {
policy.Statement.Action = '*';
check(policy, successRes);
policy.Statement.Action = undefined;
policy.Statement.NotAction = '*';
check(policy, successRes);
});
it('should fail for **', () => {
policy.Statement.Action = '**';
check(policy, failRes(errDict.pattern.Action));
policy.Statement.Action = undefined;
policy.Statement.NotAction = '**';
check(policy, failRes(errDict.pattern.Action));
});
it('should fail for foobar', () => {
policy.Statement.Action = 'foobar';
check(policy, failRes(errDict.pattern.Action));
policy.Statement.Action = undefined;
policy.Statement.NotAction = 'foobar';
check(policy, failRes(errDict.pattern.Action));
}); });
}); });
describe('Policies validation - Statement::Resource_block' + describe('Resource policies validation - Statement::Action_block', () => {
actionTests.forEach(test => {
it(`${test.name}`, () => {
resourcePolicy.Statement[0].Action = test.value;
if (test.expected === 'fail') {
check(resourcePolicy, failRes(resource, test.errMessage),
resource);
} else {
check(resourcePolicy, test.expected, resource);
}
});
});
});
const resourceTests = [
{
name: 'should succeed for arn:aws::s3:::*',
value: 'arn:aws:s3:::*',
expected: successRes,
},
{
name: 'should succeed for arn:aws:s3:::test/home/${aws:username}',
value: 'arn:aws:s3:::test/home/${aws:username}',
expected: successRes,
},
{
name: 'should succeed for arn:aws:ec2:us-west-1:1234567890:vol/*',
value: 'arn:aws:ec2:us-west-1:1234567890:vol/*',
expected: successRes,
},
{
name: 'should succeed for *',
value: '*',
expected: successRes,
},
{
name: 'should fail for arn:aws:ec2:us-west-1:vol/* - missing region',
value: 'arn:aws:ec2:us-west-1:vol/*',
expected: 'fail',
errMessage: errDict.pattern.Resource,
},
{
name: 'should fail for arn:aws:ec2:us-west-1:123456789:v/${} - ${}',
value: 'arn:aws:ec2:us-west-1:123456789:v/${}',
expected: 'fail',
errMessage: errDict.pattern.Resource,
},
{
name: 'should fail for ec2:us-west-1:qwerty:vol/* - missing arn:aws:',
value: 'ec2:us-west-1:123456789012:vol/*',
expected: 'fail',
errMessage: errDict.pattern.Resource,
},
];
describe('User policies validation - Statement::Resource_block' +
'Statement::NotResource_block', () => { 'Statement::NotResource_block', () => {
beforeEach(() => { beforeEach(() => {
policy.Statement.Resource = undefined; userPolicy.Statement.Resource = undefined;
policy.Statement.NotResource = undefined; userPolicy.Statement.NotResource = undefined;
}); });
it('should succeed for arn:aws:s3:::*', () => { resourceTests.forEach(test => {
policy.Statement.Resource = 'arn:aws:s3:::*'; it(`${test.name}`, () => {
check(policy, successRes); userPolicy.Statement.Resource = test.value;
if (test.expected === 'fail') {
check(userPolicy, failRes(user, test.errMessage), user);
} else {
check(userPolicy, test.expected, user);
}
policy.Statement.Resource = undefined; userPolicy.Statement.Resource = undefined;
policy.Statement.NotResource = 'arn:aws:s3:::*'; userPolicy.Statement.NotResource = test.value;
check(policy, successRes); if (test.expected === 'fail') {
}); check(userPolicy, failRes(user, test.errMessage), user);
} else {
it('should succeed for arn:aws:s3:::test/home/${aws:username}', () => { check(userPolicy, test.expected, user);
policy.Statement.Resource = 'arn:aws:s3:::test/home/${aws:username}'; }
check(policy, successRes); });
policy.Statement.Resource = undefined;
policy.Statement.NotResource = 'arn:aws:s3:::test/home/${aws:username}';
check(policy, successRes);
});
it('should succeed for arn:aws:ec2:us-west-1:1234567890:vol/*', () => {
policy.Statement.Resource = 'arn:aws:ec2:us-west-1:1234567890:vol/*';
check(policy, successRes);
policy.Statement.Resource = undefined;
policy.Statement.NotResource = 'arn:aws:ec2:us-west-1:1234567890:vol/*';
check(policy, successRes);
});
it('should succeed for *', () => {
policy.Statement.Resource = '*';
check(policy, successRes);
policy.Statement.Resource = undefined;
policy.Statement.NotResource = '*';
check(policy, successRes);
});
it('should fail for arn:aws:ec2:us-west-1:vol/* - missing region', () => {
policy.Statement.Resource = 'arn:aws:ec2:1234567890:vol/*';
check(policy, failRes(errDict.pattern.Resource));
policy.Statement.Resource = undefined;
policy.Statement.NotResource = 'arn:aws:ec2:1234567890:vol/*';
check(policy, failRes(errDict.pattern.Resource));
});
it('should fail for arn:aws:ec2:us-west-1:123456789:v/${} - ${}', () => {
policy.Statement.Resource = 'arn:aws:ec2:us-west-1:123456789:v/${}';
check(policy, failRes(errDict.pattern.Resource));
policy.Statement.Resource = undefined;
policy.Statement.NotResource = 'arn:aws:ec2:us-west-1:123456789:v/${}';
check(policy, failRes(errDict.pattern.Resource));
});
it('should fail for ec2:us-west-1:qwerty:vol/* - missing arn:aws:', () => {
policy.Statement.Resource = 'ec2:us-west-1:123456789012:vol/*';
check(policy, failRes(errDict.pattern.Resource));
policy.Statement.Resource = undefined;
policy.Statement.NotResource = 'ec2:us-west-1:123456789012:vol/*';
check(policy, failRes(errDict.pattern.Resource));
}); });
it('should fail for empty list of resources', () => { it('should fail for empty list of resources', () => {
policy.Statement.Resource = []; userPolicy.Statement.Resource = [];
check(policy, failRes(errDict.minItems.Resource)); check(userPolicy, failRes(user, errDict.minItems.Resource), user);
});
});
describe('Resource policies validation - Statement::Resource_block', () => {
resourceTests.forEach(test => {
it(`${test.name}`, () => {
resourcePolicy.Statement[0].Resource = test.value;
if (test.expected === 'fail') {
check(resourcePolicy, failRes(resource, test.errMessage),
resource);
} else {
check(resourcePolicy, test.expected, resource);
}
});
});
it('should fail for empty list of resources', () => {
resourcePolicy.Statement[0].Resource = [];
check(resourcePolicy, failRes(resource, errDict.minItems.Resource),
resource);
}); });
}); });
describe('Policies validation - Statement::Condition_block', () => { describe('Policies validation - Statement::Condition_block', () => {
it('should succeed for single Condition', () => { it('user policy should succeed for single Condition', () => {
check(policy, successRes); check(userPolicy, successRes, user);
}); });
it('should succeed for multiple Conditions', () => { it('resource policy should succeed for single Condition', () => {
policy.Statement.Condition = { check(resourcePolicy, successRes, resource);
StringNotLike: { 's3:prefix': ['Development/*'] },
Null: { 's3:prefix': false },
};
check(policy, successRes);
}); });
it('should fail when Condition is not an Object', () => { [
policy.Statement.Condition = 'NumericLessThanEquals'; {
check(policy, failRes()); name: 'should succeed for multiple Conditions',
}); value: {
StringNotLike: { 's3:prefix': ['Development/*'] },
Null: { 's3:prefix': false },
},
expected: successRes,
},
{
name: 'should fail when Condition is not an Object',
value: 'NumericLessThanEquals',
expected: 'fail',
},
{
name: 'should fail for an invalid Condition',
value: {
SomethingLike: { 's3:prefix': ['Development/*'] },
},
expected: 'fail',
},
{
name: 'should fail when one of the multiple conditions is invalid',
value: {
Null: { 's3:prefix': false },
SomethingLike: { 's3:prefix': ['Development/*'] },
},
expected: 'fail',
},
{
name: 'should fail when invalid property is assigned',
value: {
SomethingLike: { 's3:prefix': ['Development/*'] },
},
expected: 'fail',
},
].forEach(test => {
it(`user policy ${test.name}`, () => {
userPolicy.Statement.Condition = test.value;
if (test.expected === 'fail') {
check(userPolicy, failRes(user), user);
} else {
check(userPolicy, test.expected, user);
}
});
it('should fail for an invalid Condition', () => { it(`resource policy ${test.name}`, () => {
policy.Statement.Condition = { resourcePolicy.Statement[0].Condition = test.value;
SomethingLike: { 's3:prefix': ['Development/*'] }, if (test.expected === 'fail') {
}; check(resourcePolicy, failRes(resource), resource);
check(policy, failRes()); } else {
}); check(resourcePolicy, test.expected, resource);
}
it('should fail when one of the multiple conditions is invalid', () => { });
policy.Statement.Condition = {
Null: { 's3:prefix': false },
SomethingLike: { 's3:prefix': ['Development/*'] },
};
check(policy, failRes());
});
it('should fail when invalid property is assigned', () => {
policy.Condition = {
SomethingLike: { 's3:prefix': ['Development/*'] },
};
check(policy, failRes());
}); });
}); });