Skip to content

Commit

Permalink
refactor(gcp): Update GCP querying to use Asset Inventory API (#21)
Browse files Browse the repository at this point in the history
* refactor(gcp): Update GCP querying to use Asset Inventory API

* add RG param to KV in e2e-test to assure it can be found every time

---------

Co-authored-by: Felix <[email protected]>
  • Loading branch information
relusc and wenzel-felix authored Feb 15, 2024
1 parent 5859888 commit c86d837
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 67 deletions.
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/shiftavenue/azure-clientid-syncer
go 1.21.5

require (
cloud.google.com/go/iam v1.1.6
cloud.google.com/go/asset v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0
Expand All @@ -26,15 +26,22 @@ require (
)

require (
cloud.google.com/go v0.112.0 // indirect
cloud.google.com/go/accesscontextmanager v1.8.4 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.6 // indirect
cloud.google.com/go/longrunning v0.5.4 // indirect
cloud.google.com/go/orgpolicy v1.12.0 // indirect
cloud.google.com/go/osconfig v1.12.4 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.2.4 // indirect
Expand Down Expand Up @@ -68,6 +75,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE=
cloud.google.com/go/accesscontextmanager v1.8.4/go.mod h1:ParU+WbMpD34s5JFEnGAnPBYAgUHozaTmDJU7aCU9+M=
cloud.google.com/go/asset v1.17.0 h1:dLWfTnbwyrq/Kt8Tr2JiAbre1MEvS2Bl5cAMiYAy5Pg=
cloud.google.com/go/asset v1.17.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg=
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
cloud.google.com/go/orgpolicy v1.12.0 h1:sab7cDiyfdthpAL0JkSpyw1C3mNqkXToVOhalm79PJQ=
cloud.google.com/go/orgpolicy v1.12.0/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI=
cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I=
cloud.google.com/go/osconfig v1.12.4/go.mod h1:B1qEwJ/jzqSRslvdOCI8Kdnp0gSng0xW4LOnIebQomA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
Expand Down
4 changes: 3 additions & 1 deletion pkg/provider/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const (

// gcpRoleName represents the role associated with service accounts in GCP allowing them to use workload identity
gcpRoleName = "roles/iam.workloadIdentityUser"
// gcpResourceAssetType represents the Asset Inventory resource type for GCP service accounts
gcpResourceAssetType = "iam.googleapis.com/ServiceAccount"
// gcpServiceAccountAnnotation represents the GCP service account name to be used with the Kubernetes service account
gcpServiceAccountAnnotation = "iam.gke.io/gcp-service-account"
)
)
92 changes: 27 additions & 65 deletions pkg/provider/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ package provider

import (
"context"
"errors"
"fmt"
"log"
"regexp"
"sync"
"strings"

admin "cloud.google.com/go/iam/admin/apiv1"
"cloud.google.com/go/iam/admin/apiv1/adminpb"
"cloud.google.com/go/iam/apiv1/iampb"
asset "cloud.google.com/go/asset/apiv1"
"cloud.google.com/go/asset/apiv1/assetpb"
"github.com/go-logr/logr"
"github.com/shiftavenue/azure-clientid-syncer/pkg/config"
"google.golang.org/api/iterator"
Expand All @@ -31,83 +29,47 @@ func NewGCPQueryProvider(serviceAccount *corev1.ServiceAccount, logger logr.Logg
}

func (g *gcpQueryProvider) Query() (*corev1.ServiceAccount, error) {
// Create a new IAM client
// Create a new Asset Inventory client
ctx := context.Background()
iamClient, err := admin.NewIamClient(ctx)
assetClient, err := asset.NewClient(ctx)
if err != nil {
return nil, err
}

// List all service accounts
req := &adminpb.ListServiceAccountsRequest{
Name: fmt.Sprintf("projects/%s", g.config.GcpProjectId),
// Find GCP service account that can be impersonated by Kubernetes account using the GCP Asset Inventory
// the query looks for all resources on which the Kubernetes service account has the 'roles/iam.workloadIdentityUser' role assigned
req := &assetpb.SearchAllIamPoliciesRequest{
Scope: fmt.Sprintf("projects/%s", g.config.GcpProjectId),
Query: fmt.Sprintf("policy:%s.svc.id.goog[%s/%s] roles:%s", g.config.GcpProjectId, g.serviceAccount.Namespace, g.serviceAccount.Name, gcpRoleName),
}
it := iamClient.ListServiceAccounts(ctx, req)

ch := make(chan *string, 1)
wg := sync.WaitGroup{}
it := assetClient.SearchAllIamPolicies(ctx, req)

// Iterate through all results and find service account
gcpServiceAccountMail := ""
for {
sa, err := it.Next()
res, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}

wg.Add(1)

go func(ch chan *string, sa *adminpb.ServiceAccount) {
defer wg.Done()
// Get IAM policy for the service account
policyReq := &iampb.GetIamPolicyRequest{
Resource: sa.Name,
}
policy, err := iamClient.GetIamPolicy(ctx, policyReq)
if err != nil {
log.Printf("failed to retrieve IAM policy: %v", err)
return
}

// Iterate over policy bindings
for _, member := range policy.Members(gcpRoleName) {
fmt.Printf("Found member with role '%s': %s\n", gcpRoleName, member)
namespace, serviceAccountName := extractSubstrings(member)
if g.serviceAccount.Name == serviceAccountName && g.serviceAccount.Namespace == namespace {
fmt.Printf("Project ID: %s, Namespace: %s, Service Account: %s\n", g.config.GcpProjectId, namespace, serviceAccountName)
ch <- &sa.Name
// The service account resource is returned in the format: //iam.googleapis.com/projects/<project-id>/serviceAccounts/<sa-name>@<project-id>.iam.gserviceaccount.com
// extract relevant account mail that can be used in the annotation
if res.AssetType == gcpResourceAssetType {
// Fail if two or more service accounts were found
if gcpServiceAccountMail != "" {
return nil, errors.New("multiple service accounts were found, cannot decide which one to use")
} else {
gcpServiceAccountMail = strings.Split(res.Resource, "/")[6]
if g.serviceAccount.Annotations == nil {
g.serviceAccount.Annotations = make(map[string]string)
}
g.serviceAccount.Annotations[gcpServiceAccountAnnotation] = gcpServiceAccountMail
}
}(ch, sa)
}

go func() {
wg.Wait()
ch <- nil
close(ch)
}()

v, ok := <-ch
if !ok || v == nil {
return nil, fmt.Errorf("failed to retrieve service account")
}

if g.serviceAccount.Annotations == nil {
g.serviceAccount.Annotations = make(map[string]string)
}
}
g.serviceAccount.Annotations[gcpServiceAccountAnnotation] = *v

return g.serviceAccount, nil
}

func extractSubstrings(member string) (string, string) {
// Use a regular expression to extract the namespace and service account
re := regexp.MustCompile(`\[(.*?)\/(.*?)\]`)
match := re.FindStringSubmatch(member)

namespace := match[1]
serviceAccount := match[2]

return namespace, serviceAccount
}
1 change: 1 addition & 0 deletions tests/provision-infrastructure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ az keyvault wait --resource-group $RG --name $KV_NAME --created

KV_ID="$(az keyvault show \
--name $KV_NAME \
--resource-group $RG \
--query id \
-otsv)"

Expand Down

0 comments on commit c86d837

Please sign in to comment.