Commit 7d93b7dc authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

feat: retrieve OIDC IdP and service account from TBC standard variables

parent 7ffdba2f
Loading
Loading
Loading
Loading
+37 −20
Original line number Diff line number Diff line
@@ -78,38 +78,55 @@ test-ping:
      assert_eq "200" $response_status
      assert_eq "pong" $(cat resp.txt)

test-token-assert-200:

# test: get token with implicit TBC env detection fails if no TBC variables are set (error 400)
test-token-no-tbc-vars-fails:
  extends: .test-base
  variables:
    CI_JOB_JWT_V2: $CI_JOB_JWT_V2
    GCP_OIDC_ACCOUNT: ""
    GCP_OIDC_PROVIDER: ""
  script:
    # test: get token with valid oidc account and provider shall succeed 
    - |
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token?serviceAccount=${GCP_OIDC_ACCOUNT}&workloadIdentityProvider=${GCP_OIDC_PROVIDER}")
      assert_eq "200" $response_status
      token=`cat resp.txt`
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token")
      assert_eq "400" $response_status

      response_status=$(curl -s -o resp.txt -w "%{http_code}" -H "Authorization: Bearer $token" "https://cloudresourcemanager.googleapis.com/v1/projects/$GCP_PROJECT")
      assert_eq "200" $response_status
      project_id_result=$(cat resp.txt | jq .projectId | tr -d '"')
      assert_eq "$GCP_PROJECT" $project_id_result
  rules:
    - if: '$GCP_OIDC_ACCOUNT && $GCP_OIDC_PROVIDER'
# test: get token with implicit TBC env detection fails if missing CI_JOB_JWT_2 (error 401)
test-token-no-jwt-fails:
  extends: .test-base
  variables:
    GCP_OIDC_ACCOUNT: "invalid"
    GCP_OIDC_PROVIDER: "invalid"
  script:
    - |
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token")
      assert_eq "401" $response_status

test-token-assert-500:
# test: get token with invalid OIDC account fails with 500
test-token-invalid-oidc-fails:
  extends: .test-base
  variables:
    GCP_OIDC_ACCOUNT: "invalid"
    GCP_OIDC_PROVIDER: "invalid"
    CI_JOB_JWT_V2: $CI_JOB_JWT_V2
  # test: get token with missing oidc account fail with 500
  script:
    - |
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token?serviceAccount=invalid&workloadIdentityProvider=invalid")
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token")
      assert_eq "500" $response_status

test-token-assert-401:
# test: get token with valid OIDC account and provider shall succeed 
test-token-succeeds:
  extends: .test-base
  # test: get token with missing CI_JOB_JWT_2 fail with 401
  variables:
    CI_JOB_JWT_V2: $CI_JOB_JWT_V2
  script:
    - |
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token?serviceAccount=${GCP_OIDC_ACCOUNT}&workloadIdentityProvider=${GCP_OIDC_PROVIDER}")
      assert_eq "401" $response_status
 No newline at end of file
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token")
      assert_eq "200" $response_status
      token=`cat resp.txt`

      response_status=$(curl -s -o resp.txt -w "%{http_code}" -H "Authorization: Bearer $token" "https://cloudresourcemanager.googleapis.com/v1/projects/$GCP_PROJECT")
      assert_eq "200" $response_status
      project_id_result=$(cat resp.txt | jq .projectId | tr -d '"')
      assert_eq "$GCP_PROJECT" $project_id_result
  rules:
    - if: '$GCP_OIDC_ACCOUNT && $GCP_OIDC_PROVIDER'
 No newline at end of file
+41 −12
Original line number Diff line number Diff line
@@ -3,9 +3,8 @@
This project builds a Docker image with an API able to retrieve an access token for a given project using google apis.


It is aimed at being used in GitLab CI as a service container
in order to decouple the image of your jobs and the way of retrieving a gpc oauth access token.

It is aimed at being used in GitLab CI as a [service container](https://docs.gitlab.com/ee/ci/services/)
in order to decouple the image of your jobs and the way of retrieving a [GCP OAuth access token](https://developers.google.com/identity/protocols/oauth2).

## How to use

@@ -17,19 +16,48 @@ The service exposes one single API to retrieve a google oauth access token.

Retrieve authentication token using API.

1. with **explicit** Service Account and Workload Identity Provider:
    ```
    GET /token?serviceAccount={serviceAccount}&workloadIdentityProvider={workloadIdentityProvider}
    ```
2. with **implicit** Service Account and Workload Identity Provider retrieved from to-be-continuous standard variables for an **explicitly** specified environment (_production_ here):
    ```
    GET /token?envType=prod
    ```
3. with **implicit** Service Account and Workload Identity Provider retrieved from to-be-continuous standard variables for **implicitly** guessed current environment):
    ```
    GET /token
    ```

#### Query Parameters


| Name                       | description                                                                               | required              | 
|----------------------------|-------------------------------------------------------------------------------------------|-----------------------|
| `serviceAccount`           | Default Service Account to which impersonate with OpenID Connect authentication           | true                  |
| `workloadIdentityProvider` | Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) | true |
| `serviceAccount`           | Default Service Account to which impersonate with OpenID Connect authentication           | `false` |
| `workloadIdentityProvider` | Default Workload Identity Provider associated with GitLab to [authenticate with OpenID Connect](https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/) | `false` |
| `envType`                  | The to-be-continuous environment type to retrieve associated Service Account and Workload Identity Provider values (one of `review`, `integ`, `staging` or `prod`).<br/>_Only when `serviceAccount` and `workloadIdentityProvider` are not specified_ | `false` |

