constructor()

in lib/search-stack.ts [15:309]


  constructor(scope: Construct, id: string, props?: StackProps) {

    super(scope, id, props);

    const applicationPrefix = new CfnParameter(this, 'applicationPrefix', {
      default: this.node.tryGetContext('applicationPrefix'),
      description: "Prefix for the Amazon Cognito domain and the Amazon Elasticsearch Service domain",
      type: "String",
      allowedPattern: "^[a-z0-9]*$",
      minLength: 3,
      maxLength: 20
    }).valueAsString;

    const userPool = new CfnUserPool(this, "userPool", {
      adminCreateUserConfig: {
        allowAdminCreateUserOnly: true
      },
      usernameAttributes: ["email"],
      autoVerifiedAttributes: ["email"],
    });

    // get a unique suffix from the last element of the stackId, e.g. 06b321d6b6e2
    const suffix = Fn.select(4, Fn.split("-", Fn.select(2, Fn.split("/", this.stackId))));

    new CfnUserPoolDomain(this, "cognitoDomain", {
      domain: applicationPrefix + "-" + suffix,
      userPoolId: userPool.ref
    });

    const idPool = new CfnIdentityPool(this, "identityPool", {
      allowUnauthenticatedIdentities: false,
      cognitoIdentityProviders: []
    });

    const esLimitedUserRole = new Role(this, "esLimitedUserRole", {
      assumedBy: new FederatedPrincipal('cognito-identity.amazonaws.com', {
        "StringEquals": { "cognito-identity.amazonaws.com:aud": idPool.ref },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }, "sts:AssumeRoleWithWebIdentity")
    });


    const esAdminFnRole = new Role(this, "esAdminFnRole", {
      assumedBy: new ServicePrincipal('lambda.amazonaws.com')
    });
    esAdminFnRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"))

    const esAdminUserRole = new Role(this, "esAdminUserRole", {
      assumedBy:
        new FederatedPrincipal('cognito-identity.amazonaws.com', {
          "StringEquals": { "cognito-identity.amazonaws.com:aud": idPool.ref },
          "ForAnyValue:StringLike": {
            "cognito-identity.amazonaws.com:amr": "authenticated"
          }
        }, "sts:AssumeRoleWithWebIdentity")
    });

    const elasticsearchHttpPolicy = new ManagedPolicy(this, "elasticsearchHttpPolicy", {
      roles: [esAdminUserRole, esAdminFnRole]
    });

    new CfnUserPoolGroup(this, "userPoolAdminGroupPool", {
      userPoolId: userPool.ref,
      groupName: "es-admins",
      roleArn: esAdminUserRole.roleArn
    });

    const domainArn = "arn:aws:es:" + this.region + ":" + this.account + ":domain/" + applicationPrefix + "/*"

    elasticsearchHttpPolicy.addStatements(new PolicyStatement({
      effect: Effect.ALLOW,
      resources: [domainArn],
      actions: ['es:ESHttpPost', 'es:ESHttpGet', 'es:ESHttpPut']
    }));

    const esRole = new Role(this, "esRole", {
      assumedBy: new ServicePrincipal('es.amazonaws.com'),
      managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName("AmazonESCognitoAccess")]
    });

    const esDomain = new CfnDomain(this, "searchDomain", {
      elasticsearchClusterConfig: { instanceType: "t3.small.elasticsearch" },
      ebsOptions: { volumeSize: 10, ebsEnabled: true },
      elasticsearchVersion: "7.9",
      domainName: applicationPrefix,
      nodeToNodeEncryptionOptions: { enabled: true },
      encryptionAtRestOptions: { enabled: true },
      advancedSecurityOptions: {
        enabled: true,
        masterUserOptions: { masterUserArn: esAdminFnRole.roleArn }
      },
      domainEndpointOptions: {
        enforceHttps: true
      },
      cognitoOptions: {
        enabled: true,
        identityPoolId: idPool.ref,
        roleArn: esRole.roleArn,
        userPoolId: userPool.ref
      },

      // see recommended configuration for fgac
      // https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/fgac.html
      // don't use this without fgac, vpc support, or ip based restrictions
      // as it enables anonymous access
      accessPolicies: {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "AWS": "*"
            },
            "Action": "es:ESHttp*",
            "Resource": domainArn
          }
        ]
      }
    });

    const userPoolClients = new AwsCustomResource(this, 'clientIdResource', {
      policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: [userPool.attrArn] }),
      onCreate: {
        service: 'CognitoIdentityServiceProvider',
        action: 'listUserPoolClients',
        parameters: {
          UserPoolId: userPool.ref
        },
        physicalResourceId: PhysicalResourceId.of(`ClientId-${applicationPrefix}`)
      }
    });
    userPoolClients.node.addDependency(esDomain);

    const clientId = userPoolClients.getResponseField('UserPoolClients.0.ClientId');
    const providerName = `cognito-idp.${this.region}.amazonaws.com/${userPool.ref}:${clientId}`

    new CfnIdentityPoolRoleAttachment(this, 'userPoolRoleAttachment', {
      identityPoolId: idPool.ref,
      roles: {
        'authenticated': esLimitedUserRole.roleArn
      },
      roleMappings: new CfnJson(this, 'roleMappingsJson', {
        value: {
          [providerName]: {
            Type: 'Token',
            AmbiguousRoleResolution: 'AuthenticatedRole'
          }
        }
      }
      )
    });

    /**
     * Function implementing the requests to Amazon Elasticsearch Service
     * for the custom resource.
     */
    const esRequestsFn = new lambda.Function(this, 'esRequestsFn', {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: 'es-requests.handler',
      code: lambda.Code.fromAsset(path.join(__dirname, '..', 'functions/es-requests')),
      timeout: Duration.seconds(30),
      role: esAdminFnRole,
      environment: {
        "DOMAIN": esDomain.attrDomainEndpoint,
        "REGION": this.region
      }
    });

    const esRequestProvider = new Provider(this, 'esRequestProvider', {
      onEventHandler: esRequestsFn
    });


    /**
     * You can import files exported via Kibana's
     * Stack Management -> Save Objects as done with the
     * dashboard.ndjson below.
     */
    new CustomResource(this, 'esRequestsResource', {
      serviceToken: esRequestProvider.serviceToken,
      properties: {
        requests: [
          {
            "method": "PUT",
            "path": "_opendistro/_security/api/tenants/logs-tenant",
            "body": {
              "description": "A tenant for the sample kibana objects."
            }
          },
          {
            "method": "PUT",
            "path": "_opendistro/_security/api/rolesmapping/all_access",
            "body": {
              "backend_roles": [
                esAdminUserRole.roleArn,
                esAdminFnRole.roleArn
              ],
              "hosts": [],
              "users": []
            }
          },
          {
            "method": "PUT",
            "path": "_opendistro/_security/api/rolesmapping/security_manager",
            "body": {
              "backend_roles": [
                esAdminFnRole.roleArn,
                esAdminUserRole.roleArn
              ],
              "hosts": [],
              "users": []
            }
          },
          {
            "method": "PUT",
            "path": "_template/example-index-template",
            "body": fs.readFileSync(path.join(__dirname, "index-template.json")).toString()
          },
          {
            "method": "POST",
            "path": "_plugin/kibana/api/saved_objects/_import?overwrite=true",
            "body": fs.readFileSync(path.join(__dirname, "dashboard.ndjson")).toString(),
            "securitytenant": "logs-tenant",
            "filename": "dashboard.ndjson"
          },
          {
            "method": "PUT",
            "path": "_opendistro/_security/api/roles/logs-role",
            "body":
            {
              "cluster_permissions": [
                "cluster_composite_ops",
                "cluster_monitor"
              ],
              "index_permissions": [{
                "index_patterns": [
                  "logs-*"
                ],
                "dls": "",
                "fls": [],
                "masked_fields": [],
                "allowed_actions": [
                  "crud",
                  "create_index"
                ]
              }],
              "tenant_permissions": [{
                "tenant_patterns": [
                  "logs-tenant"
                ],
                "allowed_actions": [
                  "kibana_all_write"
                ]
              }]
            }
          },
          {
            "method": "PUT",
            "path": "_opendistro/_security/api/rolesmapping/logs-role",
            "body": {
              "backend_roles": [
                esLimitedUserRole.roleArn
              ],
              "hosts": [],
              "users": []
            }
          },
          {
            "method": "PUT",
            "path": "_opendistro/_security/api/rolesmapping/kibana_user",
            "body": {
              "backend_roles": [
                esLimitedUserRole.roleArn
              ],
              "hosts": [],
              "users": []
            }
          }
        ]
      }
    });

    new CfnOutput(this, 'createUserUrl', {
      description: "Create a new user in the user pool here - add it to the es-admins group if fine grained access controls should not apply.",
      value: "https://" + this.region + ".console.aws.amazon.com/cognito/users?region=" + this.region + "#/pool/" + userPool.ref + "/users"
    });

    new CfnOutput(this, 'kibanaUrl', {
      description: "Access Kibana via this URL.",
      value: "https://" + esDomain.attrDomainEndpoint + "/_plugin/kibana/"
    });

  }