diff --git a/.github/workflows/concierge-graphql.yml b/.github/workflows/concierge-graphql.yml index 8fd8787..53c50ca 100644 --- a/.github/workflows/concierge-graphql.yml +++ b/.github/workflows/concierge-graphql.yml @@ -2,7 +2,7 @@ name: Concierge GraphQL on: push: - branches: ["*"] + branches: ["**"] workflow_dispatch: {} jobs: diff --git a/.github/workflows/gnm-style.yml b/.github/workflows/gnm-style.yml index d1cf704..4d43afa 100644 --- a/.github/workflows/gnm-style.yml +++ b/.github/workflows/gnm-style.yml @@ -2,7 +2,7 @@ name: Build and upload on: push: - branches: ["*"] + branches: ["**"] workflow_dispatch: {} jobs: @@ -19,22 +19,45 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: corretto + java-version: 11 + cache: sbt + + - name: Build and test + env: + SBT_JUNIT_OUTPUT: ./junit-tests + JAVA_OPTS: -Dsbt.log.noformat=true + run: | + sbt 'test;debian:packageBin' + + - name: Store the built artifacts + uses: actions/upload-artifact@v4 + with: + name: backend-deb + path: target/concierge-graphql_0.1.0_all.deb + retention-days: 5 + compression-level: 0 #artifact is already compressed + + cdk-build: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 20.x cache: 'yarn' cache-dependency-path: cdk/yarn.lock -# - run: yarn install --frozen-lockfile -# name: Prepare to build explorer -# working-directory: explorer -# -# - run: yarn build -# name: Build explorer -# working-directory: explorer - - run: yarn install --frozen-lockfile name: Prepare for CDK infrastructure build working-directory: cdk @@ -45,39 +68,109 @@ jobs: name: Build infrastructure definition from CDK working-directory: cdk - - name: Setup JDK - uses: actions/setup-java@v3 + - name: Store the built artifacts + uses: actions/upload-artifact@v4 with: - distribution: corretto - java-version: 11 - cache: sbt + name: cdk.out + path: cdk/cdk.out + retention-days: 5 + compression-level: 5 -# - name: Configure AWS Credentials -# uses: aws-actions/configure-aws-credentials@v1 -# with: -# aws-region: eu-west-1 -# role-to-assume: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} -# role-session-name: content-api-concierge-graphql-build + graphiql-explorer: + runs-on: ubuntu-latest - - name: Build and test - env: - SBT_JUNIT_OUTPUT: ./junit-tests - JAVA_OPTS: -Dsbt.log.noformat=true - run: | - sbt 'test;debian:packageBin' + # The first two permissions are needed to interact with GitHub's OIDC Token endpoint. + # The second set of three permissions are needed to write test results back to GH + permissions: + id-token: write + contents: read + issues: read + checks: write + packages: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 18.x + cache: 'yarn' + cache-dependency-path: explorer/yarn.lock + + - run: yarn install --frozen-lockfile + name: Prepare to build explorer + working-directory: explorer + + - run: yarn build + name: Build explorer + working-directory: explorer + + - name: Store the built artifacts + uses: actions/upload-artifact@v4 + with: + name: explorer + path: explorer/build + retention-days: 5 + compression-level: 5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push container + uses: docker/build-push-action@v5 + with: + push: true + platforms: linux/amd64,linux/arm64 + context: explorer + tags: ghcr.io/guardian/concierge-graphql/graphiql-explorer:${{ github.run_number }} + + riffraff-upload: + runs-on: ubuntu-latest + + needs: + - concierge-graphql-gnm + - graphiql-explorer + - cdk-build + # The first two permissions are needed to interact with GitHub's OIDC Token endpoint. + # The second set of three permissions are needed to write test results back to GH + permissions: + id-token: write + contents: read + issues: read + checks: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - run: ls -lh artifacts/* - uses: guardian/actions-riff-raff@v4 with: + roleArn: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} + githubToken: ${{ secrets.GITHUB_TOKEN }} configPath: ./riff-raff.yaml projectName: Content Platforms::concierge-graphql-experimental contentDirectories: | concierge-graphql: - - target/concierge-graphql_0.1.0_all.deb + - artifacts/backend-deb/concierge-graphql_0.1.0_all.deb + graphiql-explorer: + - artifacts/explorer cloudformation: - - cdk/cdk.out/ConciergeGraphql-PROD-AARDVARK.template.json - - cdk/cdk.out/ConciergeGraphql-CODE-AARDVARK.template.json + - artifacts/cdk.out/ConciergeGraphql-PROD-AARDVARK.template.json + - artifacts/cdk.out/ConciergeGraphql-CODE-AARDVARK.template.json cloudformation-preview: - - cdk/cdk.out/ConciergeGraphql-preview-PROD-AARDVARK.template.json - - cdk/cdk.out/ConciergeGraphql-preview-CODE-AARDVARK.template.json + - artifacts/cdk.out/ConciergeGraphql-preview-PROD-AARDVARK.template.json + - artifacts/cdk.out/ConciergeGraphql-preview-CODE-AARDVARK.template.json - name: Publish Unit Test Results uses: EnricoMi/publish-unit-test-result-action@v1 if: always() #runs even if there is a test failure diff --git a/.github/workflows/graphiql-explorer.yml b/.github/workflows/graphiql-explorer.yml deleted file mode 100644 index 1644ab1..0000000 --- a/.github/workflows/graphiql-explorer.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: GraphiQL explorer - -on: - push: - branches: ["*"] - workflow_dispatch: {} - -jobs: - graphiql-explorer: - runs-on: ubuntu-latest - - # The first two permissions are needed to interact with GitHub's OIDC Token endpoint. - # The second set of three permissions are needed to write test results back to GH - permissions: - id-token: write - contents: read - issues: read - checks: write - packages: write - pull-requests: write - - steps: - - uses: actions/checkout@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/setup-node@v3 - with: - node-version: 18.x - cache: 'yarn' - cache-dependency-path: explorer/yarn.lock - - - run: yarn install --frozen-lockfile - name: Prepare to build explorer - working-directory: explorer - - - run: yarn build - name: Build explorer - working-directory: explorer - - - name: Build and push - uses: docker/build-push-action@v5 - with: - push: true - platforms: linux/amd64,linux/arm64 - context: explorer - tags: ghcr.io/guardian/concierge-graphql/graphiql-explorer:${{ github.run_number }} \ No newline at end of file diff --git a/build.sbt b/build.sbt index a4c0a6f..2125f1d 100644 --- a/build.sbt +++ b/build.sbt @@ -68,8 +68,12 @@ lazy val root = (project in file(".")) "io.prometheus" % "simpleclient_common" % prometheusVersion, // test kit - "com.sksamuel.elastic4s" %% "elastic4s-testkit" % elastic4sVersion % "test" - ), + "com.sksamuel.elastic4s" %% "elastic4s-testkit" % elastic4sVersion % "test", + + // AWS auth + "software.amazon.awssdk" % "dynamodb" % "2.25.10", + "com.gu" %% "simple-configuration-ssm" % "1.7.0", +), dependencyOverrides ++= Seq( "com.fasterxml.jackson.core" % "jackson-databind" % "2.13.5", //required for json logging encoder "io.netty" % "netty-handler" % "4.1.94.Final", diff --git a/cdk/lib/__snapshots__/concierge-graphql.test.ts.snap b/cdk/lib/__snapshots__/concierge-graphql.test.ts.snap index 96d46b7..29dfbe6 100644 --- a/cdk/lib/__snapshots__/concierge-graphql.test.ts.snap +++ b/cdk/lib/__snapshots__/concierge-graphql.test.ts.snap @@ -6,6 +6,7 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "gu:cdk:constructs": [ "GuParameter", "GuParameter", + "GuPolicy", "GuPlayApp", "GuCertificate", "GuInstanceRole", @@ -26,10 +27,20 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "GuSecurityGroup", "GuSecurityGroup", "GuSecurityGroup", + "GuStringParameter", + "GuStringParameter", ], "gu:cdk:version": "TEST", }, "Outputs": { + "ExplorerDistroUrlOut91AD6AD6": { + "Value": { + "Fn::GetAtt": [ + "GraphiQLDistroDDB6F81C", + "DomainName", + ], + }, + }, "LoadBalancerConciergegraphqlDnsName": { "Description": "DNS entry for LoadBalancerConciergegraphql", "Value": { @@ -50,6 +61,11 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "Description": "SSM parameter containing the S3 bucket name holding distribution artifacts", "Type": "AWS::SSM::Parameter::Value", }, + "ExplorerCertArn": { + "Default": "/TEST/content-api/graphiql-explorer/GlobalCertArn", + "Description": "Cert to use for graphiql TEST. This must reside in us-east-1", + "Type": "AWS::SSM::Parameter::Value", + }, "LoggingStreamName": { "Default": "/account/services/logging.stream.name", "Description": "SSM parameter containing the Name (not ARN) on the kinesis stream", @@ -59,6 +75,11 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "Default": "/account/services/capi.gutools/TEST/hostedzoneid", "Type": "AWS::SSM::Parameter::Value", }, + "StaticBucketName": { + "Default": "/account/services/static.serving.bucket", + "Description": "SSM parameter giving the name of a bucket which is to be used for static hosting", + "Type": "AWS::SSM::Parameter::Value", + }, "subnets": { "Default": "/account/vpc/PROD-live/subnets", "Description": "Subnets to deploy into", @@ -71,6 +92,86 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` }, }, "Resources": { + "AuthTable0711E62F": { + "DeletionPolicy": "Retain", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "ApiKey", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "ApiKey", + "KeyType": "HASH", + }, + ], + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/concierge-graphql", + }, + { + "Key": "Stack", + "Value": "content-api", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + "AuthTableParam9777C3B3": { + "Properties": { + "Name": "/TEST/content-api/concierge-graphql/aws/auth_table", + "Tags": { + "Stack": "content-api", + "Stage": "TEST", + "gu:cdk:version": "TEST", + "gu:repo": "guardian/concierge-graphql", + }, + "Type": "String", + "Value": { + "Ref": "AuthTable0711E62F", + }, + }, + "Type": "AWS::SSM::Parameter", + }, + "AuthTablePolicy5F6B0D1E": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:GetItem", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "AuthTable0711E62F", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AuthTablePolicy5F6B0D1E", + "Roles": [ + { + "Ref": "InstanceRoleConciergegraphql96280BE9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "AutoScalingGroupConciergegraphqlASG7A20D011": { "Properties": { "HealthCheckGracePeriod": 120, @@ -141,10 +242,10 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "CertificateConciergegraphqlC301CD47": { "DeletionPolicy": "Retain", "Properties": { - "DomainName": "concierge-graphql-preview.content.code.dev-guardianapis.com", + "DomainName": "graphql-preview.internal.content.code.dev-guardianapis.com", "DomainValidationOptions": [ { - "DomainName": "concierge-graphql-preview.content.code.dev-guardianapis.com", + "DomainName": "graphql-preview.internal.content.code.dev-guardianapis.com", "HostedZoneId": { "Ref": "SsmParameterValueaccountservicescapigutoolsTESThostedzoneidC96584B6F00A464EAD1953AFF4B05118Parameter", }, @@ -209,8 +310,25 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` }, "GWApiGW36D3A369": { "Properties": { + "CorsConfiguration": { + "AllowCredentials": true, + "AllowHeaders": [ + "content-type", + "x-api-key", + ], + "AllowMethods": [ + "POST", + "GET", + "OPTIONS", + ], + "AllowOrigins": [ + "http://localhost:8081", + "https://graphiql.capi.test.dev-gutools.co.uk", + ], + "MaxAge": 300, + }, "Description": "Gateway for the TEST concierge-graphql instance", - "Name": "ApiGW", + "Name": "concierge-graphql-TEST", "ProtocolType": "HTTP", "Tags": { "Stack": "content-api", @@ -258,12 +376,7 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` }, "PayloadFormatVersion": "1.0", "TlsConfig": { - "ServerNameToVerify": { - "Fn::GetAtt": [ - "LoadBalancerConciergegraphql238A0C8B", - "DNSName", - ], - }, + "ServerNameToVerify": "graphql-preview.internal.content.code.dev-guardianapis.com", }, }, "Type": "AWS::ApiGatewayV2::Integration", @@ -295,16 +408,9 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` ], }, ], - "SubnetIds": [ - { - "Fn::Select": [ - 0, - { - "Ref": "subnets", - }, - ], - }, - ], + "SubnetIds": { + "Ref": "subnets", + }, "Tags": { "Stack": "content-api", "Stage": "TEST", @@ -314,44 +420,6 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` }, "Type": "AWS::ApiGatewayV2::VpcLink", }, - "GWGQLUsagePlanDDFEB42C": { - "Properties": { - "ApiStages": [ - { - "ApiId": { - "Ref": "GWApiGW36D3A369", - }, - "Stage": "$default", - "Throttle": { - "$default": { - "BurstLimit": 150, - "RateLimit": 50, - }, - }, - }, - ], - "Description": "Usage plan for access to concierge-graphql", - "Tags": [ - { - "Key": "gu:cdk:version", - "Value": "TEST", - }, - { - "Key": "gu:repo", - "Value": "guardian/concierge-graphql", - }, - { - "Key": "Stack", - "Value": "content-api", - }, - { - "Key": "Stage", - "Value": "TEST", - }, - ], - }, - "Type": "AWS::ApiGateway::UsagePlan", - }, "GetDistributablePolicyConciergegraphql510C52ED": { "Properties": { "PolicyDocument": { @@ -384,6 +452,105 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` }, "Type": "AWS::IAM::Policy", }, + "GraphiQLDistroDDB6F81C": { + "Properties": { + "DistributionConfig": { + "Aliases": [ + "graphiql.capi.test.dev-gutools.co.uk", + ], + "CustomErrorResponses": [ + { + "ErrorCachingMinTTL": 5, + "ErrorCode": 403, + "ResponseCode": 200, + "ResponsePagePath": "/index.html", + }, + ], + "DefaultCacheBehavior": { + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "Compress": true, + "TargetOriginId": "ConciergeGraphqlGraphiQLDistroOrigin1368BF828", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "DomainName": { + "Fn::Join": [ + "", + [ + { + "Ref": "StaticBucketName", + }, + ".s3.", + { + "Ref": "AWS::Region", + }, + ".", + { + "Ref": "AWS::URLSuffix", + }, + ], + ], + }, + "Id": "ConciergeGraphqlGraphiQLDistroOrigin1368BF828", + "OriginPath": "/TEST/graphiql-explorer", + "S3OriginConfig": { + "OriginAccessIdentity": { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + "Ref": "GraphiQLDistroOrigin1S3OriginE0C3987C", + }, + ], + ], + }, + }, + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "AcmCertificateArn": { + "Ref": "ExplorerCertArn", + }, + "MinimumProtocolVersion": "TLSv1.2_2021", + "SslSupportMethod": "sni-only", + }, + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/concierge-graphql", + }, + { + "Key": "Stack", + "Value": "content-api", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + }, + "Type": "AWS::CloudFront::Distribution", + }, + "GraphiQLDistroOrigin1S3OriginE0C3987C": { + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Identity for ConciergeGraphqlGraphiQLDistroOrigin1368BF828", + }, + }, + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + }, "GuHttpsEgressSecurityGroupConciergegraphql1855BF47": { "Properties": { "GroupDescription": "Allow all outbound HTTPS traffic", @@ -395,6 +562,13 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "IpProtocol": "tcp", "ToPort": 443, }, + { + "CidrIp": "10.0.0.0/8", + "Description": "Allow outgoing connections to Elasticsearch", + "FromPort": 9200, + "IpProtocol": "tcp", + "ToPort": 9200, + }, ], "Tags": [ { @@ -445,6 +619,27 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` }, "Type": "AWS::EC2::SecurityGroupIngress", }, + "GuHttpsEgressSecurityGroupConciergegraphqlfromConciergeGraphqlLinkageSGConciergegraphqlB4BBACD5900025BE65A9": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupConciergegraphql1855BF47", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LinkageSGConciergegraphql642E0A21", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, "GuHttpsEgressSecurityGroupConciergegraphqlfromConciergeGraphqlLoadBalancerConciergegraphqlSecurityGroup9A9241C7900098F0B65A": { "Properties": { "Description": "Load balancer to target", @@ -723,6 +918,12 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "GroupId", ], }, + { + "Fn::GetAtt": [ + "LinkageSGConciergegraphql642E0A21", + "GroupId", + ], + }, ], "Subnets": { "Ref": "subnets", @@ -1033,6 +1234,13 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` "IpProtocol": "tcp", "ToPort": 1515, }, + { + "CidrIp": "10.0.0.0/8", + "Description": "Allow outgoing connections to Elasticsearch", + "FromPort": 9200, + "IpProtocol": "tcp", + "ToPort": 9200, + }, ], "Tags": [ { @@ -1079,6 +1287,27 @@ exports[`The ConciergeGraphql stack matches the snapshot 1`] = ` }, "Type": "AWS::EC2::SecurityGroupIngress", }, + "WazuhSecurityGroupfromConciergeGraphqlLinkageSGConciergegraphqlB4BBACD590001E26A8A8": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "WazuhSecurityGroup", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LinkageSGConciergegraphql642E0A21", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, "WazuhSecurityGroupfromConciergeGraphqlLoadBalancerConciergegraphqlSecurityGroup9A9241C79000A2D85BDF": { "Properties": { "Description": "Load balancer to target", diff --git a/cdk/lib/concierge-graphql.ts b/cdk/lib/concierge-graphql.ts index b87cb41..c6db10c 100644 --- a/cdk/lib/concierge-graphql.ts +++ b/cdk/lib/concierge-graphql.ts @@ -1,23 +1,18 @@ import type {GuStackProps} from "@guardian/cdk/lib/constructs/core"; import {GuParameter, GuStack} from "@guardian/cdk/lib/constructs/core"; import type {App} from "aws-cdk-lib"; +import {aws_ssm} from "aws-cdk-lib"; import {GuPlayApp} from "@guardian/cdk"; -import { - InstanceClass, - InstanceSize, - InstanceType, - ISubnet, - Peer, - Port, - Subnet, - SubnetSelection, - Vpc -} from "aws-cdk-lib/aws-ec2"; +import {InstanceClass, InstanceSize, InstanceType, Peer, Port, Subnet, Vpc} from "aws-cdk-lib/aws-ec2"; import {AccessScope} from "@guardian/cdk/lib/constants"; -import {aws_ssm, Fn} from "aws-cdk-lib"; import {getHostName} from "./hostname"; -import {GuSecurityGroup} from "@guardian/cdk/lib/constructs/ec2"; -import {HttpGateway} from "./gateway"; +import {GuSecurityGroup, GuVpc} from "@guardian/cdk/lib/constructs/ec2"; +import {HttpGateway, ValidStages} from "./gateway"; +import {AttributeType, BillingMode, Table} from "aws-cdk-lib/aws-dynamodb"; +import {GuPolicy} from "@guardian/cdk/lib/constructs/iam"; +import {Effect, PolicyStatement} from "aws-cdk-lib/aws-iam"; +import {StringParameter} from "aws-cdk-lib/aws-ssm"; +import {GraphiqlExplorer} from "./graphiql-explorer"; export class ConciergeGraphql extends GuStack { constructor(scope: App, id: string, props: GuStackProps) { @@ -51,7 +46,22 @@ export class ConciergeGraphql extends GuStack { const hostedZoneId = aws_ssm.StringParameter.valueForStringParameter(this, `/account/services/capi.gutools/${this.stage}/hostedzoneid`); - const {loadBalancer, listener} = new GuPlayApp(this, { + const lbDomainName = getHostName(this, ".internal"); + + const authTable = new Table(this, "AuthTable", { + billingMode: BillingMode.PAY_PER_REQUEST, + partitionKey: { + name: "ApiKey", + type: AttributeType.STRING + } + }); + + new StringParameter(this, "AuthTableParam", { + parameterName: `/${this.stage}/${this.stack}/concierge-graphql/aws/auth_table`, + stringValue: authTable.tableName + }); + + const {loadBalancer, listener, autoScalingGroup} = new GuPlayApp(this, { access: { //You should put a gateway in front of this scope: AccessScope.INTERNAL, @@ -60,7 +70,7 @@ export class ConciergeGraphql extends GuStack { app: "concierge-graphql", certificateProps: { hostedZoneId, - domainName: getHostName(this), + domainName: lbDomainName, }, instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.LARGE), monitoringConfiguration: { @@ -81,6 +91,17 @@ export class ConciergeGraphql extends GuStack { executionStatement: "dpkg -i concierge-graphql/concierge-graphql_0.1.0_all.deb" } }, + roleConfiguration: { + additionalPolicies: [ + new GuPolicy(this, "AuthTablePolicy", { + statements: [ new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["dynamodb:GetItem"], + resources: [authTable.tableArn] + })] + }) + ] + }, vpc, }); @@ -88,21 +109,28 @@ export class ConciergeGraphql extends GuStack { app: props.app ?? "concierge-graphql", vpc, }); + loadBalancer.addSecurityGroup(linkageSG); + + const subnets = GuVpc.subnets(this, subnetsList.valueAsList); - // const subnets:SubnetSelection = { - // subnets: - // } new HttpGateway(this, "GW", { - stage: props.stage as "CODE"|"PROD", + stage: props.stage as ValidStages, backendLoadbalancer: loadBalancer, + lbDomainName, + previewMode, backendListener: listener, backendLbIncomingSg: linkageSG, subnets: { - subnets: this.subnetsFromTokenList(subnetsList.valueAsList, "DeploymentSubnet"), //subnetsList.valueAsList.map(sid=>Subnet.fromSubnetId(this, sid, sid)), + subnets, }, vpc }); + autoScalingGroup.connections.allowTo(Peer.ipv4("10.0.0.0/8"), Port.tcp(9200), "Allow outgoing connections to Elasticsearch"); + + new GraphiqlExplorer(this, "Explorer", { + appName: "graphiql-explorer" //needs to match the value in riff-raff.yaml + }) //OK - so this is a good idea and should really be in here. But it's damn fiddly so leaving it out for now. //The idea is we need a connection to the relevant Elasticsearch instance. So, we define a "connection" (which basically //to an egress rule) on our SG which allows egress to the remote ES SG. You still manually need to add a rule on the relevant @@ -137,9 +165,9 @@ export class ConciergeGraphql extends GuStack { getAccountPath(scope:GuStack, isPreview:boolean, elementName: string) { const basePath = "/account/vpc"; if(isPreview) { - return scope.stage=="CODE" ? `${basePath}/CODE-preview/${elementName}` : `${basePath}/PROD-preview/${elementName}`; + return scope.stage.startsWith("CODE") ? `${basePath}/CODE-preview/${elementName}` : `${basePath}/PROD-preview/${elementName}`; } else { - return scope.stage=="CODE" ? `${basePath}/CODE-live/${elementName}` : `${basePath}/PROD-live/${elementName}`; + return scope.stage.startsWith("CODE") ? `${basePath}/CODE-live/${elementName}` : `${basePath}/PROD-live/${elementName}`; } } @@ -151,13 +179,4 @@ export class ConciergeGraphql extends GuStack { return this.getAccountPath(scope, isPreview,"subnets") } - subnetsFromTokenList(list:string[],paramName:string):ISubnet[] { - let result:ISubnet[]=[]; - - for(let i=0;i = { + "PROD-AARDVARK": "graphiql.capi.gutools.co.uk", + "CODE-AARDVARK": "graphiql.capi.code.dev-gutools.co.uk", + "TEST": "graphiql.capi.test.dev-gutools.co.uk" +}; \ No newline at end of file diff --git a/cdk/lib/gateway.ts b/cdk/lib/gateway.ts index 43cbbb9..147d573 100644 --- a/cdk/lib/gateway.ts +++ b/cdk/lib/gateway.ts @@ -1,18 +1,23 @@ import {Construct} from "constructs"; -import type {GuStack, GuStackProps} from "@guardian/cdk/lib/constructs/core"; -import {HttpApi, VpcLink} from "aws-cdk-lib/aws-apigatewayv2" -import {HttpAlbIntegration, HttpUrlIntegration} from "aws-cdk-lib/aws-apigatewayv2-integrations"; -import {IApplicationLoadBalancer, IListener} from "aws-cdk-lib/aws-elasticloadbalancingv2"; -import {ISecurityGroup, IVpc, Peer, Port, SecurityGroup, SubnetSelection} from "aws-cdk-lib/aws-ec2"; +import type {GuStack} from "@guardian/cdk/lib/constructs/core"; +import {CorsHttpMethod, HttpApi, VpcLink} from "aws-cdk-lib/aws-apigatewayv2" +import {HttpAlbIntegration} from "aws-cdk-lib/aws-apigatewayv2-integrations"; +import {IApplicationLoadBalancer} from "aws-cdk-lib/aws-elasticloadbalancingv2"; +import {ISecurityGroup, IVpc, Peer, Port, SubnetSelection} from "aws-cdk-lib/aws-ec2"; import {GuSecurityGroup} from "@guardian/cdk/lib/constructs/ec2"; -import {CfnUsagePlan, UsagePlan} from "aws-cdk-lib/aws-apigateway"; import {IApplicationListener} from "aws-cdk-lib/aws-elasticloadbalancingv2/lib/alb/application-listener"; +import {Duration} from "aws-cdk-lib"; +import {hostingDomain} from "./constants"; + +export type ValidStages = "CODE-AARDVARK"|"PROD-AARDVARK"|"CODE-ZEBRA"|"PROD-ZEBRA"; interface HttpGatewayProps { - stage: "CODE"|"PROD"; + stage: ValidStages; + previewMode: boolean; backendLoadbalancer: IApplicationLoadBalancer; backendListener: IApplicationListener; backendLbIncomingSg: ISecurityGroup; + lbDomainName: string; subnets: SubnetSelection; vpc: IVpc; } @@ -40,30 +45,42 @@ export class HttpGateway extends Construct { vpcLinkName: `VpcLink-concierge-graphql-${props.stage}` }); + const maybePreview = props.previewMode ? "preview-" : ""; + const deployedUrl = hostingDomain[props.stage]; + const httpApi = new HttpApi(this, "ApiGW", { - description: `Gateway for the ${props.stage} concierge-graphql instance`, + apiName: `concierge-graphql-${maybePreview}${props.stage}`, + description: `Gateway for the ${props.stage} concierge-graphql${maybePreview} instance`, defaultIntegration: new HttpAlbIntegration('DefaultIntegration', props.backendListener, { vpcLink, - secureServerName: props.backendLoadbalancer.loadBalancerDnsName, + secureServerName: props.lbDomainName, }), + corsPreflight: { + allowOrigins: ['http://localhost:8081', `https://${deployedUrl}`], + allowMethods: [CorsHttpMethod.POST, CorsHttpMethod.GET, CorsHttpMethod.OPTIONS], + allowHeaders: ['content-type', 'x-api-key'], + maxAge: Duration.minutes(5), + allowCredentials: true + }, createDefaultStage: true, }); - - const plan = new CfnUsagePlan(this, "GQLUsagePlan", { - apiStages: [ - { - apiId: httpApi.apiId, - stage: httpApi.defaultStage?.stageName, - throttle: { - "$default": { - burstLimit: 150, - rateLimit: 50 - } - } - } - ], - description: "Usage plan for access to concierge-graphql" - }); + // + // const plan = new CfnUsagePlan(this, "GQLUsagePlan", { + // apiStages: [ + // { + // apiId: httpApi.apiId, + // stage: httpApi.defaultStage?.stageName, + // throttle: { + // "$default": { + // burstLimit: 150, + // rateLimit: 50 + // } + // } + // } + // ], + // description: "Usage plan for access to concierge-graphql" + // }); + // plan.node.addDependency(httpApi); } } \ No newline at end of file diff --git a/cdk/lib/graphiql-explorer.ts b/cdk/lib/graphiql-explorer.ts new file mode 100644 index 0000000..3051350 --- /dev/null +++ b/cdk/lib/graphiql-explorer.ts @@ -0,0 +1,85 @@ +import {Construct} from "constructs"; +import type { GuStack } from "@guardian/cdk/lib/constructs/core"; +import { GuStringParameter } from "@guardian/cdk/lib/constructs/core"; +import { CfnOutput, Duration } from "aws-cdk-lib"; +import { Certificate } from "aws-cdk-lib/aws-certificatemanager"; +import { + Distribution, + PriceClass, + ViewerProtocolPolicy +} from "aws-cdk-lib/aws-cloudfront"; +import { RestApiOrigin, S3Origin } from "aws-cdk-lib/aws-cloudfront-origins"; +import { Effect, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam"; +import { Bucket } from "aws-cdk-lib/aws-s3"; +import {hostingDomain} from "./constants"; + +interface GraphiqlExplorerProps { + appName: string; + +} + +export class GraphiqlExplorer extends Construct { + constructor(scope: GuStack, id: string, props: GraphiqlExplorerProps) { + super(scope, id); + + //We can't create the cert here, because it must live in us-east-1 for Cloudfront to use it. + const hostingCertArn = new GuStringParameter(scope, "ExplorerCertArn", { + fromSSM: true, + default: `/${scope.stage}/${scope.stack}/${props.appName}/GlobalCertArn`, + description: `Cert to use for graphiql ${scope.stage}. This must reside in us-east-1`, + }); + const certificate = Certificate.fromCertificateArn(this, "CapiExplorerCert", hostingCertArn.valueAsString); + + const staticBucketNameParam = new GuStringParameter(scope, "StaticBucketName", { + fromSSM: true, + default: `/account/services/static.serving.bucket`, + description: "SSM parameter giving the name of a bucket which is to be used for static hosting" + }); + + const hostingBucket = Bucket.fromBucketName(this, "StaticBucket", staticBucketNameParam.valueAsString); + + const distro = new Distribution(scope, "GraphiQLDistro", { + defaultRootObject: "index.html", + certificate, + domainNames: [hostingDomain[scope.stage]], + defaultBehavior: { + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + origin: new S3Origin(hostingBucket, { + originPath: `${scope.stage}/${props.appName}` + }), + }, + enableIpv6: true, + enabled: true, + priceClass: PriceClass.PRICE_CLASS_100, //US & EU only + /* + we must tell Cloudfront to redirect 403 (forbidden/not present) exceptions from S3 into a 200 response from /index in order for react-router to work. + */ + errorResponses: [ + { + httpStatus: 403, + responseHttpStatus: 200, + responsePagePath: "/index.html", + ttl: Duration.seconds(5), + } + ] + }); + + hostingBucket.addToResourcePolicy(new PolicyStatement({ + effect: Effect.ALLOW, + principals: [ + new ServicePrincipal("cloudfront.amazonaws.com"), + ], + actions: ["s3:GetObject"], + resources: [`arn:aws:s3:::${hostingBucket.bucketName}/${scope.stage}/${props.appName}`], + conditions: { + "StringEquals": { + "AWS:SourceArn": `arn:aws:cloudfront::${scope.account}:distribution/${distro.distributionId}` + } + } + })); + + new CfnOutput(this, "DistroUrlOut", { + value: distro.distributionDomainName, + }); + } +} \ No newline at end of file diff --git a/cdk/lib/hostname.ts b/cdk/lib/hostname.ts index 88c1b55..fe84260 100644 --- a/cdk/lib/hostname.ts +++ b/cdk/lib/hostname.ts @@ -1,20 +1,20 @@ import {GuStack} from "@guardian/cdk/lib/constructs/core"; -export function getHostName(scope:GuStack):string { +export function getHostName(scope:GuStack, insert?:string):string { if(scope.stage.startsWith("CODE")) { if(scope.stack.endsWith("preview")) { - return "concierge-graphql-preview.content.code.dev-guardianapis.com"; + return `graphql-preview${insert ?? ""}.content.code.dev-guardianapis.com`; } else { - return "concierge-graphql.content.code.dev-guardianapis.com"; + return `graphql${insert ?? ""}.content.code.dev-guardianapis.com`; } } else if(scope.stage.startsWith("PROD")) { if (scope.stack.endsWith("preview")) { - return "concierge-graphql-preview.content.guardianapis.com"; + return `graphql-preview${insert ?? ""}.content.guardianapis.com`; } else { - return "concierge-graphql.content.guardianapis.com"; + return `graphql${insert ?? ""}.content.guardianapis.com`; } } else if(scope.stage=="TEST") { //CI testing - return "concierge-graphql-preview.content.code.dev-guardianapis.com"; + return `graphql-preview${insert ?? ""}.content.code.dev-guardianapis.com`; } else { throw "stage must be either CODE or PROD"; } diff --git a/explorer/package.json b/explorer/package.json index ed439e7..a55cdd3 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -11,19 +11,25 @@ "private": true, "dependencies": { "@codemirror/language": "6.0.0", + "@emotion/react": "^11.11.1", + "@guardian/source-foundations": "^14.1.4", + "@guardian/source-react-components": "^22.0.2", + "@types/react-helmet": "^6.1.11", "graphiql": "^2.4.7", "graphql": "^16.8.1", "graphql-ws": "^5.14.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-is": "^18.2.0" + "react-helmet": "^6.1.0", + "react-is": "^18.2.0", + "tslib": "^2.6.2" }, "devDependencies": { "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", + "browserify-sign": "^4.2.2", "buffer": "^6.0.3", "crypto-browserify": "^3.12.0", - "browserify-sign": "^4.2.2", "css-loader": "^6.8.1", "html-webpack-plugin": "^5.5.3", "process": "^0.11.10", diff --git a/explorer/src/LoginForm.tsx b/explorer/src/LoginForm.tsx new file mode 100644 index 0000000..f9f7301 --- /dev/null +++ b/explorer/src/LoginForm.tsx @@ -0,0 +1,140 @@ +import React, {useEffect, useMemo, useState} from "react"; +import {GraphiQL} from "graphiql"; +import { createGraphiQLFetcher, Fetcher } from '@graphiql/toolkit'; +import {css} from "@emotion/react"; +import 'graphiql/graphiql.css'; +import {Accordion, AccordionRow,Button, TextInput} from "@guardian/source-react-components"; +import {info} from "sass"; + +const loginContainer = css` + margin: auto; + width: fit-content; + height: fit-content; + display: flex; + flex-direction: column; +`; + +const loginElement = css` + align-self: center; +` +const inputStyle = css` + max-width: 800px; + width: 800px; +`; + +const buttonContainer = css` + display: flex; + margin-top: 1em; + width: 100%; + flex-direction: row; + justify-content: space-evenly; +`; + +const infoArea = css` + margin-top: 2em; +`; + +const accordionContent = css` + width: 40%; + margin-left: auto; + margin-right: auto; +`; +export const LoginForm:React.FC = () => { + const defaultBaseUrl = localStorage.getItem("CapiGQLBase") ?? "https://"; + const defaultApiKey = localStorage.getItem("CapiGQLKey") ?? ""; + const [haveCachedLogin, setHaveCachedLogin] = useState(!!(localStorage.getItem("CapiGQLBase") && localStorage.getItem("CapiGQLKey"))); + + const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); + const [apiKey, setApiKey] = useState(defaultApiKey); + const [readyToGo, setReadyToGo] = useState(false); + const [urlIsValid, setUrlIsValid] = useState(false); + const urlValidator = /^https?:\/\/[\w-:.]+$/; + + useEffect(() => { + setUrlIsValid(urlValidator.test(baseUrl)); + }, [baseUrl]); + + const clearCache = () => { + localStorage.removeItem("CapiGQLBase"); + localStorage.removeItem("CapiGQLKey"); + setBaseUrl("https://"); + setApiKey(""); + setHaveCachedLogin(false); + } + + const fetcher = useMemo(()=>{ + if(readyToGo) { + localStorage.setItem("CapiGQLBase", baseUrl); + localStorage.setItem("CapiGQLKey", apiKey); + + return createGraphiQLFetcher({url: `${baseUrl}/query`, headers: {'X-Api-Key': apiKey}}); + } else { + return null; + } + }, [readyToGo]); + + + return fetcher && readyToGo ? + : +
+
+

