I am trying to convert this CF template to AWS CDK:
Resources:
S3User:
Type: AWS::IAM::User
Properties:
Policies:
- PolicyName: UserS3Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowUserToSeeBucketListInTheConsole
Action:
- s3:ListAllMyBuckets
- s3:GetBucketLocation
Effect: Allow
Resource:
- arn:aws:s3:::*
- Sid: AllowRootAndUploadsBucket
Action:
- s3:ListBucket
Effect: Allow
Resource:
- Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: UploadBucket
Condition:
StringEquals:
s3:prefix:
- ''
- uploads/
s3:delimiter:
- "/"
- Sid: AllowListingOfUploadsFolder
Action:
- s3:ListBucket
Effect: Allow
Resource:
- Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: UploadBucket
Condition:
StringLike:
s3:prefix:
- uploads/*
- Sid: AllowAllS3ActionsInUploadsFolder
Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
Resource:
- Fn::Join:
- ''
- - 'arn:aws:s3:::'
- Ref: UploadBucket
- "/uploads"
- "/*"
Tags:
- Key: CloudFormationArn
Value: '#{AWS::StackId}'
UserAccessKey:
DependsOn: S3User
Type: AWS::IAM::AccessKey
Properties:
UserName:
Ref: S3User
Outputs:
UserAccessKeyID:
Description: The Access Key for S3 bucket access
Value:
Ref: UserAccessKey
UserAccessKeySecret:
Description: The Access Key Secret for S3 bucket access
Value:
Fn::GetAtt:
- "UserAccessKey"
- "SecretAccessKey"
Here is what I have so far:
import { Construct, Stack, StackProps, CfnOutput } from '#aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '#aws-cdk/aws-iam';
// import { Bucket } from '#aws-cdk/aws-s3';
const S3AccessGroup = 'S3AccessGroup';
const S3Users = [
'firstname.lastname#domain.tld',
];
export class CdkIamStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// const bucket = Bucket.fromBucketAttributes(this, 'ImportedBucket', {
// bucketArn: 'arn:aws:s3:::my-bucket'
// });
const AllowUserToSeeBucketListInTheConsole = new PolicyStatement({
resources: ["arn:aws:s3:::*"],
actions: [
"s3:ListAllMyBuckets",
"s3:GetBucketLocation"
],
});
const AllowRootAndUploadsBucket = new PolicyStatement({
resources: ['arn:aws:s3:::my-bucket'],
actions: [
"s3:ListBucket"
],
conditions: {'StringEquals': {
's3:prefix': [
'uploads',
],
's3:delimiter': [
'/',
]
}
}
});
const AllowListingOfUploadsFolder = new PolicyStatement({
resources: ['arn:aws:s3:::my-bucket'],
actions: [
"s3:ListBucket"
],
conditions: {'StringEquals': {
's3:prefix': [
'uploads/*',
]
}
}
});
const AllowAllS3ActionsInUploadsFolder = new PolicyStatement({
resources: ['arn:aws:s3:::my-bucket/uploads/*'],
actions: [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion"
],
});
const UserS3Access = new Policy(this, 'UserS3Access', {
policyName: "UserS3Access",
statements: [
AllowUserToSeeBucketListInTheConsole,
AllowRootAndUploadsBucket,
AllowListingOfUploadsFolder,
AllowAllS3ActionsInUploadsFolder
],
});
const S3Group = new Group(this, S3AccessGroup, { groupName: S3AccessGroup });
S3Group.attachInlinePolicy(UserS3Access);
S3Users.forEach((S3User) => {
const user = new User(this, S3User, {
userName: S3User,
groups: [S3Group]
});
});
// new CfnOutput(this, 'accessKeyId', { value: accessKey.ref });
// new CfnOutput(this, 'secretAccessKey', { value: accessKey.attrSecretAccessKey });
}
}
How do I output the accessKeyId and secretAccessKey for each user created?
Is this the correct way to generate users using AWS CDK?
Any advice is much appreciated
After creating the user, you need to create the key as well:
S3Users.forEach((S3User) => {
const user = new User(this, S3User, {
userName: S3User,
groups: [S3Group]
});
const accessKey = new CfnAccessKey(this, `${S3User}AccessKey`, {
userName: user.userName,
});
new CfnOutput(this, `${S3User}AccessKeyId`, { value: accessKey.ref });
new CfnOutput(this, `${S3User}SecretAccessKey`, { value: accessKey.attrSecretAccessKey });
});
Your Users have been created and can be viewed in IAM console. Similar to how you generate your access keys in the console. You have to do the same in the CDK code.
Following code snippets might help you:
AWS CDK v2(2.37.1)
S3Users.forEach((S3User) => {
// create user
const user = new User(this, S3User, {
userName: S3User,
groups: [S3Group]
});
// create access key
const accessKey = new iam.AccessKey(this, 'AccessKey', { user });
// display access key and secret access key
new CfnOutput(this, `${S3User}AccessKeyId`, { value: accessKey.accessKeyId});
new CfnOutput(this, `${S3User}SecretAccessKey`, { value: accessKey.secretAccessKey });
})
Related
I'm developing a little CDKv2 script to instantiate a few AWS services.
I have some lambda code deployed in the lambda/ folder and the frontend stored in a bucket populated using the frontend/ folder in the source.
I've noticed that whenever I make a change to any of the file inside these two, cdk watch return the following error and falls back to perform a full redeploy (which is significantly slow).
Could not perform a hotswap deployment, because the CloudFormation template could not be resolved: Parameter or resource 'DomainPrefix' could not be found for evaluation
Falling back to doing a full deployment
Is there any way to make changes in these folders only trigger updating the related bucket content or the related lambda?
Following here the stack.ts for quick reference, just in case here you can take a look at the repo.
export class CdkAuthWebappStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const domainPrefixParam = new CfnParameter(this, 'DomainPrefix', {
type: 'String',
description: 'You have to set it in google cloud as well', //(TODO: add link to explain properly)
default: process.env.DOMAIN_NAME || ''
})
const googleClientIdParam = new CfnParameter(this, 'GoogleClientId', {
type: 'String',
description: 'From google project',
noEcho: true,
default: process.env.GOOGLE_CLIENT_ID || ''
})
const googleClientSecretParam = new CfnParameter(this, 'GoogleClientSecret', {
type: 'String',
description: 'From google project',
noEcho: true,
default: process.env.GOOGLE_CLIENT_SECRET || ''
})
if(!domainPrefixParam.value || !googleClientIdParam.value || !googleClientSecretParam.value){
throw new Error('Make sure you initialized DomainPrefix, GoogleClientId and GoogleClientSecret in the stack parameters')
}
const s3frontend = new s3.Bucket(this, 'Bucket', {
bucketName: domainPrefixParam.valueAsString+'-frontend-bucket',
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
enforceSSL: true,
versioned: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
websiteIndexDocument: "index.html",
});
//TODO: fare in modo che questa origin access identity non sia legacy quando deployo
const cfdistributionoriginaccessidentity = new cloudfront.OriginAccessIdentity(this, 'CFOriginAccessIdentity', {
comment: "Used to give bucket read to cloudfront"
})
const cfdistribution = new cloudfront.CloudFrontWebDistribution(this, 'CFDistributionFrontend', {
originConfigs: [
{
s3OriginSource: {
s3BucketSource: s3frontend,
originAccessIdentity: cfdistributionoriginaccessidentity
},
behaviors: [{
isDefaultBehavior: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
forwardedValues: {
queryString: true,
cookies: { forward: 'all' }
},
minTtl: cdk.Duration.seconds(0),
defaultTtl: cdk.Duration.seconds(3600),
maxTtl: cdk.Duration.seconds(86400)
}]
}
]
})
s3frontend.grantRead(cfdistributionoriginaccessidentity)
const cfdistributionpolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['cloudfront:CreateInvalidation'],
resources: [`"arn:aws:cloudfront::${this.account}:distribution/${cfdistribution.distributionId}"`]
});
const userpool = new cognito.UserPool(this, 'WebAppUserPool', {
userPoolName: 'web-app-user-pool',
selfSignUpEnabled: false
})
const userpoolidentityprovidergoogle = new cognito.UserPoolIdentityProviderGoogle(this, 'WebAppUserPoolIdentityGoogle', {
clientId: googleClientIdParam.valueAsString,
clientSecret: googleClientSecretParam.valueAsString,
userPool: userpool,
attributeMapping: {
email: cognito.ProviderAttribute.GOOGLE_EMAIL
},
scopes: [ 'email' ]
})
// this is used to make the hostedui reachable
userpool.addDomain('Domain', {
cognitoDomain: {
domainPrefix: domainPrefixParam.valueAsString
}
})
const CLOUDFRONT_PUBLIC_URL = `https://${cfdistribution.distributionDomainName}/`
const client = userpool.addClient('Client', {
oAuth: {
flows: {
authorizationCodeGrant: true
},
callbackUrls: [
CLOUDFRONT_PUBLIC_URL
],
logoutUrls: [
CLOUDFRONT_PUBLIC_URL
],
scopes: [
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.OPENID,
cognito.OAuthScope.PHONE
]
},
supportedIdentityProviders: [
cognito.UserPoolClientIdentityProvider.GOOGLE
]
})
client.node.addDependency(userpoolidentityprovidergoogle)
// defines an AWS Lambda resource
const securedlambda = new lambda.Function(this, 'AuhtorizedRequestsHandler', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'secured.handler'
});
const lambdaapiintegration = new apigw.LambdaIntegration(securedlambda)
const backendapigw = new apigw.RestApi(this, 'AuthorizedRequestAPI', {
restApiName: domainPrefixParam.valueAsString,
defaultCorsPreflightOptions: {
"allowOrigins": apigw.Cors.ALL_ORIGINS,
"allowMethods": apigw.Cors.ALL_METHODS,
}
})
const backendapiauthorizer = new apigw.CognitoUserPoolsAuthorizer(this, 'BackendAPIAuthorizer', {
cognitoUserPools: [userpool]
})
const authorizedresource = backendapigw.root.addMethod('GET', lambdaapiintegration, {
authorizer: backendapiauthorizer,
authorizationType: apigw.AuthorizationType.COGNITO
})
const s3deploymentfrontend = new s3deployment.BucketDeployment(this, 'DeployFrontEnd', {
sources: [
s3deployment.Source.asset('./frontend'),
s3deployment.Source.data('constants.js', `const constants = {domainPrefix:'${domainPrefixParam.valueAsString}', region:'${this.region}', cognito_client_id:'${client.userPoolClientId}', apigw_id:'${backendapigw.restApiId}'}`)
],
destinationBucket: s3frontend,
distribution: cfdistribution
})
new cdk.CfnOutput(this, 'YourPublicCloudFrontURL', {
value: CLOUDFRONT_PUBLIC_URL,
description: 'Navigate to the URL to access your deployed application'
})
}
}
Recording the solution from the comments:
Cause:
cdk watch apparently does not work with template parameters. I guess this is because the default --hotswap option bypasses CloudFormation and deploys instead via SDK commands.
Solution:
Remove the CfnParamters from the template. CDK recommends not using parameters in any case.
Perhaps cdk watch --no-hotswap would also work?
How to get a webACLId of WAF
I'm trying to create Cloudfront distribution with WAF.
I tried to create custom resource to get it, but no luck
new cr.AwsCustomResource(this, 'GetParameter', {
onUpdate: { // will also be called for a CREATE event
service: 'WAF',
action: 'ListWebACLs',
// parameters: {
// Limit: 10,
// NextMarker: 'cloudfrontwebacl'
// },
physicalResourceId: cr.PhysicalResourceId.of('Date.now().toString()'),
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
Ohh. The webACLId should be extract by using Ref
Example:
// 👇 Create WAF Rule
const rule = new CfnRule(this,'rule',{
metricName:'rule',
name:'rule',
predicates:[
...
]
})
...
// 👇 Create WAF WebACL
const CloudfrontWebACL = new CfnWebACL(this, "cloudfrontwebacl", {
name: "cloudfrontwebacl",
defaultAction: { type: 'ALLOW' },
metricName: 'cloudfrontwebacl',
rules:[
{
priority: 0,
ruleId: rule.ref,
action:{
type: 'BLOCK',
}
}
]
})
...
// 👇 Create Cloudfront distribution for web
...
webAclId: CloudfrontWebACL.ref,
...
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-waf-webacl.html#aws-resource-waf-webacl-return-values
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html
There is an existing API described in a Coludformation template. Now I want to document the API using Swagger. Is there a way to parse the Cloudformation template to create the swagger.yaml specification file? I would like to avoid writing the API a second time, if possible.
Note: I am aware that you can define your API using Swagger, then import the API configuration in your Cloudformation template. This is not what I need. The Cloudformation already exists and will not be changed. Hence, I need the opposite: a Swagger configuration file based on an existing Cloudformation template.
There is no way to convert the template to a swagger file that I know about. But if you are looking for a way to keep service-spec in one place only (template) and you have it deployed, you can take swagger or OAS file from the stage (so to do it you must have a stage as well) in two ways at least:
By Web console. Use Amazon API Gateway->
APIs->Your API->Stages>Your Stage -> Export tab. See the picture: exporting Swagger or OAS as a file by Web console
aws apigateway get-export ... Here is an example:
aws apigateway get-export --rest-api-id ${API_ID} --stage-name ${STAGE_NAME} --export-type swagger swagger.json
I just made this, it is not setup for perfect plug/play, but will give you an idea what you need to adjust to get it working (also need to make sure you CF template is setup so it has the needed info, on mine I had to add some missing requestParams I was missing, also use this site to test your results from this code to see it works with swagger):
const yaml = require('js-yaml');
const fs = require('fs');
// Get document, or throw exception on error
try {
// loads file from local
const inputStr = fs.readFileSync('../template.yaml', { encoding: 'UTF-8' });
// creating a schema to handle custom tags (cloud formation) which then js-yaml can handle when parsing
const CF_SCHEMA = yaml.DEFAULT_SCHEMA.extend([
new yaml.Type('!ImportValue', {
kind: 'scalar',
construct: function (data) {
return { 'Fn::ImportValue': data };
},
}),
new yaml.Type('!Ref', {
kind: 'scalar',
construct: function (data) {
return { Ref: data };
},
}),
new yaml.Type('!Equals', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Equals': data };
},
}),
new yaml.Type('!Not', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Not': data };
},
}),
new yaml.Type('!Sub', {
kind: 'scalar',
construct: function (data) {
return { 'Fn::Sub': data };
},
}),
new yaml.Type('!If', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::If': data };
},
}),
new yaml.Type('!Join', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Join': data };
},
}),
new yaml.Type('!Select', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Select': data };
},
}),
new yaml.Type('!FindInMap', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::FindInMap': data };
},
}),
new yaml.Type('!GetAtt', {
kind: 'scalar',
construct: function (data) {
return { 'Fn::GetAtt': data };
},
}),
new yaml.Type('!GetAZs', {
kind: 'scalar',
construct: function (data) {
return { 'Fn::GetAZs': data };
},
}),
new yaml.Type('!Base64', {
kind: 'mapping',
construct: function (data) {
return { 'Fn::Base64': data };
},
}),
]);
const input = yaml.load(inputStr, { schema: CF_SCHEMA });
// now that we have our AWS yaml copied and formatted into an object, lets pluck what we need to match up with the swagger.yaml format
const rawResources = input.Resources;
let guts = [];
// if an object does not contain a properties.path object then we need to remove it as a possible api to map for swagger
for (let i in rawResources) {
if (rawResources[i].Properties.Events) {
for (let key in rawResources[i].Properties.Events) {
// console.log(i, rawResources[i]);
if (rawResources[i].Properties.Events[key].Properties.Path) {
let tempResource = rawResources[i].Properties.Events[key].Properties;
tempResource.Name = key;
guts.push(tempResource);
}
}
}
} // console.log(guts);
const defaultResponses = {
'200': {
description: 'successful operation',
},
'400': {
description: 'Invalid ID supplied',
},
};
const formattedGuts = guts.map(function (x) {
if (x.RequestParameters) {
if (
Object.keys(x.RequestParameters[0])[0].includes('path') &&
x.RequestParameters.length > 1
) {
return {
[x.Path]: {
[x.Method]: {
tags: [x.RestApiId.Ref],
summary: x.Name,
parameters: [
{
name: Object.keys(x.RequestParameters[0])[0].split('method.request.path.')[1],
in: 'path',
type: 'string',
required: Object.values(x.RequestParameters[0])[0].Required,
},
{
name: Object.keys(x.RequestParameters[1])[0].split('method.request.path.')[1],
in: 'path',
type: 'string',
required: Object.values(x.RequestParameters[1])[0].Required,
},
],
responses: defaultResponses,
},
},
};
} else if (Object.keys(x.RequestParameters[0])[0].includes('path')) {
return {
[x.Path]: {
[x.Method]: {
tags: [x.RestApiId.Ref],
summary: x.Name,
parameters: [
{
name: Object.keys(x.RequestParameters[0])[0].split('method.request.path.')[1],
in: 'path',
type: 'string',
required: Object.values(x.RequestParameters[0])[0].Required,
},
],
responses: defaultResponses,
},
},
};
} else if (Object.keys(x.RequestParameters[0])[0].includes('querystring')) {
return {
[x.Path]: {
[x.Method]: {
tags: [x.RestApiId.Ref],
summary: x.Name,
parameters: [
{
name: Object.keys(x.RequestParameters[0])[0].split(
'method.request.querystring.'
)[1],
in: 'query',
type: 'string',
required: Object.values(x.RequestParameters[0])[0].Required,
},
],
responses: defaultResponses,
},
},
};
}
}
return {
[x.Path]: {
[x.Method]: {
tags: [x.RestApiId.Ref],
summary: x.Name,
responses: defaultResponses,
},
},
};
});
const swaggerYaml = yaml.dump(
{
swagger: '2.0',
info: {
description: '',
version: '1.0.0',
title: '',
},
paths: Object.assign({}, ...formattedGuts),
},
{ noRefs: true }
); // need to keep noRefs as true, otherwise you will see "*ref_0" instead of the response obj
// console.log(swaggerYaml);
fs.writeFile('../swagger.yaml', swaggerYaml, 'utf8', function (err) {
if (err) return console.log(err);
});
} catch (e) {
console.log(e);
console.log('error above');
}
I have a basic react functional component and I bind an array in which there are strings to be localized. Is there any other way to do it? I am trying as below and it says "Invalid Hook Call"
import { useIntl } from "react-intl";
const NavBasicExample: React.FunctionComponent = () => {
return (
<Nav
groups={navLinkGroups}
/>
</div>
);
};
const navLinkGroups: INavLinkGroup[] = [
{
name: getFormattedString(Strings.details),//This fails and says invalidHookCall
links: [{ name: Strings.appDetails, url: "" }]
},
{
name: Strings.capabilities,
links: [
{ name: Strings.tabs},
{ name: Strings.bots}
]
}
];
const getFormattedString = (inputString: string) => {
const intl = useIntl(); //This fails.
return intl.formatMessage({ id: "details", defaultMessage: "Login });
};
The problem is that you are calling a Hook from a non-react function. You are not allowed to do that. Try moving the "navLinkGroups" into the "NavBasicExample" and it should work
import { useIntl } from "react-intl";
const NavBasicExample: React.FunctionComponent = () => {
const intl = useIntl();
const navLinkGroups: INavLinkGroup[] = [
{
name: getFormattedString(Strings.details),
links: [{ name: Strings.appDetails, url: "" }]
},
{
name: Strings.capabilities,
links: [
{ name: Strings.tabs},
{ name: Strings.bots}
]
}
];
const getFormattedString = (inputString: string) => {
return intl.formatMessage({ id: "details", defaultMessage: "Login" });
};
return (
<Nav
groups={navLinkGroups}
/>
</div>
);
};
Starting up NestJS & GraphQL using yarn start:dev using await app.listen(3200);. When trying to connect to my Neo4J Desktop, I get this error trying to get my queries at localhost:3200/graphQL:
"errors": [
{
"message": "Client network socket disconnected before secure TLS connection was established",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"getMovies"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"code": "ServiceUnavailable",
"name": "Neo4jError"
}
}
}
],
"data": null
}
So I figured my local Neo4J desktop graph is not running correctly, but I can't seem to find any answer how to solve it.. Currently I have a config.ts file which has:
export const HOSTNAME = 'localhost';
export const NEO4J_USER = 'neo4j';
export const NEO4J_PASSWORD = '123';
and a file neogql.resolver.ts:
import {
Resolver,
Query,
Args,
ResolveProperty,
Parent,
} from '#nestjs/graphql';
import { HOSTNAME, NEO4J_USER, NEO4J_PASSWORD } from '../config';
import { Movie } from '../graphql';
import { Connection, relation, node } from 'cypher-query-builder';
import { NotFoundException } from '#nestjs/common';
const db = new Connection(`bolt://${HOSTNAME}`, {
username: NEO4J_USER,
password: NEO4J_PASSWORD,
});
#Resolver('Movie')
export class NeogqlResolver {
#Query()
async getMovies(): Promise<Movie> {
const movies = (await db
.matchNode('movies', 'Movie')
.return([
{
movies: [{ id: 'id', title: 'title', year: 'year' }],
},
])
.run()) as any;
return movies;
}
#Query('movie')
async getMovieById(
#Args('id')
id: string,
): Promise<any> {
const movie = (await db
.matchNode('movie', 'Movie')
.where({ 'movie.id': id })
.return([
{
movie: [{ id: 'id', title: 'title', year: 'year' }],
},
])
.run<any>()) as any;
if (movie.length === 0) {
throw new NotFoundException(
`Movie id '${id}' does not exist in database `,
);
}
return movie[0];
}
#ResolveProperty()
async actors(#Parent() movie: any) {
const { id } = movie;
return (await db
.match([node('actors', 'Actor'), relation('in'), node('movie', 'Movie')])
.where({ 'movie.id': id })
.return([
{
actors: [
{
id: 'id',
name: 'name',
born: 'born',
},
],
},
])
.run()) as any;
}
}
Be sure to pass the Config object like this:
var hostname = this.configService.get<string>('NEO4J_URL');
var username = this.configService.get<string>('NEO4J_USERNAME');
var password = this.configService.get<string>('NEO4J_PASSWORD');
db = new Connection(`${hostname}`, {
username: username,
password: password,
}, {
driverConfig: { encrypted: "ENCRYPTION_OFF" }
});
I had the same problem with grandSTACK when running against a neo4j version 4 server. According to Will Lyon this is due to mismatched encryption defaults between driver and database: https://community.neo4j.com/t/migrating-an-old-grandstack-project-to-neo4j-4/16911/2
So passing a config object with
{ encrypted: "ENCRYPTION_OFF"}
to the Connection constructor should do the trick.