Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/flavors/assetinventory/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (s *strategy) initAzureFetchers(_ context.Context) ([]inventory.AssetFetche
}

func (s *strategy) initGcpFetchers(ctx context.Context) ([]inventory.AssetFetcher, error) {
cfgProvider := &gcp_auth.ConfigProvider{AuthProvider: &gcp_auth.GoogleAuthProvider{}}
cfgProvider := gcp_auth.NewConfigProvider()
gcpConfig, err := cfgProvider.GetGcpClientConfig(ctx, s.cfg.CloudConfig.Gcp, s.logger)
if err != nil {
return nil, fmt.Errorf("failed to initialize gcp config: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion internal/flavors/benchmark/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func GetStrategy(cfg *config.Config, log *clog.Logger, statusHandler statushandl
}, nil
case config.CIS_GCP:
return &GCP{
CfgProvider: &gcp_auth.ConfigProvider{AuthProvider: &gcp_auth.GoogleAuthProvider{}},
CfgProvider: gcp_auth.NewConfigProvider(),
inventoryInitializer: &gcp_inventory.ProviderInitializer{},
}, nil
case config.CIS_AZURE:
Expand Down
40 changes: 40 additions & 0 deletions internal/resources/providers/gcplib/auth/audience.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package auth

import "strings"

// projectNumberFromAudience extracts the GCP project number from a Workload Identity
// Federation audience URL. Format: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/...
// Returns the project number and true if found, or empty string and false otherwise.
func projectNumberFromAudience(audience string) (string, bool) {
if audience == "" {
return "", false
}
parts := strings.Split(audience, "/")
for i := 0; i < len(parts)-1; i++ {
if parts[i] == "projects" {
num := strings.TrimSpace(parts[i+1])
if num != "" {
return num, true
}
return "", false
}
}
return "", false
}
89 changes: 89 additions & 0 deletions internal/resources/providers/gcplib/auth/audience_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package auth

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestProjectNumberFromAudience(t *testing.T) {
tests := []struct {
name string
audience string
wantNum string
wantOK bool
}{
{
name: "empty audience returns false",
audience: "",
wantNum: "",
wantOK: false,
},
{
name: "Workload Identity Federation audience format",
audience: "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/test-pool/providers/test-provider",
wantNum: "123456",
wantOK: true,
},
{
name: "audience with no projects segment",
audience: "//iam.googleapis.com/locations/global/workloadIdentityPools/test-pool",
wantNum: "",
wantOK: false,
},
{
name: "projects segment with empty next segment",
audience: "//iam.googleapis.com/projects//locations/global",
wantNum: "",
wantOK: false,
},
{
name: "project number with surrounding whitespace is trimmed",
audience: "prefix/projects/ 987654 /locations/global",
wantNum: "987654",
wantOK: true,
},
{
name: "returns first project number when multiple projects segments",
audience: "projects/111/locations/global/projects/222/locations/eu",
wantNum: "111",
wantOK: true,
},
{
name: "projects at end without number",
audience: "//iam.googleapis.com/some/path/projects",
wantNum: "",
wantOK: false,
},
{
name: "single segment projects",
audience: "projects/42",
wantNum: "42",
wantOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotNum, gotOK := projectNumberFromAudience(tt.audience)
assert.Equal(t, tt.wantNum, gotNum)
assert.Equal(t, tt.wantOK, gotOK)
})
}
}
58 changes: 25 additions & 33 deletions internal/resources/providers/gcplib/auth/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,31 @@ type GoogleAuthProviderAPI interface {
FindCloudConnectorsCredentials(ctx context.Context, ccConfig config.CloudConnectorsConfig, audience string, serviceAccountEmail string) ([]option.ClientOption, error)
}

// DefaultCredentialsFinder is the minimal interface needed to resolve project ID from
// application default credentials (e.g. metadata server on GCP). *GoogleAuthProvider implements it.
type DefaultCredentialsFinder interface {
FindDefaultCredentials(ctx context.Context) (*google.Credentials, error)
}

// ParentResolver returns the GCP parent (e.g. "projects/pid" or "organizations/oid")
// for the given config and client options.
type ParentResolver interface {
GetParent(ctx context.Context, cfg config.GcpConfig, clientOpts []option.ClientOption) (string, error)
}

type ConfigProvider struct {
AuthProvider GoogleAuthProviderAPI
AuthProvider GoogleAuthProviderAPI
ParentResolver ParentResolver // required; use DefaultParentResolver in production
}

// NewConfigProvider returns a ConfigProvider wired with the default auth provider
// and default parent resolver (project + organization). Use this in production.
func NewConfigProvider() *ConfigProvider {
auth := &GoogleAuthProvider{}
return &ConfigProvider{
AuthProvider: auth,
ParentResolver: NewDefaultParentResolver(auth),
}
}

var ErrMissingOrgId = errors.New("organization ID is required for organization account type")
Expand All @@ -70,7 +93,7 @@ func (p *ConfigProvider) GetGcpClientConfig(ctx context.Context, cfg config.GcpC
}

func (p *ConfigProvider) getGcpFactoryConfig(ctx context.Context, cfg config.GcpConfig, clientOpts []option.ClientOption) (*GcpFactoryConfig, error) {
parent, err := getGcpConfigParentValue(ctx, *p, cfg)
parent, err := p.ParentResolver.GetParent(ctx, cfg, clientOpts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -118,19 +141,6 @@ func (p *ConfigProvider) getCustomCredentials(ctx context.Context, cfg config.Gc

return p.getGcpFactoryConfig(ctx, cfg, opts)
}
func (p *ConfigProvider) getProjectId(ctx context.Context, cfg config.GcpConfig) (string, error) {
if cfg.ProjectId != "" {
return cfg.ProjectId, nil
}

// Try to get project ID from metadata server in case we are running on GCP VM
cred, err := p.AuthProvider.FindDefaultCredentials(ctx)
if err == nil {
return cred.ProjectID, nil
}

return "", ErrProjectNotFound
}

func validateJSONFromFile(filePath string) error {
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
Expand All @@ -148,21 +158,3 @@ func validateJSONFromFile(filePath string) error {

return nil
}

func getGcpConfigParentValue(ctx context.Context, provider ConfigProvider, cfg config.GcpConfig) (string, error) {
switch cfg.AccountType {
case config.OrganizationAccount:
if cfg.OrganizationId == "" {
return "", ErrMissingOrgId
}
return fmt.Sprintf("organizations/%s", cfg.OrganizationId), nil
case config.SingleAccount:
projectId, err := provider.getProjectId(ctx, cfg)
if err != nil {
return "", fmt.Errorf("failed to get project ID: %v", err)
}
return fmt.Sprintf("projects/%s", projectId), nil
default:
return "", fmt.Errorf("invalid gcp account type: %s", cfg.AccountType)
}
}
Loading
Loading