Skip to content

Commit

Permalink
Add script generator for setting up wif
Browse files Browse the repository at this point in the history
  • Loading branch information
JakobGray committed Jun 18, 2024
1 parent 573da59 commit 6040f8b
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 90 deletions.
106 changes: 89 additions & 17 deletions cmd/ocm/gcp/create-wif-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/pkg/errors"

"github.com/spf13/cobra"
"google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/googleapi"
"google.golang.org/api/iam/v1"
iamv1 "google.golang.org/api/iam/v1"
Expand All @@ -32,6 +33,7 @@ var (
}

impersonatorServiceAccount = "projects/sda-ccs-3/serviceAccounts/[email protected]"
impersonatorEmail = "[email protected]"
)

const (
Expand Down Expand Up @@ -84,20 +86,13 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) {
PoolIdentityProviderId: wifConfig.Status.WorkloadIdentityPoolData.IdentityProviderId,
}

// TODO: implement scripting for dry run
if CreateWorkloadIdentityConfigurationOpts.DryRun {
log.Printf("Dry run option not yet available")

// identityPoolContent := createIdentityPoolScriptContent(poolSpec)
// identityProviderContent := createIdentityProviderScriptContent(poolSpec)
// serviceAccountContent := createServiceAccountScriptContent(wifConfig)
// err = createScriptFile(CreateWorkloadIdentityConfigurationOpts.TargetDir,
// identityPoolContent,
// identityProviderContent,
// serviceAccountContent)
// if err != nil {
// log.Fatalf("Failed to create script files: %s", err)
// }
log.Printf("Writing script files to %s", CreateWorkloadIdentityConfigurationOpts.TargetDir)

err := createScript(CreateWorkloadIdentityConfigurationOpts.TargetDir, wifConfig)
if err != nil {
log.Fatalf("Failed to create script files: %s", err)
}
return
}

Expand Down Expand Up @@ -258,15 +253,17 @@ func createServiceAccounts(ctx context.Context, gcpClient gcp.GcpClient, wifOutp
serviceAccountID := serviceAccount.GetId()

fmt.Printf("\t\tBinding roles to %s\n", serviceAccount.Id)
roles := make([]string, 0, len(serviceAccount.Roles))
for _, role := range serviceAccount.Roles {
if !role.Predefined {
fmt.Printf("Skipping role %q for service account %q as custom roles are not yet supported.", role.Id, serviceAccount.Id)
continue
}
err := gcpClient.BindRole(serviceAccountID, projectId, fmtRoleResourceId(role))
if err != nil {
panic(err)
}
roles = append(roles, fmtRoleResourceId(role))
}
err := EnsurePolicyBindingsForProject(gcpClient, roles, fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccountID, projectId), projectId)
if err != nil {
log.Fatalf("Failed to bind roles to service account %s: %s", serviceAccountID, err)
}
fmt.Printf("\t\tRoles bound to %s\n", serviceAccount.Id)

Expand Down Expand Up @@ -317,3 +314,78 @@ func generateServiceAccountID(serviceAccount models.ServiceAccount) string {
}
return serviceAccountID
}

// EnsurePolicyBindingsForProject ensures that given roles and member, appropriate binding is added to project
func EnsurePolicyBindingsForProject(gcpClient gcp.GcpClient, roles []string, member string, projectName string) error {
needPolicyUpdate := false

policy, err := gcpClient.GetProjectIamPolicy(projectName, &cloudresourcemanager.GetIamPolicyRequest{})

if err != nil {
return fmt.Errorf("error fetching policy for project: %v", err)
}

// Validate that each role exists, and add the policy binding as needed
for _, definedRole := range roles {
// Earlier we've verified that the requested roles already exist.

// Add policy binding
modified := addPolicyBindingForProject(policy, definedRole, member)
if modified {
needPolicyUpdate = true
}

}

if needPolicyUpdate {
return setProjectIamPolicy(gcpClient, policy, projectName)
}

// If we made it this far there were no updates needed
return nil
}

func addPolicyBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) bool {
for i, binding := range policy.Bindings {
if binding.Role == roleName {
return addMemberToBindingForProject(memberName, policy.Bindings[i])
}
}

// if we didn't find an existing binding entry, then make one
createMemberRoleBindingForProject(policy, roleName, memberName)

return true
}

func createMemberRoleBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) {
policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{
Members: []string{memberName},
Role: roleName,
})
}

// adds member to existing binding. returns bool indicating if an entry was made
func addMemberToBindingForProject(memberName string, binding *cloudresourcemanager.Binding) bool {
for _, member := range binding.Members {
if member == memberName {
// already present
return false
}
}

binding.Members = append(binding.Members, memberName)
return true
}

func setProjectIamPolicy(gcpClient gcp.GcpClient, policy *cloudresourcemanager.Policy, projectName string) error {
policyRequest := &cloudresourcemanager.SetIamPolicyRequest{
Policy: policy,
}

_, err := gcpClient.SetProjectIamPolicy(projectName, policyRequest)
if err != nil {
return fmt.Errorf("error setting project policy: %v", err)
}
return nil
}
1 change: 1 addition & 0 deletions cmd/ocm/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func NewGcpCmd() *cobra.Command {
gcpCmd.AddCommand(NewGetCmd())
gcpCmd.AddCommand(NewListCmd())
gcpCmd.AddCommand(NewDescribeCmd())
gcpCmd.AddCommand(NewGenerateCommand())

return gcpCmd
}
Expand Down
61 changes: 61 additions & 0 deletions cmd/ocm/gcp/generate-wif-script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package gcp

import (
"log"

alphaocm "github.com/openshift-online/ocm-cli/pkg/alpha_ocm"
"github.com/spf13/cobra"
)

var (
// CreateWorkloadIdentityPoolOpts captures the options that affect creation of the workload identity pool
GenerateScriptOpts = options{
TargetDir: "",
}
)

func NewGenerateCommand() *cobra.Command {
generateScriptCmd := &cobra.Command{
Use: "generate [wif-config ID]",
Short: "Generate script based on a wif-config",
Args: cobra.ExactArgs(1),
Run: generateCreateScriptCmd,
PersistentPreRun: validationForGenerateCreateScriptCmd,
}

generateScriptCmd.PersistentFlags().StringVar(&GenerateScriptOpts.TargetDir, "output-dir", "", "Directory to place generated files (defaults to current directory)")

return generateScriptCmd
}

func validationForGenerateCreateScriptCmd(cmd *cobra.Command, argv []string) {
if len(argv) != 1 {
log.Fatal(
"Expected exactly one command line parameters containing the id " +
"of the WIF config.",
)
}
}

