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) +}