##### How are implicitly determined `serviceAccount` and `workloadIdentityProvider` values?

When not explicitly set, `serviceAccount` and `workloadIdentityProvider` values are retrieved from to-be-continuous standard variables for the associated `envType` (`envType` itself may also be guessed, see next chapter):

| `envType`        | `serviceAccount` value | `workloadIdentityProvider` value |
| ---------------- | ---------------------- | -------------------------------- |
| `prod`           | `$GCP_PROD_OIDC_ACCOUNT` or `$GCP_OIDC_ACCOUNT` (fallback) | `$GCP_PROD_OIDC_PROVIDER` or `$GCP_OIDC_PROVIDER` (fallback) |
| `staging`        | `$GCP_STAGING_OIDC_ACCOUNT` or `$GCP_OIDC_ACCOUNT` (fallback) | `$GCP_STAGING_OIDC_PROVIDER` or `$GCP_OIDC_PROVIDER` (fallback) |
| `integ`          | `$GCP_INTEG_OIDC_ACCOUNT` or `$GCP_OIDC_ACCOUNT` (fallback) | `$GCP_INTEG_OIDC_PROVIDER` or `$GCP_OIDC_PROVIDER` (fallback) |
| `review`         | `$GCP_REVIEW_OIDC_ACCOUNT` or `$GCP_OIDC_ACCOUNT` (fallback) | `$GCP_REVIEW_OIDC_PROVIDER` or `$GCP_OIDC_PROVIDER` (fallback) |

##### How is guessed `envType`?

When not explicitly set, `envType` is automatically guessed based on [GitLab predefined variables](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html):

| `$CI_REF_NAME`     | `envType` value |
| ------------------ | --------------- |
| `master` or `main` | **prod** if `$CI_JOB_STAGE` is one of `publish`, `infra-prod`, `production`, `.post`<br/>**staging** otherwise |
| `develop`          | **integ** |
| _any other branch_ | **review** |

#### Example

@@ -52,18 +80,19 @@ Finally, the Docker image can be used in your GitLab CI files as follows:

```yaml
variables:
  GCP_OIDC_PROVIDER: "foo"
  GCP_OIDC_ACCOUNT: "bar"
  CI_JOB_JWT_V2: $CI_JOB_JWT_V2
  GCP_OIDC_PROVIDER: "your-oidc-identity-provider-here"
  GCP_OIDC_ACCOUNT: "your-oidc-service-account-here"

deploy-job:
  image: my-deploy-tool:latest
  services:
    # add GCP Auth Provider as a service
    - name: $CI_REGISTRY/to-be-continuous/tools/gcp-auth-provider:master
    - name: $CI_REGISTRY/to-be-continuous/tools/gcp-auth-provider:main
      alias: gcp-auth-provider
  before-script:
    # retrieve some token from Vault server
    - my_token=$(curl -s -S -f "http://gcp-auth-provider/token?serviceAccount={GCP_OIDC_ACCOUNT}&workloadIdentityProvider={GCP_OIDC_PROVIDER}
    - my_token=$(curl -s -S -f "http://gcp-auth-provider/token")
    # then login
    - my-deploy-tool login --token $my_token
  script:
+42 −4
Original line number Diff line number Diff line
from fastapi import FastAPI, Query
import os
import re

from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import PlainTextResponse
from pydantic import Required

from app.gcp_client import get_iam_credentials, get_sts_token

app = FastAPI()

def guess_env_type() -> str:
  ref_name = os.getenv("CI_REF_NAME", "-")
  prod_ref = os.getenv("PROD_REF", "/^(master|main)$/").strip("/")
  integ_ref = os.getenv("INTEG_REF", "/^develop$/").strip("/")

  if re.match(prod_ref, ref_name):
    # could be staging or prod
    if os.getenv("CI_JOB_STAGE", "-") in ["publish", "infra-prod", "production", ".post"]:
      return "prod"
    return "staging"
  elif re.match(integ_ref, ref_name):
    return "integ"
  else:
    return "review"


def get_oidc_account(env_type: str) -> str:
  return os.getenv(f"GCP_{env_type.upper()}_OIDC_ACCOUNT") or os.getenv("GCP_OIDC_ACCOUNT")


def get_oidc_provider(env_type: str) -> str:
  return os.getenv(f"GCP_{env_type.upper()}_OIDC_PROVIDER") or os.getenv("GCP_OIDC_PROVIDER")


@app.get('/ping', response_class=PlainTextResponse)
def ping():
@@ -14,10 +39,23 @@ def ping():

@app.get('/token')
def token(
    workload_identity_provider: str = Query(default=Required, alias='workloadIdentityProvider'),
    service_account: str = Query(default=Required, alias='serviceAccount')
    workload_identity_provider: str = Query(default=None, alias='workloadIdentityProvider'),
    service_account: str = Query(default=None, alias='serviceAccount'),
    env_type: str = Query(default=None, alias='envType'),
):
    # projects/%s/locations/global/workloadIdentityPools/%s/providers/%s
    if (not workload_identity_provider) or (not service_account):
        # retrieve from TBC standard variables
        if env_type is None:
            env_type = guess_env_type()
        workload_identity_provider = get_oidc_provider(env_type)
        service_account = get_oidc_account(env_type)
        if (not workload_identity_provider) or (not service_account):
            raise HTTPException(
                status_code=400,
                detail=f"Couldn't retrieve implicit OIDC provider/account from env for '{env_type}'"
            )

    audience = "//iam.googleapis.com/%s" % workload_identity_provider

    federated_token = get_sts_token(audience)