google / googleapis.dart

Repository for building the googleapis packages
https://pub.dev/packages/googleapis
BSD 3-Clause "New" or "Revised" License
393 stars 118 forks source link

Support authentication with external_account with Workload Identity Federation #606

Open alexeyinkin opened 7 months ago

alexeyinkin commented 7 months ago

I try to run an app from a GitHub workflow. It needs to communicate with Google Cloud. The workflow uses Workload Identity Federation to authenticate. It produces a key of the type external_account while googleapis_auth currently only handles the type service_account.

Given that Workflow Identity Federation is the recommended way to access Google Cloud resources, I think this should be fixed.

Steps to Reproduce

1. Create a project and configure Workload Identity Federation:

export PROJECT='my-project-id'
export ORGANIZATION='my-organization-id'
export GITHUB_USERNAME='my-github-username'
export REPO='my-github-repo'

gcloud projects create $PROJECT --name=$PROJECT --organization=$ORGANIZATION
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT --format='value(projectNumber)')

gcloud services enable cloudbilling.googleapis.com --project=$PROJECT
gcloud services enable iam.googleapis.com --project=$PROJECT
gcloud iam service-accounts create "testing" --project=$PROJECT

gcloud projects add-iam-policy-binding $PROJECT \
  --member="serviceAccount:testing@$PROJECT.iam.gserviceaccount.com" \
  --role="roles/owner" \
  --project=$PROJECT

gcloud iam workload-identity-pools create "github" --location="global" --project=$PROJECT

gcloud iam \
  workload-identity-pools providers create-oidc "github" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --location="global" \
  --workload-identity-pool="github" \
  --project=$PROJECT

gcloud projects add-iam-policy-binding $PROJECT \
  --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/github/attribute.repository/$GITHUB_USERNAME/$REPO" \
  --role="roles/viewer" \
  --project=$PROJECT

2. Create a Dart app

pubspec.yaml:

name: test1
publish_to: none

environment:
  sdk: ^3.2.2

dependencies:
  googleapis_auth: ^1.4.1

main.dart:

import 'dart:io';

import 'package:googleapis_auth/auth_io.dart';

Future<void> main() async {
  print(File(Platform.environment['GOOGLE_APPLICATION_CREDENTIALS']!).readAsStringSync());
  await clientViaApplicationDefaultCredentials(scopes: []);
}

3. Set up the workflow

Create the repository variables: PROJECT_NAME, PROJECT_NUMBER.

The workflow:

on:
  - workflow_dispatch

jobs:
  test:
    permissions:
      id-token: write

    runs-on: ubuntu-latest
    steps:

      - uses: actions/checkout@v4

      - uses: 'google-github-actions/auth@v2.1.2'
        with:
          workload_identity_provider: 'projects/${{ vars.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github/providers/github'
          service_account: 'testing@${{ vars.PROJECT_NAME }}.iam.gserviceaccount.com'

      - uses: dart-lang/setup-dart@v1
        with:
          sdk: 3.3.2

      - name: 'Test'
        run: |
          dart pub get
          dart main.dart

4. Run the workflow

Expected

No error

Actual

Run dart pub get
  dart pub get
  dart main.dart
  shell: /usr/bin/bash -e {0}
  env:
    CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: /home/runner/work/my-github-repo/my-github-repo/gha-creds-5647adab863c00a9.json
    GOOGLE_APPLICATION_CREDENTIALS: /home/runner/work/my-github-repo/my-github-repo/gha-creds-5647adab863c00a9.json
    GOOGLE_GHA_CREDS_PATH: /home/runner/work/my-github-repo/my-github-repo/gha-creds-5647adab863c00a9.json
    CLOUDSDK_CORE_PROJECT: my-project-id
    CLOUDSDK_PROJECT: my-project-id
    GCLOUD_PROJECT: my-project-id
    GCP_PROJECT: my-project-id
    GOOGLE_CLOUD_PROJECT: my-project-id
    DART_HOME: /opt/hostedtoolcache/dart/3.3.2/x64
    PUB_CACHE: /home/runner/.pub-cache
    PUB_TOKEN: ***
Resolving dependencies...
+ args 2.4.2
+ async 2.11.0
+ collection 1.18.0
+ crypto 3.0.3
+ google_identity_services_web 0.3.1+1
+ googleapis_auth 1.5.0
+ http 1.2.1
+ http_parser 4.0.2
+ meta 1.12.0
+ path 1.9.0
+ source_span 1.10.0
+ string_scanner 1.2.0
+ term_glyph 1.2.1
+ typed_data 1.3.2
+ web 0.5.1
Changed 15 dependencies!
{"type":"external_account","audience":"//iam.googleapis.com/projects/my-project-number/locations/global/workloadIdentityPools/github/providers/github","subject_token_type":"urn:ietf:params:oauth:token-type:jwt","token_url":"https://sts.googleapis.com/v1/token","credential_source":{"url":"https://pipelinesghubeus14.actions.githubusercontent.com/***/00000000-0000-0000-0000-000000000000/_apis/distributedtask/hubs/Actions/plans/***/jobs/5264e576-3c6f-51f6-f055-fab409685f20/idtoken?api-version=2.0&audience=https%3A%2F%2Fiam.googleapis.com%2Fprojects%2Fmy-project-number%2Flocations%2Fglobal%2FworkloadIdentityPools%2Fgithub%2Fproviders%2Fgithub","headers":{"Authorization":"***"},"format":{"type":"json","subject_token_field_name":"value"}},"service_account_impersonation_url":"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testing@my-project-id.iam.gserviceaccount.com:generateAccessToken"}
Unhandled exception:
Invalid argument(s): The given credentials are not of type service_account (was: external_account).
#0      new ServiceAccountCredentials.fromJson (package:googleapis_auth/src/service_account_credentials.dart:55:7)
#1      fromApplicationsCredentialsFile (package:googleapis_auth/src/adc_utils.dart:59:31)
<asynchronous suspension>
#2      clientViaApplicationDefaultCredentials (package:googleapis_auth/auth_io.dart:61:12)
<asynchronous suspension>
#3      main (file:///home/runner/work/my-github-repo/my-github-repo/main.dart:7:3)
<asynchronous suspension>
Error: Process completed with exit code 255.
Isakdl commented 1 week ago

Ran into the same issue. Looking at the code it seems there is a check for this and throws the ArgumentError but the actual type is never used by the code.

Offending code: https://github.com/google/googleapis.dart/blob/e7ae6b27ad412d9a858935a88b28297ff548cdc4/googleapis_auth/lib/src/service_account_credentials.dart#L52C1-L59C6

I wonder if there are some restrictions in GCP or could this check simply be removed?

Edit: No these are indeed very different ways to authenticate and the block above is needed for the current way the package is authenticating in.