Compare commits

...

1 Commits

Author SHA1 Message Date
Rahul Padigela 2c2626132d FT: User Policy validator 2016-06-29 19:13:26 -07:00
5 changed files with 801 additions and 10 deletions

View File

@ -293,13 +293,13 @@
"description": "The request signature we calculated does not match the signature you provided." "description": "The request signature we calculated does not match the signature you provided."
}, },
"_comment" : { "_comment" : {
"note" : "This is an AWS S3 specific error. We are opting to use the more general 'ServiceUnavailable' error used throughout AWS (IAM/EC2) to have uniformity of error messages even though we are potentially compromising S3 compatibility.", "note" : "This is an AWS S3 specific error. We are opting to use the more general 'ServiceUnavailable' error used throughout AWS (IAM/EC2) to have uniformity of error messages even though we are potentially compromising S3 compatibility.",
"ServiceUnavailable": { "ServiceUnavailable": {
"code": 503, "code": 503,
"description": "Reduce your request rate." "description": "Reduce your request rate."
} }
}, },
"ServiceUnavailable": { "ServiceUnavailable": {
"code": 503, "code": 503,
"description": "The request has failed due to a temporary failure of the server." "description": "The request has failed due to a temporary failure of the server."
}, },
@ -550,7 +550,7 @@
"SecretKeyDoesNotExist": { "SecretKeyDoesNotExist": {
"description": "secret key does not exist", "description": "secret key does not exist",
"code": 5030 "code": 5030
}, },
"InvalidRegion": { "InvalidRegion": {
"description": "Region was not provided or is not recognized by the system", "description": "Region was not provided or is not recognized by the system",
"code": 5031 "code": 5031
@ -583,15 +583,15 @@
"BadUrl": { "BadUrl": {
"description": "url not ok", "description": "url not ok",
"code": 5038 "code": 5038
}, },
"BadClientIdList": { "BadClientIdList": {
"description": "client id list not ok'", "description": "client id list not ok'",
"code": 5039 "code": 5039
}, },
"BadThumbprintList": { "BadThumbprintList": {
"description": "thumbprint list not ok'", "description": "thumbprint list not ok'",
"code": 5040 "code": 5040
}, },
"BadObject": { "BadObject": {
"description": "Object not ok'", "description": "Object not ok'",
"code": 5041 "code": 5041
@ -600,12 +600,12 @@
"BadRole": { "BadRole": {
"description": "role not ok", "description": "role not ok",
"code": 5042 "code": 5042
}, },
"_comment": "#### SamlpErrors ####", "_comment": "#### SamlpErrors ####",
"BadSamlp": { "BadSamlp": {
"description": "samlp not ok", "description": "samlp not ok",
"code": 5043 "code": 5043
}, },
"BadMetadataDocument": { "BadMetadataDocument": {
"description": "metadata document not ok", "description": "metadata document not ok",
"code": 5044 "code": 5044
@ -671,5 +671,46 @@
"NotEnoughMapsInConfig:": { "NotEnoughMapsInConfig:": {
"description": "NotEnoughMapsInConfig", "description": "NotEnoughMapsInConfig",
"code": 400 "code": 400
} },
"_comment": "--------------------- Policies ---------------------",
"InvalidPolicyJSON": {
"description": "Policy contains JSON errors",
"code": 400
},
"InvalidPolicyVersion": {
"description": "Version field must be a valid string",
"code": 400
},
"InvalidPolicyEffect": {
"description": "Invalid value for Effect",
"code": 400
},
"InvalidPolicyAction": {
"description": "Invalid service prefix for Action",
"code": 400
},
"InvalidPolicyResource": {
"description": "Invalid value for Resource",
"code": 400
},
"MissingPolicyVersion": {
"description": "Missing required field Version",
"code": 400
},
"MissingPolicyStatement": {
"description": "Missing required field Statement",
"code": 400
},
"MissingPolicyAction": {
"description": "Missing required field Action",
"code": 400
},
"MissingPolicyEffect": {
"description": "Missing required field Effect",
"code": 400
},
"MissingPolicyResource": {
"description": "Missing required field Resource",
"code": 400
}
} }

View File

@ -0,0 +1,108 @@
'use strict'; // eslint-disable-line strict
const Ajv = require('ajv');
const userPolicySchema = require('./userPolicySchema');
const errors = require('../errors');
const ajValidate = new Ajv({ allErrors: true });
// compiles schema to functions and caches them for all cases
const userPolicyValidate = ajValidate.compile(userPolicySchema);
// parse ajv errors and build list of erros
function _parseErrors(ajvErrors) {
let parsedErr;
ajvErrors.some(err => {
const resource = err.dataPath.replace('.', '');
if (err.keyword === 'required' && err.params) {
const field = err.params.missingProperty;
if (field === 'Version') {
parsedErr = errors.MissingPolicyVersion;
} else if (field === 'Statement') {
parsedErr = errors.MissingPolicyStatement;
} else if (field === 'Action') {
parsedErr = errors.MissingPolicyAction;
} else if (field === 'Effect') {
parsedErr = errors.MissingPolicyEffect;
} else if (field === 'Resource') {
parsedErr = errors.MissingPolicyResource;
} else {
parsedErr = errors.InvalidPolicyDocument;
}
} else if (err.keyword === 'minItems' && resource === 'Statement') {
parsedErr = errors.InvalidPolicyStatement;
} else if (err.keyword === 'pattern') {
parsedErr = errors.InvalidPolicyDocument;
} else if (err.keyword === 'type') {
// skip if it's Statement as it does not have enough
// error context
if (resource === 'Version') {
parsedErr = errors.PolicyInvalidVersion;
}
} else {
parsedErr = errors.InvalidPolicyDocument;
}
return parsedErr instanceof Error;
});
return parsedErr;
}
// parse JSON safely without throwing an exception
function _safeJSONParse(s) {
let res;
try {
res = JSON.parse(s);
} catch (e) {
return e;
}
return res;
}
// validates policy using the validation schema
function _validatePolicy(type, policy) {
if (type === 'user') {
const parseRes = _safeJSONParse(policy);
if (parseRes instanceof Error) {
return { error: errors.PolicyInvalidJSON, valid: false };
}
userPolicyValidate(parseRes);
if (userPolicyValidate.errors) {
return { error: _parseErrors(userPolicyValidate.errors),
valid: false };
}
return { error: null, valid: true };
}
// todo: add support for resource policies
return { error: errors.NotImplemented, valid: false };
}
/**
* @typedef ValidationResult
* @type Object
* @property {Array|null} error - list of validation errors or null
* @property {Bool} valid - true/false depending on the validation result
*/
/**
* Validates user policy
* @param {String} policy - policy json
* @returns {Object} - returns object with properties error and value
* @returns {ValidationResult} - result of the validation
*/
function validateUserPolicy(policy) {
return _validatePolicy('user', policy);
}
/**
* Validates resource policy
* @param {String} policy - policy json
* @returns {Object} - returns object with properties error and value
* @returns {ValidationResult} - result of the validation
*/
function validateResourcePolicy(policy) {
return _validatePolicy('resource', policy);
}
module.exports = {
validateUserPolicy,
validateResourcePolicy,
};

View File

@ -0,0 +1,401 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "AWS Policy schema.",
"description": "This schema describes user policy per AWS policy grammar rules",
"definitions": {
"actionItem": {
"type": "string",
"pattern": "^[^*:]+:([^:])+|^\\*{1}$"
},
"resourceItem": {
"type": "string",
"pattern": "^\\*|arn:aws(:(\\*{1}|[a-z0-9\\*\\-]{2,})*){2}:.*$"
},
"conditions": {
"type": "object",
"properties": {
"StringEquals": {
"type": "object"
},
"StringNotEquals": {
"type": "object"
},
"StringEqualsIgnoreCase": {
"type": "object"
},
"StringNotEqualsIgnoreCase": {
"type": "object"
},
"StringLike": {
"type": "object"
},
"StringNotLike": {
"type": "object"
},
"NumericEquals": {
"type": "object"
},
"NumericNotEquals": {
"type": "object"
},
"NumericLessThan": {
"type": "object"
},
"NumericLessThanEquals": {
"type": "object"
},
"NumericGreaterThan": {
"type": "object"
},
"NumericGreaterThanEquals": {
"type": "object"
},
"DateEquals": {
"type": "object"
},
"DateNotEquals": {
"type": "object"
},
"DateLessThan": {
"type": "object"
},
"DateLessThanEquals": {
"type": "object"
},
"DateGreaterThan": {
"type": "object"
},
"DateGreaterThanEquals": {
"type": "object"
},
"Bool": {
"type": "object"
},
"BinaryEquals": {
"type": "object"
},
"IpAddress": {
"type": "object"
},
"NotIpAddress": {
"type": "object"
},
"ArnEquals": {
"type": "object"
},
"ArnNotEquals": {
"type": "object"
},
"ArnLike": {
"type": "object"
},
"ArnNotLike": {
"type": "object"
},
"Null": {
"type": "object"
},
"StringEqualsIfExists": {
"type": "object"
},
"StringNotEqualsIfExists": {
"type": "object"
},
"StringEqualsIgnoreCaseIfExists": {
"type": "object"
},
"StringNotEqualsIgnoreCaseIfExists": {
"type": "object"
},
"StringLikeIfExists": {
"type": "object"
},
"StringNotLikeIfExists": {
"type": "object"
},
"NumericEqualsIfExists": {
"type": "object"
},
"NumericNotEqualsIfExists": {
"type": "object"
},
"NumericLessThanIfExists": {
"type": "object"
},
"NumericLessThanEqualsIfExists": {
"type": "object"
},
"NumericGreaterThanIfExists": {
"type": "object"
},
"NumericGreaterThanEqualsIfExists": {
"type": "object"
},
"DateEqualsIfExists": {
"type": "object"
},
"DateNotEqualsIfExists": {
"type": "object"
},
"DateLessThanIfExists": {
"type": "object"
},
"DateLessThanEqualsIfExists": {
"type": "object"
},
"DateGreaterThanIfExists": {
"type": "object"
},
"DateGreaterThanEqualsIfExists": {
"type": "object"
},
"BoolIfExists": {
"type": "object"
},
"BinaryEqualsIfExists": {
"type": "object"
},
"IpAddressIfExists": {
"type": "object"
},
"NotIpAddressIfExists": {
"type": "object"
},
"ArnEqualsIfExists": {
"type": "object"
},
"ArnNotEqualsIfExists": {
"type": "object"
},
"ArnLikeIfExists": {
"type": "object"
},
"ArnNotLikeIfExists": {
"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]+"
},
"Effect": {
"type": "string",
"enum": [
"Allow",
"Deny"
]
},
"Action": {
"oneOf": [
{
"$ref": "#/definitions/actionItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/actionItem"
}
}
]
},
"NotAction": {
"oneOf": [
{
"$ref": "#/definitions/actionItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/actionItem"
}
}
]
},
"Resource": {
"oneOf": [
{
"$ref": "#/definitions/resourceItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/resourceItem"
}
}
]
},
"NotResource": {
"oneOf": [
{
"$ref": "#/definitions/resourceItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/resourceItem"
}
}
]
},
"Condition": {
"$ref": "#/definitions/conditions"
}
},
"oneOf": [
{
"required": [
"Effect",
"Action",
"Resource"
]
}, {
"required": [
"Effect",
"Action",
"NotResource"
]
}, {
"required": [
"Effect",
"NotAction",
"Resource"
]
}, {
"required": [
"Effect",
"NotAction",
"NotResource"
]
}
]
}
},
{
"type": [
"object"
],
"properties": {
"Sid": {
"type": "string",
"pattern": "[a-zA-Z0-9]+"
},
"Effect": {
"type": "string",
"enum": [
"Allow",
"Deny"
]
},
"Action": {
"oneOf": [
{
"$ref": "#/definitions/actionItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/actionItem"
}
}
]
},
"NotAction": {
"oneOf": [
{
"$ref": "#/definitions/actionItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/actionItem"
}
}
]
},
"Resource": {
"oneOf": [
{
"$ref": "#/definitions/resourceItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/resourceItem"
}
}
]
},
"NotResource": {
"oneOf": [
{
"$ref": "#/definitions/resourceItem"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/resourceItem"
}
}
]
},
"Condition": {
"$ref": "#/definitions/conditions"
}
},
"oneOf": [
{
"required": [
"Action",
"Effect",
"Resource"
]
}, {
"required": [
"Action",
"Effect",
"NotResource"
]
}, {
"required": [
"Effect",
"NotAction",
"Resource"
]
}, {
"required": [
"Effect",
"NotAction",
"NotResource"
]
}
]
}
]
}
},
"required": [
"Version",
"Statement"
]
}

