├── lombok.config ├── NOTICE ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── maven.yml │ └── maven-release.yml ├── src ├── test │ ├── resources │ │ ├── empty-schema.json │ │ ├── no-additional-properties-schema.json │ │ ├── minimal-schema.json │ │ ├── singleton-test-schema.json │ │ ├── test-schema-with-empty-write-only.json │ │ ├── invalid-taggable-schema.json │ │ ├── invalid-handlers-schema.json │ │ ├── valid-with-non-taggable-schema.json │ │ ├── invalid-tagProperty-schema.json │ │ ├── minimal-schema-with-typeconfiguration.json │ │ ├── invalid-bad-ref-schema.json │ │ ├── valid-with-allof-schema.json │ │ ├── valid-with-anyof-schema.json │ │ ├── valid-with-oneof-schema.json │ │ ├── minimal-schema-with-invalid-typeconfiguration.json │ │ ├── valid-with-handlers-schema.json │ │ ├── valid-with-refs-schema.json │ │ ├── invalid-update-tagging-schema.json │ │ ├── invalid-with-tagging-bad-pointer-schema.json │ │ ├── valid-with-tagging-schema.json │ │ ├── invalid-with-tagging-bad-reference-schema.json │ │ ├── test-schema-with-hidden-pointers.json │ │ ├── valid-with-tagging-missing-tagProperty-schema.json │ │ ├── common.types.v1.json │ │ ├── test-schema-with-invalid-hidden-pointers.json │ │ ├── test-resource-schema-with-list-override.json │ │ ├── valid-with-tagging-nested-property-schema.json │ │ ├── test-schema.json │ │ ├── scrubbed-values-schema.json │ │ └── test-nested-tagging-schema.json │ └── java │ │ ├── software │ │ └── amazon │ │ │ └── cloudformation │ │ │ └── resource │ │ │ ├── ResourceTaggingTest.java │ │ │ ├── exceptions │ │ │ └── ValidationExceptionTest.java │ │ │ ├── ValidatorRefResolutionTests.java │ │ │ └── BaseValidatorTest.java │ │ └── org │ │ └── everit │ │ └── json │ │ └── schema │ │ └── PublicJSONPointerTest.java └── main │ ├── resources │ ├── licenseHeader │ ├── schema │ │ ├── provider.configuration.definition.schema.v1.json │ │ ├── schema │ │ ├── base.definition.schema.v1.json │ │ └── provider.definition.schema.v1.json │ ├── examples │ │ └── resource │ │ │ └── initech.tps.report.v1.json │ └── checkstyle.xml │ └── java │ ├── software │ └── amazon │ │ └── cloudformation │ │ └── resource │ │ ├── Handler.java │ │ ├── SchemaValidator.java │ │ ├── ResourceTagging.java │ │ ├── Validator.java │ │ ├── exceptions │ │ └── ValidationException.java │ │ ├── BaseValidator.java │ │ └── ResourceTypeSchema.java │ └── org │ └── everit │ └── json │ └── schema │ └── PublicJSONPointer.java ├── CODE_OF_CONDUCT.md ├── .pre-commit-config.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── pom.xml └── README.md /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Cloudformation Resource Schema 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /src/test/resources/empty-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": {}, 5 | "primaryIdentifier": [ 6 | "/properties/NONE" 7 | ], 8 | "additionalProperties": false 9 | } 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /src/test/resources/no-additional-properties-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/PropertyA" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/minimal-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/PropertyA" 11 | ], 12 | "additionalProperties": false 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/singleton-test-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/PropertyA" 11 | ], 12 | "replacementStrategy": "delete_then_create", 13 | "additionalProperties": false 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/test-schema-with-empty-write-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "string" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/PropertyA" 11 | ], 12 | "additionalProperties": false, 13 | "writeOnlyProperties": [ 14 | "" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/invalid-taggable-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/propertyA" 11 | ], 12 | "replacementStrategy": "delete_then_create", 13 | "taggable": true, 14 | "tagging": { 15 | "taggable": false 16 | }, 17 | "additionalProperties": false 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/invalid-handlers-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Valid::TypeName", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "property1": { 6 | "type": "array" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/property1" 11 | ], 12 | "handlers": { 13 | "create": { 14 | "permissions": [ 15 | "test:permission" 16 | ] 17 | }, 18 | "read": {} 19 | }, 20 | "additionalProperties": false 21 | } 22 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-non-taggable-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/propertyA" 11 | ], 12 | "replacementStrategy": "delete_then_create", 13 | "tagging": { 14 | "taggable": false, 15 | "tagProperty": "/properties/propertyB" 16 | }, 17 | "additionalProperties": false 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/licenseHeader: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | -------------------------------------------------------------------------------- /src/test/resources/invalid-tagProperty-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/propertyA" 11 | ], 12 | "replacementStrategy": "delete_then_create", 13 | "tagging": { 14 | "taggable": true, 15 | "tagOnCreate": false, 16 | "tagUpdatable": false, 17 | "cloudFormationSystemTags": true, 18 | "tagProperty": "/properties/propertyB" 19 | }, 20 | "additionalProperties": false 21 | } 22 | -------------------------------------------------------------------------------- /src/test/resources/minimal-schema-with-typeconfiguration.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/properties/PropertyA" 11 | ], 12 | "additionalProperties": false, 13 | "typeConfiguration": { 14 | "additionalProperties": false, 15 | "properties": { 16 | "Configuration": { 17 | "type": "string" 18 | }, 19 | "APIKey": { 20 | "type": "string" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/invalid-bad-ref-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/resource-definition-with-refs.json", 3 | "typeName": "CFN::Test::Resource", 4 | "description": "propertyB definition uses a ref pointer to a non-existent remote schema location", 5 | "sourceUrl": "https://mycorp.com/my-repo.git", 6 | "properties": { 7 | "Time": { 8 | "$ref": "./common.types.v1.json#/definitions/iso8601UTC" 9 | }, 10 | "propertyB": { 11 | "type": "array", 12 | "items": { 13 | "$ref": "./common.types.v1.json#/definitions/badProperty" 14 | } 15 | } 16 | }, 17 | "primaryIdentifier": [ 18 | "/properties/Time" 19 | ], 20 | "additionalProperties": false 21 | } 22 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-allof-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "sourceUrl": "https://mycorp.com/my-repo.git", 5 | "properties": { 6 | "id": { 7 | "type": "string" 8 | }, 9 | "propertyA": { 10 | "type": "string" 11 | }, 12 | "propertyB": { 13 | "type": "string" 14 | } 15 | }, 16 | "allOf": [ 17 | { 18 | "required": [ 19 | "propertyA" 20 | ] 21 | }, 22 | { 23 | "required": [ 24 | "propertyB" 25 | ] 26 | } 27 | ], 28 | "primaryIdentifier": [ 29 | "/properties/id" 30 | ], 31 | "additionalProperties": false 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-anyof-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "sourceUrl": "https://mycorp.com/my-repo.git", 5 | "properties": { 6 | "id": { 7 | "type": "string" 8 | }, 9 | "propertyA": { 10 | "type": "string" 11 | }, 12 | "propertyB": { 13 | "type": "string" 14 | } 15 | }, 16 | "anyOf": [ 17 | { 18 | "required": [ 19 | "propertyA" 20 | ] 21 | }, 22 | { 23 | "required": [ 24 | "propertyB" 25 | ] 26 | } 27 | ], 28 | "primaryIdentifier": [ 29 | "/properties/id" 30 | ], 31 | "additionalProperties": false 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-oneof-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "sourceUrl": "https://mycorp.com/my-repo.git", 5 | "properties": { 6 | "id": { 7 | "type": "string" 8 | }, 9 | "propertyA": { 10 | "type": "string" 11 | }, 12 | "propertyB": { 13 | "type": "string" 14 | } 15 | }, 16 | "oneOf": [ 17 | { 18 | "required": [ 19 | "propertyA" 20 | ] 21 | }, 22 | { 23 | "required": [ 24 | "propertyB" 25 | ] 26 | } 27 | ], 28 | "primaryIdentifier": [ 29 | "/properties/id" 30 | ], 31 | "additionalProperties": false 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/minimal-schema-with-invalid-typeconfiguration.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test invalid schema for unit tests.", 4 | "$comment": "This schema is invalid as additionalProperties is missing in typeConfiguration and also typeConfiguration properties should not start with CloudFormation", 5 | "properties": { 6 | "propertyA": { 7 | "type": "boolean" 8 | } 9 | }, 10 | "primaryIdentifier": [ 11 | "/properties/PropertyA" 12 | ], 13 | "additionalProperties": false, 14 | "typeConfiguration": { 15 | "properties": { 16 | "CloudFormationConfiguration": { 17 | "type": "string" 18 | }, 19 | "APIKey": { 20 | "type": "string" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-handlers-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Valid::TypeName", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "property1": { 6 | "type": "array" 7 | } 8 | }, 9 | "primaryIdentifier": [ 10 | "/property1" 11 | ], 12 | "handlers": { 13 | "create": { 14 | "permissions": [ 15 | "test:permission" 16 | ], 17 | "timeoutInMinutes": 200 18 | }, 19 | "read": { 20 | "permissions": [] 21 | }, 22 | "delete": { 23 | "permissions": [ 24 | "test:permissionA" 25 | ] 26 | }, 27 | "list": { 28 | "permissions": [ 29 | "test:permissionB" 30 | ] 31 | } 32 | }, 33 | "additionalProperties": false 34 | } 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.4.0 4 | hooks: 5 | - id: check-case-conflict 6 | - id: detect-private-key 7 | - id: end-of-file-fixer 8 | - id: mixed-line-ending 9 | args: 10 | - --fix=lf 11 | - id: trailing-whitespace 12 | - id: pretty-format-json 13 | args: 14 | - --autofix 15 | - --indent=4 16 | - --no-sort-keys 17 | - id: check-merge-conflict 18 | - id: check-yaml 19 | - repo: local 20 | hooks: 21 | - id: mvn 22 | name: Run Java unit tests 23 | # from Maven 3.6.1+, should use `--no-transfer-progress` instead of Slf4jMavenTransferListener 24 | entry: > 25 | mvn 26 | -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn 27 | -B 28 | clean verify 29 | language: system 30 | pass_filenames: false 31 | always_run: true 32 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Set up Python 3.8 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: 3.8 27 | - name: install pre-commit 28 | uses: pre-commit/action@v2.0.0 29 | with: 30 | extra_args: --all-files 31 | - name: Failure diff 32 | if: ${{ failure() }} 33 | run: git diff 34 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-refs-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/resource-definition-with-refs.json", 3 | "$schema": "https://schema.cloudformation.us-east-1.amazonaws.com/provider.definition.schema.v1.json", 4 | "typeName": "CFN::Test::Resource", 5 | "description": "Simple resource definition with ref pointers to definitions in a remote schema", 6 | "sourceUrl": "https://mycorp.com/my-repo.git", 7 | "properties": { 8 | "Time": { 9 | "$ref": "./common.types.v1.json#/definitions/iso8601UTC" 10 | }, 11 | "propertyB": { 12 | "type": "array", 13 | "items": { 14 | "$ref": "./common.types.v1.json#/definitions/ipv4Address" 15 | } 16 | } 17 | }, 18 | "primaryIdentifier": [ 19 | "/properties/Time" 20 | ], 21 | "additionalProperties": false 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/invalid-update-tagging-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | }, 8 | "propertyB": { 9 | "description": "A list of tags to apply to the resource.", 10 | "type": "array", 11 | "uniqueItems": true, 12 | "arrayType": "Standard", 13 | "insertionOrder": false 14 | } 15 | }, 16 | "primaryIdentifier": [ 17 | "/properties/propertyA" 18 | ], 19 | "replacementStrategy": "delete_then_create", 20 | "tagging": { 21 | "taggable": true, 22 | "tagOnCreate": true, 23 | "tagUpdatable": true, 24 | "cloudFormationSystemTags": false, 25 | "tagProperty": "/properties/propertyB" 26 | }, 27 | "additionalProperties": false 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/software/amazon/cloudformation/resource/Handler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import java.util.Set; 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.Data; 21 | 22 | @Data 23 | @AllArgsConstructor 24 | class Handler { 25 | private Set permissions; 26 | private Integer timeoutInMinutes; 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/invalid-with-tagging-bad-pointer-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | }, 8 | "propertyB": { 9 | "description": "A list of tags to apply to the resource.", 10 | "type": "array", 11 | "uniqueItems": true, 12 | "arrayType": "Standard", 13 | "insertionOrder": false 14 | } 15 | }, 16 | "primaryIdentifier": [ 17 | "/properties/propertyA" 18 | ], 19 | "replacementStrategy": "delete_then_create", 20 | "tagging": { 21 | "taggable": true, 22 | "tagOnCreate": true, 23 | "tagUpdatable": false, 24 | "cloudFormationSystemTags": false, 25 | "tagProperty": "propertyB", 26 | "permissions": [ 27 | "test:permission" 28 | ] 29 | }, 30 | "additionalProperties": false 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-tagging-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | }, 8 | "propertyB": { 9 | "description": "A list of tags to apply to the resource.", 10 | "type": "array", 11 | "uniqueItems": true, 12 | "arrayType": "Standard", 13 | "insertionOrder": false 14 | } 15 | }, 16 | "primaryIdentifier": [ 17 | "/properties/propertyA" 18 | ], 19 | "replacementStrategy": "delete_then_create", 20 | "tagging": { 21 | "taggable": true, 22 | "tagOnCreate": true, 23 | "tagUpdatable": false, 24 | "cloudFormationSystemTags": false, 25 | "tagProperty": "/properties/propertyB", 26 | "permissions": [ 27 | "test:permission" 28 | ] 29 | }, 30 | "additionalProperties": false 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/invalid-with-tagging-bad-reference-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "properties": { 5 | "propertyA": { 6 | "type": "boolean" 7 | }, 8 | "propertyB": { 9 | "description": "A list of tags to apply to the resource.", 10 | "type": "array", 11 | "uniqueItems": true, 12 | "arrayType": "Standard", 13 | "insertionOrder": false 14 | } 15 | }, 16 | "primaryIdentifier": [ 17 | "/properties/propertyA" 18 | ], 19 | "replacementStrategy": "delete_then_create", 20 | "tagging": { 21 | "taggable": true, 22 | "tagOnCreate": true, 23 | "tagUpdatable": false, 24 | "cloudFormationSystemTags": false, 25 | "tagProperty": "/propertyB", 26 | "permissions": [ 27 | "test:permission" 28 | ] 29 | }, 30 | "additionalProperties": false 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/maven-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will put the project in our staging repo 2 | name: Releasing Project to maven 3 | 4 | on: 5 | release: 6 | types: [ published ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Java & publishing credentials 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 8 17 | server-id: sonatype-nexus-staging # Value of the distributionManagement/repository/id field of the pom.xml 18 | server-username: SONATYPE_USERNAME # env variable for username in deploy 19 | server-password: SONATYPE_PASSWORD # env variable for token in deploy 20 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} # Value of the GPG private key to import 21 | gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase 22 | - name: Deploy to sonatype staging repo 23 | run: mvn deploy -Ppublishing 24 | env: 25 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 26 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 27 | MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 28 | -------------------------------------------------------------------------------- /src/test/resources/test-schema-with-hidden-pointers.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema with hidden paths for unit tests", 4 | "sourceUrl": "https://mycorp.com/my-repo.git", 5 | "definitions": { 6 | "definition1": { 7 | "type": "string" 8 | } 9 | }, 10 | "properties": { 11 | "propertyA": { 12 | "type": "string" 13 | }, 14 | "propertyB": { 15 | "type": "array", 16 | "items": { 17 | "type": "integer" 18 | } 19 | }, 20 | "propertyC": { 21 | "type": "object", 22 | "properties": { 23 | "nestedProperty": { 24 | "type": "string" 25 | }, 26 | "writeOnlyArray": { 27 | "type": "string" 28 | } 29 | } 30 | } 31 | }, 32 | "nonPublicProperties": [ 33 | "/properties/propertyC/properties/nestedProperty", 34 | "/properties/propertyA" 35 | ], 36 | "nonPublicDefinitions": [ 37 | "/definitions/definition1" 38 | ], 39 | "primaryIdentifier": [ 40 | "/properties/propertyA" 41 | ], 42 | "replacementStrategy": "create_then_delete", 43 | "taggable": false, 44 | "additionalProperties": false 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/everit/json/schema/PublicJSONPointer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package org.everit.json.schema; 16 | 17 | import java.util.List; 18 | 19 | import org.json.JSONObject; 20 | 21 | public class PublicJSONPointer extends JSONPointer { 22 | 23 | public PublicJSONPointer(final List refTokens) { 24 | super(refTokens); 25 | } 26 | 27 | public PublicJSONPointer(final String pointer) { 28 | super(pointer); 29 | } 30 | 31 | public List getRefTokens() { 32 | return super.getRefTokens(); 33 | } 34 | 35 | public boolean isInObject(final JSONObject jsonObject) { 36 | try { 37 | return this.queryFrom(jsonObject) != null; 38 | } catch (JSONPointerException e) { 39 | return false; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-tagging-missing-tagProperty-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "definitions": { 5 | "Tag": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "Key": { 10 | "description": "The key name of the tag", 11 | "type": "string" 12 | }, 13 | "Value": { 14 | "description": "The value for the tag", 15 | "type": "string" 16 | } 17 | }, 18 | "required": [ 19 | "Value", 20 | "Key" 21 | ] 22 | } 23 | }, 24 | "properties": { 25 | "propertyA": { 26 | "type": "boolean" 27 | }, 28 | "Tags": { 29 | "type": "array", 30 | "items": { 31 | "$ref": "#/definitions/Tag" 32 | } 33 | } 34 | }, 35 | "primaryIdentifier": [ 36 | "/properties/propertyA" 37 | ], 38 | "replacementStrategy": "delete_then_create", 39 | "tagging": { 40 | "taggable": true, 41 | "tagOnCreate": true, 42 | "tagUpdatable": false, 43 | "cloudFormationSystemTags": false 44 | }, 45 | "additionalProperties": false 46 | } 47 | -------------------------------------------------------------------------------- /src/test/resources/common.types.v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/common.types.v1.json", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "description": "Common Resource Type Schemas", 5 | "definitions": { 6 | "cidrBlock": { 7 | "$comment": "TODO: regex could be more strict, for example this allows the cidr 999.999.999.999 and 999.999.999.999/32", 8 | "description": "Classless Inter-Domain Routing (CIDR) block", 9 | "type": "string", 10 | "pattern": "^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" 11 | }, 12 | "ipV6CidrBlock": { 13 | "$comment": "TODO: find sane regex for this", 14 | "description": "IPV6 Classless Inter-Domain Routing (CIDR) block", 15 | "type": "string" 16 | }, 17 | "iso8601UTC": { 18 | "description": "The date value in ISO 8601 format. The timezone is always UTC. (YYYY-MM-DDThh:mm:ssZ)", 19 | "type": "string", 20 | "pattern": "^([0-2]\\d{3})-(0[0-9]|1[0-2])-([0-2]\\d|3[01])T([01]\\d|2[0-4]):([0-5]\\d):([0-6]\\d)((\\.\\d{3})?)Z$" 21 | }, 22 | "ipv4Address": { 23 | "description": "IPV4 address", 24 | "type": "string" 25 | }, 26 | "ipv6Address": { 27 | "description": "IPV6 address", 28 | "type": "string" 29 | } 30 | }, 31 | "primaryIdentifier": [ 32 | "#/" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/test/resources/test-schema-with-invalid-hidden-pointers.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema with hidden paths for unit tests", 4 | "sourceUrl": "https://mycorp.com/my-repo.git", 5 | "definitions": { 6 | "definition1": { 7 | "type": "string" 8 | } 9 | }, 10 | "properties": { 11 | "nonPublicProperties": [ 12 | "/properties/propertyC/properties/nestedProperty", 13 | "/properties/propertyA" 14 | ], 15 | "nonPublicDefinitions": [ 16 | "/definitions/definition1" 17 | ], 18 | "propertyA": { 19 | "type": "string" 20 | }, 21 | "propertyB": { 22 | "type": "array", 23 | "items": { 24 | "type": "integer" 25 | } 26 | }, 27 | "propertyC": { 28 | "type": "object", 29 | "properties": { 30 | "nestedProperty": { 31 | "type": "string" 32 | }, 33 | "writeOnlyArray": { 34 | "type": "string" 35 | } 36 | } 37 | } 38 | }, 39 | "nonPublicProperties": [ 40 | "/properties/propertyC/properties/nestedProperty", 41 | "/properties/propertyA" 42 | ], 43 | "nonPublicDefinitions": [ 44 | "/definitions/definition1" 45 | ], 46 | "primaryIdentifier": [ 47 | "/properties/propertyA" 48 | ], 49 | "replacementStrategy": "create_then_delete", 50 | "taggable": false, 51 | "additionalProperties": false 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/software/amazon/cloudformation/resource/SchemaValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import org.everit.json.schema.Schema; 18 | import org.json.JSONObject; 19 | 20 | import software.amazon.cloudformation.resource.exceptions.ValidationException; 21 | 22 | public interface SchemaValidator { 23 | 24 | String DEFINITION_SCHEMA_PATH = "/schema/provider.definition.schema.v1.json"; 25 | 26 | /** 27 | * Perform JSON Schema validation for the input model against the specified 28 | * schema 29 | * 30 | * @param modelObject JSON-encoded resource model 31 | * @param schemaObject The JSON schema object to validate the model against 32 | * @throws ValidationException Thrown for any schema validation errors 33 | */ 34 | void validateObject(JSONObject modelObject, JSONObject schemaObject) throws ValidationException; 35 | 36 | void validateObjectByListHandlerSchema(JSONObject modelObject, JSONObject schemaObject) throws ValidationException; 37 | 38 | Schema getListHandlerSchema(JSONObject definitionSchemaObject) throws ValidationException; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/software/amazon/cloudformation/resource/ResourceTaggingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | import java.util.ArrayList; 20 | 21 | import org.everit.json.schema.JSONPointer; 22 | import org.junit.jupiter.api.Test; 23 | 24 | public class ResourceTaggingTest { 25 | @Test 26 | public void testResetTaggable() { 27 | 28 | final ResourceTagging resourceTagging = new ResourceTagging(true, true, true, 29 | true, new JSONPointer("/properties/tags"), new ArrayList<>()); 30 | resourceTagging.resetTaggable(false); 31 | 32 | assertThat(resourceTagging.isTaggable()).isEqualTo(false); 33 | assertThat(resourceTagging.isTagOnCreate()).isEqualTo(false); 34 | assertThat(resourceTagging.isTagUpdatable()).isEqualTo(false); 35 | assertThat(resourceTagging.isCloudFormationSystemTags()).isEqualTo(false); 36 | assertThat(resourceTagging.getTagProperty().toString()).isEqualTo("/properties/tags"); 37 | assertThat(resourceTagging.getTagPermissions().isEmpty()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/resources/test-resource-schema-with-list-override.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::TEST::RESOURCE", 3 | "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-test", 4 | "definitions": { 5 | "PersonalInfo": { 6 | "type": "object", 7 | "properties": { 8 | "Name": { 9 | "type": "string" 10 | }, 11 | "LastName": { 12 | "type": "string" 13 | } 14 | }, 15 | "required": [ 16 | "Name" 17 | ], 18 | "additionalProperties": false 19 | } 20 | }, 21 | "properties": { 22 | "ID": { 23 | "type": "string" 24 | }, 25 | "Person": { 26 | "$ref": "#/definitions/PersonalInfo" 27 | }, 28 | "Human": { 29 | "$ref": "#/definitions/PersonalInfo" 30 | } 31 | }, 32 | "handlers": { 33 | "list": { 34 | "handlerSchema": { 35 | "properties": { 36 | "Person": { 37 | "$ref": "resource-schema.json#/properties/Person" 38 | }, 39 | "Human": { 40 | "type": "object", 41 | "properties": { 42 | "LastName": { 43 | "type": "string" 44 | } 45 | }, 46 | "required": [ 47 | "LastName" 48 | ], 49 | "additionalProperties": false 50 | } 51 | } 52 | }, 53 | "permissions": [ 54 | "..." 55 | ] 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | .vscode 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # macOS 103 | .DS_Store 104 | ._* 105 | 106 | # PyCharm 107 | *.iml 108 | 109 | # reStructuredText preview 110 | README.html 111 | 112 | .idea 113 | out.java 114 | out/ 115 | 116 | # Maven outputs 117 | .classpath 118 | 119 | # IntelliJ 120 | .settings 121 | .project 122 | -------------------------------------------------------------------------------- /src/test/resources/valid-with-tagging-nested-property-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "definitions": { 5 | "propertyC": { 6 | "type": "object", 7 | "properties": { 8 | "Tags": { 9 | "description": "An array of arbitrary tags (key-value pairs) to associate with the stage.", 10 | "type": "array", 11 | "uniqueItems": false, 12 | "insertionOrder": false, 13 | "items": { 14 | "$ref": "#/definitions/Tag" 15 | } 16 | } 17 | } 18 | }, 19 | "Tag": { 20 | "type": "object", 21 | "additionalProperties": false, 22 | "properties": { 23 | "Key": { 24 | "description": "The key name of the tag", 25 | "type": "string" 26 | }, 27 | "Value": { 28 | "description": "The value for the tag", 29 | "type": "string" 30 | } 31 | }, 32 | "required": [ 33 | "Value", 34 | "Key" 35 | ] 36 | } 37 | }, 38 | "properties": { 39 | "propertyA": { 40 | "type": "boolean" 41 | }, 42 | "propertyC": { 43 | "$ref": "#/definitions/propertyC" 44 | } 45 | }, 46 | "primaryIdentifier": [ 47 | "/properties/propertyA" 48 | ], 49 | "replacementStrategy": "delete_then_create", 50 | "tagging": { 51 | "taggable": true, 52 | "tagOnCreate": true, 53 | "tagUpdatable": false, 54 | "cloudFormationSystemTags": false, 55 | "tagProperty": "/properties/propertyC/Tags", 56 | "permissions": [ 57 | "test:permission" 58 | ] 59 | }, 60 | "additionalProperties": false 61 | } 62 | -------------------------------------------------------------------------------- /src/test/resources/test-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "sourceUrl": "https://mycorp.com/my-repo.git", 5 | "properties": { 6 | "propertyA": { 7 | "type": "string" 8 | }, 9 | "propertyB": { 10 | "type": "array", 11 | "arrayType": "AttributeList", 12 | "items": { 13 | "type": "integer" 14 | } 15 | }, 16 | "propertyC": { 17 | "type": "string" 18 | }, 19 | "propertyD": { 20 | "type": "boolean" 21 | }, 22 | "propertyE": { 23 | "type": "object", 24 | "properties": { 25 | "nestedProperty": { 26 | "type": "string" 27 | }, 28 | "writeOnlyArray": { 29 | "type": "string" 30 | } 31 | } 32 | }, 33 | "propertyF": { 34 | "type": "string" 35 | } 36 | }, 37 | "propertyTransform": { 38 | "/properties/propertyA": "$join([$string(test), propertyA])", 39 | "/properties/propertyB": "$count(propertyB) = 0 ? null : propertyB" 40 | }, 41 | "required": [ 42 | "propertyB" 43 | ], 44 | "conditionalCreateOnlyProperties": [ 45 | "/properties/propertyF" 46 | ], 47 | "createOnlyProperties": [ 48 | "/properties/propertyA", 49 | "/properties/propertyD" 50 | ], 51 | "deprecatedProperties": [ 52 | "/properties/propertyC" 53 | ], 54 | "readOnlyProperties": [ 55 | "/properties/propertyB" 56 | ], 57 | "writeOnlyProperties": [ 58 | "/properties/propertyC", 59 | "/properties/propertyE/nestedProperty" 60 | ], 61 | "primaryIdentifier": [ 62 | "/properties/propertyA" 63 | ], 64 | "additionalIdentifiers": [ 65 | [ 66 | "/properties/propertyB" 67 | ] 68 | ], 69 | "replacementStrategy": "create_then_delete", 70 | "taggable": false, 71 | "additionalProperties": false 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/org/everit/json/schema/PublicJSONPointerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package org.everit.json.schema; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | import java.util.Arrays; 20 | 21 | import org.json.JSONObject; 22 | import org.junit.jupiter.api.Test; 23 | 24 | public class PublicJSONPointerTest { 25 | @Test 26 | public void isInObject_objectHasProperty_shouldReturnTrue() { 27 | final JSONObject jsonObject = new JSONObject().put("propertyThatExists", "value"); 28 | assertThat(new PublicJSONPointer("/propertyThatExists").isInObject(jsonObject)).isTrue(); 29 | } 30 | 31 | @Test 32 | public void isInObject_nestedObjectHasProperty_shouldReturnTrue() { 33 | final JSONObject jsonObject = new JSONObject().put("propertyThatExists", new JSONObject().put("nested", "value")); 34 | assertThat(new PublicJSONPointer(Arrays.asList("propertyThatExists", "nested")).isInObject(jsonObject)).isTrue(); 35 | } 36 | 37 | @Test 38 | public void isInObject_nestedObjectDoesNotHaveProperty_shouldReturnFalse() { 39 | final JSONObject jsonObject = new JSONObject(); 40 | assertThat(new PublicJSONPointer(Arrays.asList("propertyThatExists", "nested")).isInObject(jsonObject)).isFalse(); 41 | } 42 | 43 | @Test 44 | public void isInObject_objectDoesNotHaveProperty_shouldReturnFalse() { 45 | final JSONObject jsonObject = new JSONObject(); 46 | assertThat(new PublicJSONPointer("/propertyThatDoesNotExist").isInObject(jsonObject)).isFalse(); 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/schema/provider.configuration.definition.schema.v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/provider.configuration.definition.schema.v1.json", 4 | "title": "CloudFormation Type Provider Configuration Definition MetaSchema", 5 | "description": "This schema validates a CloudFormation type provider configuration definition.", 6 | "type": "object", 7 | "properties": { 8 | "additionalProperties": { 9 | "$comment": "All properties must be expressed in the schema - arbitrary inputs are not allowed", 10 | "type": "boolean", 11 | "const": false 12 | }, 13 | "deprecatedProperties": { 14 | "$ref": "base.definition.schema.v1.json#/properties/deprecatedProperties" 15 | }, 16 | "allOf": { 17 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 18 | }, 19 | "anyOf": { 20 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 21 | }, 22 | "oneOf": { 23 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 24 | }, 25 | "required": { 26 | "$ref": "base.definition.schema.v1.json#/properties/required" 27 | }, 28 | "description": { 29 | "$comment": "A short description of the type configuration. This will be shown in the AWS CloudFormation console.", 30 | "$ref": "base.definition.schema.v1.json#/properties/description" 31 | }, 32 | "properties": { 33 | "type": "object", 34 | "patternProperties": { 35 | "(?!CloudFormation)^[A-Za-z0-9]{1,64}$": { 36 | "$comment": "TypeConfiguration properties starting with `CloudFormation` are reserved for CloudFormation use", 37 | "$ref": "base.definition.schema.v1.json#/definitions/properties" 38 | } 39 | }, 40 | "minProperties": 1, 41 | "additionalProperties": false 42 | } 43 | }, 44 | "required": [ 45 | "properties", 46 | "additionalProperties" 47 | ], 48 | "additionalProperties": false 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/software/amazon/cloudformation/resource/exceptions/ValidationExceptionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource.exceptions; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | 23 | import org.junit.jupiter.api.Test; 24 | 25 | public class ValidationExceptionTest { 26 | 27 | @Test 28 | public void ctor_noCausingExceptions() { 29 | ValidationException e = new ValidationException("some error", "key1", "#/properties"); 30 | assertThat(e.getCausingExceptions()).isNotNull(); 31 | assertThat(e.getCausingExceptions().size()).isEqualTo(0); 32 | assertThat(e.getMessage()).isEqualTo("some error"); 33 | } 34 | 35 | @Test 36 | public void buildFullExceptionMessage_single() { 37 | ValidationException e = new ValidationException("Root error", null, "#"); 38 | 39 | final String message = ValidationException.buildFullExceptionMessage(e); 40 | 41 | assertThat(message).isEqualTo(e.getMessage()); 42 | } 43 | 44 | @Test 45 | public void buildFullExceptionMessage_isNullSafe() { 46 | ValidationException e = new ValidationException(null, null, null, null); 47 | 48 | final String message = ValidationException.buildFullExceptionMessage(e); 49 | 50 | assertThat(message).isEmpty(); 51 | } 52 | 53 | @Test 54 | public void buildFullExceptionMessage_multiple() { 55 | ValidationException e1 = new ValidationException("First error", "key1", "#/properties"); 56 | ValidationException e2 = new ValidationException("Second error", "key2", "#/properties"); 57 | ValidationException e3 = new ValidationException("Third error", "key3", "#/properties"); 58 | 59 | final List causes = new ArrayList<>(Arrays.asList(e1, e2, e3)); 60 | ValidationException e = new ValidationException("Root error", causes, null, "#"); 61 | 62 | final String message = ValidationException.buildFullExceptionMessage(e); 63 | 64 | assertThat(message).doesNotEndWith("\n"); 65 | 66 | List messageParts = new ArrayList<>(Arrays.asList(message.split("\n"))); 67 | 68 | assertThat(messageParts).hasSize(3); 69 | 70 | causes.forEach(ve -> messageParts.contains(ve.getMessage())); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/resources/scrubbed-values-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "sourceUrl": "my-repo.git", 5 | "definitions": { 6 | "numberType": { 7 | "type": "number", 8 | "exclusiveMinimum": 10, 9 | "exclusiveMaximum": 20 10 | }, 11 | "integerType": { 12 | "type": "integer", 13 | "minimum": 10, 14 | "maximum": 100, 15 | "multipleOf": 5 16 | }, 17 | "arrayType": { 18 | "type": "array", 19 | "items": { 20 | "type": "string" 21 | }, 22 | "minItems": 2, 23 | "maxItems": 2, 24 | "contains": { 25 | "type": "string", 26 | "minLength": 2 27 | }, 28 | "uniqueItems": true 29 | }, 30 | "stringType": { 31 | "type": "string", 32 | "pattern": "Too", 33 | "minLength": 10, 34 | "maxLength": 15 35 | }, 36 | "objectType": { 37 | "type": "object", 38 | "minProperties": 2, 39 | "maxProperties": 2, 40 | "dependencies": { 41 | "dep": [ 42 | "otherKey" 43 | ] 44 | } 45 | } 46 | }, 47 | "properties": { 48 | "StringProperty": { 49 | "$ref": "#/definitions/stringType" 50 | }, 51 | "enumProperty": { 52 | "type": "string", 53 | "enum": [ 54 | "ABC" 55 | ] 56 | }, 57 | "constProperty": { 58 | "type": "string", 59 | "const": "ABC" 60 | }, 61 | "ArrayProperty": { 62 | "$ref": "#/definitions/arrayType" 63 | }, 64 | "IntProperty": { 65 | "$ref": "#/definitions/integerType" 66 | }, 67 | "NumberProperty": { 68 | "$ref": "#/definitions/numberType" 69 | }, 70 | "BooleanProperty": { 71 | "type": "boolean" 72 | }, 73 | "ObjectProperty": { 74 | "$ref": "#/definitions/objectType" 75 | }, 76 | "MapProperty": { 77 | "type": "object", 78 | "patternProperties": { 79 | "abc": { 80 | "type": "string" 81 | } 82 | }, 83 | "additionalProperties": false 84 | }, 85 | "anyOfProperty": { 86 | "anyOf": [ 87 | { 88 | "type": "integer" 89 | } 90 | ] 91 | }, 92 | "oneOfProperty": { 93 | "oneOf": [ 94 | { 95 | "type": "integer" 96 | } 97 | ] 98 | }, 99 | "allOfProperty": { 100 | "allOf": [ 101 | { 102 | "type": "integer" 103 | } 104 | ] 105 | } 106 | }, 107 | "primaryIdentifier": [ 108 | "/properties/StringProperty" 109 | ], 110 | "additionalProperties": false 111 | } 112 | -------------------------------------------------------------------------------- /src/main/resources/examples/resource/initech.tps.report.v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "Initech::TPS::Report", 3 | "description": "An example resource schema demonstrating some basic constructs and validation rules.", 4 | "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git", 5 | "definitions": { 6 | "InitechDateFormat": { 7 | "$comment": "Use the `definitions` block to provide shared resource property schemas", 8 | "type": "string", 9 | "format": "date-time" 10 | }, 11 | "Memo": { 12 | "type": "object", 13 | "properties": { 14 | "Heading": { 15 | "type": "string" 16 | }, 17 | "Body": { 18 | "type": "string" 19 | } 20 | } 21 | } 22 | }, 23 | "properties": { 24 | "TPSCode": { 25 | "description": "A TPS Code is automatically generated on creation and assigned as the unique identifier.", 26 | "type": "string", 27 | "pattern": "^[A-Z]{3,5}[0-9]{8}-[0-9]{4}$" 28 | }, 29 | "Title": { 30 | "description": "The title of the TPS report is a mandatory element.", 31 | "type": "string", 32 | "minLength": 20, 33 | "maxLength": 250 34 | }, 35 | "CoverSheetIncluded": { 36 | "description": "Required for all TPS Reports submitted after 2/19/1999", 37 | "type": "boolean" 38 | }, 39 | "DueDate": { 40 | "$ref": "#/definitions/InitechDateFormat" 41 | }, 42 | "ApprovalDate": { 43 | "$ref": "#/definitions/InitechDateFormat" 44 | }, 45 | "Memo": { 46 | "$ref": "#/definitions/Memo" 47 | }, 48 | "SecondCopyOfMemo": { 49 | "description": "In case you didn't get the first one.", 50 | "$ref": "#/definitions/Memo" 51 | }, 52 | "TestCode": { 53 | "type": "string", 54 | "enum": [ 55 | "NOT_STARTED", 56 | "CANCELLED" 57 | ] 58 | }, 59 | "Authors": { 60 | "type": "array", 61 | "items": { 62 | "type": "string" 63 | } 64 | } 65 | }, 66 | "required": [ 67 | "TestCode", 68 | "Title" 69 | ], 70 | "readOnlyProperties": [ 71 | "/properties/TPSCode" 72 | ], 73 | "primaryIdentifier": [ 74 | "/properties/TPSCode" 75 | ], 76 | "handlers": { 77 | "create": { 78 | "permissions": [ 79 | "initech:CreateReport" 80 | ] 81 | }, 82 | "read": { 83 | "permissions": [ 84 | "initech:DescribeReport" 85 | ] 86 | }, 87 | "update": { 88 | "permissions": [ 89 | "initech:UpdateReport" 90 | ] 91 | }, 92 | "delete": { 93 | "permissions": [ 94 | "initech:DeleteReport" 95 | ] 96 | }, 97 | "list": { 98 | "permissions": [ 99 | "initech:ListReports" 100 | ] 101 | } 102 | }, 103 | "resourceLink": { 104 | "$comment": "This is an example of a relative, AWS-internal service link. For external links, use an absolute URI.", 105 | "templateUri": "/tps/v2/home?region=${awsRegion}#Report:tpsCode=${TPSCode}", 106 | "mappings": { 107 | "TPSCode": "/TPSCode" 108 | } 109 | }, 110 | "additionalProperties": false 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/software/amazon/cloudformation/resource/ResourceTagging.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | import lombok.AllArgsConstructor; 21 | import lombok.Data; 22 | 23 | import org.everit.json.schema.JSONPointer; 24 | import org.everit.json.schema.Schema; 25 | 26 | import software.amazon.cloudformation.resource.exceptions.ValidationException; 27 | 28 | @Data 29 | @AllArgsConstructor 30 | public class ResourceTagging { 31 | public static final String TAGGABLE = "taggable"; 32 | public static final String TAG_ON_CREATE = "tagOnCreate"; 33 | public static final String TAG_UPDATABLE = "tagUpdatable"; 34 | public static final String CLOUDFORMATION_SYSTEM_TAGS = "cloudFormationSystemTags"; 35 | public static final String TAG_PROPERTY = "tagProperty"; 36 | public static final String TAG_PERMISSIONS = "permissions"; 37 | public static final ResourceTagging DEFAULT = new ResourceTagging(true); 38 | 39 | private boolean taggable; 40 | private boolean tagOnCreate; 41 | private boolean tagUpdatable; 42 | private boolean cloudFormationSystemTags; 43 | private JSONPointer tagProperty; 44 | private List tagPermissions; 45 | 46 | public ResourceTagging(final boolean taggableValue) { 47 | this.taggable = taggableValue; 48 | this.tagOnCreate = taggableValue; 49 | this.tagUpdatable = taggableValue; 50 | this.cloudFormationSystemTags = taggableValue; 51 | this.tagProperty = new JSONPointer("/properties/Tags"); 52 | this.tagPermissions = new ArrayList<>(); 53 | } 54 | 55 | public void resetTaggable(final boolean taggableValue) { 56 | this.taggable = taggableValue; 57 | this.tagOnCreate = taggableValue; 58 | this.tagUpdatable = taggableValue; 59 | this.cloudFormationSystemTags = taggableValue; 60 | } 61 | 62 | public void validateTaggingMetadata(final boolean containUpdateHandler, final Schema schema) { 63 | if (this.tagUpdatable && !containUpdateHandler) { 64 | throw new ValidationException("Invalid tagUpdatable value since update handler is missing", "tagging", 65 | "#/tagging/tagUpdatable"); 66 | } 67 | 68 | final String propertiesPrefix = "/properties/"; 69 | if (!this.tagProperty.toString().startsWith(propertiesPrefix)) { 70 | final String errorMessage = String.format("Invalid tagProperty value %s must start with \"/properties\"", 71 | this.tagProperty.toString()); 72 | throw new ValidationException(errorMessage, "tagging", "#/tagging/tagProperty"); 73 | } 74 | 75 | final String propertyName = this.tagProperty.toString().substring(propertiesPrefix.length()).replaceAll("/\\*/", "/0/"); 76 | 77 | if (this.taggable && !schema.definesProperty(propertyName)) { 78 | final String errorMessage = String.format("Invalid tagProperty value since %s not found in schema", propertyName); 79 | throw new ValidationException(errorMessage, "tagging", "#/tagging/tagProperty"); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-cloudformation/aws-cloudformation-resource-schema/issues), or [recently closed](https://github.com/aws-cloudformation/aws-cloudformation-resource-schema/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-cloudformation/aws-cloudformation-resource-schema/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-cloudformation/aws-cloudformation-resource-schema/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /src/test/resources/test-nested-tagging-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWS::Test::TestModel", 3 | "description": "A test schema for unit tests.", 4 | "sourceUrl": "https://mycorp.com/my-repo.git", 5 | "definitions": { 6 | "NestedDefinition": { 7 | "type": "object", 8 | "additionalProperties": false, 9 | "properties": { 10 | "TagSpecifications": { 11 | "type": "array", 12 | "uniqueItems": true, 13 | "items": { 14 | "$ref": "#/definitions/TagSpecificationDefinition" 15 | } 16 | } 17 | } 18 | }, 19 | "TagSpecificationDefinition": { 20 | "type": "object", 21 | "additionalProperties": false, 22 | "properties": { 23 | "Tags": { 24 | "type": "array", 25 | "uniqueItems": false, 26 | "items": { 27 | "$ref": "#/definitions/Tag" 28 | } 29 | } 30 | } 31 | }, 32 | "Tag": { 33 | "type": "object", 34 | "additionalProperties": false, 35 | "properties": { 36 | "Key": { 37 | "type": "string" 38 | }, 39 | "Value": { 40 | "type": "string" 41 | } 42 | }, 43 | "required": [ 44 | "Value", 45 | "Key" 46 | ] 47 | } 48 | }, 49 | "properties": { 50 | "propertyA": { 51 | "type": "string" 52 | }, 53 | "propertyB": { 54 | "type": "array", 55 | "arrayType": "AttributeList", 56 | "items": { 57 | "type": "integer" 58 | } 59 | }, 60 | "propertyC": { 61 | "type": "string" 62 | }, 63 | "propertyD": { 64 | "type": "boolean" 65 | }, 66 | "propertyE": { 67 | "type": "object", 68 | "properties": { 69 | "nestedProperty": { 70 | "type": "string" 71 | }, 72 | "writeOnlyArray": { 73 | "type": "string" 74 | } 75 | } 76 | }, 77 | "propertyF": { 78 | "type": "string" 79 | }, 80 | "NestedTagProperty": { 81 | "$ref": "#/definitions/NestedDefinition" 82 | } 83 | }, 84 | "tagging": { 85 | "taggable": true, 86 | "tagOnCreate": true, 87 | "tagUpdatable": false, 88 | "cloudFormationSystemTags": true, 89 | "tagProperty": "/properties/NestedTagProperty/TagSpecifications/*/Tags", 90 | "permissions": [ 91 | "permission:AddTags" 92 | ] 93 | }, 94 | "propertyTransform": { 95 | "/properties/propertyA": "$join([$string(test), propertyA])", 96 | "/properties/propertyB": "$count(propertyB) = 0 ? null : propertyB" 97 | }, 98 | "required": [ 99 | "propertyB" 100 | ], 101 | "conditionalCreateOnlyProperties": [ 102 | "/properties/propertyF" 103 | ], 104 | "createOnlyProperties": [ 105 | "/properties/propertyA", 106 | "/properties/propertyD" 107 | ], 108 | "deprecatedProperties": [ 109 | "/properties/propertyC" 110 | ], 111 | "readOnlyProperties": [ 112 | "/properties/propertyB" 113 | ], 114 | "writeOnlyProperties": [ 115 | "/properties/propertyC", 116 | "/properties/propertyE/nestedProperty" 117 | ], 118 | "primaryIdentifier": [ 119 | "/properties/propertyA" 120 | ], 121 | "additionalIdentifiers": [ 122 | [ 123 | "/properties/propertyB" 124 | ] 125 | ], 126 | "replacementStrategy": "create_then_delete", 127 | "additionalProperties": false 128 | } 129 | -------------------------------------------------------------------------------- /src/main/resources/schema/schema: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://json-schema.org/draft-07/schema", 4 | "title": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "nonNegativeInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "nonNegativeIntegerDefault0": { 16 | "allOf": [ 17 | { "$ref": "#/definitions/nonNegativeInteger" }, 18 | { "default": 0 } 19 | ] 20 | }, 21 | "simpleTypes": { 22 | "enum": [ 23 | "array", 24 | "boolean", 25 | "integer", 26 | "null", 27 | "number", 28 | "object", 29 | "string" 30 | ] 31 | }, 32 | "stringArray": { 33 | "type": "array", 34 | "items": { "type": "string" }, 35 | "uniqueItems": true, 36 | "default": [] 37 | } 38 | }, 39 | "type": ["object", "boolean"], 40 | "properties": { 41 | "$id": { 42 | "type": "string", 43 | "format": "uri-reference" 44 | }, 45 | "$schema": { 46 | "type": "string", 47 | "format": "uri" 48 | }, 49 | "$ref": { 50 | "type": "string", 51 | "format": "uri-reference" 52 | }, 53 | "$comment": { 54 | "type": "string" 55 | }, 56 | "title": { 57 | "type": "string" 58 | }, 59 | "description": { 60 | "type": "string" 61 | }, 62 | "default": true, 63 | "readOnly": { 64 | "type": "boolean", 65 | "default": false 66 | }, 67 | "examples": { 68 | "type": "array", 69 | "items": true 70 | }, 71 | "multipleOf": { 72 | "type": "number", 73 | "exclusiveMinimum": 0 74 | }, 75 | "maximum": { 76 | "type": "number" 77 | }, 78 | "exclusiveMaximum": { 79 | "type": "number" 80 | }, 81 | "minimum": { 82 | "type": "number" 83 | }, 84 | "exclusiveMinimum": { 85 | "type": "number" 86 | }, 87 | "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, 88 | "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 89 | "pattern": { 90 | "type": "string", 91 | "format": "regex" 92 | }, 93 | "additionalItems": { "$ref": "#" }, 94 | "items": { 95 | "anyOf": [ 96 | { "$ref": "#" }, 97 | { "$ref": "#/definitions/schemaArray" } 98 | ], 99 | "default": true 100 | }, 101 | "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, 102 | "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 103 | "uniqueItems": { 104 | "type": "boolean", 105 | "default": false 106 | }, 107 | "contains": { "$ref": "#" }, 108 | "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, 109 | "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 110 | "required": { "$ref": "#/definitions/stringArray" }, 111 | "additionalProperties": { "$ref": "#" }, 112 | "definitions": { 113 | "type": "object", 114 | "additionalProperties": { "$ref": "#" }, 115 | "default": {} 116 | }, 117 | "properties": { 118 | "type": "object", 119 | "additionalProperties": { "$ref": "#" }, 120 | "default": {} 121 | }, 122 | "patternProperties": { 123 | "type": "object", 124 | "additionalProperties": { "$ref": "#" }, 125 | "propertyNames": { "format": "regex" }, 126 | "default": {} 127 | }, 128 | "dependencies": { 129 | "type": "object", 130 | "additionalProperties": { 131 | "anyOf": [ 132 | { "$ref": "#" }, 133 | { "$ref": "#/definitions/stringArray" } 134 | ] 135 | } 136 | }, 137 | "propertyNames": { "$ref": "#" }, 138 | "const": true, 139 | "enum": { 140 | "type": "array", 141 | "items": true, 142 | "minItems": 1, 143 | "uniqueItems": true 144 | }, 145 | "type": { 146 | "anyOf": [ 147 | { "$ref": "#/definitions/simpleTypes" }, 148 | { 149 | "type": "array", 150 | "items": { "$ref": "#/definitions/simpleTypes" }, 151 | "minItems": 1, 152 | "uniqueItems": true 153 | } 154 | ] 155 | }, 156 | "format": { "type": "string" }, 157 | "contentMediaType": { "type": "string" }, 158 | "contentEncoding": { "type": "string" }, 159 | "if": { "$ref": "#" }, 160 | "then": { "$ref": "#" }, 161 | "else": { "$ref": "#" }, 162 | "allOf": { "$ref": "#/definitions/schemaArray" }, 163 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 164 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 165 | "not": { "$ref": "#" } 166 | }, 167 | "default": true 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/software/amazon/cloudformation/resource/ValidatorRefResolutionTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 18 | import static org.mockito.Mockito.times; 19 | import static org.mockito.Mockito.verify; 20 | import static org.mockito.Mockito.when; 21 | import static software.amazon.cloudformation.resource.ValidatorTest.loadJSON; 22 | 23 | import org.everit.json.schema.Schema; 24 | import org.everit.json.schema.loader.SchemaClient; 25 | import org.json.JSONObject; 26 | import org.junit.jupiter.api.BeforeEach; 27 | import org.junit.jupiter.api.Test; 28 | import org.junit.jupiter.api.extension.ExtendWith; 29 | import org.mockito.Mock; 30 | import org.mockito.junit.jupiter.MockitoExtension; 31 | 32 | import software.amazon.cloudformation.resource.exceptions.ValidationException; 33 | 34 | /** 35 | * 36 | */ 37 | @ExtendWith(MockitoExtension.class) 38 | public class ValidatorRefResolutionTests { 39 | 40 | public static final String RESOURCE_DEFINITION_PATH = "/valid-with-refs-schema.json"; 41 | private final static String COMMON_TYPES_PATH = "/common.types.v1.json"; 42 | private final String expectedRefUrl = "https://schema.cloudformation.us-east-1.amazonaws.com/common.types.v1.json"; 43 | 44 | @Mock 45 | private SchemaClient downloader; 46 | private Validator validator; 47 | 48 | @BeforeEach 49 | public void beforeEach() { 50 | when(downloader.get(expectedRefUrl)).thenAnswer(x -> ValidatorTest.getResourceAsStream(COMMON_TYPES_PATH)); 51 | 52 | this.validator = new Validator(downloader); 53 | } 54 | 55 | @Test 56 | public void validateResourceDefinition_validRelativeRef_shouldSucceed() { 57 | 58 | JSONObject schema = loadJSON(RESOURCE_DEFINITION_PATH); 59 | validator.validateResourceDefinition(schema); 60 | 61 | // valid-with-refs.json contains two refs pointing at locations inside 62 | // common.types.v1.json 63 | // Everit will attempt to download the remote schema once for each $ref - it 64 | // doesn't cache remote schemas. Expect the downloader to be called twice 65 | verify(downloader, times(2)).get(expectedRefUrl); 66 | } 67 | 68 | /** 69 | * expect a valid resource schema contains a ref to a non-existent property in a 70 | * remote meta-schema 71 | */ 72 | @Test 73 | public void validateResourceDefinition_invalidRelativeRef_shouldThrow() { 74 | 75 | JSONObject badSchema = loadJSON("/invalid-bad-ref-schema.json"); 76 | 77 | assertThatExceptionOfType(ValidationException.class) 78 | .isThrownBy(() -> validator.validateResourceDefinition(badSchema)); 79 | } 80 | 81 | /** example of using ResourceTypeSchema to validate a model */ 82 | @Test 83 | public void validateModel_containsValidRefs_shouldSucceed() { 84 | 85 | JSONObject resourceDefinition = loadJSON(RESOURCE_DEFINITION_PATH); 86 | Schema rawSchema = validator.loadResourceDefinitionSchema(resourceDefinition); 87 | ResourceTypeSchema schema = new ResourceTypeSchema(rawSchema); 88 | 89 | schema.validate(getValidModelWithRefs()); 90 | } 91 | 92 | /** 93 | * model that contains an invalid value in one of its properties fails 94 | * validation 95 | */ 96 | @Test 97 | public void validateModel_containsBadRef_shoudThrow() { 98 | JSONObject resourceDefinition = loadJSON(RESOURCE_DEFINITION_PATH); 99 | Schema rawSchema = validator.loadResourceDefinitionSchema(resourceDefinition); 100 | ResourceTypeSchema schema = new ResourceTypeSchema(rawSchema); 101 | 102 | final JSONObject template = getValidModelWithRefs(); 103 | // make the model invalid by adding a property containing a malformed IP address 104 | template.put("propertyB", "not.an.IP.address"); 105 | 106 | assertThatExceptionOfType(org.everit.json.schema.ValidationException.class) 107 | .isThrownBy(() -> schema.validate(template)); 108 | } 109 | 110 | /** 111 | * resource schema located at RESOURCE_DEFINITION_PATH declares two properties: 112 | * "Time" in ISO 8601 format (UTC only) and "propertyB" - an IP address Both 113 | * fields are declares as refs to common.types.v1.json. "Time" is marked as 114 | * required property getSampleTemplate constructs a JSON object with a single 115 | * Time property. 116 | */ 117 | private JSONObject getValidModelWithRefs() { 118 | return new JSONObject().put("Time", "2019-12-12T10:10:22.212Z"); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/software/amazon/cloudformation/resource/Validator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import java.net.URI; 18 | 19 | import lombok.Builder; 20 | 21 | import org.everit.json.schema.Schema; 22 | import org.everit.json.schema.loader.SchemaClient; 23 | import org.everit.json.schema.loader.SchemaLoader.SchemaLoaderBuilder; 24 | import org.everit.json.schema.loader.internal.DefaultSchemaClient; 25 | import org.json.JSONObject; 26 | 27 | import software.amazon.cloudformation.resource.exceptions.ValidationException; 28 | 29 | public class Validator extends BaseValidator { 30 | 31 | protected static final String RESOURCE_DEFINITION_SCHEMA_PATH = "/schema/" 32 | + "provider.definition.schema.v1.json"; 33 | protected static final String TYPE_CONFIGURATION_DEFINITION_SCHEMA_PATH = "/schema/" 34 | + "provider.configuration.definition.schema.v1.json"; 35 | private static final URI RESOURCE_DEFINITION_SCHEMA_URI = newURI( 36 | "https://schema.cloudformation.us-east-1.amazonaws.com/provider.definition.schema.v1.json"); 37 | private JSONObject typeConfigurationDefinitionJson; 38 | 39 | public Validator(SchemaClient downloader) { 40 | this(loadResourceAsJSON(RESOURCE_DEFINITION_SCHEMA_PATH), downloader); 41 | this.typeConfigurationDefinitionJson = loadResourceAsJSON(TYPE_CONFIGURATION_DEFINITION_SCHEMA_PATH); 42 | } 43 | 44 | private Validator(JSONObject definitionSchema, 45 | SchemaClient downloader) { 46 | super(definitionSchema, downloader); 47 | } 48 | 49 | @Builder 50 | public Validator() { 51 | this(new DefaultSchemaClient()); 52 | } 53 | 54 | /** 55 | * builds a Schema instance that can be used to validate Resource Definition Schema as a JSON object 56 | */ 57 | private Schema makeResourceDefinitionSchema() { 58 | SchemaLoaderBuilder builder = getSchemaLoader(); 59 | registerMetaSchema(builder, typeConfigurationDefinitionJson); 60 | builder.schemaJson(definitionSchemaJsonObject); 61 | return builder.build().load().build(); 62 | } 63 | 64 | /** 65 | * Performs JSON Schema validation for the input resource definition against the resource provider definition schema 66 | * 67 | * @param resourceDefinition JSON-encoded resource definition 68 | * @throws ValidationException Thrown for any schema validation errors 69 | */ 70 | void validateResourceDefinition(final JSONObject resourceDefinition) { 71 | // loading resource definition always performs validation 72 | loadResourceDefinitionSchema(resourceDefinition); 73 | } 74 | 75 | /** 76 | * create a Schema instance that can be used to validate CloudFormation resources. 77 | * 78 | * @param resourceDefinition - CloudFormation Resource Provider Schema (Resource Definition) 79 | * @return - Schema instance for the given Resource Definition 80 | * @throws ValidationException if supplied resourceDefinition is invalid. 81 | */ 82 | public Schema loadResourceDefinitionSchema(final JSONObject resourceDefinition) { 83 | 84 | // inject/replace $schema URI to ensure that provider definition schema is used 85 | resourceDefinition.put("$schema", RESOURCE_DEFINITION_SCHEMA_URI.toString()); 86 | 87 | try { 88 | // step 1: validate resourceDefinition as a JSON object 89 | // this validator cannot validate schema-specific attributes. For example if definition 90 | // contains "propertyA": { "$ref":"./some-non-existent-location.json#definitions/PropertyX"} 91 | // validateObject will succeed, because all it cares about is that "$ref" is a URI 92 | // In order to validate that $ref points at an existing location in an existing document 93 | // we have to "load" the schema 94 | Schema resourceDefValidator = makeResourceDefinitionSchema(); 95 | resourceDefValidator.validate(resourceDefinition); 96 | 97 | // step 2: load resource definition as a Schema that can be used to validate resource models; 98 | // definitionSchemaJsonObject becomes a meta-schema 99 | SchemaLoaderBuilder builder = getSchemaLoader(); 100 | registerMetaSchema(builder, resourceDefinition); 101 | registerMetaSchema(builder, typeConfigurationDefinitionJson); 102 | builder.schemaJson(resourceDefinition); 103 | // when resource definition is loaded as a schema, $refs are resolved and validated 104 | return builder.build().load().build(); 105 | } catch (final org.everit.json.schema.ValidationException e) { 106 | throw ValidationException.newScrubbedException(e); 107 | } catch (final org.everit.json.schema.SchemaException e) { 108 | throw new ValidationException(e.getMessage(), e.getSchemaLocation(), e); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/software/amazon/cloudformation/resource/exceptions/ValidationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource.exceptions; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.Collections; 20 | import java.util.List; 21 | 22 | import lombok.Getter; 23 | 24 | @Getter 25 | public class ValidationException extends RuntimeException { 26 | private static final long serialVersionUID = 42L; 27 | 28 | /** 29 | * Error messages thrown for these keywords don't contain values 30 | */ 31 | private static final List SAFE_KEYWORDS = Arrays.asList( 32 | // object keywords 33 | "required", "minProperties", "maxProperties", "dependencies", "additionalProperties", 34 | // string keywords 35 | "minLength", "maxLength", 36 | // array keywords 37 | "minItems", "maxItems", "uniqueItems", "contains", 38 | // misc keywords 39 | "type", "allOf", "anyOf", "oneOf"); 40 | @SuppressWarnings({ "serial" }) 41 | private final List causingExceptions; 42 | private final String keyword; 43 | private final String schemaPointer; 44 | 45 | public ValidationException(final String message, 46 | final String keyword, 47 | final String schemaPointer) { 48 | this(message, Collections.emptyList(), keyword, schemaPointer); 49 | } 50 | 51 | public ValidationException(final String message, 52 | final String schemaPointer, 53 | final Exception cause) { 54 | super(message, cause); 55 | this.causingExceptions = Collections.emptyList(); 56 | this.keyword = ""; 57 | this.schemaPointer = schemaPointer; 58 | } 59 | 60 | public ValidationException(final String message, 61 | final List causingExceptions, 62 | final String keyword, 63 | final String schemaPointer) { 64 | super(message); 65 | this.causingExceptions = Collections 66 | .unmodifiableList(causingExceptions == null ? Collections.emptyList() : causingExceptions); 67 | this.keyword = keyword; 68 | this.schemaPointer = schemaPointer; 69 | } 70 | 71 | /** 72 | * Marked private -- must use {@link #newScrubbedException} 73 | */ 74 | private ValidationException(final org.everit.json.schema.ValidationException validationException) { 75 | this(validationException.getMessage(), validationException); 76 | } 77 | 78 | /** 79 | * Marked private -- must use {@link #newScrubbedException} 80 | */ 81 | private ValidationException(final String errorMessage, 82 | final org.everit.json.schema.ValidationException validationException) { 83 | super(errorMessage); 84 | this.keyword = validationException.getKeyword(); 85 | this.schemaPointer = validationException.getPointerToViolation(); 86 | 87 | final List causingExceptions = new ArrayList<>(); 88 | if (validationException.getCausingExceptions() != null) { 89 | for (final org.everit.json.schema.ValidationException e : validationException.getCausingExceptions()) { 90 | causingExceptions.add(newScrubbedException(e)); 91 | } 92 | } 93 | this.causingExceptions = Collections.unmodifiableList(causingExceptions); 94 | } 95 | 96 | /** 97 | * /** In order to ensure sensitive properties aren't displayed, scrub any error 98 | * messages that emit property values 99 | * 100 | * @param e The exception to redact 101 | * @return a redacted {@link ValidationException} 102 | */ 103 | public static ValidationException newScrubbedException(final org.everit.json.schema.ValidationException e) { 104 | // A parent exception has multiple errors in the subSchema, and will just emit 105 | // "{X} schema validations found" 106 | final boolean isParentException = e.getKeyword() == null && e.getCausingExceptions() != null 107 | && !e.getCausingExceptions().isEmpty(); 108 | if (isParentException || SAFE_KEYWORDS.contains(e.getKeyword())) { 109 | return new ValidationException(e); 110 | } else { 111 | final String errorMessage = String.format("%s: failed validation constraint for keyword [%s]", 112 | e.getPointerToViolation(), e.getKeyword()); 113 | 114 | return new ValidationException(errorMessage, e); 115 | } 116 | } 117 | 118 | /** 119 | * Build an exception message containing all nested exceptions 120 | * 121 | * @param e the exception to construct a message from 122 | * @return a standard exception message from a {@link ValidationException} tree 123 | */ 124 | public static String buildFullExceptionMessage(final ValidationException e) { 125 | return buildFullExceptionMessageHelper(e).trim(); 126 | } 127 | 128 | private static String buildFullExceptionMessageHelper(final ValidationException e) { 129 | StringBuilder builder = new StringBuilder(); 130 | final boolean isParentException = e.getKeyword() == null && e.getCausingExceptions() != null 131 | && !e.getCausingExceptions().isEmpty(); 132 | if (!isParentException && e.getMessage() != null) { 133 | builder.append(e.getMessage() + "\n"); 134 | } 135 | if (e.getCausingExceptions() != null) { 136 | for (ValidationException cause : e.getCausingExceptions()) { 137 | builder.append(buildFullExceptionMessageHelper(cause)); 138 | } 139 | } 140 | return builder.toString(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/software/amazon/cloudformation/resource/BaseValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import java.net.URI; 18 | import java.net.URISyntaxException; 19 | 20 | import lombok.SneakyThrows; 21 | 22 | import org.everit.json.schema.Schema; 23 | import org.everit.json.schema.loader.SchemaClient; 24 | import org.everit.json.schema.loader.SchemaLoader; 25 | import org.everit.json.schema.loader.SchemaLoader.SchemaLoaderBuilder; 26 | import org.json.JSONException; 27 | import org.json.JSONObject; 28 | import org.json.JSONTokener; 29 | 30 | import software.amazon.cloudformation.resource.exceptions.ValidationException; 31 | 32 | class BaseValidator implements SchemaValidator { 33 | protected static final String ID_KEY = "$id"; 34 | protected static final String SOURCE_URL = "sourceUrl"; 35 | protected static final String TYPE_NAME = "typeName"; 36 | protected static final URI JSON_SCHEMA_URI_HTTP = newURI("http://json-schema.org/draft-07/schema"); 37 | protected static final String JSON_SCHEMA_PATH = "/schema/schema"; 38 | protected static final String BASE_DEFINITION_SCHEMA_PATH = "/schema/base.definition.schema.v1.json"; 39 | 40 | /** 41 | * resource definition schema ("resource schema schema"). All resource schemas 42 | * are validated against this one and JSON schema draft v7 below. 43 | */ 44 | protected final JSONObject definitionSchemaJsonObject; 45 | 46 | /** 47 | * locally cached draft-07 JSON schema. All resource schemas are validated 48 | * against it 49 | */ 50 | private final JSONObject jsonSchemaObject; 51 | 52 | /** 53 | * locally cached draft-07 JSON schema. All resource schemas are validated 54 | * against it 55 | */ 56 | private final JSONObject baseDefinitionSchemaObject; 57 | /** 58 | * this is what SchemaLoader uses to download remote $refs. Not necessarily an 59 | * HTTP client, see the docs for details. We override the default SchemaClient 60 | * client in unit tests to be able to control how remote refs are resolved. 61 | */ 62 | private final SchemaClient downloader; 63 | 64 | BaseValidator(JSONObject definitionSchema, 65 | SchemaClient downloader) { 66 | this.jsonSchemaObject = loadResourceAsJSON(JSON_SCHEMA_PATH); 67 | this.definitionSchemaJsonObject = definitionSchema; 68 | this.downloader = downloader; 69 | this.baseDefinitionSchemaObject = loadResourceAsJSON(BASE_DEFINITION_SCHEMA_PATH); 70 | } 71 | 72 | @Override 73 | public void validateObject(final JSONObject modelObject, final JSONObject definitionSchemaObject) throws ValidationException { 74 | final SchemaLoaderBuilder loader = getSchemaLoader(definitionSchemaObject); 75 | 76 | try { 77 | final Schema schema = loader.build().load().build(); 78 | schema.validate(modelObject); // throws a ValidationException if this object is invalid 79 | } catch (final org.everit.json.schema.ValidationException e) { 80 | throw ValidationException.newScrubbedException(e); 81 | } 82 | } 83 | 84 | @Override 85 | public Schema getListHandlerSchema(final JSONObject definitionSchemaObject) 86 | throws ValidationException { 87 | final JSONObject handlers = definitionSchemaObject.has("handlers") 88 | ? definitionSchemaObject.getJSONObject("handlers") 89 | : new JSONObject(); // MUST always exist 90 | final JSONObject list = handlers.has("list") ? handlers.getJSONObject("list") : new JSONObject(); // 91 | final JSONObject emptySchema = new JSONObject(); 92 | emptySchema.put("additionalProperties", true); 93 | 94 | final JSONObject schemaOverride = list.has("handlerSchema") ? list.getJSONObject("handlerSchema") : emptySchema; 95 | final SchemaLoaderBuilder loader = getSchemaLoader(definitionSchemaObject, schemaOverride); 96 | 97 | final Schema schema = loader.build().load().build(); 98 | return schema; 99 | } 100 | 101 | @Override 102 | public void validateObjectByListHandlerSchema(final JSONObject modelObject, final JSONObject definitionSchemaObject) 103 | throws ValidationException { 104 | final Schema schema = getListHandlerSchema(definitionSchemaObject); 105 | schema.validate(modelObject); // throws a ValidationException if this object is invalid 106 | } 107 | 108 | /** 109 | * Register a meta-schema with the SchemaLoaderBuilder. The meta-schema $id is used to generate schema URI 110 | * This has the effect of caching the meta-schema. When SchemaLoaderBuilder is used to build the Schema object, 111 | * the cached version will be used. No calls to remote URLs will be made. 112 | * Validator caches JSON schema (/resources/schema) and Resource Definition Schema 113 | * (/resources/provider.definition.schema.v1.json) 114 | * 115 | * @param loaderBuilder 116 | * @param schema meta-schema JSONObject to be cached. Must have a valid $id property 117 | */ 118 | void registerMetaSchema(final SchemaLoaderBuilder loaderBuilder, JSONObject schema) { 119 | try { 120 | final String id; 121 | if (schema.has(ID_KEY)) { 122 | id = schema.getString(ID_KEY); 123 | 124 | if (id.isEmpty()) { 125 | throw new ValidationException("Invalid $id value", "$id", "[empty string]"); 126 | } 127 | final URI uri = new URI(id); 128 | loaderBuilder.registerSchemaByURI(uri, schema); 129 | } 130 | } catch (URISyntaxException | JSONException e) { 131 | // $id is not a string or URI is invalid 132 | throw new ValidationException("Invalid $id value", "$id", e); 133 | } 134 | } 135 | 136 | /** 137 | * Convenience method - creates a SchemaLoaderBuilder with cached JSON draft-07 meta-schema 138 | * 139 | * @param schemaObject 140 | * @return 141 | */ 142 | SchemaLoaderBuilder getSchemaLoader(JSONObject schemaObject) { 143 | return getSchemaLoader().schemaJson(schemaObject); 144 | } 145 | 146 | SchemaLoaderBuilder getSchemaLoader(JSONObject schemaObject, JSONObject handlerSchema) { 147 | SchemaLoaderBuilder schemaLoader = getSchemaLoader(); 148 | 149 | if (schemaObject.has(SOURCE_URL)) { 150 | final String sourceUrl = schemaObject.getString(SOURCE_URL); 151 | final String schema_file = "resource-schema.json"; 152 | schemaLoader.registerSchemaByURI(newURI(sourceUrl + "/" + schema_file), schemaObject) 153 | .resolutionScope(newURI(sourceUrl + "/")); 154 | } 155 | return schemaLoader.schemaJson(handlerSchema); 156 | } 157 | 158 | /** get schema-builder preloaded with JSON draft V7 meta-schema */ 159 | SchemaLoaderBuilder getSchemaLoader() { 160 | final SchemaLoaderBuilder builder = SchemaLoader 161 | .builder() 162 | .draftV7Support() 163 | .schemaClient(downloader); 164 | // // registers the local schema with the draft-07 url 165 | // // draftV7 schema is registered twice because - once for HTTP and once for HTTPS URIs 166 | builder.registerSchemaByURI(JSON_SCHEMA_URI_HTTP, jsonSchemaObject); 167 | // registers the local base definition schema to resolve the refs 168 | registerMetaSchema(builder, baseDefinitionSchemaObject); 169 | return builder; 170 | } 171 | 172 | static JSONObject loadResourceAsJSON(String path) { 173 | return new JSONObject(new JSONTokener(BaseValidator.class.getResourceAsStream(path))); 174 | } 175 | 176 | /** wrapper around new URI that throws an unchecked exception */ 177 | @SneakyThrows(URISyntaxException.class) 178 | static URI newURI(final String uri) { 179 | return new URI(uri); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | software.amazon.cloudformation 5 | aws-cloudformation-resource-schema 6 | AWS CloudFormation Resource Schema Validator 7 | This library contains the metaschema for CloudFormation resource types and validation tools. 8 | ttps://github.com/aws-cloudformation/aws-cloudformation-resource-schema 9 | 10 | 2.0.10 11 | 12 | 1.8 13 | 1.8 14 | UTF-8 15 | UTF-8 16 | 3.1.1 17 | 3.0.1 18 | 19 | 20 | 21 | 22 | Apache License, Version 2.0 23 | https://aws.amazon.com/apache2.0 24 | repo 25 | 26 | 27 | 28 | 29 | 30 | aws-cloudformation-developers 31 | Amazon Web Services 32 | https://aws.amazon.com 33 | 34 | developer 35 | 36 | 37 | 38 | 39 | 40 | scm:git:https://github.com/aws-cloudformation/aws-cloudformation-resource-schema.git 41 | scm:git:git@github.com:aws-cloudformation/aws-cloudformation-resource-schema.git 42 | https://github.com/aws-cloudformation/aws-cloudformation-resource-schema 43 | 44 | 45 | 46 | 47 | com.github.erosb 48 | everit-json-schema 49 | 1.14.1 50 | 51 | 52 | 53 | org.assertj 54 | assertj-core 55 | 3.12.2 56 | test 57 | 58 | 59 | 60 | org.mockito 61 | mockito-junit-jupiter 62 | 2.22.0 63 | test 64 | 65 | 66 | 67 | org.junit.jupiter 68 | junit-jupiter-api 69 | 5.7.0 70 | test 71 | 72 | 73 | 74 | org.junit.jupiter 75 | junit-jupiter-params 76 | 5.7.0 77 | test 78 | 79 | 80 | 81 | org.junit.jupiter 82 | junit-jupiter-engine 83 | 5.7.0 84 | test 85 | 86 | 87 | 88 | org.projectlombok 89 | lombok 90 | 1.18.20 91 | provided 92 | 93 | 94 | 95 | 96 | 97 | ${basedir}/src/main/resources 98 | 99 | 100 | ${basedir}/src/test/resources 101 | 102 | 103 | 104 | 105 | com.diffplug.spotless 106 | spotless-maven-plugin 107 | 1.23.1 108 | 109 | 110 | 111 | 112 | /src/main/resources/eclipse-java-formatter.xml 113 | 4.11.0 114 | 115 | 116 | /src/main/resources/licenseHeader 117 | 118 | 119 | 120 | com,java,javax,org,software 121 | 122 | 123 | 124 | 125 | 126 | lint 127 | validate 128 | 129 | apply 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-compiler-plugin 137 | 3.8.0 138 | 139 | true 140 | 141 | -Xlint:all,-options,-processing 142 | -Werror 143 | 144 | 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-javadoc-plugin 149 | ${maven-javadoc-plugin.version} 150 | 151 | public 152 | 153 | 154 | 155 | 156 | attach-javadocs 157 | 158 | jar 159 | 160 | 161 | 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-source-plugin 167 | ${maven-source-plugin.version} 168 | 169 | 170 | attach-sources 171 | 172 | jar 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-surefire-plugin 180 | 3.0.0-M3 181 | 182 | 183 | org.jacoco 184 | jacoco-maven-plugin 185 | 0.8.4 186 | 187 | 188 | default-prepare-agent 189 | 190 | prepare-agent 191 | 192 | 193 | 194 | default-report 195 | 196 | report 197 | 198 | 199 | 200 | default-check 201 | 202 | check 203 | 204 | 205 | 206 | 207 | CLASS 208 | 209 | 210 | LINE 211 | COVEREDRATIO 212 | 0.87 213 | 214 | 215 | BRANCH 216 | COVEREDRATIO 217 | 0.75 218 | 219 | 220 | INSTRUCTION 221 | COVEREDRATIO 222 | 0.87 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | org.apache.maven.plugins 233 | maven-checkstyle-plugin 234 | 3.1.0 235 | 236 | src/main/resources/checkstyle.xml 237 | UTF-8 238 | true 239 | true 240 | false 241 | 242 | 243 | 244 | validate 245 | validate 246 | 247 | check 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | publishing 257 | 258 | 259 | 260 | org.apache.maven.plugins 261 | maven-gpg-plugin 262 | 1.6 263 | 264 | 265 | sign-artifacts 266 | verify 267 | 268 | sign 269 | 270 | 271 | 272 | --pinentry-mode 273 | loopback 274 | 275 | 276 | 277 | 278 | 279 | 280 | org.sonatype.plugins 281 | nexus-staging-maven-plugin 282 | 1.6.8 283 | true 284 | 285 | sonatype-nexus-staging 286 | https://aws.oss.sonatype.org 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /src/main/resources/schema/base.definition.schema.v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/base.definition.schema.v1.json", 4 | "title": "CloudFormation Provider Base Definition MetaSchema", 5 | "description": "All the basic building blocks for the provider definition schemas are in this schema to maintain consistency among different provider definition schemas. Provider definition schemas could refer to this schema for using basic things like properties, definitions etc.", 6 | "definitions": { 7 | "httpsUrl": { 8 | "type": "string", 9 | "pattern": "^https://[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])(:[0-9]*)*([?/#].*)?$", 10 | "maxLength": 4096 11 | }, 12 | "jsonPointerArray": { 13 | "type": "array", 14 | "minItems": 1, 15 | "items": { 16 | "type": "string", 17 | "format": "json-pointer" 18 | } 19 | }, 20 | "schemaArray": { 21 | "type": "array", 22 | "minItems": 1, 23 | "items": { 24 | "$ref": "#/definitions/properties" 25 | } 26 | }, 27 | "validations": { 28 | "dependencies": { 29 | "enum": { 30 | "$comment": "Enforce that properties are strongly typed when enum, or const is specified.", 31 | "required": [ 32 | "type" 33 | ] 34 | }, 35 | "const": { 36 | "required": [ 37 | "type" 38 | ] 39 | }, 40 | "properties": { 41 | "$comment": "An object cannot have both defined and undefined properties; therefore, patternProperties is not allowed when properties is specified.", 42 | "not": { 43 | "required": [ 44 | "patternProperties" 45 | ] 46 | } 47 | } 48 | } 49 | }, 50 | "properties": { 51 | "allOf": [ 52 | { 53 | "$ref": "#/definitions/validations" 54 | }, 55 | { 56 | "$comment": "The following subset of draft-07 property references is supported for resource definitions. Nested properties are disallowed and should be specified as a $ref to a definitions block.", 57 | "type": "object", 58 | "properties": { 59 | "insertionOrder": { 60 | "description": "When set to true, this flag indicates that the order of insertion of the array will be honored, and that changing the order of the array would indicate a diff", 61 | "type": "boolean", 62 | "default": true 63 | }, 64 | "arrayType": { 65 | "description": "When set to AttributeList, it indicates that the array is of nested type objects, and when set to Standard it indicates that the array consists of primitive types", 66 | "type": "string", 67 | "default": "Standard", 68 | "enum": [ 69 | "Standard", 70 | "AttributeList" 71 | ] 72 | }, 73 | "$ref": { 74 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$ref" 75 | }, 76 | "$comment": { 77 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$comment" 78 | }, 79 | "title": { 80 | "$ref": "http://json-schema.org/draft-07/schema#/properties/title" 81 | }, 82 | "description": { 83 | "$ref": "http://json-schema.org/draft-07/schema#/properties/description" 84 | }, 85 | "examples": { 86 | "$ref": "http://json-schema.org/draft-07/schema#/properties/examples" 87 | }, 88 | "default": { 89 | "$ref": "http://json-schema.org/draft-07/schema#/properties/default" 90 | }, 91 | "multipleOf": { 92 | "$ref": "http://json-schema.org/draft-07/schema#/properties/multipleOf" 93 | }, 94 | "maximum": { 95 | "$ref": "http://json-schema.org/draft-07/schema#/properties/maximum" 96 | }, 97 | "exclusiveMaximum": { 98 | "$ref": "http://json-schema.org/draft-07/schema#/properties/exclusiveMaximum" 99 | }, 100 | "minimum": { 101 | "$ref": "http://json-schema.org/draft-07/schema#/properties/minimum" 102 | }, 103 | "exclusiveMinimum": { 104 | "$ref": "http://json-schema.org/draft-07/schema#/properties/exclusiveMinimum" 105 | }, 106 | "maxLength": { 107 | "$ref": "http://json-schema.org/draft-07/schema#/properties/maxLength" 108 | }, 109 | "minLength": { 110 | "$ref": "http://json-schema.org/draft-07/schema#/properties/minLength" 111 | }, 112 | "pattern": { 113 | "$ref": "http://json-schema.org/draft-07/schema#/properties/pattern" 114 | }, 115 | "items": { 116 | "$comment": "Redefined as just a schema. A list of schemas is not allowed", 117 | "$ref": "#/definitions/properties", 118 | "default": {} 119 | }, 120 | "maxItems": { 121 | "$ref": "http://json-schema.org/draft-07/schema#/properties/maxItems" 122 | }, 123 | "minItems": { 124 | "$ref": "http://json-schema.org/draft-07/schema#/properties/minItems" 125 | }, 126 | "uniqueItems": { 127 | "$ref": "http://json-schema.org/draft-07/schema#/properties/uniqueItems" 128 | }, 129 | "contains": { 130 | "$ref": "http://json-schema.org/draft-07/schema#/properties/contains" 131 | }, 132 | "maxProperties": { 133 | "$ref": "http://json-schema.org/draft-07/schema#/properties/maxProperties" 134 | }, 135 | "minProperties": { 136 | "$ref": "http://json-schema.org/draft-07/schema#/properties/minProperties" 137 | }, 138 | "required": { 139 | "$ref": "http://json-schema.org/draft-07/schema#/properties/required" 140 | }, 141 | "properties": { 142 | "type": "object", 143 | "patternProperties": { 144 | "^[A-Za-z0-9]{1,64}$": { 145 | "$ref": "#/definitions/properties" 146 | } 147 | }, 148 | "additionalProperties": false, 149 | "minProperties": 1 150 | }, 151 | "additionalProperties": { 152 | "$comment": "All properties of a resource must be expressed in the schema - arbitrary inputs are not allowed", 153 | "type": "boolean", 154 | "const": false 155 | }, 156 | "patternProperties": { 157 | "$comment": "patternProperties allow providers to introduce a specification for key-value pairs, or Map inputs.", 158 | "type": "object", 159 | "propertyNames": { 160 | "format": "regex" 161 | } 162 | }, 163 | "dependencies": { 164 | "$comment": "Redefined to capture our properties override.", 165 | "type": "object", 166 | "additionalProperties": { 167 | "anyOf": [ 168 | { 169 | "$ref": "#/definitions/properties" 170 | }, 171 | { 172 | "$ref": "http://json-schema.org/draft-07/schema#/definitions/stringArray" 173 | } 174 | ] 175 | } 176 | }, 177 | "const": { 178 | "$ref": "http://json-schema.org/draft-07/schema#/properties/const" 179 | }, 180 | "enum": { 181 | "$ref": "http://json-schema.org/draft-07/schema#/properties/enum" 182 | }, 183 | "type": { 184 | "$ref": "http://json-schema.org/draft-07/schema#/properties/type" 185 | }, 186 | "format": { 187 | "$ref": "http://json-schema.org/draft-07/schema#/properties/format" 188 | }, 189 | "allOf": { 190 | "$ref": "#/definitions/schemaArray" 191 | }, 192 | "anyOf": { 193 | "$ref": "#/definitions/schemaArray" 194 | }, 195 | "oneOf": { 196 | "$ref": "#/definitions/schemaArray" 197 | } 198 | }, 199 | "additionalProperties": false 200 | } 201 | ] 202 | } 203 | }, 204 | "type": "object", 205 | "patternProperties": { 206 | "^\\$id$": { 207 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$id" 208 | } 209 | }, 210 | "properties": { 211 | "$schema": { 212 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$schema" 213 | }, 214 | "typeName": { 215 | "$comment": "Resource Type Identifier", 216 | "examples": [ 217 | "Organization::Service::Resource", 218 | "AWS::EC2::Instance", 219 | "Initech::TPS::Report" 220 | ], 221 | "type": "string", 222 | "pattern": "^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$" 223 | }, 224 | "$comment": { 225 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$comment" 226 | }, 227 | "title": { 228 | "$ref": "http://json-schema.org/draft-07/schema#/properties/title" 229 | }, 230 | "description": { 231 | "$comment": "A short description of the resource provider. This will be shown in the AWS CloudFormation console.", 232 | "$ref": "http://json-schema.org/draft-07/schema#/properties/description" 233 | }, 234 | "sourceUrl": { 235 | "$comment": "The location of the source code for this resource provider, to help interested parties submit issues or improvements.", 236 | "examples": [ 237 | "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-s3" 238 | ], 239 | "$ref": "#/definitions/httpsUrl" 240 | }, 241 | "documentationUrl": { 242 | "$comment": "A page with supplemental documentation. The property documentation in schemas should be able to stand alone, but this is an opportunity for e.g. rich examples or more guided documents.", 243 | "examples": [ 244 | "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/CHAP_Using.html" 245 | ], 246 | "$ref": "#/definitions/httpsUrl" 247 | }, 248 | "additionalProperties": { 249 | "$comment": "All properties of a resource must be expressed in the schema - arbitrary inputs are not allowed", 250 | "type": "boolean", 251 | "const": false 252 | }, 253 | "properties": { 254 | "type": "object", 255 | "patternProperties": { 256 | "^[A-Za-z0-9]{1,64}$": { 257 | "$ref": "#/definitions/properties" 258 | } 259 | }, 260 | "additionalProperties": false, 261 | "minProperties": 1 262 | }, 263 | "definitions": { 264 | "type": "object", 265 | "patternProperties": { 266 | "^[A-Za-z0-9]{1,64}$": { 267 | "$ref": "#/definitions/properties" 268 | } 269 | }, 270 | "additionalProperties": false 271 | }, 272 | "remote": { 273 | "description": "Reserved for CloudFormation use. A namespace to inline remote schemas.", 274 | "type": "object", 275 | "patternProperties": { 276 | "^schema[0-9]+$": { 277 | "description": "Reserved for CloudFormation use. A inlined remote schema.", 278 | "type": "object", 279 | "properties": { 280 | "$comment": { 281 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$comment" 282 | }, 283 | "properties": { 284 | "$ref": "#/properties/properties" 285 | }, 286 | "definitions": { 287 | "$ref": "#/properties/definitions" 288 | } 289 | }, 290 | "additionalProperties": true 291 | } 292 | }, 293 | "additionalProperties": false 294 | }, 295 | "deprecatedProperties": { 296 | "description": "A list of JSON pointers to properties that have been deprecated by the underlying service provider. These properties are still accepted in create & update operations, however they may be ignored, or converted to a consistent model on application. Deprecated properties are not guaranteed to be present in read paths.", 297 | "$ref": "#/definitions/jsonPointerArray" 298 | }, 299 | "required": { 300 | "$ref": "http://json-schema.org/draft-07/schema#/properties/required" 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/main/java/software/amazon/cloudformation/resource/ResourceTypeSchema.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.HashMap; 20 | import java.util.HashSet; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Optional; 24 | import java.util.Set; 25 | import java.util.stream.Collectors; 26 | 27 | import lombok.AccessLevel; 28 | import lombok.Getter; 29 | 30 | import org.everit.json.schema.CombinedSchema; 31 | import org.everit.json.schema.JSONPointer; 32 | import org.everit.json.schema.JSONPointerException; 33 | import org.everit.json.schema.ObjectSchema; 34 | import org.everit.json.schema.PublicJSONPointer; 35 | import org.everit.json.schema.Schema; 36 | import org.json.JSONObject; 37 | 38 | import software.amazon.cloudformation.resource.exceptions.ValidationException; 39 | 40 | @Getter 41 | public class ResourceTypeSchema { 42 | 43 | private static final Validator VALIDATOR = new Validator(); 44 | private static final Integer DEFAULT_TIMEOUT_IN_MINUTES = 120; 45 | 46 | private final Map unprocessedProperties = new HashMap<>(); 47 | 48 | private final String sourceUrl; 49 | private final String documentationUrl; 50 | private final String typeName; 51 | private final String schemaUrl; // $schema 52 | @Getter(AccessLevel.NONE) 53 | private JSONObject schemaDefinition = null; 54 | 55 | private final String replacementStrategy; 56 | private final boolean taggable; 57 | private ResourceTagging tagging = ResourceTagging.DEFAULT; 58 | private boolean hasConfiguredTagging = false; 59 | private final List createOnlyProperties = new ArrayList<>(); 60 | private final List conditionalCreateOnlyProperties = new ArrayList<>(); 61 | private final List deprecatedProperties = new ArrayList<>(); 62 | private final List primaryIdentifier = new ArrayList<>(); 63 | private final List> additionalIdentifiers = new ArrayList<>(); 64 | private final List readOnlyProperties = new ArrayList<>(); 65 | private final List writeOnlyProperties = new ArrayList<>(); 66 | private final Map propertyTransform = new HashMap<>(); 67 | @Getter(AccessLevel.NONE) 68 | private final Map handlers = new HashMap<>(); 69 | private final Schema schema; 70 | 71 | public ResourceTypeSchema(Schema schema) { 72 | this.schema = schema; 73 | schema.getUnprocessedProperties().forEach(this.unprocessedProperties::put); 74 | 75 | this.sourceUrl = this.unprocessedProperties.containsKey("sourceUrl") 76 | ? this.unprocessedProperties.get("sourceUrl").toString() 77 | : null; 78 | this.unprocessedProperties.remove("sourceUrl"); 79 | 80 | this.documentationUrl = this.unprocessedProperties.containsKey("documentationUrl") 81 | ? this.unprocessedProperties.get("documentationUrl").toString() 82 | : null; 83 | this.unprocessedProperties.remove("documentationUrl"); 84 | 85 | // typeName is mandatory by schema 86 | this.typeName = this.unprocessedProperties.get("typeName").toString(); 87 | this.unprocessedProperties.remove("typeName"); 88 | 89 | this.schemaUrl = this.unprocessedProperties.containsKey("$schema") 90 | ? this.unprocessedProperties.get("$schema").toString() 91 | : null; 92 | this.unprocessedProperties.remove("$schema"); 93 | 94 | this.replacementStrategy = this.unprocessedProperties.containsKey("replacementStrategy") 95 | ? this.unprocessedProperties.get("replacementStrategy").toString() 96 | : "create_then_delete"; 97 | this.unprocessedProperties.remove("replacementStrategy"); 98 | 99 | this.unprocessedProperties.computeIfPresent("conditionalCreateOnlyProperties", (k, v) -> { 100 | ((ArrayList) v).forEach(p -> this.conditionalCreateOnlyProperties.add(new JSONPointer(p.toString()))); 101 | return null; 102 | }); 103 | 104 | this.unprocessedProperties.computeIfPresent("createOnlyProperties", (k, v) -> { 105 | ((ArrayList) v).forEach(p -> this.createOnlyProperties.add(new JSONPointer(p.toString()))); 106 | return null; 107 | }); 108 | 109 | this.unprocessedProperties.computeIfPresent("deprecatedProperties", (k, v) -> { 110 | ((ArrayList) v).forEach(p -> this.deprecatedProperties.add(new JSONPointer(p.toString()))); 111 | return null; 112 | }); 113 | 114 | this.unprocessedProperties.computeIfPresent("primaryIdentifier", (k, v) -> { 115 | ((ArrayList) v).forEach(p -> this.primaryIdentifier.add(new JSONPointer(p.toString()))); 116 | return null; 117 | }); 118 | 119 | this.unprocessedProperties.computeIfPresent("additionalIdentifiers", (k, v) -> { 120 | ((ArrayList) v).forEach(p -> { 121 | final ArrayList identifiers = new ArrayList<>(); 122 | ((ArrayList) p).forEach(pi -> identifiers.add(new JSONPointer(pi.toString()))); 123 | this.additionalIdentifiers.add(identifiers); 124 | }); 125 | return null; 126 | }); 127 | 128 | this.unprocessedProperties.computeIfPresent("readOnlyProperties", (k, v) -> { 129 | ((ArrayList) v).forEach(p -> this.readOnlyProperties.add(new JSONPointer(p.toString()))); 130 | return null; 131 | }); 132 | 133 | this.unprocessedProperties.computeIfPresent("writeOnlyProperties", (k, v) -> { 134 | ((ArrayList) v).forEach(p -> this.writeOnlyProperties.add(new JSONPointer(p.toString()))); 135 | return null; 136 | }); 137 | this.unprocessedProperties.computeIfPresent("propertyTransform", (k, v) -> { 138 | ((Map) v).forEach((key, value) -> { 139 | this.propertyTransform.put(key.toString(), value.toString()); 140 | }); 141 | return null; 142 | }); 143 | this.unprocessedProperties.computeIfPresent("handlers", (k, v) -> { 144 | ((HashMap) v).keySet().forEach(handlerKey -> { 145 | HashMap handlerInfo = (HashMap) ((HashMap) v).get(handlerKey); 146 | HashSet handlerPermissions = new HashSet<>(); 147 | ((List) handlerInfo.get("permissions")).forEach(permission -> handlerPermissions.add(permission.toString())); 148 | Integer timeoutInMinutes = handlerInfo.containsKey("timeoutInMinutes") 149 | ? ((Integer) handlerInfo.get("timeoutInMinutes")) 150 | : DEFAULT_TIMEOUT_IN_MINUTES; 151 | this.handlers.put(handlerKey.toString(), new Handler(handlerPermissions, timeoutInMinutes)); 152 | }); 153 | return null; 154 | }); 155 | 156 | this.unprocessedProperties.computeIfPresent("tagging", (k, v) -> { 157 | hasConfiguredTagging = true; 158 | final ResourceTagging taggingValue = new ResourceTagging(true); 159 | ((Map) v).forEach((key, value) -> { 160 | if (key.equals(ResourceTagging.TAGGABLE)) { 161 | taggingValue.setTaggable(Boolean.parseBoolean(value.toString())); 162 | } else if (key.equals(ResourceTagging.TAG_ON_CREATE)) { 163 | taggingValue.setTagOnCreate(Boolean.parseBoolean(value.toString())); 164 | } else if (key.equals(ResourceTagging.TAG_UPDATABLE)) { 165 | taggingValue.setTagUpdatable(Boolean.parseBoolean(value.toString())); 166 | } else if (key.equals(ResourceTagging.CLOUDFORMATION_SYSTEM_TAGS)) { 167 | taggingValue.setCloudFormationSystemTags(Boolean.parseBoolean(value.toString())); 168 | } else if (key.equals(ResourceTagging.TAG_PROPERTY)) { 169 | taggingValue.setTagProperty(new JSONPointer(value.toString())); 170 | } else if (key.equals(ResourceTagging.TAG_PERMISSIONS)) { 171 | List tagPermissions = new ArrayList<>(); 172 | ((List) value).forEach(p -> tagPermissions.add(p.toString())); 173 | taggingValue.setTagPermissions(tagPermissions); 174 | } else { 175 | throw new ValidationException("Unexpected tagging metadata attribute", "tagging", "#/tagging/" + key); 176 | } 177 | }); 178 | if (!taggingValue.isTaggable()) { 179 | // reset other metadata values if resource is not taggable 180 | taggingValue.resetTaggable(taggingValue.isTaggable()); 181 | } 182 | taggingValue.validateTaggingMetadata(this.handlers.containsKey("update"), this.schema); 183 | this.tagging = taggingValue; 184 | return null; 185 | }); 186 | 187 | if (this.unprocessedProperties.containsKey("taggable")) { 188 | this.taggable = Boolean.parseBoolean(this.unprocessedProperties.get("taggable").toString()); 189 | if (!hasConfiguredTagging) { 190 | // set tagging metadata based on deprecated taggable value 191 | this.tagging = new ResourceTagging(this.taggable); 192 | } else { 193 | throw new ValidationException("More than one configuration found for taggable value." + 194 | " Please remove the deprecated taggable property.", "tagging", "#/tagging/taggable"); 195 | } 196 | } else { 197 | this.taggable = true; 198 | } 199 | this.unprocessedProperties.remove("taggable"); 200 | } 201 | 202 | public static ResourceTypeSchema load(final JSONObject resourceDefinition) { 203 | Schema schema = VALIDATOR.loadResourceDefinitionSchema(resourceDefinition); 204 | return new ResourceTypeSchema(schema); 205 | } 206 | 207 | public String getDescription() { 208 | return schema.getDescription(); 209 | } 210 | 211 | public List getConditionalCreateOnlyPropertiesAsStrings() throws ValidationException { 212 | return this.conditionalCreateOnlyProperties.stream().map(JSONPointer::toString).collect(Collectors.toList()); 213 | } 214 | 215 | public List getCreateOnlyPropertiesAsStrings() throws ValidationException { 216 | return this.createOnlyProperties.stream().map(JSONPointer::toString).collect(Collectors.toList()); 217 | } 218 | 219 | public List getDeprecatedPropertiesAsStrings() throws ValidationException { 220 | return this.deprecatedProperties.stream().map(JSONPointer::toString).collect(Collectors.toList()); 221 | } 222 | 223 | public List getPrimaryIdentifierAsStrings() throws ValidationException { 224 | return this.primaryIdentifier.stream().map(JSONPointer::toString).collect(Collectors.toList()); 225 | } 226 | 227 | public List> getAdditionalIdentifiersAsStrings() throws ValidationException { 228 | final List> identifiers = new ArrayList<>(); 229 | this.additionalIdentifiers.forEach(i -> identifiers.add(i.stream().map(Object::toString).collect(Collectors.toList()))); 230 | return identifiers; 231 | } 232 | 233 | public List getReadOnlyPropertiesAsStrings() throws ValidationException { 234 | return this.readOnlyProperties.stream().map(JSONPointer::toString).collect(Collectors.toList()); 235 | } 236 | 237 | public List getWriteOnlyPropertiesAsStrings() throws ValidationException { 238 | return this.writeOnlyProperties.stream().map(JSONPointer::toString).collect(Collectors.toList()); 239 | } 240 | 241 | public Set getHandlerPermissions(String action) { 242 | return handlers.containsKey(action) ? handlers.get(action).getPermissions() : null; 243 | } 244 | 245 | public Integer getHandlerTimeoutInMinutes(String action) { 246 | return handlers.containsKey(action) ? handlers.get(action).getTimeoutInMinutes() : null; 247 | } 248 | 249 | public Boolean hasHandler(String action) { 250 | return handlers.containsKey(action); 251 | } 252 | 253 | public Map getUnprocessedProperties() { 254 | return Collections.unmodifiableMap(this.unprocessedProperties); 255 | } 256 | 257 | public void removeWriteOnlyProperties(final JSONObject resourceModel) { 258 | this.getWriteOnlyPropertiesAsStrings().stream().forEach(writeOnlyProperty -> removeProperty( 259 | new PublicJSONPointer(writeOnlyProperty.replaceFirst("^/properties", "")), resourceModel)); 260 | } 261 | 262 | public String getReplacementStrategy() { 263 | return this.replacementStrategy; 264 | } 265 | 266 | public static void removeProperty(final PublicJSONPointer property, final JSONObject resourceModel) { 267 | List refTokens = property.getRefTokens(); 268 | if (refTokens.size() > 0) { 269 | final String key = refTokens.get(refTokens.size() - 1); 270 | try { 271 | // if size is more than one, fetch parent object/array of key to remove so that 272 | // we can remove 273 | if (refTokens.size() > 1) { 274 | // use sublist to specify to point at the parent object 275 | final JSONPointer parentObjectPointer = new JSONPointer(refTokens.subList(0, refTokens.size() - 1)); 276 | final Optional parentObject = Optional.ofNullable( 277 | (JSONObject) parentObjectPointer.queryFrom(resourceModel)); 278 | parentObject.ifPresent(jsonObject -> jsonObject.remove(key)); 279 | } else { 280 | resourceModel.remove(key); 281 | } 282 | } catch (JSONPointerException | NumberFormatException e) { 283 | // do nothing, as this indicates the model does not have a value for the pointer 284 | } 285 | } 286 | } 287 | 288 | public boolean definesProperty(String field) { 289 | // when schema contains combining properties 290 | // (keywords for combining schemas together, with options being "oneOf", "anyOf", and "allOf"), 291 | // schema will be a CombinedSchema with 292 | // - an allOf criterion 293 | // - subschemas 294 | // - an ObjectSchema that contains properties to be checked 295 | // - other CombinedSchemas corresponding to the usages of combining properties. 296 | // These CombinedSchemas should be ignored. Otherwise, JSON schema's definesProperty method 297 | // will search for field as a property in the CombinedSchema, which is not desired. 298 | Schema schemaToCheck = schema instanceof CombinedSchema 299 | ? ((CombinedSchema) schema).getSubschemas().stream() 300 | .filter(subschema -> subschema instanceof ObjectSchema) 301 | .findFirst().get() 302 | : schema; 303 | 304 | return schemaToCheck.definesProperty(field); 305 | } 306 | 307 | public void validate(JSONObject json) { 308 | getSchema().validate(json); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/main/resources/schema/provider.definition.schema.v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/provider.definition.schema.v1.json", 4 | "title": "CloudFormation Resource Provider Definition MetaSchema", 5 | "description": "This schema validates a CloudFormation resource provider definition.", 6 | "definitions": { 7 | "handlerSchema": { 8 | "type": "object", 9 | "properties": { 10 | "properties": { 11 | "$ref": "base.definition.schema.v1.json#/properties/properties" 12 | }, 13 | "required": { 14 | "$ref": "base.definition.schema.v1.json#/properties/required" 15 | }, 16 | "allOf": { 17 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 18 | }, 19 | "anyOf": { 20 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 21 | }, 22 | "oneOf": { 23 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 24 | } 25 | }, 26 | "required": [ 27 | "properties" 28 | ], 29 | "additionalProperties": false 30 | }, 31 | "handlerDefinitionWithSchemaOverride": { 32 | "description": "Defines any execution operations which can be performed on this resource provider", 33 | "type": "object", 34 | "properties": { 35 | "handlerSchema": { 36 | "$ref": "#/definitions/handlerSchema" 37 | }, 38 | "permissions": { 39 | "type": "array", 40 | "items": { 41 | "type": "string" 42 | }, 43 | "additionalItems": false 44 | }, 45 | "timeoutInMinutes": { 46 | "description": "Defines the timeout for the entire operation to be interpreted by the invoker of the handler. The default is 120 (2 hours).", 47 | "type": "integer", 48 | "minimum": 2, 49 | "maximum": 2160, 50 | "default": 120 51 | } 52 | }, 53 | "additionalProperties": false, 54 | "required": [ 55 | "permissions" 56 | ] 57 | }, 58 | "handlerDefinition": { 59 | "description": "Defines any execution operations which can be performed on this resource provider", 60 | "type": "object", 61 | "properties": { 62 | "permissions": { 63 | "type": "array", 64 | "items": { 65 | "type": "string" 66 | }, 67 | "additionalItems": false 68 | }, 69 | "timeoutInMinutes": { 70 | "description": "Defines the timeout for the entire operation to be interpreted by the invoker of the handler. The default is 120 (2 hours).", 71 | "type": "integer", 72 | "minimum": 2, 73 | "maximum": 2160, 74 | "default": 120 75 | } 76 | }, 77 | "additionalProperties": false, 78 | "required": [ 79 | "permissions" 80 | ] 81 | }, 82 | "replacementStrategy": { 83 | "type": "string", 84 | "description": "The valid replacement strategies are [create_then_delete] and [delete_then_create]. All other inputs are invalid.", 85 | "default": "create_then_delete", 86 | "enum": [ 87 | "create_then_delete", 88 | "delete_then_create" 89 | ] 90 | }, 91 | "resourceLink": { 92 | "type": "object", 93 | "properties": { 94 | "$comment": { 95 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$comment" 96 | }, 97 | "templateUri": { 98 | "type": "string", 99 | "pattern": "^(/|https:)" 100 | }, 101 | "mappings": { 102 | "type": "object", 103 | "patternProperties": { 104 | "^[A-Za-z0-9]{1,64}$": { 105 | "type": "string", 106 | "format": "json-pointer" 107 | } 108 | }, 109 | "additionalProperties": false 110 | } 111 | }, 112 | "required": [ 113 | "templateUri", 114 | "mappings" 115 | ], 116 | "additionalProperties": false 117 | } 118 | }, 119 | "type": "object", 120 | "patternProperties": { 121 | "^\\$id$": { 122 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$id" 123 | } 124 | }, 125 | "properties": { 126 | "$schema": { 127 | "$ref": "base.definition.schema.v1.json#/properties/$schema" 128 | }, 129 | "type": { 130 | "$comment": "Resource Type", 131 | "type": "string", 132 | "const": "RESOURCE" 133 | }, 134 | "typeName": { 135 | "$comment": "Resource Type Identifier", 136 | "examples": [ 137 | "Organization::Service::Resource", 138 | "AWS::EC2::Instance", 139 | "Initech::TPS::Report" 140 | ], 141 | "$ref": "base.definition.schema.v1.json#/properties/typeName" 142 | }, 143 | "$comment": { 144 | "$ref": "base.definition.schema.v1.json#/properties/$comment" 145 | }, 146 | "title": { 147 | "$ref": "base.definition.schema.v1.json#/properties/title" 148 | }, 149 | "description": { 150 | "$comment": "A short description of the resource provider. This will be shown in the AWS CloudFormation console.", 151 | "$ref": "base.definition.schema.v1.json#/properties/description" 152 | }, 153 | "sourceUrl": { 154 | "$comment": "The location of the source code for this resource provider, to help interested parties submit issues or improvements.", 155 | "examples": [ 156 | "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-s3" 157 | ], 158 | "$ref": "base.definition.schema.v1.json#/properties/sourceUrl" 159 | }, 160 | "documentationUrl": { 161 | "$comment": "A page with supplemental documentation. The property documentation in schemas should be able to stand alone, but this is an opportunity for e.g. rich examples or more guided documents.", 162 | "examples": [ 163 | "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/CHAP_Using.html" 164 | ], 165 | "$ref": "base.definition.schema.v1.json#/definitions/httpsUrl" 166 | }, 167 | "taggable": { 168 | "description": "(Deprecated, please use new metadata attribute tagging) A boolean flag indicating whether this resource type supports tagging.", 169 | "type": "boolean", 170 | "default": true 171 | }, 172 | "tagging": { 173 | "type": "object", 174 | "properties": { 175 | "taggable": { 176 | "description": "A boolean flag indicating whether this resource type supports tagging.", 177 | "type": "boolean", 178 | "default": true 179 | }, 180 | "tagOnCreate": { 181 | "description": "A boolean flag indicating whether this resource type supports tagging resources upon creation.", 182 | "type": "boolean", 183 | "default": true 184 | }, 185 | "tagUpdatable": { 186 | "description": "A boolean flag indicating whether this resource type supports updatable tagging.", 187 | "type": "boolean", 188 | "default": true 189 | }, 190 | "cloudFormationSystemTags": { 191 | "description": "A boolean flag indicating whether this resource type supports CloudFormation system tags.", 192 | "type": "boolean", 193 | "default": true 194 | }, 195 | "tagProperty": { 196 | "description": "A reference to the Tags property in the schema.", 197 | "$ref": "http://json-schema.org/draft-07/schema#/properties/$ref", 198 | "default": "/properties/Tags" 199 | }, 200 | "permissions": { 201 | "type": "array", 202 | "items": { 203 | "type": "string" 204 | }, 205 | "additionalItems": false 206 | } 207 | }, 208 | "required": [ 209 | "taggable" 210 | ], 211 | "additionalProperties": false 212 | }, 213 | "replacementStrategy": { 214 | "$comment": "The order of replacement for an immutable resource update.", 215 | "$ref": "#/definitions/replacementStrategy" 216 | }, 217 | "additionalProperties": { 218 | "$comment": "All properties of a resource must be expressed in the schema - arbitrary inputs are not allowed", 219 | "$ref": "base.definition.schema.v1.json#/properties/additionalProperties" 220 | }, 221 | "properties": { 222 | "$ref": "base.definition.schema.v1.json#/properties/properties" 223 | }, 224 | "definitions": { 225 | "$ref": "base.definition.schema.v1.json#/properties/definitions" 226 | }, 227 | "handlers": { 228 | "description": "Defines the provisioning operations which can be performed on this resource type", 229 | "type": "object", 230 | "properties": { 231 | "create": { 232 | "$ref": "#/definitions/handlerDefinition" 233 | }, 234 | "read": { 235 | "$ref": "#/definitions/handlerDefinition" 236 | }, 237 | "update": { 238 | "$ref": "#/definitions/handlerDefinition" 239 | }, 240 | "delete": { 241 | "$ref": "#/definitions/handlerDefinition" 242 | }, 243 | "list": { 244 | "$ref": "#/definitions/handlerDefinitionWithSchemaOverride" 245 | } 246 | }, 247 | "additionalProperties": false 248 | }, 249 | "remote": { 250 | "description": "Reserved for CloudFormation use. A namespace to inline remote schemas.", 251 | "$ref": "base.definition.schema.v1.json#/properties/remote" 252 | }, 253 | "readOnlyProperties": { 254 | "description": "A list of JSON pointers to properties that are able to be found in a Read request but unable to be specified by the customer", 255 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 256 | }, 257 | "writeOnlyProperties": { 258 | "description": "A list of JSON pointers to properties (typically sensitive) that are able to be specified by the customer but unable to be returned in a Read request", 259 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 260 | }, 261 | "conditionalCreateOnlyProperties": { 262 | "description": "A list of JSON pointers for properties that can only be updated under certain conditions. For example, you can upgrade the engine version of an RDS DBInstance but you cannot downgrade it. When updating this property for a resource in a CloudFormation stack, the resource will be replaced if it cannot be updated.", 263 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 264 | }, 265 | "nonPublicProperties": { 266 | "description": "A list of JSON pointers for properties that are hidden. These properties will still be used but will not be visible", 267 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 268 | }, 269 | "nonPublicDefinitions": { 270 | "description": "A list of JSON pointers for definitions that are hidden. These definitions will still be used but will not be visible", 271 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 272 | }, 273 | "createOnlyProperties": { 274 | "description": "A list of JSON pointers to properties that are only able to be specified by the customer when creating a resource. Conversely, any property *not* in this list can be applied to an Update request.", 275 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 276 | }, 277 | "deprecatedProperties": { 278 | "description": "A list of JSON pointers to properties that have been deprecated by the underlying service provider. These properties are still accepted in create & update operations, however they may be ignored, or converted to a consistent model on application. Deprecated properties are not guaranteed to be present in read paths.", 279 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 280 | }, 281 | "primaryIdentifier": { 282 | "description": "A required identifier which uniquely identifies an instance of this resource type. An identifier is a non-zero-length list of JSON pointers to properties that form a single key. An identifier can be a single or multiple properties to support composite-key identifiers.", 283 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 284 | }, 285 | "additionalIdentifiers": { 286 | "description": "An optional list of supplementary identifiers, each of which uniquely identifies an instance of this resource type. An identifier is a non-zero-length list of JSON pointers to properties that form a single key. An identifier can be a single or multiple properties to support composite-key identifiers.", 287 | "type": "array", 288 | "minItems": 1, 289 | "items": { 290 | "$ref": "base.definition.schema.v1.json#/definitions/jsonPointerArray" 291 | } 292 | }, 293 | "required": { 294 | "$ref": "base.definition.schema.v1.json#/properties/required" 295 | }, 296 | "allOf": { 297 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 298 | }, 299 | "anyOf": { 300 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 301 | }, 302 | "oneOf": { 303 | "$ref": "base.definition.schema.v1.json#/definitions/schemaArray" 304 | }, 305 | "resourceLink": { 306 | "description": "A template-able link to a resource instance. AWS-internal service links must be relative to the AWS console domain. External service links must be absolute, HTTPS URIs.", 307 | "$ref": "#/definitions/resourceLink" 308 | }, 309 | "propertyTransform": { 310 | "description": "A map which allows resource owners to define a function for a property with possible transformation. This property helps ensure the input to the model is equal to output", 311 | "type": "object", 312 | "patternProperties": { 313 | "^[A-Za-z0-9]{1,64}$": { 314 | "type": "string" 315 | } 316 | } 317 | }, 318 | "typeConfiguration": { 319 | "description": "TypeConfiguration to set the configuration data for registry types. This configuration data is not passed through the resource properties in template. One of the possible use cases is configuring auth keys for 3P resource providers.", 320 | "$ref": "provider.configuration.definition.schema.v1.json" 321 | } 322 | }, 323 | "required": [ 324 | "typeName", 325 | "properties", 326 | "description", 327 | "primaryIdentifier", 328 | "additionalProperties" 329 | ], 330 | "additionalProperties": false 331 | } 332 | -------------------------------------------------------------------------------- /src/main/resources/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 86 | 87 | 88 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 113 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 163 | 164 | 165 | 166 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 190 | 191 | 192 | 193 | 194 | 195 | 197 | 198 | 199 | 200 | 201 | 202 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 213 | 214 | 215 | 216 | 217 | 218 | 220 | 221 | 222 | 223 | 225 | 226 | 227 | 228 | 230 | 231 | 232 | 233 | 234 | 235 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 246 | 248 | 250 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | -------------------------------------------------------------------------------- /src/test/java/software/amazon/cloudformation/resource/BaseValidatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package software.amazon.cloudformation.resource; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 19 | import static org.assertj.core.api.Assertions.catchThrowableOfType; 20 | import static software.amazon.cloudformation.resource.ValidatorTest.loadJSON; 21 | 22 | import java.net.URISyntaxException; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | import org.everit.json.schema.loader.SchemaLoader; 27 | import org.everit.json.schema.loader.internal.DefaultSchemaClient; 28 | import org.json.JSONObject; 29 | import org.json.JSONTokener; 30 | import org.junit.jupiter.api.BeforeEach; 31 | import org.junit.jupiter.api.Test; 32 | import org.junit.jupiter.params.ParameterizedTest; 33 | import org.junit.jupiter.params.provider.CsvSource; 34 | import org.junit.jupiter.params.provider.ValueSource; 35 | 36 | import software.amazon.cloudformation.resource.exceptions.ValidationException; 37 | 38 | public class BaseValidatorTest { 39 | 40 | private static final String RESOURCE_DEFINITION_SCHEMA_PATH = "/schema/provider.definition.schema.v1.json"; 41 | private static final String TEST_SCHEMA_PATH = "/test-schema.json"; 42 | private static final String TEST_MINIMAL_SCHEMA_PATH = "/minimal-schema.json"; 43 | private static final String TEST_RESOURCE_SCHEMA_WITH_OVERRIDE_PATH = "/test-resource-schema-with-list-override.json"; 44 | private static final String TEST_VALUE_SCHEMA_PATH = "/scrubbed-values-schema.json"; 45 | 46 | private BaseValidator baseValidator; 47 | 48 | @BeforeEach 49 | public void setUp() { 50 | baseValidator = new BaseValidator(new JSONObject(), new DefaultSchemaClient()); 51 | } 52 | 53 | @Test 54 | public void testNewURI_happyCase() { 55 | final String uriString = "http://json-schema.org/draft-07/schema"; 56 | assertThat(BaseValidator.newURI(uriString).toString()).isEqualTo(uriString); 57 | } 58 | 59 | @Test 60 | public void testNewURI_IncorrectSyntax_ShouldThrow() { 61 | final String uriString = "json-schema.org/draft-07?schema/q/h?s=^IXIC"; 62 | assertThatExceptionOfType(URISyntaxException.class).isThrownBy(() -> BaseValidator.newURI(uriString)); 63 | } 64 | 65 | @Test 66 | public void validateObject_validObject_shouldPassHandlerSchemaValidation() { 67 | final JSONObject object = new JSONObject().put("Person", new JSONObject().put("Name", "Jon")).put("Human", 68 | new JSONObject().put("LastName", "Snow")); 69 | 70 | baseValidator.validateObjectByListHandlerSchema(object, 71 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_RESOURCE_SCHEMA_WITH_OVERRIDE_PATH)))); 72 | } 73 | 74 | @Test 75 | public void validateObject_validObject_shouldNotPassHandlerSchemaValidation() { 76 | final JSONObject object = new JSONObject().put("Person", new JSONObject().put("Name", "Jon")).put("Human", 77 | new JSONObject().put("LastName", "Snow").put("LastName2", "Stark")); 78 | final org.everit.json.schema.ValidationException e = catchThrowableOfType( 79 | () -> baseValidator.validateObjectByListHandlerSchema(object, 80 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_RESOURCE_SCHEMA_WITH_OVERRIDE_PATH)))), 81 | org.everit.json.schema.ValidationException.class); 82 | assertThat(e.getMessage()).isEqualTo("#/Human: extraneous key [LastName2] is not permitted"); 83 | } 84 | 85 | @Test 86 | public void validateObject_validObject_shouldPassHandlerSchemaValidationEmptySchema() { 87 | final JSONObject object = new JSONObject().put("Person", new JSONObject().put("Name", "Jon")); 88 | baseValidator.validateObjectByListHandlerSchema(object, 89 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_MINIMAL_SCHEMA_PATH)))); 90 | } 91 | 92 | /** 93 | * trivial coverage test: cannot cache a schema if it has an invalid $id 94 | */ 95 | @ParameterizedTest 96 | @ValueSource(strings = { ":invalid/uri", "" }) 97 | public void registerMetaSchema_invalidRelativeRef_shouldThrow(String uri) { 98 | JSONObject badSchema = loadJSON(RESOURCE_DEFINITION_SCHEMA_PATH); 99 | badSchema.put("$id", uri); 100 | assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> { 101 | baseValidator.registerMetaSchema(SchemaLoader.builder(), badSchema); 102 | }); 103 | } 104 | 105 | @Test 106 | public void registerMetaSchema_happyCase() { 107 | final String uriString = "http://json-schema.org/draft-07/schema"; 108 | JSONObject schema = loadJSON(RESOURCE_DEFINITION_SCHEMA_PATH); 109 | schema.put("$id", uriString); 110 | baseValidator.registerMetaSchema(SchemaLoader.builder(), schema); 111 | } 112 | 113 | @Test 114 | public void validateObject_validObject_shouldNotThrow() { 115 | final JSONObject object = new JSONObject().put("propertyA", "abc").put("propertyB", Arrays.asList(1, 2, 3)); 116 | 117 | baseValidator.validateObject(object, 118 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_SCHEMA_PATH)))); 119 | } 120 | 121 | @Test 122 | public void validateObject_invalidObjectMissingRequiredProperties_shouldThrow() { 123 | final String propVal = "abc"; 124 | final JSONObject object = new JSONObject().put("propertyA", propVal); 125 | 126 | final ValidationException e = catchThrowableOfType( 127 | () -> baseValidator.validateObject(object, 128 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_SCHEMA_PATH)))), 129 | ValidationException.class); 130 | 131 | assertThat(e).hasNoCause().hasMessageContaining("propertyB").hasMessageNotContaining(propVal); 132 | assertThat(e.getCausingExceptions()).isEmpty(); 133 | assertThat(e.getSchemaPointer()).isEqualTo("#"); 134 | assertThat(e.getKeyword()).isEqualTo("required"); 135 | } 136 | 137 | @Test 138 | public void validateObject_invalidObjectAdditionalProperties_shouldThrow() { 139 | final String propValue = "notpartofschema"; 140 | final JSONObject object = new JSONObject().put("propertyA", "abc").put("propertyB", Arrays.asList(1, 2, 3)) 141 | .put("propertyX", propValue); 142 | 143 | final ValidationException e = catchThrowableOfType( 144 | () -> baseValidator.validateObject(object, 145 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_SCHEMA_PATH)))), 146 | ValidationException.class); 147 | 148 | assertThat(e).hasNoCause().hasMessageContaining("propertyX"); 149 | assertThat(e).hasMessageNotContaining(propValue); 150 | assertThat(e.getCausingExceptions()).isEmpty(); 151 | assertThat(e.getSchemaPointer()).isEqualTo("#"); 152 | assertThat(e.getKeyword()).isEqualTo("additionalProperties"); 153 | } 154 | 155 | @Test 156 | public void validateObject_invalidType_messageShouldNotContainValue() { 157 | final JSONObject object = new JSONObject().put("BooleanProperty", "true"); 158 | 159 | final ValidationException e = catchThrowableOfType( 160 | () -> baseValidator.validateObject(object, 161 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_VALUE_SCHEMA_PATH)))), 162 | ValidationException.class); 163 | 164 | assertThat(e).hasMessageNotContaining("true"); 165 | assertThat(e.getCausingExceptions()).isEmpty(); 166 | assertThat(e.getKeyword()).isEqualTo("type"); 167 | } 168 | 169 | @ParameterizedTest 170 | @CsvSource({ "WaaaaaaaayTooLong,maxLength", "TooShort,minLength", "NoPatternMatch,pattern" }) 171 | public void validateObject_invalidStringValue_messageShouldNotContainValue(final String value, final String keyword) { 172 | final JSONObject object = new JSONObject().put("StringProperty", value); 173 | 174 | final ValidationException e = catchThrowableOfType( 175 | () -> baseValidator.validateObject(object, loadJSON(TEST_VALUE_SCHEMA_PATH)), 176 | ValidationException.class); 177 | 178 | assertThat(e.getSchemaPointer()).isEqualTo("#/StringProperty"); 179 | assertThat(e.getKeyword()).isEqualTo(keyword); 180 | assertThat(e.getMessage()).doesNotContain(value); 181 | assertThat(e.getCausingExceptions()).isEmpty(); 182 | } 183 | 184 | @ParameterizedTest 185 | @ValueSource(strings = { "enum", "const" }) 186 | public void validateObject_invalidEnumValue_messageShouldNotContainValue(final String keyword) { 187 | final String propName = keyword + "Property"; 188 | final String propVal = "NotPartOfEnum"; 189 | final JSONObject object = new JSONObject().put(propName, propVal); 190 | 191 | final ValidationException e = catchThrowableOfType( 192 | () -> baseValidator.validateObject(object, 193 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_VALUE_SCHEMA_PATH)))), 194 | ValidationException.class); 195 | 196 | final String pointer = "#/" + propName; 197 | assertThat(e.getSchemaPointer()).isEqualTo(pointer); 198 | assertThat(e.getKeyword()).isEqualTo("allOf"); 199 | assertThat(e).hasMessageNotContaining(propVal); 200 | assertThat(e.getCausingExceptions()).hasSize(1); 201 | 202 | final ValidationException enumEx = e.getCausingExceptions().get(0); 203 | assertThat(enumEx.getSchemaPointer()).isEqualTo(pointer); 204 | assertThat(enumEx).hasMessageNotContaining(propVal); 205 | assertThat(enumEx.getKeyword()).isEqualTo(keyword); 206 | assertThat(enumEx.getCausingExceptions()).isEmpty(); 207 | } 208 | 209 | @ParameterizedTest 210 | @CsvSource({ "test-test,uniqueItems", "test,minItems", "test-test2-test3,maxItems", "Y-X,contains" }) 211 | public void validateObject_invalidArrayValue_messageShouldNotContainValue(final String listAsString, final String keyword) { 212 | final List values = Arrays.asList(listAsString.split("-")); 213 | final JSONObject object = new JSONObject().put("ArrayProperty", values); 214 | 215 | final ValidationException e = catchThrowableOfType( 216 | () -> baseValidator.validateObject(object, loadJSON(TEST_VALUE_SCHEMA_PATH)), 217 | ValidationException.class); 218 | 219 | assertThat(e.getKeyword()).isEqualTo(keyword); 220 | assertThat(e.getSchemaPointer()).isEqualTo("#/ArrayProperty"); 221 | assertThat(e.getCausingExceptions()).isEmpty(); 222 | 223 | values.forEach(v -> assertThat(e).hasMessageNotContaining(v)); 224 | } 225 | 226 | @ParameterizedTest 227 | @CsvSource({ "5,IntProperty,minimum", "300,IntProperty,maximum", "23,IntProperty,multipleOf", 228 | "5,NumberProperty,exclusiveMinimum", "300,NumberProperty,exclusiveMaximum" }) 229 | public void validateObject_invalidNumValue_messageShouldNotContainValue(final String numAsString, 230 | final String propName, 231 | final String keyword) { 232 | final JSONObject object = new JSONObject().put(propName, Integer.valueOf(numAsString)); 233 | 234 | final ValidationException e = catchThrowableOfType( 235 | () -> baseValidator.validateObject(object, loadJSON(TEST_VALUE_SCHEMA_PATH)), 236 | ValidationException.class); 237 | 238 | assertThat(e.getKeyword()).isEqualTo(keyword); 239 | assertThat(e.getSchemaPointer()).isEqualTo("#/" + propName); 240 | assertThat(e).hasMessageNotContaining(numAsString); 241 | assertThat(e.getCausingExceptions()).isEmpty(); 242 | } 243 | 244 | @ParameterizedTest 245 | @CsvSource({ "test,minProperties", "test-test1-test2,maxProperties", "test-dep,dependencies" }) 246 | public void validateObject_invalidSubObject_messageShouldNotContainValue(final String keysAsString, final String keyword) { 247 | final String val = "testValue"; 248 | final JSONObject subSchema = new JSONObject(); 249 | 250 | final List keys = Arrays.asList(keysAsString.split("-")); 251 | keys.forEach(k -> subSchema.put(k, val)); 252 | 253 | final JSONObject object = new JSONObject().put("ObjectProperty", subSchema); 254 | 255 | final ValidationException e = catchThrowableOfType( 256 | () -> baseValidator.validateObject(object, 257 | new JSONObject(new JSONTokener(this.getClass().getResourceAsStream(TEST_VALUE_SCHEMA_PATH)))), 258 | ValidationException.class); 259 | 260 | assertThat(e.getKeyword()).isEqualTo(keyword); 261 | assertThat(e.getSchemaPointer()).isEqualTo("#/ObjectProperty"); 262 | assertThat(e).hasMessageNotContaining(val); 263 | assertThat(e.getCausingExceptions()).isEmpty(); 264 | } 265 | 266 | @ParameterizedTest 267 | @CsvSource({ "test,minProperties", "test-test1-test2,maxProperties", "test-dep,dependencies" }) 268 | public void validateObject_invalidPatternProperties_messageShouldNotContainValue(final String keysAsString, 269 | final String keyword) { 270 | final String val = "Value"; 271 | final JSONObject object = new JSONObject().put("MapProperty", new JSONObject().put("def", "val")); 272 | 273 | final ValidationException e = catchThrowableOfType( 274 | () -> baseValidator.validateObject(object, loadJSON(TEST_VALUE_SCHEMA_PATH)), 275 | ValidationException.class); 276 | 277 | assertThat(e.getSchemaPointer()).isEqualTo("#/MapProperty"); 278 | assertThat(e.getKeyword()).isEqualTo("additionalProperties"); 279 | assertThat(e).hasMessageNotContaining(val); 280 | assertThat(e.getCausingExceptions()).isEmpty(); 281 | 282 | } 283 | 284 | @ParameterizedTest 285 | @ValueSource(strings = { "anyOf", "allOf", "oneOf" }) 286 | public void validateObject_invalidCombiner_messageShouldNotContainValue(final String keyword) { 287 | final String propName = keyword + "Property"; 288 | final String propVal = "NotAnInteger"; 289 | final JSONObject object = new JSONObject().put(propName, propVal); 290 | 291 | final ValidationException e = catchThrowableOfType( 292 | () -> baseValidator.validateObject(object, loadJSON(TEST_VALUE_SCHEMA_PATH)), 293 | ValidationException.class); 294 | 295 | final String pointer = "#/" + propName; 296 | assertThat(e.getSchemaPointer()).isEqualTo(pointer); 297 | assertThat(e.getKeyword()).isEqualTo(keyword); 298 | assertThat(e).hasMessageNotContaining(propVal); 299 | assertThat(e.getCausingExceptions()).hasSize(1); 300 | 301 | final ValidationException enumEx = e.getCausingExceptions().get(0); 302 | assertThat(enumEx.getSchemaPointer()).isEqualTo(pointer); 303 | assertThat(enumEx).hasMessageNotContaining(propVal); 304 | assertThat(enumEx.getKeyword()).isEqualTo("type"); 305 | assertThat(enumEx.getCausingExceptions()).isEmpty(); 306 | } 307 | 308 | @Test 309 | public void validateObject_invalidObjectMultiple_messageShouldNotContainValue() { 310 | final String propValue = "notpartofschema"; 311 | final JSONObject object = new JSONObject().put("propertyA", 123).put("propertyB", Arrays.asList(1, 2, 3)) 312 | .put("propertyX", propValue).put("propertyY", propValue); 313 | 314 | final ValidationException e = catchThrowableOfType( 315 | () -> baseValidator.validateObject(object, loadJSON(TEST_SCHEMA_PATH)), 316 | ValidationException.class); 317 | 318 | assertThat(e.getCausingExceptions()).hasSize(3); 319 | assertThat(e).hasMessage("#: 3 schema violations found"); 320 | assertThat(e.getSchemaPointer()).isEqualTo("#"); 321 | assertThat(e.getKeyword()).isNull(); 322 | 323 | e.getCausingExceptions().forEach(ce -> { 324 | assertThat(ce).hasMessageNotContaining(propValue); 325 | assertThat(ce.getCausingExceptions()).isEmpty(); 326 | }); 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS CloudFormation Resource Schema 2 | 3 | [![Build Status](https://travis-ci.com/aws-cloudformation/aws-cloudformation-resource-schema.svg?branch=master)](https://travis-ci.com/aws-cloudformation/aws-cloudformation-resource-schema) 4 | 5 | This document describes the [Resource Provider Definition Schema](https://github.com/aws-cloudformation/aws-cloudformation-resource-schema/blob/master/src/main/resources/schema/provider.definition.schema.v1.json) which is a _meta-schema_ that extends [draft-07](http://json-schema.org/draft-07/json-schema-release-notes.html) of [JSON Schema](http://json-schema.org/) to define a validating document against which resource schemas can be authored. 6 | 7 | ## Examples 8 | 9 | Numerous [examples](https://github.com/aws-cloudformation/aws-cloudformation-resource-schema/tree/master/src/main/resources/examples/resource) exist in this repository to help you understand various shape and semantic definition models you can apply to your own resource definitions. 10 | 11 | ## Defining Resources 12 | 13 | ### Overview 14 | 15 | The _meta-schema_ which controls and validates your resource type definition is called the [Resource Provider Definition Schema](https://github.com/aws-cloudformation/aws-cloudformation-resource-schema/blob/master/src/main/resources/schema/provider.definition.schema.v1.json). It is fully compliant with [draft-07](http://json-schema.org/draft-07/json-schema-release-notes.html) of [JSON Schema](http://json-schema.org/) and many IDEs including [IntelliJ](https://www.jetbrains.com/idea/), [PyCharm](https://www.jetbrains.com/pycharm/) and [Visual Studio Code](https://code.visualstudio.com/) come with built-in or plugin-based support for code-completion and syntax validation while editing documents for JSON Schema compliance. Comprehensive [documentation](http://json-schema.org/understanding-json-schema/reference/) for JSON Schema exists and can answer many questions around correct usage. 16 | 17 | To get started, you will author a _specification_ for your resource type in a JSON document, which must be compliant with this _meta-schema_. To make authoring resource _specifications_ simpler, we have constrained the scope of the full JSON Schema standard to apply opinions around how certain validations can be expressed and encourage consistent modelling for all resource schemas. These opinions are codified in the _meta-schema_ and described in this document. 18 | 19 | 20 | ### Resource Type Name 21 | 22 | All resources **MUST** specify a `typeName` which adheres to the Regular Expression `^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$`. This expression defines a 3-part namespace for your resource, with a suggested shape of `Organization::Service::Resource`. For example `AWS::EC2::Instance` or `Initech::TPS::Report`. This `typeName` is how you will address your resources for use in CloudFormation and other provisioning tools. 23 | 24 | ### Resource Shape 25 | 26 | The _shape_ of your resource defines the properties for that resource and how they should be applied. This includes the type of each property, validation patterns or enums, and additional descriptive metadata such as documentation and example usage. Refer to the `#/definitions/properties` section of the _meta-schema_ for the full set of supported properties you can use to describe your resource _shape_. 27 | 28 | ### Resource Semantics 29 | 30 | Certain properties of a resource are _semantic_ and have special meaning when used in different contexts. For example, a property of a resource may be `readOnly` when read back for state changes - but can be specified in a settable context when used as the target of a `$ref` from a related resource. Because of this semantic difference in how this property metadata should be interpreted, certain aspects of the resource definition are applied to the parent resource definition, rather than at a property level. Those elements are; 31 | 32 | * **`primaryIdentifier`**: Must be either a single property, or a set of properties which can be used to uniquely identify the resource. If multiple properties are specified, these are treated as a **composite key** and combined into a single logical identifier. You would use this modelling to express contained identity (such as a named service within a container). This property can be independently provided as keys to a **READ** or **DELETE** request and **MUST** be supported as the only input to those operations. These properties are usually also marked as `readOnlyProperties` and **MUST** be returned from **READ** and **LIST** operations. 33 | * **`additionalIdentifiers`**: Each property listed in the `additionalIdentifiers` section must be able to be used to uniquely identify the resource. These properties can be independently provided as keys to a **READ** or **DELETE** request and **MUST** be supported as the only input to those operations. These properties are usually also marked as `readOnlyProperties` and **MUST** be returned from **READ** and **LIST** operations. A provider is not required to support `additionalIdentifiers`; doing so allows for other unique keys to be used to **READ** resources. 34 | * **`readOnlyProperties`**: A property in the `readOnlyProperties` list cannot be specified by the customer. 35 | * **`writeOnlyProperties`**: A property in the `writeOnlyProperties` cannot be returned in a **READ** or **LIST** request, and can be used to express things like passwords, secrets or other sensitive data. 36 | * **`createOnlyProperties`**: A property in the `createOnlyProperties` cannot be specified in an **UPDATE** request, and can only be specified in a **CREATE** request. Another way to think about this - these are properties which are 'write-once', such as the [`Engine`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-engine) property for an [`AWS::RDS::DBInstance`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html) and if you wish to change such a property on a live resource, you should replace that resource by creating a new instance of the resource and terminating the old one. This is the behaviour CloudFormation follows for all properties documented as _'Update Requires: Replacement'_. An attempt to supply these properties to an **UPDATE** request will produce a runtime error from the handler. 37 | * **`deprecatedProperties`**: A property in the `deprecatedProperties` is not guaranteed to be present in the response from a **READ** request. These fields will still be accepted as input to **CREATE** and **UPDATE** requests however they may be ignored, or converted to new API forms when outbound service calls are made. 38 | * **`replacementStrategy`**: As mentioned above, changing a `createOnlyProperty` requires replacement of the resource by creating a new one and deleting the old one. The default CloudFormation replacement behavior is to create a new resource first, then delete the old resource, so as to avoid any downtime. However, some resources are singleton resources, meaning that only one can exist at a time. In this case, it is not possible to create a second resource first, so CloudFormation must Delete first and then Create. Specify either `create_then_delete` or `delete_then_create`. Default value is `create_then_delete` 39 | * **`taggable`**: [DEPRECATED] ~~A boolean type property which defaults to true, indicating this resource type supports updatable [`tagging property`](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html). Otherwise, it indicates this resource type does not contain any updatable [`tagging properties`](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html)~~. 40 | * **`tagging`**: An object type property that indicates whether this resource type supports [AWS tags](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html), tagging behavior, and what property is used to set tags: 41 | * `taggable`: A boolean flag indicating whether the resource type supports tagging. 42 | * `tagOnCreate`: A boolean flag indicating whether the resource type supports passing tags in the create API. 43 | * `tagUpdatable`: A boolean flag indicating whether the resource type can modify resouce's tags using update handler. 44 | * `cloudFormationSystemTags`: A boolean flag indicating whether the resource type create handler can apply `aws` prefixed tags, [CloudFormation system tags](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html). 45 | * `tagProperty`: A reference to the Tags property in the schema. 46 | * Examples: 47 | * ```json 48 | "tagging": { 49 | "taggable": false 50 | } 51 | ``` 52 | * ```json 53 | "tagging": { 54 | "taggable": true, 55 | "tagOnCreate": true, 56 | "tagUpdatable": true, 57 | "cloudFormationSystemTags": true, 58 | "tagProperty": "/properties/Tags" 59 | } 60 | ``` 61 | * **`propertyTransform`**: Is a map (Map) with the keys being property paths and values being jsonata transformation functions (https://jsonata.org/). This property is used to avoid falsely drifted resources. If the handler transforms the input to the resource to an expected value a transform function can be defined for this property to avoid drift. 62 | #### Application 63 | 64 | When defining resource semantics like `createOnlyProperties`, `primaryIdentifier` you are expected to use a JSON Pointer to a property definition in the same resource document. Schemas you author can be checked with the CFN CLI `validate` command. 65 | 66 | The following (truncated) example shows some of the semantic definitions for an `AWS::S3::Bucket` resource type; 67 | 68 | ``` 69 | { 70 | "$id": "aws-s3-bucket.json", 71 | "typeName": "AWS::S3::Bucket", 72 | "resourceLink": { 73 | "templateUri": "/s3/home?region=${awsRegion}&bucket=${BucketName}", 74 | "mappings": { 75 | "BucketName": "/BucketName" 76 | } 77 | }, 78 | "definitions": { 79 | "NestedDefinitions" : { 80 | "type" : "object", 81 | "additionalProperties" : false, 82 | "properties" : { 83 | "ReturnData" : { 84 | "type" : "boolean" 85 | }, 86 | "Expression" : { 87 | "type" : "string" 88 | } 89 | }, 90 | }, 91 | "properties": { 92 | "Arn": { 93 | "$ref": "aws.common.types.v1.json#/definitions/Arn" 94 | }, 95 | "BucketName": { 96 | "type": "string" 97 | }, 98 | "Id": { 99 | "type": "integer" 100 | }, 101 | "NestedProperty": { 102 | "$ref": "#/definitions/NestedDefinitions" 103 | } 104 | }, 105 | "createOnlyProperties": [ 106 | "/properties/BucketName" 107 | ], 108 | "readOnlyProperties": [ 109 | "/properties/Arn" 110 | ], 111 | "primaryIdentifier": [ 112 | "/properties/BucketName" 113 | ], 114 | "additionalIdentifiers": [ 115 | "/properties/Arn", 116 | "/properties/WebsiteURL" 117 | ], 118 | "propertyTransform": { 119 | "/properties/Id": "$abs(Id) $OR $power(Id, 2)", 120 | "/properties/NestedProperty/Expression": $join(["Prefix", Expression]) 121 | } 122 | } 123 | ``` 124 | **Note:** $OR is supported between 2 Jsontata functions or experessions. It is not supported as part of a string. 125 | Following use of $OR is not supported in propertyTransform: 126 | ```"/properties/e": '$join([e, "T $OR Y"])',``` 127 | 128 | ### Relationships 129 | 130 | Relationships between resources can be expressed through the use of the `$ref` keyword when defining a property schema. The use of the `$ref` keyword to establish relationships is described in [JSON Schema documentation](https://cswr.github.io/JsonSchema/spec/definitions_references/#reference-specification). 131 | 132 | #### Example 133 | 134 | The following example shows a property relationship between an `AWS::EC2::Subnet.VpcId` and an `AWS::EC2::VPC.Id`. The schema for the 'remote' type (`AWS::EC2::VPC`) is used to validate the content of the 'local' type (`AWS::EC2::Subnet`) and can be inferred as a dependency from the local to the remote type. 135 | 136 | Setting the $id property to a remote location will make validation framework to pull dependencies expressed using relative `$ref` URIs from the remote hosts. In this example, `VpcId` property will be verified against the schema for `AWS::EC2::VPC.Id` hosted at `https://schema.cloudformation.us-east-1.amazonaws.com/aws-ec2-vpc.json` 137 | 138 | ``` 139 | { 140 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/aws-ec2-subnet.json", 141 | "typeName": "AWS::EC2::Subnet", 142 | "definitions": { ... }, 143 | "properties": { 144 | { ... } 145 | "VpcId": { 146 | "$ref": "aws-ec2-vpc.json#/properties/Id" 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | ``` 153 | { 154 | "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/aws-ec2-vpc.json", 155 | "typeName": "AWS::EC2::VPC", 156 | "definitions": { ... }, 157 | "properties": { 158 | "Id": { 159 | "type": "string", 160 | "pattern": "$vpc-[0-9]{8,10}^" 161 | } 162 | } 163 | } 164 | ``` 165 | 166 | ## Divergence From JSON Schema 167 | 168 | ### Changes 169 | 170 | We have taken an opinion on certain aspects of the core JSON Schema and introduced certain constrains and changes from the core schema. In the context of this project, we are not building arbitrary documents, but rather, defining a very specific shape and semantic for cloud resources. 171 | 172 | * **`readOnly`**: the readOnly field as defined in JSON Schema does not align with our determination that this is actually a restriction with semantic meaning. A property may be readOnly when specified for a particular resource (for example it's `Arn`), but when that same property is _referenced_ (using `$ref` tokens) from a dependency, the dependency must be allowed to specify an input for that property, and as such, it is no longer `readOnly` in that context. The AWS CloudFormation Resource Schema uses the concept of `readOnlyProperties` for this mechanic. 173 | * **`writeOnly`**: see above 174 | 175 | ### New Schema-Level Properties 176 | 177 | #### insertionOrder 178 | 179 | Array types can define a boolean `insertionOrder`, which specifies whether the order in which elements are specified should be honored when processing a diff between two sets of properties. If `insertionOrder` is true, then a change in order of the elements will constitute a diff. The default for `insertionOrder` is true. 180 | 181 | Together with the `uniqueItems` property (which is native to JSON Schema), complex array types can be defined, as in the following table: 182 | 183 | | insertionOrder | uniqueItems | result | 184 | | ---------------- | ---------------- | ---------- | 185 | | true | false | list | 186 | | false | false | multiset | 187 | | true | true | ordered set | 188 | | false | true | set | 189 | 190 | #### arrayType 191 | 192 | `arrayType` is used to specify the type of array and is only applicable for properties of type array. When set to `AttributeList`, it indicates that the array is used to represent a list of additional properties, and when set to `Standard` it indicates that the array consists of a list of values. The default for `arrayType` is `Standard`. 193 | For example, 'Standard' would be used for an array of Arn values, where the addition of the values themselves has significance. 194 | An example of using 'AttributeList' would be for a list of optional, and often defaulted, values that can be specified. For example, 'AttributeList' would be used for an array of TargetGroupAttributes for ELB where addition of the default values has no significance. 195 | 196 | ### Constraints 197 | 198 | * **`$id`**: an `$id` property is not valid for a resource property. 199 | * **`$schema`**: a `$schema` property is not valid for a resource property. 200 | * **`if`, `then`, `else`, `not`**: these imperative constructs can lead to confusion both in authoring a resource definition, and for customers authoring a resource description against your schema. Also this construct is not widely supported by validation tools and is disallowed here. 201 | * **`propertyNames`**: use of `propertyNames` implies a set of properties without a defined shape and is disallowed. To constrain property names, use `patternProperties` statements with defined shapes. 202 | * **`additionalProperties`** use of `additionalProperties` is not valid for a resource property. Use `patternProperties` instead to define the shape and allowed values of extraneous keys. 203 | * **`properties` and `patternProperties`** it is not valid to use both properties and patternProperties together in the same shape, as a shape should not contain both defined and undefined values. In order to implement this, the set of undefined values should itself be a subshape. 204 | * **`items` and `additionalItems`** the `items` in an array may only have one schema and may not use a list of schemas, as an ordered tuple of different objects is confusing for both developers and customers. This should be expressed as key:value object pairs. Similarly, `additionalItems` is not allowed. 205 | * **`replacementStrategy`**: a `replacementStrategy` is not valid for a mutable resource that does not need replacement during an update. 206 | 207 | ## handlers 208 | 209 | The `handlers` section of the schema allows you to specify which CRUDL operations (create, read, update, delete, list) are available for your resource, as well as some additional metadata about each handler. 210 | 211 | ### permissions 212 | 213 | For each handler, you should define a list of API `permissions` required to perform the operation. Currently, this is used to generate IAM policy templates and is assumed to be AWS API permissions, but you may list 3rd party APIs here as well. 214 | 215 | ### timeoutInMinutes 216 | 217 | For each handler, you may define a `timeoutInMinutes` property, which defines the *maximum* timeout of the operation. This timeout is used by the invoker of the handler (such as CloudFormation) to stop listening and cancel the operation. Note that the handler may of course decide to timeout and return a failure prior to this max timeout period. Currently, this value is only used for `Create`, `Update`, and `Delete` handlers, while `Read` and `List` handlers are expected to return synchronously within 30 seconds. 218 | 219 | ## License 220 | 221 | This library is licensed under the Apache 2.0 License. 222 | --------------------------------------------------------------------------------