Permission Issues When Creating GCP Projects with GitHub Actions and Workload Identity Federation

69 views Asked by At

I am trying to run a script in my Github Actions CI/CD Workflow that checks for the existence of a Google Cloud Project and if not found it will create the project.

But I am having permission problems.

This is the relevant part of the Github Workflow that uses the Google Auth action with Direct Workload Identity Federation and then calls the JavaScript script for project check and creation:

      - name: Google Cloud Auth
        uses: google-github-actions/auth@v2
        with:
          project_id: ${{ vars.GCP_ADMIN_PROJECT_ID }}
          workload_identity_provider: ${{ secrets.GCP_ADMIN_PROJECT_WORKLOAD_IDENTITY_PROVIDER }}

      - name: Check and Maybe Create Project
        run: node ./ci-cd/scripts/create-project.js
        env:
          PROJECT_ID: ${{ env.PROJECT_ID }}
          PROJECT_DISPLAY_NAME: ${{ inputs.environment }}

And here is the relevant JavaScript code for create-project.js:

import { cleanEnv, makeValidator } from "envalid";
import { google } from "googleapis";

// Validate environment variables are set and are non-empty strings
const nonEmptyString = makeValidator((x) => {
  if (typeof x === "string" && x.trim().length > 0) return x;
  else throw new Error("Env variable must be a non-empty string");
});
const env = cleanEnv(process.env, {
  PROJECT_ID: nonEmptyString(),
  PROJECT_DISPLAY_NAME: nonEmptyString(),
});

// Check and create the project
const createProject = async () => {
  // Google Auth
  const auth = new google.auth.GoogleAuth();
  const client = await auth.getClient();
  google.options({ auth: client });

  // Check if project exists, if not create it
  const cloudresourcemanager = google.cloudresourcemanager("v3");
  try {
    await cloudresourcemanager.projects.get({
      name: `projects/${env.PROJECT_ID}`,
    });
    console.log("Project already exists.");
  } catch (error) {
    // If the project does not exist there will be an error, so catch it and create the project here
    try {
      const operation = await cloudresourcemanager.projects.create({
        requestBody: {
          projectId: env.PROJECT_ID,
          displayName: env.PROJECT_DISPLAY_NAME,
        },
      });
      // Poll the Operation until done
      let response;
      do {
        try {
          response = await cloudresourcemanager.operations.get({
            name: operation.name,
          });
          await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds before polling again
        } catch (error) {
          console.error(`Failed to get the Google Operation: ${error}`);
          throw error;
        }
      } while (!response.data.done);
    } catch (error) {
      console.error(`Failed to create the Google Project: ${error}`);
      throw error;
    }
  }

};

await createProject();

When I run the workflow the Google Cloud Auth step passes and works correctly but the Check and Maybe Create Project step fails with this error:

Failed to create the Google Project: Error: The caller does not have permission

      error: {
        code: 403,
        message: 'The caller does not have permission',
        errors: [
          {
            message: 'The caller does not have permission',
            domain: 'global',
            reason: 'forbidden'
          }
        ],
        status: 'PERMISSION_DENIED'
      }

Since I am using Direct Workload Identity Federation in the Google Auth Action I thought maybe I need to assign the roles/resourcemanager.projectCreator role at the organisation level for the Workload Pool. I did this using the below gcloud CLI command. Unfortunately it did not work because I still see the same error (as shown above).

export REPO="MyOrg/myrepo"
export WORKLOAD_IDENTITY_POOL_ID="projects/111111111111/locations/global/workloadIdentityPools/github"

gcloud organizations add-iam-policy-binding "222222222222" \
  --role="roles/resourcemanager.projectCreator" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

Note: I am using an "Admin" project which is where the Workload Identity Pool is setup. It is used for Google Auth and creating the new project. The Logs Explorer for this project shows nothing at all, not even logs showing Workload Identity Pool auth. But I am not sure if that would show in the logs even if everything was working.

1

There are 1 answers

0
TinyTiger On

Turns out I needed parent listed on the project I was trying to create. That property is needed in combination with the roles/resourcemanager.projectCreator role at the organisation level which I already has as per my original post.

Here is the full code, also updated to use latest "@google-cloud/resource-manager package instead of googleapis.

import { cleanEnv, makeValidator } from "envalid";
import { ProjectsClient } from "@google-cloud/resource-manager";

// Validate environment variables are non-empty strings
const nonEmptyString = makeValidator((x) => {
  if (typeof x === "string" && x.trim().length > 0) return x;
  else throw new Error("Env variable must be a non-empty string");
});
const env = cleanEnv(process.env, {
  PROJECT_ID: nonEmptyString(), // Needs to be set during script execution
  PROJECT_DISPLAY_NAME: nonEmptyString(), // Needs to be set during script execution
  ORGANIZATION_ID: nonEmptyString(), // Needs to be set during script execution
  BUCKET_NAME: nonEmptyString(), // Needs to be set during script execution
});

const createProject = async () => {
  const projectsClient = new ProjectsClient();
  try {
    console.log("Attempting to create a Project.");
    const project = {
      projectId: env.PROJECT_ID,
      displayName: env.PROJECT_DISPLAY_NAME,
      parent: `organizations/${env.ORGANIZATION_ID}`,
    };
    const [operation] = await projectsClient.createProject({ project });
    const [response] = await operation.promise();
    console.log(`Project ${response.projectId} created.`);
  } catch (error) {
    if (error.code === 6) {
      // Project already exists, check if it's active or pending deletion
      const [project] = await projectsClient.getProject({
        name: `projects/${env.PROJECT_ID}`,
      });
      if (project.lifecycleState === "ACTIVE") {
        // Project already exists and is active, swallow the error and do nothing
        console.log(
          "Project already exists and is active, no need to create it.",
        );
      } else {
        // Project already exists and is pending deletion, throw an error
        throw new Error(
          `Project ${env.PROJECT_ID} already exists and is pending deletion. Undelete the project if you need it.`,
        );
      }
    } else {
      // Unexpected error
      throw error;
    }
  }
};

try {
  await createProject();
} catch (error) {
  console.error(error);
  throw error;
}