From 254d60873578725151e6a46c70ad12750254965a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 2 Apr 2020 14:27:01 -0600 Subject: [PATCH 01/65] Update cryptography from 2.8 to 2.9 --- lambda/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 23c3235a..3f63bc55 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -5,7 +5,7 @@ pyjwt==1.7.1 chalice==1.13.0 jwcrypto==0.7 netaddr==0.7.19 -cryptography==2.8 +cryptography==2.9 pyOpenSSL==19.1.0 # maybe not necessary python-jose==3.1.0 From dc04e8e7077034f8fabf258e99feadaf9648554f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Apr 2020 10:54:57 -0800 Subject: [PATCH 02/65] Created lamda funcation to update IAM roles based on region --- cloudformation/thin-egress-app.yaml | 34 +++++++++++++++++ lambda/app.py | 7 +++- lambda/update_lamda.py | 57 ++++++++++++++++++++++++++++ thin-egress-app-code.zip | Bin 0 -> 35208 bytes 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 lambda/update_lamda.py create mode 100644 thin-egress-app-code.zip diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index cf8724c3..a611c089 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -385,6 +385,40 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" + UpdatePolicyLambda: + Type: AWS::Lambda::Function + Properties: + Code: + S3Bucket: !Ref LambdaCodeS3Bucket + S3Key: !Ref LambdaCodeS3Key + Role: !GetAtt DownloadRoleInRegion.Arn + FunctionName: !Sub "${AWS::StackName}-UpdatePolicyLambda" + VpcConfig: + !If + - UsePrivateVPC + - SecurityGroupIds: + !Split [ ',', !Ref VPCSecurityGroupIDs ] + SubnetIds: + !Split [ ',', !Ref VPCSubnetIDs ] + - !Ref "AWS::NoValue" + Subscription: + Type: 'AWS::SNS::Subscription' + Properties: + TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged + Endpoint: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged + Protocol: lamda + RawMessageDelivery: 'true' + Environment: + Variables: + iam_role_name: "bb-tea-test-DownloadRoleInRegion" + policy_name: "bb-tea-test-IamPolicyDownload" + prefix: "rain-uw2-d-s1-" + Timeout: !Ref LambdaTimeout + Handler: update_lamda.py + Runtime: 'python3.7' + Layers: + - !Ref UpdatePolicyLambda + #MemorySize: 128 EgressLambdaDependencyLayer: Type: AWS::Lambda::LayerVersion diff --git a/lambda/app.py b/lambda/app.py index d0e02ef2..b005bf0f 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -3,6 +3,7 @@ from botocore.exceptions import ClientError import os import json + from urllib.parse import urlparse, quote_plus from rain_api_core.general_util import get_log @@ -120,11 +121,15 @@ def get_bucket_region(session, bucketname) ->str: log.debug("bucket {0} is in region {1}".format(bucketname, bucket_region)) except ClientError as e: # We hit here if the download role cannot access a bucket, or if it doesn't exist - log.error("Coud not access download bucket {0}: {1}".format(bucketname, e)) + log.error("Could not access download bucket {0}: {1}".format(bucketname, e)) raise return bucket_region +#Lambda that on-demand Creates and Re-Creates the in-region download role based and pulls in the fresh ip-ranges.json file each time. +def create_in_region_download_role(filename): + + return None; def try_download_from_bucket(bucket, filename, user_profile): diff --git a/lambda/update_lamda.py b/lambda/update_lamda.py new file mode 100644 index 00000000..3255e9e2 --- /dev/null +++ b/lambda/update_lamda.py @@ -0,0 +1,57 @@ +import json +import boto3 +import urllib.request +import os + + +def lambda_handler(event, context): + # Get current region + session = boto3.session.Session() + current_region = session.region_name + + print(f"Current reigon in {current_region}") + cidr_list = get_region_cidrs(current_region) + + # Get the base policy and add IP list as a conidtion + new_policy = get_base_policy(os.getenv("prefix")) + new_policy["Statement"][0]["Condition"] = {"IpAddress": {"aws:SourceIp": cidr_list}} + + client = boto3.client('iam') + response = client.put_role_policy(RoleName=os.getenv('iam_role_name'), PolicyName=os.getenv('policy_name'), + PolicyDocument=json.dumps(new_policy)) + + return response + + +def get_region_cidrs(current_region): + output = urllib.request.urlopen('https://ip-ranges.amazonaws.com/ip-ranges.json').read().decode('utf-8') + ip_ranges = json.loads(output)['prefixes'] + in_region_amazon_ips = [item['ip_prefix'] for item in ip_ranges if + item["service"] == "AMAZON" and item["region"] == current_region] + return (in_region_amazon_ips) + + +def get_base_policy(prefix): + policy = """ + + { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "s3:GetObject", + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": [ + "arn:aws:s3:::{prefix}-*/*", + "arn:aws:s3:::{prefix}-*" + ], + "Effect": "Allow" + } + ] +} + + """ + + return json.loads(policy) diff --git a/thin-egress-app-code.zip b/thin-egress-app-code.zip new file mode 100644 index 0000000000000000000000000000000000000000..cb188644f9ba1ecdf43ace52ed335b5684cab0e9 GIT binary patch literal 35208 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 literal 0 HcmV?d00001 From df230ddbd56e6d3fd23fbe1b9254c73c33360ae9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Apr 2020 12:17:53 -0800 Subject: [PATCH 03/65] Revised last commit and added an IAM role --- cloudformation/thin-egress-app.yaml | 40 ++++++++++++++++++++++++---- lambda/app.py | 5 ---- lambda/update_lamda.py | 4 +-- thin-egress-app-code.zip | Bin 35208 -> 0 bytes 4 files changed, 37 insertions(+), 12 deletions(-) delete mode 100644 thin-egress-app-code.zip diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index a611c089..2b0d589d 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -385,6 +385,38 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" + UpdatePolicyLambdaIamRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${AWS::StackName}-UpdatePolicyLambdaIamRole" + PermissionsBoundary: + !If + - UsePermissionsBoundary + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/${PermissionsBoundaryName}" + - !Ref "AWS::NoValue" + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + Action: sts:AssumeRole + Principal: + Service: + - lambda.amazonaws.com + Effect: Allow + Policies: + - PolicyName: !Sub "${AWS::StackName}-IamPolicy" + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + -'iam:PutRolePolicy' + Resource: "arn:aws:logs:us-west-2:117169578524:log-group:/aws/lambda/update_region_lambda:*" + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "arn:aws:logs:*:*:*" UpdatePolicyLambda: Type: AWS::Lambda::Function Properties: @@ -410,14 +442,12 @@ Resources: RawMessageDelivery: 'true' Environment: Variables: - iam_role_name: "bb-tea-test-DownloadRoleInRegion" - policy_name: "bb-tea-test-IamPolicyDownload" - prefix: "rain-uw2-d-s1-" + iam_role_name: !GetAtt DownloadRoleInRegion.RoleName + policy_name: !GetAtt DownloadRoleInRegion.PolicyName + prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" Timeout: !Ref LambdaTimeout Handler: update_lamda.py Runtime: 'python3.7' - Layers: - - !Ref UpdatePolicyLambda #MemorySize: 128 EgressLambdaDependencyLayer: diff --git a/lambda/app.py b/lambda/app.py index b005bf0f..bbd78bc0 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -126,11 +126,6 @@ def get_bucket_region(session, bucketname) ->str: return bucket_region -#Lambda that on-demand Creates and Re-Creates the in-region download role based and pulls in the fresh ip-ranges.json file each time. -def create_in_region_download_role(filename): - - return None; - def try_download_from_bucket(bucket, filename, user_profile): # Attempt to pull userid from profile diff --git a/lambda/update_lamda.py b/lambda/update_lamda.py index 3255e9e2..6496a27f 100644 --- a/lambda/update_lamda.py +++ b/lambda/update_lamda.py @@ -44,8 +44,8 @@ def get_base_policy(prefix): "s3:GetBucketLocation" ], "Resource": [ - "arn:aws:s3:::{prefix}-*/*", - "arn:aws:s3:::{prefix}-*" + "arn:aws:s3:::{prefix}*/*", + "arn:aws:s3:::{prefix}*/*" ], "Effect": "Allow" } diff --git a/thin-egress-app-code.zip b/thin-egress-app-code.zip deleted file mode 100644 index cb188644f9ba1ecdf43ace52ed335b5684cab0e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35208 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 From fbf1cea5c83b2b2c2eed2fd9cc706d950e944e8d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Apr 2020 12:23:48 -0800 Subject: [PATCH 04/65] Fixed lambda name --- cloudformation/thin-egress-app.yaml | 2 +- lambda/{update_lamda.py => update_lambda.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lambda/{update_lamda.py => update_lambda.py} (100%) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 2b0d589d..b50d7568 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -446,7 +446,7 @@ Resources: policy_name: !GetAtt DownloadRoleInRegion.PolicyName prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" Timeout: !Ref LambdaTimeout - Handler: update_lamda.py + Handler: update_lamda.lambda_handler Runtime: 'python3.7' #MemorySize: 128 diff --git a/lambda/update_lamda.py b/lambda/update_lambda.py similarity index 100% rename from lambda/update_lamda.py rename to lambda/update_lambda.py From b58818f2532f50b8a5edef4b7245122bb99401c3 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Apr 2020 13:56:26 -0800 Subject: [PATCH 05/65] moved subscriptions into its own resource --- cloudformation/thin-egress-app.yaml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index b50d7568..d4a22f54 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -433,18 +433,19 @@ Resources: SubnetIds: !Split [ ',', !Ref VPCSubnetIDs ] - !Ref "AWS::NoValue" - Subscription: - Type: 'AWS::SNS::Subscription' - Properties: - TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged - Endpoint: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged - Protocol: lamda - RawMessageDelivery: 'true' Environment: Variables: iam_role_name: !GetAtt DownloadRoleInRegion.RoleName policy_name: !GetAtt DownloadRoleInRegion.PolicyName prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" + Resources: + Subscription: + Type: 'AWS::SNS::Subscription' + Properties: + TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged + Endpoint: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged + Protocol: lamda + RawMessageDelivery: 'true' Timeout: !Ref LambdaTimeout Handler: update_lamda.lambda_handler Runtime: 'python3.7' @@ -797,6 +798,18 @@ Resources: - !Sub "/${StageName}" RetentionInDays: 30 +UpdateInRegionLogGroup: + Type: AWS::Logs::LogGroup + Condition: ApiGatewayLogToCloudWatchIsSet + Properties: + LogGroupName: + Fn::Join: + - "" + - - "API-Gateway-Execution-Logs_" + - !Ref EgressApiGateway + - !Sub "/${StageName}" + RetentionInDays: 30 + EgressStage: Type: AWS::ApiGateway::Stage DependsOn: From dc1645f4f4c9bb00fbeb61ae8cf8375e57aca123 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 6 Apr 2020 16:34:59 -0600 Subject: [PATCH 06/65] Update chalice from 1.13.0 to 1.13.1 --- lambda/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 23c3235a..6498d4bd 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -2,7 +2,7 @@ jinja2==2.11.1 PyYAML==5.3.1 aws-requests-auth==0.4.2 pyjwt==1.7.1 -chalice==1.13.0 +chalice==1.13.1 jwcrypto==0.7 netaddr==0.7.19 cryptography==2.8 From 4f276b4bc521e8d7f8cf80abdee1d11a12161704 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 7 Apr 2020 08:18:38 -0800 Subject: [PATCH 07/65] Revised from PR --- cloudformation/thin-egress-app.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index d4a22f54..01ce2601 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -274,7 +274,7 @@ Resources: Type: AWS::IAM::Role Condition: CreateDownloadRole Properties: - RoleName: !Sub "${AWS::StackName}-DownloadRoleInRegion" + RoleName: !Sub "${AWS::StackName}-IamPolicyDownload" PermissionsBoundary: !If - UsePermissionsBoundary @@ -410,7 +410,7 @@ Resources: - Effect: Allow Action: -'iam:PutRolePolicy' - Resource: "arn:aws:logs:us-west-2:117169578524:log-group:/aws/lambda/update_region_lambda:*" + Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/update_region_lambda:*" - Effect: Allow Action: - logs:CreateLogGroup @@ -435,10 +435,10 @@ Resources: - !Ref "AWS::NoValue" Environment: Variables: - iam_role_name: !GetAtt DownloadRoleInRegion.RoleName - policy_name: !GetAtt DownloadRoleInRegion.PolicyName + iam_role_name: !Ref DownloadRoleInRegion + policy_name: !Sub "${AWS::StackName}-IamPolicyDownload" prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" - Resources: + Resources: Subscription: Type: 'AWS::SNS::Subscription' Properties: @@ -804,10 +804,7 @@ UpdateInRegionLogGroup: Properties: LogGroupName: Fn::Join: - - "" - - - "API-Gateway-Execution-Logs_" - - !Ref EgressApiGateway - - !Sub "/${StageName}" + - ${AWS::StackName}-UpdateRole RetentionInDays: 30 EgressStage: From 114b3ddcc2a227612e0a64a8557a7fc63794b2fb Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 7 Apr 2020 08:48:50 -0800 Subject: [PATCH 08/65] added downlaod role condition statement --- cloudformation/thin-egress-app.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 01ce2601..2823e4f4 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -387,6 +387,7 @@ Resources: Resource: "arn:aws:logs:*:*:*" UpdatePolicyLambdaIamRole: Type: AWS::IAM::Role + Condition: CreateDownloadRole Properties: RoleName: !Sub "${AWS::StackName}-UpdatePolicyLambdaIamRole" PermissionsBoundary: @@ -419,6 +420,7 @@ Resources: Resource: "arn:aws:logs:*:*:*" UpdatePolicyLambda: Type: AWS::Lambda::Function + Condition: CreateDownloadRole Properties: Code: S3Bucket: !Ref LambdaCodeS3Bucket @@ -441,6 +443,7 @@ Resources: Resources: Subscription: Type: 'AWS::SNS::Subscription' + Condition: CreateDownloadRole Properties: TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged Endpoint: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged @@ -800,7 +803,7 @@ Resources: UpdateInRegionLogGroup: Type: AWS::Logs::LogGroup - Condition: ApiGatewayLogToCloudWatchIsSet + Condition: CreateDownloadRole Properties: LogGroupName: Fn::Join: From 9c33b85280f3d02891574f6da02097342b600bbe Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 8 Apr 2020 05:02:34 -0800 Subject: [PATCH 09/65] revereted change to download In region --- cloudformation/thin-egress-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 2823e4f4..81cff868 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -274,7 +274,7 @@ Resources: Type: AWS::IAM::Role Condition: CreateDownloadRole Properties: - RoleName: !Sub "${AWS::StackName}-IamPolicyDownload" + RoleName: !Sub "${AWS::StackName}-DownloadRoleInRegion" PermissionsBoundary: !If - UsePermissionsBoundary From a929d5b9505d8f8bff1304152caf94d61bf22a4f Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 8 Apr 2020 11:05:23 -0800 Subject: [PATCH 10/65] edited the resource for subscription --- cloudformation/thin-egress-app.yaml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 81cff868..117dfbc6 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -411,7 +411,7 @@ Resources: - Effect: Allow Action: -'iam:PutRolePolicy' - Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/update_region_lambda:*" + Resource: !GetAtt DownloadRoleInRegion.Arn - Effect: Allow Action: - logs:CreateLogGroup @@ -440,20 +440,21 @@ Resources: iam_role_name: !Ref DownloadRoleInRegion policy_name: !Sub "${AWS::StackName}-IamPolicyDownload" prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" - Resources: - Subscription: - Type: 'AWS::SNS::Subscription' - Condition: CreateDownloadRole - Properties: - TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged - Endpoint: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged - Protocol: lamda - RawMessageDelivery: 'true' Timeout: !Ref LambdaTimeout Handler: update_lamda.lambda_handler Runtime: 'python3.7' #MemorySize: 128 + RegionUpdateSnsSubscription: + Type: 'AWS::SNS::Subscription' + Condition: CreateDownloadRole + Properties: + TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged + Endpoint:Lambda ARN: !Ref UpdatePolicyLambda.Arn + Protocol: lambda + RawMessageDelivery: 'true' + + EgressLambdaDependencyLayer: Type: AWS::Lambda::LayerVersion Properties: @@ -807,7 +808,7 @@ UpdateInRegionLogGroup: Properties: LogGroupName: Fn::Join: - - ${AWS::StackName}-UpdateRole + - !Sub: "${AWS::StackName}-UpdateRole" RetentionInDays: 30 EgressStage: From 688b43bef3404eaec5af43b6e7c7d7b2e33a9f57 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Wed, 8 Apr 2020 12:58:54 -0800 Subject: [PATCH 11/65] Freshen the NGAP deploy instruction. --- NGAP-DEPLOY-README.MD | 58 ++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/NGAP-DEPLOY-README.MD b/NGAP-DEPLOY-README.MD index cc9234f9..82447a16 100644 --- a/NGAP-DEPLOY-README.MD +++ b/NGAP-DEPLOY-README.MD @@ -1,18 +1,24 @@ # Deploying Thin Egress to NGAP -The Thin Egress App is available as a [Terraform module](https://www.terraform.io/docs/configuration/modules.html). It can be added to your Terraform configuration using a `module` block. If your deployment does not yet use Terraform, [these instructions](https://nasa.github.io/cumulus/docs/deployment/components) should prove useful. +### ⚠️ If you're deploying from Cumulus, most or all of this provided for you. Please consult current Cumulus Documentation ⚠️ -The lastest version of the module can be found at -[https://s3.amazonaws.com/asf.public.code/index.html](https://s3.amazonaws.com/asf.public.code/index.html). +Cumulus provides a baseline Bucket Map, but its often desirable to overwrite that bucket map with one that fits your particular and evolving deployment, data structure, and DAAC needs. For more information on creating a bucket map file, see the [**Create A Bucket Map**](https://github.com/asfadmin/thin-egress-app/blob/devel/NGAP-DEPLOY-README.MD#create-a-bucket-map-file) Section below. + +### Using Terraform to deploy TEA + +The Thin Egress App is available as a [Terraform module](https://www.terraform.io/docs/configuration/modules.html). It can be added to your Terraform configuration using a `module` block. + +The latest version of TEA and the Terrafrom module can be found at +[https://s3.amazonaws.com/asf.public.code/index.html](https://s3.amazonaws.com/asf.public.code/index.html). The below guide is intended to be a step-by-step set of instructions for deploying TEA into NGAP. As of this writing, the lastest module source is -`https://s3.amazonaws.com/asf.public.code/thin-egress-app/tea-terraform-build.16.zip`. +`https://s3.amazonaws.com/asf.public.code/thin-egress-app/tea-terraform-build.74.zip`, The latest released version is ![Last Release](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fs3.amazonaws.com%2Fasf.public.code%2Fthin-egress-app%2Flastrelease.json) ```hcl # Example Thin Egress App configuration module "thin_egress_app" { - source = "https://s3.amazonaws.com/asf.public.code/thin-egress-app/tea-terraform-build.16.zip" + source = "https://s3.amazonaws.com/asf.public.code/thin-egress-app/tea-terraform-build.74.zip" bucketname_prefix = "" config_bucket = "my-config-bucket" @@ -21,31 +27,32 @@ module "thin_egress_app" { } ``` + + ## Input variables ### Required -* **bucketname_prefix** (string) - all data buckets should have names prefixed with this. Must be compatible with S3 naming conventions (lower case only, etc). An empty string can be used to indicate no prefix. +* **bucketname_prefix** (string) - all data buckets should have names prefixed with this. Must be compatible with S3 naming conventions (lower case only, etc). An empty string can be used to indicate no prefix * **config_bucket** (string) - the bucket where config files can be found +* **jwt_secret_name** (string) - name of AWS secret where keys for JWT encode/decode are stored * **stack_name** (string) - the name of the Thin Egress App CloudFormation stack -* **urs_auth_creds_secret_name** (string) - AWS Secrets Manager name of URS creds. Must consist of two rows, names 'UrsId' and 'UrsAuth'. +* **urs_auth_creds_secret_name** (string) - AWS Secrets Manager name of URS creds. Must consist of two rows, names 'UrsId' and 'UrsAuth' ### Optional * **auth_base_url** (string) - the 'AUTH_BASE_URL' env var in the lambda. Defaults to "https://urs.earthdata.nasa.gov". -* **bucket_map_file** (string) - path and file of bucketmap file's location in the ConfigBucket. Defaults to "bucket_map.yaml". +* **bucket_map_file** (string) - path and file of bucketmap file's location in the config_bucket. Defaults to "bucket_map.yaml". * **domain_name** (string) - custom domain name used by redirect_url * **download_role_arn** (string) - ARN for reading of data buckets * **html_template_dir** (string) - directory in ConfigBucket where the lambda will look for html templates. Lambda will not look into subdirectories. Please put only html templates in this dir. Leave this field blank to use default templates that are included with the lambda code zip file. +* **jwt_algo** (string) - Algorithm with which to encode the JWT cookie. Defautls to "RS256" * **lambda_code_s3_bucket** (string) - S3 bucket of packaged lambda egress code. Defaults to "asf.public.code" * **lambda_code_s3_key** (string) - S3 key of packaged lambda egress code. Defaults to "thin-egress-app/tea-code-.zip", where "BUILD_ID" is determined when the Terraform module was pacakged. * **log_level** (string) - Python loglevel. Defaults to "DEBUG" * **maturity** (string) - maturity of deployment. Defaults to "DEV". * **permissions_boundary_name** (string) - PermissionsBoundary Policy name to be used when creating IAM roles * **private_vpc** (string) - internal VPC to deploy to -* **private_buckets_file** (string) - path and file of private buckets file's location in the ConfigBucket -* **public_buckets_file** (string) - path and file of public buckets file's location in the ConfigBucket -* **session_store** (string) - "DB" for storing sessions in DynamoDB, "S3" for storing sessions in S3. Defaults to "DB". * **session_ttl** (number) - time to live for auth session, in hours. Defaults to 168. * **stage_name** (string) - this value will show up as the base of the url path as so: `https://xxxxxxxx.execute-api.us-east-1.amazonaws.com//and/so/on`. Defaults to "API". * **use_reverse_bucket_map** (bool) - standard bucketmaps are not reverse. Defaults to false. @@ -61,14 +68,14 @@ module "thin_egress_app" { ### Create a bucket map file -Prefix is `sirc-ingest-dev-`, so `public` maps to `sirc-ingest-dev-public`. +In the example below, `bucketname_prefix` is `data-ingest-dev-`, so `public` maps to `data-ingest-dev-public`. If you have not supplied a prefix, you can instead use the full bucket name. Use a prefix can be desirable as it allows bucket maps to potentially be viable across maturies and projects -Data in s3://sirc-ingest-dev-public will be addressable at https://endpoint/SIRC/BROWSE/{object} +Data in s3://data-ingest-dev-public will be addressable at https://endpoint/DATA/BROWSE/{object} ```shell cat > /tmp/bucket_map.yaml </bucket_map.yaml ``` + +### Create URS Credentials And JWT Secrets: + +You will need to provide your URS App credentails (`urs_auth_creds_secret_name`) and the JWT Cookie Keys (`jwt_secret_name`) as AWS Screts Manager items. + +See [AWS Secrets](https://github.com/asfadmin/thin-egress-app/#aws-secrets) for more information. + +### Update URS redirect_uri: + +The TEA Terraform module has the output value `urs_redirect_uri`. This URL should be added to your URS App as a valid Redirect URI. If updating the app to supply CloudFront URL or Custom DNS records using the `domain_name` and `cookie_domain` fields, it will be important to update or replace the output `urs_redirect_uri` value in your URS App. From 8dd9dc22fa316d8681cfc9b2946bef394100f1d1 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Wed, 8 Apr 2020 13:02:08 -0800 Subject: [PATCH 12/65] 1 more output --- NGAP-DEPLOY-README.MD | 1 + 1 file changed, 1 insertion(+) diff --git a/NGAP-DEPLOY-README.MD b/NGAP-DEPLOY-README.MD index 82447a16..033c1dfb 100644 --- a/NGAP-DEPLOY-README.MD +++ b/NGAP-DEPLOY-README.MD @@ -63,6 +63,7 @@ module "thin_egress_app" { * **api_endpoint** (string) - the API Gateway endpoint of the deployment * **urs_redirect_uri** (string) - the URS redirect URI to be configured for the app in Earthdata Login +* **egress_log_group** (string) - API Gateway Access logs LogGroup, if enabled ## Pre-deployment configuration steps From 81710f58f55e2b096a2dd5f04a5868ac60541b69 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 8 Apr 2020 13:20:38 -0800 Subject: [PATCH 13/65] fixed endpoint --- cloudformation/thin-egress-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 117dfbc6..bf944675 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -450,7 +450,7 @@ Resources: Condition: CreateDownloadRole Properties: TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged - Endpoint:Lambda ARN: !Ref UpdatePolicyLambda.Arn + Endpoint: !Ref UpdatePolicyLambda.Arn Protocol: lambda RawMessageDelivery: 'true' From fe3d93b523530688c30df39025c63b5178eaf735 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 9 Apr 2020 10:24:12 -0800 Subject: [PATCH 14/65] Iam role now --- cloudformation/thin-egress-app.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index bf944675..03c5c009 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -425,7 +425,7 @@ Resources: Code: S3Bucket: !Ref LambdaCodeS3Bucket S3Key: !Ref LambdaCodeS3Key - Role: !GetAtt DownloadRoleInRegion.Arn + Role: !GetAtt UpdatePolicyLambdaIamRole.Arn FunctionName: !Sub "${AWS::StackName}-UpdatePolicyLambda" VpcConfig: !If @@ -437,7 +437,7 @@ Resources: - !Ref "AWS::NoValue" Environment: Variables: - iam_role_name: !Ref DownloadRoleInRegion + iam_role_name: !Ref UpdatePolicyLambdaIamRole policy_name: !Sub "${AWS::StackName}-IamPolicyDownload" prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" Timeout: !Ref LambdaTimeout From 96bcaf5916596f2666e8566fc2c213c67b24caca Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 9 Apr 2020 10:29:02 -0800 Subject: [PATCH 15/65] removed join statement --- cloudformation/thin-egress-app.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 03c5c009..ce9aa592 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -806,9 +806,7 @@ UpdateInRegionLogGroup: Type: AWS::Logs::LogGroup Condition: CreateDownloadRole Properties: - LogGroupName: - Fn::Join: - - !Sub: "${AWS::StackName}-UpdateRole" + LogGroupName: !Sub: "${AWS::StackName}-UpdateRole" RetentionInDays: 30 EgressStage: From e6e322524516a1173051af78d1540688aefeafb4 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 10 Apr 2020 05:24:41 -0800 Subject: [PATCH 16/65] Fixed Log Group --- cloudformation/thin-egress-app.yaml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index ce9aa592..764351d4 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -385,6 +385,7 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" + UpdatePolicyLambdaIamRole: Type: AWS::IAM::Role Condition: CreateDownloadRole @@ -413,11 +414,12 @@ Resources: -'iam:PutRolePolicy' Resource: !GetAtt DownloadRoleInRegion.Arn - Effect: Allow - Action: + Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Resource: "arn:aws:logs:*:*:*" + Resource: "arn:aws:logs:*:*:*" + UpdatePolicyLambda: Type: AWS::Lambda::Function Condition: CreateDownloadRole @@ -446,11 +448,11 @@ Resources: #MemorySize: 128 RegionUpdateSnsSubscription: - Type: 'AWS::SNS::Subscription' + Type: 'AWS::SNS::Subscription' Condition: CreateDownloadRole Properties: TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged - Endpoint: !Ref UpdatePolicyLambda.Arn + Endpoint: !GetAtt UpdatePolicyLambda.Arn Protocol: lambda RawMessageDelivery: 'true' @@ -802,11 +804,11 @@ Resources: - !Sub "/${StageName}" RetentionInDays: 30 -UpdateInRegionLogGroup: + UpdateInRegionLogGroup: Type: AWS::Logs::LogGroup Condition: CreateDownloadRole Properties: - LogGroupName: !Sub: "${AWS::StackName}-UpdateRole" + LogGroupName: !Sub "${AWS::StackName}-UpdateRole" RetentionInDays: 30 EgressStage: From 57434da41061532f32dc91b5b8916d38472d42c3 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 10 Apr 2020 08:41:45 -0800 Subject: [PATCH 17/65] validated code in cloud form template --- cloudformation/thin-egress-app.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 764351d4..fcac5a31 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -411,7 +411,7 @@ Resources: Statement: - Effect: Allow Action: - -'iam:PutRolePolicy' + - 'iam:PutRolePolicy' Resource: !GetAtt DownloadRoleInRegion.Arn - Effect: Allow Action: @@ -428,7 +428,7 @@ Resources: S3Bucket: !Ref LambdaCodeS3Bucket S3Key: !Ref LambdaCodeS3Key Role: !GetAtt UpdatePolicyLambdaIamRole.Arn - FunctionName: !Sub "${AWS::StackName}-UpdatePolicyLambda" + FunctionName: !Sub "${AWS::StackName} -UpdatePolicyLambda" VpcConfig: !If - UsePrivateVPC @@ -451,11 +451,9 @@ Resources: Type: 'AWS::SNS::Subscription' Condition: CreateDownloadRole Properties: - TopicArn: arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged + TopicArn: !Sub 'arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged' Endpoint: !GetAtt UpdatePolicyLambda.Arn Protocol: lambda - RawMessageDelivery: 'true' - EgressLambdaDependencyLayer: Type: AWS::Lambda::LayerVersion From 6dd6b71ca6879b1d7a9c61157554258686e5e0f9 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 10 Apr 2020 08:52:35 -0800 Subject: [PATCH 18/65] updated spacing to insure similarity between cloud template --- cloudformation/thin-egress-app.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index fcac5a31..0cb890fe 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -419,7 +419,6 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" - UpdatePolicyLambda: Type: AWS::Lambda::Function Condition: CreateDownloadRole @@ -428,7 +427,7 @@ Resources: S3Bucket: !Ref LambdaCodeS3Bucket S3Key: !Ref LambdaCodeS3Key Role: !GetAtt UpdatePolicyLambdaIamRole.Arn - FunctionName: !Sub "${AWS::StackName} -UpdatePolicyLambda" + FunctionName: !Sub "${AWS::StackName}-UpdatePolicyLambda" VpcConfig: !If - UsePrivateVPC @@ -454,6 +453,8 @@ Resources: TopicArn: !Sub 'arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged' Endpoint: !GetAtt UpdatePolicyLambda.Arn Protocol: lambda + Region: !Sub 'us-east-1' + EgressLambdaDependencyLayer: Type: AWS::Lambda::LayerVersion From eee482d0dae0d7fe377bfc317522fe9e0af34551 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 12:13:19 -0800 Subject: [PATCH 19/65] Update update_lambda.py to allow Custom Resource callback --- lambda/update_lambda.py | 43 +++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/lambda/update_lambda.py b/lambda/update_lambda.py index 6496a27f..c4ca772c 100644 --- a/lambda/update_lambda.py +++ b/lambda/update_lambda.py @@ -3,26 +3,45 @@ import urllib.request import os +import cfnresponse + def lambda_handler(event, context): - # Get current region - session = boto3.session.Session() - current_region = session.region_name - print(f"Current reigon in {current_region}") - cidr_list = get_region_cidrs(current_region) + try: + # Get current region + session = boto3.session.Session() + current_region = session.region_name + + print(f"Current reigon in {current_region}") + cidr_list = get_region_cidrs(current_region) - # Get the base policy and add IP list as a conidtion - new_policy = get_base_policy(os.getenv("prefix")) - new_policy["Statement"][0]["Condition"] = {"IpAddress": {"aws:SourceIp": cidr_list}} + # Get the base policy and add IP list as a conidtion + new_policy = get_base_policy(os.getenv("prefix")) + new_policy["Statement"][0]["Condition"] = {"IpAddress": {"aws:SourceIp": cidr_list}} - client = boto3.client('iam') - response = client.put_role_policy(RoleName=os.getenv('iam_role_name'), PolicyName=os.getenv('policy_name'), - PolicyDocument=json.dumps(new_policy)) + client = boto3.client('iam') + response = client.put_role_policy(RoleName=os.getenv('iam_role_name'), PolicyName=os.getenv('policy_name'), + PolicyDocument=json.dumps(new_policy)) - return response + # Check if response is coming from CloudFormation + if 'ResponseURL' in event: + print ("Sending success message to callback URL {0}".format(event['ResponseURL'])) + cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Data': "Good"}) + return response + except Exception as e: + error_string = "There was a problem updating policy {0} for Role {1} in region {2}: {3}".format( + os.getenv('policy_name'), os.getenv('iam_role_name'), current_region, e ) + print (error_string) + + if 'ResponseURL' in event: + print ("Sending FAILURE message to callback URL {0}".format(event['ResponseURL'])) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Data':error_string}) + + return False + def get_region_cidrs(current_region): output = urllib.request.urlopen('https://ip-ranges.amazonaws.com/ip-ranges.json').read().decode('utf-8') ip_ranges = json.loads(output)['prefixes'] From d3b0896e8868f81120d95016b7431f63c2b7a331 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 12:14:33 -0800 Subject: [PATCH 20/65] Update thin-egress-app.yaml --- cloudformation/thin-egress-app.yaml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 0cb890fe..2d272ac6 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -385,7 +385,7 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" - + UpdatePolicyLambdaIamRole: Type: AWS::IAM::Role Condition: CreateDownloadRole @@ -405,7 +405,7 @@ Resources: - lambda.amazonaws.com Effect: Allow Policies: - - PolicyName: !Sub "${AWS::StackName}-IamPolicy" + - PolicyName: !Sub "${AWS::StackName}-UpdateLambdaIamPolicy" PolicyDocument: Version: 2012-10-17 Statement: @@ -419,6 +419,7 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" + UpdatePolicyLambda: Type: AWS::Lambda::Function Condition: CreateDownloadRole @@ -438,11 +439,11 @@ Resources: - !Ref "AWS::NoValue" Environment: Variables: - iam_role_name: !Ref UpdatePolicyLambdaIamRole + iam_role_name: !Ref DownloadRoleInRegion policy_name: !Sub "${AWS::StackName}-IamPolicyDownload" prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" Timeout: !Ref LambdaTimeout - Handler: update_lamda.lambda_handler + Handler: update_lambda.lambda_handler Runtime: 'python3.7' #MemorySize: 128 @@ -807,9 +808,9 @@ Resources: Type: AWS::Logs::LogGroup Condition: CreateDownloadRole Properties: - LogGroupName: !Sub "${AWS::StackName}-UpdateRole" + LogGroupName: !Sub "/aws/lambda/${AWS::StackName}-UpdatePolicyLambda" RetentionInDays: 30 - + EgressStage: Type: AWS::ApiGateway::Stage DependsOn: @@ -849,3 +850,11 @@ Resources: Action: lambda:InvokeFunction Principal: apigateway.amazonaws.com SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${EgressApiGateway}/*" + + TriggerInRegionCIDRUpdate: + Type: AWS::CloudFormation::CustomResource + Condition: CreateDownloadRole + Version: "1.0" + Properties: + ServiceToken: !GetAtt UpdatePolicyLambda.Arn + From d75b7da5ecf17aa2c23b20db2a3fa5117f008bba Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 12:17:32 -0800 Subject: [PATCH 21/65] Update requirements.txt --- lambda/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 23c3235a..859b0173 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -8,4 +8,5 @@ netaddr==0.7.19 cryptography==2.8 pyOpenSSL==19.1.0 # maybe not necessary python-jose==3.1.0 +cfnresponse==1.0.2 From 12ccc1d4e44e12df4549080b17484ae79afda9fb Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 12:21:37 -0800 Subject: [PATCH 22/65] Need the layer to pull in cfnresponse --- cloudformation/thin-egress-app.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 2d272ac6..6302d64c 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -422,6 +422,8 @@ Resources: UpdatePolicyLambda: Type: AWS::Lambda::Function + DependsOn: + - EgressLambdaDependencyLayer Condition: CreateDownloadRole Properties: Code: @@ -445,6 +447,8 @@ Resources: Timeout: !Ref LambdaTimeout Handler: update_lambda.lambda_handler Runtime: 'python3.7' + Layers: + - !Ref EgressLambdaDependencyLayer #MemorySize: 128 RegionUpdateSnsSubscription: From 495e7c7de4485c38f4c9606507466001f55ef4e1 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 12:54:02 -0800 Subject: [PATCH 23/65] unwanted whitespace --- cloudformation/thin-egress-app.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 6302d64c..ea7d520f 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -385,7 +385,7 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" - + UpdatePolicyLambdaIamRole: Type: AWS::IAM::Role Condition: CreateDownloadRole @@ -419,7 +419,7 @@ Resources: - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" - + UpdatePolicyLambda: Type: AWS::Lambda::Function DependsOn: @@ -814,7 +814,7 @@ Resources: Properties: LogGroupName: !Sub "/aws/lambda/${AWS::StackName}-UpdatePolicyLambda" RetentionInDays: 30 - + EgressStage: Type: AWS::ApiGateway::Stage DependsOn: @@ -854,11 +854,11 @@ Resources: Action: lambda:InvokeFunction Principal: apigateway.amazonaws.com SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${EgressApiGateway}/*" - + TriggerInRegionCIDRUpdate: Type: AWS::CloudFormation::CustomResource Condition: CreateDownloadRole Version: "1.0" Properties: ServiceToken: !GetAtt UpdatePolicyLambda.Arn - + From 0fd7b87ab67849aebf60550c2ebb340731c279d9 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 13:00:33 -0800 Subject: [PATCH 24/65] Update update_lambda.py --- lambda/update_lambda.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lambda/update_lambda.py b/lambda/update_lambda.py index c4ca772c..9f822269 100644 --- a/lambda/update_lambda.py +++ b/lambda/update_lambda.py @@ -43,7 +43,8 @@ def lambda_handler(event, context): return False def get_region_cidrs(current_region): - output = urllib.request.urlopen('https://ip-ranges.amazonaws.com/ip-ranges.json').read().decode('utf-8') + # Bandit complains with B310 on the line below. We know the URL, this is safe! + output = urllib.request.urlopen('https://ip-ranges.amazonaws.com/ip-ranges.json').read().decode('utf-8') #nosec ip_ranges = json.loads(output)['prefixes'] in_region_amazon_ips = [item['ip_prefix'] for item in ip_ranges if item["service"] == "AMAZON" and item["region"] == current_region] From 3f839dfc43efa4879c514731f7f6b86720455ed9 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 13:23:34 -0800 Subject: [PATCH 25/65] Update Jenkinsfile --- build/Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/build/Jenkinsfile b/build/Jenkinsfile index 5c560945..0d3ad511 100644 --- a/build/Jenkinsfile +++ b/build/Jenkinsfile @@ -79,6 +79,7 @@ pipeline { sh """ cd ${WORKSPACE}/lambda && \ sed -i -e "s//${BUILDTAG}/" ./app.py && \ zip -g ../${CODE_ARCHIVE_FILENAME} ./app.py && \ + zip -g ../${CODE_ARCHIVE_FILENAME} ./update_lambda.py && \ zip -g -r ../${CODE_ARCHIVE_FILENAME} ./templates && \ cd .. && \ cd rain-api-core && \ From 5e1d5e2e77e2328a8a7cbfa005b7ffdf71095628 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 13:24:58 -0800 Subject: [PATCH 26/65] Update README.MD --- README.MD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.MD b/README.MD index 25ff9f58..ff460fe2 100644 --- a/README.MD +++ b/README.MD @@ -92,6 +92,9 @@ zip -r9 ./${CODE_ARCHIVE_FILENAME} ./lambda # Add the egress python code zip -g ./${CODE_ARCHIVE_FILENAME} ./app.py +# Add the update lambda +zip -g ./${CODE_ARCHIVE_FILENAME} ./update_lambda.py + # Add the html templates zip -g -r2 ./${CODE_ARCHIVE_FILENAME} ./templates From af1b08182278059a744184517e969f11e2729e64 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 14:58:50 -0800 Subject: [PATCH 27/65] Remove UpdateLambda Log Group This fails on established stacks --- cloudformation/thin-egress-app.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index ea7d520f..e457450f 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -808,13 +808,6 @@ Resources: - !Sub "/${StageName}" RetentionInDays: 30 - UpdateInRegionLogGroup: - Type: AWS::Logs::LogGroup - Condition: CreateDownloadRole - Properties: - LogGroupName: !Sub "/aws/lambda/${AWS::StackName}-UpdatePolicyLambda" - RetentionInDays: 30 - EgressStage: Type: AWS::ApiGateway::Stage DependsOn: From a59221d96fc5890ae134e11c1c4d3d3162af24f3 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 15:52:55 -0800 Subject: [PATCH 28/65] Fix Preix --- lambda/update_lambda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambda/update_lambda.py b/lambda/update_lambda.py index 9f822269..4b593529 100644 --- a/lambda/update_lambda.py +++ b/lambda/update_lambda.py @@ -64,8 +64,8 @@ def get_base_policy(prefix): "s3:GetBucketLocation" ], "Resource": [ - "arn:aws:s3:::{prefix}*/*", - "arn:aws:s3:::{prefix}*/*" + """ + f'"arn:aws:s3:::{prefix}' + """*/*", + """ + f'"arn:aws:s3:::{prefix}' + """*" ], "Effect": "Allow" } From 2b1e02cd89aa42930910921d3edf13a244c0c803 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Fri, 10 Apr 2020 15:55:03 -0800 Subject: [PATCH 29/65] Change the prefix env --- cloudformation/thin-egress-app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index e457450f..26d0e856 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -443,7 +443,7 @@ Resources: Variables: iam_role_name: !Ref DownloadRoleInRegion policy_name: !Sub "${AWS::StackName}-IamPolicyDownload" - prefix: !Sub "arn:aws:s3:::${BucketnamePrefix}*/*" + prefix: !Sub "${BucketnamePrefix}" Timeout: !Ref LambdaTimeout Handler: update_lambda.lambda_handler Runtime: 'python3.7' From 176306b35b441946994a0a2007e921084065089f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 13 Apr 2020 11:00:28 -0600 Subject: [PATCH 30/65] Update jinja2 from 2.11.1 to 2.11.2 --- lambda/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 0191c121..3faf302f 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -1,4 +1,4 @@ -jinja2==2.11.1 +jinja2==2.11.2 PyYAML==5.3.1 aws-requests-auth==0.4.2 pyjwt==1.7.1 From 06f52d69507919e550b8a791685c667f19c8a0d9 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Wed, 15 Apr 2020 14:58:36 -0800 Subject: [PATCH 31/65] NGAP integration --- lambda/update_lambda.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lambda/update_lambda.py b/lambda/update_lambda.py index 4b593529..b6817d67 100644 --- a/lambda/update_lambda.py +++ b/lambda/update_lambda.py @@ -21,6 +21,8 @@ def lambda_handler(event, context): new_policy["Statement"][0]["Condition"] = {"IpAddress": {"aws:SourceIp": cidr_list}} client = boto3.client('iam') + # Delete and replace Policy + response = client.delete_role_policy(RoleName=os.getenv('iam_role_name'), PolicyName=os.getenv('policy_name')) response = client.put_role_policy(RoleName=os.getenv('iam_role_name'), PolicyName=os.getenv('policy_name'), PolicyDocument=json.dumps(new_policy)) From 209c7c1ec8cbd498ce3cbf812e356e4b626672e5 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Wed, 15 Apr 2020 15:00:08 -0800 Subject: [PATCH 32/65] Add iam:DeleteRolePolicy to update role --- cloudformation/thin-egress-app.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 26d0e856..97f8a72c 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -412,6 +412,7 @@ Resources: - Effect: Allow Action: - 'iam:PutRolePolicy' + - 'iam:DeleteRolePolicy' Resource: !GetAtt DownloadRoleInRegion.Arn - Effect: Allow Action: From 7cb71b7882df965c021cfc017986458cd5e95186 Mon Sep 17 00:00:00 2001 From: bbuechler Date: Wed, 15 Apr 2020 15:22:27 -0800 Subject: [PATCH 33/65] One more --- cloudformation/thin-egress-app.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 97f8a72c..38e9f4e8 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -414,6 +414,12 @@ Resources: - 'iam:PutRolePolicy' - 'iam:DeleteRolePolicy' Resource: !GetAtt DownloadRoleInRegion.Arn + - Effect: Allow + Action: + - ec2:CreateNetworkInterface + - ec2:DescribeNetworkInterfaces + - ec2:DeleteNetworkInterface + Resource: '*' - Effect: Allow Action: - logs:CreateLogGroup From 03c061b7ab16ab6b641ae5baa41c0ae335768a7b Mon Sep 17 00:00:00 2001 From: bbuechler Date: Wed, 15 Apr 2020 17:12:56 -0800 Subject: [PATCH 34/65] Add iam:ListRolePolicies to update lambda role --- cloudformation/thin-egress-app.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 38e9f4e8..94820dea 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -413,6 +413,7 @@ Resources: Action: - 'iam:PutRolePolicy' - 'iam:DeleteRolePolicy' + - 'iam:ListRolePolicies' Resource: !GetAtt DownloadRoleInRegion.Arn - Effect: Allow Action: From 0d7c336ecdb397b94686b0c23712e6ccac9f4f8e Mon Sep 17 00:00:00 2001 From: bbuechler Date: Wed, 15 Apr 2020 17:16:43 -0800 Subject: [PATCH 35/65] Try to delete only existing policies from role --- lambda/update_lambda.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lambda/update_lambda.py b/lambda/update_lambda.py index b6817d67..a27be425 100644 --- a/lambda/update_lambda.py +++ b/lambda/update_lambda.py @@ -12,6 +12,7 @@ def lambda_handler(event, context): # Get current region session = boto3.session.Session() current_region = session.region_name + client = boto3.client('iam') print(f"Current reigon in {current_region}") cidr_list = get_region_cidrs(current_region) @@ -20,10 +21,16 @@ def lambda_handler(event, context): new_policy = get_base_policy(os.getenv("prefix")) new_policy["Statement"][0]["Condition"] = {"IpAddress": {"aws:SourceIp": cidr_list}} - client = boto3.client('iam') - # Delete and replace Policy - response = client.delete_role_policy(RoleName=os.getenv('iam_role_name'), PolicyName=os.getenv('policy_name')) - response = client.put_role_policy(RoleName=os.getenv('iam_role_name'), PolicyName=os.getenv('policy_name'), + # Clear out any pre-existing roles: + RoleName=os.getenv('iam_role_name') + response = client.list_role_policies(RoleName=RoleName) + if 'PolicyNames' in response: + for PolicyName in response['PolicyNames']: + print(f"Removing old Policy {PolicyName} from Role {RoleName}") + response = client.delete_role_policy(RoleName=RoleName, PolicyName=PolicyName) + + # Put the new policy + response = client.put_role_policy(RoleName=RoleName, PolicyName=os.getenv('policy_name'), PolicyDocument=json.dumps(new_policy)) # Check if response is coming from CloudFormation From f255a659d95012f7713314c8540291d636348cde Mon Sep 17 00:00:00 2001 From: bbuechler Date: Thu, 16 Apr 2020 14:22:30 -0800 Subject: [PATCH 36/65] Use HEAD to validate an object exists. --- lambda/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index bbd78bc0..11c3ea0d 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -167,10 +167,10 @@ def try_download_from_bucket(bucket, filename, user_profile): # Make sure this file exists, don't ACTUALLY download range_header = get_range_header_val() if not range_header: - client.get_object(Bucket=bucket, Key=filename) + client.head_object(Bucket=bucket, Key=filename) redirheaders = {} else: - client.get_object(Bucket=bucket, Key=filename, Range=range_header) + client.head_object(Bucket=bucket, Key=filename, Range=range_header) redirheaders = {'Range': range_header} # Generate URL From 05607c1e110a52d902dadb62a57ce88232758b1d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 22 Apr 2020 10:07:00 -0600 Subject: [PATCH 37/65] Update chalice from 1.13.1 to 1.14.0 --- lambda/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 0191c121..4570a436 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -2,7 +2,7 @@ jinja2==2.11.1 PyYAML==5.3.1 aws-requests-auth==0.4.2 pyjwt==1.7.1 -chalice==1.13.1 +chalice==1.14.0 jwcrypto==0.7 netaddr==0.7.19 cryptography==2.9 From b84275b7ec96f4f311705dbf77481e44509740bc Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 22 Apr 2020 17:57:45 -0600 Subject: [PATCH 38/65] Update cryptography from 2.9 to 2.9.2 --- lambda/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 0191c121..f9e7ab87 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -5,7 +5,7 @@ pyjwt==1.7.1 chalice==1.13.1 jwcrypto==0.7 netaddr==0.7.19 -cryptography==2.9 +cryptography==2.9.2 pyOpenSSL==19.1.0 # maybe not necessary python-jose==3.1.0 cfnresponse==1.0.2 From 72f8cb181d69f625c0711032a6c79a3a40f6e27b Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Thu, 23 Apr 2020 04:48:19 -0400 Subject: [PATCH 39/65] Send Cache-Control header indicating presigned URL expiration (#145) --- lambda/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lambda/app.py b/lambda/app.py index bbd78bc0..f23b7e06 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -173,8 +173,11 @@ def try_download_from_bucket(bucket, filename, user_profile): client.get_object(Bucket=bucket, Key=filename, Range=range_header) redirheaders = {'Range': range_header} + expires_in = 24 * 3600 + redirheaders['Cache-Control'] = 'private, max-age={0}'.format(expires_in - 60) + # Generate URL - presigned_url = get_presigned_url(creds, bucket, filename, bucket_region, 24 * 3600, user_id) + presigned_url = get_presigned_url(creds, bucket, filename, bucket_region, expires_in, user_id) s3_host = urlparse(presigned_url).netloc log.debug("Presigned URL host was {0}".format(s3_host)) From 868a8ca3de9448c171fa4f704d82f044aff43390 Mon Sep 17 00:00:00 2001 From: Patrick Quinn Date: Thu, 23 Apr 2020 04:51:38 -0400 Subject: [PATCH 40/65] Fix typo in make_redirect --- lambda/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index bbd78bc0..e0a38754 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -80,7 +80,7 @@ def do_auth_and_return(ctxt): return Response(body='', status_code=302, headers={'Location': urs_url}) -def make_redriect(to_url, headers=None, status_code=301): +def make_redirect(to_url, headers=None, status_code=301): if headers is None: headers = {} headers['Location'] = to_url @@ -179,7 +179,7 @@ def try_download_from_bucket(bucket, filename, user_profile): log.debug("Presigned URL host was {0}".format(s3_host)) log.info("Using REDIRECT because no PROXY in egresslambda") - return make_redriect(presigned_url, redirheaders, 303) + return make_redirect(presigned_url, redirheaders, 303) except ClientError as e: log.warning("Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) @@ -316,7 +316,7 @@ def try_download_head(bucket, filename): # Return a redirect to a HEAD log.debug("Presigned HEAD URL host was {0}".format(s3_host)) - return make_redriect(presigned_url, {}, 303) + return make_redirect(presigned_url, {}, 303) # Attempt to validate HEAD request From 162d64201bf9d42ca6dbb039c15b02caf7176abd Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Thu, 30 Apr 2020 16:24:05 -0800 Subject: [PATCH 41/65] #165 Updated log messages to make cumulus metrics easier --- lambda/app.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index 0a3558d1..ac045084 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -84,8 +84,8 @@ def make_redirect(to_url, headers=None, status_code=301): if headers is None: headers = {} headers['Location'] = to_url - log.debug('to_url: {}'.format(to_url)) - log.debug('headers: {}'.format(headers)) + log.info(f'TEA success {status_code}. Redirect created. to_url: {to_url}') # cumulus uses this log message + # for metrics purposes. return Response(body='', headers=headers, status_code=status_code) @@ -145,7 +145,8 @@ def try_download_from_bucket(bucket, filename, user_profile): try: bucket_region = get_bucket_region(session, bucket) except ClientError as e: - log.error(f'ClientError while {user_id} tried downloading {bucket}/{filename}: {e}') + # cumulus uses this log message for metrics purposes. + log.error(f'TEA failure 500. ClientError while {user_id} tried downloading {bucket}/{filename}: {e}') template_vars = {'contentstring': 'There was a problem accessing download data.', 'title': 'Data Not Available'} headers = {} return make_html_response(template_vars, headers, 500, 'error.html') @@ -181,16 +182,17 @@ def try_download_from_bucket(bucket, filename, user_profile): s3_host = urlparse(presigned_url).netloc log.debug("Presigned URL host was {0}".format(s3_host)) - log.info("Using REDIRECT because no PROXY in egresslambda") return make_redirect(presigned_url, redirheaders, 303) except ClientError as e: - log.warning("Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) - # Watch for bad range request: if e.response['ResponseMetadata']['HTTPStatusCode'] == 416: + # cumulus uses this log message for metrics purposes. + log.error("TEA failure 416. Invalid Range, Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) return Response(body='Invalid Range', status_code=416, headers={}) + # cumulus uses this log message for metrics purposes. + log.warning("TEA failure 404. Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) template_vars = {'contentstring': 'Could not find requested data.', 'title': 'Data Not Available'} headers = {} return make_html_response(template_vars, headers, 404, 'error.html') @@ -293,7 +295,8 @@ def try_download_head(bucket, filename): log.info("Downloading range {0}".format(range_header)) download = client.get_object(Bucket=bucket, Key=filename, Range=range_header) except ClientError as e: - log.warning("Could get head for s3://{0}/{1}: {2}".format(bucket, filename, e)) + # cumulus uses this log message for metrics purposes. + log.warning("TEA failure 404. Could not get head for s3://{0}/{1}: {2}".format(bucket, filename, e)) template_vars = {'contentstring': 'File not found', 'title': 'File not found'} headers = {} From b947a53386b3d9541d96256c13c21dc1cc861ea1 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Fri, 1 May 2020 10:07:46 -0800 Subject: [PATCH 42/65] #165 cumulus log messages as JSON --- lambda/app.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index ac045084..8f8286ea 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -34,6 +34,18 @@ 'content-length': 'Content-Length'} +def cumulus_log_message(type: str, code: int, http_method:str, k_v: dict): + if type == 'success': + logkey = 'successes' + elif type == 'failure': + logkey = 'failures' + else: + logkey = 'other' + k_v.update({'code': code, 'http_method': http_method, 'status': type}) + jsonstr = json.dumps(k_v) + log.info(f'`{logkey}` {jsonstr}') + + def restore_bucket_vars(): global b_map #pylint: disable=global-statement @@ -84,8 +96,9 @@ def make_redirect(to_url, headers=None, status_code=301): if headers is None: headers = {} headers['Location'] = to_url - log.info(f'TEA success {status_code}. Redirect created. to_url: {to_url}') # cumulus uses this log message - # for metrics purposes. + log.info(f'Redirect created. to_url: {to_url}') + cumulus_log_message('success', status_code, 'GET', {'redirect': 'yes', 'redirect_URL': to_url}) + return Response(body='', headers=headers, status_code=status_code) @@ -145,8 +158,8 @@ def try_download_from_bucket(bucket, filename, user_profile): try: bucket_region = get_bucket_region(session, bucket) except ClientError as e: - # cumulus uses this log message for metrics purposes. - log.error(f'TEA failure 500. ClientError while {user_id} tried downloading {bucket}/{filename}: {e}') + log.error(f'ClientError while {user_id} tried downloading {bucket}/{filename}: {e}') + cumulus_log_message('failure', 500, 'GET', {'reason': 'ClientError', 's3': f'{bucket}/{filename}'}) template_vars = {'contentstring': 'There was a problem accessing download data.', 'title': 'Data Not Available'} headers = {} return make_html_response(template_vars, headers, 500, 'error.html') @@ -188,13 +201,15 @@ def try_download_from_bucket(bucket, filename, user_profile): # Watch for bad range request: if e.response['ResponseMetadata']['HTTPStatusCode'] == 416: # cumulus uses this log message for metrics purposes. - log.error("TEA failure 416. Invalid Range, Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) + log.error("Invalid Range 416, Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) + cumulus_log_message('failure', 416, 'GET', {'reason': 'Invalid Range', 's3': f'{bucket}/{filename}'}) return Response(body='Invalid Range', status_code=416, headers={}) # cumulus uses this log message for metrics purposes. - log.warning("TEA failure 404. Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) + log.warning("Could not download s3://{0}/{1}: {2}".format(bucket, filename, e)) template_vars = {'contentstring': 'Could not find requested data.', 'title': 'Data Not Available'} headers = {} + cumulus_log_message('failure', 404, 'GET', {'reason': 'Could not find requested data', 's3': f'{bucket}/{filename}'}) return make_html_response(template_vars, headers, 404, 'error.html') @@ -295,11 +310,13 @@ def try_download_head(bucket, filename): log.info("Downloading range {0}".format(range_header)) download = client.get_object(Bucket=bucket, Key=filename, Range=range_header) except ClientError as e: + log.warning("Could not get head for s3://{0}/{1}: {2}".format(bucket, filename, e)) # cumulus uses this log message for metrics purposes. - log.warning("TEA failure 404. Could not get head for s3://{0}/{1}: {2}".format(bucket, filename, e)) + template_vars = {'contentstring': 'File not found', 'title': 'File not found'} headers = {} + cumulus_log_message('failure', 404, 'HEAD', {'reason': 'Could not find requested data', 's3': f'{bucket}/{filename}'}) return make_html_response(template_vars, headers, 404, 'error.html') log.debug(download) From 147476d43ab89e6873ab4408f20f4964ff526f21 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 4 May 2020 12:34:14 -0800 Subject: [PATCH 43/65] #165 Not redefining `type` anymore --- lambda/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index 8f8286ea..027a3dee 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -34,14 +34,14 @@ 'content-length': 'Content-Length'} -def cumulus_log_message(type: str, code: int, http_method:str, k_v: dict): - if type == 'success': +def cumulus_log_message(outcome: str, code: int, http_method:str, k_v: dict): + if outcome == 'success': logkey = 'successes' - elif type == 'failure': + elif outcome == 'failure': logkey = 'failures' else: logkey = 'other' - k_v.update({'code': code, 'http_method': http_method, 'status': type}) + k_v.update({'code': code, 'http_method': http_method, 'status': outcome}) jsonstr = json.dumps(k_v) log.info(f'`{logkey}` {jsonstr}') From f9cfd50182a84244eb31703e6483d057b6ea28a5 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Wed, 6 May 2020 15:31:08 -0800 Subject: [PATCH 44/65] PR-2391 updated to submodule to dev version --- rain-api-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rain-api-core b/rain-api-core index 9997d01a..03ca54fa 160000 --- a/rain-api-core +++ b/rain-api-core @@ -1 +1 @@ -Subproject commit 9997d01a5c04bf977d5b84c50a466178f5acee34 +Subproject commit 03ca54fa4703b6d9f4327c4fbfef47e4d403cbab From 100c2263c54f3726fdc93a15948a5255e39ec1fb Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Wed, 6 May 2020 16:45:25 -0800 Subject: [PATCH 45/65] PR-2391 updated submodule to latest dev --- rain-api-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rain-api-core b/rain-api-core index 03ca54fa..04e54f47 160000 --- a/rain-api-core +++ b/rain-api-core @@ -1 +1 @@ -Subproject commit 03ca54fa4703b6d9f4327c4fbfef47e4d403cbab +Subproject commit 04e54f4711eff841c6886f3f459adc5e3a234b73 From ae69639600819cb8c0a80476f535e2e601d102fa Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Wed, 6 May 2020 16:59:30 -0800 Subject: [PATCH 46/65] PR-2391 switched to new function that gets headers from bucketmap --- lambda/app.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index 027a3dee..282cf146 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -9,8 +9,8 @@ from rain_api_core.general_util import get_log from rain_api_core.urs_util import get_urs_url, do_login, user_in_group from rain_api_core.aws_util import get_yaml_file, get_s3_resource, get_role_session, get_role_creds, check_in_region_request -from rain_api_core.view_util import get_html_body, get_cookie_vars, make_set_cookie_headers_jwt -from rain_api_core.egress_util import get_presigned_url, process_varargs, check_private_bucket, check_public_bucket +from rain_api_core.view_util import get_html_body, get_cookie_vars, make_set_cookie_headers_jwt, JWT_COOKIE_NAME +from rain_api_core.egress_util import get_presigned_url, process_request, check_private_bucket, check_public_bucket app = Chalice(app_name='egress-lambda') log = get_log() @@ -214,9 +214,9 @@ def try_download_from_bucket(bucket, filename, user_profile): def get_jwt_field(cookievar: dict, fieldname: str): - if os.getenv('JWT_COOKIENAME', 'asf-urs') in cookievar: - if fieldname in cookievar[os.getenv('JWT_COOKIENAME', 'asf-urs')]: - return cookievar[os.getenv('JWT_COOKIENAME', 'asf-urs')][fieldname] + if os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME) in cookievar: + if fieldname in cookievar[os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME)]: + return cookievar[os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME)][fieldname] return None @@ -229,9 +229,9 @@ def root(): cookievars = get_cookie_vars(app.current_request.headers) if cookievars: - if os.getenv('JWT_COOKIENAME', 'asf-urs') in cookievars: + if os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME) in cookievars: # We have a JWT cookie - user_profile = cookievars[os.getenv('JWT_COOKIENAME', 'asf-urs')] + user_profile = cookievars[os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME)] if user_profile: if os.getenv('MATURITY') == 'DEV': @@ -249,7 +249,7 @@ def logout(): cookievars = get_cookie_vars(app.current_request.headers) template_vars = {'title': 'Logged Out', 'URS_URL': get_urs_url(app.current_request.context)} - if os.getenv('JWT_COOKIENAME', 'asf-urs') in cookievars: + if os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME) in cookievars: template_vars['contentstring'] = 'You are logged out.' else: @@ -350,7 +350,7 @@ def dynamic_url_head(): restore_bucket_vars() if 'proxy' in app.current_request.uri_params: - path, bucket, filename = process_varargs(app.current_request.uri_params['proxy'], b_map) + path, bucket, filename, custom_headers = process_request(app.current_request.uri_params['proxy'], b_map) process_results = 'path: {}, bucket: {}, filename:{}'.format(path, bucket, filename) log.debug(process_results) @@ -372,7 +372,7 @@ def dynamic_url(): restore_bucket_vars() if 'proxy' in app.current_request.uri_params: - path, bucket, filename = process_varargs(app.current_request.uri_params['proxy'], b_map) + path, bucket, filename, custom_headers = process_request(app.current_request.uri_params['proxy'], b_map) log.debug('path, bucket, filename: {}'.format(( path, bucket, filename))) if not bucket: template_vars = {'contentstring': 'File not found', 'title': 'File not found'} @@ -385,9 +385,9 @@ def dynamic_url(): user_profile = None if cookievars: log.debug('cookievars: {}'.format(cookievars)) - if os.getenv('JWT_COOKIENAME', 'asf-urs') in cookievars: + if os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME) in cookievars: # this means our cookie is a jwt and we don't need to go digging in the session db - user_profile = cookievars[os.getenv('JWT_COOKIENAME', 'asf-urs')] + user_profile = cookievars[os.getenv('JWT_COOKIENAME', JWT_COOKIE_NAME)] else: log.warning('jwt cookie not found') # Not kicking user out just yet. We might be dealing with a public bucket From 45b3795e8114efae267904890b098e2bfffe860b Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Thu, 7 May 2020 13:22:42 -0800 Subject: [PATCH 47/65] PR-2391 using headers gotten from bucketmap --- lambda/app.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index 282cf146..fe1564bd 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -98,7 +98,7 @@ def make_redirect(to_url, headers=None, status_code=301): headers['Location'] = to_url log.info(f'Redirect created. to_url: {to_url}') cumulus_log_message('success', status_code, 'GET', {'redirect': 'yes', 'redirect_URL': to_url}) - + log.debug(f'headers for redirect: {headers}') return Response(body='', headers=headers, status_code=status_code) @@ -139,7 +139,8 @@ def get_bucket_region(session, bucketname) ->str: return bucket_region -def try_download_from_bucket(bucket, filename, user_profile): + +def try_download_from_bucket(bucket, filename, user_profile, headers: list): # Attempt to pull userid from profile user_id = None @@ -189,6 +190,9 @@ def try_download_from_bucket(bucket, filename, user_profile): expires_in = 24 * 3600 redirheaders['Cache-Control'] = 'private, max-age={0}'.format(expires_in - 60) + if isinstance(headers, list): + log.debug(f'adding {headers} to redirheaders {redirheaders}') + redirheaders.update(headers) # Generate URL presigned_url = get_presigned_url(creds, bucket, filename, bucket_region, expires_in, user_id) @@ -367,13 +371,13 @@ def dynamic_url_head(): @app.route('/{proxy+}', methods=['GET']) def dynamic_url(): - + custom_headers = {} log.debug('attempting to GET a thing') restore_bucket_vars() if 'proxy' in app.current_request.uri_params: path, bucket, filename, custom_headers = process_request(app.current_request.uri_params['proxy'], b_map) - log.debug('path, bucket, filename: {}'.format(( path, bucket, filename))) + log.debug('path, bucket, filename, custom_headers: {}'.format(( path, bucket, filename, custom_headers))) if not bucket: template_vars = {'contentstring': 'File not found', 'title': 'File not found'} headers = {} @@ -419,8 +423,8 @@ def dynamic_url(): template_vars = {'contentstring': 'Request does not appear to be valid.', 'title': 'Request Not Serviceable'} headers = {} return make_html_response(template_vars, headers, 404, 'error.html') - - return try_download_from_bucket(bucket, filename, user_profile) + log.debug(f'custom headers before try download from bucket: {custom_headers}') + return try_download_from_bucket(bucket, filename, user_profile, custom_headers) @app.route('/profile') From 009020215b1f9669cfd10f0c5bc7df2c579b1ca8 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Thu, 7 May 2020 13:23:19 -0800 Subject: [PATCH 48/65] PR-2391 updated bucketmap section for custom headers also generally clarified things, I hope. --- README.MD | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/README.MD b/README.MD index ff460fe2..ff6ff2e9 100644 --- a/README.MD +++ b/README.MD @@ -166,7 +166,7 @@ profile_name= aws_region= bash setup_jwt_cookie.sh ### Buckets and Bucket map -The bucket map allows the app to determine in which bucket to look when given the path from the URL. It's possible to separate the maps into bucket, public and private, but this functionality is deprecated and will be removed in a future version of TEA. +The bucket map allows the app to determine in which bucket to look when given the path from the URL. It's possible to separate the maps into separate files for bucket, public and private, but this functionality is deprecated and will be removed in a future version of TEA. If a url for a product would look like: ```https://datapile.domain.com/STAGE/PROCESSING_TYPE_1/PLATFORM_A/datafile.dat``` @@ -179,6 +179,10 @@ And if we have a data bucket prefix of `prfx-d-` and our data bucket list looks - prfx-d-pb-pt1 - prfx-d-pa-pt2 - prfx-d-pb-pt2 +- prfx-d-pa-pt3a +- prfx-d-pb-pt3b +- prfx-d-pa-pt3a +- prfx-d-pb-pt3b - prfx-d-private-x - prfx-d-private-y @@ -191,9 +195,29 @@ MAP: PROCESSING_TYPE_1: PLATFORM_A: pa-pt1 PLATFORM_B: pb-pt1 + + # Custom response headers for the redirect are supported: PROCESSING_TYPE_2: - PLATFORM_A: pa-pt2 - PLATFORM_B: pb-pt2 + PLATFORM_A: + bucket: pa-pt2 + headers: + x-custom-header-1: "custom header 1 value" + x-custom-header-2: "custom header 2 value" + PLATFORM_B: + bucket: pb-pt2 + headers: + x-custom-header-1: "custom header 1 value" + x-custom-header-2: "custom header 2 value" + + # arbritary number of "subdirectories" are supported: + PROCESSING_TYPE_3: + SUB_PROCESSING_TYPE_3A: + PLATFORM_A: pa-pt3a + PLATFORM_B: pb-pt3a + SUB_PROCESSING_TYPE_3B: + PLATFORM_A: pa-pt3b + PLATFORM_B: pb-pt3b + THUMBNAIL: PLATFORM_A: imgs PLATFORM_B: imgs @@ -202,12 +226,19 @@ PUBLIC_BUCKETS: imgs: 'BROWSE IMAGERY' PRIVATE_BUCKETS: - private-x: + pa-pt1: - urs_group_name_0 - private-y: + pa-pt2: - urs_group_name_1 ``` + +Given this bucketmap and a bucket prefix of `prfx-d-`: +* A URL like `http://domain.com/PROCESSING_TYPE_1/PLATFORM_A/file.dat` would cause TEA to look for `file.dat` in a bucket named `prfx-d-pa-pt1`. Additionally, because that bucket is listed in the `PRIVATE_BUCKETS` section, it will only be available to users belonging to a URS group named `urs_group_name_0`. +* A URL like `http://domain.com/PROCESSING_TYPE_2/PLATFORM_A/file.dat` would cause TEA to look for `file.dat` in a bucket named `prfx-d-pa-pt2`. It will send response headers defined in that section with the redirect. +* A URL like `http://domain.com/THUMBNAIL/PLATFORM_A/file.dat` would cause TEA to look for `file.dat` in a bucket named `prfx-d-imgs`. Because `imgs` is listed under `PUBLIC_BUCKETS`, the file would be public and available to all. + + ### Config bucket You will need a bucket for config and optionally the html templates. This should be in the same region as the stack. From db4c8d61fc6a17df26b316cc656f5521fc78ce89 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Fri, 8 May 2020 11:34:59 -0800 Subject: [PATCH 49/65] PR-2391 updated submodule to fresh master that incorporates support for custom headers. --- rain-api-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rain-api-core b/rain-api-core index 04e54f47..f3ec7baa 160000 --- a/rain-api-core +++ b/rain-api-core @@ -1 +1 @@ -Subproject commit 04e54f4711eff841c6886f3f459adc5e3a234b73 +Subproject commit f3ec7baab4967c98f4e8fda144e2ec0071d62557 From 4097962c4e2a40d14d14365f7a883c5f60016194 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 12:28:29 -0800 Subject: [PATCH 50/65] PR-2391 headers are a dict, not list --- lambda/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index fe1564bd..fa4558fd 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -140,7 +140,7 @@ def get_bucket_region(session, bucketname) ->str: return bucket_region -def try_download_from_bucket(bucket, filename, user_profile, headers: list): +def try_download_from_bucket(bucket, filename, user_profile, headers: dict): # Attempt to pull userid from profile user_id = None @@ -190,7 +190,7 @@ def try_download_from_bucket(bucket, filename, user_profile, headers: list): expires_in = 24 * 3600 redirheaders['Cache-Control'] = 'private, max-age={0}'.format(expires_in - 60) - if isinstance(headers, list): + if isinstance(headers, dict): log.debug(f'adding {headers} to redirheaders {redirheaders}') redirheaders.update(headers) From 15b349d1f1b28b6039704bf6c92f3f2953479e03 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 12:58:14 -0800 Subject: [PATCH 51/65] PR-2391 updated with support for dict-not-list --- rain-api-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rain-api-core b/rain-api-core index f3ec7baa..4b1372e2 160000 --- a/rain-api-core +++ b/rain-api-core @@ -1 +1 @@ -Subproject commit f3ec7baab4967c98f4e8fda144e2ec0071d62557 +Subproject commit 4b1372e2216ade19fdda539a0779c1a7fa82ef7b From bbc02ab85ab63c630b9c946eb48fa0dcd52b46a7 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 15:37:52 -0800 Subject: [PATCH 52/65] PR-2392 Added test for custom header --- build/download_test.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/build/download_test.sh b/build/download_test.sh index 0d6c0983..272321d9 100755 --- a/build/download_test.sh +++ b/build/download_test.sh @@ -13,6 +13,7 @@ if [[ -z $DOMAIN_NAME ]]; then API=$(aws apigateway get-rest-apis --query "item echo " >>> APIROOT is $APIROOT" METADATA_FILE=SA/METADATA_GRD_HS/S1A_EW_GRDM_1SDH_20190206T190846_20190206T190951_025813_02DF0B_781A.iso.xml +METADATA_FILE_CH=SA/METADATA_GRD_HS_CH/S1A_EW_GRDM_1SDH_20190206T190846_20190206T190951_025813_02DF0B_781A.iso.xml METADATA_CHECK='S1A_EW_GRDM_1SDH_20190206T190846_20190206T190951_025813_02DF0B_781A.iso.xml' BROWSE_FILE=SA/BROWSE/S1A_EW_GRDM_1SDH_20190206T190846_20190206T190951_025813_02DF0B_781A.jpg @@ -80,22 +81,31 @@ grep 'HTTP/1.1 403 Forbidden' /tmp/test8 && grep -q 'HTTP/1.1 403 Forbidden' /tm if [ $? -ne 0 ]; then echo; echo " >> Could not verify PRIVATE access was restricted (TEST 8) << "; echo; FC=$((FC+1)); else echo " >>> TEST 8 PASSED"; fi # Validating objects with prefix -echo " >>> Validating accessing objects with prefix's" +echo " >>> Validating accessing objects with prefixes" echo " > curl -s -L $APIROOT/SA/BROWSE/dir1/dir2/deepfile.txt | grep 'The file was successfully downloaded'" curl -s -L $APIROOT/SA/BROWSE/dir1/dir2/deepfile.txt 2>&1 &> /tmp/test9 cat /tmp/test9 && grep -q 'The file was successfully downloaded' /tmp/test9 if [ $? -ne 0 ]; then echo; echo " >> Could not verify prefixed file access (TEST9) << "; echo; FC=$((FC+1)); else echo " >>> TEST 9 PASSED"; fi +# Validating custom headers +echo " >>> Validating custom headers" +echo " > curl -s -o /dev/null -b ./urscookie.txt -c ./urscookie.txt -D - $METADATA_FILE_CH | grep 'x-rainheader'" +curl -s -L $APIROOT/SA/BROWSE/dir1/dir2/deepfile.txt 2>&1 &> /tmp/test10 +cat /tmp/test10 && grep -q 'x-rainheader' /tmp/test10 +if [ $? -ne 0 ]; then echo; echo " >> Could not custom headers (TEST10) << "; echo; FC=$((FC+1)); else echo " >>> TEST 10 PASSED"; fi + +#curl -s -o /dev/null -b ./urscookie.txt -c ./urscookie.txt -D - $METADATA_FILE_CH + # Build Summary if [ $FC -le 0 ]; then echo " >>> All Tests Passed!" echo '{ "schemaVersion": 1, "label": "Tests", "message": "All Tests Passed", "color": "success" }' > /tmp/testresults.json elif [ $FC -lt 3 ]; then echo " >>> Some Tests Failed" - echo '{ "schemaVersion": 1, "label": "Tests", "message": "'$FC'/9 Tests Failed ⚠️", "color": "important" }' > /tmp/testresults.json + echo '{ "schemaVersion": 1, "label": "Tests", "message": "'$FC'/10 Tests Failed ⚠️", "color": "important" }' > /tmp/testresults.json else echo " >>> TOO MANY TEST FAILURES! " - echo '{ "schemaVersion": 1, "label": "Tests", "message": "'$FC'/9 Tests Failed ☠", "color": "critical" }' > /tmp/testresults.json + echo '{ "schemaVersion": 1, "label": "Tests", "message": "'$FC'/10 Tests Failed ☠", "color": "critical" }' > /tmp/testresults.json fi # Upload test results From 7bb5a130f8bb7a0d7e530064301f26321da2d078 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 15:59:55 -0800 Subject: [PATCH 53/65] PR-2391 corrected bucketmap file location values for deployment --- build/Jenkinsfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build/Jenkinsfile b/build/Jenkinsfile index 0d3ad511..f00f00a2 100644 --- a/build/Jenkinsfile +++ b/build/Jenkinsfile @@ -132,9 +132,9 @@ pipeline { URSAuthCredsSecretName=${URS_CREDS_SECRET_NAME} \ ConfigBucket=${BUCKETNAME_PREFIX}config \ PermissionsBoundaryName= \ - BucketMapFile=bucket_map.yaml \ - PublicBucketsFile=public_buckets.yaml \ - PrivateBucketsFile=private_buckets.yaml \ + BucketMapFile=bucket_map_customheaders.yaml \ + PublicBucketsFile="" \ + PrivateBucketsFile="" \ BucketnamePrefix=${BUCKETNAME_PREFIX} \ DownloadRoleArn="" \ DownloadRoleInRegionArn="" \ @@ -166,9 +166,9 @@ pipeline { URSAuthCredsSecretName=${URS_CREDS_SECRET_NAME} \ ConfigBucket=rain-t-config \ PermissionsBoundaryName= \ - BucketMapFile=bucket_map_TEST_ngap2.yaml \ - PublicBucketsFile=public_buckets_TEA.yaml \ - PrivateBucketsFile=private_buckets_TEA.yaml \ + BucketMapFile=bucket_map_customheaders.yaml \ + PublicBucketsFile="" \ + PrivateBucketsFile="" \ BucketnamePrefix=${BUCKETNAME_PREFIX_SCND} \ DownloadRoleArn=${DOWNLOAD_ROLE_ARN} \ DownloadRoleInRegionArn=${DOWNLOAD_ROLE_ARN_INREGION} \ From 4a75bb240bb97b3115dc843f105fadb69233ead9 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 17:17:56 -0800 Subject: [PATCH 54/65] PR-2391 Implemented option of getting private and public from main bucketmap --- lambda/app.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index fe1564bd..eae48cde 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -52,25 +52,27 @@ def restore_bucket_vars(): global public_buckets #pylint: disable=global-statement global private_buckets #pylint: disable=global-statement - log.debug('conf bucket: {}, bucket_map_file: {}, ' + - 'public_buckets_file: {}, private buckets file: {}'.format(conf_bucket, - bucket_map_file, - public_buckets_file, - private_buckets_file)) + log.debug(f'conf bucket: {conf_bucket}, bucket_map_file: {bucket_map_file}, public_buckets_file: {public_buckets_file}, private buckets file: {private_buckets_file}') if b_map is None or public_buckets is None or private_buckets is None: - log.info('downloading various bucket configs from {}: bucketmapfile: {}, ' + - 'public buckets file: {}, private buckets file: {}'.format(conf_bucket, - bucket_map_file, - public_buckets_file, - private_buckets_file)) + log.info(f'downloading various bucket configs from {conf_bucket}: bucketmapfile: {bucket_map_file}, public buckets file: {public_buckets_file}, private buckets file: {private_buckets_file}') b_map = get_yaml_file(conf_bucket, bucket_map_file, s3_resource) log.debug('bucket map: {}'.format(b_map)) - if public_buckets_file: + + if 'PUBLIC_BUCKETS' in b_map: + public_buckets = {'PUBLIC_BUCKETS': None} + public_buckets['PUBLIC_BUCKETS'] = b_map['PUBLIC_BUCKETS'] + log.debug(f'This is public_buckets: {public_buckets}') + elif public_buckets_file: log.debug('fetching public buckets yaml file: {}'.format(public_buckets_file)) public_buckets = get_yaml_file(conf_bucket, public_buckets_file, s3_resource) else: public_buckets = {} - if private_buckets_file: + + if 'PRIVATE' in b_map: + private_buckets = {'PRIVATE': None} + private_buckets['PRIVATE'] = b_map['PRIVATE'] + log.debug(f'This is private_buckets: {private_buckets}') + elif private_buckets_file: private_buckets = get_yaml_file(conf_bucket, private_buckets_file, s3_resource) else: private_buckets = {} From 17e2112ccc0a5737f18ad60831d4c95edf6763da Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 17:20:50 -0800 Subject: [PATCH 55/65] PR-2392 fixed path to cookie in test 10 --- build/download_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/download_test.sh b/build/download_test.sh index 272321d9..849ac0c6 100755 --- a/build/download_test.sh +++ b/build/download_test.sh @@ -89,7 +89,7 @@ if [ $? -ne 0 ]; then echo; echo " >> Could not verify prefixed file access (TES # Validating custom headers echo " >>> Validating custom headers" -echo " > curl -s -o /dev/null -b ./urscookie.txt -c ./urscookie.txt -D - $METADATA_FILE_CH | grep 'x-rainheader'" +echo " > curl -s -o /dev/null -b /tmp/urscookie.txt -c /tmp/urscookie.txt -D - $METADATA_FILE_CH | grep 'x-rainheader'" curl -s -L $APIROOT/SA/BROWSE/dir1/dir2/deepfile.txt 2>&1 &> /tmp/test10 cat /tmp/test10 && grep -q 'x-rainheader' /tmp/test10 if [ $? -ne 0 ]; then echo; echo " >> Could not custom headers (TEST10) << "; echo; FC=$((FC+1)); else echo " >>> TEST 10 PASSED"; fi From dde269288455dc77281672541553854b60840c91 Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 17:37:31 -0800 Subject: [PATCH 56/65] PR-2391 reverted restore_bucket_vars() --- lambda/app.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index eae48cde..2725db69 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -52,27 +52,25 @@ def restore_bucket_vars(): global public_buckets #pylint: disable=global-statement global private_buckets #pylint: disable=global-statement - log.debug(f'conf bucket: {conf_bucket}, bucket_map_file: {bucket_map_file}, public_buckets_file: {public_buckets_file}, private buckets file: {private_buckets_file}') + log.debug('conf bucket: {}, bucket_map_file: {}, ' + + 'public_buckets_file: {}, private buckets file: {}'.format(conf_bucket, + bucket_map_file, + public_buckets_file, + private_buckets_file)) if b_map is None or public_buckets is None or private_buckets is None: - log.info(f'downloading various bucket configs from {conf_bucket}: bucketmapfile: {bucket_map_file}, public buckets file: {public_buckets_file}, private buckets file: {private_buckets_file}') + log.info('downloading various bucket configs from {}: bucketmapfile: {}, ' + + 'public buckets file: {}, private buckets file: {}'.format(conf_bucket, + bucket_map_file, + public_buckets_file, + private_buckets_file)) b_map = get_yaml_file(conf_bucket, bucket_map_file, s3_resource) log.debug('bucket map: {}'.format(b_map)) - - if 'PUBLIC_BUCKETS' in b_map: - public_buckets = {'PUBLIC_BUCKETS': None} - public_buckets['PUBLIC_BUCKETS'] = b_map['PUBLIC_BUCKETS'] - log.debug(f'This is public_buckets: {public_buckets}') - elif public_buckets_file: + if public_buckets_file: log.debug('fetching public buckets yaml file: {}'.format(public_buckets_file)) public_buckets = get_yaml_file(conf_bucket, public_buckets_file, s3_resource) else: public_buckets = {} - - if 'PRIVATE' in b_map: - private_buckets = {'PRIVATE': None} - private_buckets['PRIVATE'] = b_map['PRIVATE'] - log.debug(f'This is private_buckets: {private_buckets}') - elif private_buckets_file: + if private_buckets_file: private_buckets = get_yaml_file(conf_bucket, private_buckets_file, s3_resource) else: private_buckets = {} @@ -80,6 +78,7 @@ def restore_bucket_vars(): log.info('reusing old bucket configs') + def do_auth_and_return(ctxt): log.debug('context: {}'.format(ctxt)) From 0645ee8fed6983577363fe033267ef86d4b9545e Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Mon, 11 May 2020 18:20:35 -0800 Subject: [PATCH 57/65] PR-2392 fixed customheader test --- build/download_test.sh | 4 ++-- rain-api-core | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/download_test.sh b/build/download_test.sh index 849ac0c6..84425f64 100755 --- a/build/download_test.sh +++ b/build/download_test.sh @@ -89,8 +89,8 @@ if [ $? -ne 0 ]; then echo; echo " >> Could not verify prefixed file access (TES # Validating custom headers echo " >>> Validating custom headers" -echo " > curl -s -o /dev/null -b /tmp/urscookie.txt -c /tmp/urscookie.txt -D - $METADATA_FILE_CH | grep 'x-rainheader'" -curl -s -L $APIROOT/SA/BROWSE/dir1/dir2/deepfile.txt 2>&1 &> /tmp/test10 +echo " > curl -s -o /dev/null -b /tmp/urscookie.txt -c /tmp/urscookie.txt -D - $APIROOT/$METADATA_FILE_CH | grep 'x-rainheader'" +curl -s -o /dev/null -b /tmp/urscookie.txt -c /tmp/urscookie.txt -D - $APIROOT/$METADATA_FILE_CH 2>&1 &> /tmp/test10 cat /tmp/test10 && grep -q 'x-rainheader' /tmp/test10 if [ $? -ne 0 ]; then echo; echo " >> Could not custom headers (TEST10) << "; echo; FC=$((FC+1)); else echo " >>> TEST 10 PASSED"; fi diff --git a/rain-api-core b/rain-api-core index 4b1372e2..f3ec7baa 160000 --- a/rain-api-core +++ b/rain-api-core @@ -1 +1 @@ -Subproject commit 4b1372e2216ade19fdda539a0779c1a7fa82ef7b +Subproject commit f3ec7baab4967c98f4e8fda144e2ec0071d62557 From 44c13d03efed3e99f0f54b4d689f5931acf7ea63 Mon Sep 17 00:00:00 2001 From: Jonathan Kovarik Date: Mon, 11 May 2020 23:35:08 -0600 Subject: [PATCH 58/65] Add '/locate' endpoint Commit adds '/locate' endpoint, updates cloudformation/stack --- build/dependency_builder.sh | 1 + cloudformation/thin-egress-app.yaml | 32 +++++++++++++++++++++++++++++ lambda/app.py | 12 +++++++++++ lambda/requirements.txt | 12 +++++------ 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/build/dependency_builder.sh b/build/dependency_builder.sh index 4ecd216c..a288d471 100755 --- a/build/dependency_builder.sh +++ b/build/dependency_builder.sh @@ -11,6 +11,7 @@ mkdir -p /depbuild/pkg/python cd /depbuild/pkg/python || exit +pip3 install --upgrade setuptools pip3 install -r /depbuild/in/requirements_tea.txt --target . pip3 install -r /depbuild/in/requirements_rain-api-core.txt --target . diff --git a/cloudformation/thin-egress-app.yaml b/cloudformation/thin-egress-app.yaml index 94820dea..4c1d62f4 100644 --- a/cloudformation/thin-egress-app.yaml +++ b/cloudformation/thin-egress-app.yaml @@ -615,6 +615,15 @@ Resources: PathPart: 'profile' RestApiId: !Ref EgressApiGateway + EgressApiResourceLocate: + Type: AWS::ApiGateway::Resource + DependsOn: + - EgressApiGateway + Properties: + ParentId: !GetAtt EgressApiGateway.RootResourceId + PathPart: 'locate' + RestApiId: !Ref EgressApiGateway + EgressApiResourceVersion: Type: AWS::ApiGateway::Resource DependsOn: @@ -624,6 +633,29 @@ Resources: PathPart: 'version' RestApiId: !Ref EgressApiGateway + EgressAPIMethodLocate: + Type: AWS::ApiGateway::Method + Properties: + ApiKeyRequired: false + AuthorizationType: 'NONE' + HttpMethod: 'GET' + Integration: + IntegrationHttpMethod: 'POST' + IntegrationResponses: + - StatusCode: 200 + - StatusCode: 404 + Type: 'AWS_PROXY' + Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EgressLambda.Arn}/invocations" + MethodResponses: + - ResponseParameters: + 'method.response.header.Set-Cookie': true + StatusCode: 200 + OperationName: 'locate method' + RequestParameters: + 'method.request.header.Cookie': true + ResourceId: !Ref EgressApiResourceLocate + RestApiId: !Ref EgressApiGateway + EgressAPIrootMethod: Type: AWS::ApiGateway::Method Properties: diff --git a/lambda/app.py b/lambda/app.py index 027a3dee..c8bfd935 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -1,6 +1,7 @@ from chalice import Chalice, Response from botocore.config import Config as bc_Config from botocore.exceptions import ClientError +import flatdict import os import json @@ -139,6 +140,7 @@ def get_bucket_region(session, bucketname) ->str: return bucket_region + def try_download_from_bucket(bucket, filename, user_profile): # Attempt to pull userid from profile @@ -277,6 +279,16 @@ def version(): return json.dumps({'version_id': ''}) +@app.route('/locate') +def locate(): + search_field = app.current_request.query_params['bucket_name'] + search_map = flatdict.FlatDict(get_yaml_file(conf_bucket, bucket_map_file, s3_resource)['MAP'], delimiter='/') + matching_paths = [key for key, value in search_map.items() if value == search_field] + if(len(matching_paths) > 0): + return json.dumps(matching_paths) + return Response(body=f'No route defined for {search_field}',status_code=404,headers={'Content-Type': 'text/plain'}) + + def get_range_header_val(): if 'Range' in app.current_request.headers: diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 0ab70577..7dd7c287 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -1,12 +1,12 @@ -jinja2==2.11.2 -PyYAML==5.3.1 aws-requests-auth==0.4.2 -pyjwt==1.7.1 +cfnresponse==1.0.2 chalice==1.14.0 +cryptography==2.9.2 +flatdict==4.0.1 +jinja2==2.11.2 jwcrypto==0.7 netaddr==0.7.19 -cryptography==2.9.2 +pyjwt==1.7.1 pyOpenSSL==19.1.0 # maybe not necessary python-jose==3.1.0 -cfnresponse==1.0.2 - +PyYAML==5.3.1 \ No newline at end of file From 2e20ffa1d9294acde370def195b0007b211e27a1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 12 May 2020 10:45:44 -0600 Subject: [PATCH 59/65] Update chalice from 1.14.0 to 1.14.1 --- lambda/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda/requirements.txt b/lambda/requirements.txt index 0ab70577..71f16549 100644 --- a/lambda/requirements.txt +++ b/lambda/requirements.txt @@ -2,7 +2,7 @@ jinja2==2.11.2 PyYAML==5.3.1 aws-requests-auth==0.4.2 pyjwt==1.7.1 -chalice==1.14.0 +chalice==1.14.1 jwcrypto==0.7 netaddr==0.7.19 cryptography==2.9.2 From cf287c1c7af328c280d34db8ecffd23530172c7f Mon Sep 17 00:00:00 2001 From: Jonathan Kovarik Date: Tue, 12 May 2020 15:54:40 -0600 Subject: [PATCH 60/65] Add prefix strip to bucket_name on /locate query params --- lambda/app.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index 4db3a624..95a2919d 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -11,7 +11,7 @@ from rain_api_core.urs_util import get_urs_url, do_login, user_in_group from rain_api_core.aws_util import get_yaml_file, get_s3_resource, get_role_session, get_role_creds, check_in_region_request from rain_api_core.view_util import get_html_body, get_cookie_vars, make_set_cookie_headers_jwt, JWT_COOKIE_NAME -from rain_api_core.egress_util import get_presigned_url, process_request, check_private_bucket, check_public_bucket +from rain_api_core.egress_util import get_presigned_url, process_request, prepend_bucketname, check_private_bucket, check_public_bucket app = Chalice(app_name='egress-lambda') log = get_log() @@ -285,12 +285,16 @@ def version(): @app.route('/locate') def locate(): - search_field = app.current_request.query_params['bucket_name'] + prefix = prepend_bucketname('') + bucket_name = app.current_request.query_params['bucket_name'] + if bucket_name.startswith(prefix): + bucket_name = bucket_name[len(prefix):] search_map = flatdict.FlatDict(get_yaml_file(conf_bucket, bucket_map_file, s3_resource)['MAP'], delimiter='/') - matching_paths = [key for key, value in search_map.items() if value == search_field] + matching_paths = [key for key, value in search_map.items() if value == bucket_name] if(len(matching_paths) > 0): return json.dumps(matching_paths) - return Response(body=f'No route defined for {search_field}',status_code=404,headers={'Content-Type': 'text/plain'}) + return Response(body=f'No route defined for {bucket_name}',status_code=404,headers={'Content-Type': 'text/plain'}) + def get_range_header_val(): From f5ec274ffc9f32c31c83db05464ca3ce22d4a5ec Mon Sep 17 00:00:00 2001 From: Jonathan Kovarik Date: Tue, 12 May 2020 18:03:43 -0600 Subject: [PATCH 61/65] Add logic to handle {bucket: value, config1: foo, config2: bar} keys --- lambda/app.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lambda/app.py b/lambda/app.py index 95a2919d..67e9fa2e 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -289,7 +289,8 @@ def locate(): bucket_name = app.current_request.query_params['bucket_name'] if bucket_name.startswith(prefix): bucket_name = bucket_name[len(prefix):] - search_map = flatdict.FlatDict(get_yaml_file(conf_bucket, bucket_map_file, s3_resource)['MAP'], delimiter='/') + bucket_map = collapse_bucket_configuration(get_yaml_file(conf_bucket, bucket_map_file, s3_resource)['MAP']) + search_map = flatdict.FlatDict(bucket_map, delimiter='/') matching_paths = [key for key, value in search_map.items() if value == bucket_name] if(len(matching_paths) > 0): return json.dumps(matching_paths) @@ -297,6 +298,17 @@ def locate(): +def collapse_bucket_configuration(bucket_map): + for k, v in bucket_map.items(): + if isinstance(v, dict): + if 'bucket' in v: + bucket_map[k] = v['bucket'] + else: + collapse_bucket_configuration(v) + + return bucket_map + + def get_range_header_val(): if 'Range' in app.current_request.headers: From e337b002a5e2951cbabc6ab581108ca601446b5d Mon Sep 17 00:00:00 2001 From: Jonathan Kovarik Date: Tue, 12 May 2020 22:12:15 -0600 Subject: [PATCH 62/65] Remove prefix filtering, add base integration test --- build/download_test.sh | 9 +++++++++ lambda/app.py | 4 ---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/build/download_test.sh b/build/download_test.sh index 0d6c0983..05ca6eab 100755 --- a/build/download_test.sh +++ b/build/download_test.sh @@ -86,6 +86,15 @@ curl -s -L $APIROOT/SA/BROWSE/dir1/dir2/deepfile.txt 2>&1 &> /tmp/test9 cat /tmp/test9 && grep -q 'The file was successfully downloaded' /tmp/test9 if [ $? -ne 0 ]; then echo; echo " >> Could not verify prefixed file access (TEST9) << "; echo; FC=$((FC+1)); else echo " >>> TEST 9 PASSED"; fi +# Validate /locate handles complex configuration keys +# LOCATE_OUTPUT should be set e.g. '["SA/OCN", "SA/OCN_CH", "SB/OCN_CN", "SB/OCN_CH"]' +# LOCATE_BUCKET should be set +echo " >>> Validating /locate endpoint handles complex bucket map configuration" +echo " > curl $APIROOT/locate?bucket_name=$LOCATE_BUCKET |grep -F \"$LOCATE_OUTPUT\"" +curl -sv $APIROOT/locate?bucket_name=$LOCATE_BUCKET |grep -F "$LOCATE_OUTPUT" +if [ $? -ne 0 ]; then echo; echo " >> Could not validate $LOCATE_BUCKET mapping on /locate endpoint"; else echo " >>> TEST 11 PASSED"; fi + + # Build Summary if [ $FC -le 0 ]; then echo " >>> All Tests Passed!" diff --git a/lambda/app.py b/lambda/app.py index 67e9fa2e..efe3c9b8 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -285,10 +285,7 @@ def version(): @app.route('/locate') def locate(): - prefix = prepend_bucketname('') bucket_name = app.current_request.query_params['bucket_name'] - if bucket_name.startswith(prefix): - bucket_name = bucket_name[len(prefix):] bucket_map = collapse_bucket_configuration(get_yaml_file(conf_bucket, bucket_map_file, s3_resource)['MAP']) search_map = flatdict.FlatDict(bucket_map, delimiter='/') matching_paths = [key for key, value in search_map.items() if value == bucket_name] @@ -297,7 +294,6 @@ def locate(): return Response(body=f'No route defined for {bucket_name}',status_code=404,headers={'Content-Type': 'text/plain'}) - def collapse_bucket_configuration(bucket_map): for k, v in bucket_map.items(): if isinstance(v, dict): From 5518262c8cf661b68479c2becf040f777cf3eddd Mon Sep 17 00:00:00 2001 From: Jonathan Kovarik Date: Thu, 14 May 2020 07:43:53 -0600 Subject: [PATCH 63/65] Add bad parameter catch, update response types --- lambda/app.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lambda/app.py b/lambda/app.py index efe3c9b8..0d385c89 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -251,7 +251,6 @@ def root(): @app.route('/logout') def logout(): - cookievars = get_cookie_vars(app.current_request.headers) template_vars = {'title': 'Logged Out', 'URS_URL': get_urs_url(app.current_request.context)} @@ -285,13 +284,24 @@ def version(): @app.route('/locate') def locate(): - bucket_name = app.current_request.query_params['bucket_name'] - bucket_map = collapse_bucket_configuration(get_yaml_file(conf_bucket, bucket_map_file, s3_resource)['MAP']) + query_params = app.current_request.query_params + if query_params is None or query_params.get('bucket_name') is None: + return Response(body='Required "bucket_name" query paramater not specified', + status_code=400, + headers={'Content-Type': 'text/plain'}) + bucket_name = app.current_request.query_params.get('bucket_name', None) + bucket_map = collapse_bucket_configuration(get_yaml_file(conf_bucket, + bucket_map_file, + s3_resource)['MAP']) search_map = flatdict.FlatDict(bucket_map, delimiter='/') matching_paths = [key for key, value in search_map.items() if value == bucket_name] if(len(matching_paths) > 0): - return json.dumps(matching_paths) - return Response(body=f'No route defined for {bucket_name}',status_code=404,headers={'Content-Type': 'text/plain'}) + return Response(body=json.dumps(matching_paths), + status_code=200, + headers={'Content-Type': 'application/json'}) + return Response(body=f'No route defined for {bucket_name}', + status_code=404, + headers={'Content-Type': 'text/plain'}) def collapse_bucket_configuration(bucket_map): @@ -301,7 +311,6 @@ def collapse_bucket_configuration(bucket_map): bucket_map[k] = v['bucket'] else: collapse_bucket_configuration(v) - return bucket_map From edf210ded3cd501b35aa19aa92bbb8899166df0e Mon Sep 17 00:00:00 2001 From: Ben Barton Date: Thu, 14 May 2020 16:14:21 -0800 Subject: [PATCH 64/65] PR-2392 latest rain-api-core --- rain-api-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rain-api-core b/rain-api-core index f3ec7baa..4b1372e2 160000 --- a/rain-api-core +++ b/rain-api-core @@ -1 +1 @@ -Subproject commit f3ec7baab4967c98f4e8fda144e2ec0071d62557 +Subproject commit 4b1372e2216ade19fdda539a0779c1a7fa82ef7b From e3c8b9aadd024b3829d1861f6cde0c69bee8ea41 Mon Sep 17 00:00:00 2001 From: Jonathan Kovarik Date: Fri, 5 Jun 2020 12:41:58 -0600 Subject: [PATCH 65/65] Updates TEA outputs to provide consistent internal API reference This is needed as the api_endpoint output provides the external facing endpoint, however we don't always want internal/deployment queries to hit cloudfront/external access solutions --- terraform/outputs.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 11027208..ab39c365 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -9,6 +9,10 @@ output "api_endpoint" { value = var.domain_name == null ? aws_cloudformation_stack.thin_egress_app.outputs.ApiEndpoint : "https://${var.domain_name}/" } +output "internal_api_endpoint" { + value = aws_cloudformation_stack.thin_egress_app.outputs.ApiEndpoint +} + output "urs_redirect_uri" { value = var.domain_name == null ? aws_cloudformation_stack.thin_egress_app.outputs.URSredirectURI : "https://${var.domain_name}/login" }