Skip to content

Commit dd6420e

Browse files
committed
chore: added Google Workload Identity Federation test
1 parent 566fc7f commit dd6420e

File tree

7 files changed

+329
-4
lines changed

7 files changed

+329
-4
lines changed

Taskfile.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ tasks:
5858
cmds:
5959
- task: test-setup
6060
- task: test-integ-run
61+
- task: test-authentication
6162
- task: test-teardown
6263

6364
test-setup:
@@ -182,6 +183,15 @@ tasks:
182183
cmds:
183184
- tests/install/test-install.sh
184185

186+
# Tests Novops authentication methods
187+
test-authentication:
188+
cmds:
189+
- task: test-authentication-gcp-wif
190+
191+
# Test Google Worklow Identity Federation authentication
192+
test-authentication-gcp-wif:
193+
cmd: tests/authentication/test-gcp-workload-identity-federation-creds.sh
194+
185195
#
186196
# Release
187197
#
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
3+
# Test novops Google Workload Identity Federation authentication using Azure credentials
4+
# - Run authentication steps in container to avoid side-effect with locally configured accounts
5+
# - Generate an Azure token with Google credential file which Novops can read
6+
# - Run Novops and check authentication happens properly
7+
8+
set -e
9+
10+
AZ_STACK_OUTPUTS=$(pulumi -C tests/setup/pulumi/azure/ -s test stack output --json --show-secrets)
11+
AZ_TENANT=$(echo $AZ_STACK_OUTPUTS | jq -r .tenantId)
12+
AZ_USERNAME=$(echo $AZ_STACK_OUTPUTS | jq -r .servicePrincipalClientId)
13+
AZ_PASSWORD=$(echo $AZ_STACK_OUTPUTS | jq -r .password)
14+
AZ_IDENTIFIER_URI=$(echo $AZ_STACK_OUTPUTS | jq -r .identifierUri)
15+
16+
echo "Login to Azure using Tenant ID '$AZ_TENANT' Service Principal '$AZ_USERNAME' to get token for Identifier URI '$AZ_IDENTIFIER_URI'"
17+
18+
mkdir -p ./tmp
19+
20+
# Authenticate with Azure in a container (to avoid side effect with local Azure config)
21+
# And save token to file via bind mount
22+
podman run -u 0 --rm -it -v $PWD:/novops --entrypoint bash bitnami/azure-cli:2.67.0 -c \
23+
"az login --service-principal --username $AZ_USERNAME --password $AZ_PASSWORD --tenant $AZ_TENANT && \
24+
az account get-access-token --resource $AZ_IDENTIFIER_URI --query accessToken --output tsv > /novops/tmp/az-token.txt"
25+
26+
# Generate Google credential JSON file for our Azure token
27+
GCP_STACK_OUTPUTS=$(pulumi -C tests/setup/pulumi/gcp/ -s test stack output --json)
28+
WIF_NAME=$(echo $GCP_STACK_OUTPUTS | jq -r .workloadIdentityPoolProviderName)
29+
GCP_PROJECT_NAME=$(echo $GCP_STACK_OUTPUTS | jq -r .projectName)
30+
31+
echo "Using Workload Identity Federation provider: $WIF_NAME"
32+
33+
# Project name is asked but actually not used
34+
gcloud iam workload-identity-pools create-cred-config \
35+
--project "dummy" \
36+
$WIF_NAME \
37+
--credential-source-file=$PWD/tmp/az-token.txt \
38+
--output-file=$PWD/tmp/gcp-auth.json
39+
40+
export GOOGLE_APPLICATION_CREDENTIALS="$PWD/tmp/gcp-auth.json"
41+
42+
echo "Google auth file ready in $GOOGLE_APPLICATION_CREDENTIALS"
43+
44+
RUST_LOG=novops=debug cargo run -- load -c tests/.novops.gcloud_secretmanager.yml --skip-tty-check

tests/setup/pulumi/azure/index.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import * as az from "@pulumi/azure-native";
1+
import * as az from "@pulumi/azure-native"
2+
import * as azuread from "@pulumi/azuread"
3+
import * as pulumi from "@pulumi/pulumi"
24

