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
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package securitycenter

import (
"context"
"fmt"
"log"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-google/google/registry"
rmClient "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager/client"
"github.com/hashicorp/terraform-provider-google/google/services/serviceusage"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
)

func ResourceSecurityCenterNotificationServiceAccount() *schema.Resource {
return &schema.Resource{
Create: resourceSecurityCenterNotificationServiceAccountCreate,
Read: resourceSecurityCenterNotificationServiceAccountRead,
Delete: resourceSecurityCenterNotificationServiceAccountDelete,

Importer: &schema.ResourceImporter{
StateContext: ResourceSecurityCenterNotificationServiceAccountImport,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(20 * time.Minute),
Read: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(20 * time.Minute),
},

CustomizeDiff: customdiff.All(
resourceSecurityCenterNotificationServiceAccountCustomizeDiff,
),

Schema: map[string]*schema.Schema{
"organization": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ExactlyOneOf: []string{"organization", "project"},
},
"project": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ExactlyOneOf: []string{"organization", "project"},
},
"email": {
Type: schema.TypeString,
Computed: true,
Description: `The email address of the Cloud Security Command Center Notification service account.`,
},
"member": {
Type: schema.TypeString,
Computed: true,
Description: `The Identity of the Cloud Security Command Center Notification service account in the form 'serviceAccount:{email}'. This value is often used to refer to the service account in order to grant IAM permissions.`,
},
},
UseJSONNumber: true,
}
}

// resourceSecurityCenterNotificationServiceAccountCustomizeDiff defaults the 'project' attribute
// to the provider-level default project only when 'organization' is not configured in HCL.
// We check GetRawConfig() rather than GetOk() so that unknown/computed values during planning
// are properly detected as configured, preventing plan inconsistencies.
func resourceSecurityCenterNotificationServiceAccountCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error {
rawConfig := diff.GetRawConfig()
if !rawConfig.IsNull() {
if !rawConfig.GetAttr("organization").IsNull() {
return nil
}
}
return tpgresource.DefaultProviderProject(ctx, diff, meta)
}

// getSccNotificationServiceAccountProjectNumber resolves a string Project ID (e.g., "my-project")
// to a numerical Project Number (e.g., "123456789"). If the input is already numerical digits,
// it returns immediately without making an API call.
// This is required because SCC notification service accounts strictly embed the numerical project
// number in their email addresses (service-<PROJECT_NUMBER>@...).
func getSccNotificationServiceAccountProjectNumber(d *schema.ResourceData, config *transport_tpg.Config, project, userAgent string) (string, error) {
if _, err := strconv.ParseInt(project, 10, 64); err == nil {
return project, nil
}

log.Printf("[DEBUG] Retrieving project number for SCC notification service account by doing a GET with the project id %q", project)
billingProject := project
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
billingProject = bp
}

getProjectCall := rmClient.NewClient(config, userAgent).Projects.Get(project)
if config.UserProjectOverride {
getProjectCall.Header().Add("X-Goog-User-Project", billingProject)
}
projectCall, err := getProjectCall.Do()
if err != nil {
return "", fmt.Errorf("Failed to retrieve project %s: %w", project, err)
}

return strconv.FormatInt(projectCall.ProjectNumber, 10), nil
}

// getSccNotificationServiceAccountDetails inspects the configured scope ('organization' or 'project') and computes:
// 1. The target ServiceUsage endpoint URL for :generateServiceIdentity.
// 2. The deterministic SCC service account email address.
// 3. The canonical Terraform state ID (matching the email address).
//
// While :generateServiceIdentity provisions the P4SA in IAM as a side effect, ServiceUsage's API
// response does not populate .email for multi-agent SCC tags. Therefore, we compute the email
// deterministically using the numerical org or project ID.
func getSccNotificationServiceAccountDetails(d *schema.ResourceData, config *transport_tpg.Config, userAgent string) (string, string, string, error) {
if v, ok := d.GetOk("organization"); ok {
org := v.(string)
url, err := tpgresource.ReplaceVars(d, config, "{{ServiceUsageBasePath}}organizations/{{organization}}/services/securitycenter.googleapis.com:generateServiceIdentity")
if err != nil {
return "", "", "", err
}
email := fmt.Sprintf("service-org-%s@gcp-sa-scc-notification.iam.gserviceaccount.com", org)
parentId := fmt.Sprintf("organizations/%s", org)
return url, email, parentId, nil
}

if v, ok := d.GetOk("project"); ok {
project := v.(string)
projectNumber, err := getSccNotificationServiceAccountProjectNumber(d, config, project, userAgent)
if err != nil {
return "", "", "", err
}
url, err := tpgresource.ReplaceVars(d, config, "{{ServiceUsageBasePath}}projects/{{project}}/services/securitycenter.googleapis.com:generateServiceIdentity")
if err != nil {
return "", "", "", err
}
email := fmt.Sprintf("service-%s@gcp-sa-scc-notification.iam.gserviceaccount.com", projectNumber)
parentId := fmt.Sprintf("projects/%s", project)
return url, email, parentId, nil
}

return "", "", "", fmt.Errorf("one of organization or project must be specified")
}