Please log in

+
+
+ setBaseUrl(evt.target.value)}/> +
+
+ setApiKey(evt.target.value)} + /> +
+
+
+ +
+
+ +
+
+ + +
+

We are experimenting with using GraphQL to query our content and possibly other things

+

This user interface will let you into the GraphiQL editor to explore the data currently available + (see https://github.com/graphql/graphiql/blob/main/packages/graphiql/README.md +  for more information on the GraphiQL user interface) +

+

+ At present, it's only possible to query the development/staging environment. +

+

+ You need to be issued with an API key in order to be allowed access to this system - this is not the same +  as a standard CAPI API key. +

+
+
+ +
+

+ You must contact the team directly. At present, that means talking to Andy Gallagher + and/or + Jon Herbert in the Content Pipeline team. +

+
+
+
+
+} \ No newline at end of file diff --git a/explorer/src/main.tsx b/explorer/src/main.tsx index 54294e1..733b051 100644 --- a/explorer/src/main.tsx +++ b/explorer/src/main.tsx @@ -1,17 +1,19 @@ -import { createGraphiQLFetcher } from '@graphiql/toolkit'; -import { GraphiQL } from 'graphiql'; import React from 'react'; import ReactDOM from 'react-dom'; +import {Helmet} from 'react-helmet'; -import 'graphiql/graphiql.css'; - -const fetcher = createGraphiQLFetcher({ url: "/query", headers: {'X-Consumer-Username': ":internal"} }); +import {LoginForm} from "./LoginForm"; const rootElem = document.createElement('div'); rootElem.setAttribute("style","height: 100vh"); document.body.append(rootElem) ReactDOM.render( - , + <> + + CAPI GraphQL Experiments + + + , rootElem, ); diff --git a/explorer/tsconfig.json b/explorer/tsconfig.json index 9d7fbc7..162ae22 100644 --- a/explorer/tsconfig.json +++ b/explorer/tsconfig.json @@ -2,13 +2,14 @@ "compilerOptions": { "allowJs": true, "esModuleInterop": true, - "jsx": "react", "module": "ES2015", "moduleResolution": "node", "sourceMap": true, "strict": true, "isolatedModules": true, - "target": "ES2016" + "target": "ES2016", + "jsx": "react-jsx", + "jsxImportSource": "@emotion/react" }, "include": ["./src/", "__tests__", "types.d.ts"], "exclude": ["node_modules"] diff --git a/explorer/yarn.lock b/explorer/yarn.lock index 67306b0..4e357c0 100644 --- a/explorer/yarn.lock +++ b/explorer/yarn.lock @@ -2,6 +2,40 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + +"@babel/helper-module-imports@^7.16.7": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" @@ -9,6 +43,22 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" + integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/types@^7.22.15": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" + integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@codemirror/language@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.0.0.tgz#f590558447c01f430fb3ef3297c41b8cd3ae9190" @@ -40,6 +90,94 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@emotion/babel-plugin@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" + integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/serialize" "^1.1.2" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" + +"@emotion/hash@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" + integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== + +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + +"@emotion/react@^11.11.1": + version "11.11.4" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d" + integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/cache" "^11.11.0" + "@emotion/serialize" "^1.1.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.3.tgz#84b77bfcfe3b7bb47d326602f640ccfcacd5ffb0" + integrity sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" + +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + +"@emotion/unitless@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== + +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== + +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + "@graphiql/react@^0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.17.6.tgz#54e3745f74ccf5cd69540aecc9dbcd15a7e28c1c" @@ -68,6 +206,18 @@ "@n1ru4l/push-pull-async-iterable-iterator" "^3.1.0" meros "^1.1.4" +"@guardian/source-foundations@^14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@guardian/source-foundations/-/source-foundations-14.1.4.tgz#157b875b81abe0c376b6992505fa76773e355538" + integrity sha512-SHkFVBxsE2dSNTKfzmGY1hD9BA7qJ2+bGY1plrUJlYJBCRQdno/YuNummO+wm0Q+kMgxRT0iz5md2DjKYERzQw== + dependencies: + mini-svg-data-uri "1.4.4" + +"@guardian/source-react-components@^22.0.2": + version "22.0.2" + resolved "https://registry.yarnpkg.com/@guardian/source-react-components/-/source-react-components-22.0.2.tgz#3650d828b5d3c4c6ba44a79bc795e60793d9f79c" + integrity sha512-B6wdpnCKyfT410PZyZIKy7L4UB+L/8Qau/eHd4p1fYN9+SGdRenJ6qMaiIod/tIRsPzHP044N7mPyTKI6fU82w== + "@jridgewell/gen-mapping@^0.3.0": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -402,6 +552,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe" integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg== +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + "@types/prop-types@*": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" @@ -424,6 +579,13 @@ dependencies: "@types/react" "*" +"@types/react-helmet@^6.1.11": + version "6.1.11" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.11.tgz#8cafcafff38f75361f451563ba7b406b0c5d3907" + integrity sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^18.2.13": version "18.2.13" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.13.tgz#a98c09bde8b18f80021935b11d2d29ef5f4dcb2f" @@ -699,6 +861,13 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -744,6 +913,15 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -942,6 +1120,11 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + camel-case@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" @@ -955,6 +1138,15 @@ caniuse-lite@^1.0.30001503: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001507.tgz#fae53f6286e7564783eadea9b447819410a59534" integrity sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A== +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -1024,6 +1216,13 @@ codemirror@^5.65.3: resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.13.tgz#c098a6f409db8b5a7c5722788bd9fa3bb2367f2e" integrity sha512-SVWEzKXmbHmTQQWaz03Shrh4nybG0wXx2MEu3FO4ezbPW8IbnZEd5iGHGEffSUaitKYa3i+pHpBsSvw8sPHtzg== +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1031,6 +1230,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -1098,6 +1302,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -1120,6 +1329,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -1393,6 +1613,13 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.9.0.tgz#47594a13081be0d9be6e513534e8c58dbb26c7a1" integrity sha512-RODB4txU+xImYDemN5DqaKC0CHk05XSVkOX4pq0hK26Qx+1LChkuOyUDlGEjYb3ACr0n9qBhFjg37hQuJvpkRQ== +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-module-lexer@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" @@ -1408,6 +1635,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -1550,6 +1787,11 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -1607,6 +1849,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.3: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" @@ -1696,6 +1943,11 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -1742,6 +1994,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -1756,6 +2015,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -1883,6 +2149,14 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1939,6 +2213,11 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1958,6 +2237,13 @@ is-core-module@^2.11.0: dependencies: has "^1.0.3" +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -2051,12 +2337,12 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" -"js-tokens@^3.0.0 || ^4.0.0": +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -json-parse-even-better-errors@^2.3.1: +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -2084,6 +2370,11 @@ launch-editor@^2.6.0: picocolors "^1.0.0" shell-quote "^1.7.3" +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + linkify-it@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" @@ -2224,6 +2515,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mini-svg-data-uri@1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" + integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -2406,6 +2702,13 @@ param-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + parse-asn1@^5.0.0, parse-asn1@^5.1.5, parse-asn1@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" @@ -2417,6 +2720,16 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5, parse-asn1@^5.1.6: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -2455,6 +2768,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + pbkdf2@^3.0.3: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" @@ -2637,6 +2955,11 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-fast-compare@^3.1.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + react-focus-lock@^2.5.2: version "2.9.4" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.4.tgz#4753f6dcd167c39050c9d84f9c63c71b3ff8462e" @@ -2649,7 +2972,17 @@ react-focus-lock@^2.5.2: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-is@^16.13.1: +react-helmet@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" + integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.1.1" + react-side-effect "^2.1.0" + +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -2678,6 +3011,11 @@ react-remove-scroll@^2.4.3: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-side-effect@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" + integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -2735,6 +3073,11 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -2768,11 +3111,25 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve@^1.19.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.20.0: version "1.22.2" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" @@ -3013,6 +3370,11 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + source-map@^0.6.0, source-map@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -3095,6 +3457,18 @@ style-mod@^4.0.0: resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.3.tgz#136c4abc905f82a866a18b39df4dc08ec762b1ad" integrity sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw== +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -3155,6 +3529,11 @@ tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -3187,6 +3566,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -3462,3 +3846,8 @@ yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== diff --git a/riff-raff.yaml b/riff-raff.yaml index 3cf5936..0230c11 100644 --- a/riff-raff.yaml +++ b/riff-raff.yaml @@ -1,5 +1,10 @@ regions: - eu-west-1 +allowedStages: + - CODE-AARDVARK + - PROD-AARDVARK + - CODE-ZEBRA + - PROD-ZEBRA deployments: cloudformation: type: cloud-formation @@ -16,26 +21,42 @@ deployments: Recipe: ubuntu-focal-capi-arm-jdk11 AmigoStage: PROD BuiltBy: amigo - cloudformation-preview: - type: cloud-formation - app: concierge-graphql - stacks: - - content-api-preview - parameters: - templateStagePaths: - CODE-AARDVARK: ConciergeGraphql-preview-CODE-AARDVARK.template.json - PROD-AARDVARK: ConciergeGraphql-preview-PROD-AARDVARK.template.json - amiParameter: AMIConciergegraphql - amiEncrypted: true - amiTags: - Recipe: ubuntu-focal-capi-arm-jdk11 - AmigoStage: PROD - BuiltBy: amigo +# cloudformation-preview: +# type: cloud-formation +# app: concierge-graphql +# stacks: +# - content-api-preview +# parameters: +# templateStagePaths: +# CODE-AARDVARK: ConciergeGraphql-preview-CODE-AARDVARK.template.json +# PROD-AARDVARK: ConciergeGraphql-preview-PROD-AARDVARK.template.json +# amiParameter: AMIConciergegraphql +# amiEncrypted: true +# amiTags: +# Recipe: ubuntu-focal-capi-arm-jdk11 +# AmigoStage: PROD +# BuiltBy: amigo concierge-graphql: type: autoscaling stacks: - content-api - - content-api-preview +# - content-api-preview parameters: bucketSsmLookup: true - dependencies: [ cloudformation, cloudformation-preview ] \ No newline at end of file + dependencies: [ cloudformation ] +# dependencies: [ cloudformation, cloudformation-preview ] + + graphiql-explorer: + type: aws-s3 + stacks: + - content-api #we don't need multiple stacks here + parameters: + bucketSsmKey: /account/services/static.serving.bucket + cacheControl: + - pattern: ".*.html$" + value: "public, max-age=60" + - pattern: ".*" + value: "public, max-age=3600" + publicReadAcl: true + prefixStack: false + dependencies: ["cloudformation"] \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 3c3f1aa..18c21d1 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -14,10 +14,11 @@ - + + - - + + \ No newline at end of file diff --git a/src/main/scala/ElasticSearchResolver.scala b/src/main/scala/ElasticSearchResolver.scala index 5df2feb..3bd8062 100644 --- a/src/main/scala/ElasticSearchResolver.scala +++ b/src/main/scala/ElasticSearchResolver.scala @@ -1,8 +1,20 @@ import com.sksamuel.elastic4s.ElasticNodeEndpoint +import com.typesafe.config.Config +import org.slf4j.LoggerFactory + +import java.net.URL +import scala.util.Try object ElasticSearchResolver { - private def local = Option(ElasticNodeEndpoint("http","localhost",9200, None)) + private val logger = LoggerFactory.getLogger(getClass) + + private def local = { + logger.info("No elasticsearch URL found, defaulting to localhost") + Option(ElasticNodeEndpoint("http","localhost",9200, None)) + } + private def fromEnvironment = { + logger.info("Trying to find elasticsearch URL from environment vars") Option(System.getenv("ELASTICSEARCH_HOST")).map(host=>{ val proto = if(System.getenv("ELASTICSEARCH_HTTPS")==null) "http" else "https" val port = Option(System.getenv("ELASTICSEARCH_PORT")).map(_.toInt).getOrElse(9200) @@ -10,7 +22,18 @@ object ElasticSearchResolver { }) } - def resolve():ElasticNodeEndpoint = { - fromEnvironment orElse local + private def fromConfig(config:Config) = { + logger.info("Trying to find elasticsearch URL from config...") + Try { + val urlString = config.getString("elasticsearch.url") + val url = new URL(urlString) + val result = ElasticNodeEndpoint(url.getProtocol, url.getHost, url.getPort, None) + logger.info(s"Got Elasticsearch URL ${url.toString} from config") + result + }.toOption + } + + def resolve(config:Config):ElasticNodeEndpoint = { + fromConfig(config) orElse fromEnvironment orElse local }.get } diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala index 0075d20..2f3b790 100644 --- a/src/main/scala/Main.scala +++ b/src/main/scala/Main.scala @@ -1,7 +1,5 @@ import cats.effect._ -import cats.effect.unsafe.implicits.global import com.comcast.ip4s.{IpLiteralSyntax, Ipv4Address, Ipv6Address} -import com.sksamuel.elastic4s.ElasticNodeEndpoint import datastore.ElasticsearchRepo import io.prometheus.client.hotspot.DefaultExports import org.http4s._ @@ -10,26 +8,32 @@ import org.http4s.server.Router import org.http4s.ember.server._ import org.http4s.implicits._ import org.slf4j.LoggerFactory -import security.Security.limitByTier -import security.{DeveloperTier, InternalTier, UserTier} +import security.{ApiKeyAuth, DeveloperTier, InternalTier, Security, UserTier} import internalmetrics.PrometheusMetrics import scala.concurrent.duration._ +import utils.Config.fetchConfig object Main extends IOApp { private val logger = LoggerFactory.getLogger(getClass) + val config = fetchConfig().get //it's OK to throw exception here, that will then block startup + DefaultExports.initialize() - val documentRepo = new ElasticsearchRepo(ElasticSearchResolver.resolve()) + val documentRepo = new ElasticsearchRepo(ElasticSearchResolver.resolve(config)) val server = new GraphQLServer(documentRepo) + private val security = Security(config) + val graphqlService = HttpRoutes.of[IO] { case OPTIONS -> Root / "query" => IO(Response( Status.Ok, - headers=Headers("Access-Control-Allow-Origin" -> "*", "Access-Control-Allow-Methods"->"POST, GET, OPTIONS", "Access-Control-Allow-Headers" -> s"Content-Type, ${security.KongHeader.name}") + headers=Headers("Access-Control-Allow-Origin" -> "http://localhost:8081", + "Access-Control-Allow-Methods"->"POST, GET, OPTIONS", + "Access-Control-Allow-Headers" -> s"Content-Type, ${ApiKeyAuth.name.toString}") )) case req @ POST -> Root / "query" => - limitByTier(req, DeveloperTier) { tier=> + security.limitByTier(req, DeveloperTier) { tier=> server.handleRequest(req, tier) .compile .onlyOrError diff --git a/src/main/scala/security/ApiKeyAuth.scala b/src/main/scala/security/ApiKeyAuth.scala new file mode 100644 index 0000000..4b9b6ae --- /dev/null +++ b/src/main/scala/security/ApiKeyAuth.scala @@ -0,0 +1,108 @@ +package security + +import cats.effect.IO +import org.http4s.Request +import org.slf4j.LoggerFactory +import org.typelevel.ci.CIString +import software.amazon.awssdk.auth.credentials.{AwsCredentialsProviderChain, EnvironmentVariableCredentialsProvider, InstanceProfileCredentialsProvider, ProfileCredentialsProvider, SystemPropertyCredentialsProvider} +import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import software.amazon.awssdk.services.dynamodb.model.{AttributeValue, GetItemRequest} +import utils.AWSUtils +import java.time.{Duration, Instant} +import java.util.{Timer, TimerTask} +import scala.collection.SortedSet +import scala.concurrent.duration.FiniteDuration +import scala.jdk.CollectionConverters._ + +case class CacheTrackingEntry(key:String, time:Instant) + +object CacheTrackingEntry { + implicit val ordering:Ordering[CacheTrackingEntry] = + (x: CacheTrackingEntry, y: CacheTrackingEntry) => if ((x.time.toEpochMilli - y.time.toEpochMilli) < 0) { + -1 + } else if (x.time.toEpochMilli == y.time.toEpochMilli) { + 0 + } else { + 1 + } +} + +class ApiKeyAuth(dynamoDbClient:DynamoDbClient, tableName:String, cachingTtl:FiniteDuration) { + private val logger = LoggerFactory.getLogger(getClass) + logger.info(s"AWS dynamodb auth initialised. TableName is $tableName") + + private var localCache:Map[String,UserTier] = Map() + + private var cacheTracking:SortedSet[CacheTrackingEntry] = SortedSet()(CacheTrackingEntry.ordering) + + private val timer = new Timer() + + timer.schedule(new TimerTask { + override def run(): Unit = { + val cutoff = Instant.now().minus(cachingTtl.length, cachingTtl.unit.toChronoUnit) + this.synchronized { + cacheTracking = cacheTracking.filter(e=>e.time.isAfter(cutoff)) + } + } + }, 1000L, 1000L) + + def extractUserTier(req:Request[IO]):Option[UserTier] = { + req.headers + .get(ApiKeyAuth.name) + .flatMap(keyValue=>{ + lookUpInCache(keyValue.head.value) match { + case Some(tier)=>Some(tier) + case None=> + for { + tierName <- lookUpKey(keyValue.head.value) + tier <- UserTier(tierName) + _ = updateCache(keyValue.head.value, tier) + } yield tier + } + }) + } + + private def lookUpInCache(keyValue:String):Option[UserTier] = this.synchronized { + localCache.get(keyValue) + } + + private def updateCache(keyValue:String, userTier:UserTier):Unit = this.synchronized { + localCache = localCache + (keyValue -> userTier) + cacheTracking = cacheTracking + CacheTrackingEntry(keyValue, Instant.now()) + } + + /** + * Internal method to look up a key in the backing store + * @param keyValue + * @return + */ + private def lookUpKey(keyValue:String):Option[String] = { + try { + val response = dynamoDbClient.getItem(GetItemRequest.builder() + .tableName(tableName) + .key(Map("ApiKey"->AttributeValue.fromS(keyValue)).asJava) + .build() + ) + + for { + maybeItem <- Option(response.item()) + itemAsScala = maybeItem.asScala + attributeValue <- itemAsScala.get("UserTier") + tier = attributeValue.s() + } yield tier + } catch { + case err:Throwable=> + logger.error(s"Unable to verify API key ${keyValue}: ${err.getMessage}", err) + None + } + } +} + +object ApiKeyAuth { + val name = CIString("X-Api-Key") + + def apply(cachingTtl: FiniteDuration, authTable: String): ApiKeyAuth = { + val ddbClient = DynamoDbClient.builder().credentialsProvider(AWSUtils.credsProvider).build() + new ApiKeyAuth(ddbClient, authTable, cachingTtl) + } +} \ No newline at end of file diff --git a/src/main/scala/security/Authenticator.scala b/src/main/scala/security/Authenticator.scala new file mode 100644 index 0000000..621595e --- /dev/null +++ b/src/main/scala/security/Authenticator.scala @@ -0,0 +1,8 @@ +package security + +import cats.effect.IO +import org.http4s.{Request, Response} + +trait Authenticator { + def limitByTier(req:Request[IO], minTier:UserTier)(protectedCb: UserTier => IO[Response[IO]]):IO[Response[IO]] +} diff --git a/src/main/scala/security/KongHeader.scala b/src/main/scala/security/KongHeader.scala index c6a0c9a..a322314 100644 --- a/src/main/scala/security/KongHeader.scala +++ b/src/main/scala/security/KongHeader.scala @@ -4,7 +4,7 @@ import cats.effect.IO import org.http4s.Request import org.typelevel.ci.CIString -object KongHeader { +object KongHeader { val name = CIString("X-Consumer-Username") /** diff --git a/src/main/scala/security/Security.scala b/src/main/scala/security/Security.scala index de202a1..dd8a9df 100644 --- a/src/main/scala/security/Security.scala +++ b/src/main/scala/security/Security.scala @@ -1,15 +1,20 @@ package security import cats.effect.IO +import com.typesafe.config.Config import org.http4s.{Request, Response} import org.http4s.dsl.io._ +import org.slf4j.LoggerFactory -object Security { - class PermissionDeniedException extends Exception +import java.util.concurrent.TimeUnit +import scala.concurrent.duration._ +class Security(private val auth:ApiKeyAuth) extends Authenticator { + private val logger = LoggerFactory.getLogger(getClass) def limitByTier(req:Request[IO], minTier:UserTier)(protectedCb: UserTier => IO[Response[IO]]):IO[Response[IO]] = { - KongHeader.extractUserTier(req) match { + auth.extractUserTier(req) match { case Some(tier)=> + logger.debug(s"User tier is $tier") if(tier < minTier) { Forbidden("Currently only internal-tier keys are allowed access") } else { @@ -19,5 +24,14 @@ object Security { Forbidden("You must have an API key to access this resource") } } - } + +object Security { + class PermissionDeniedException extends Exception + + val authCacheTtl = 5.minutes + def apply(config:Config) = { + val auth = ApiKeyAuth(authCacheTtl, config.getString("aws.auth_table")) + new Security(auth) + } +} \ No newline at end of file diff --git a/src/main/scala/utils/AWSUtils.scala b/src/main/scala/utils/AWSUtils.scala new file mode 100644 index 0000000..522aabc --- /dev/null +++ b/src/main/scala/utils/AWSUtils.scala @@ -0,0 +1,12 @@ +package utils + +import software.amazon.awssdk.auth.credentials._ + +object AWSUtils { + val credsProvider: AwsCredentialsProviderChain = AwsCredentialsProviderChain.builder().credentialsProviders( + InstanceProfileCredentialsProvider.create(), + ProfileCredentialsProvider.create("capi"), + SystemPropertyCredentialsProvider.create(), + EnvironmentVariableCredentialsProvider.create() + ).build() +} diff --git a/src/main/scala/utils/Config.scala b/src/main/scala/utils/Config.scala new file mode 100644 index 0000000..dc1277c --- /dev/null +++ b/src/main/scala/utils/Config.scala @@ -0,0 +1,24 @@ +package utils + +import com.gu.conf.{ConfigurationLoader, SSMConfigurationLocation} +import com.gu.{AppIdentity, AwsIdentity, DevIdentity} +import com.typesafe.config.Config + +import scala.util.{Success, Try} + +object Config { + def fetchConfig():Try[Config] = { + val CredentialsProvider = AWSUtils.credsProvider + val isDev = Option(System.getenv("DEV_MODE")).isDefined || Option(System.getProperty("DEV_MODE")).isDefined + + for { + identity <- if (isDev) + Success(DevIdentity("concierge-graphql")) + else + AppIdentity.whoAmI(defaultAppName = "concierge-graphql", CredentialsProvider) + config <- Try(ConfigurationLoader.load(identity, CredentialsProvider) { + case identity: AwsIdentity => SSMConfigurationLocation.default(identity) + }) + } yield config + } +} diff --git a/src/test/scala/security/ApiKeyAuthSpec.scala b/src/test/scala/security/ApiKeyAuthSpec.scala new file mode 100644 index 0000000..e88ca19 --- /dev/null +++ b/src/test/scala/security/ApiKeyAuthSpec.scala @@ -0,0 +1,57 @@ +package security + +import cats.effect.IO +import org.http4s._ +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.{times, verify, when} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.scalatestplus.mockito.MockitoSugar +import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import software.amazon.awssdk.services.dynamodb.model.{AttributeValue, GetItemRequest, GetItemResponse} + +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +class ApiKeyAuthSpec extends AnyFlatSpec with Matchers with MockitoSugar { + "APIKeyAuth.extractUserTier" should "look up the incoming key in Dynamo just once, then cache it" in { + val mockClient = mock[DynamoDbClient] + when(mockClient.getItem(any[GetItemRequest])).thenReturn(GetItemResponse.builder() + .item(Map("UserTier" -> AttributeValue.fromS("internal")).asJava) + .build() + ) + + val toTest = new ApiKeyAuth(mockClient, "", 5.seconds) + val req = Request[IO](method=Method.POST,headers = Headers("X-Api-Key"->"some-key")) + val result = toTest.extractUserTier(req) + result shouldEqual Some(InternalTier) + + val secondResult = toTest.extractUserTier(req) + secondResult shouldEqual Some(InternalTier) + + verify(mockClient, times(1)).getItem(GetItemRequest.builder() + .tableName("") + .key(Map("ApiKey"->AttributeValue.fromS("some-key")).asJava) + .build() + ) + } + + "APIKeyAuth.extractUserTier" should "return None if the given key does not exist" in { + val mockClient = mock[DynamoDbClient] + when(mockClient.getItem(any[GetItemRequest])).thenReturn(GetItemResponse.builder().build()) + + val toTest = new ApiKeyAuth(mockClient, "", 5.seconds) + val req = Request[IO](method=Method.POST,headers = Headers("X-Api-Key"->"some-key")) + val result = toTest.extractUserTier(req) + result shouldEqual None + + val secondResult = toTest.extractUserTier(req) + secondResult shouldEqual None + + verify(mockClient, times(2)).getItem(GetItemRequest.builder() + .tableName("") + .key(Map("ApiKey"->AttributeValue.fromS("some-key")).asJava) + .build() + ) + } +}