├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .mocharc.js ├── CHANGELOG.md ├── DEVELOPMENT.md ├── LICENSE.md ├── Makefile ├── README.md ├── package-lock.json ├── package.json ├── src ├── arrays.ts ├── condition │ ├── condition.ts │ ├── deserialiser.ts │ └── serialiser.ts ├── index.ts ├── normaliser.ts ├── policy │ ├── deserialiser.ts │ ├── policy.ts │ ├── serialiser.ts │ └── sid-uniqueness.ts ├── principals │ ├── account.ts │ ├── anonymous.ts │ ├── arn.ts │ ├── base.ts │ ├── cloudfront.ts │ ├── deserialiser.ts │ ├── federated.ts │ ├── role.ts │ ├── root-account.ts │ ├── serialiser.ts │ ├── service.ts │ ├── user.ts │ └── wildcard.ts └── statement │ ├── deserialiser.ts │ ├── serialiser.ts │ └── statement.ts ├── tests ├── arrays.spec.ts ├── condition │ ├── condition.spec.ts │ ├── deserialiser.spec.ts │ └── serialiser.spec.ts ├── normaliser.spec.ts ├── policy │ ├── deserialiser.spec.ts │ ├── policy.spec.ts │ ├── serialiser.spec.ts │ └── sid-uniqueness.spec.ts ├── principals │ ├── account.spec.ts │ ├── anonymous.spec.ts │ ├── arn.spec.ts │ ├── cloudfront.spec.ts │ ├── deserialiser.spec.ts │ ├── federated.spec.ts │ ├── role.spec.ts │ ├── root-account.spec.ts │ ├── serialiser.spec.ts │ ├── service.spec.ts │ ├── user.spec.ts │ └── wildcard.spec.ts └── statement │ ├── deserialiser.spec.ts │ ├── serialiser.spec.ts │ └── statement.spec.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'root': true, 3 | 'ignorePatterns': ['!.*rc.js'], 4 | 'env': { 5 | 'browser': true, 6 | 'es2021': true, 7 | 'mocha': true, 8 | }, 9 | 'extends': [ 10 | 'google', 11 | ], 12 | 'parser': '@typescript-eslint/parser', 13 | 'parserOptions': { 14 | 'ecmaVersion': 12, 15 | 'sourceType': 'module', 16 | }, 17 | 'plugins': [ 18 | '@typescript-eslint', 19 | ], 20 | 'rules': { 21 | 'max-len': ['error', {'code': 120}], 22 | 'no-multi-str': 'off', 23 | 'require-jsdoc': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | name: build (node ${{ matrix.node }}) 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | node: [18, 20, 22] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node }} 25 | registry-url: 'https://registry.npmjs.org' 26 | - name: Install dependencies 27 | run: npm ci --ignore-scripts 28 | - name: Run tests 29 | run: npm test 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: ['v[0-9]+.[0-9]+.[0-9]+*'] # only a valid semver tag 6 | 7 | jobs: 8 | publish-package: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version-file: package.json 18 | registry-url: 'https://registry.npmjs.org' 19 | - name: Install dependencies 20 | run: npm ci --ignore-scripts 21 | - name: Publish package 22 | run: npm publish --access public 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | 4 | #Visual Studio Code settings 5 | /.vscode/ 6 | 7 | #Jetbrains settings 8 | .idea 9 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extension: ['ts'], 3 | spec: './tests/**/*.spec.ts', 4 | require: 'ts-node/register', 5 | }; 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0 4 | 5 | * Add support for Policy `Id` to the `PolicyDocument` ([#29](https://github.com/thinkinglabs/aws-iam-policy/issues/29)). 6 | 7 | * Move the policy `Version` at the top of the serialised JSON policy document. 8 | 9 | * Drop the minimal Node.js version requirement 10 | 11 | * Replace `npm` by [`volta`](https://volta.sh/) as Node.js version manager. 12 | 13 | * Introduce a matrix build on Node.js v18, v20, v22 ([#30](https://github.com/thinkinglabs/aws-iam-policy/issues/30)) 14 | 15 | ## 3.0.0 (7 September 2024) 16 | 17 | * Extend the validation with policy document size quota ([#6](https://github.com/thinkinglabs/aws-iam-policy/issues/6)). 18 | 19 | * Extend the validation with the valid `Sid` values for IAM policy, KMS key policy, S3 bucket policy and SecretsManager secret policy ([#5](https://github.com/thinkinglabs/aws-iam-policy/issues/5)). 20 | 21 | * Fix a bug where the root account principal was deserialised as an `ArnPrincipal` ([#26](https://github.com/thinkinglabs/aws-iam-policy/issues/26)). 22 | 23 | :rotating_light: **BREAKING CHANGE** 24 | 25 | * Consolidate `PolicyDocument.validateForAnyPolicy`, `PolicyDocument.validateForIndentityPolicy` and `PolicyDocument.validateForResourcePolicy` into `PolicyDocument.validate(PolicyType)` where `PolicyType` accepts `IAM`, `KMS`, `S3` and `SecretsManager` ([#6](https://github.com/thinkinglabs/aws-iam-policy/issues/6)). 26 | 27 | * Add support for the role principal [#16](https://github.com/thinkinglabs/aws-iam-policy/issues/16) 28 | 29 | Replaces `ArnPrincipal` used for an IAM Role with ARN `arn:aws:iam::123456789000:role/a/path/a_role`. 30 | 31 | Serialising `ArnPrincipal` will still produce a valid IAM Policy Statement AWS Principal JSON fragment `{"AWS": "arn:aws:iam::123456789000:role/a/path/a_role"}`. 32 | 33 | Deserialising an AWS Principal JSON fragment `{ "AWS": "arn:aws:iam::123456789000:role/a/path/a_role" }` will now produce a `RolePrincipal` instead of an `ArnPrincipal`. 34 | 35 | * Add support for the user principal [#16](https://github.com/thinkinglabs/aws-iam-policy/issues/16) 36 | 37 | Replaces `ArnPrincipal` used for an IAM User with ARN `arn:aws:iam::123456789000:user/a/path/a_user`. 38 | 39 | Serialising `ArnPrincipal` will still produce a valid IAM Policy Statement AWS Principal JSON fragment `{"AWS": "arn:aws:iam::123456789000:user/a/path/a_user"}`. 40 | 41 | Deserialising an AWS Principal JSON fragment `{ "AWS": "arn:aws:iam::123456789000:user/a/path/a_user" }` will now produce a `UserPrincipal` instead of an `ArnPrincipal`. 42 | 43 | ## 2.7.0 (11 August 2024) 44 | 45 | * Add support for the CloudFront principal [#24](https://github.com/thinkinglabs/aws-iam-policy/issues/36) reported and fixed ([#25](https://github.com/thinkinglabs/aws-iam-policy/pull/25)) by [@araguacaima](https://github.com/araguacaima) 46 | 47 | ## 2.6.1 (30 June 2024) 48 | 49 | * Export the `WildcardPrincipal` ([#23](https://github.com/thinkinglabs/aws-iam-policy/pull/23) by [@gabegorelick](https://github.com/gabegorelick)). 50 | 51 | ## 2.6.0 (26 November 2023) 52 | 53 | * Add support for the wildcard principal ([#22](https://github.com/thinkinglabs/aws-iam-policy/issues/22) reported by [@gabegorelick](https://github.com/gabegorelick)). 54 | 55 | ## 2.5.1 (19 May 2022) 56 | 57 | * Fix the GitHub Action that publishes the npm package to include the prepublish typescript compilation 58 | 59 | ## 2.5.0 (19 May 2022) [unusable!!] 60 | 61 | * Add support for Federated principals ([#14](https://github.com/thinkinglabs/aws-iam-policy/issues/14)) by [@ringods](https://github.com/ringods) 62 | 63 | ## 2.4.0 (12 April 2022) 64 | 65 | * Add support for `NotAction` ([#2](https://github.com/thinkinglabs/aws-iam-policy/issues/2)), `NotPrincipal` ([#3](https://github.com/thinkinglabs/aws-iam-policy/issues/3)), `NotResource` ([#4](https://github.com/thinkinglabs/aws-iam-policy/issues/4)) by [@ringods](https://github.com/ringods) 66 | 67 | ## 2.3.0 (1 February 2022) 68 | 69 | * Add support for string value for `Condition` key values ([#9](https://github.com/thinkinglabs/aws-iam-policy/issues/9)) 70 | * Add support for string value for `Principal` type values ([#10](https://github.com/thinkinglabs/aws-iam-policy/issues/10)) 71 | 72 | ## 2.2.0 (23 January 2022) 73 | 74 | * Add support for string value for `Action` and `Resource` ([#7](https://github.com/thinkinglabs/aws-iam-policy/issues/7)) by [@danopia](https://github.com/danopia) 75 | 76 | ## 2.1.0 (3 June 2021) 77 | 78 | * Make `PolicyDocument.addStatement(Statement)` public 79 | 80 | ## 2.0.0 (19 May 2021) 81 | 82 | * Add support for Condition 83 | 84 | This adds an object model for the Condition element of an IAM Policy 85 | Statement. To build a Statement having a Condition: 86 | 87 | ```typescript 88 | new Statement({ 89 | effect: "Deny", 90 | ... 91 | conditions: [ 92 | new Condition("StringNotLike", "aws:userId", ["userId1", "userId2", ...]), 93 | ] 94 | }) 95 | ``` 96 | 97 | ## 1.0.2 (22 March 2021) 98 | 99 | * Initial release 100 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Guidelines 2 | 3 | ## Setup 4 | 5 | The Node.js version is managed by [`volta`](https://volta.sh/). 6 | 7 | Volta will automatically download the pinned Node.js version from 8 | `package.json`. 9 | 10 | Therefore, the project does not have a `.nvmrc` file. 11 | 12 | ## Publishing a new package 13 | 14 | To create a new version of the package, we run the following command manually: 15 | 16 | ```bash 17 | make release version=".." 18 | ``` 19 | 20 | This will bump the version in the `package.json` and `package-lock.json` files, 21 | commit it to git and tag it with the same version, and push to the remote repo. 22 | 23 | Both the commit and the tag are pushed and the Github Actions workflow will kick 24 | in to publish the package to the NPM Registry. 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ThinkingLabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | .PHONY: tag install test 3 | 4 | release: ## Release a version 5 | npm version ${version} -m "Bump v${version}" 6 | 7 | install: ## Install dependencies 8 | npm install 9 | 10 | test: ## Execute tests 11 | npm test 12 | 13 | help: 14 | @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-iam-policy [![Build Status](https://travis-ci.org/thinkinglabs/aws-iam-policy.svg?branch=main)](https://travis-ci.org/thinkinglabs/aws-iam-policy) 2 | 3 | A Node.js package for working with AWS IAM Policy documents. 4 | 5 | The primary reasons for creating the library were: 6 | 7 | - simplify the declaration of IAM identity policies as well as resource 8 | policies for S3 Bucket, KMS Keys or Secrets Manager secrets via coding that 9 | are created with the [Pulumi](https://www.pulumi.com/) provisioning tool. 10 | - simplify the unit testing of those policies and more specifically testing of 11 | single policy statements. 12 | 13 | ## Requirements 14 | 15 | Node.js lts/gallium (16.x) 16 | 17 | ## Features 18 | 19 | - Reading/writing AWS IAM Policy JSON documents. 20 | - An object model for building an IAM Policy document. 21 | - Validating an IAM Policy document for identity- or resource-based policies. 22 | - Validating the uniqueness of `Sid` within the scope of an IAM Policy document 23 | when adding Statements. 24 | - Retrieval of Policy Statements by `Sid`. 25 | 26 | ## Documentation 27 | 28 | Install the package. 29 | 30 | ```bash 31 | npm install --save-dev @thinkinglabs/aws-iam-policy 32 | ``` 33 | 34 | Create a policy document. 35 | 36 | ```typescript 37 | import * as iam from '@thinkinglabs/aws-iam-policy'; 38 | 39 | function kmsKeyPolicy(accountId: string, keyAdminArns: string[], keyUserArns: string[]) { 40 | return new iam.PolicyDocument([ 41 | new iam.Statement({ 42 | sid: 'Enable IAM User Permissions', 43 | effect: 'Allow', 44 | principals: [new iam.RootAccountPrincipal(accountId)], 45 | actions: ['kms:*'], 46 | resources: ['*'], 47 | }), 48 | new iam.Statement({ 49 | sid: 'Allow access for Key Administrators', 50 | effect: 'Allow', 51 | principals: keyAdminArns.map((arn) => new iam.ArnPrincipal(arn)), 52 | actions: ['kms:*'], 53 | resources: ['*'], 54 | }), 55 | new iam.Statement({ 56 | sid: 'Allow use of the key', 57 | effect: 'Allow', 58 | principals: keyUserArns.map((arn) => new iam.ArnPrincipal(arn)), 59 | actions: [ 60 | 'kms:Encrypt', 61 | 'kms:Decrypt', 62 | 'kms:ReEncrypt*', 63 | 'kms:GenerateDataKey*', 64 | 'kms:DescribeKey', 65 | ], 66 | resources: ['*'], 67 | }), 68 | ]).json; 69 | }); 70 | ``` 71 | 72 | Add a `Statement` to an existing policy document. 73 | 74 | ```typescript 75 | const policy = new iam.PolicyDocument(); 76 | policy.addStatements(new iam.Statement({ 77 | sid: 'Enable IAM User Permissions', 78 | effect: 'Allow', 79 | principals: [new iam.RootAccountPrincipal(accountId)], 80 | actions: ['kms:*'], 81 | resources: ['*'], 82 | }); 83 | ``` 84 | 85 | Unit testing a statement from a policy document. You can retrieve a single 86 | statement using the Sid of that statement. 87 | 88 | ```typescript 89 | import {expect} from 'chai'; 90 | import * as iam from '@thinkinglabs/aws-iam-policy'; 91 | 92 | describe('kms key policy', function() { 93 | const accountId = '123456789012'; 94 | const keyAdminArns = [ 95 | `arn:aws:iam::${accountId}:role/admin1`, 96 | `arn:aws:iam::${accountId}:role/admin2`, 97 | ]; 98 | const keyUsers = [ 99 | `arn:aws:iam::${accountId}:role/user1`, 100 | ]; 101 | const policy = sut.kmsKeyPolicy(accountId, keyAdminArns, keyUserArns); 102 | 103 | it('should enable IAM User permissions', function() { 104 | const statement = policy.getStatement('Enable IAM User Permissions'); 105 | 106 | expect(statement).to.deep.equal(new iam.Statement({ 107 | actions: ['kms:*'], 108 | effect: 'Allow', 109 | principals: [new iam.RootAccountPrincipal('123456789012')], 110 | resources: ['*'], 111 | sid: 'Enable IAM User Permissions', 112 | })); 113 | }); 114 | } 115 | ``` 116 | 117 | Serialising to and from JSON. 118 | 119 | ```typescript 120 | const policy = new iam.PolicyDocument(); 121 | const json = policy.json; 122 | const newPolicy = iam.PolicyDocument.fromJson(json); 123 | ``` 124 | 125 | Supports different principals. 126 | 127 | ```typescript 128 | // "Principal": {"Service": ["ec2.amazonaws.com"]} 129 | const servicePrincipal = new iam.ServicePrincipal('ec2.amazonaws.com'); 130 | 131 | // "Principal": {"AWS": ["arn:aws:iam::123456789012:user/a/path/user-name"]} 132 | const userPrincipal = new iam.UserPrincipal('123456789012', 'user-name', '/a/path/') 133 | 134 | // "Principal": {"AWS": ["arn:aws:iam::123456789012:role/a/path/role-name"]} 135 | const rolePrincipal = new iam.RolePrincipal('123456789012', 'role-name', '/a/path/') 136 | 137 | // "Principal": {"AWS": ["arn:aws:iam::123456789012:role/role-name"]} 138 | const arnPrincipal = new iam.ArnPrincipal('arn:aws:iam::123456789012:role/role-name'); 139 | 140 | // "Principal": {"AWS": ["arn:aws:iam::123456789012:root"]} 141 | const rootAccountPrincipal = new iam.RootAccountPrincipal('123456789012'); 142 | 143 | // "Principal": {"AWS": ["123456789012"]} 144 | const accountPrincipal = new iam.AccountPrincipal('123456789012'); 145 | 146 | // "Principal": {"AWS": ["*"]} 147 | const anonymousUserPrincipal = new iam.AnonymousUserPrincipal(); 148 | 149 | // "Principal" : "*" 150 | const wildcardPrincipal = new iam.WildcardPrincipal(); 151 | ``` 152 | 153 | Validate a policy document. 154 | 155 | ```typescript 156 | // validate any policy 157 | // when valid returns an empty list 158 | // when invalid returns a list of errors 159 | const errors = policy.validate(); 160 | if (errors) { 161 | throw errors; 162 | } 163 | 164 | // validate an IAM policy document 165 | const errors = policy.validate(PolicyType.IAM); 166 | if (errors) { 167 | throw errors; 168 | } 169 | 170 | //validate a KMS key policy document. 171 | const errors = policy.validate(PolicyType.KMS); 172 | if (errors) { 173 | throw errors; 174 | } 175 | 176 | //validate an S3 bucket policy document. 177 | const errors = policy.validate(PolicyType.S3); 178 | if (errors) { 179 | throw errors; 180 | } 181 | 182 | //validate a SecretsManager secret policy document. 183 | const errors = policy.validate(PolicyType.SecretsManager); 184 | if (errors) { 185 | throw errors; 186 | } 187 | ``` 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@thinkinglabs/aws-iam-policy", 3 | "version": "3.1.0", 4 | "description": "TypeScript library for handling AWS IAM Policy documents", 5 | "homepage": "https://github.com/thinkinglabs/aws-iam-policy", 6 | "repository": "git://github.com/thinkinglabs/aws-iam-policy.git", 7 | "main": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "scripts": { 10 | "lint": "eslint ./src/**/* ./tests/**/*", 11 | "test": "npm run lint && mocha", 12 | "preversion": "npm test", 13 | "postversion": "git push origin main --follow-tags", 14 | "prepublishOnly": "npm test && tsc" 15 | }, 16 | "files": [ 17 | "dist", 18 | "README.md", 19 | "LICENSE.md" 20 | ], 21 | "keywords": [ 22 | "aws", 23 | "iam", 24 | "policy" 25 | ], 26 | "author": "ThinkingLabs", 27 | "license": "MIT", 28 | "volta": { 29 | "node": "22.9.0" 30 | }, 31 | "devDependencies": { 32 | "@types/chai": "^4.2.15", 33 | "@types/mocha": "^8.2.0", 34 | "@types/node": "^10.0.0", 35 | "@typescript-eslint/eslint-plugin": "^4.14.2", 36 | "@typescript-eslint/parser": "^4.14.2", 37 | "chai": "^4.2.0", 38 | "eslint": "^7.19.0", 39 | "eslint-config-google": "^0.14.0", 40 | "mocha": "^8.2.1", 41 | "ts-node": "^9.1.1", 42 | "typescript": "^4.1.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/arrays.ts: -------------------------------------------------------------------------------- 1 | 2 | export function parseArray(obj: any): string[] { 3 | if (obj === undefined) { 4 | return []; 5 | } 6 | if (typeof obj === 'string') { 7 | return [obj]; 8 | } 9 | if (Array.isArray(obj)) { 10 | if (isArrayOfStrings(obj)) { 11 | return obj; 12 | } 13 | throw new Error('Unsupported type: expecting an array of strings'); 14 | } 15 | throw new Error('Unsupported type: expecting an array or a string'); 16 | } 17 | 18 | function isArrayOfStrings(obj: any[]) { 19 | return obj.every((element) => typeof element === 'string'); 20 | } 21 | -------------------------------------------------------------------------------- /src/condition/condition.ts: -------------------------------------------------------------------------------- 1 | 2 | export class Condition { 3 | public readonly operator: string; 4 | public readonly key: string; 5 | public readonly values: string[]; 6 | 7 | constructor(operator: string, key: string, values: string[]) { 8 | if (operator === '') { 9 | throw new Error('operator should not be empty'); 10 | } 11 | if (key === '') { 12 | throw new Error('key should not be empty'); 13 | } 14 | if (values.length === 0) { 15 | throw new Error('values should not be empty'); 16 | } 17 | if (values.filter((value) => value === '').length > 0) { 18 | throw new Error('values should not have an empty string'); 19 | } 20 | this.operator = operator; 21 | this.key = key; 22 | this.values = values; 23 | } 24 | 25 | toJSON() { 26 | const result: { [operator: string]: { [key:string]: string[] }; } = {}; 27 | result[this.operator] = {}; 28 | result[this.operator][this.key] = this.values; 29 | return result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/condition/deserialiser.ts: -------------------------------------------------------------------------------- 1 | import {Condition} from './condition'; 2 | import {parseArray} from '../arrays'; 3 | 4 | export class ConditionJSONDeserialiser { 5 | static fromJSON(input: any): Condition[] { 6 | return parseCondition(input); 7 | 8 | function parseCondition(input: any) { 9 | if (input === undefined) { 10 | return []; 11 | } 12 | 13 | if (typeof input !== 'object') { 14 | throw new Error( 15 | `Unsupported Condition type ${typeof input}: ` + 16 | `expecting an object {[operator:string]: {[key:string]:string[]}}`); 17 | } 18 | 19 | if (Array.isArray(input)) { 20 | throw new Error( 21 | `Unsupported Condition type array: ` + 22 | `expecting an object {[operator:string]: {[key:string]:string[]}}`); 23 | } 24 | 25 | const result: Condition[] = Object.keys(input).flatMap((operator: any) => { 26 | const operatorValue = input[operator]; 27 | 28 | return parseOperator(operator, operatorValue); 29 | }); 30 | 31 | return result; 32 | } 33 | 34 | function parseOperator(operator: string, operatorValue: any) { 35 | if (typeof operatorValue !== 'object') { 36 | throw new Error( 37 | `Unsupported Condition operator type ${typeof operatorValue}: ` + 38 | 'expecting an object {[key:string]:string[]}'); 39 | } 40 | 41 | if (Array.isArray(operatorValue)) { 42 | throw new Error( 43 | 'Unsupported Condition operator type array: ' + 44 | 'expecting an object {[key:string]:string[]}'); 45 | } 46 | 47 | return Object.keys(operatorValue).map((key) => { 48 | const values = operatorValue[key]; 49 | 50 | return new Condition(operator, key, parseArray(values)); 51 | }); 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/condition/serialiser.ts: -------------------------------------------------------------------------------- 1 | import {Condition} from './condition'; 2 | 3 | export class ConditionJSONSerialiser { 4 | static toJSON(conditions: Condition[]) { 5 | if (conditions.length == 0) { 6 | return undefined; 7 | } 8 | return merge(conditions); 9 | 10 | function merge(conditions: Condition[]) { 11 | const result: { [operator: string]: {[key: string]: string[]}} = {}; 12 | 13 | conditions.forEach((condition) => { 14 | const json = condition.toJSON(); 15 | 16 | Object.keys(json).forEach((operator) => { 17 | mergeOperator(result, json, operator); 18 | }); 19 | }); 20 | return result; 21 | } 22 | 23 | function mergeOperator( 24 | result: {[operator: string]: {[key: string]: string[]}}, 25 | json: {[operator: string]: {[key: string]: string[]}}, 26 | operator: string, 27 | ) { 28 | const jsonOperator = json[operator]; 29 | if (operator in result) { 30 | Object.keys(jsonOperator).forEach((key) => { 31 | mergeKey(result[operator], jsonOperator, key); 32 | }); 33 | } else { 34 | result[operator] = jsonOperator; 35 | } 36 | } 37 | 38 | function mergeKey( 39 | resultOperator: {[key:string]: string[]}, 40 | jsonOperator: {[key:string]: string[]}, 41 | key: string, 42 | ) { 43 | const values = jsonOperator[key]; 44 | if (key in resultOperator) { 45 | resultOperator[key].push(...values); 46 | } else { 47 | resultOperator[key] = values; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './policy/policy'; 2 | export * from './statement/statement'; 3 | export * from './principals/base'; 4 | export * from './principals/anonymous'; 5 | export * from './principals/arn'; 6 | export * from './principals/service'; 7 | export * from './principals/account'; 8 | export * from './principals/root-account'; 9 | export * from './principals/federated'; 10 | export * from './principals/wildcard'; 11 | export * from './principals/cloudfront'; 12 | export * from './condition/condition'; 13 | export * from './principals/user'; 14 | export * from './principals/role'; 15 | -------------------------------------------------------------------------------- /src/normaliser.ts: -------------------------------------------------------------------------------- 1 | 2 | function normalise(obj: any): any | undefined { 3 | if (obj === undefined) { 4 | return undefined; 5 | } 6 | if (Array.isArray(obj)) { 7 | if (obj.length === 0) { 8 | return undefined; 9 | } 10 | return obj; 11 | } 12 | if (typeof (obj) === 'object') { 13 | if (Object.keys(obj).length === 0) { 14 | return undefined; 15 | } 16 | } 17 | return obj; 18 | } 19 | 20 | export {normalise}; 21 | -------------------------------------------------------------------------------- /src/policy/deserialiser.ts: -------------------------------------------------------------------------------- 1 | import {Statement} from '../statement/statement'; 2 | import {PolicyDocument} from './policy'; 3 | 4 | export class PolicyDocumentJSONDeserialiser { 5 | static fromJSON(obj: any) { 6 | if (obj.Id !== undefined && typeof obj.Id !== 'string') { 7 | throw new Error('Unexpected type: Id must be a string'); 8 | } 9 | 10 | const statements = obj.Statement; 11 | if (statements === undefined) { 12 | return new PolicyDocument(undefined, obj.Id); 13 | } 14 | if (!Array.isArray(statements)) { 15 | throw new Error('Unexpected type: Statement must be an array'); 16 | } 17 | 18 | const result = new PolicyDocument( 19 | statements.map((statement: any) => Statement.fromJSON(statement)), 20 | obj.Id, 21 | ); 22 | return result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/policy/policy.ts: -------------------------------------------------------------------------------- 1 | import {PolicyDocumentJSONSerialiser} from './serialiser'; 2 | import {PolicyDocumentJSONDeserialiser} from './deserialiser'; 3 | import {Statement} from '../statement/statement'; 4 | import {SidUniquenessValidator} from './sid-uniqueness'; 5 | 6 | export class PolicyDocument { 7 | private _id?: string; 8 | private _statements: Statement[] = []; 9 | 10 | constructor(statements?: Statement[], id?: string) { 11 | this.addStatements(...statements || []); 12 | this._id = id; 13 | } 14 | 15 | get id() { 16 | return this._id; 17 | } 18 | 19 | get isEmpty() { 20 | return this.statementCount === 0; 21 | } 22 | 23 | get statements() { 24 | return this._statements; 25 | } 26 | 27 | get statementCount() { 28 | return this._statements.length; 29 | } 30 | 31 | public addStatements(...statements: Statement[]) { 32 | statements.forEach((statement) => { 33 | if (!new SidUniquenessValidator(this._statements).validate(statement)) { 34 | throw new Error(`Non-unique Sid "${statement.sid}"`); 35 | } 36 | this._statements.push(statement); 37 | }); 38 | } 39 | 40 | getStatement(sid: string): Statement | undefined { 41 | return this._statements.find((stmt) => stmt.sid === sid); 42 | } 43 | 44 | 45 | get object() { 46 | return PolicyDocumentJSONSerialiser.toJSON(this); 47 | } 48 | 49 | get json() { 50 | return JSON.stringify(this.object); 51 | } 52 | 53 | static fromJson(json: string) { 54 | const obj = JSON.parse(json); 55 | return PolicyDocumentJSONDeserialiser.fromJSON(obj); 56 | } 57 | 58 | validate(policyType?: PolicyType) { 59 | const errors: string[] = []; 60 | this._statements.forEach((stmt) => { 61 | errors.push(...stmt.validateForAnyPolicy()); 62 | }); 63 | if (policyType === undefined) { 64 | return errors; 65 | } 66 | if (policyType === PolicyType.IAM) { 67 | if (this._id) { 68 | errors.push('Policy Id is not allowed for identity-based policies'); 69 | } 70 | 71 | this._statements.forEach((stmt) => { 72 | errors.push(...stmt.validateForIdentityPolicy()); 73 | }); 74 | const doc = this.json; 75 | if (doc.length > 6144) { 76 | errors.push(`The size of an IAM policy document (${doc.length}) should not exceed 6.144 characters.`); 77 | } 78 | } 79 | if (policyType === PolicyType.KMS || 80 | policyType === PolicyType.S3 || 81 | policyType === PolicyType.SecretsManager) { 82 | this._statements.forEach((stmt) => { 83 | errors.push(...stmt.validateForResourcePolicy(policyType)); 84 | }); 85 | const doc = this.json; 86 | if (policyType === PolicyType.KMS && doc.length > 32*1024) { 87 | errors.push(`The size of a KMS key policy document (${doc.length}) should not exceed 32kB.`); 88 | } 89 | if (policyType === PolicyType.S3 && doc.length > 20*1024) { 90 | errors.push(`The size of an S3 bucket policy document (${doc.length}) should not exceed 20kB.`); 91 | } 92 | if (policyType === PolicyType.SecretsManager && doc.length > 20*1024) { 93 | errors.push(`The size of a SecretsManager secret policy document (${doc.length}) should not exceed 20kB.`); 94 | } 95 | } 96 | return errors; 97 | } 98 | } 99 | 100 | /* eslint-disable no-unused-vars */ 101 | export enum PolicyType { 102 | IAM, 103 | KMS, 104 | S3, 105 | SecretsManager, 106 | } 107 | /* eslint-enable no-unused-vars */ 108 | -------------------------------------------------------------------------------- /src/policy/serialiser.ts: -------------------------------------------------------------------------------- 1 | import {PolicyDocument} from './policy'; 2 | 3 | export class PolicyDocumentJSONSerialiser { 4 | static toJSON(policy: PolicyDocument) { 5 | if (policy.isEmpty) { 6 | return undefined; 7 | } 8 | return { 9 | Version: '2012-10-17', 10 | ...(policy.id ? {Id: policy.id} : {}), 11 | Statement: policy.statements.map((stmt) => stmt.toJSON()), 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/policy/sid-uniqueness.ts: -------------------------------------------------------------------------------- 1 | import {Statement} from '../statement/statement'; 2 | 3 | class SidUniquenessValidator { 4 | private statements: Statement[]; 5 | constructor(statements: Statement[]) { 6 | this.statements = statements; 7 | } 8 | 9 | validate(newStatement: Statement) { 10 | if (newStatement.sid === undefined) { 11 | return true; 12 | } 13 | const found = this.statements.find((stmt) => { 14 | return stmt.sid !== undefined && 15 | stmt.sid === newStatement.sid; 16 | }); 17 | return found === undefined; 18 | } 19 | } 20 | 21 | export {SidUniquenessValidator}; 22 | -------------------------------------------------------------------------------- /src/principals/account.ts: -------------------------------------------------------------------------------- 1 | import {AbstractBasePrincipal} from './base'; 2 | 3 | class AccountPrincipal extends AbstractBasePrincipal { 4 | private accountId: string; 5 | constructor(accountId: string) { 6 | super(); 7 | this.accountId = accountId; 8 | } 9 | 10 | toJSON() { 11 | return {AWS: [this.accountId]}; 12 | } 13 | 14 | static validate(input: string): AccountPrincipal | undefined { 15 | const regexp = new RegExp('^[0-9]{12}$'); 16 | const result = regexp.exec(input) as RegExpExecArray; 17 | return result ? new AccountPrincipal(result[0]) : undefined; 18 | } 19 | } 20 | 21 | export {AccountPrincipal}; 22 | -------------------------------------------------------------------------------- /src/principals/anonymous.ts: -------------------------------------------------------------------------------- 1 | import {ArnPrincipal} from './arn'; 2 | 3 | class AnonymousUserPrincipal extends ArnPrincipal { 4 | constructor() { 5 | super('*'); 6 | } 7 | 8 | static validate(input: string): AnonymousUserPrincipal | undefined { 9 | const regex = new RegExp('^\\*$'); 10 | const result = regex.exec(input) as RegExpExecArray; 11 | return result ? new AnonymousUserPrincipal() : undefined; 12 | } 13 | } 14 | 15 | export {AnonymousUserPrincipal}; 16 | -------------------------------------------------------------------------------- /src/principals/arn.ts: -------------------------------------------------------------------------------- 1 | import {AbstractBasePrincipal} from './base'; 2 | 3 | class ArnPrincipal extends AbstractBasePrincipal { 4 | private arn: string; 5 | constructor(arn: string) { 6 | super(); 7 | this.arn = arn; 8 | } 9 | 10 | toJSON() { 11 | return {AWS: this.arn}; 12 | } 13 | 14 | static validate(input: string): ArnPrincipal | undefined { 15 | const regex = new RegExp('^arn:aws:iam::[0-9]{12}:((user|role)/.*|root)$'); 16 | const result = regex.exec(input) as RegExpExecArray; 17 | return result ? new ArnPrincipal(result[0]) : undefined; 18 | } 19 | } 20 | 21 | export {ArnPrincipal}; 22 | -------------------------------------------------------------------------------- /src/principals/base.ts: -------------------------------------------------------------------------------- 1 | export type PrincipalValues = string | string[]; 2 | export type AnonymousValue = string; 3 | 4 | interface Principal { 5 | toJSON(): {[key: string]: PrincipalValues} | AnonymousValue; 6 | } 7 | 8 | abstract class AbstractBasePrincipal implements Principal { 9 | abstract toJSON(): {[key: string]: PrincipalValues}; 10 | } 11 | 12 | export {Principal, AbstractBasePrincipal}; 13 | -------------------------------------------------------------------------------- /src/principals/cloudfront.ts: -------------------------------------------------------------------------------- 1 | import {AbstractBasePrincipal} from './base'; 2 | 3 | class CloudFrontPrincipal extends AbstractBasePrincipal { 4 | private arn: string; 5 | 6 | constructor(arn: string) { 7 | super(); 8 | this.arn = arn; 9 | } 10 | 11 | toJSON() { 12 | return {AWS: this.arn}; 13 | } 14 | 15 | static validate(input: string): CloudFrontPrincipal | undefined { 16 | const regex = new RegExp( 17 | '^arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]{6,14}$'); 18 | const result = regex.exec(input) as RegExpExecArray; 19 | return result ? new CloudFrontPrincipal(result[0]) : undefined; 20 | } 21 | } 22 | 23 | export {CloudFrontPrincipal}; 24 | -------------------------------------------------------------------------------- /src/principals/deserialiser.ts: -------------------------------------------------------------------------------- 1 | import {AnonymousValue, Principal, PrincipalValues} from './base'; 2 | import {UserPrincipal} from './user'; 3 | import {RolePrincipal} from './role'; 4 | import {RootAccountPrincipal} from './root-account'; 5 | import {ArnPrincipal} from './arn'; 6 | import {AnonymousUserPrincipal} from './anonymous'; 7 | import {WildcardPrincipal} from './wildcard'; 8 | import {AccountPrincipal} from './account'; 9 | import {ServicePrincipal} from './service'; 10 | import {FederatedPrincipal} from './federated'; 11 | import {parseArray} from '../arrays'; 12 | import {CloudFrontPrincipal} from './cloudfront'; 13 | 14 | class PrincipalJSONDeserialiser { 15 | static fromJSON(input: { [key: string]: PrincipalValues } | AnonymousValue | undefined): Principal[] { 16 | if (input === undefined) { 17 | return []; 18 | } 19 | 20 | if (input === '*') { 21 | return [new WildcardPrincipal()]; 22 | } 23 | 24 | const result: Principal[] = []; 25 | 26 | const principals = input as { [key: string]: string[] | string }; 27 | const principalTypes = Object.keys(principals); 28 | principalTypes.forEach((principalType) => { 29 | const principalValues = parseArray(principals[principalType]); 30 | switch (principalType) { 31 | case 'AWS': 32 | result.push(...principalValues.map(parseAWSPrincipal)); 33 | break; 34 | case 'Service': 35 | result.push(...principalValues.map(parseServicePrincipal)); 36 | break; 37 | case 'Federated': 38 | result.push(...principalValues.map(parseFederatedPrincipal)); 39 | break; 40 | default: 41 | throw new Error(`Unsupported principal "${principalType}"`); 42 | } 43 | }); 44 | return result; 45 | 46 | function parseAWSPrincipal(value: string) { 47 | let result : Principal | undefined = UserPrincipal.validate(value); 48 | if (result) { 49 | return result; 50 | } 51 | result = RolePrincipal.validate(value); 52 | if (result) { 53 | return result; 54 | } 55 | result = RootAccountPrincipal.validate(value); 56 | if (result) { 57 | return result; 58 | } 59 | result = ArnPrincipal.validate(value); 60 | if (result) { 61 | return result; 62 | } 63 | result = AccountPrincipal.validate(value); 64 | if (result) { 65 | return result; 66 | } 67 | result = AnonymousUserPrincipal.validate(value); 68 | if (result) { 69 | return result; 70 | } 71 | result = CloudFrontPrincipal.validate(value); 72 | if (result) { 73 | return result; 74 | } 75 | throw new Error(`Unsupported AWS principal value "${value}"`); 76 | } 77 | 78 | function parseServicePrincipal(value: string) { 79 | return new ServicePrincipal(value); 80 | } 81 | 82 | function parseFederatedPrincipal(value: string) { 83 | return new FederatedPrincipal(value); 84 | } 85 | } 86 | } 87 | 88 | export {PrincipalJSONDeserialiser}; 89 | -------------------------------------------------------------------------------- /src/principals/federated.ts: -------------------------------------------------------------------------------- 1 | import {AbstractBasePrincipal} from './base'; 2 | 3 | class FederatedPrincipal extends AbstractBasePrincipal { 4 | private identityProvider: string; 5 | constructor(identityProvider: string) { 6 | super(); 7 | this.identityProvider = identityProvider; 8 | } 9 | 10 | toJSON() { 11 | return {Federated: this.identityProvider}; 12 | } 13 | } 14 | 15 | export {FederatedPrincipal}; 16 | -------------------------------------------------------------------------------- /src/principals/role.ts: -------------------------------------------------------------------------------- 1 | import {ArnPrincipal} from './arn'; 2 | 3 | class RolePrincipal extends ArnPrincipal { 4 | private accountId: string; 5 | private userName: string; 6 | private path: string; 7 | constructor(accountId: string, userName: string, path: string = '/') { 8 | super(`arn:aws:iam::${accountId}:role${path}${userName}`); 9 | this.accountId = accountId; 10 | this.userName = userName; 11 | this.path = path; 12 | } 13 | 14 | static validate(arn: string): RolePrincipal | undefined { 15 | const regex = new RegExp( 16 | '^arn:aws:iam::([0-9]{12}):role((/[/a-zA-Z\+=\.@_-]{1,510})?/)([a-zA-Z0-9\+=\.@_-]{1,64})$', 17 | ); 18 | const result = regex.exec(arn) as RegExpExecArray; 19 | return result ? new RolePrincipal(result[1], result[4], result[2]) : undefined; 20 | } 21 | } 22 | 23 | export {RolePrincipal}; 24 | -------------------------------------------------------------------------------- /src/principals/root-account.ts: -------------------------------------------------------------------------------- 1 | import {ArnPrincipal} from './arn'; 2 | 3 | class RootAccountPrincipal extends ArnPrincipal { 4 | constructor(accountId: string) { 5 | super(`arn:aws:iam::${accountId}:root`); 6 | } 7 | 8 | static validate(input: string): RootAccountPrincipal | undefined { 9 | const regex = new RegExp('^arn:aws:iam::([0-9]{12}):root$'); 10 | const result = regex.exec(input) as RegExpExecArray; 11 | return result ? new RootAccountPrincipal(result[1]) : undefined; 12 | } 13 | } 14 | 15 | export {RootAccountPrincipal}; 16 | -------------------------------------------------------------------------------- /src/principals/serialiser.ts: -------------------------------------------------------------------------------- 1 | import {Principal, PrincipalValues} from './base'; 2 | import {normalise} from '../normaliser'; 3 | import {WildcardPrincipal} from './wildcard'; 4 | 5 | class PrincipalJSONSerialiser { 6 | static toJSON(principals: Principal[]) { 7 | return normalise(merge(principals)); 8 | 9 | function merge(principals: Principal[]) { 10 | if (principals[0] instanceof WildcardPrincipal) { 11 | return principals[0].toJSON(); 12 | } 13 | 14 | const result: {[key: string]: PrincipalValues;} = {}; 15 | 16 | principals.forEach((principal) => { 17 | const json = principal.toJSON() as {[key: string]: PrincipalValues}; 18 | Object.keys(json).forEach((key) =>{ 19 | let intermediateResult = result[key]; 20 | const value = json[key]; // string, string[] 21 | if (intermediateResult === undefined) { // undefined 22 | intermediateResult = value; 23 | } else if (typeof(intermediateResult) === 'string') { // string 24 | if (!Array.isArray(value)) { 25 | intermediateResult = [intermediateResult, value]; 26 | } else { 27 | intermediateResult = [intermediateResult, ...value]; 28 | } 29 | } else { // string[] 30 | if (!Array.isArray(value)) { 31 | intermediateResult.push(value); 32 | } else { 33 | intermediateResult.push(...value); 34 | } 35 | } 36 | result[key] = intermediateResult; 37 | }); 38 | }); 39 | return result; 40 | } 41 | } 42 | } 43 | 44 | export {PrincipalJSONSerialiser}; 45 | 46 | -------------------------------------------------------------------------------- /src/principals/service.ts: -------------------------------------------------------------------------------- 1 | import {AbstractBasePrincipal} from './base'; 2 | 3 | export class ServicePrincipal extends AbstractBasePrincipal { 4 | private service: string; 5 | constructor(service: string) { 6 | super(); 7 | this.service = service; 8 | } 9 | 10 | toJSON() { 11 | return {Service: this.service}; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/principals/user.ts: -------------------------------------------------------------------------------- 1 | import {ArnPrincipal} from './arn'; 2 | 3 | class UserPrincipal extends ArnPrincipal { 4 | private accountId: string; 5 | private userName: string; 6 | private path: string; 7 | constructor(accountId: string, userName: string, path: string = '/') { 8 | super(`arn:aws:iam::${accountId}:user${path}${userName}`); 9 | this.accountId = accountId; 10 | this.userName = userName; 11 | this.path = path; 12 | } 13 | 14 | static validate(arn: string): UserPrincipal | undefined { 15 | const regex = new RegExp( 16 | '^arn:aws:iam::([0-9]{12}):user((/[/a-zA-Z\+=\.@_-]{1,510})?/)([a-zA-Z0-9\+=\.@_-]{1,64})$', 17 | ); 18 | const result = regex.exec(arn) as RegExpExecArray; 19 | return result ? new UserPrincipal(result[1], result[4], result[2]) : undefined; 20 | } 21 | } 22 | 23 | export {UserPrincipal}; 24 | -------------------------------------------------------------------------------- /src/principals/wildcard.ts: -------------------------------------------------------------------------------- 1 | import {Principal} from './base'; 2 | 3 | class WildcardPrincipal implements Principal { 4 | toJSON() { 5 | return '*'; 6 | } 7 | } 8 | 9 | export {WildcardPrincipal}; 10 | -------------------------------------------------------------------------------- /src/statement/deserialiser.ts: -------------------------------------------------------------------------------- 1 | import {ConditionJSONDeserialiser} from '../condition/deserialiser'; 2 | import {PrincipalJSONDeserialiser} from '../principals/deserialiser'; 3 | import {Statement} from '../statement/statement'; 4 | import {parseArray} from '../arrays'; 5 | 6 | export class StatementJSONDeserialiser { 7 | static fromJSON(obj: any) { 8 | return new Statement({ 9 | sid: obj.Sid, 10 | effect: obj.Effect, 11 | principals: PrincipalJSONDeserialiser.fromJSON(obj.Principal), 12 | notprincipals: PrincipalJSONDeserialiser.fromJSON(obj.NotPrincipal), 13 | actions: parseArray(obj.Action), 14 | notactions: parseArray(obj.NotAction), 15 | resources: parseArray(obj.Resource), 16 | notresources: parseArray(obj.NotResource), 17 | conditions: ConditionJSONDeserialiser.fromJSON(obj.Condition), 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/statement/serialiser.ts: -------------------------------------------------------------------------------- 1 | import {Statement} from './statement'; 2 | import {normalise} from '../normaliser'; 3 | import {ConditionJSONSerialiser} from '../condition/serialiser'; 4 | import {PrincipalJSONSerialiser} from '../principals/serialiser'; 5 | 6 | export class StatementJSONSerialiser { 7 | static toJSON(statement: Statement) { 8 | return { 9 | Sid: normalise(statement.sid), 10 | Effect: normalise(statement.effect), 11 | Principal: PrincipalJSONSerialiser.toJSON(statement.principals), 12 | NotPrincipal: PrincipalJSONSerialiser.toJSON(statement.notprincipals), 13 | Action: normalise(statement.actions), 14 | NotAction: normalise(statement.notactions), 15 | Resource: normalise(statement.resources), 16 | NotResource: normalise(statement.notresources), 17 | Condition: ConditionJSONSerialiser.toJSON(statement.conditions), 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/statement/statement.ts: -------------------------------------------------------------------------------- 1 | import {Principal} from '../principals/base'; 2 | import {Condition} from '../condition/condition'; 3 | import {StatementJSONDeserialiser} from './deserialiser'; 4 | import {StatementJSONSerialiser} from './serialiser'; 5 | import {WildcardPrincipal} from '../principals/wildcard'; 6 | import {PolicyType} from '../policy/policy'; 7 | 8 | /* eslint-disable no-unused-vars */ 9 | export type Effect = 'Allow' | 'Deny' 10 | /* eslint-enable no-unused-vars */ 11 | 12 | export class Statement { 13 | public sid: string | undefined; 14 | public effect: Effect; 15 | public principals: Principal[] = []; 16 | public notprincipals: Principal[] = []; 17 | public actions: string[] = []; 18 | public notactions: string[] = []; 19 | public resources: string[] = []; 20 | public notresources: string[] = []; 21 | public conditions: Condition[] = []; 22 | 23 | constructor(props?: StatementArgs) { 24 | this.sid = props?.sid; 25 | this.effect = props?.effect || 'Allow'; 26 | 27 | this.addPrincipals(props?.principals || []); 28 | this.addNotPrincipals(props?.notprincipals || []); 29 | this.addActions(props?.actions || []); 30 | this.addNotActions(props?.notactions || []); 31 | this.addResources(props?.resources || []); 32 | this.addNotResources(props?.notresources || []); 33 | this.addConditions(props?.conditions || []); 34 | } 35 | 36 | private addPrincipals(principals: Principal[]) { 37 | this.onlyHasOneWildCardPrincipalOrNone(principals); 38 | this.principals.push(...principals); 39 | } 40 | 41 | private addNotPrincipals(principals: Principal[]) { 42 | this.onlyHasOneWildCardPrincipalOrNone(principals); 43 | this.notprincipals.push(...principals); 44 | } 45 | 46 | private onlyHasOneWildCardPrincipalOrNone(principals: Principal[]) { 47 | if (principals.length > 1) { 48 | const anonymousPrincipal = principals.find((principal) => principal instanceof WildcardPrincipal); 49 | const hasAnonymousPrincipal = (anonymousPrincipal !== undefined); 50 | if (hasAnonymousPrincipal) { 51 | throw new Error('In case of the AnonymousPrincipal there can only be one principal'); 52 | } 53 | } 54 | } 55 | 56 | private addActions(actions: string[]) { 57 | this.actions.push(...actions); 58 | }; 59 | 60 | private addNotActions(actions: string[]) { 61 | this.notactions.push(...actions); 62 | }; 63 | 64 | private addResources(resources: string[]) { 65 | this.resources.push(...resources); 66 | }; 67 | 68 | private addNotResources(resources: string[]) { 69 | this.notresources.push(...resources); 70 | }; 71 | 72 | private addConditions(conditions: Condition[]) { 73 | this.conditions.push(...conditions); 74 | } 75 | 76 | toJSON() { 77 | return StatementJSONSerialiser.toJSON(this); 78 | } 79 | 80 | static fromJSON(obj: any): Statement { 81 | return StatementJSONDeserialiser.fromJSON(obj); 82 | } 83 | 84 | validateForAnyPolicy() { 85 | const errors: string[] = []; 86 | if ((this.actions.length === 0) && (this.notactions.length === 0)) { 87 | errors.push(`Statement(${this.sid}) must specify at least one 'action' or 'notaction'.`); 88 | } 89 | return errors; 90 | } 91 | 92 | validateForResourcePolicy(policyType?: PolicyType) { 93 | const errors: string[] = []; 94 | if (Object.keys(this.principals).length === 0 && Object.keys(this.notprincipals).length === 0) { 95 | errors.push(`Statement(${this.sid}) must specify at least one 'principal' or 'notprincipal'.`); 96 | } 97 | if (this.sid && policyType == PolicyType.SecretsManager) { 98 | const sidRegEx = new RegExp('^[a-zA-Z0-9]*$'); 99 | if (!sidRegEx.test(this.sid)) { 100 | errors.push( 101 | `Statement(${this.sid}) should only accept alphanumeric characters for 'sid'` + 102 | ' in the case of a SecretsManager secret policy.', 103 | ); 104 | } 105 | } 106 | if (this.sid && policyType == PolicyType.S3) { 107 | const sidRegEx = new RegExp('^[a-zA-Z0-9 ]*$'); 108 | if (!sidRegEx.test(this.sid)) { 109 | errors.push( 110 | `Statement(${this.sid}) should only accept alphanumeric characters and spaces for 'sid'` + 111 | ' in the case of an S3 bucket policy.', 112 | ); 113 | } 114 | } 115 | if (this.sid && policyType == PolicyType.KMS) { 116 | const sidRegEx = new RegExp('^[a-zA-Z0-9 ]*$'); 117 | if (!sidRegEx.test(this.sid)) { 118 | errors.push( 119 | `Statement(${this.sid}) should only accept alphanumeric characters and spaces for 'sid'` + 120 | ' in the case of a KMS key policy.', 121 | ); 122 | } 123 | } 124 | return errors; 125 | } 126 | 127 | validateForIdentityPolicy() { 128 | const errors: string[] = []; 129 | if (Object.keys(this.principals).length > 0 || Object.keys(this.notprincipals).length > 0) { 130 | errors.push(`Statement(${this.sid}) cannot specify any 'principal' or 'notprincipal'.`); 131 | } 132 | if ((Object.keys(this.resources).length === 0) && (Object.keys(this.notresources).length === 0)) { 133 | errors.push(`Statement(${this.sid}) must specify at least one 'resource' or 'notresource'.`); 134 | } 135 | if (this.sid) { 136 | const sidRegEx = new RegExp('^[a-zA-Z0-9]*$'); 137 | if (!sidRegEx.test(this.sid)) { 138 | errors.push( 139 | `Statement(${this.sid}) should only accept alphanumeric characters for 'sid'` + 140 | ' in the case of an IAM policy.', 141 | ); 142 | } 143 | } 144 | return errors; 145 | } 146 | }; 147 | 148 | interface StatementArgs { 149 | readonly sid?: string; 150 | readonly effect?: Effect; 151 | readonly principals?: Principal[]; 152 | readonly notprincipals?: Principal[]; 153 | readonly actions?: string[]; 154 | readonly notactions?: string[]; 155 | readonly resources?: string[]; 156 | readonly notresources?: string[]; 157 | readonly conditions?: Condition[]; 158 | } 159 | -------------------------------------------------------------------------------- /tests/arrays.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {parseArray} from '../src/arrays'; 3 | 4 | describe('arrays', function() { 5 | describe('#parse', function() { 6 | describe('when an object is undefined', function() { 7 | it('should return an empty array', function() { 8 | const input = undefined; 9 | expect(parseArray(input)).to.be.an('array').that.is.empty; 10 | }); 11 | }); 12 | 13 | describe('when an object is a string', function() { 14 | it('should return a 1-length array', function() { 15 | const input= 'foobar'; 16 | expect(parseArray(input)).to.be.an('array').that.deep.equal(['foobar']); 17 | }); 18 | }); 19 | 20 | describe('when an object is an array', function() { 21 | describe('of strings', function() { 22 | it('should return the array of strings', function() { 23 | const input = ['foo', 'bar']; 24 | const expected = ['foo', 'bar']; 25 | expect(parseArray(input)).to.be.an('array').that.deep.equal(expected); 26 | }); 27 | }); 28 | 29 | describe('of numbers', function() { 30 | it('should throw an Error', function() { 31 | const input = [1, 2]; 32 | expect(() => parseArray(input)).to.throw(Error) 33 | .with.property('message', 'Unsupported type: expecting an array of strings'); 34 | }); 35 | }); 36 | 37 | describe('of objects', function() { 38 | it('should throw an Error', function() { 39 | const input = [{aProperty: 'aValue'}, {anotherProperty: 'anotherValue'}]; 40 | expect(() => parseArray(input)).to.throw(Error) 41 | .with.property('message', 'Unsupported type: expecting an array of strings'); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('when an object is a number', function() { 47 | it('should throw an Error', function() { 48 | const input = 1234; 49 | expect(() => parseArray(input)).to.throw(Error) 50 | .with.property('message', 'Unsupported type: expecting an array or a string'); 51 | }); 52 | }); 53 | 54 | describe('when an object is an object', function() { 55 | it('should throw an Error', function() { 56 | const input = {aProperty: 'aValue'}; 57 | expect(() => parseArray(input)).to.throw(Error) 58 | .with.property('message', 'Unsupported type: expecting an array or a string'); 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/condition/condition.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {Condition} from '../../src'; 3 | 4 | describe('#Condition', function() { 5 | describe('constructor', function() { 6 | describe('when operator is empty', function() { 7 | it('should raise an error', function() { 8 | expect(() => new Condition('', 'key', ['value'])).to 9 | .throw(Error).with.property('message', 'operator should not be empty'); 10 | }); 11 | }); 12 | describe('when key is empty', function() { 13 | it('should raise an error', function() { 14 | expect(() => new Condition('operator', '', ['value'])).to 15 | .throw(Error).with.property('message', 'key should not be empty'); 16 | }); 17 | }); 18 | describe('when values is empty', function() { 19 | it('should raise an error', function() { 20 | expect(() => new Condition('operator', 'key', [])).to 21 | .throw(Error).with.property('message', 'values should not be empty'); 22 | }); 23 | }); 24 | describe('when values contains one empty string at position zero', function() { 25 | it('should raise an error', function() { 26 | expect(() => new Condition('operator', 'key', [''])).to 27 | .throw(Error).with.property('message', 'values should not have an empty string'); 28 | }); 29 | }); 30 | describe('when values contains one empty string at position one', function() { 31 | it('should raise an error', function() { 32 | expect(() => new Condition('operator', 'key', ['value', ''])).to 33 | .throw(Error).with.property('message', 'values should not have an empty string'); 34 | }); 35 | }); 36 | describe('when values contains multiple empty strings', function() { 37 | it('should raise an error', function() { 38 | expect(() => new Condition('operator', 'key', ['value1', '', 'value3', 'value4', ''])).to 39 | .throw(Error).with.property('message', 'values should not have an empty string'); 40 | }); 41 | }); 42 | }); 43 | 44 | describe('#toJSON', function() { 45 | describe('when condition has one value', function() { 46 | const condition = new Condition('StringLike', 'aws:userid', ['12345']); 47 | it('should return a JSON object', function() { 48 | const expected = { 49 | StringLike: {'aws:userid': ['12345']}, 50 | }; 51 | expect(condition.toJSON()).to.deep.equal(expected); 52 | }); 53 | }); 54 | 55 | describe('when condition has two values', function() { 56 | const condition = new Condition('StringLike', 'aws:userid', ['12345', '67890']); 57 | it('should return a JSON object', function() { 58 | const expected = { 59 | StringLike: {'aws:userid': ['12345', '67890']}, 60 | }; 61 | expect(condition.toJSON()).to.deep.equal(expected); 62 | }); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/condition/deserialiser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {ConditionJSONDeserialiser} from '../../src/condition/deserialiser'; 3 | import {Condition} from '../../src'; 4 | 5 | describe('#ConditionJSONDeserialiser', function() { 6 | describe('#fromJSON', function() { 7 | describe('when Condition is undefined', function() { 8 | it('should return an empty array', function() { 9 | const input = undefined; 10 | const expected: Condition[] = []; 11 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 12 | }); 13 | }); 14 | describe('when Condition is a string', function() { 15 | it('should throw an Error', function() { 16 | const input = 'condition'; 17 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 18 | .with.property( 19 | 'message', 20 | 'Unsupported Condition type string: expecting an object {[operator:string]: {[key:string]:string[]}}'); 21 | }); 22 | }); 23 | describe('when Condition is a number', function() { 24 | it('should throw an Error', function() { 25 | const input = 1234; 26 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 27 | .with.property( 28 | 'message', 29 | 'Unsupported Condition type number: expecting an object {[operator:string]: {[key:string]:string[]}}'); 30 | }); 31 | }); 32 | describe('when Condition is an array', function() { 33 | it('should throw an Error', function() { 34 | const input = ['condition']; 35 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 36 | .with.property( 37 | 'message', 38 | 'Unsupported Condition type array: expecting an object {[operator:string]: {[key:string]:string[]}}'); 39 | }); 40 | }); 41 | describe('when Condition is an object', function() { 42 | describe('and it is empty', function() { 43 | it('should return an empty array', function() { 44 | const input = {}; 45 | const expected: Condition[] = []; 46 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 47 | }); 48 | }); 49 | describe('and it has an operator property', function() { 50 | describe('and its value is undefined', function() { 51 | it('should throw an Error', function() { 52 | const input = {operator: undefined}; 53 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 54 | .with.property( 55 | 'message', 56 | 'Unsupported Condition operator type undefined: expecting an object {[key:string]:string[]}'); 57 | }); 58 | }); 59 | describe('and its value is a string', function() { 60 | it('should throw an Error', function() { 61 | const input = {operator: 'value'}; 62 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 63 | .with.property( 64 | 'message', 65 | 'Unsupported Condition operator type string: expecting an object {[key:string]:string[]}'); 66 | }); 67 | }); 68 | describe('and its value is a number', function() { 69 | it('should throw an Error', function() { 70 | const input = {operator: 12345}; 71 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 72 | .with.property( 73 | 'message', 74 | 'Unsupported Condition operator type number: expecting an object {[key:string]:string[]}'); 75 | }); 76 | }); 77 | describe('and its value is an array', function() { 78 | it('should throw an Error', function() { 79 | const input = {operator: ['value']}; 80 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 81 | .with.property( 82 | 'message', 83 | 'Unsupported Condition operator type array: expecting an object {[key:string]:string[]}'); 84 | }); 85 | }); 86 | describe('and its value is an object', function() { 87 | describe('and it is empty', function() { 88 | it('should return an empty array', function() { 89 | const input = {operator: {}}; 90 | const expected: Condition[] = []; 91 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 92 | }); 93 | }); 94 | describe('and it has a key property', function() { 95 | describe('and its value is undefined', function() { 96 | it('should throw an Error', function() { 97 | const input = {operator: {key: undefined}}; 98 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 99 | .with.property( 100 | 'message', 101 | 'values should not be empty'); 102 | }); 103 | }); 104 | describe('and its value is an object', function() { 105 | it('should throw an Error', function() { 106 | const input = {operator: {key: {}}}; 107 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 108 | .with.property( 109 | 'message', 110 | 'Unsupported type: expecting an array or a string'); 111 | }); 112 | }); 113 | describe('and its value is a string', function() { 114 | it('should return a 1-length Condition array', function() { 115 | const input = {operator: {key: 'value'}}; 116 | const expected = [new Condition('operator', 'key', ['value'])]; 117 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 118 | }); 119 | }); 120 | describe('and its value is an array', function() { 121 | describe('and it is empty', function() { 122 | it('should throw an Error', function() { 123 | const input = {operator: {key: []}}; 124 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 125 | .with.property( 126 | 'message', 127 | 'values should not be empty'); 128 | }); 129 | }); 130 | describe('and it contains a number', function() { 131 | it('should throw an Error', function() { 132 | const input = {operator: {key: [12345]}}; 133 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 134 | .with.property( 135 | 'message', 136 | 'Unsupported type: expecting an array of strings'); 137 | }); 138 | }); 139 | describe('and it contains an object', function() { 140 | it('should throw an Error', function() { 141 | const input = {operator: {key: [{}]}}; 142 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 143 | .with.property( 144 | 'message', 145 | 'Unsupported type: expecting an array of strings'); 146 | }); 147 | }); 148 | describe('and it contains an undefined value', function() { 149 | it('should throw an Error', function() { 150 | const input = {operator: {key: [undefined, 'value']}}; 151 | expect(() => ConditionJSONDeserialiser.fromJSON(input)).to.throw(Error) 152 | .with.property( 153 | 'message', 154 | 'Unsupported type: expecting an array of strings'); 155 | }); 156 | }); 157 | }); 158 | }); 159 | }); 160 | }); 161 | }); 162 | describe('when Condition has one operator, one key and one value', function() { 163 | it('should return one Condition', function() { 164 | const input = { 165 | operator: {key: ['value']}, 166 | }; 167 | const expected = [new Condition('operator', 'key', ['value'])]; 168 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 169 | }); 170 | }); 171 | describe('when Condition has one operator, one key and two values', function() { 172 | it('should return one Condition', function() { 173 | const input = { 174 | operator: {key: ['value1', 'value2']}, 175 | }; 176 | const expected = [new Condition('operator', 'key', ['value1', 'value2'])]; 177 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 178 | }); 179 | }); 180 | describe('when Condition has one operator, two keys each with one value', function() { 181 | it('should return two conditions', function() { 182 | const input = { 183 | operator: {key1: ['value1'], key2: ['value2']}, 184 | }; 185 | const expected = [ 186 | new Condition('operator', 'key1', ['value1']), 187 | new Condition('operator', 'key2', ['value2']), 188 | ]; 189 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 190 | }); 191 | }); 192 | describe('when Condition has two operators each with one key and one value', function() { 193 | it('should return two conditions', function() { 194 | const input = { 195 | operator1: {key1: ['value1']}, 196 | operator2: {key2: ['value2']}, 197 | }; 198 | const expected = [ 199 | new Condition('operator1', 'key1', ['value1']), 200 | new Condition('operator2', 'key2', ['value2']), 201 | ]; 202 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 203 | }); 204 | }); 205 | describe('when Condition has two operators each with two keys and two values', function() { 206 | it('should return two conditions', function() { 207 | const input = { 208 | operator1: {key11: ['value111', 'value112'], key12: ['value121', 'value122']}, 209 | operator2: {key21: ['value211', 'value212'], key22: ['value221', 'value222']}, 210 | }; 211 | const expected = [ 212 | new Condition('operator1', 'key11', ['value111', 'value112']), 213 | new Condition('operator1', 'key12', ['value121', 'value122']), 214 | new Condition('operator2', 'key21', ['value211', 'value212']), 215 | new Condition('operator2', 'key22', ['value221', 'value222']), 216 | ]; 217 | expect(ConditionJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 218 | }); 219 | }); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /tests/condition/serialiser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {ConditionJSONSerialiser} from '../../src/condition/serialiser'; 3 | import {Condition} from '../../src'; 4 | 5 | describe('#ConditionJSONSerialiser', function() { 6 | describe('#toJSON', function() { 7 | describe('when having an empty list of conditions', function() { 8 | const conditions: Condition[] = []; 9 | it('should return undefined', function() { 10 | expect(ConditionJSONSerialiser.toJSON(conditions)).to.be.undefined; 11 | }); 12 | }); 13 | 14 | describe('when having one condition', function() { 15 | const conditions = [new Condition('anOperator', 'aKey', ['aValue'])]; 16 | it('should return the Condition JSON object', function() { 17 | const expected = { 18 | 'anOperator': {'aKey': ['aValue']}, 19 | }; 20 | expect(ConditionJSONSerialiser.toJSON(conditions)).to.deep.equal(expected); 21 | }); 22 | }); 23 | 24 | describe('when having two conditions having a different operator', function() { 25 | const conditions = [ 26 | new Condition('anOperator1', 'aKey1', ['aValue1']), 27 | new Condition('anOperator2', 'aKey2', ['aValue2']), 28 | ]; 29 | it('should return the Condition JSON object', function() { 30 | const expected = { 31 | 'anOperator1': {'aKey1': ['aValue1']}, 32 | 'anOperator2': {'aKey2': ['aValue2']}, 33 | }; 34 | expect(ConditionJSONSerialiser.toJSON(conditions)).to.deep.equal(expected); 35 | }); 36 | }); 37 | 38 | describe('when having two conditions having the same operator', function() { 39 | const conditions = [ 40 | new Condition('anOperator', 'aKey1', ['aValue1']), 41 | new Condition('anOperator', 'aKey2', ['aValue2']), 42 | ]; 43 | it('should return the Condition JSON object', function() { 44 | const expected = { 45 | 'anOperator': {'aKey1': ['aValue1'], 'aKey2': ['aValue2']}, 46 | }; 47 | expect(ConditionJSONSerialiser.toJSON(conditions)).to.deep.equal(expected); 48 | }); 49 | }); 50 | 51 | describe('when having two conditions having the same operator and same key', function() { 52 | const conditions = [ 53 | new Condition('anOperator', 'aKey', ['aValue1']), 54 | new Condition('anOperator', 'aKey', ['aValue2']), 55 | ]; 56 | it('should return the Condition JSON object', function() { 57 | const expected = { 58 | 'anOperator': {'aKey': ['aValue1', 'aValue2']}, 59 | }; 60 | expect(ConditionJSONSerialiser.toJSON(conditions)).to.deep.equal(expected); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/normaliser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {normalise} from '../src/normaliser'; 3 | 4 | describe('#normalise', function() { 5 | describe('when an object is undefined', function() { 6 | it('should return undefined', function() { 7 | expect(normalise(undefined)).to.be.undefined; 8 | }); 9 | }); 10 | 11 | describe('when an object has no properties', function() { 12 | it('should return undefined', function() { 13 | expect(normalise({})).to.be.undefined; 14 | }); 15 | }); 16 | 17 | describe('when an object has one property', function() { 18 | it('should return the object', function() { 19 | const obj = {aProperty: ''}; 20 | const expected = Object.assign(obj); 21 | expect(normalise(obj)).to.deep.equal(expected); 22 | }); 23 | }); 24 | 25 | describe('when an object has two properties', function() { 26 | it('should return the object', function() { 27 | const obj = {property1: 'a string', property2: 1}; 28 | const expected = Object.assign(obj); 29 | expect(normalise(obj)).to.deep.equal(expected); 30 | }); 31 | }); 32 | 33 | describe('when an object is an empty array', function() { 34 | it('should return undefined', function() { 35 | expect(normalise([])).to.be.undefined; 36 | }); 37 | }); 38 | 39 | describe('when an object is an array having one element', function() { 40 | it('should return undefined', function() { 41 | const obj = ['a string']; 42 | const expected = [...obj]; 43 | expect(normalise(obj)).to.deep.eq(expected); 44 | }); 45 | }); 46 | 47 | describe('when an object is an array having two elements', function() { 48 | it('should return undefined', function() { 49 | const obj = [1, 2]; 50 | const expected = [...obj]; 51 | expect(normalise(obj)).to.deep.eq(expected); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/policy/deserialiser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {PolicyDocumentJSONDeserialiser} from '../../src/policy/deserialiser'; 3 | import { 4 | PolicyDocument, 5 | Statement, 6 | } from '../../src'; 7 | 8 | describe('#PolicyDocumentJSONDeserialiser', function() { 9 | describe('#fromJSON', function() { 10 | describe('when json is empty', function() { 11 | const json = {}; 12 | it('should return an empty Policy', function() { 13 | const expected = new PolicyDocument(); 14 | expect(PolicyDocumentJSONDeserialiser.fromJSON(json)).to.deep.equal(expected); 15 | }); 16 | }); 17 | 18 | describe('when json has no Statement', function() { 19 | describe('and Id is a string', function() { 20 | const json = {Id: 'an-id'}; 21 | it('should return a PolicyDocument with an id', function() { 22 | const expected = new PolicyDocument([], 'an-id'); 23 | expect(PolicyDocumentJSONDeserialiser.fromJSON(json)).to.deep.equal(expected); 24 | }); 25 | }); 26 | 27 | describe('and Id is a number', function() { 28 | const json = {Id: 123}; 29 | it('should throw an Error', function() { 30 | expect(() => PolicyDocumentJSONDeserialiser.fromJSON(json)).to.throw(Error) 31 | .with.property('message', 'Unexpected type: Id must be a string'); 32 | }); 33 | }); 34 | 35 | describe('and Id is a boolean', function() { 36 | const json = {Id: true}; 37 | it('should throw an Error', function() { 38 | expect(() => PolicyDocumentJSONDeserialiser.fromJSON(json)).to.throw(Error) 39 | .with.property('message', 'Unexpected type: Id must be a string'); 40 | }); 41 | }); 42 | 43 | describe('and Id is an object', function() { 44 | const json = {Id: {}}; 45 | it('should throw an Error', function() { 46 | expect(() => PolicyDocumentJSONDeserialiser.fromJSON(json)).to.throw(Error) 47 | .with.property('message', 'Unexpected type: Id must be a string'); 48 | }); 49 | }); 50 | 51 | describe('and Id is an array', function() { 52 | const json = {Id: []}; 53 | it('should throw an Error', function() { 54 | expect(() => PolicyDocumentJSONDeserialiser.fromJSON(json)).to.throw(Error) 55 | .with.property('message', 'Unexpected type: Id must be a string'); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('when json has a Statement', function() { 61 | describe('and Id is not a string', function() { 62 | const json = {Id: 123}; 63 | it('should throw an Error', function() { 64 | expect(() => PolicyDocumentJSONDeserialiser.fromJSON(json)).to.throw(Error) 65 | .with.property('message', 'Unexpected type: Id must be a string'); 66 | }); 67 | }); 68 | 69 | describe('and Statement is a string', function() { 70 | const json = {Statement: 'statement'}; 71 | it('should throw an Error', function() { 72 | expect(() => PolicyDocumentJSONDeserialiser.fromJSON(json)).to.throw(Error) 73 | .with.property('message', 'Unexpected type: Statement must be an array'); 74 | }); 75 | }); 76 | 77 | describe('and Statement is an array', function() { 78 | const json = { 79 | Statement: [{Sid: 'sid1'}, {Sid: 'sid2'}], 80 | }; 81 | it('should return a Policy with Statements', function() { 82 | const expected = new PolicyDocument([ 83 | new Statement({sid: 'sid1'}), 84 | new Statement({sid: 'sid2'}), 85 | ]); 86 | expect(PolicyDocumentJSONDeserialiser.fromJSON(json)).to.deep.equal(expected); 87 | }); 88 | }); 89 | 90 | describe('with an Id and a Statement array', function() { 91 | const json = { 92 | Id: 'an-id', 93 | Statement: [{Sid: 'sid1'}, {Sid: 'sid2'}], 94 | }; 95 | it('should return a Policy with Statements', function() { 96 | const expected = new PolicyDocument([ 97 | new Statement({sid: 'sid1'}), 98 | new Statement({sid: 'sid2'}), 99 | ], 'an-id'); 100 | expect(PolicyDocumentJSONDeserialiser.fromJSON(json)).to.deep.equal(expected); 101 | }); 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /tests/policy/policy.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import { 3 | PolicyDocument, 4 | Statement, 5 | UserPrincipal, 6 | RolePrincipal, 7 | ServicePrincipal, 8 | RootAccountPrincipal, 9 | AccountPrincipal, 10 | WildcardPrincipal, 11 | Condition, 12 | PolicyType, 13 | } from '../../src'; 14 | 15 | describe('#PolicyDocument', function() { 16 | describe('when serialising to JSON', function() { 17 | const policy = new PolicyDocument([ 18 | new Statement({ 19 | sid: 'anSID', 20 | effect: 'Allow', 21 | principals: [ 22 | new UserPrincipal('123456789000', 'aUser'), 23 | new RootAccountPrincipal('123456789000'), 24 | new AccountPrincipal('123456789000'), 25 | new ServicePrincipal('aservice.amazonaws.com'), 26 | ], 27 | actions: ['ec2:Describe*', 'ec2:Get*'], 28 | resources: [ 29 | 'arn:aws:ec2:eu-west-1:123456789000:instance/i-123456', 30 | 'arn:aws:ec2:eu-west-1:123456789000:image/ami-123456', 31 | ], 32 | conditions: [ 33 | new Condition('StringEquals', 'kms:CallerAccount', ['456252097346']), 34 | new Condition('StringEquals', 'kms:ViaService', ['secretsmanager.eu-west-1.amazonaws.com']), 35 | new Condition('StringNotEquals', 'aws:userid', ['anId1', 'anId2']), 36 | ], 37 | }), 38 | new Statement({ 39 | sid: 'anSID2', 40 | effect: 'Deny', 41 | principals: [new RolePrincipal('123456789000', 'aRole')], 42 | actions: ['ec2:TerminateInstance'], 43 | resources: ['arn:aws:ec2:eu-west-1:123456789000:instance/i-123456'], 44 | }), 45 | new Statement({ 46 | sid: 'anSID3', 47 | principals: [new WildcardPrincipal()], 48 | }), 49 | ], 'an-id'); 50 | 51 | it('should successfully pass a JSON round trip', function() { 52 | const json = policy.json; 53 | const actual = PolicyDocument.fromJson(json); 54 | expect(actual).to.deep.equal(policy); 55 | }); 56 | }); 57 | 58 | describe('#fromJson', function() { 59 | describe('when Statement is not an array', function() { 60 | it('should throw an Error', function() { 61 | const input = JSON.stringify({Statement: new Statement({sid: 'not an array'})}); 62 | expect(() => PolicyDocument.fromJson(input)).to.throw(Error) 63 | .with.property('message', 'Unexpected type: Statement must be an array'); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('#constructor', function() { 69 | describe('when empty', function() { 70 | const policy = new PolicyDocument(); 71 | 72 | it('should be empty', function() { 73 | expect(policy.isEmpty).to.be.true; 74 | }); 75 | }); 76 | 77 | describe('when adding 1 statement', function() { 78 | const policy = new PolicyDocument([new Statement()]); 79 | 80 | it('should have one statement', function() { 81 | expect(policy.statementCount).to.be.equal(1); 82 | }); 83 | }); 84 | 85 | describe('when adding 2 statements having the same Sid', function() { 86 | it('should throw an error', function() { 87 | const sid = 'anSid'; 88 | expect(() => new PolicyDocument([ 89 | new Statement({sid: sid}), 90 | new Statement({sid: sid, resources: ['*']}), 91 | ])).to.throw(Error).with.property('message', 'Non-unique Sid "anSid"'); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('when having statements', function() { 97 | const policy = new PolicyDocument([ 98 | new Statement({sid: 'first sid', resources: ['resource1']}), 99 | new Statement({resources: ['resource2']}), 100 | new Statement({sid: 'third sid', resources: ['resource3']}), 101 | new Statement({sid: 'fourth sid', resources: ['resource4']}), 102 | ]); 103 | 104 | describe('#getStatement', function() { 105 | describe('when Sid exists', function() { 106 | it('should return the statement having the given Sid', function() { 107 | const expected = new Statement({sid: 'third sid', resources: ['resource3']}); 108 | expect(policy.getStatement('third sid')).to.deep.equal(expected); 109 | }); 110 | }); 111 | 112 | describe('when Sid doesn\'t exist', function() { 113 | it('should return undefined', function() { 114 | expect(policy.getStatement('an sid')).to.be.undefined; 115 | }); 116 | }); 117 | }); 118 | }); 119 | 120 | describe('#addStatements', function() { 121 | describe('when policy is empty', function() { 122 | describe('when adding 1 statement', function() { 123 | const policy = new PolicyDocument(); 124 | const statement = new Statement({sid: 'sid', resources: ['resource']}); 125 | policy.addStatements(statement); 126 | it('should have one statement', function() { 127 | expect(policy.statementCount).to.be.equal(1); 128 | expect(policy.getStatement('sid')).to.deep.equal(statement); 129 | }); 130 | }); 131 | 132 | describe('when adding 2 statements', function() { 133 | const policy = new PolicyDocument(); 134 | const statement1 = new Statement({sid: 'sid1', resources: ['resource1']}); 135 | const statement2 = new Statement({sid: 'sid2', resources: ['resource2']}); 136 | policy.addStatements(statement1, statement2); 137 | it('should have two statements', function() { 138 | expect(policy.statementCount).to.be.equal(2); 139 | expect(policy.getStatement('sid1')).to.deep.equal(statement1); 140 | expect(policy.getStatement('sid2')).to.deep.equal(statement2); 141 | }); 142 | }); 143 | }); 144 | 145 | describe('when policy is not empty', function() { 146 | describe('when adding 1 statement', function() { 147 | const policy = new PolicyDocument([ 148 | new Statement({sid: 'sid1', resources: ['resource1']}), 149 | ]); 150 | const statement = new Statement({sid: 'sid2', resources: ['resource2']}); 151 | policy.addStatements(statement); 152 | it('should have one statement', function() { 153 | expect(policy.statementCount).to.be.equal(2); 154 | expect(policy.getStatement('sid2')).to.deep.equal(statement); 155 | }); 156 | }); 157 | 158 | describe('when adding 2 statements', function() { 159 | const policy = new PolicyDocument([ 160 | new Statement({sid: 'sid1', resources: ['resource1']}), 161 | ]); 162 | const statement2 = new Statement({sid: 'sid2', resources: ['resource2']}); 163 | const statement3 = new Statement({sid: 'sid3', resources: ['resource3']}); 164 | policy.addStatements(statement2, statement3); 165 | it('should have one statement', function() { 166 | expect(policy.statementCount).to.be.equal(3); 167 | expect(policy.getStatement('sid2')).to.deep.equal(statement2); 168 | expect(policy.getStatement('sid3')).to.deep.equal(statement3); 169 | }); 170 | }); 171 | }); 172 | }); 173 | 174 | describe('identity-based policy', function() { 175 | const policy = new PolicyDocument([ 176 | new Statement({sid: '1st', actions: ['action'], resources: ['resource']}), 177 | new Statement({sid: '2nd', actions: ['action'], resources: ['resource']}), 178 | ]); 179 | 180 | it('should be valid for identity-based policy', function() { 181 | expect(policy.validate(PolicyType.IAM)).to.have.empty; 182 | }); 183 | 184 | it('should be invalid for resource-based policy', function() { 185 | const errors = policy.validate(PolicyType.S3); 186 | expect(errors).to.deep.equal([ 187 | 'Statement(1st) must specify at least one \'principal\' or \'notprincipal\'.', 188 | 'Statement(2nd) must specify at least one \'principal\' or \'notprincipal\'.', 189 | ]); 190 | }); 191 | 192 | describe('with a policy id', function() { 193 | const policy = new PolicyDocument([ 194 | new Statement({sid: '1st', actions: ['action'], resources: ['resource']}), 195 | ], 'an-id'); 196 | it('should be invalid', function() { 197 | const errors = policy.validate(PolicyType.IAM); 198 | expect(errors).to.deep.equal([ 199 | 'Policy Id is not allowed for identity-based policies', 200 | ]); 201 | }); 202 | }); 203 | }); 204 | 205 | describe('resource-based policy', function() { 206 | const policy = new PolicyDocument([ 207 | new Statement({sid: '1st', principals: [new AccountPrincipal('012345678900')], actions: ['action']}), 208 | new Statement({sid: '2nd', principals: [new AccountPrincipal('012345678900')], actions: ['action']}), 209 | ]); 210 | 211 | it('should be valid for resource-based policy', function() { 212 | expect(policy.validate(PolicyType.S3)).to.have.empty; 213 | }); 214 | 215 | it('should be invalid for identity-based policy', function() { 216 | const errors = policy.validate(PolicyType.IAM); 217 | expect(errors).to.deep.equal([ 218 | 'Statement(1st) cannot specify any \'principal\' or \'notprincipal\'.', 219 | 'Statement(1st) must specify at least one \'resource\' or \'notresource\'.', 220 | 'Statement(2nd) cannot specify any \'principal\' or \'notprincipal\'.', 221 | 'Statement(2nd) must specify at least one \'resource\' or \'notresource\'.', 222 | ]); 223 | }); 224 | 225 | describe('with a policy id', function() { 226 | const policy = new PolicyDocument([ 227 | new Statement({sid: '1st', principals: [new AccountPrincipal('012345678900')], actions: ['action']}), 228 | ], 'an-id'); 229 | it('should be valid', function() { 230 | expect(policy.validate(PolicyType.KMS)).to.be.empty; 231 | }); 232 | }); 233 | }); 234 | 235 | describe('policy without actions', function() { 236 | const policy = new PolicyDocument([ 237 | new Statement({sid: '1st'}), 238 | new Statement({sid: '2nd'}), 239 | ]); 240 | 241 | it('should be invalid for any policy', function() { 242 | const errors = policy.validate(); 243 | expect(errors).to.deep.equal([ 244 | 'Statement(1st) must specify at least one \'action\' or \'notaction\'.', 245 | 'Statement(2nd) must specify at least one \'action\' or \'notaction\'.', 246 | ]); 247 | }); 248 | 249 | it('should be invalid for S3 bucket policy', function() { 250 | const errors = policy.validate(PolicyType.S3); 251 | expect(errors).to.deep.equal([ 252 | 'Statement(1st) must specify at least one \'action\' or \'notaction\'.', 253 | 'Statement(2nd) must specify at least one \'action\' or \'notaction\'.', 254 | 'Statement(1st) must specify at least one \'principal\' or \'notprincipal\'.', 255 | 'Statement(2nd) must specify at least one \'principal\' or \'notprincipal\'.', 256 | ]); 257 | }); 258 | it('should be invalid for KMS key policy', function() { 259 | const errors = policy.validate(PolicyType.KMS); 260 | expect(errors).to.deep.equal([ 261 | 'Statement(1st) must specify at least one \'action\' or \'notaction\'.', 262 | 'Statement(2nd) must specify at least one \'action\' or \'notaction\'.', 263 | 'Statement(1st) must specify at least one \'principal\' or \'notprincipal\'.', 264 | 'Statement(2nd) must specify at least one \'principal\' or \'notprincipal\'.', 265 | ]); 266 | }); 267 | it('should be invalid for SecretsManager secret policy', function() { 268 | const errors = policy.validate(PolicyType.SecretsManager); 269 | expect(errors).to.deep.equal([ 270 | 'Statement(1st) must specify at least one \'action\' or \'notaction\'.', 271 | 'Statement(2nd) must specify at least one \'action\' or \'notaction\'.', 272 | 'Statement(1st) must specify at least one \'principal\' or \'notprincipal\'.', 273 | 'Statement(2nd) must specify at least one \'principal\' or \'notprincipal\'.', 274 | ]); 275 | }); 276 | 277 | it('should be invalid for identity-based policy', function() { 278 | const errors = policy.validate(PolicyType.IAM); 279 | expect(errors).to.deep.equal([ 280 | 'Statement(1st) must specify at least one \'action\' or \'notaction\'.', 281 | 'Statement(2nd) must specify at least one \'action\' or \'notaction\'.', 282 | 'Statement(1st) must specify at least one \'resource\' or \'notresource\'.', 283 | 'Statement(2nd) must specify at least one \'resource\' or \'notresource\'.', 284 | ]); 285 | }); 286 | }); 287 | 288 | describe('#IAM policy', function() { 289 | describe('id', function() { 290 | const policy = new PolicyDocument([], 'an-id'); 291 | expect(policy.id).to.equal('an-id'); 292 | }); 293 | 294 | describe('document longer than 6144 characters', function() { 295 | const policy = new PolicyDocument(); 296 | for (let i = 1; i < 84; i++) { 297 | policy.addStatements(new Statement({sid: '' + i, actions: ['action'], resources: ['resource']})); 298 | } 299 | it('should be invalid', function() { 300 | const errors = policy.validate(PolicyType.IAM); 301 | expect(errors).to.deep.equal([ 302 | 'The size of an IAM policy document (6171) should not exceed 6.144 characters.', 303 | ]); 304 | }); 305 | }); 306 | describe('having a Statement with alphanumeric Sid', function() { 307 | const policy = new PolicyDocument([ 308 | new Statement({ 309 | sid: 'aBcDe1234', 310 | effect: 'Allow', 311 | actions: ['iam:GetRole'], 312 | resources: ['*'], 313 | }), 314 | ]); 315 | it('should be valid', function() { 316 | expect(policy.validate(PolicyType.IAM)).to.be.empty; 317 | }); 318 | }); 319 | describe('having a Statement with non alphanumeric Sid with spaces', function() { 320 | const policy = new PolicyDocument([ 321 | new Statement({ 322 | sid: 'aBcDe 1234', 323 | effect: 'Allow', 324 | actions: ['iam:GetRole'], 325 | resources: ['*'], 326 | }), 327 | ]); 328 | it('should be invalid', function() { 329 | const errors = policy.validate(PolicyType.IAM); 330 | expect(errors).to.deep.equal([ 331 | 'Statement(aBcDe 1234) should only accept alphanumeric characters for \'sid\' in the case of an IAM policy.', 332 | ]); 333 | }); 334 | }); 335 | }); 336 | 337 | describe('#KMS key policy', function() { 338 | describe('document longer than 32kB', function() { 339 | const policy = new PolicyDocument(); 340 | for (let i = 1; i < 245; i++) { 341 | policy.addStatements(new Statement({ 342 | sid: '' + i, 343 | principals: [new RolePrincipal('123456789000', 'a_role')], 344 | actions: ['action'], 345 | resources: ['resource'], 346 | })); 347 | } 348 | it('should be invalid', function() { 349 | const errors = policy.validate(PolicyType.KMS); 350 | expect(errors).to.deep.equal([ 351 | 'The size of a KMS key policy document (32870) should not exceed 32kB.', 352 | ]); 353 | }); 354 | }); 355 | describe('having a Statement with alphanumeric Sid', function() { 356 | const policy = new PolicyDocument([ 357 | new Statement({ 358 | sid: 'aBcDe1234', 359 | effect: 'Allow', 360 | principals: [new RolePrincipal('123456789000', 'a_role')], 361 | actions: ['action'], 362 | resources: ['resource'], 363 | }), 364 | ]); 365 | it('should be valid', function() { 366 | expect(policy.validate(PolicyType.KMS)).to.be.empty; 367 | }); 368 | }); 369 | describe('having a Statement with alphanumeric Sid with spaces', function() { 370 | const policy = new PolicyDocument([ 371 | new Statement({ 372 | sid: 'aBcDe 1234', 373 | effect: 'Allow', 374 | principals: [new RolePrincipal('123456789000', 'a_role')], 375 | actions: ['action'], 376 | resources: ['resource'], 377 | }), 378 | ]); 379 | it('should be valid', function() { 380 | const errors = policy.validate(PolicyType.KMS); 381 | expect(errors).to.be.empty; 382 | }); 383 | }); 384 | describe('having a Statement with non-alphanumeric Sid', function() { 385 | const policy = new PolicyDocument([ 386 | new Statement({ 387 | sid: 'aBcDe1234*', 388 | effect: 'Allow', 389 | principals: [new RolePrincipal('123456789000', 'a_role')], 390 | actions: ['action'], 391 | resources: ['resource'], 392 | }), 393 | ]); 394 | it('should be invalid', function() { 395 | const errors = policy.validate(PolicyType.KMS); 396 | expect(errors).to.deep.equal([ 397 | 'Statement(aBcDe1234*) should only accept alphanumeric characters and spaces for \'sid\'' + 398 | ' in the case of a KMS key policy.', 399 | ]); 400 | }); 401 | }); 402 | }); 403 | 404 | describe('#S3 bucket policy', function() { 405 | describe('document longer than 20kB', function() { 406 | const policy = new PolicyDocument(); 407 | for (let i = 1; i < 154; i++) { 408 | policy.addStatements(new Statement({ 409 | sid: '' + i, 410 | principals: [new RolePrincipal('123456789000', 'a_role')], 411 | actions: ['action'], 412 | resources: ['resource'], 413 | })); 414 | } 415 | it('should be invalid', function() { 416 | const errors = policy.validate(PolicyType.S3); 417 | expect(errors).to.deep.equal([ 418 | 'The size of an S3 bucket policy document (20585) should not exceed 20kB.', 419 | ]); 420 | }); 421 | }); 422 | describe('having a Statement with alphanumeric Sid', function() { 423 | const policy = new PolicyDocument([ 424 | new Statement({ 425 | sid: 'aBcDe1234', 426 | effect: 'Allow', 427 | principals: [new RolePrincipal('123456789000', 'a_role')], 428 | actions: ['action'], 429 | resources: ['resource'], 430 | }), 431 | ]); 432 | it('should be valid', function() { 433 | expect(policy.validate(PolicyType.S3)).to.be.empty; 434 | }); 435 | }); 436 | describe('having a Statement with alphanumeric Sid with spaces', function() { 437 | const policy = new PolicyDocument([ 438 | new Statement({ 439 | sid: 'aBcDe 1234', 440 | effect: 'Allow', 441 | principals: [new RolePrincipal('123456789000', 'a_role')], 442 | actions: ['action'], 443 | resources: ['resource'], 444 | }), 445 | ]); 446 | it('should be valid', function() { 447 | const errors = policy.validate(PolicyType.S3); 448 | expect(errors).to.be.empty; 449 | }); 450 | }); 451 | describe('having a Statement with non-alphanumeric Sid', function() { 452 | const policy = new PolicyDocument([ 453 | new Statement({ 454 | sid: 'aBcDe1234*', 455 | effect: 'Allow', 456 | principals: [new RolePrincipal('123456789000', 'a_role')], 457 | actions: ['action'], 458 | resources: ['resource'], 459 | }), 460 | ]); 461 | it('should be invalid', function() { 462 | const errors = policy.validate(PolicyType.S3); 463 | expect(errors).to.deep.equal([ 464 | 'Statement(aBcDe1234*) should only accept alphanumeric characters and spaces for \'sid\'' + 465 | ' in the case of an S3 bucket policy.', 466 | ]); 467 | }); 468 | }); 469 | }); 470 | 471 | describe('#SecretsManager secret policy', function() { 472 | describe('document longer than 20kB', function() { 473 | const policy = new PolicyDocument(); 474 | for (let i = 1; i < 154; i++) { 475 | policy.addStatements(new Statement({ 476 | sid: '' + i, 477 | principals: [new RolePrincipal('123456789000', 'a_role')], 478 | actions: ['action'], 479 | resources: ['resource'], 480 | })); 481 | } 482 | it('should be invalid', function() { 483 | const errors = policy.validate(PolicyType.SecretsManager); 484 | expect(errors).to.deep.equal([ 485 | 'The size of a SecretsManager secret policy document (20585) should not exceed 20kB.', 486 | ]); 487 | }); 488 | }); 489 | describe('having a Statement with alphanumeric Sid', function() { 490 | const policy = new PolicyDocument([ 491 | new Statement({ 492 | sid: 'aBcDe1234', 493 | effect: 'Allow', 494 | principals: [new RolePrincipal('123456789000', 'a_role')], 495 | actions: ['action'], 496 | resources: ['resource'], 497 | }), 498 | ]); 499 | it('should be valid', function() { 500 | expect(policy.validate(PolicyType.SecretsManager)).to.be.empty; 501 | }); 502 | }); 503 | describe('having a Statement with alphanumeric Sid with spaces', function() { 504 | const policy = new PolicyDocument([ 505 | new Statement({ 506 | sid: 'aBcDe 1234', 507 | effect: 'Allow', 508 | principals: [new RolePrincipal('123456789000', 'a_role')], 509 | actions: ['action'], 510 | resources: ['resource'], 511 | }), 512 | ]); 513 | it('should be invalid', function() { 514 | const errors = policy.validate(PolicyType.SecretsManager); 515 | expect(errors).to.deep.equal([ 516 | 'Statement(aBcDe 1234) should only accept alphanumeric characters for \'sid\'' + 517 | ' in the case of a SecretsManager secret policy.', 518 | ]); 519 | }); 520 | }); 521 | }); 522 | }); 523 | -------------------------------------------------------------------------------- /tests/policy/serialiser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {PolicyDocumentJSONSerialiser} from '../../src/policy/serialiser'; 3 | import { 4 | PolicyDocument, 5 | Statement, 6 | } from '../../src'; 7 | 8 | describe('#PolicyDocumentJSONSerialiser', function() { 9 | describe('#toJSON', function() { 10 | describe('when policy is empty', function() { 11 | const input = new PolicyDocument(); 12 | it('should return undefined', function() { 13 | expect(PolicyDocumentJSONSerialiser.toJSON(input)).to.equal(undefined); 14 | }); 15 | }); 16 | 17 | describe('when policy has an empty statement', function() { 18 | const input = new PolicyDocument([new Statement()]); 19 | it('should return a JSON policy with an empty Statement', function() { 20 | const expected = { 21 | Statement: [{ 22 | Sid: undefined, 23 | Effect: 'Allow', 24 | Principal: undefined, 25 | NotPrincipal: undefined, 26 | Action: undefined, 27 | NotAction: undefined, 28 | Resource: undefined, 29 | NotResource: undefined, 30 | Condition: undefined, 31 | }], 32 | Version: '2012-10-17', 33 | }; 34 | expect(PolicyDocumentJSONSerialiser.toJSON(input)).to.deep.equal(expected); 35 | }); 36 | }); 37 | 38 | describe('when policy has an id set', function() { 39 | const input = new PolicyDocument([new Statement({sid: 'an-sid'})], 'an-id'); 40 | it('should return a JSON policy with an Id element', function() { 41 | const expected = { 42 | Id: 'an-id', 43 | Statement: [{ 44 | Sid: 'an-sid', 45 | Effect: 'Allow', 46 | Principal: undefined, 47 | NotPrincipal: undefined, 48 | Action: undefined, 49 | NotAction: undefined, 50 | Resource: undefined, 51 | NotResource: undefined, 52 | Condition: undefined, 53 | }], 54 | Version: '2012-10-17', 55 | }; 56 | expect(PolicyDocumentJSONSerialiser.toJSON(input)).to.deep.equal(expected); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/policy/sid-uniqueness.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {SidUniquenessValidator} from '../../src/policy/sid-uniqueness'; 3 | import {Statement} from '../../src/statement/statement'; 4 | 5 | describe('#SidUniquenessValidator', function() { 6 | describe('#validate', function() { 7 | describe('when statements is empty', function() { 8 | const validator = new SidUniquenessValidator([]); 9 | it('should return true when adding an empty statement', function() { 10 | expect(validator.validate(new Statement())).to.be.true; 11 | }); 12 | 13 | it('should return true when adding a statement having an Sid', function() { 14 | expect(validator.validate(new Statement({sid: 'an sid'}))).to.be.true; 15 | }); 16 | }); 17 | 18 | describe('when statements has one statement without Sid', function() { 19 | const validator = new SidUniquenessValidator([new Statement({resources: ['*']})]); 20 | it('should return true when adding an empty statement', function() { 21 | expect(validator.validate(new Statement())).to.be.true; 22 | }); 23 | 24 | it('should return true when adding a statement having an Sid', function() { 25 | expect(validator.validate(new Statement({sid: 'an sid'}))).to.be.true; 26 | }); 27 | }); 28 | 29 | describe('when statements has one statement with Sid', function() { 30 | const sid = 'an sid'; 31 | const validator = new SidUniquenessValidator([new Statement({sid: sid})]); 32 | it('should return true when adding an empty statement', function() { 33 | expect(validator.validate(new Statement())).to.be.true; 34 | }); 35 | 36 | it('should return false when adding a statement with the same Sid', function() { 37 | expect(validator.validate(new Statement({sid: sid}))).to.be.false; 38 | }); 39 | }); 40 | 41 | describe('when statements has one statement having an Sid', function() { 42 | const sid = 'an sid'; 43 | const validator = new SidUniquenessValidator([new Statement({sid: sid})]); 44 | it('should return true when adding an empty statement', function() { 45 | expect(validator.validate(new Statement())).to.be.true; 46 | }); 47 | 48 | it('should return false when adding a statement with the same Sid', function() { 49 | expect(validator.validate(new Statement({sid: sid}))).to.be.false; 50 | }); 51 | }); 52 | 53 | describe('when statements has 5 statements having an Sid', function() { 54 | const sid = 'the sid'; 55 | const validator = new SidUniquenessValidator([ 56 | new Statement({sid: 'sid1'}), 57 | new Statement({sid: 'sid2'}), 58 | new Statement({sid: sid}), 59 | new Statement({sid: 'sid4'}), 60 | new Statement({sid: 'sid5'}), 61 | ]); 62 | it('should return true when adding an empty statement', function() { 63 | expect(validator.validate(new Statement())).to.be.true; 64 | }); 65 | 66 | it('should return false when adding a statement having an existing Sid', function() { 67 | expect(validator.validate(new Statement({sid: sid}))).to.be.false; 68 | }); 69 | }); 70 | 71 | describe('when statements has a mix of statements having and having not an Sid', function() { 72 | const sid = 'the sid'; 73 | const validator = new SidUniquenessValidator([ 74 | new Statement({resources: ['arn:aws:ec2:eu-west-1:112233445566:instance/i-1234']}), 75 | new Statement({sid: 'sid2'}), 76 | new Statement({sid: sid}), 77 | new Statement({resources: ['arn:aws:ec2:eu-west-1:112233445566:instance/i-5678']}), 78 | new Statement({sid: 'sid5'}), 79 | ]); 80 | it('should return true when adding an empty statement', function() { 81 | expect(validator.validate(new Statement())).to.be.true; 82 | }); 83 | 84 | it('should return false when adding a statement having an existing Sid', function() { 85 | expect(validator.validate(new Statement({sid: sid}))).to.be.false; 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/principals/account.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {AccountPrincipal} from '../../src'; 3 | 4 | describe('#AccountPrincipal', function() { 5 | describe('#validate', function() { 6 | describe('when given a valid account ID', function() { 7 | it('should return the AWS account ID', function() { 8 | expect(AccountPrincipal.validate('012345678900')).to.deep.equal(new AccountPrincipal('012345678900')); 9 | }); 10 | }); 11 | 12 | describe('when given an invalid arn', function() { 13 | it('should return null', function() { 14 | expect(AccountPrincipal.validate('anARN')).to.be.undefined; 15 | }); 16 | }); 17 | 18 | describe('when given a valid IAM User arn', function() { 19 | it('should return undefined', function() { 20 | expect(AccountPrincipal.validate('arn:aws:iam::012345678900:user/aUser')).to.be.undefined; 21 | }); 22 | }); 23 | 24 | describe('when given a valid IAM Role arn', function() { 25 | it('should return null', function() { 26 | expect(AccountPrincipal.validate('arn:aws:iam::012345678900:role/aUser')).to.be.undefined; 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/principals/anonymous.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {AnonymousUserPrincipal} from '../../src'; 3 | 4 | describe('#AnonymousUserPrincipal', function() { 5 | describe('#toJSON', function() { 6 | const principle = new AnonymousUserPrincipal(); 7 | 8 | it('should return the AWS principal JSON fragment', function() { 9 | const expected = { 10 | 'AWS': '*', 11 | }; 12 | expect(principle.toJSON()).to.deep.equal(expected); 13 | }); 14 | }); 15 | 16 | describe('#validate', function() { 17 | describe('when given a valid IAM user arn', function() { 18 | it('should return the anonymous user arn', function() { 19 | const arn = '*'; 20 | expect(AnonymousUserPrincipal.validate(arn)).to.deep.equal(new AnonymousUserPrincipal()); 21 | }); 22 | }); 23 | 24 | describe('when given a string', function() { 25 | it('should return undefined', function() { 26 | expect(AnonymousUserPrincipal.validate('anARN')).to.be.undefined; 27 | }); 28 | }); 29 | 30 | describe('when given a wildcard user arn', function() { 31 | it('should return null', function() { 32 | const arn = 'arn:aws:iam::112233445566:user/*'; 33 | expect(AnonymousUserPrincipal.validate(arn)).to.be.undefined; 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/principals/arn.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {ArnPrincipal} from '../../src'; 3 | 4 | describe('#ArnPrincipal', function() { 5 | describe('#toJSON', function() { 6 | const arn = 'arn:aws:iam::123456789000:user/aUser'; 7 | const principal = new ArnPrincipal(arn); 8 | 9 | it('should return the AWS principal JSON fragment', function() { 10 | const expected = { 11 | 'AWS': arn, 12 | }; 13 | expect(principal.toJSON()).to.deep.equal(expected); 14 | }); 15 | }); 16 | 17 | describe('#validate', function() { 18 | describe('when given a valid IAM user arn', function() { 19 | it('should return the IAM user arn', function() { 20 | const arn = 'arn:aws:iam::012345678900:user/aUser'; 21 | expect(ArnPrincipal.validate(arn)).to.deep.equal(new ArnPrincipal(arn)); 22 | }); 23 | }); 24 | 25 | describe('when given a valid IAM role arn', function() { 26 | it('should return the IAM role arn', function() { 27 | const arn = 'arn:aws:iam::012345678900:user/aRole'; 28 | expect(ArnPrincipal.validate(arn)).to.deep.equal(new ArnPrincipal(arn)); 29 | }); 30 | }); 31 | 32 | describe('when given an invalid arn', function() { 33 | it('should return undefined', function() { 34 | expect(ArnPrincipal.validate('anARN')).to.be.undefined; 35 | }); 36 | }); 37 | 38 | describe('when given a valid root account arn', function() { 39 | it('should return the root account arn', function() { 40 | const arn = 'arn:aws:iam::012345678900:root'; 41 | expect(ArnPrincipal.validate(arn)).to.deep.equal(new ArnPrincipal(arn)); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/principals/cloudfront.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {CloudFrontPrincipal} from '../../src'; 3 | 4 | describe('#CloudFrontPrincipal', function() { 5 | describe('#toJSON', function() { 6 | const arn = 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E12345678ABCDE'; 7 | const principal = new CloudFrontPrincipal(arn); 8 | 9 | it('should return the AWS principal JSON fragment', function() { 10 | const expected = { 11 | 'AWS': arn, 12 | }; 13 | expect(principal.toJSON()).to.deep.equal(expected); 14 | }); 15 | }); 16 | 17 | describe('#validate', function() { 18 | describe('when given a valid CloudFront user arn', function() { 19 | it('should return the IAM user arn', function() { 20 | const arn = 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E1ABCDEFGHIJ'; 21 | expect(CloudFrontPrincipal.validate(arn)).to.deep.equal(new CloudFrontPrincipal(arn)); 22 | }); 23 | }); 24 | 25 | describe('when given an invalid arn with a valid CloudFront id', function() { 26 | it('should return undefined', function() { 27 | const arn = 'arn:aws:iam::cloudfront:user/World Origin Access Identity E1A2B3C4D5E6F'; 28 | expect(CloudFrontPrincipal.validate(arn)).to.be.undefined; 29 | }); 30 | }); 31 | 32 | describe('when given an valid arn with an invalid CloudFront id', function() { 33 | it('should return the root account arn', function() { 34 | const arn = 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity EABCDEFGHIJKLMNO'; 35 | expect(CloudFrontPrincipal.validate(arn)).to.be.undefined; 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/principals/deserialiser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {PrincipalJSONDeserialiser} from '../../src/principals/deserialiser'; 3 | import { 4 | AccountPrincipal, 5 | AnonymousUserPrincipal, 6 | UserPrincipal, 7 | RolePrincipal, 8 | CloudFrontPrincipal, 9 | FederatedPrincipal, 10 | RootAccountPrincipal, 11 | ServicePrincipal, 12 | WildcardPrincipal, 13 | } from '../../src'; 14 | 15 | describe('#PrincipalJSONDeserialise', function() { 16 | describe('#fromJSON', function() { 17 | describe('when having an undefined principal', function() { 18 | it('should return an empty principal array', function() { 19 | const input = undefined; 20 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([]); 21 | }); 22 | }); 23 | 24 | describe('when having an unknown principal', function() { 25 | it('should throw an error', function() { 26 | const input = {Unknown: ['unknown']}; 27 | expect(() => PrincipalJSONDeserialiser.fromJSON(input)).to.throw(Error) 28 | .with.property('message', 'Unsupported principal "Unknown"'); 29 | }); 30 | }); 31 | 32 | describe('when having an AWS principal', function() { 33 | describe('with one IAM User arn as a 1-length array', function() { 34 | it('should return one UserPrincipal', function() { 35 | const arn = 'arn:aws:iam::012345678900:user/aUser'; 36 | const input = {AWS: [arn]}; 37 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([new UserPrincipal('012345678900', 'aUser')]); 38 | }); 39 | }); 40 | 41 | describe('with one IAM User arn as a single string', function() { 42 | it('should return one UserPrincipal', function() { 43 | const arn = 'arn:aws:iam::012345678900:user/aUser'; 44 | const input = {AWS: arn}; 45 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([new UserPrincipal('012345678900', 'aUser')]); 46 | }); 47 | }); 48 | 49 | describe('with two ARNs', function() { 50 | it('should return a list having two ArnPrincipal', function() { 51 | const role1Arn = 'arn:aws:iam::111122223333:role/role1'; 52 | const role2Arn = 'arn:aws:iam::111122223333:role/role2'; 53 | const input = {AWS: [role1Arn, role2Arn]}; 54 | const expected = [new RolePrincipal('111122223333', 'role1'), new RolePrincipal('111122223333', 'role2')]; 55 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 56 | }); 57 | }); 58 | 59 | describe('with an IAM user, a root user and an account ID', function() { 60 | it('should return a list having a UserPrincipal, RootAccountPrincipal and AccountPrincipal', function() { 61 | const accountID = '012345678900'; 62 | const userArn = `arn:aws:iam::${accountID}:user/aUser`; 63 | const rootAccountArn = `arn:aws:iam::${accountID}:root`; 64 | const input = {AWS: [userArn, rootAccountArn, accountID]}; 65 | const expected = [ 66 | new UserPrincipal(accountID, 'aUser'), 67 | new RootAccountPrincipal(accountID), 68 | new AccountPrincipal(accountID), 69 | ]; 70 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 71 | }); 72 | }); 73 | 74 | describe('with an IAM Role arn', function() { 75 | it('should return one AnonymousUserPrincipal', function() { 76 | const input = {AWS: ['arn:aws:iam::123456789000:role/aRole']}; 77 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([new RolePrincipal('123456789000', 'aRole')]); 78 | }); 79 | }); 80 | 81 | describe('with an anonymous user principal', function() { 82 | it('should return one AnonymousUserPrincipal', function() { 83 | const input = {AWS: ['*']}; 84 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([new AnonymousUserPrincipal()]); 85 | }); 86 | }); 87 | 88 | describe('with a CloudFront user', function() { 89 | it('should return a CloudFrontPrincipal', function() { 90 | const validCloudFrontIds = [ 91 | 'E1ABCDEFGHIJ', 92 | 'E12345ABCDE', 93 | 'EABCD1234567', 94 | 'E1A2B3C4D5E6F', 95 | 'E12345678ABCDE', 96 | ]; 97 | for (const validCloudFrontId of validCloudFrontIds) { 98 | const arn = `arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${validCloudFrontId}`; 99 | const input = {AWS: arn}; 100 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([new CloudFrontPrincipal(arn)]); 101 | } 102 | }); 103 | it('should fail with invalid CloudFront ids', function() { 104 | const invalidCloudFrontIds = [ 105 | 'EABCDEFGHIJKL', 106 | 'E123456789', 107 | 'EABCDEFGHIJKLMNO', 108 | '1EABC1234567', 109 | 'EFGHJKLMNOP', 110 | ]; 111 | let arn: string | undefined; 112 | for (const invalidCloudFrontId of invalidCloudFrontIds) { 113 | arn = `arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${invalidCloudFrontId}`; 114 | const input = {AWS: arn}; 115 | try { 116 | PrincipalJSONDeserialiser.fromJSON(input); 117 | } catch (error) { 118 | const error_ = error as Error; 119 | if (error_.message !== `Unsupported AWS principal value "${arn}"`) { 120 | throw error; 121 | } 122 | } 123 | } 124 | }); 125 | }); 126 | 127 | describe('with an unsupported ARN', function() { 128 | it('should throw an error', function() { 129 | expect(() => PrincipalJSONDeserialiser.fromJSON({AWS: ['anArn']})).to.throw(Error) 130 | .with.property('message', 'Unsupported AWS principal value "anArn"'); 131 | }); 132 | }); 133 | }); 134 | 135 | describe('when having a Service principal', function() { 136 | describe('with one service', function() { 137 | it('should return one ServicePrincipal', function() { 138 | const input = {Service: ['aService']}; 139 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([new ServicePrincipal('aService')]); 140 | }); 141 | }); 142 | 143 | describe('with two services', function() { 144 | it('should return a list having two ServicePrincipal', function() { 145 | const input = {Service: ['service1', 'service2']}; 146 | const expected = [new ServicePrincipal('service1'), new ServicePrincipal('service2')]; 147 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 148 | }); 149 | }); 150 | }); 151 | 152 | 153 | describe('when having a Federated principal', function() { 154 | it('should return one FederatedPrincipal', function() { 155 | const input = {Federated: ['www.amazon.com']}; 156 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal([new FederatedPrincipal('www.amazon.com')]); 157 | }); 158 | }); 159 | 160 | describe('when having both an AWS and Service principal', function() { 161 | it('should return a list having an ArnPrincipal and a ServicePrincipal', function() { 162 | const arn = 'arn:aws:iam::123456789012:user/aUser'; 163 | const input = {AWS: [arn], Service: ['aService']}; 164 | const expected = [new UserPrincipal('123456789012', 'aUser'), new ServicePrincipal('aService')]; 165 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 166 | }); 167 | }); 168 | 169 | describe('when having an anonymous principal', function() { 170 | it('should return an AnonymousPrincipal', function() { 171 | const input = '*'; 172 | const expected = [new WildcardPrincipal()]; 173 | expect(PrincipalJSONDeserialiser.fromJSON(input)).to.deep.equal(expected); 174 | }); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /tests/principals/federated.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {FederatedPrincipal} from '../../src'; 3 | 4 | describe('#ServicePrincipal', function() { 5 | describe('#toJSON', function() { 6 | const identityProvider = 'www.amazon.com'; 7 | const policy = new FederatedPrincipal(identityProvider); 8 | 9 | it('should return the AWS federated principal JSON fragment', function() { 10 | const expected = { 11 | 'Federated': identityProvider, 12 | }; 13 | expect(policy.toJSON()).to.deep.equal(expected); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/principals/role.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {RolePrincipal} from '../../src'; 3 | 4 | describe('#RolePrincipal', function() { 5 | describe('#toJSON', function() { 6 | describe('a role with default path', function() { 7 | const principal = new RolePrincipal('123456789000', 'aRole'); 8 | it('should return the AWS principal JSON fragment', function() { 9 | const expected = { 10 | 'AWS': 'arn:aws:iam::123456789000:role/aRole', 11 | }; 12 | expect(principal.toJSON()).to.deep.equal(expected); 13 | }); 14 | }); 15 | 16 | describe('a role with a path', function() { 17 | const principal = new RolePrincipal('123456789000', 'aRole', '/aPath/'); 18 | it('should return the AWS principal JSON fragment', function() { 19 | const expected = { 20 | 'AWS': 'arn:aws:iam::123456789000:role/aPath/aRole', 21 | }; 22 | expect(principal.toJSON()).to.deep.equal(expected); 23 | }); 24 | }); 25 | }); 26 | describe('#validate', function() { 27 | describe('when given a valid IAM role arn without path', function() { 28 | it('should return the IAM role principal', function() { 29 | const accountId = '012345678900'; 30 | const userName = 'aRole'; 31 | const arn = `arn:aws:iam::${accountId}:role/${userName}`; 32 | expect(RolePrincipal.validate(arn)).to.deep.equal(new RolePrincipal(accountId, userName)); 33 | }); 34 | }); 35 | describe('when given a valid IAM role arn with path', function() { 36 | it('should return the IAM role principal', function() { 37 | const accountId = '012345678900'; 38 | const userName = 'aRole'; 39 | const path = '/aPath/'; 40 | const arn = `arn:aws:iam::${accountId}:role${path}${userName}`; 41 | expect(RolePrincipal.validate(arn)).to.deep.equal(new RolePrincipal(accountId, userName, path)); 42 | }); 43 | }); 44 | describe('when given an invalid IAM role arn having 11 digits for account id', function() { 45 | it('should return the IAM role principal', function() { 46 | const accountId = '01234567890'; 47 | const arn = `arn:aws:iam::${accountId}:role/aRole`; 48 | expect(RolePrincipal.validate(arn)).to.be.undefined; 49 | }); 50 | }); 51 | describe('when given an invalid IAM role arn having 13 digits for account id', function() { 52 | it('should return the IAM role principal', function() { 53 | const accountId = '0123456789001'; 54 | const arn = `arn:aws:iam::${accountId}:role/aRole`; 55 | expect(RolePrincipal.validate(arn)).to.be.undefined; 56 | }); 57 | }); 58 | describe('when given an invalid IAM role arn having alpha numerical characters for account id', function() { 59 | it('should return the IAM role principal', function() { 60 | const accountId = 'a12345678900'; 61 | const arn = `arn:aws:iam::${accountId}:role/aRole`; 62 | expect(RolePrincipal.validate(arn)).to.be.undefined; 63 | }); 64 | }); 65 | describe('when given a valid IAM role arn having valid characters for user name', function() { 66 | it('should return the IAM role principal', function() { 67 | const accountId = '012345678900'; 68 | const userName = 'a_user_with_valid_char=0123456789@AbCdEfGhIjKlMnOpQrStUvWxYz.+-'; 69 | const arn = `arn:aws:iam::${accountId}:role/${userName}`; 70 | expect(RolePrincipal.validate(arn)).to.deep.equal(new RolePrincipal(accountId, userName)); 71 | }); 72 | }); 73 | describe('when given a valid IAM role arn having 64 characters for user name', function() { 74 | it('should return the IAM role principal', function() { 75 | const accountId = '012345678900'; 76 | const userName = 'x'.repeat(64); 77 | const arn = `arn:aws:iam::${accountId}:role/${userName}`; 78 | expect(RolePrincipal.validate(arn)).to.be.deep.equal(new RolePrincipal(accountId, userName)); 79 | }); 80 | }); 81 | describe('when given an invalid IAM role arn having 65 characters fore user name', function() { 82 | it('should return undefined', function() { 83 | const userName = 'x'.repeat(65); 84 | const arn = `arn:aws:iam::0123456789001:role/${userName}`; 85 | expect(RolePrincipal.validate(arn)).to.be.undefined; 86 | }); 87 | }); 88 | describe('when given a valid IAM role arn having valid characters for path', function() { 89 | it('should return the IAM role principal', function() { 90 | const accountId = '012345678900'; 91 | const userName = 'aRole'; 92 | const path = '/a/path/with/valid/characters/@=+._-/'; 93 | const arn = `arn:aws:iam::${accountId}:role${path}${userName}`; 94 | expect(RolePrincipal.validate(arn)).to.be.deep.equal(new RolePrincipal(accountId, userName, path)); 95 | }); 96 | }); 97 | describe('when given a valid IAM role arn having 512 characters for path', function() { 98 | it('should return the IAM role principal', function() { 99 | const accountId = '012345678900'; 100 | const userName = 'aRole'; 101 | const path = '/' + 'x'.repeat(510) + '/'; 102 | const arn = `arn:aws:iam::${accountId}:role${path}${userName}`; 103 | expect(RolePrincipal.validate(arn)).to.be.deep.equal(new RolePrincipal(accountId, userName, path)); 104 | }); 105 | }); 106 | describe('when given an valid IAM role arn having 513 characters for path', function() { 107 | it('should return the IAM role principal', function() { 108 | const path = '/' + 'x'.repeat(511) + '/'; 109 | const arn = `arn:aws:iam::0123456789001:role${path}aRole`; 110 | expect(RolePrincipal.validate(arn)).to.be.undefined; 111 | }); 112 | }); 113 | describe('when given a valid IAM user arn', function() { 114 | it('should return undefined', function() { 115 | const arn = 'arn:aws:iam::012345678900:user/aUser'; 116 | expect(RolePrincipal.validate(arn)).to.be.undefined; 117 | }); 118 | }); 119 | describe('when given a valid root account arn', function() { 120 | it('should return undefined', function() { 121 | const arn = 'arn:aws:iam::012345678900:root'; 122 | expect(RolePrincipal.validate(arn)).to.be.undefined; 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /tests/principals/root-account.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {RootAccountPrincipal} from '../../src'; 3 | 4 | describe('#RootAccountPrincipal', function() { 5 | describe('#toJSON', function() { 6 | const account = '123456789000'; 7 | const principal = new RootAccountPrincipal(account); 8 | 9 | it('should return the AWS principal JSON fragment', function() { 10 | const expected = { 11 | 'AWS': 'arn:aws:iam::123456789000:root', 12 | }; 13 | expect(principal.toJSON()).to.deep.equal(expected); 14 | }); 15 | }); 16 | 17 | describe('#validate', function() { 18 | describe('when given a valid root account arn', function() { 19 | it('should return the AWS account arn', function() { 20 | const arn = 'arn:aws:iam::012345678900:root'; 21 | expect(RootAccountPrincipal.validate(arn)) 22 | .to.deep.equal(new RootAccountPrincipal('012345678900')); 23 | }); 24 | }); 25 | 26 | describe('when given a valid IAM User arn', function() { 27 | it('should return undefined', function() { 28 | expect(RootAccountPrincipal.validate('arn:aws:iam::012345678900:user/aUser')).to.be.undefined; 29 | }); 30 | }); 31 | 32 | describe('when given a valid IAM Role arn', function() { 33 | it('should return undefined', function() { 34 | expect(RootAccountPrincipal.validate('arn:aws:iam::012345678900:role/aRole')).to.be.undefined; 35 | }); 36 | }); 37 | 38 | describe('when given an invalid arn', function() { 39 | it('should return undefined', function() { 40 | expect(RootAccountPrincipal.validate('anARN')).to.be.undefined; 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/principals/serialiser.spec.ts: -------------------------------------------------------------------------------- 1 | // import {expect} from 'chai'; 2 | 3 | import {expect} from 'chai'; 4 | import {PrincipalJSONSerialiser} from '../../src/principals/serialiser'; 5 | import { 6 | AccountPrincipal, 7 | ArnPrincipal, 8 | UserPrincipal, 9 | RolePrincipal, 10 | Principal, 11 | ServicePrincipal, 12 | FederatedPrincipal, 13 | } from '../../src'; 14 | 15 | describe('#PrincipalJSONSerialiser', function() { 16 | describe('#toJSON', function() { 17 | describe('when having an empty list of principals', function() { 18 | const principals: Principal[] = []; 19 | it('should return undefined', function() { 20 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.be.undefined; 21 | }); 22 | }); 23 | 24 | describe('when having one AWS principal', function() { 25 | const arn = 'arn:aws:iam::012345678900:user/aUser'; 26 | const principals = [new ArnPrincipal(arn)]; 27 | it('should return a JSON object having an AWS property having one string', function() { 28 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({AWS: arn}); 29 | }); 30 | }); 31 | 32 | describe('when having one IAM User as AWS principal', function() { 33 | const accountId = '012345678900'; 34 | const userName ='aUser'; 35 | const arn = `arn:aws:iam::${accountId}:user/${userName}`; 36 | const principals = [new UserPrincipal(accountId, userName)]; 37 | it('should return a JSON object having an AWS property having one string', function() { 38 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({AWS: arn}); 39 | }); 40 | }); 41 | 42 | describe('when having one IAM Role as AWS principal', function() { 43 | const accountId = '012345678900'; 44 | const userName ='aUser'; 45 | const arn = `arn:aws:iam::${accountId}:role/${userName}`; 46 | const principals = [new RolePrincipal(accountId, userName)]; 47 | it('should return a JSON object having an AWS property having one string', function() { 48 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({AWS: arn}); 49 | }); 50 | }); 51 | 52 | describe('when having one Service principal', function() { 53 | const service = 'aservice.amazonaws.com'; 54 | const principals = [new ServicePrincipal(service)]; 55 | it('should return a JSON object having a Service property having one string', function() { 56 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({Service: service}); 57 | }); 58 | }); 59 | 60 | describe('when having one Federated principal', function() { 61 | const identityProvider = 'www.amazon.com'; 62 | const principals = [new FederatedPrincipal(identityProvider)]; 63 | it('should return a JSON object having a Federated property having one string', function() { 64 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({Federated: identityProvider}); 65 | }); 66 | }); 67 | 68 | describe('when having an AWS and Service principal', function() { 69 | const arn = 'arn:aws:iam::012345678900:user/aUser'; 70 | const service = 'aservice.amazonaws.com'; 71 | const principals = [new ArnPrincipal(arn), new ServicePrincipal(service)]; 72 | it('should return a JSON object having an AWS and Service property each having one string', 73 | function() { 74 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({AWS: arn, Service: service}); 75 | }); 76 | }); 77 | 78 | describe('when having two AWS principals', function() { 79 | const accountID = '012345678900'; 80 | const userArn = `arn:aws:iam::${accountID}:user/user1`; 81 | const principals = [new ArnPrincipal(userArn), new AccountPrincipal(accountID)]; 82 | it('should return a JSON object having an AWS property having a two item string array', function() { 83 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({AWS: [userArn, accountID]}); 84 | }); 85 | }); 86 | 87 | describe('when having two identical AWS principals', function() { 88 | const userArn = `arn:aws:iam::111122223333:role/aRole`; 89 | const principals = [new ArnPrincipal(userArn), new ArnPrincipal(userArn)]; 90 | it('should return a JSON object having an AWS property having a two item string array', function() { 91 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({AWS: [userArn, userArn]}); 92 | }); 93 | }); 94 | 95 | describe('when having two Service principals', function() { 96 | const service1 = 'service1.amazonaws.com'; 97 | const service2 = 'service2.amazonaws.com'; 98 | const principals = [new ServicePrincipal(service1), new ServicePrincipal(service2)]; 99 | it('should return a JSON object having an Service property having a two item string array', function() { 100 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({Service: [service1, service2]}); 101 | }); 102 | }); 103 | 104 | describe('when having two identical Service principals', function() { 105 | const service = 'aservice.amazonaws.com'; 106 | const principals = [new ServicePrincipal(service), new ServicePrincipal(service)]; 107 | it('should return a JSON object having an Service property having a two item string array', function() { 108 | expect(PrincipalJSONSerialiser.toJSON(principals)).to.deep.equal({Service: [service, service]}); 109 | }); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/principals/service.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {ServicePrincipal} from '../../src'; 3 | 4 | describe('#ServicePrincipal', function() { 5 | describe('#toJSON', function() { 6 | const service = 'aservice.amazonaws.com'; 7 | const policy = new ServicePrincipal(service); 8 | 9 | it('should return the AWS service principal JSON fragment', function() { 10 | const expected = { 11 | 'Service': service, 12 | }; 13 | expect(policy.toJSON()).to.deep.equal(expected); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/principals/user.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {UserPrincipal} from '../../src'; 3 | 4 | describe('#UserPrincipal', function() { 5 | describe('#toJSON', function() { 6 | describe('a user with default path', function() { 7 | const principal = new UserPrincipal('123456789000', 'aUser'); 8 | it('should return the AWS principal JSON fragment', function() { 9 | const expected = { 10 | 'AWS': 'arn:aws:iam::123456789000:user/aUser', 11 | }; 12 | expect(principal.toJSON()).to.deep.equal(expected); 13 | }); 14 | }); 15 | 16 | describe('a user with a path', function() { 17 | const principal = new UserPrincipal('123456789000', 'aUser', '/aPath/'); 18 | it('should return the AWS principal JSON fragment', function() { 19 | const expected = { 20 | 'AWS': 'arn:aws:iam::123456789000:user/aPath/aUser', 21 | }; 22 | expect(principal.toJSON()).to.deep.equal(expected); 23 | }); 24 | }); 25 | }); 26 | describe('#validate', function() { 27 | describe('when given a valid IAM user arn without path', function() { 28 | it('should return the IAM user principal', function() { 29 | const accountId = '012345678900'; 30 | const userName = 'aUser'; 31 | const arn = `arn:aws:iam::${accountId}:user/${userName}`; 32 | expect(UserPrincipal.validate(arn)).to.deep.equal(new UserPrincipal(accountId, userName)); 33 | }); 34 | }); 35 | describe('when given a valid IAM user arn with path', function() { 36 | it('should return the IAM user principal', function() { 37 | const accountId = '012345678900'; 38 | const userName = 'aUser'; 39 | const path = '/aPath/'; 40 | const arn = `arn:aws:iam::${accountId}:user${path}${userName}`; 41 | expect(UserPrincipal.validate(arn)).to.deep.equal(new UserPrincipal(accountId, userName, path)); 42 | }); 43 | }); 44 | describe('when given an invalid IAM user arn having 11 digits for account id', function() { 45 | it('should return the IAM user principal', function() { 46 | const accountId = '01234567890'; 47 | const arn = `arn:aws:iam::${accountId}:user/aUser`; 48 | expect(UserPrincipal.validate(arn)).to.be.undefined; 49 | }); 50 | }); 51 | describe('when given an invalid IAM user arn having 13 digits for account id', function() { 52 | it('should return the IAM user principal', function() { 53 | const accountId = '0123456789001'; 54 | const arn = `arn:aws:iam::${accountId}:user/aUser`; 55 | expect(UserPrincipal.validate(arn)).to.be.undefined; 56 | }); 57 | }); 58 | describe('when given an invalid IAM user arn having alpha numerical characters for account id', function() { 59 | it('should return the IAM user principal', function() { 60 | const accountId = 'a12345678900'; 61 | const arn = `arn:aws:iam::${accountId}:user/aUser`; 62 | expect(UserPrincipal.validate(arn)).to.be.undefined; 63 | }); 64 | }); 65 | describe('when given a valid IAM user arn having valid characters for user name', function() { 66 | it('should return the IAM user principal', function() { 67 | const accountId = '012345678900'; 68 | const userName = 'a_user_with_valid_char=0123456789@AbCdEfGhIjKlMnOpQrStUvWxYz.+-'; 69 | const arn = `arn:aws:iam::${accountId}:user/${userName}`; 70 | expect(UserPrincipal.validate(arn)).to.deep.equal(new UserPrincipal(accountId, userName)); 71 | }); 72 | }); 73 | describe('when given a valid IAM user arn having 64 characters for user name', function() { 74 | it('should return the IAM user principal', function() { 75 | const accountId = '012345678900'; 76 | const userName = 'x'.repeat(64); 77 | const arn = `arn:aws:iam::${accountId}:user/${userName}`; 78 | expect(UserPrincipal.validate(arn)).to.be.deep.equal(new UserPrincipal(accountId, userName)); 79 | }); 80 | }); 81 | describe('when given an invalid IAM user arn having 65 characters fore user name', function() { 82 | it('should return undefined', function() { 83 | const userName = 'x'.repeat(65); 84 | const arn = `arn:aws:iam::0123456789001:user/${userName}`; 85 | expect(UserPrincipal.validate(arn)).to.be.undefined; 86 | }); 87 | }); 88 | describe('when given a valid IAM user arn having valid characters for path', function() { 89 | it('should return the IAM user principal', function() { 90 | const accountId = '012345678900'; 91 | const userName = 'aUser'; 92 | const path = '/a/path/with/valid/characters/@=+._-/'; 93 | const arn = `arn:aws:iam::${accountId}:user${path}${userName}`; 94 | expect(UserPrincipal.validate(arn)).to.be.deep.equal(new UserPrincipal(accountId, userName, path)); 95 | }); 96 | }); 97 | describe('when given a valid IAM user arn having 512 characters for path', function() { 98 | it('should return the IAM user principal', function() { 99 | const accountId = '012345678900'; 100 | const userName = 'aUser'; 101 | const path = '/' + 'x'.repeat(510) + '/'; 102 | const arn = `arn:aws:iam::${accountId}:user${path}${userName}`; 103 | expect(UserPrincipal.validate(arn)).to.be.deep.equal(new UserPrincipal(accountId, userName, path)); 104 | }); 105 | }); 106 | describe('when given an valid IAM user arn having 513 characters for path', function() { 107 | it('should return the IAM user principal', function() { 108 | const path = '/' + 'x'.repeat(511) + '/'; 109 | const arn = `arn:aws:iam::0123456789001:user${path}aUser`; 110 | expect(UserPrincipal.validate(arn)).to.be.undefined; 111 | }); 112 | }); 113 | describe('when given a valid IAM role arn', function() { 114 | it('should return undefined', function() { 115 | const arn = 'arn:aws:iam::012345678900:role/aRole'; 116 | expect(UserPrincipal.validate(arn)).to.be.undefined; 117 | }); 118 | }); 119 | describe('when given a valid root account arn', function() { 120 | it('should return undefined', function() { 121 | const arn = 'arn:aws:iam::012345678900:root'; 122 | expect(UserPrincipal.validate(arn)).to.be.undefined; 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /tests/principals/wildcard.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {WildcardPrincipal} from '../../src'; 3 | 4 | describe('#WildcardPrincipal', function() { 5 | describe('#toJSON', function() { 6 | const principal = new WildcardPrincipal(); 7 | 8 | it('should return the wildcard principal JSON fragment', function() { 9 | const expected = '*'; 10 | expect(principal.toJSON()).to.equal(expected); 11 | }); 12 | }); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /tests/statement/deserialiser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {StatementJSONDeserialiser} from '../../src/statement/deserialiser'; 3 | import { 4 | Statement, 5 | UserPrincipal, 6 | RolePrincipal, 7 | AccountPrincipal, 8 | RootAccountPrincipal, 9 | WildcardPrincipal, 10 | Condition, 11 | } from '../../src'; 12 | 13 | describe('#StatementDeserialiser', function() { 14 | describe('when JSON is empty', function() { 15 | const json = {}; 16 | it('should return an empty Statement', function() { 17 | const expected = new Statement(); 18 | expect(StatementJSONDeserialiser.fromJSON(json)).to.deep.equal(expected); 19 | }); 20 | }); 21 | 22 | describe('when JSON has an Action', function() { 23 | describe('and its value is an object', function() { 24 | const json = { 25 | Action: {property: 'value'}, 26 | }; 27 | it('should throw an Error', function() { 28 | expect(() => StatementJSONDeserialiser.fromJSON(json)).to.throw(Error) 29 | .with.property('message', 'Unsupported type: expecting an array or a string'); 30 | }); 31 | }); 32 | 33 | describe('and its value is a string', function() { 34 | const json = { 35 | Action: 'action', 36 | }; 37 | it('should return a Statement with actions', function() { 38 | const actual = StatementJSONDeserialiser.fromJSON(json); 39 | const expected = new Statement({ 40 | actions: ['action'], 41 | }); 42 | expect(actual).to.deep.equal(expected); 43 | }); 44 | }); 45 | 46 | describe('and its value is an array of strings', function() { 47 | const json = { 48 | Action: ['action'], 49 | }; 50 | it('should return a Statement with actions', function() { 51 | const expected = new Statement({actions: ['action']}); 52 | expect(StatementJSONDeserialiser.fromJSON(json)).to.deep.equal(expected); 53 | }); 54 | }); 55 | 56 | describe('and its value is an array of numbers', function() { 57 | const json = { 58 | Action: [123], 59 | }; 60 | it('should throw an Error', function() { 61 | expect(() => StatementJSONDeserialiser.fromJSON(json)).to.throw(Error) 62 | .with.property('message', 'Unsupported type: expecting an array of strings'); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('when JSON has a Resource', function() { 68 | describe('and its value is an object', function() { 69 | const json = { 70 | Resource: {property: 'value'}, 71 | }; 72 | it('should throw an Error', function() { 73 | expect(() => StatementJSONDeserialiser.fromJSON(json)).to.throw(Error) 74 | .with.property('message', 'Unsupported type: expecting an array or a string'); 75 | }); 76 | }); 77 | 78 | describe('and its value is a string', function() { 79 | const json = { 80 | Resource: 'resource', 81 | }; 82 | it('should return a Statement with resources', function() { 83 | const actual = StatementJSONDeserialiser.fromJSON(json); 84 | const expected = new Statement({ 85 | resources: ['resource'], 86 | }); 87 | expect(actual).to.deep.equal(expected); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('when JSON has a NotResource', function() { 93 | describe('and its value is an object', function() { 94 | const json = { 95 | NotResource: {property: 'value'}, 96 | }; 97 | it('should throw an Error', function() { 98 | expect(() => StatementJSONDeserialiser.fromJSON(json)).to.throw(Error) 99 | .with.property('message', 'Unsupported type: expecting an array or a string'); 100 | }); 101 | }); 102 | 103 | describe('and its value is a string', function() { 104 | const json = { 105 | NotResource: 'resource', 106 | }; 107 | it('should return a Statement with notresources', function() { 108 | const actual = StatementJSONDeserialiser.fromJSON(json); 109 | const expected = new Statement({ 110 | notresources: ['resource'], 111 | }); 112 | expect(actual).to.deep.equal(expected); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('when JSON has a Principal', function() { 118 | describe('with an AWS principal type', function() { 119 | describe('and its value is a string', function() { 120 | it('should return a Statement with principals', function() { 121 | const json = { 122 | Principal: {AWS: '123456789012'}, 123 | }; 124 | const actual = StatementJSONDeserialiser.fromJSON(json); 125 | const expected = new Statement({ 126 | principals: [new AccountPrincipal('123456789012')], 127 | }); 128 | expect(actual).to.deep.equal(expected); 129 | }); 130 | }); 131 | 132 | describe('and its value is a 1-length array', function() { 133 | it('should return a Statement with principals', function() { 134 | const json = { 135 | Principal: {AWS: ['123456789012']}, 136 | }; 137 | const actual = StatementJSONDeserialiser.fromJSON(json); 138 | const expected = new Statement({ 139 | principals: [new AccountPrincipal('123456789012')], 140 | }); 141 | expect(actual).to.deep.equal(expected); 142 | }); 143 | }); 144 | 145 | describe('and its value is a 2-length array', function() { 146 | it('should return a Statement with principals', function() { 147 | const json = { 148 | Principal: {AWS: [ 149 | '123456789012', 150 | 'arn:aws:iam::123456789012:user/foo', 151 | ]}, 152 | }; 153 | const actual = StatementJSONDeserialiser.fromJSON(json); 154 | const expected = new Statement({ 155 | principals: [ 156 | new AccountPrincipal('123456789012'), 157 | new UserPrincipal('123456789012', 'foo'), 158 | ], 159 | }); 160 | expect(actual).to.deep.equal(expected); 161 | }); 162 | }); 163 | 164 | describe('and its value is an IAM role arn', function() { 165 | it('should return a Statement with UserPrincipal', function() { 166 | const json = { 167 | Principal: {AWS: 'arn:aws:iam::123456789012:user/aUser'}, 168 | }; 169 | const actual = StatementJSONDeserialiser.fromJSON(json); 170 | const expected = new Statement({ 171 | principals: [ 172 | new UserPrincipal('123456789012', 'aUser'), 173 | ], 174 | }); 175 | expect(actual).to.deep.equal(expected); 176 | }); 177 | }); 178 | 179 | describe('and its value is an AWS root account user arn', function() { 180 | it('should return a Statement with RootAccountPrincipal', function() { 181 | const json = { 182 | Principal: {AWS: 'arn:aws:iam::123456789012:root'}, 183 | }; 184 | const actual = StatementJSONDeserialiser.fromJSON(json); 185 | const expected = new Statement({ 186 | principals: [ 187 | new RootAccountPrincipal('123456789012'), 188 | ], 189 | }); 190 | expect(actual).to.deep.equal(expected); 191 | }); 192 | }); 193 | 194 | describe('and its value is an IAM role arn', function() { 195 | it('should return a Statement with RolePrincipal', function() { 196 | const json = { 197 | Principal: {AWS: 'arn:aws:iam::123456789012:role/aRole'}, 198 | }; 199 | const actual = StatementJSONDeserialiser.fromJSON(json); 200 | const expected = new Statement({ 201 | principals: [ 202 | new RolePrincipal('123456789012', 'aRole'), 203 | ], 204 | }); 205 | expect(actual).to.deep.equal(expected); 206 | }); 207 | }); 208 | }); 209 | 210 | describe('with a wildcard principal', function() { 211 | it('should return a Statement with an AnonymousPrincipal', function() { 212 | const json = { 213 | Principal: '*', 214 | }; 215 | const actual = StatementJSONDeserialiser.fromJSON(json); 216 | const expected = new Statement({ 217 | principals: [new WildcardPrincipal()], 218 | }); 219 | expect(actual).to.deep.equal(expected); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('when JSON has a NotPrincipal', function() { 225 | describe('with an AWS principal type', function() { 226 | describe('and its value is a string', function() { 227 | it('should return a Statement with principals', function() { 228 | const json = { 229 | NotPrincipal: {AWS: '123456789012'}, 230 | }; 231 | const actual = StatementJSONDeserialiser.fromJSON(json); 232 | const expected = new Statement({ 233 | notprincipals: [new AccountPrincipal('123456789012')], 234 | }); 235 | expect(actual).to.deep.equal(expected); 236 | }); 237 | }); 238 | 239 | describe('and its value is a 1-length array', function() { 240 | it('should return a Statement with principals', function() { 241 | const json = { 242 | NotPrincipal: {AWS: ['123456789012']}, 243 | }; 244 | const actual = StatementJSONDeserialiser.fromJSON(json); 245 | const expected = new Statement({ 246 | notprincipals: [new AccountPrincipal('123456789012')], 247 | }); 248 | expect(actual).to.deep.equal(expected); 249 | }); 250 | }); 251 | 252 | describe('and its value is a 2-length array', function() { 253 | it('should return a Statement with principals', function() { 254 | const json = { 255 | NotPrincipal: {AWS: [ 256 | '123456789012', 257 | 'arn:aws:iam::123456789012:user/foo', 258 | ]}, 259 | }; 260 | const actual = StatementJSONDeserialiser.fromJSON(json); 261 | const expected = new Statement({ 262 | notprincipals: [ 263 | new AccountPrincipal('123456789012'), 264 | new UserPrincipal('123456789012', 'foo'), 265 | ], 266 | }); 267 | expect(actual).to.deep.equal(expected); 268 | }); 269 | }); 270 | }); 271 | }); 272 | 273 | describe('when JSON has a Condition', function() { 274 | describe('with one operator', function() { 275 | describe('and one key', function() { 276 | describe('and its key value is a string', function() { 277 | it('should return a Statement with conditions', function() { 278 | const json = { 279 | Condition: {StringEquals: {'aws:username': 'johndoe'}}, 280 | }; 281 | const actual = StatementJSONDeserialiser.fromJSON(json); 282 | const expected = new Statement({ 283 | conditions: [new Condition('StringEquals', 'aws:username', ['johndoe'])], 284 | }); 285 | expect(actual).to.deep.equal(expected); 286 | }); 287 | }); 288 | 289 | describe('and its value is a 1-length array', function() { 290 | it('should return a Statement with conditions', function() { 291 | const json = { 292 | Condition: {StringEquals: {'aws:username': ['johndoe']}}, 293 | }; 294 | const actual = StatementJSONDeserialiser.fromJSON(json); 295 | const expected = new Statement({ 296 | conditions: [new Condition('StringEquals', 'aws:username', ['johndoe'])], 297 | }); 298 | expect(actual).to.deep.equal(expected); 299 | }); 300 | }); 301 | 302 | describe('and its value is a 2-length array', function() { 303 | it('should return a Statement with principals', function() { 304 | const json = { 305 | Condition: {StringEquals: {'aws:username': ['johndoe', 'joesixpack']}}, 306 | }; 307 | const actual = StatementJSONDeserialiser.fromJSON(json); 308 | const expected = new Statement({ 309 | conditions: [new Condition('StringEquals', 'aws:username', ['johndoe', 'joesixpack'])], 310 | }); 311 | expect(actual).to.deep.equal(expected); 312 | }); 313 | }); 314 | }); 315 | }); 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /tests/statement/serialiser.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {StatementJSONSerialiser} from '../../src/statement/serialiser'; 3 | import { 4 | Statement, 5 | ArnPrincipal, 6 | WildcardPrincipal, 7 | Condition, 8 | } from '../../src'; 9 | 10 | describe('#StatementJSONSerialiser', function() { 11 | describe('when statement is empty', function() { 12 | const statement = new Statement(); 13 | it('should return a JSON object with default Effect and undefined properties', function() { 14 | const expected = { 15 | Sid: undefined, 16 | Effect: 'Allow', 17 | Principal: undefined, 18 | NotPrincipal: undefined, 19 | Action: undefined, 20 | NotAction: undefined, 21 | Resource: undefined, 22 | NotResource: undefined, 23 | Condition: undefined, 24 | }; 25 | expect(StatementJSONSerialiser.toJSON(statement)).to.deep.equal(expected); 26 | }); 27 | }); 28 | 29 | describe('when statement has an Sid', function() { 30 | const statement = new Statement({sid: 'an sid'}); 31 | it('should return a JSON object with an Sid', function() { 32 | const expected = { 33 | Sid: 'an sid', 34 | Effect: 'Allow', 35 | Principal: undefined, 36 | NotPrincipal: undefined, 37 | Action: undefined, 38 | NotAction: undefined, 39 | Resource: undefined, 40 | NotResource: undefined, 41 | Condition: undefined, 42 | }; 43 | expect(StatementJSONSerialiser.toJSON(statement)).to.deep.equal(expected); 44 | }); 45 | }); 46 | 47 | describe('when statement has an empty array for Principal, NotPrincipal, Action and Resource', function() { 48 | const statement = new Statement({principals: [], notprincipals: [], actions: [], resources: []}); 49 | it('should return a JSON object with undefined Principal, NotPrincipal, Action and Resource', function() { 50 | const expected = { 51 | Sid: undefined, 52 | Effect: 'Allow', 53 | Principal: undefined, 54 | NotPrincipal: undefined, 55 | Action: undefined, 56 | NotAction: undefined, 57 | Resource: undefined, 58 | NotResource: undefined, 59 | Condition: undefined, 60 | }; 61 | expect(StatementJSONSerialiser.toJSON(statement)).to.deep.equal(expected); 62 | }); 63 | }); 64 | 65 | describe('when statement has a resource', function() { 66 | const statement = new Statement({ 67 | principals: [new ArnPrincipal('arn:aws:iam::98765432100:user/user1')], 68 | actions: ['action1'], 69 | resources: ['resource1'], 70 | conditions: [new Condition('operator', 'key', ['value'])], 71 | }); 72 | it('should return a JSON object', function() { 73 | const expected = { 74 | Sid: undefined, 75 | Effect: 'Allow', 76 | Principal: {AWS: 'arn:aws:iam::98765432100:user/user1'}, 77 | NotPrincipal: undefined, 78 | Action: ['action1'], 79 | NotAction: undefined, 80 | Resource: ['resource1'], 81 | NotResource: undefined, 82 | Condition: {operator: {key: ['value']}}, 83 | }; 84 | expect(StatementJSONSerialiser.toJSON(statement)).to.deep.equal(expected); 85 | }); 86 | }); 87 | 88 | describe('when statement has a notresource', function() { 89 | const statement = new Statement({ 90 | principals: [new ArnPrincipal('arn:aws:iam::98765432100:user/user1')], 91 | actions: ['action1'], 92 | notresources: ['resource1'], 93 | conditions: [new Condition('operator', 'key', ['value'])], 94 | }); 95 | it('should return a JSON object', function() { 96 | const expected = { 97 | Sid: undefined, 98 | Effect: 'Allow', 99 | Principal: {AWS: 'arn:aws:iam::98765432100:user/user1'}, 100 | NotPrincipal: undefined, 101 | Action: ['action1'], 102 | NotAction: undefined, 103 | Resource: undefined, 104 | NotResource: ['resource1'], 105 | Condition: {operator: {key: ['value']}}, 106 | }; 107 | expect(StatementJSONSerialiser.toJSON(statement)).to.deep.equal(expected); 108 | }); 109 | }); 110 | 111 | describe('when statement has an WildcardPrincipal', function() { 112 | const statement = new Statement({ 113 | principals: [new WildcardPrincipal()], 114 | }); 115 | it('should return a JSON object having the wild card principal', function() { 116 | const expected = { 117 | Sid: undefined, 118 | Effect: 'Allow', 119 | Principal: '*', 120 | NotPrincipal: undefined, 121 | Action: undefined, 122 | NotAction: undefined, 123 | Resource: undefined, 124 | NotResource: undefined, 125 | Condition: undefined, 126 | }; 127 | expect(StatementJSONSerialiser.toJSON(statement)).to.deep.equal(expected); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /tests/statement/statement.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import { 3 | Statement, 4 | ArnPrincipal, 5 | UserPrincipal, 6 | ServicePrincipal, 7 | RootAccountPrincipal, 8 | AccountPrincipal, 9 | WildcardPrincipal, 10 | Condition, 11 | PolicyType, 12 | } from '../../src'; 13 | 14 | describe('#Statement', function() { 15 | describe('when serialising to JSON', function() { 16 | const statement = new Statement({ 17 | sid: 'anSID', 18 | effect: 'Allow', 19 | principals: [ 20 | new UserPrincipal('123456789000', 'aUser'), 21 | new RootAccountPrincipal('123456789000'), 22 | new AccountPrincipal('123456789000'), 23 | new ServicePrincipal('aservice.amazonaws.com'), 24 | ], 25 | actions: ['ec2:Describe*', 'ec2:Get*'], 26 | resources: [ 27 | 'arn:aws:ec2:eu-west-1:123456789000:instance/i-123456', 28 | 'arn:aws:ec2:eu-west-1:123456789000:image/ami-123456', 29 | ], 30 | conditions: [ 31 | new Condition('StringEquals', 'kms:CallerAccount', ['456252097346']), 32 | new Condition('StringEquals', 'kms:ViaService', ['secretsmanager.eu-west-1.amazonaws.com']), 33 | new Condition('StringNotEquals', 'aws:userid', ['anId1', 'anId2']), 34 | ], 35 | }); 36 | 37 | it('should successfully pass a JSON round trip', function() { 38 | const json = JSON.stringify(statement.toJSON()); 39 | const actual = Statement.fromJSON(JSON.parse(json)); 40 | expect(actual).to.deep.equal(statement); 41 | }); 42 | }); 43 | 44 | describe('for identity-based policies', function() { 45 | const statement = new Statement({ 46 | sid: 'ValidForIdentity', 47 | effect: 'Allow', 48 | actions: ['ec2:*'], 49 | resources: ['*'], 50 | }); 51 | 52 | it('should be valid for identity-based policies', function() { 53 | expect(statement.validateForIdentityPolicy()).to.have.empty; 54 | }); 55 | 56 | it('should be invalid for resource-based policies', function() { 57 | const errors = statement.validateForResourcePolicy(); 58 | expect(errors).to.have.length(1); 59 | expect(errors[0]).to.equal( 60 | 'Statement(ValidForIdentity) must specify at least one \'principal\' or \'notprincipal\'.', 61 | ); 62 | }); 63 | }); 64 | 65 | describe('for resource-based policies', function() { 66 | const statement = new Statement({ 67 | sid: 'ValidForResource', 68 | effect: 'Allow', 69 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 70 | actions: ['ec2:*'], 71 | }); 72 | 73 | it('should be valid for resource-based policies', function() { 74 | expect(statement.validateForResourcePolicy()).to.have.empty; 75 | }); 76 | 77 | it('should be invalid for identity-based policies', function() { 78 | const errors = statement.validateForIdentityPolicy(); 79 | expect(errors).to.have.length(2); 80 | expect(errors[0]).to.equal( 81 | 'Statement(ValidForResource) cannot specify any \'principal\' or \'notprincipal\'.', 82 | ); 83 | expect(errors[1]).to.equal( 84 | 'Statement(ValidForResource) must specify at least one \'resource\' or \'notresource\'.', 85 | ); 86 | }); 87 | }); 88 | 89 | describe('without actions', function() { 90 | const statement = new Statement({ 91 | sid: 'Invalid', 92 | effect: 'Allow', 93 | principals: [new ArnPrincipal('arn:aws:iam::123456789000:user/aUser')], 94 | resources: ['*'], 95 | }); 96 | 97 | it('should be invalid', function() { 98 | const errors = statement.validateForAnyPolicy(); 99 | expect(errors).to.have.length(1); 100 | expect(errors[0]).to.equal( 101 | 'Statement(Invalid) must specify at least one \'action\' or \'notaction\'.', 102 | ); 103 | }); 104 | }); 105 | 106 | describe('when empty', function() { 107 | let statement: Statement; 108 | 109 | beforeEach(function() { 110 | statement = new Statement(); 111 | }); 112 | 113 | it('should have effect set by default to ALLOW', function() { 114 | expect(statement.effect).to.equal('Allow'); 115 | }); 116 | }); 117 | 118 | describe('when Sid has alphanumeric characters', function() { 119 | it('should be valid for an identity based policies', function() { 120 | const statement = new Statement({ 121 | sid: 'ValidForIdentity', 122 | effect: 'Allow', 123 | actions: ['ec2:*'], 124 | resources: ['*'], 125 | }); 126 | expect(statement.validateForIdentityPolicy()).to.be.empty; 127 | }); 128 | it('should be valid for a SecretsManager secret policy', function() { 129 | const statement = new Statement({ 130 | sid: 'ValidForSecret', 131 | effect: 'Allow', 132 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 133 | actions: ['secretsmanager:GetSecretValue'], 134 | }); 135 | expect(statement.validateForResourcePolicy(PolicyType.SecretsManager)).to.be.empty; 136 | }); 137 | it('should be valid for an S3 bucket policy', function() { 138 | const statement = new Statement({ 139 | sid: 'ValidForBucket', 140 | effect: 'Allow', 141 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 142 | actions: ['s3:GetObject'], 143 | }); 144 | expect(statement.validateForResourcePolicy(PolicyType.S3)).to.be.empty; 145 | }); 146 | it('should be valid for a KMS key policy', function() { 147 | const statement = new Statement({ 148 | sid: 'ValidForKey', 149 | effect: 'Allow', 150 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 151 | actions: ['kms:Decrypt'], 152 | }); 153 | expect(statement.validateForResourcePolicy(PolicyType.KMS)).to.be.empty; 154 | }); 155 | }); 156 | 157 | describe('when Sid has alphanumeric characters with spaces', function() { 158 | it('should be invalid for an identity based policies', function() { 159 | const statement = new Statement({ 160 | sid: 'Invalid for Identity', 161 | effect: 'Allow', 162 | actions: ['ec2:*'], 163 | resources: ['*'], 164 | }); 165 | expect(statement.validateForIdentityPolicy()).to.deep.equal([ 166 | 'Statement(Invalid for Identity) should only accept alphanumeric characters for \'sid\'' + 167 | ' in the case of an IAM policy.', 168 | ]); 169 | }); 170 | it('should be invalid for a SecretsManager secret policy', function() { 171 | const statement = new Statement({ 172 | sid: 'Invalid for Secret', 173 | effect: 'Allow', 174 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 175 | actions: ['secretsmanager:GetSecretValue'], 176 | }); 177 | expect(statement.validateForResourcePolicy(PolicyType.SecretsManager)).to.deep.equal([ 178 | 'Statement(Invalid for Secret) should only accept alphanumeric characters for \'sid\'' + 179 | ' in the case of a SecretsManager secret policy.', 180 | ]); 181 | }); 182 | it('should be valid for an S3 bucket policy', function() { 183 | const statement = new Statement({ 184 | sid: 'Valid for Bucket', 185 | effect: 'Allow', 186 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 187 | actions: ['s3:GetObject'], 188 | }); 189 | expect(statement.validateForResourcePolicy(PolicyType.S3)).to.be.empty; 190 | }); 191 | it('should be valid for a KMS key policy', function() { 192 | const statement = new Statement({ 193 | sid: 'Valid For Key', 194 | effect: 'Allow', 195 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 196 | actions: ['kms:Decrypt'], 197 | }); 198 | expect(statement.validateForResourcePolicy(PolicyType.KMS)).to.be.empty; 199 | }); 200 | }); 201 | describe('when Sid has non-alphanumeric characters', function() { 202 | it('should be invalid for an identity based policies', function() { 203 | const statement = new Statement({ 204 | sid: 'Invalid for Identity!', 205 | effect: 'Allow', 206 | actions: ['ec2:*'], 207 | resources: ['*'], 208 | }); 209 | expect(statement.validateForIdentityPolicy()).to.deep.equal([ 210 | 'Statement(Invalid for Identity!) should only accept alphanumeric characters for \'sid\'' + 211 | ' in the case of an IAM policy.', 212 | ]); 213 | }); 214 | it('should be invalid for a SecretsManager secret policy', function() { 215 | const statement = new Statement({ 216 | sid: 'Invalid for Secret!', 217 | effect: 'Allow', 218 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 219 | actions: ['secretsmanager:GetSecretValue'], 220 | }); 221 | expect(statement.validateForResourcePolicy(PolicyType.SecretsManager)).to.deep.equal([ 222 | 'Statement(Invalid for Secret!) should only accept alphanumeric characters for \'sid\'' + 223 | ' in the case of a SecretsManager secret policy.', 224 | ]); 225 | }); 226 | it('should be invalid for an S3 bucket policy', function() { 227 | const statement = new Statement({ 228 | sid: 'Invalid for Bucket!', 229 | effect: 'Allow', 230 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 231 | actions: ['s3:GetObject'], 232 | }); 233 | expect(statement.validateForResourcePolicy(PolicyType.SecretsManager)).to.deep.equal([ 234 | 'Statement(Invalid for Bucket!) should only accept alphanumeric characters for \'sid\'' + 235 | ' in the case of a SecretsManager secret policy.', 236 | ]); 237 | }); 238 | it('should be invalid for a KMS key policy', function() { 239 | const statement = new Statement({ 240 | sid: 'Invalid for Key!', 241 | effect: 'Allow', 242 | principals: [new ServicePrincipal('aservice.amazonaws.com')], 243 | actions: ['kms:Decrypt'], 244 | }); 245 | expect(statement.validateForResourcePolicy(PolicyType.KMS)).to.deep.equal([ 246 | 'Statement(Invalid for Key!) should only accept alphanumeric characters and spaces for \'sid\'' + 247 | ' in the case of a KMS key policy.', 248 | ]); 249 | }); 250 | }); 251 | 252 | describe('when principal contains only a wildcard principal', function() { 253 | it('should not throw an Error', function() { 254 | expect(() => new Statement({ 255 | principals: [new WildcardPrincipal()], 256 | actions: ['*'], 257 | resources: ['*'], 258 | })).to.not.throw(Error); 259 | }); 260 | }); 261 | 262 | describe('when principal contains non-wildcard principals', function() { 263 | it('should not throw an Error', function() { 264 | expect(() => new Statement({ 265 | principals: [ 266 | new ArnPrincipal('arn:aws:iam::123456789000:user/aUser'), 267 | new RootAccountPrincipal('123456789000'), 268 | new AccountPrincipal('123456789000'), 269 | new ServicePrincipal('aservice.amazonaws.com'), 270 | ], 271 | actions: ['*'], 272 | resources: ['*'], 273 | })).to.not.throw(Error); 274 | }); 275 | }); 276 | 277 | describe('when principal contains a wildcard principal together with another principal', function() { 278 | it('should throw an Error', function() { 279 | expect(() => new Statement({ 280 | principals: [ 281 | new WildcardPrincipal(), 282 | new ArnPrincipal('arn:aws:iam::123456789000:user/aUser'), 283 | ], 284 | actions: ['*'], 285 | resources: ['*'], 286 | })).to.throw(Error) 287 | .with.property('message', 'In case of the AnonymousPrincipal there can only be one principal'); 288 | }); 289 | }); 290 | 291 | describe('when notprincipal contains only a wildcard principal', function() { 292 | it('should not throw an Error', function() { 293 | expect(() => new Statement({ 294 | notprincipals: [new WildcardPrincipal()], 295 | actions: ['*'], 296 | resources: ['*'], 297 | })).to.not.throw(Error); 298 | }); 299 | }); 300 | 301 | describe('when notprincipal contains non-wildcard principals', function() { 302 | it('should not throw an Error', function() { 303 | expect(() => new Statement({ 304 | notprincipals: [ 305 | new ArnPrincipal('arn:aws:iam::123456789000:user/aUser'), 306 | new RootAccountPrincipal('123456789000'), 307 | new AccountPrincipal('123456789000'), 308 | new ServicePrincipal('aservice.amazonaws.com'), 309 | ], 310 | actions: ['*'], 311 | resources: ['*'], 312 | })).to.not.throw(Error); 313 | }); 314 | }); 315 | 316 | describe('when notprincipal contains a wildcard principal together with another principal', function() { 317 | it('should throw an Error', function() { 318 | expect(() => new Statement({ 319 | notprincipals: [ 320 | new WildcardPrincipal(), 321 | new ArnPrincipal('arn:aws:iam::123456789000:user/aUser'), 322 | ], 323 | actions: ['*'], 324 | resources: ['*'], 325 | })).to.throw(Error) 326 | .with.property('message', 'In case of the AnonymousPrincipal there can only be one principal'); 327 | }); 328 | }); 329 | }); 330 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "lib": ["ES2019"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "experimentalDecorators": true, 11 | "pretty": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitReturns": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "declaration": true, 16 | }, 17 | "files": [ 18 | "src/index.ts" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------