Skip to content

Commit

Permalink
woof - very confusing oidc string mapping from gcp to aws but hey we …
Browse files Browse the repository at this point in the history
…got there. read the docs, kids. running into an issue with upload but looks to be on the aws side so we have successfully crossed the moat with oidc
  • Loading branch information
GondekNP committed Jan 29, 2024
1 parent 69a78fa commit 1de2975
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 30 deletions.
5 changes: 4 additions & 1 deletion .deployment/tofu/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@ locals {
google_project_number = data.google_project.project.number
aws_account_id = data.aws_caller_identity.current.account_id
aws_region = data.aws_region.current.name
oidc_provider_domain_url = "https://accounts.google.com"
# oidc_provider_domain_url = "https://accounts.google.com"
oidc_provider_domain_url = "accounts.google.com"
gcp_cloud_run_client_id = "117526146749746854545" ## This is the ClientID of the cloud run instance, and can't be output from terraform!
}

# Initialize the modules
module "static_io" {
source = "./modules/static_io"
google_project_number = local.google_project_number
gcp_service_account_s3_email = module.burn_backend.gcp_service_account_s3_email
gcp_cloud_run_client_id = local.gcp_cloud_run_client_id
aws_account_id = local.aws_account_id
oidc_provider_domain_url = local.oidc_provider_domain_url
}
Expand Down
6 changes: 6 additions & 0 deletions .deployment/tofu/modules/burn_backend/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ resource "google_project_iam_member" "log_writer" {
member = "serviceAccount:${google_service_account.burn-backend-service.email}"
}

resource "google_project_iam_member" "oidc_token_creator" {
project = "dse-nps"
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${google_service_account.burn-backend-service.email}"
}

