diff --git a/docs/utilities/jmespath_functions.md b/docs/utilities/jmespath_functions.md index 45250ea0fcd..209bf4fffe9 100644 --- a/docs/utilities/jmespath_functions.md +++ b/docs/utilities/jmespath_functions.md @@ -134,7 +134,7 @@ This sample will decode the base64 value within the `data` key, and deserialize === "powertools_base64_jmespath_function.py" - ```python hl_lines="7 10 37 48 52 54 56" + ```python hl_lines="7 10 37 49 53 55 57" --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_function.py" ``` diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index ec795c99bef..c9cd5813086 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -3,6 +3,8 @@ title: Validation description: Utility --- + + This utility provides JSON Schema validation for events and responses, including JMESPath support to unwrap events before validation. ## Key features @@ -13,14 +15,17 @@ This utility provides JSON Schema validation for events and responses, including ## Getting started -???+ tip "Tip: Using JSON Schemas for the first time?" - Check this [step-by-step tour in the official JSON Schema website](https://json-schema.org/learn/getting-started-step-by-step.html){target="_blank"}. +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. You can validate inbound and outbound events using [`validator` decorator](#validator-decorator). You can also use the standalone `validate` function, if you want more control over the validation process such as handling a validation error. -We support any JSONSchema draft supported by [fastjsonschema](https://horejsek.github.io/python-fastjsonschema/){target="_blank"} library. +???+ tip "Tip: Using JSON Schemas for the first time?" + Check this [step-by-step tour in the official JSON Schema website](https://json-schema.org/learn/getting-started-step-by-step.html){target="_blank"}. + + We support any JSONSchema draft supported by [fastjsonschema](https://horejsek.github.io/python-fastjsonschema/){target="_blank"} library. ???+ warning Both `validator` decorator and `validate` standalone function expects your JSON Schema to be a **dictionary**, not a filename. @@ -31,31 +36,22 @@ We support any JSONSchema draft supported by [fastjsonschema](https://horejsek.g It will fail fast with `SchemaValidationError` exception if event or response doesn't conform with given JSON Schema. -=== "validator_decorator.py" +=== "getting_started_validator_decorator_function.py" - ```python hl_lines="3 5" - from aws_lambda_powertools.utilities.validation import validator + ```python hl_lines="8 27 28 42" + --8<-- "examples/validation/src/getting_started_validator_decorator_function.py" + ``` - import schemas +=== "getting_started_validator_decorator_schema.py" - @validator(inbound_schema=schemas.INPUT, outbound_schema=schemas.OUTPUT) - def handler(event, context): - return event - ``` + ```python hl_lines="10 12 17 19 24 26 28 44 46 51 53" + --8<-- "examples/validation/src/getting_started_validator_decorator_schema.py" + ``` -=== "event.json" +=== "getting_started_validator_decorator_payload.json" ```json - { - "message": "hello world", - "username": "lessa" - } - ``` - -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + --8<-- "examples/validation/src/getting_started_validator_decorator_payload.json" ``` ???+ note @@ -67,71 +63,48 @@ It will fail fast with `SchemaValidationError` exception if event or response do You can also gracefully handle schema validation errors by catching `SchemaValidationError` exception. -=== "validator_decorator.py" +=== "getting_started_validator_standalone_function.py" - ```python hl_lines="8" - from aws_lambda_powertools.utilities.validation import validate - from aws_lambda_powertools.utilities.validation.exceptions import SchemaValidationError + ```python hl_lines="5 16 17 26" + --8<-- "examples/validation/src/getting_started_validator_standalone_function.py" + ``` - import schemas +=== "getting_started_validator_standalone_schema.py" - def handler(event, context): - try: - validate(event=event, schema=schemas.INPUT) - except SchemaValidationError as e: - # do something before re-raising - raise - - return event - ``` + ```python hl_lines="7 8 10 12 17 19 24 26 28" + --8<-- "examples/validation/src/getting_started_validator_standalone_schema.py" + ``` -=== "event.json" +=== "getting_started_validator_standalone_payload.json" ```json - { - "data": "hello world", - "username": "lessa" - } - ``` - -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + --8<-- "examples/validation/src/getting_started_validator_standalone_payload.json" ``` ### Unwrapping events prior to validation -You might want to validate only a portion of your event - This is where the `envelope` parameter is for. +You might want to validate only a portion of your event - This is what the `envelope` parameter is for. Envelopes are [JMESPath expressions](https://jmespath.org/tutorial.html) to extract a portion of JSON you want before applying JSON Schema validation. Here is a sample custom EventBridge event, where we only validate what's inside the `detail` key: -=== "unwrapping_events.py" +=== "getting_started_validator_unwrapping_function.py" - We use the `envelope` parameter to extract the payload inside the `detail` key before validating. + ```python hl_lines="2 6 12" + --8<-- "examples/validation/src/getting_started_validator_unwrapping_function.py" + ``` - ```python hl_lines="5" - from aws_lambda_powertools.utilities.validation import validator +=== "getting_started_validator_unwrapping_schema.py" - import schemas + ```python hl_lines="9-14 23 25 28 33 36 41 44 48 51" + --8<-- "examples/validation/src/getting_started_validator_unwrapping_schema.py" + ``` - @validator(inbound_schema=schemas.INPUT, envelope="detail") - def handler(event, context): - return event - ``` - -=== "sample_wrapped_event.json" - - ```python hl_lines="11-14" - --8<-- "docs/shared/validation_basic_eventbridge_event.json" - ``` +=== "getting_started_validator_unwrapping_payload.json" -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + ```json + --8<-- "examples/validation/src/getting_started_validator_unwrapping_payload.json" ``` This is quite powerful because you can use JMESPath Query language to extract records from [arrays](https://jmespath.org/tutorial.html#list-and-slice-projections), combine [pipe](https://jmespath.org/tutorial.html#pipe-expressions) and [function expressions](https://jmespath.org/tutorial.html#functions). @@ -140,30 +113,24 @@ When combined, these features allow you to extract what you need before validati ### Built-in envelopes -This utility comes with built-in envelopes to easily extract the payload from popular event sources. +We provide built-in envelopes to easily extract the payload from popular event sources. -=== "unwrapping_popular_event_sources.py" +=== "unwrapping_popular_event_source_function.py" - ```python hl_lines="5 7" - from aws_lambda_powertools.utilities.validation import envelopes, validator + ```python hl_lines="2 7 12" + --8<-- "examples/validation/src/unwrapping_popular_event_source_function.py" + ``` - import schemas +=== "unwrapping_popular_event_source_schema.py" - @validator(inbound_schema=schemas.INPUT, envelope=envelopes.EVENTBRIDGE) - def handler(event, context): - return event - ``` + ```python hl_lines="7 9 12 17 20" + --8<-- "examples/validation/src/unwrapping_popular_event_source_schema.py" + ``` -=== "sample_wrapped_event.json" +=== "unwrapping_popular_event_source_payload.json" - ```python hl_lines="11-14" - --8<-- "docs/shared/validation_basic_eventbridge_event.json" - ``` - -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + ```json hl_lines="12 13" + --8<-- "examples/validation/src/unwrapping_popular_event_source_payload.json" ``` Here is a handy table with built-in envelopes along with their JMESPath expressions in case you want to build your own. @@ -186,243 +153,35 @@ Here is a handy table with built-in envelopes along with their JMESPath expressi ???+ note JSON Schema DRAFT 7 [has many new built-in formats](https://json-schema.org/understanding-json-schema/reference/string.html#format){target="_blank"} such as date, time, and specifically a regex format which might be a better replacement for a custom format, if you do have control over the schema. -JSON Schemas with custom formats like `int64` will fail validation. If you have these, you can pass them using `formats` parameter: +JSON Schemas with custom formats like `awsaccountid` will fail validation. If you have these, you can pass them using `formats` parameter: ```json title="custom_json_schema_type_format.json" { - "lastModifiedTime": { - "format": "int64", - "type": "integer" + "accountid": { + "format": "awsaccountid", + "type": "string" } } ``` For each format defined in a dictionary key, you must use a regex, or a function that returns a boolean to instruct the validator on how to proceed when encountering that type. -=== "validate_custom_format.py" +=== "custom_format_function.py" - ```python hl_lines="5-8 10" - from aws_lambda_powertools.utilities.validation import validate + ```python hl_lines="5 8 10 11 17 27" + --8<-- "examples/validation/src/custom_format_function.py" + ``` - import schema +=== "custom_format_schema.py" - custom_format = { - "int64": True, # simply ignore it, - "positive": lambda x: False if x < 0 else True - } + ```python hl_lines="7 9 12 13 17 20" + --8<-- "examples/validation/src/custom_format_schema.py" + ``` - validate(event=event, schema=schemas.INPUT, formats=custom_format) - ``` +=== "custom_format_payload.json" -=== "schemas.py" - - ```python hl_lines="68" 91 93" - INPUT = { - "$schema": "http://json-schema.org/draft-04/schema#", - "definitions": { - "AWSAPICallViaCloudTrail": { - "properties": { - "additionalEventData": {"$ref": "#/definitions/AdditionalEventData"}, - "awsRegion": {"type": "string"}, - "errorCode": {"type": "string"}, - "errorMessage": {"type": "string"}, - "eventID": {"type": "string"}, - "eventName": {"type": "string"}, - "eventSource": {"type": "string"}, - "eventTime": {"format": "date-time", "type": "string"}, - "eventType": {"type": "string"}, - "eventVersion": {"type": "string"}, - "recipientAccountId": {"type": "string"}, - "requestID": {"type": "string"}, - "requestParameters": {"$ref": "#/definitions/RequestParameters"}, - "resources": {"items": {"type": "object"}, "type": "array"}, - "responseElements": {"type": ["object", "null"]}, - "sourceIPAddress": {"type": "string"}, - "userAgent": {"type": "string"}, - "userIdentity": {"$ref": "#/definitions/UserIdentity"}, - "vpcEndpointId": {"type": "string"}, - "x-amazon-open-api-schema-readOnly": {"type": "boolean"}, - }, - "required": [ - "eventID", - "awsRegion", - "eventVersion", - "responseElements", - "sourceIPAddress", - "eventSource", - "requestParameters", - "resources", - "userAgent", - "readOnly", - "userIdentity", - "eventType", - "additionalEventData", - "vpcEndpointId", - "requestID", - "eventTime", - "eventName", - "recipientAccountId", - ], - "type": "object", - }, - "AdditionalEventData": { - "properties": { - "objectRetentionInfo": {"$ref": "#/definitions/ObjectRetentionInfo"}, - "x-amz-id-2": {"type": "string"}, - }, - "required": ["x-amz-id-2"], - "type": "object", - }, - "Attributes": { - "properties": { - "creationDate": {"format": "date-time", "type": "string"}, - "mfaAuthenticated": {"type": "string"}, - }, - "required": ["mfaAuthenticated", "creationDate"], - "type": "object", - }, - "LegalHoldInfo": { - "properties": { - "isUnderLegalHold": {"type": "boolean"}, - "lastModifiedTime": {"format": "int64", "type": "integer"}, - }, - "type": "object", - }, - "ObjectRetentionInfo": { - "properties": { - "legalHoldInfo": {"$ref": "#/definitions/LegalHoldInfo"}, - "retentionInfo": {"$ref": "#/definitions/RetentionInfo"}, - }, - "type": "object", - }, - "RequestParameters": { - "properties": { - "bucketName": {"type": "string"}, - "key": {"type": "string"}, - "legal-hold": {"type": "string"}, - "retention": {"type": "string"}, - }, - "required": ["bucketName", "key"], - "type": "object", - }, - "RetentionInfo": { - "properties": { - "lastModifiedTime": {"format": "int64", "type": "integer"}, - "retainUntilMode": {"type": "string"}, - "retainUntilTime": {"format": "int64", "type": "integer"}, - }, - "type": "object", - }, - "SessionContext": { - "properties": {"attributes": {"$ref": "#/definitions/Attributes"}}, - "required": ["attributes"], - "type": "object", - }, - "UserIdentity": { - "properties": { - "accessKeyId": {"type": "string"}, - "accountId": {"type": "string"}, - "arn": {"type": "string"}, - "principalId": {"type": "string"}, - "sessionContext": {"$ref": "#/definitions/SessionContext"}, - "type": {"type": "string"}, - }, - "required": ["accessKeyId", "sessionContext", "accountId", "principalId", "type", "arn"], - "type": "object", - }, - }, - "properties": { - "account": {"type": "string"}, - "detail": {"$ref": "#/definitions/AWSAPICallViaCloudTrail"}, - "detail-type": {"type": "string"}, - "id": {"type": "string"}, - "region": {"type": "string"}, - "resources": {"items": {"type": "string"}, "type": "array"}, - "source": {"type": "string"}, - "time": {"format": "date-time", "type": "string"}, - "version": {"type": "string"}, - }, - "required": ["detail-type", "resources", "id", "source", "time", "detail", "region", "version", "account"], - "title": "AWSAPICallViaCloudTrail", - "type": "object", - "x-amazon-events-detail-type": "AWS API Call via CloudTrail", - "x-amazon-events-source": "aws.s3", - } - ``` - -=== "event.json" - - ```json - { - "account": "123456789012", - "detail": { - "additionalEventData": { - "AuthenticationMethod": "AuthHeader", - "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", - "SignatureVersion": "SigV4", - "bytesTransferredIn": 0, - "bytesTransferredOut": 0, - "x-amz-id-2": "ejUr9Nd/4IO1juF/a6GOcu+PKrVX6dOH6jDjQOeCJvtARUqzxrhHGrhEt04cqYtAZVqcSEXYqo0=", - }, - "awsRegion": "us-west-1", - "eventCategory": "Data", - "eventID": "be4fdb30-9508-4984-b071-7692221899ae", - "eventName": "HeadObject", - "eventSource": "s3.amazonaws.com", - "eventTime": "2020-12-22T10:05:29Z", - "eventType": "AwsApiCall", - "eventVersion": "1.07", - "managementEvent": False, - "readOnly": True, - "recipientAccountId": "123456789012", - "requestID": "A123B1C123D1E123", - "requestParameters": { - "Host": "lambda-artifacts-deafc19498e3f2df.s3.us-west-1.amazonaws.com", - "bucketName": "lambda-artifacts-deafc19498e3f2df", - "key": "path1/path2/path3/file.zip", - }, - "resources": [ - { - "ARN": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df/path1/path2/path3/file.zip", - "type": "AWS::S3::Object", - }, - { - "ARN": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df", - "accountId": "123456789012", - "type": "AWS::S3::Bucket", - }, - ], - "responseElements": None, - "sourceIPAddress": "AWS Internal", - "userAgent": "AWS Internal", - "userIdentity": { - "accessKeyId": "ABCDEFGHIJKLMNOPQR12", - "accountId": "123456789012", - "arn": "arn:aws:sts::123456789012:assumed-role/role-name1/1234567890123", - "invokedBy": "AWS Internal", - "principalId": "ABCDEFGHIJKLMN1OPQRST:1234567890123", - "sessionContext": { - "attributes": {"creationDate": "2020-12-09T09:58:24Z", "mfaAuthenticated": "false"}, - "sessionIssuer": { - "accountId": "123456789012", - "arn": "arn:aws:iam::123456789012:role/role-name1", - "principalId": "ABCDEFGHIJKLMN1OPQRST", - "type": "Role", - "userName": "role-name1", - }, - }, - "type": "AssumedRole", - }, - "vpcEndpointId": "vpce-a123cdef", - }, - "detail-type": "AWS API Call via CloudTrail", - "id": "e0bad426-0a70-4424-b53a-eb902ebf5786", - "region": "us-west-1", - "resources": [], - "source": "aws.s3", - "time": "2020-12-22T10:05:29Z", - "version": "0", - } + ```json hl_lines="12 13" + --8<-- "examples/validation/src/custom_format_payload.json" ``` ### Built-in JMESPath functions diff --git a/examples/validation/src/custom_format_function.py b/examples/validation/src/custom_format_function.py new file mode 100644 index 00000000000..bf589018c5c --- /dev/null +++ b/examples/validation/src/custom_format_function.py @@ -0,0 +1,34 @@ +import json +import re + +import boto3 +import custom_format_schema as schemas + +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + +# awsaccountid must have 12 digits +custom_format = {"awsaccountid": lambda value: re.match(r"^(\d{12})$", value)} + + +def lambda_handler(event, context: LambdaContext) -> dict: + try: + # validate input using custom json format + validate(event=event, schema=schemas.INPUT, formats=custom_format) + + client_organization = boto3.client("organizations", region_name=event.get("region")) + account_data = client_organization.describe_account(AccountId=event.get("accountid")) + + return { + "account": json.dumps(account_data.get("Account"), default=str), + "message": "Success", + "statusCode": 200, + } + except SchemaValidationError as exception: + return return_error_message(str(exception)) + except Exception as exception: + return return_error_message(str(exception)) + + +def return_error_message(message: str) -> dict: + return {"account": None, "message": message, "statusCode": 400} diff --git a/examples/validation/src/custom_format_payload.json b/examples/validation/src/custom_format_payload.json new file mode 100644 index 00000000000..8f0607f94b0 --- /dev/null +++ b/examples/validation/src/custom_format_payload.json @@ -0,0 +1,4 @@ +{ + "accountid": "200984112386", + "region": "us-east-1" +} diff --git a/examples/validation/src/custom_format_schema.py b/examples/validation/src/custom_format_schema.py new file mode 100644 index 00000000000..e06a4b35e2d --- /dev/null +++ b/examples/validation/src/custom_format_schema.py @@ -0,0 +1,26 @@ +INPUT = { + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/object1660245931.json", + "title": "Root", + "type": "object", + "required": ["accountid", "region"], + "properties": { + "accountid": { + "$id": "#root/accountid", + "title": "The accountid", + "type": "string", + "format": "awsaccountid", + "default": "", + "examples": ["123456789012"], + }, + "region": { + "$id": "#root/region", + "title": "The region", + "type": "string", + "default": "", + "examples": ["us-east-1"], + "pattern": "^.*$", + }, + }, +} diff --git a/examples/validation/src/getting_started_validator_decorator_function.py b/examples/validation/src/getting_started_validator_decorator_function.py new file mode 100644 index 00000000000..bc371742860 --- /dev/null +++ b/examples/validation/src/getting_started_validator_decorator_function.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass, field +from uuid import uuid4 + +import getting_started_validator_decorator_schema as schemas + +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import validator + +# we can get list of allowed IPs from AWS Parameter Store using Parameters Utility +# See: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/parameters/ +ALLOWED_IPS = parameters.get_parameter("/lambda-powertools/allowed_ips") + + +class UserPermissionsError(Exception): + ... + + +@dataclass +class User: + ip: str + permissions: list + user_id: str = field(default_factory=lambda: f"{uuid4()}") + name: str = "Project Lambda Powertools" + + +# using a decorator to validate input and output data +@validator(inbound_schema=schemas.INPUT, outbound_schema=schemas.OUTPUT) +def lambda_handler(event, context: LambdaContext) -> dict: + + try: + user_details: dict = {} + + # get permissions by user_id and project + if ( + event.get("user_id") == "0d44b083-8206-4a3a-aa95-5d392a99be4a" + and event.get("project") == "powertools" + and event.get("ip") in ALLOWED_IPS + ): + user_details = User(ip=event.get("ip"), permissions=["read", "write"]).__dict__ + + # the body must be an object because must match OUTPUT schema, otherwise it fails + return {"body": user_details or None, "statusCode": 200 if user_details else 204} + except Exception as e: + raise UserPermissionsError(str(e)) diff --git a/examples/validation/src/getting_started_validator_decorator_payload.json b/examples/validation/src/getting_started_validator_decorator_payload.json new file mode 100644 index 00000000000..0e8bb8b752b --- /dev/null +++ b/examples/validation/src/getting_started_validator_decorator_payload.json @@ -0,0 +1,6 @@ + +{ + "user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", + "project": "powertools", + "ip": "192.168.0.1" +} diff --git a/examples/validation/src/getting_started_validator_decorator_schema.py b/examples/validation/src/getting_started_validator_decorator_schema.py new file mode 100644 index 00000000000..1f74a2cc711 --- /dev/null +++ b/examples/validation/src/getting_started_validator_decorator_schema.py @@ -0,0 +1,60 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", "project": "powertools", "ip": "192.168.0.1"}], + "required": ["user_id", "project", "ip"], + "properties": { + "user_id": { + "$id": "#/properties/user_id", + "type": "string", + "title": "The user_id", + "examples": ["0d44b083-8206-4a3a-aa95-5d392a99be4a"], + "maxLength": 50, + }, + "project": { + "$id": "#/properties/project", + "type": "string", + "title": "The project", + "examples": ["powertools"], + "maxLength": 30, + }, + "ip": { + "$id": "#/properties/ip", + "type": "string", + "title": "The ip", + "format": "ipv4", + "examples": ["192.168.0.1"], + "maxLength": 30, + }, + }, +} + +OUTPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample outgoing schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"statusCode": 200, "body": {}}], + "required": ["statusCode", "body"], + "properties": { + "statusCode": { + "$id": "#/properties/statusCode", + "type": "integer", + "title": "The statusCode", + "examples": [200], + "maxLength": 3, + }, + "body": { + "$id": "#/properties/body", + "type": "object", + "title": "The body", + "examples": [ + '{"ip": "192.168.0.1", "permissions": ["read", "write"], "user_id": "7576b683-295e-4f69-b558-70e789de1b18", "name": "Project Lambda Powertools"}' # noqa E501 + ], + }, + }, +} diff --git a/examples/validation/src/getting_started_validator_standalone_function.py b/examples/validation/src/getting_started_validator_standalone_function.py new file mode 100644 index 00000000000..1680511766b --- /dev/null +++ b/examples/validation/src/getting_started_validator_standalone_function.py @@ -0,0 +1,30 @@ +import getting_started_validator_standalone_schema as schemas + +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + +# we can get list of allowed IPs from AWS Parameter Store using Parameters Utility +# See: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/parameters/ +ALLOWED_IPS = parameters.get_parameter("/lambda-powertools/allowed_ips") + + +def lambda_handler(event, context: LambdaContext) -> dict: + try: + user_authenticated: str = "" + + # using standalone function to validate input data only + validate(event=event, schema=schemas.INPUT) + + if ( + event.get("user_id") == "0d44b083-8206-4a3a-aa95-5d392a99be4a" + and event.get("project") == "powertools" + and event.get("ip") in ALLOWED_IPS + ): + user_authenticated = "Allowed" + + # in this example the body can be of any type because we are not validating the OUTPUT + return {"body": user_authenticated, "statusCode": 200 if user_authenticated else 204} + except SchemaValidationError as exception: + # SchemaValidationError indicates where a data mismatch is + return {"body": str(exception), "statusCode": 400} diff --git a/examples/validation/src/getting_started_validator_standalone_payload.json b/examples/validation/src/getting_started_validator_standalone_payload.json new file mode 100644 index 00000000000..0e8bb8b752b --- /dev/null +++ b/examples/validation/src/getting_started_validator_standalone_payload.json @@ -0,0 +1,6 @@ + +{ + "user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", + "project": "powertools", + "ip": "192.168.0.1" +} diff --git a/examples/validation/src/getting_started_validator_standalone_schema.py b/examples/validation/src/getting_started_validator_standalone_schema.py new file mode 100644 index 00000000000..28711157196 --- /dev/null +++ b/examples/validation/src/getting_started_validator_standalone_schema.py @@ -0,0 +1,33 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", "powertools": "lessa", "ip": "192.168.0.1"}], + "required": ["user_id", "project", "ip"], + "properties": { + "user_id": { + "$id": "#/properties/user_id", + "type": "string", + "title": "The user_id", + "examples": ["0d44b083-8206-4a3a-aa95-5d392a99be4a"], + "maxLength": 50, + }, + "project": { + "$id": "#/properties/project", + "type": "string", + "title": "The project", + "examples": ["powertools"], + "maxLength": 30, + }, + "ip": { + "$id": "#/properties/ip", + "type": "string", + "title": "The ip", + "format": "ipv4", + "examples": ["192.168.0.1"], + "maxLength": 30, + }, + }, +} diff --git a/examples/validation/src/getting_started_validator_unwrapping_function.py b/examples/validation/src/getting_started_validator_unwrapping_function.py new file mode 100644 index 00000000000..96c66a6f2d3 --- /dev/null +++ b/examples/validation/src/getting_started_validator_unwrapping_function.py @@ -0,0 +1,36 @@ +import boto3 +import getting_started_validator_unwrapping_schema as schemas + +from aws_lambda_powertools.utilities.data_classes.event_bridge_event import EventBridgeEvent +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import validator + +s3_client = boto3.resource("s3") + + +# we use the 'envelope' parameter to extract the payload inside the 'detail' key before validating +@validator(inbound_schema=schemas.INPUT, envelope="detail") +def lambda_handler(event: dict, context: LambdaContext) -> dict: + my_event = EventBridgeEvent(event) + data = my_event.detail.get("data", {}) + s3_bucket, s3_key = data.get("s3_bucket"), data.get("s3_key") + + try: + s3_object = s3_client.Object(bucket_name=s3_bucket, key=s3_key) + payload = s3_object.get()["Body"] + content = payload.read().decode("utf-8") + + return {"message": process_data_object(content), "success": True} + except s3_client.meta.client.exceptions.NoSuchBucket as exception: + return return_error_message(str(exception)) + except s3_client.meta.client.exceptions.NoSuchKey as exception: + return return_error_message(str(exception)) + + +def return_error_message(message: str) -> dict: + return {"message": message, "success": False} + + +def process_data_object(content: str) -> str: + # insert logic here + return "Data OK" diff --git a/examples/validation/src/getting_started_validator_unwrapping_payload.json b/examples/validation/src/getting_started_validator_unwrapping_payload.json new file mode 100644 index 00000000000..7757085361b --- /dev/null +++ b/examples/validation/src/getting_started_validator_unwrapping_payload.json @@ -0,0 +1,17 @@ +{ + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "detail-type": "CustomEvent", + "source": "mycompany.service", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-east-1", + "resources": [], + "detail": { + "data": { + "s3_bucket": "aws-lambda-powertools", + "s3_key": "folder/event.txt", + "file_size": 200, + "file_type": "text/plain" + } + } +} \ No newline at end of file diff --git a/examples/validation/src/getting_started_validator_unwrapping_schema.py b/examples/validation/src/getting_started_validator_unwrapping_schema.py new file mode 100644 index 00000000000..2db9b8a4ab9 --- /dev/null +++ b/examples/validation/src/getting_started_validator_unwrapping_schema.py @@ -0,0 +1,59 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/object1660222326.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [ + { + "data": { + "s3_bucket": "aws-lambda-powertools", + "s3_key": "event.txt", + "file_size": 200, + "file_type": "text/plain", + } + } + ], + "required": ["data"], + "properties": { + "data": { + "$id": "#root/data", + "title": "Root", + "type": "object", + "required": ["s3_bucket", "s3_key", "file_size", "file_type"], + "properties": { + "s3_bucket": { + "$id": "#root/data/s3_bucket", + "title": "The S3 Bucker", + "type": "string", + "default": "", + "examples": ["aws-lambda-powertools"], + "pattern": "^.*$", + }, + "s3_key": { + "$id": "#root/data/s3_key", + "title": "The S3 Key", + "type": "string", + "default": "", + "examples": ["folder/event.txt"], + "pattern": "^.*$", + }, + "file_size": { + "$id": "#root/data/file_size", + "title": "The file size", + "type": "integer", + "examples": [200], + "default": 0, + }, + "file_type": { + "$id": "#root/data/file_type", + "title": "The file type", + "type": "string", + "default": "", + "examples": ["text/plain"], + "pattern": "^.*$", + }, + }, + } + }, +} diff --git a/examples/validation/src/unwrapping_popular_event_source_function.py b/examples/validation/src/unwrapping_popular_event_source_function.py new file mode 100644 index 00000000000..8afbb5c727f --- /dev/null +++ b/examples/validation/src/unwrapping_popular_event_source_function.py @@ -0,0 +1,24 @@ +import boto3 +import unwrapping_popular_event_source_schema as schemas +from botocore.exceptions import ClientError + +from aws_lambda_powertools.utilities.data_classes.event_bridge_event import EventBridgeEvent +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import envelopes, validator + + +# extracting detail from EventBridge custom event +# see: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/jmespath_functions/#built-in-envelopes +@validator(inbound_schema=schemas.INPUT, envelope=envelopes.EVENTBRIDGE) +def lambda_handler(event: dict, context: LambdaContext) -> dict: + my_event = EventBridgeEvent(event) + ec2_client = boto3.resource("ec2", region_name=my_event.region) + + try: + instance_id = my_event.detail.get("instance_id") + instance = ec2_client.Instance(instance_id) + instance.stop() + + return {"message": f"Successfully stopped {instance_id}", "success": True} + except ClientError as exception: + return {"message": str(exception), "success": False} diff --git a/examples/validation/src/unwrapping_popular_event_source_payload.json b/examples/validation/src/unwrapping_popular_event_source_payload.json new file mode 100644 index 00000000000..271e0be5b27 --- /dev/null +++ b/examples/validation/src/unwrapping_popular_event_source_payload.json @@ -0,0 +1,16 @@ + +{ + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-east-1", + "resources": [ + "arn:aws:events:us-east-1:123456789012:rule/ExampleRule" + ], + "detail": { + "instance_id": "i-042dd005362091826", + "region": "us-east-2" + } +} diff --git a/examples/validation/src/unwrapping_popular_event_source_schema.py b/examples/validation/src/unwrapping_popular_event_source_schema.py new file mode 100644 index 00000000000..0c5cc746250 --- /dev/null +++ b/examples/validation/src/unwrapping_popular_event_source_schema.py @@ -0,0 +1,26 @@ +INPUT = { + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/object1660233148.json", + "title": "Root", + "type": "object", + "required": ["instance_id", "region"], + "properties": { + "instance_id": { + "$id": "#root/instance_id", + "title": "Instance_id", + "type": "string", + "default": "", + "examples": ["i-042dd005362091826"], + "pattern": "^.*$", + }, + "region": { + "$id": "#root/region", + "title": "Region", + "type": "string", + "default": "", + "examples": ["us-east-1"], + "pattern": "^.*$", + }, + }, +} diff --git a/poetry.lock b/poetry.lock index 08dc7dc6dd7..6322897771d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -633,8 +633,8 @@ packaging = "*" "ruamel.yaml" = "*" [package.extras] -test = ["flake8 (>=3.0)", "coverage"] -dev = ["pypandoc (>=1.4)", "flake8 (>=3.0)", "coverage"] +dev = ["coverage", "flake8 (>=3.0)", "pypandoc (>=1.4)"] +test = ["coverage", "flake8 (>=3.0)"] [[package]] name = "mkdocs"