View File

@ -10,10 +10,11 @@
"author": "Giorgio Regni", "author": "Giorgio Regni",
"license": "Apache-2.0", "license": "Apache-2.0",
"bugs": { "bugs": {
"url": "https://github.com/scality/Arsenal/issues" "url": "https://github.com/scality/Arsenal/issues"
}, },
"homepage": "https://github.com/scality/Arsenal#readme", "homepage": "https://github.com/scality/Arsenal#readme",
"dependencies": { "dependencies": {
"ajv": "^4.1.3",
"utf8": "~2.1.1" "utf8": "~2.1.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,240 @@
'use strict'; // eslint-disable-line strict
const assert = require('assert');
const policyValidator = require('../../../lib/policy/policyValidator');
const errors = require('../../../lib/errors');
const validateUserPolicy = policyValidator.validateUserPolicy;
const successRes = { error: null, valid: true };
const samplePolicy = {
Version: '2012-10-17',
Statement: {
Sid: 'FooBar1234',
Effect: 'Allow',
Action: 's3:PutObject',
Resource: 'arn:aws:s3:::my_bucket/uploads/widgetco/*',
Condition: { NumericLessThanEquals: { 's3:max-keys': '10' } },
},
};
let policy;
function failRes(error) {
return { error, valid: false };
}
function check(input, expected) {
const result = validateUserPolicy(JSON.stringify(input));
assert.deepStrictEqual(result, expected);
}
beforeEach(() => {
policy = JSON.parse(JSON.stringify(samplePolicy));
});
describe('Policies validation - Invalid JSON', () => {
it('should return error for invalid JSON', () => {
const result = validateUserPolicy('{"Version":"2012-10-17",' +
'"Statement":{"Effect":"Allow""Action":"s3:PutObject",' +
'"Resource":"arn:aws:s3*"}}');
assert.deepStrictEqual(result, failRes(errors.PolicyInvalidJSON));
});
});
describe('Policies validation - Version', () => {
it('should validate with version date 2012-10-17', () => {
check(policy, successRes);
});
it('should return error for other dates', () => {
policy.Version = '2012-11-17';
check(policy, failRes(errors.InvalidPolicyDocument));
});
it('should return error if Version field is missing', () => {
policy.Version = undefined;
check(policy, failRes(errors.MissingPolicyVersion));
});
});
describe('Policies validation - Statement', () => {
it('should succeed for a valid object', () => {
check(policy, successRes);
});
it('should succeed for a valid array', () => {
policy.Statement = [
{
Effect: 'Allow',
Action: 's3:PutObject',
Resource: 'arn:aws:s3:::my_bucket/uploads/widgetco/*',
},
{
Effect: 'Deny',
Action: 's3:DeleteObject',
Resource: 'arn:aws:s3:::my_bucket/uploads/widgetco/*',
},
];
check(policy, successRes);
});
it('should return an error for undefined', () => {
policy.Statement = undefined;
check(policy, failRes(errors.MissingPolicyStatement));
});
it('should return an error for an empty list', () => {
policy.Statement = [];
check(policy, failRes(errors.InvalidPolicyDocument));
});
it('should return an error for an empty object', () => {
policy.Statement = {};
check(policy, failRes(errors.MissingPolicyAction));
});
it('should return an error for missing a required field - Action', () => {
delete policy.Statement.Action;
check(policy, failRes(errors.MissingPolicyAction));
});
it('should return an error for missing a required field - Effect', () => {
delete policy.Statement.Effect;
check(policy, failRes(errors.MissingPolicyEffect));
});
it('should return an error for missing a required field - Resource', () => {
delete policy.Statement.Resource;
check(policy, failRes(errors.MissingPolicyResource));
});
it('should return an error for missing multiple required fields', () => {
delete policy.Statement.Effect;
delete policy.Statement.Resource;
check(policy, failRes(errors.MissingPolicyEffect));
});
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', () => {
it('should succeed if Sid is any alphanumeric string', () => {
check(policy, successRes);
});
it('should fail if Sid is not a string', () => {
policy.Statement.Sid = 1234;
check(policy, failRes(errors.InvalidPolicyDocument));
});
});
describe('Policies validation - Statement::Effect_block', () => {
it('should succeed for Allow', () => {
check(policy, successRes);
});
it('should succeed for Deny', () => {
policy.Statement.Effect = 'Deny';
check(policy, successRes);
});
it('should fail for strings other than Allow/Deny', () => {
policy.Statement.Effect = 'Reject';
check(policy, failRes(errors.InvalidPolicyDocument));
});
it('should fail if Effect is not a string', () => {
policy.Statement.Effect = 1;
check(policy, failRes(errors.InvalidPolicyDocument));
});
});
describe('Policies validation - Statement::Action_block', () => {
it('should succeed for foo:bar', () => {
policy.Statement.Action = 'foo:bar';
check(policy, successRes);
});
it('should succeed for foo:*', () => {
policy.Statement.Action = 'foo:*';
check(policy, successRes);
});
it('should succeed for *', () => {
policy.Statement.Action = '*';
check(policy, successRes);
});
it('should fail for **', () => {
policy.Statement.Action = '**';
check(policy, failRes(errors.InvalidPolicyDocument));
});
it('should fail for foobar', () => {
policy.Statement.Action = 'foobar';
check(policy, failRes(errors.InvalidPolicyDocument));
});
});
describe('Policies validation - Statement::Resource_block', () => {
it('should succeed for arn:aws:s3:::*', () => {
policy.Statement.Resource = 'arn:aws:s3:::*';
check(policy, successRes);
});
it('should succeed for arn:aws:s3:::test/home/${aws:username}', () => {
policy.Statement.Resource = 'arn:aws:s3:::test/home/${aws:username}';
check(policy, successRes);
});
it('should succeed for arn:aws:ec2:us-west-1:1234qwerty:volume/*', () => {
policy.Statement.Resource = 'arn:aws:ec2:us-west-1:1234qwerty:volume/*';
check(policy, successRes);
});
it('should succeed for *', () => {
policy.Statement.Resource = '*';
check(policy, successRes);
});
it('should fail for ec2:us-west-1:1234qwerty:volume/*', () => {
policy.Statement.Resource = 'ec2:us-west-1:1234qwerty:volume/*';
check(policy, failRes(errors.InvalidPolicyDocument));
});
});
describe('Policies validation - Statement::Condition_block', () => {
it('should succeed for single Condition', () => {
check(policy, successRes);
});
it('should succeed for multiple Conditions', () => {
policy.Statement.Condition = {
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(errors.InvalidPolicyDocument));
});
it('should fail for an invalid Condition', () => {
policy.Statement.Condition = {
SomethingLike: { 's3:prefix': ['Development/*'] },
};
check(policy, failRes(errors.InvalidPolicyDocument));
});
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(errors.InvalidPolicyDocument));
});
});