From 2fc68df05b86df6a5c6bc18c18888f0b43f44bda Mon Sep 17 00:00:00 2001 From: knakul853 Date: Mon, 9 Mar 2026 20:51:31 +0530 Subject: [PATCH] fix(gcp): allow org-level discovery when Projects.List returns zero results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When organization_id is set without project_ids, the SA may not have resourcemanager.projects.list permission but can still query the Asset API at the organization scope. Previously, the code unconditionally failed with "no projects available for organization discovery" which blocked org-level discovery for these SAs. Now project listing failures are logged as warnings and the provider proceeds with org-level Asset API calls (organizations/{id}), which is the intended fallback path in Resources(). Configured project_ids are still strictly validated — only the auto-discovery path is made lenient. Co-Authored-By: Claude Opus 4.6 --- pkg/providers/gcp/gcp.go | 42 +++++++++++++++----------- pkg/providers/gcp/gcp_test.go | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 pkg/providers/gcp/gcp_test.go diff --git a/pkg/providers/gcp/gcp.go b/pkg/providers/gcp/gcp.go index e9aed1e..0e8f25e 100644 --- a/pkg/providers/gcp/gcp.go +++ b/pkg/providers/gcp/gcp.go @@ -761,11 +761,11 @@ func newOrganizationProvider(options schema.OptionBlock, id, JSONData, organizat // Get projects under the organization projects := []string{} - manager, err := cloudresourcemanager.NewService(context.Background(), creds) - if err != nil { - return nil, errkit.Wrap(err, "could not create resource manager") - } if len(configuredProjects) > 0 { + manager, err := cloudresourcemanager.NewService(context.Background(), creds) + if err != nil { + return nil, errkit.Wrap(err, "could not create resource manager") + } scope := newProjectScope(configuredProjects) if scope == nil { return nil, errkit.New("no valid project ids provided in configuration") @@ -775,23 +775,29 @@ func newOrganizationProvider(options schema.OptionBlock, id, JSONData, organizat } projects = scope.listIDs() provider.projectScope = scope + if len(projects) == 0 { + return nil, errkit.New("no valid project ids available after resolution") + } + gologger.Info().Msgf("Restricting organization discovery to %d configured project(s)", len(projects)) } else { - list := manager.Projects.List() - err = list.Pages(context.Background(), func(resp *cloudresourcemanager.ListProjectsResponse) error { - for _, project := range resp.Projects { - projects = append(projects, project.ProjectId) - } - return nil - }) + manager, err := cloudresourcemanager.NewService(context.Background(), creds) if err != nil { - return nil, errkit.Wrap(err, "could not list projects") + gologger.Warning().Msgf("Could not create resource manager to list projects: %s", err) + } else { + list := manager.Projects.List() + err = list.Pages(context.Background(), func(resp *cloudresourcemanager.ListProjectsResponse) error { + for _, project := range resp.Projects { + projects = append(projects, project.ProjectId) + } + return nil + }) + if err != nil { + gologger.Warning().Msgf("Could not list projects under organization: %s", err) + } + } + if len(projects) == 0 { + gologger.Info().Msgf("No projects listed, will use organization-level Asset API discovery for org %s", organizationID) } - } - if len(projects) == 0 { - return nil, errkit.New("no projects available for organization discovery") - } - if len(configuredProjects) > 0 { - gologger.Info().Msgf("Restricting organization discovery to %d configured project(s)", len(projects)) } provider.projects = projects diff --git a/pkg/providers/gcp/gcp_test.go b/pkg/providers/gcp/gcp_test.go new file mode 100644 index 0000000..013ac01 --- /dev/null +++ b/pkg/providers/gcp/gcp_test.go @@ -0,0 +1,57 @@ +package gcp + +import ( + "testing" + + "github.com/projectdiscovery/cloudlist/pkg/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewOrganizationProvider_NoProjectsListedFallsBackToOrgLevel(t *testing.T) { + // When organization_id is set without project_ids, the provider should be + // created successfully regardless of whether Projects.List returns results. + // Previously, the code returned "no projects available for organization discovery" + // which blocked org-level Asset API discovery for SAs without project-list permission. + + options := schema.OptionBlock{ + "provider": "gcp", + "organization_id": "123456789", + "extended_metadata": "false", + } + + provider, err := newOrganizationProvider(options, "test-id", "", "123456789") + + if err != nil { + assert.NotContains(t, err.Error(), "no projects available for organization discovery", + "org-level provider should not fail just because Projects.List returns 0 projects") + t.Skipf("skipping (no GCP creds): %v", err) + } + + require.NotNil(t, provider) + assert.Equal(t, "123456789", provider.organizationID) + assert.Nil(t, provider.projectScope, "projectScope should be nil for org-level discovery") +} + +func TestNewOrganizationProvider_ConfiguredProjectsStillValidated(t *testing.T) { + // When project_ids are explicitly configured, we should still fail if they + // resolve to an empty list — this is a real config error. + options := schema.OptionBlock{ + "provider": "gcp", + "organization_id": "123456789", + "project_ids": "", + } + + provider, err := newOrganizationProvider(options, "test-id", "", "123456789") + + // With empty project_ids, getProjectIDsFromOptions returns nil, so it + // takes the org-level path (no project scope). Should not fail. + if err != nil { + assert.NotContains(t, err.Error(), "no projects available", + "empty project_ids should fall through to org-level discovery") + t.Skipf("skipping in CI (no GCP creds): %v", err) + } + + require.NotNil(t, provider) + assert.Nil(t, provider.projectScope) +}