# Give the service account permissions to deploy to Cloud Run, and to Cloud Build, and to the Workload Identity Pool
resource "google_project_iam_member" "run_admin" {
project = "dse-nps"
Expand Down
7 changes: 6 additions & 1 deletion .deployment/tofu/modules/burn_backend/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ output "burn_backend_server_endpoint" {
value = google_cloud_run_v2_service.tf-rest-burn-severity.uri
}

output "burn_backend_server_uuid" {
description = "The UUID of the Cloud Run service"
value = google_cloud_run_v2_service.tf-rest-burn-severity.uid
}

output "gcp_service_account_s3_email" {
description = "The email of the service account used by the backend service on GCP Cloud Run"
value = google_service_account.burn-backend-service.email
}
}
41 changes: 38 additions & 3 deletions .deployment/tofu/modules/static_io/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -213,22 +213,57 @@ resource "aws_s3_bucket_object" "assets" {
# Set up STS to allow the GCP server to assume a role for AWS secrets

# Defines who can assume the role.
# Confusing string mapping for the OIDC provider URL (https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#ck_aud)
# example paylod of our token looks like:/
# {
# "aud": "sts.amazonaws.com",
# "azp": "117526146749746854545",
# "email": "[email protected]",
# "email_verified": true,
# "exp": 1706551963,
# "iat": 1706548363,
# "iss": "https://accounts.google.com",
# "sub": "117526146749746854545"
# }
# AWS says: aud -> azp, oaud -> aud, sub -> sub

data "aws_iam_policy_document" "oidc_assume_role_policy" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
actions = [
"sts:AssumeRoleWithWebIdentity"
]
effect = "Allow"

principals {
type = "Federated"
identifiers = ["arn:aws:iam::${var.aws_account_id}:oidc-provider/${var.oidc_provider_domain_url}"]
# identifiers = ["arn:aws:iam::${var.aws_account_id}:oidc-provider/${var.oidc_provider_domain_url}"]
identifiers = ["accounts.google.com"]
}

condition {
test = "StringEquals"
variable = "${var.oidc_provider_domain_url}:sub"

values = [
"system:serviceaccount:${var.google_project_number}.svc.id.goog[default/${var.gcp_service_account_s3_email}]"
"${var.gcp_cloud_run_client_id}"
]
}

condition {
test = "StringEquals"
variable = "${var.oidc_provider_domain_url}:aud"

values = [
"${var.gcp_cloud_run_client_id}"
]
}

condition {
test = "StringEquals"
variable = "${var.oidc_provider_domain_url}:oaud"

values = [
"sts.amazonaws.com"
]
}
}
Expand Down
5 changes: 5 additions & 0 deletions .deployment/tofu/modules/static_io/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ variable "aws_account_id" {
variable "oidc_provider_domain_url" {
description = "OIDC provider domain URL for GCP"
type = string
}

variable "gcp_cloud_run_client_id" {
description = "GCP Cloud Run client id for burn backend service"
type = string
}
5 changes: 5 additions & 0 deletions .deployment/tofu/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ output "s3_from_gcp_arn" {
description = "The ARN of the IAM Role which allows GCP to access S3"
value = module.static_io.s3_from_gcp_arn
}

output "gcp_cloud_run_uuid" {
description = "The UUID of the Cloud Run burn-backend service"
value = module.burn_backend.burn_backend_server_uuid
}
10 changes: 4 additions & 6 deletions .devcontainer/scripts/export_tofu_dotenv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ tofu refresh

export gcp_cloud_run_endpoint="$(tofu output gcp_cloud_run_endpoint)"
export s3_from_gcp_arn="$(tofu output s3_from_gcp_arn)"
export gcp_service_account_s3_email=$(tofu output gcp_service_account_s3_email)

# Remove quotes from the email to avoid issue with the impersonation below
export gcp_service_account_s3_email=$(tofu output gcp_service_account_s3_email | tr -d '"')

echo "# TOFU ENV VARS" >> /workspace/.devcontainer/.env
echo "ENV=LOCAL" >> /workspace/.devcontainer/.env
echo "S3_FROM_GCP_ARN=$s3_from_gcp_arn" >> /workspace/.devcontainer/.env
echo "GCP_CLOUD_RUN_ENDPOINT=$gcp_cloud_run_endpoint" >> /workspace/.devcontainer/.env
echo "GCP_SERVICE_ACCOUNT_S3_EMAIL=$gcp_service_account_s3_email" >> /workspace/.devcontainer/.env

# Set gcloud config to allow local development to behave as if it were running in the cloud
gcloud auth application-default login --impersonate-service-account $gcp_service_account_s3_email
echo "GOOGLE_APPLICATION_CREDENTIALS=/root/.config/gcloud/application_default_credentials.json" >> /workspace/.devcontainer/.env
echo "GCP_SERVICE_ACCOUNT_S3_EMAIL=$gcp_service_account_s3_email" >> /workspace/.devcontainer/.env
73 changes: 54 additions & 19 deletions src/util/cloud_static_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
import subprocess
import os
import boto3
from google.auth.transport import requests
from google.auth import exceptions
from google.oauth2 import id_token
import google.auth
import requests
from google.auth.transport import requests as gcp_requests
from google.auth import impersonated_credentials, exceptions

# TODO [#9]: Convert to agnostic Boto client
# Use the slick smart-open library to handle S3 connections. This maintains the agnostic nature
Expand Down Expand Up @@ -54,6 +55,7 @@ def __init__(self, bucket_name, provider):

self.env = os.environ.get("ENV")
self.role_arn = os.environ.get("S3_FROM_GCP_ARN")
self.service_account_email = os.environ.get("GCP_SERVICE_ACCOUNT_S3_EMAIL")
self.role_session_name = "burn-backend-session"

self.bucket_name = bucket_name
Expand All @@ -63,43 +65,76 @@ def __init__(self, bucket_name, provider):
log_name = "burn-backend"
self.logger = logging_client.logger(log_name)

boto3.set_stream_logger('')
self.sts_client = boto3.client('sts')

if provider == "s3":
self.prefix = f"s3://{self.bucket_name}/public"
else:
raise Exception(f"Provider {provider} not supported")

self.token = None
self.token_time_remaining = 0
self.iam_credentials = None
self.validate_credentials()

self.logger.log_text(f"Initialized CloudStaticIOClient for {self.bucket_name} with provider {provider}")

def impersonate_service_account(self):
# Load the credentials of the user
source_credentials, project = google.auth.default()

# Define the scopes of the impersonated credentials
target_scopes = ["https://www.googleapis.com/auth/cloud-platform"]

# Create the IAM credentials client for the impersonated service account
iam_credentials = impersonated_credentials.Credentials(
source_credentials=source_credentials,
target_principal=self.service_account_email,
target_scopes=target_scopes,
lifetime=3600
)

# Refresh the client
self.iam_credentials = iam_credentials

def fetch_id_token(self, audience):
if not self.iam_credentials.valid:
# Refresh the credentials
self.iam_credentials.refresh(Request())

# Make an authenticated HTTP request to the Google OAuth2 v1/token endpoint
url = f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{self.service_account_email}:generateIdToken"
headers = {"Authorization": f"Bearer {self.iam_credentials.token}"}
body = {"audience": audience, "includeEmail": True}
response = requests.post(url, headers=headers, json=body)

# Check the response
if response.status_code != 200:
raise exceptions.DefaultCredentialsError(
"Failed to fetch ID token: " + response.text
)

# Return the ID token
return response.json()["token"]

def validate_credentials(self):
oidc_token = None
request = gcp_requests.Request()

if self.env == 'LOCAL':
oidc_cli_call = subprocess.run(
["gcloud", "auth", "print-identity-token"],
stdout=subprocess.PIPE,
check=True,
)
oidc_token = oidc_cli_call.stdout.decode().strip()
elif self.env == 'CLOUD':
try:
oidc_token = id_token.fetch_id_token(requests.Request(), target_audience="sts.amazonaws.com")
except exceptions.GoogleAuthError as e:
print(f"Error when fetching ID token: {e}")

if not self.iam_credentials or self.iam_credentials.expired:
self.impersonate_service_account()
self.iam_credentials.refresh(request)

oidc_token = self.fetch_id_token(audience="sts.amazonaws.com")
if not oidc_token:
raise ValueError("Failed to retrieve OIDC token")

sts_response = self.sts_client.assume_role_with_web_identity(
RoleArn=self.role_arn,
RoleSessionName=self.role_session_name,
WebIdentityToken=oidc_token
)

return sts_response['Credentials']

def download(self, remote_path, target_local_path):
Expand Down

0 comments on commit 1de2975

Please sign in to comment.