func resourceSecurityCenterNotificationServiceAccountCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}

url, email, parentId, err := getSccNotificationServiceAccountDetails(d, config, userAgent)
if err != nil {
return err
}

billingProject := ""
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
billingProject = bp
}

// Call ServiceUsage :generateServiceIdentity to trigger Service Agent Manager (SAM)
// to provision the identity in IAM immediately. This ensures the service account exists
// before Terraform applies downstream resources like google_pubsub_topic_iam_member.
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "POST",
Project: billingProject,
RawURL: url,
UserAgent: userAgent,
Timeout: d.Timeout(schema.TimeoutCreate),
})
if err != nil {
return fmt.Errorf("Error creating Cloud SCC Notification Service Account: %s", err)
}

var opRes map[string]interface{}
err = serviceusage.ServiceUsageOperationWaitTimeWithResponse(
config, res, &opRes, billingProject, "Creating Cloud SCC Notification Service Account", userAgent,
d.Timeout(schema.TimeoutCreate))
if err != nil {
return err
}

d.SetId(email)
if err := d.Set("email", email); err != nil {
return fmt.Errorf("Error setting email: %s", err)
}
if err := d.Set("member", "serviceAccount:"+email); err != nil {
return fmt.Errorf("Error setting member: %s", err)
}

log.Printf("[DEBUG] Created Cloud SCC Notification Service Account %q for parent %q", email, parentId)
return nil
}

func resourceSecurityCenterNotificationServiceAccountRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}

_, email, _, err := getSccNotificationServiceAccountDetails(d, config, userAgent)
if err != nil {
return err
}

d.SetId(email)
if err := d.Set("email", email); err != nil {
return fmt.Errorf("Error setting email: %s", err)
}
if err := d.Set("member", "serviceAccount:"+email); err != nil {
return fmt.Errorf("Error setting member: %s", err)
}
return nil
}

func resourceSecurityCenterNotificationServiceAccountDelete(d *schema.ResourceData, meta interface{}) error {
return nil
}

func ResourceSecurityCenterNotificationServiceAccountImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
id := d.Id()
if strings.HasPrefix(id, "organizations/") {
if err := d.Set("organization", strings.TrimPrefix(id, "organizations/")); err != nil {
return nil, fmt.Errorf("Error setting organization: %s", err)
}
} else if strings.HasPrefix(id, "projects/") {
if err := d.Set("project", strings.TrimPrefix(id, "projects/")); err != nil {
return nil, fmt.Errorf("Error setting project: %s", err)
}
} else if strings.Contains(id, "@gcp-sa-scc-notification.iam.gserviceaccount.com") {
parts := strings.Split(id, "@")
accountPart := parts[0]
if strings.HasPrefix(accountPart, "service-org-") {
if err := d.Set("organization", strings.TrimPrefix(accountPart, "service-org-")); err != nil {
return nil, fmt.Errorf("Error setting organization: %s", err)
}
} else if strings.HasPrefix(accountPart, "service-") {
if err := d.Set("project", strings.TrimPrefix(accountPart, "service-")); err != nil {
return nil, fmt.Errorf("Error setting project: %s", err)
}
} else {
return nil, fmt.Errorf("Unsupported import format %q for google_scc_notification_service_account", id)
}
} else {
return nil, fmt.Errorf("Unsupported import format %q for google_scc_notification_service_account: expected organizations/{org_id}, projects/{project_id}, or email address", id)
}
return []*schema.ResourceData{d}, nil
}

func init() {
registry.Schema{
Name: "google_scc_notification_service_account",
ProductName: "securitycenter",
Type: registry.SchemaTypeResource,
Schema: ResourceSecurityCenterNotificationServiceAccount(),
}.Register()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource: 'google_scc_notification_service_account'
generation_type: 'handwritten'
api_service_name: 'serviceusage.googleapis.com'
api_version: 'v1beta1'
api_resource_type_kind: 'Service'
fields:
- field: 'organization'
- field: 'project'
- field: 'email'
- field: 'member'
Loading
Loading