func generateCreateScriptCmd(cmd *cobra.Command, argv []string) {
// Create the client for the OCM API:
ocmClient, err := alphaocm.NewOcmClient()
if err != nil {
log.Fatalf("failed to create backend client: %v", err)
}

wifConfigId := argv[0]
if wifConfigId == "" {
log.Fatal("WIF config ID is required")
}

wifConfig, err := ocmClient.GetWifConfig(wifConfigId)
if err != nil {
log.Fatalf("failed to get wif-config: %v", err)
}

log.Printf("Writing script files to %s", GenerateScriptOpts.TargetDir)
if err := createScript(GenerateScriptOpts.TargetDir, &wifConfig); err != nil {
log.Fatalf("failed to generate script: %v", err)
}
}
137 changes: 86 additions & 51 deletions cmd/ocm/gcp/scripting.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,50 +15,72 @@ import (
// addPolicyBindingForSvcAcctCmd = "gcloud iam service-accounts add-iam-policy-binding <POPULATE_SERVICE_ACCOUNT_EMAIL> --member=%s --role=%s"
// )

func createScript(targetDir string, wifConfig *models.WifConfigOutput) error {
// Write the script content to the path
scriptContent := generateScriptContent(wifConfig)
err := os.WriteFile(filepath.Join(targetDir, "script.sh"), []byte(scriptContent), 0644)
if err != nil {
return err
}
// Write jwk json file to the path
jwkPath := filepath.Join(targetDir, "jwk.json")
err = os.WriteFile(jwkPath, []byte(wifConfig.Status.WorkloadIdentityPoolData.Jwks), 0644)
if err != nil {
return err
}
return nil
}

func generateScriptContent(wifConfig *models.WifConfigOutput) string {
poolSpec := gcp.WorkloadIdentityPoolSpec{
PoolName: wifConfig.Status.WorkloadIdentityPoolData.PoolId,
ProjectId: wifConfig.Status.WorkloadIdentityPoolData.ProjectId,
Jwks: wifConfig.Status.WorkloadIdentityPoolData.Jwks,
IssuerUrl: wifConfig.Status.WorkloadIdentityPoolData.IssuerUrl,
PoolIdentityProviderId: wifConfig.Status.WorkloadIdentityPoolData.IdentityProviderId,
}

scriptContent := "#!/bin/bash\n"

// Create a script to create the workload identity pool
scriptContent += createIdentityPoolScriptContent(poolSpec)

// Append the script to create the identity provider
scriptContent += createIdentityProviderScriptContent(poolSpec)

// Append the script to create the service accounts
scriptContent += createServiceAccountScriptContent(wifConfig)

return scriptContent
}

func createIdentityPoolScriptContent(spec gcp.WorkloadIdentityPoolSpec) string {
name := spec.PoolName
project := spec.ProjectId

return fmt.Sprintf(`#!/bin/bash
return fmt.Sprintf(`
# Create a workload identity pool
gcloud iam workload-identity-pools create %s \
--project=%s \
--location=global \
--description="Workload Identity Pool for %s" \
--display-name="%s"
`, name, project, poolDescription, name)

// // Create the output directory if it doesn't exist
// err := os.MkdirAll("output", 0755)
// if err != nil {
// return err
// }

// // Write the script content to the file
// err = ioutil.WriteFile("output/createWorkloadIdentityPool.sh", []byte(scriptContent), 0644)
// if err != nil {
// return err
// }

// return nil
}

func createIdentityProviderScriptContent(spec gcp.WorkloadIdentityPoolSpec) string {
return fmt.Sprintf(`#!/bin/bash
return fmt.Sprintf(`
# Create a workload identity provider
gcloud iam workload-identity-pools providers create-oidc %s \
--display-name="%s" \
--description=\"%s\" \
--description="%s" \
--location=global \
--issuer-uri="%s" \
--jwk-json-path="path/to/jwk.json"
--jwk-json-path="jwk.json" \
--allowed-audiences="%s" \
--attribute-mapping=\"google.subject=assertion.sub\" \
--attribute-mapping="google.subject=assertion.sub" \
--workload-identity-pool=%s
`, spec.PoolName, spec.PoolName, poolDescription, spec.IssuerUrl, openShiftAudience, spec.PoolName)

}

// This returns the gcloud commands to create a service account, bind roles, and grant access
Expand All @@ -67,43 +89,56 @@ func createServiceAccountScriptContent(wifConfig *models.WifConfigOutput) string
// For each service account, create a service account and bind it to the workload identity pool
var sb strings.Builder

sb.WriteString("# Create service accounts:\n")
sb.WriteString("\n# Create service accounts:\n")
for _, sa := range wifConfig.Status.ServiceAccounts {
sb.WriteString(fmt.Sprintf("gcloud iam service-accounts create %s --display-name=%s --project=%s\n",
sa.Id, sa.Id, wifConfig.Spec.ProjectId))
project := wifConfig.Spec.ProjectId
serviceAccountID := sa.GetId()
serviceAccountName := wifConfig.Spec.DisplayName + "-" + serviceAccountID
serviceAccountDesc := poolDescription + " for WIF config " + wifConfig.Spec.DisplayName
sb.WriteString(fmt.Sprintf("gcloud iam service-accounts create %s --display-name=%s --description=\"%s\" --project=%s\n",
serviceAccountID, serviceAccountName, serviceAccountDesc, project))
}
sb.WriteString("# Bind service account roles:\n")
sb.WriteString("\n# Bind service account roles:\n")
for _, sa := range wifConfig.Status.ServiceAccounts {
for _, role := range sa.Roles {
sb.WriteString(fmt.Sprintf("gcloud projects add-iam-policy-binding %s --member=serviceAccount:%s --role=%s\n",
wifConfig.Spec.ProjectId, sa.Id, role.Id))
project := wifConfig.Spec.ProjectId
member := fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", sa.GetId(), project)
sb.WriteString(fmt.Sprintf("gcloud projects add-iam-policy-binding %s --member=%s --role=roles/%s\n",
project, member, role.Id))
}
}
sb.WriteString("\n# Grant access:\n")
for _, sa := range wifConfig.Status.ServiceAccounts {
if sa.AccessMethod == "wif" {
project := wifConfig.Spec.ProjectId
serviceAccount := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", sa.GetId(), project)
members := fmtMembers(sa, wifConfig.Status.WorkloadIdentityPoolData.ProjectNumber, wifConfig.Status.WorkloadIdentityPoolData.PoolId)
for _, member := range members {
sb.WriteString(fmt.Sprintf("gcloud iam service-accounts add-iam-policy-binding %s --member=%s --role=roles/iam.workloadIdentityUser --project=%s\n",
serviceAccount, member, project))
}
} else if sa.AccessMethod == "impersonate" {
// gcloud iam service-accounts add-iam-policy-binding SERVICE_ACCOUNT_EMAIL \
// --member='serviceAccount:IMPERSONATOR_EMAIL' \
// --role='roles/iam.serviceAccountTokenCreator' \
// --project=PROJECT_ID
project := wifConfig.Spec.ProjectId
serviceAccount := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", sa.GetId(), project)
// saResource := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", wifConfig.Spec.ProjectId, sa.Id, wifConfig.Spec.ProjectId)
impersonator := fmt.Sprintf("serviceAccount:%s", impersonatorEmail)
sb.WriteString(fmt.Sprintf("gcloud iam service-accounts add-iam-policy-binding %s --member=%s --role=roles/iam.serviceAccountTokenCreator --project=%s\n",
serviceAccount, impersonator, wifConfig.Spec.ProjectId))
}
}
sb.WriteString("# Grant access:\n")
// for _, sa := range wifConfig.Status.ServiceAccounts {
// if sa.AccessMethod == "impersonate" {
// sb.WriteString(fmt.Sprintf("gcloud iam service-accounts add-iam-policy-binding %s --member=serviceAccount:%s --role=roles/iam.workloadIdentityUser\n",
// sa.Id, sa.Id))
// } else if sa.AccessMethod == "wif" {
// // gcloud iam service-accounts add-iam-policy-binding SERVICE_ACCOUNT_ID \
// // --member='serviceAccount:PROJECT_ID.svc.id.goog[WORKLOAD_IDENTITY_POOL_ID/WORKLOAD_IDENTITY_PROVIDER_ID]' \
// // --role='roles/iam.workloadIdentityUser' \
// // --project=PROJECT_ID
// sb.WriteString(fmt.Sprintf("gcloud iam service-accounts add-iam-policy-binding %s --member='serviceAccount:%s.svc.id.goog[%s/%s]' --role='roles/iam.workloadIdentityUser' --project=%s\n",
// }
// }
return sb.String()
}

func createScriptFile(targetDir string, content ...string) error {
// Concatenate the content strings
scriptContent := strings.Join(content, "\n")

// Write the script content to the file
err := os.WriteFile(filepath.Join(targetDir, "script.sh"), []byte(scriptContent), 0644)
if err != nil {
return err
func fmtMembers(sa models.ServiceAccount, projectNum int64, poolId string) []string {
members := []string{}
for _, saName := range sa.GetServiceAccountNames() {
members = append(members, fmt.Sprintf(
"principal://iam.googleapis.com/projects/%d/locations/global/workloadIdentityPools/%s/subject/system:serviceaccount:%s:%s",
projectNum, poolId, sa.GetSecretNamespace(), saName))
}

return nil
return members
}
Loading

0 comments on commit 6040f8b

Please sign in to comment.