35
const resourceGroup = new az.resources.ResourceGroup("novops-testing", {
46
resourceGroupName: "novops-testing",
57
})
68

79
const currentConfig = az.authorization.getClientConfigOutput()
810

11+
// Key Vault secrets from testing
912
const keyVault = new az.keyvault.Vault("novops-test", {
1013
resourceGroupName: resourceGroup.name,
1114
vaultName: "novops-test",
@@ -35,4 +38,39 @@ const secret = new az.keyvault.Secret("novops-test-secret", {
3538
properties: {
3639
value: "v3rySecret!",
3740
},
38-
})
41+
})
42+
43+
// Entra ID app to test Google Workload Identity Federation auth
44+
45+
const appDisplayName = "novops-test-google-workload-id-fed"
46+
export const identifierUri = "api://novops-test/google-workload-id-fed"
47+
48+
const app = new azuread.Application("novops-test-google-workload-id-fed-app", {
49+
displayName: appDisplayName,
50+
owners: [currentConfig.objectId],
51+
identifierUris: [identifierUri]
52+
})
53+
54+
const servicePrincipal = new azuread.ServicePrincipal("novops-test-google-workload-id-fed-service-principal", {
55+
clientId: app.clientId,
56+
owners: [currentConfig.objectId]
57+
})
58+
const servicePrincipalPassword = new azuread.ServicePrincipalPassword("novops-test-google-workload-id-fed-sp-password", {
59+
servicePrincipalId: servicePrincipal.id,
60+
endDate: "2099-01-01T01:01:42Z"
61+
})
62+
63+
const roleReader = "acdd72a7-3385-48ef-bd42-f606fba81ae7"
64+
const readerRoleAssignment = new az.authorization.RoleAssignment("novops-test-google-workload-id-fed-role-assignment", {
65+
principalId: servicePrincipal.objectId,
66+
principalType: "ServicePrincipal",
67+
roleDefinitionId: pulumi.interpolate`/subscriptions/${currentConfig.subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${roleReader}`,
68+
scope: pulumi.interpolate`/subscriptions/${currentConfig.subscriptionId}`,
69+
})
70+
71+
export const servicePrincipalClientId = servicePrincipal.clientId
72+
export const servicePrincipalObjectId = servicePrincipal.objectId
73+
export const tenantId = servicePrincipal.applicationTenantId
74+
export const applicationObjectId = app.objectId
75+
export const applicationClientId = app.clientId
76+
export const password = servicePrincipalPassword.value

tests/setup/pulumi/gcp/Pulumi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ name: novops-test-infra-gcp
22
runtime: nodejs
33
description: Setup Google Cloud infra for Novops tests
44
config:
5-
gcp:project: novops-testing
5+
gcp:project: "398497848942"

tests/setup/pulumi/gcp/index.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import * as gcp from "@pulumi/gcp";
2+
import * as pulumi from "@pulumi/pulumi";
3+
4+
const gcpConfig = new pulumi.Config("gcp");
5+
const projectId = gcpConfig.require("project")
26

