@mstuercke/pulumi-modules 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@mstuercke/pulumi-modules",
3
+ "version": "0.0.1",
4
+ "files": [
5
+ "src/**/!(*.test|*.fixture).ts",
6
+ "README.md"
7
+ ],
8
+ "main": "src/index.ts",
9
+ "scripts": {
10
+ "typeCheck": "tsc --noEmit",
11
+ "bump:major": "npm version major --git-tag-version false",
12
+ "bump:minor": "npm version minor --git-tag-version false",
13
+ "bump:patch": "npm version patch --git-tag-version false",
14
+ "release": "bun run --tsconfig-override ./tsconfig.scripts.json scripts/release.ts"
15
+ },
16
+ "dependencies": {
17
+ "@pulumi/archive": "^0.2.2",
18
+ "@pulumi/auth0": "^3.8.1",
19
+ "@pulumi/aws": "^6.56.1",
20
+ "@pulumi/mongodbatlas": "^3.20.2",
21
+ "@pulumi/pulumi": "^3.137.0",
22
+ "@pulumi/random": "^4.16.7",
23
+ "@pulumiverse/sentry": "^0.0.8",
24
+ "@types/mime": "^3.0.4",
25
+ "glob": "^11.0.0",
26
+ "mime": "^3.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@mstuercke/bun-utils": "7.0.6",
30
+ "@types/bun": "1.1.12",
31
+ "typescript": "5.6.3"
32
+ }
33
+ }
@@ -0,0 +1,162 @@
1
+ import {type CustomResourceOptions, type Output, Resource} from '@pulumi/pulumi'
2
+ import {Client, ClientCredentials, Connection, ConnectionClient, Provider, Tenant, User} from '@pulumi/auth0'
3
+
4
+ export type Auth0Domain = `${string}.auth0.com`
5
+ export type Auth0URL = `http://${string}` | `https://${string}`
6
+
7
+ export interface Auth0User {
8
+ name: string
9
+ email: string
10
+ password: string
11
+ emailVerified: boolean
12
+ appMetadata?: Record<string, unknown>
13
+ }
14
+
15
+ export interface Auth0Args {
16
+ projectId: string
17
+ environmentName: string
18
+ tenantDisplayName: string
19
+ clientDisplayName: string
20
+ domain: Auth0Domain
21
+ m2mClientId: string
22
+ allowedUrls: Auth0URL[]
23
+ allowedLoginCallbackUrls?: Auth0URL[]
24
+ allowedLogoutCallbackUrls?: Auth0URL[]
25
+ nativeAppId?: `com.${string}`
26
+ defaultUsers?: Auth0User[]
27
+ disableSignup?: boolean
28
+ }
29
+
30
+ export class Auth0 extends Resource {
31
+ domain: string
32
+ clientId: Output<string>
33
+
34
+ constructor(id: string, args: Auth0Args, opts?: CustomResourceOptions) {
35
+ super('mstuercke:auth0:Auth0', id, false, undefined, opts)
36
+
37
+ const {
38
+ projectId,
39
+ environmentName,
40
+ tenantDisplayName,
41
+ clientDisplayName,
42
+ domain,
43
+ m2mClientId,
44
+ allowedUrls,
45
+ allowedLoginCallbackUrls,
46
+ allowedLogoutCallbackUrls,
47
+ nativeAppId,
48
+ defaultUsers = [],
49
+ disableSignup = false,
50
+ } = args
51
+
52
+ new Provider('provider', {}, {parent: this})
53
+
54
+ new Tenant(
55
+ 'tenant',
56
+ {
57
+ friendlyName: tenantDisplayName,
58
+ flags: {
59
+ enableClientConnections: false,
60
+ },
61
+ },
62
+ {parent: this},
63
+ )
64
+
65
+ const nativeAppCallbackUrls = nativeAppId
66
+ ? [
67
+ `${nativeAppId}.auth0://${domain}/ios/${nativeAppId}/callback`,
68
+ `${nativeAppId}.auth0://${domain}/android/${nativeAppId}/callback`,
69
+ ]
70
+ : []
71
+ const client = new Client(
72
+ 'client',
73
+ {
74
+ name: clientDisplayName,
75
+ appType: 'spa',
76
+ callbacks: [...(allowedLoginCallbackUrls || allowedUrls), ...nativeAppCallbackUrls],
77
+ webOrigins: allowedUrls,
78
+ allowedLogoutUrls: [...(allowedLogoutCallbackUrls || allowedUrls), ...nativeAppCallbackUrls],
79
+ jwtConfiguration: {
80
+ alg: 'RS256',
81
+ },
82
+ addons: {},
83
+ },
84
+ {parent: this},
85
+ )
86
+
87
+ const connection = new Connection(
88
+ 'password-authentication',
89
+ {
90
+ name: `${projectId}-${environmentName}`,
91
+ strategy: 'auth0',
92
+ options: {
93
+ disableSignup: disableSignup,
94
+ bruteForceProtection: true,
95
+ },
96
+ },
97
+ {parent: this},
98
+ )
99
+
100
+ const m2mClientConnection = new ConnectionClient(
101
+ 'password-authentication-m2m-client',
102
+ {
103
+ clientId: m2mClientId,
104
+ connectionId: connection.id,
105
+ },
106
+ {parent: client},
107
+ )
108
+
109
+ const clientConnection = new ConnectionClient(
110
+ 'password-authentication-client',
111
+ {
112
+ clientId: client.id,
113
+ connectionId: connection.id,
114
+ },
115
+ {parent: client},
116
+ )
117
+
118
+ // TODO: Implement disabling of google-oauth2 (If possible, also disable "Username-Password-Authentication")
119
+ // // disable google-oauth2 on all clients
120
+ // const googleOauth2Connection = output(getConnection({name: 'google-oauth2'}))
121
+ // new ConnectionClients(
122
+ // 'google-oauth2-clients',
123
+ // {
124
+ // connectionId: googleOauth2Connection.id,
125
+ // enabledClients: [],
126
+ // },
127
+ // {parent: client, dependsOn: [client]},
128
+ // )
129
+
130
+ new ClientCredentials(
131
+ 'credentials',
132
+ {
133
+ clientId: client.id,
134
+ authenticationMethod: 'none',
135
+ },
136
+ {parent: this},
137
+ )
138
+
139
+ for (const defaultUser of defaultUsers) {
140
+ const {name, email, emailVerified, password, appMetadata} = defaultUser
141
+
142
+ new User(
143
+ `default-user-${defaultUser.email}`,
144
+ {
145
+ connectionName: connection.name,
146
+ name: name,
147
+ email: email,
148
+ emailVerified: emailVerified,
149
+ password: password,
150
+ appMetadata: appMetadata ? JSON.stringify(appMetadata) : undefined,
151
+ },
152
+ {
153
+ dependsOn: [m2mClientConnection, clientConnection],
154
+ parent: connection,
155
+ },
156
+ )
157
+ }
158
+
159
+ this.domain = domain
160
+ this.clientId = client.id
161
+ }
162
+ }
@@ -0,0 +1,7 @@
1
+ import {Auth0} from './Auth0'
2
+
3
+ export * from './Auth0'
4
+
5
+ export const auth0 = {
6
+ Auth0,
7
+ }
@@ -0,0 +1,32 @@
1
+ import {acm, type Provider, route53} from '@pulumi/aws'
2
+
3
+ export interface DataDomainOptions {
4
+ usEast1Provider: Provider
5
+ name: `${string}.${string}`
6
+ }
7
+
8
+ export type DomainResult = {
9
+ name: string
10
+ zoneId: string
11
+ nameServers: string[]
12
+ usEast1CertificateArn: string
13
+ euCentral1CertificateArn: string
14
+ }
15
+
16
+ export const getDomain = async (options: DataDomainOptions): Promise<DomainResult> => {
17
+ const {usEast1Provider, name} = options
18
+
19
+ const zone = await route53.getZone({name})
20
+
21
+ const euCentral1Certificate = await acm.getCertificate({domain: name})
22
+
23
+ const usEast1Certificate = await acm.getCertificate({domain: name}, {provider: usEast1Provider})
24
+
25
+ return {
26
+ name: zone.name,
27
+ zoneId: zone.zoneId,
28
+ nameServers: zone.nameServers,
29
+ usEast1CertificateArn: usEast1Certificate.arn,
30
+ euCentral1CertificateArn: euCentral1Certificate.arn,
31
+ }
32
+ }
@@ -0,0 +1,7 @@
1
+ import {getDomain} from './getDomain'
2
+
3
+ export * from './getDomain'
4
+
5
+ export const domain = {
6
+ getDomain,
7
+ }
@@ -0,0 +1,165 @@
1
+ import {iam} from '@pulumi/aws'
2
+ import type {SimpleIamRolePolicyConfig} from './SimpleIamRolePolicyConfig'
3
+ import type {CustomResourceOptions, Input} from '@pulumi/pulumi'
4
+ import type {RolePolicyArgs} from '@pulumi/aws/iam'
5
+ import type {PolicyStatement} from '@pulumi/aws/iam/documents'
6
+
7
+ export class SimpleIamRolePolicy extends iam.RolePolicy {
8
+ constructor(iamRoleId: string, config: SimpleIamRolePolicyConfig, opts?: CustomResourceOptions) {
9
+ switch (config.type) {
10
+ case 'custom':
11
+ super(config.name, mapIamRolePolicyConfig(iamRoleId, config.name, config.statements), opts)
12
+ break
13
+
14
+ case 'dynamodb':
15
+ if (config.level !== 'full-access') throw mapError(config)
16
+ super(
17
+ config.name,
18
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
19
+ {
20
+ Action: ['dynamodb:*'],
21
+ Effect: 'Allow',
22
+ Resource: [`${config.tableArn}`, `${config.tableArn}/*`],
23
+ },
24
+ ]),
25
+ opts,
26
+ )
27
+ break
28
+
29
+ case 'timestream':
30
+ if (config.level !== 'full-access') throw mapError(config)
31
+
32
+ super(
33
+ config.name,
34
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
35
+ {
36
+ Action: ['timestream:*'],
37
+ Effect: 'Allow',
38
+ Resource: [config.dbArn, config.tableArn],
39
+ },
40
+ {
41
+ Action: ['timestream:DescribeEndpoints'],
42
+ Effect: 'Allow',
43
+ Resource: ['*'],
44
+ },
45
+ ]),
46
+ opts,
47
+ )
48
+ break
49
+
50
+ case 's3':
51
+ if (config.level !== 'full-access') throw mapError(config)
52
+
53
+ super(
54
+ config.name,
55
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
56
+ {
57
+ Action: ['s3:*'],
58
+ Effect: 'Allow',
59
+ Resource: [`${config.bucketArn}`, `${config.bucketArn}/*`],
60
+ },
61
+ ]),
62
+ opts,
63
+ )
64
+ break
65
+
66
+ case 'cognito':
67
+ if (config.level !== 'full-access') throw mapError(config)
68
+
69
+ super(
70
+ config.name,
71
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
72
+ {
73
+ Action: ['cognito-idp:*'],
74
+ Effect: 'Allow',
75
+ Resource: [config.userPoolArn],
76
+ },
77
+ ]),
78
+ opts,
79
+ )
80
+ break
81
+
82
+ case 'websocket':
83
+ if (config.level !== 'full-access') throw mapError(config)
84
+
85
+ super(
86
+ config.name,
87
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
88
+ {
89
+ Effect: 'Allow',
90
+ Action: 'execute-api:*',
91
+ Resource: config.apiArn,
92
+ },
93
+ ]),
94
+ opts,
95
+ )
96
+ break
97
+
98
+ case 'sns':
99
+ if (config.level !== 'publish-message') throw mapError(config)
100
+
101
+ super(
102
+ config.name,
103
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
104
+ {
105
+ Effect: 'Allow',
106
+ Action: 'sns:Publish',
107
+ Resource: config.snsTopicArn,
108
+ },
109
+ ]),
110
+ opts,
111
+ )
112
+ break
113
+
114
+ case 'execute-lambda':
115
+ super(
116
+ config.name,
117
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
118
+ {
119
+ Effect: 'Allow',
120
+ Action: 'lambda:InvokeFunction',
121
+ Resource: config.lambdaArn,
122
+ },
123
+ ]),
124
+ opts,
125
+ )
126
+ break
127
+
128
+ case 'event-bridge':
129
+ super(
130
+ config.name,
131
+ mapIamRolePolicyConfig(iamRoleId, config.name, [
132
+ {
133
+ Effect: 'Allow',
134
+ Action: ['events:DescribeRule', 'events:DisableRule', 'events:EnableRule'],
135
+ Resource: config.ruleArn,
136
+ },
137
+ ]),
138
+ opts,
139
+ )
140
+ break
141
+
142
+ default:
143
+ throw mapError(config)
144
+ }
145
+ }
146
+ }
147
+
148
+ function mapIamRolePolicyConfig(
149
+ iamRoleId: Input<string>,
150
+ name: string,
151
+ statements: Input<Input<PolicyStatement>[]>,
152
+ ): RolePolicyArgs {
153
+ return {
154
+ role: iamRoleId,
155
+ name: name,
156
+ policy: {
157
+ Version: '2012-10-17',
158
+ Statement: statements,
159
+ },
160
+ }
161
+ }
162
+
163
+ function mapError(config: SimpleIamRolePolicyConfig): Error {
164
+ return new Error(`Unable to create policy with config: ${JSON.stringify(config)}`)
165
+ }
@@ -0,0 +1,75 @@
1
+ import type {Input} from '@pulumi/pulumi'
2
+ import type {PolicyStatement} from '@pulumi/aws/iam/documents'
3
+
4
+ export type SimpleIamRolePolicyConfig =
5
+ | CustomPolicyConfig
6
+ | DynamoDbPolicyConfig
7
+ | TimestreamPolicyConfig
8
+ | S3PolicyConfig
9
+ | CognitoPolicyConfig
10
+ | WebsocketPolicyConfig
11
+ | ExecuteLambdaConfig
12
+ | SnsPolicyConfig
13
+ | EventBridgeConfig
14
+
15
+ interface CustomPolicyConfig {
16
+ type: 'custom'
17
+ name: string
18
+ statements: PolicyStatement[]
19
+ }
20
+
21
+ interface DynamoDbPolicyConfig {
22
+ type: 'dynamodb'
23
+ level: 'full-access'
24
+ name: string
25
+ tableArn: Input<string>
26
+ }
27
+
28
+ interface TimestreamPolicyConfig {
29
+ type: 'timestream'
30
+ level: 'full-access'
31
+ name: string
32
+ dbArn: Input<string>
33
+ tableArn: Input<string>
34
+ }
35
+
36
+ interface S3PolicyConfig {
37
+ type: 's3'
38
+ level: 'full-access'
39
+ name: string
40
+ bucketArn: Input<string>
41
+ }
42
+
43
+ interface CognitoPolicyConfig {
44
+ type: 'cognito'
45
+ level: 'full-access'
46
+ name: string
47
+ userPoolArn: Input<string>
48
+ }
49
+
50
+ interface WebsocketPolicyConfig {
51
+ type: 'websocket'
52
+ level: 'full-access'
53
+ name: string
54
+ apiArn: '*'
55
+ }
56
+
57
+ interface SnsPolicyConfig {
58
+ type: 'sns'
59
+ level: 'publish-message'
60
+ name: string
61
+ snsTopicArn: Input<string>
62
+ }
63
+
64
+ interface ExecuteLambdaConfig {
65
+ type: 'execute-lambda'
66
+ name: string
67
+ lambdaArn: Input<string>
68
+ }
69
+
70
+ interface EventBridgeConfig {
71
+ type: 'event-bridge'
72
+ level: 'enable-disable-rule'
73
+ name: string
74
+ ruleArn: Input<string>
75
+ }
@@ -0,0 +1,8 @@
1
+ import {SimpleIamRolePolicy} from './SimpleIamRolePolicy'
2
+
3
+ export * from './SimpleIamRolePolicy'
4
+ export * from './SimpleIamRolePolicyConfig'
5
+
6
+ export const iam = {
7
+ SimpleIamRolePolicy,
8
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ import {auth0} from './auth0'
2
+ import {domain} from './domain'
3
+ import {iam} from './iam'
4
+ import {rest} from './rest'
5
+ import {lambda} from './lambda'
6
+ import {mongodb} from './mongodb'
7
+ import {s3} from './s3'
8
+ import {website} from './website'
9
+ import {websocket} from './websocket'
10
+
11
+ export * from './auth0'
12
+ export * from './domain'
13
+ export * from './iam'
14
+ export * from './lambda'
15
+ export * from './mongodb'
16
+ export * from './rest'
17
+ export * from './s3'
18
+ export * from './website'
19
+ export * from './websocket'
20
+
21
+ export const mstuercke = {
22
+ auth0,
23
+ domain,
24
+ iam,
25
+ lambda,
26
+ mongodb,
27
+ rest,
28
+ s3,
29
+ website,
30
+ websocket,
31
+ }
@@ -0,0 +1,120 @@
1
+ import {iam, lambda} from '@pulumi/aws'
2
+ import {getFile} from '@pulumi/archive'
3
+ import {asset, type CustomResourceOptions, type Output, Resource} from '@pulumi/pulumi'
4
+
5
+ import type {SimpleIamRolePolicyConfig} from '../iam'
6
+ import type {WithInputs} from './WithInputs'
7
+ import {mstuercke} from '../index'
8
+
9
+ export type NodeLambdaFunctionArgs<EnvironmentVariables> = {
10
+ name: string
11
+ runtime?: string
12
+ functionHandler?: string
13
+ memorySize?: number
14
+ timeoutSeconds?: number
15
+ lambdaPath: string
16
+ additionalIamRolePolicies?: SimpleIamRolePolicyConfig[]
17
+ environmentVariables?: WithInputs<EnvironmentVariables>
18
+ projectId: string
19
+ }
20
+
21
+ export class NodeLambdaFunction<
22
+ EnvironmentVariables extends Record<string, string> = Record<string, string>,
23
+ > extends Resource {
24
+ name: string
25
+ arn: Output<string>
26
+ invokeArn: Output<string>
27
+
28
+ constructor(args: NodeLambdaFunctionArgs<EnvironmentVariables>, opts?: CustomResourceOptions) {
29
+ super('mstuercke:lambda:NodeLambdaFunction', args.name, false, undefined, opts)
30
+ const {
31
+ name,
32
+ additionalIamRolePolicies = [],
33
+ environmentVariables = undefined,
34
+ runtime = 'nodejs20.x',
35
+ functionHandler = 'lambdaFunction.lambdaHandler',
36
+ memorySize = 128,
37
+ timeoutSeconds = 30,
38
+ projectId,
39
+ } = args
40
+
41
+ const role = new iam.Role(
42
+ name,
43
+ {
44
+ name: `${name}-lambda`,
45
+ assumeRolePolicy: JSON.stringify({
46
+ Version: '2012-10-17',
47
+ Statement: [
48
+ {
49
+ Action: 'sts:AssumeRole',
50
+ Principal: {Service: 'lambda.amazonaws.com'},
51
+ Effect: 'Allow',
52
+ },
53
+ ],
54
+ }),
55
+ tags: {
56
+ project: projectId,
57
+ },
58
+ },
59
+ {parent: this},
60
+ )
61
+
62
+ role.id.apply((roleId) => {
63
+ new mstuercke.iam.SimpleIamRolePolicy(
64
+ roleId,
65
+ {
66
+ type: 'custom',
67
+ name: 'logging',
68
+ statements: [
69
+ {
70
+ Effect: 'Allow',
71
+ Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
72
+ Resource: '*',
73
+ },
74
+ ],
75
+ },
76
+ {parent: this},
77
+ )
78
+
79
+ for (const config of additionalIamRolePolicies) {
80
+ new mstuercke.iam.SimpleIamRolePolicy(roleId, config, {parent: this})
81
+ }
82
+ })
83
+
84
+ const lambdaZip = getFile({
85
+ type: 'zip',
86
+ sourceDir: args.lambdaPath,
87
+ outputPath: `dist/lambda/${name}.zip`,
88
+ })
89
+
90
+ const lambdaFunction = new lambda.Function(
91
+ name,
92
+ {
93
+ name: name,
94
+ handler: functionHandler,
95
+ runtime: runtime,
96
+ memorySize: memorySize,
97
+ timeout: timeoutSeconds,
98
+
99
+ code: new asset.FileArchive(lambdaZip.then((lambdaZip) => lambdaZip.outputPath)),
100
+ sourceCodeHash: lambdaZip.then((lambdaZip) => lambdaZip.outputBase64sha256),
101
+
102
+ role: role.arn,
103
+
104
+ environment: {
105
+ variables: {
106
+ ...environmentVariables,
107
+ },
108
+ },
109
+ tags: {
110
+ project: projectId,
111
+ },
112
+ },
113
+ {parent: this},
114
+ )
115
+
116
+ this.name = name
117
+ this.arn = lambdaFunction.arn
118
+ this.invokeArn = lambdaFunction.invokeArn
119
+ }
120
+ }
@@ -0,0 +1,5 @@
1
+ import type {Input} from '@pulumi/pulumi'
2
+
3
+ export type WithInputs<T> = {
4
+ [key in keyof T]: Input<T[key]>
5
+ }
@@ -0,0 +1,7 @@
1
+ import {NodeLambdaFunction} from './NodeLambdaFunction'
2
+
3
+ export * from './NodeLambdaFunction'
4
+
5
+ export const lambda = {
6
+ NodeLambdaFunction,
7
+ }