diff --git a/examples/tests/sdk_tests/bucket/load_test.test.w b/examples/tests/sdk_tests/bucket/load_test.test.w index 8231cb5da8c..0c4bd3a6545 100644 --- a/examples/tests/sdk_tests/bucket/load_test.test.w +++ b/examples/tests/sdk_tests/bucket/load_test.test.w @@ -2,8 +2,8 @@ bring cloud; let b = new cloud.Bucket(); -test "uploading many objects" { +new std.Test(inflight () => { for i in 0..500 { b.put("test{i}", "{i}"); } -} +}, timeout: 3m) as "uploading many objects"; diff --git a/libs/wingsdk/src/target-tf-gcp/bucket.ts b/libs/wingsdk/src/target-tf-gcp/bucket.ts index a0d7f3e6968..23b6cd25134 100644 --- a/libs/wingsdk/src/target-tf-gcp/bucket.ts +++ b/libs/wingsdk/src/target-tf-gcp/bucket.ts @@ -1,7 +1,8 @@ +import { Fn } from "cdktf"; import { Construct } from "constructs"; import { App } from "./app"; import { Function as GCPFunction } from "./function"; -import { calculateBucketPermissions } from "./permissions"; +import { createBucketPermissions } from "./permissions"; import { ProjectService } from "../.gen/providers/google/project-service"; import { StorageBucket } from "../.gen/providers/google/storage-bucket"; import { StorageBucketIamMember } from "../.gen/providers/google/storage-bucket-iam-member"; @@ -197,8 +198,24 @@ export class Bucket extends cloud.Bucket { throw new Error("buckets can only be bound by tfgcp.Function for now"); } - const permissions = calculateBucketPermissions(ops); - host.addPermissions(permissions); + if (ops.includes(cloud.BucketInflightMethods.SIGNED_URL)) { + host._addTokenCreator(); + } + + for (const role of createBucketPermissions(ops)) { + const bucketHash = Fn.sha256(this.bucket.name).slice(-8); + const permissionHash = Fn.sha256(role).slice(-8); + + new StorageBucketIamMember( + this, + `bucket-iam-member-${bucketHash}-${permissionHash}`, + { + bucket: this.bucket.name, + role: role, + member: `serviceAccount:${host.serviceAccountEmail}`, + } + ); + } host.addEnvironment(this.envName(), this.bucket.name); super.onLift(host, ops); diff --git a/libs/wingsdk/src/target-tf-gcp/counter.ts b/libs/wingsdk/src/target-tf-gcp/counter.ts index 2abd137632c..160bc296d62 100644 --- a/libs/wingsdk/src/target-tf-gcp/counter.ts +++ b/libs/wingsdk/src/target-tf-gcp/counter.ts @@ -72,8 +72,9 @@ export class Counter extends cloud.Counter { throw new Error("counters can only be bound by tfgcp.Function for now"); } - const permissions = calculateCounterPermissions(ops); - host.addPermissions(permissions); + for (const role of calculateCounterPermissions(ops)) { + host._addProjectIamMember(role); + } host.addEnvironment(this.envName(), this.database.name); super.onLift(host, ops); diff --git a/libs/wingsdk/src/target-tf-gcp/function.ts b/libs/wingsdk/src/target-tf-gcp/function.ts index fd644a1f0de..93ce82b28df 100644 --- a/libs/wingsdk/src/target-tf-gcp/function.ts +++ b/libs/wingsdk/src/target-tf-gcp/function.ts @@ -7,9 +7,9 @@ import { Bucket } from "./bucket"; import { core } from ".."; import { CloudfunctionsFunction } from "../.gen/providers/google/cloudfunctions-function"; import { CloudfunctionsFunctionIamMember } from "../.gen/providers/google/cloudfunctions-function-iam-member"; -import { ProjectIamCustomRole } from "../.gen/providers/google/project-iam-custom-role"; import { ProjectIamMember } from "../.gen/providers/google/project-iam-member"; import { ServiceAccount } from "../.gen/providers/google/service-account"; +import { ServiceAccountIamMember } from "../.gen/providers/google/service-account-iam-member"; import { StorageBucketObject } from "../.gen/providers/google/storage-bucket-object"; import * as cloud from "../cloud"; import { LiftMap } from "../core"; @@ -73,11 +73,8 @@ export class Function extends cloud.Function { } private readonly function: CloudfunctionsFunction; - private readonly functionServiceAccount: ServiceAccount; - private readonly functionCustomRole: ProjectIamCustomRole; - private readonly permissions: Set = new Set([ - "cloudfunctions.functions.get", - ]); + public readonly functionServiceAccount: ServiceAccount; + private roles?: Set = new Set(); private assetPath: string | undefined; // posix path @@ -136,7 +133,7 @@ export class Function extends cloud.Function { } ); - // Step 1: Create Custom Service Account + // Create Custom Service Account this.functionServiceAccount = new ServiceAccount( this, `ServiceAccount${this.node.addr.substring(-8)}`, @@ -147,25 +144,7 @@ export class Function extends cloud.Function { )}`, } ); - // Step 2: Create Custom Role - this.functionCustomRole = new ProjectIamCustomRole( - this, - `CustomRole${this.node.addr.substring(-8)}`, - { - roleId: `cloudfunctions.custom${this.node.addr.substring(-8)}`, - title: `Custom Role for Cloud Function ${this.node.addr.substring(-8)}`, - permissions: Lazy.listValue({ - produce: () => Array.from(this.permissions), - }), - } - ); - // Step 3: Grant Custom Role to Custom Service Account on the Project - new ProjectIamMember(this, "ProjectIamMember", { - project: app.projectId, - role: `projects/${app.projectId}/roles/${this.functionCustomRole.roleId}`, - member: `serviceAccount:${this.functionServiceAccount.email}`, - }); - // Step 4: Create the Cloud Function with Custom Service Account + // Create the Cloud Function with Custom Service Account this.function = new CloudfunctionsFunction(this, "DefaultFunction", { name: ResourceNames.generateName(this, FUNCTION_NAME_OPTS), description: "This function was created by Wing", @@ -290,12 +269,6 @@ export class Function extends cloud.Function { ); } - public addPermissions(permissions: string[]): void { - permissions.forEach((permission) => { - this.permissions.add(permission); - }); - } - public onLift(host: IInflightHost, ops: string[]): void { if (!(host instanceof Function)) { throw new Error( @@ -304,7 +277,7 @@ export class Function extends cloud.Function { } if (ops.includes(cloud.FunctionInflightMethods.INVOKE)) { - host.addPermissions(["cloudfunctions.functions.invoke"]); + this._addPermissionToInvoke(host.serviceAccountEmail); } const { region, projectId } = App.of(this) as App; @@ -320,16 +293,43 @@ export class Function extends cloud.Function { * @param serviceAccount The service account to grant invoke permissions to. * @internal */ - public _addPermissionToInvoke(serviceAccount: ServiceAccount): void { - const hash = Fn.sha256(serviceAccount.email).slice(-8); + public _addPermissionToInvoke(serviceAccountEmail: string): void { + const hash = Fn.sha256(serviceAccountEmail).slice(-8); new CloudfunctionsFunctionIamMember(this, `invoker-permission-${hash}`, { project: this.function.project, region: this.function.region, cloudFunction: this.function.name, role: "roles/cloudfunctions.invoker", - member: `serviceAccount:${serviceAccount.email}`, + member: `serviceAccount:${serviceAccountEmail}`, + }); + } + + public _addTokenCreator() { + const tokenCreatorRole = "roles/iam.serviceAccountTokenCreator"; + if (this.roles?.has(tokenCreatorRole)) { + return; + } + const hash = Fn.sha256(this.serviceAccountEmail).slice(-8); + new ServiceAccountIamMember(this, `service-iam-member-${hash}`, { + serviceAccountId: this.functionServiceAccount.name, + role: "roles/iam.serviceAccountTokenCreator", + member: `serviceAccount:${this.functionServiceAccount.email}`, + }); + this.roles?.add(tokenCreatorRole); + } + + public _addProjectIamMember(role: string) { + if (this.roles?.has(role)) { + return; + } + const hash = Fn.sha256(this.functionServiceAccount.email).slice(-8); + new ProjectIamMember(this, `project-iam-member-${hash}`, { + project: (App.of(this) as any).projectId, + member: `serviceAccount:${this.functionServiceAccount.email}`, + role, }); + this.roles?.add(role); } /** @internal */ diff --git a/libs/wingsdk/src/target-tf-gcp/permissions.ts b/libs/wingsdk/src/target-tf-gcp/permissions.ts index 3a5aa902a4e..b6589a2146a 100644 --- a/libs/wingsdk/src/target-tf-gcp/permissions.ts +++ b/libs/wingsdk/src/target-tf-gcp/permissions.ts @@ -1,59 +1,42 @@ import * as cloud from "../cloud"; -export function calculateBucketPermissions(ops: string[]): string[] { +export function createBucketPermissions(ops: string[]): string[] { const permissions: string[] = []; - if ( - ops.includes(cloud.BucketInflightMethods.GET) || - ops.includes(cloud.BucketInflightMethods.GET_JSON) || - ops.includes(cloud.BucketInflightMethods.TRY_GET) || - ops.includes(cloud.BucketInflightMethods.TRY_GET_JSON) || - ops.includes(cloud.BucketInflightMethods.TRY_DELETE) || - ops.includes(cloud.BucketInflightMethods.EXISTS) || - ops.includes(cloud.BucketInflightMethods.METADATA) || - ops.includes(cloud.BucketInflightMethods.PUBLIC_URL) || - ops.includes(cloud.BucketInflightMethods.COPY) || - ops.includes(cloud.BucketInflightMethods.RENAME) || - ops.includes(cloud.BucketInflightMethods.SIGNED_URL) - ) { - permissions.push("storage.objects.get"); + if (ops.includes(cloud.BucketInflightMethods.PUBLIC_URL)) { + permissions.push("roles/storage.insightsCollectorService"); } if ( - ops.includes(cloud.BucketInflightMethods.PUT) || - ops.includes(cloud.BucketInflightMethods.PUT_JSON) || + ops.includes(cloud.BucketInflightMethods.DELETE) || + ops.includes(cloud.BucketInflightMethods.TRY_DELETE) || ops.includes(cloud.BucketInflightMethods.COPY) || - ops.includes(cloud.BucketInflightMethods.RENAME) || - ops.includes(cloud.BucketInflightMethods.SIGNED_URL) + ops.includes(cloud.BucketInflightMethods.RENAME) ) { - permissions.push("storage.objects.create"); + permissions.push("roles/storage.objectUser"); + return permissions; } if ( - ops.includes(cloud.BucketInflightMethods.DELETE) || - ops.includes(cloud.BucketInflightMethods.TRY_DELETE) || ops.includes(cloud.BucketInflightMethods.PUT) || ops.includes(cloud.BucketInflightMethods.PUT_JSON) || - ops.includes(cloud.BucketInflightMethods.COPY) || - ops.includes(cloud.BucketInflightMethods.RENAME) || ops.includes(cloud.BucketInflightMethods.SIGNED_URL) ) { - permissions.push("storage.objects.delete"); + permissions.push("roles/storage.objectCreator"); } if ( + ops.includes(cloud.BucketInflightMethods.GET) || + ops.includes(cloud.BucketInflightMethods.GET_JSON) || + ops.includes(cloud.BucketInflightMethods.TRY_GET) || + ops.includes(cloud.BucketInflightMethods.TRY_GET_JSON) || + ops.includes(cloud.BucketInflightMethods.EXISTS) || + ops.includes(cloud.BucketInflightMethods.METADATA) || + ops.includes(cloud.BucketInflightMethods.PUBLIC_URL) || ops.includes(cloud.BucketInflightMethods.LIST) || ops.includes(cloud.BucketInflightMethods.SIGNED_URL) ) { - permissions.push("storage.objects.list"); - } - - if (ops.includes(cloud.BucketInflightMethods.PUBLIC_URL)) { - permissions.push("storage.buckets.get"); - } - - if (ops.includes(cloud.BucketInflightMethods.SIGNED_URL)) { - permissions.push("iam.serviceAccounts.signBlob"); + permissions.push("roles/storage.objectViewer"); } return permissions; @@ -62,29 +45,13 @@ export function calculateBucketPermissions(ops: string[]): string[] { export function calculateCounterPermissions(ops: string[]): string[] { const permissions: string[] = []; - if ( - ops.includes(cloud.CounterInflightMethods.PEEK) || - ops.includes(cloud.CounterInflightMethods.INC) || - ops.includes(cloud.CounterInflightMethods.DEC) - ) { - permissions.push("datastore.entities.get"); - } - - if ( - ops.includes(cloud.CounterInflightMethods.PEEK) || - ops.includes(cloud.CounterInflightMethods.INC) || - ops.includes(cloud.CounterInflightMethods.DEC) || - ops.includes(cloud.CounterInflightMethods.SET) - ) { - permissions.push("datastore.entities.create"); - } - if ( ops.includes(cloud.CounterInflightMethods.INC) || ops.includes(cloud.CounterInflightMethods.DEC) || - ops.includes(cloud.CounterInflightMethods.SET) + ops.includes(cloud.CounterInflightMethods.SET) || + ops.includes(cloud.CounterInflightMethods.PEEK) ) { - permissions.push("datastore.entities.update"); + permissions.push("roles/datastore.user"); } return permissions; diff --git a/libs/wingsdk/src/target-tf-gcp/schedule.ts b/libs/wingsdk/src/target-tf-gcp/schedule.ts index 7a9609605de..d2bb294e636 100644 --- a/libs/wingsdk/src/target-tf-gcp/schedule.ts +++ b/libs/wingsdk/src/target-tf-gcp/schedule.ts @@ -66,7 +66,7 @@ export class Schedule extends cloud.Schedule { ); // allow scheduler service account to invoke cron function - cronFunction._addPermissionToInvoke(schedulerServiceAccount); + cronFunction._addPermissionToInvoke(schedulerServiceAccount.email); // create scheduler new CloudSchedulerJob(this, "Scheduler", {