37
const secret = new gcp.secretmanager.Secret("test-secret", {
48
secretId: "novops-test-secret",
@@ -10,4 +14,91 @@ const secret = new gcp.secretmanager.Secret("test-secret", {
1014
const secretVersion = new gcp.secretmanager.SecretVersion("test-secret-version", {
1115
secret: secret.id,
1216
secretData: "very!S3cret",
13-
})
17+
})
18+
19+
// Workload Identity Federation config to allow Azure authentication and test of Google WIF
20+
// Retrieve outputs from Azure setup stack
21+
// As documented on https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#azure_2
22+
23+
const azStackRef = new pulumi.StackReference("az-stack-ref", { name: "pierrebeucher/novops-test-infra-azure/test" })
24+
const azServicePrincipalObjectId = azStackRef.requireOutput("servicePrincipalObjectId") as pulumi.Output<string>
25+
const azTenantId = azStackRef.requireOutput("tenantId") as pulumi.Output<string>
26+
const azAppIdentifierUri = azStackRef.requireOutput("identifierUri") as pulumi.Output<string>
27+
28+
const poolId = "novops-test-identity-pool"
29+
const providerId = "azure"
30+
31+
// Use an import-if-exists and retain pattern to avoid deletion on stack down and re-import on stack up
32+
// Otherwise, as resources are soft-deleted, Google API would consider them already existing if Pulumi tried to recreate them
33+
// See https://github.com/pulumi/pulumi-gcp/issues/1149
34+
const identityPool = pulumi.output(gcp.iam.getWorkloadIdentityPool({
35+
workloadIdentityPoolId: poolId,
36+
}).then(existingPool =>
37+
new gcp.iam.WorkloadIdentityPool("workload-identity-pool", {
38+
workloadIdentityPoolId: poolId,
39+
}, {
40+
import: existingPool.workloadIdentityPoolId,
41+
retainOnDelete: true,
42+
})
43+
).catch(e =>
44+
new gcp.iam.WorkloadIdentityPool("workload-identity-pool", {
45+
workloadIdentityPoolId: poolId,
46+
}, {
47+
retainOnDelete: true,
48+
})
49+
))
50+
51+
const workloadIdentityProvider = pulumi.output(gcp.iam.getWorkloadIdentityPoolProvider({
52+
workloadIdentityPoolProviderId: providerId,
53+
workloadIdentityPoolId: poolId
54+
}).then(existingWifProvider =>
55+
new gcp.iam.WorkloadIdentityPoolProvider("workload-identity-pool-provider", {
56+
workloadIdentityPoolProviderId: providerId,
57+
workloadIdentityPoolId: poolId,
58+
oidc: {
59+
issuerUri: pulumi.interpolate`https://sts.windows.net/${azTenantId}/`,
60+
allowedAudiences: [azAppIdentifierUri],
61+
},
62+
attributeMapping: {
63+
"google.subject": "assertion.sub",
64+
},
65+
}, {
66+
import: existingWifProvider.name,
67+
retainOnDelete: true,
68+
})
69+
).catch(e =>
70+
new gcp.iam.WorkloadIdentityPoolProvider("workload-identity-pool-provider", {
71+
workloadIdentityPoolProviderId: providerId,
72+
workloadIdentityPoolId: poolId,
73+
oidc: {
74+
issuerUri: pulumi.interpolate`https://sts.windows.net/${azTenantId}/`,
75+
allowedAudiences: [azAppIdentifierUri],
76+
},
77+
attributeMapping: {
78+
"google.subject": "assertion.sub",
79+
},
80+
}, {
81+
retainOnDelete: true,
82+
})
83+
))
84+
85+
const secretManagerIamBinding = new gcp.projects.IAMMember("secret-manager-iam-binding", {
86+
project: projectId,
87+
role: "roles/secretmanager.secretAccessor",
88+
// Service Principal Object ID is the ID Google uses to identify or Azure user
89+
member: pulumi.interpolate`principal://iam.googleapis.com/projects/${projectId}/locations/global/workloadIdentityPools/${poolId}/subject/${azServicePrincipalObjectId}`,
90+
}, {
91+
dependsOn: [workloadIdentityProvider]
92+
})
93+
94+
const viewerIamBinding = new gcp.projects.IAMMember("viewer-iam-binding", {
95+
project: projectId,
96+
role: "roles/viewer",
97+
member: pulumi.interpolate`principal://iam.googleapis.com/projects/${projectId}/locations/global/workloadIdentityPools/${poolId}/subject/${azServicePrincipalObjectId}`,
98+
}, {
99+
dependsOn: [workloadIdentityProvider]
100+
})
101+
102+
export const workloadIdentityPoolName = identityPool.name
103+
export const workloadIdentityPoolProviderName = workloadIdentityProvider.name
104+
export const providerResourceName = workloadIdentityProvider.id

tests/setup/pulumi/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"@pulumi/aws": "^6.0.0",
99
"@pulumi/awsx": "^2.0.2",
1010
"@pulumi/azure-native": "^2.48.0",
11+
"@pulumi/azuread": "^6.0.1",
1112
"@pulumi/docker": "^4.5.4",
1213
"@pulumi/gcp": "^7.29.0",
1314
"@pulumi/kubernetes": "^4.14.0",

0 commit comments

Comments
 (0)