From 369ab83c11bae3a07874201feb0ba661554800c1 Mon Sep 17 00:00:00 2001 From: Nathan Brahms Date: Fri, 7 Jun 2024 07:55:06 -0700 Subject: [PATCH] Add gcp org-access-logs (#32) This is a new feature in P0's Google Gcloud installation. Add supporting resource. --- docs/resources/gcp_access_logs.md | 73 +++++++++ .../resources/gcp_organization_access_logs.md | 110 ++++++++++++++ .../resource.tf | 2 +- .../resource.tf | 62 ++++++++ internal/provider/provider.go | 3 +- .../provider/resources/install/gcp/common.go | 23 ++- .../resources/install/gcp/org_access_logs.go | 139 ++++++++++++++++++ 7 files changed, 406 insertions(+), 6 deletions(-) create mode 100644 docs/resources/gcp_organization_access_logs.md rename examples/resources/{p0_access_logs => p0_gcp_access_logs}/resource.tf (96%) create mode 100644 examples/resources/p0_gcp_organization_access_logs/resource.tf create mode 100644 internal/provider/resources/install/gcp/org_access_logs.go diff --git a/docs/resources/gcp_access_logs.md b/docs/resources/gcp_access_logs.md index 556e0c9..8b8d2a3 100644 --- a/docs/resources/gcp_access_logs.md +++ b/docs/resources/gcp_access_logs.md @@ -27,7 +27,80 @@ Use the read-only attributes defined on `p0_gcp` to create the requisite Google P0 recommends defining this infrastructure according to the example usage pattern. +## Example Usage +```terraform +resource "p0_gcp" "example" { + organization_id = "123456789012" +} + +locals { + project = "my_project_id" +} + +# Follow instructions for creating Terraform for IAM assessment in p0_gcp_iam_assessment documentation +# ... + +resource "p0_gcp_iam_assessment" "example" { + project = locals.project + depends_on = [google_project_iam_member.example] +} + +resource "google_project_iam_audit_config" "example" { + project = locals.project + service = "allServices" + audit_log_config { + log_type = "ADMIN_READ" + } + audit_log_config { + log_type = "DATA_READ" + } + audit_log_config { + log_type = "DATA_WRITE" + } +} + +# Data access logs are sent to P0 using this Pub/Sub topic +resource "google_pubsub_topic" "example" { + project = locals.project + name = p0_gcp.example.access_logs.pub_sub.topic_id +} + +# The log sink that writes to the P0 access-logging Pub/Sub topic +resource "google_logging_project_sink" "example" { + project = locals.project + name = p0_gcp.example.access_logs.logging.sink_id + destination = "pubsub.googleapis.com/projects/my_project/topics/${google_pubsub_topic.example.name}" + description = "P0 data access log sink" + + filter = p0_gcp.example.access_logs.logging.filter +} + +# Grants the logging service account permission to write to the access-logging Pub/Sub topic +resource "google_pubsub_topic_iam_member" "logging_example" { + project = locals.project + role = p0_gcp.example.access_logs.logging.role + topic = google_pubsub_topic.example.name + member = google_logging_project_sink.example.writer_identity +} + +# Grants P0 permission to read from the access-logging Pub/Sub topic +resource "google_pubsub_topic_iam_member" "p0_example" { + project = locals.project + role = p0_gcp.example.access_logs.predefined_role + topic = google_pubsub_topic.example.name + member = "serviceAccount:${p0_gcp.example.serviceAccountEmail}" +} + +# Finish the P0 access-logs installation +resource "p0_gcp_access_logs" "example" { + project = locals.project + depends_on = [ + google_logging_project_sink.example, + google_pubsub_topic_iam_member.p0_example + ] +} +``` ## Schema diff --git a/docs/resources/gcp_organization_access_logs.md b/docs/resources/gcp_organization_access_logs.md new file mode 100644 index 0000000..e58c2f4 --- /dev/null +++ b/docs/resources/gcp_organization_access_logs.md @@ -0,0 +1,110 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "p0_gcp_organization_access_logs Resource - p0" +subcategory: "" +description: |- + An installation of P0, on an entire Google Cloud organization, for access-log collection, + which enhances IAM assessment. Note that P0 will have access to logs from all your projects, not just those + configured for IAM assessment. + To use this resource, you must also: + create a Pub/Sub topic,create an organization logging sink, publishing to this topic,grant your logging service account permissions to publish to this Pub/Sub topic, andgrant P0 the ability to subscribe to this Pub/Sub topic. + Use the read-only attributes defined on p0_gcp to create the requisite Google Cloud infrastructure. + P0 recommends defining this infrastructure according to the example usage pattern. +--- + +# p0_gcp_organization_access_logs (Resource) + +An installation of P0, on an entire Google Cloud organization, for access-log collection, +which enhances IAM assessment. Note that P0 will have access to logs from all your projects, not just those +configured for IAM assessment. + +To use this resource, you must also: +- create a Pub/Sub topic, +- create an organization logging sink, publishing to this topic, +- grant your logging service account permissions to publish to this Pub/Sub topic, and +- grant P0 the ability to subscribe to this Pub/Sub topic. + +Use the read-only attributes defined on `p0_gcp` to create the requisite Google Cloud infrastructure. + +P0 recommends defining this infrastructure according to the example usage pattern. + +## Example Usage + +```terraform +resource "p0_gcp" "example" { + organization_id = "123456789012" +} + +locals { + logs_topic_project = "my-logs-project" +} + +resource "google_organization_iam_audit_config" "example" { + org_id = p0_gcp.example.org_id + service = "allServices" + audit_log_config { + log_type = "ADMIN_READ" + } + audit_log_config { + log_type = "DATA_READ" + } + audit_log_config { + log_type = "DATA_WRITE" + } +} + +# Data access logs are sent to P0 using this Pub/Sub topic +resource "google_pubsub_topic" "example" { + project = locals.logs_topic_project + name = p0_gcp.example.access_logs.pub_sub.topic_id +} + +# The log sink that writes to the P0 access-logging Pub/Sub topic +resource "google_logging_organization_sink" "example" { + org_id = p0_gcp.example.org_id + name = p0_gcp.example.access_logs.logging.sink_id + destination = "pubsub.googleapis.com/projects/${locals.logs_topic_project}/topics/${google_pubsub_topic.example.name}" + description = "P0 data access log sink" + + filter = p0_gcp.example.access_logs.logging.filter +} + +# Grants the logging service account permission to write to the access-logging Pub/Sub topic +resource "google_pubsub_topic_iam_member" "logging_example" { + project = locals.logs_topic_project + role = p0_gcp.example.access_logs.logging.role + topic = google_pubsub_topic.example.name + member = google_logging_organization_sink.example.writer_identity +} + +# Grants P0 permission to read from the access-logging Pub/Sub topic +resource "google_pubsub_topic_iam_member" "p0_example" { + project = locals.logs_topic_project + role = p0_gcp.example.access_logs.predefined_role + topic = google_pubsub_topic.example.name + member = "serviceAccount:${p0_gcp.example.serviceAccountEmail}" +} + +# Install organization access logging in P0 +resource "p0_gcp_access_logs" "example" { + topic_project_id = locals.logs_topic_project + depends_on = [ + google_logging_project_sink.example, + google_pubsub_topic_iam_member.p0_example + ] +} +``` + + +## Schema + +### Required + +- `topic_project_id` (String) The project identifier where the access-logs Pub/Sub topic should reside + +### Read-Only + +- `state` (String) This item's install progress in the P0 application: + - 'stage': The item has been staged for installation + - 'configure': The item is available to be added to P0, and may be configured + - 'installed': The item is fully installed diff --git a/examples/resources/p0_access_logs/resource.tf b/examples/resources/p0_gcp_access_logs/resource.tf similarity index 96% rename from examples/resources/p0_access_logs/resource.tf rename to examples/resources/p0_gcp_access_logs/resource.tf index db881c1..5c7c5bb 100644 --- a/examples/resources/p0_access_logs/resource.tf +++ b/examples/resources/p0_gcp_access_logs/resource.tf @@ -49,7 +49,7 @@ resource "google_pubsub_topic_iam_member" "logging_example" { project = locals.project role = p0_gcp.example.access_logs.logging.role topic = google_pubsub_topic.example.name - member = "your logging service account email" + member = google_logging_project_sink.example.writer_identity } # Grants P0 permission to read from the access-logging Pub/Sub topic diff --git a/examples/resources/p0_gcp_organization_access_logs/resource.tf b/examples/resources/p0_gcp_organization_access_logs/resource.tf new file mode 100644 index 0000000..36ab05c --- /dev/null +++ b/examples/resources/p0_gcp_organization_access_logs/resource.tf @@ -0,0 +1,62 @@ +resource "p0_gcp" "example" { + organization_id = "123456789012" +} + +locals { + logs_topic_project = "my-logs-project" +} + +resource "google_organization_iam_audit_config" "example" { + org_id = p0_gcp.example.org_id + service = "allServices" + audit_log_config { + log_type = "ADMIN_READ" + } + audit_log_config { + log_type = "DATA_READ" + } + audit_log_config { + log_type = "DATA_WRITE" + } +} + +# Data access logs are sent to P0 using this Pub/Sub topic +resource "google_pubsub_topic" "example" { + project = locals.logs_topic_project + name = p0_gcp.example.access_logs.pub_sub.topic_id +} + +# The log sink that writes to the P0 access-logging Pub/Sub topic +resource "google_logging_organization_sink" "example" { + org_id = p0_gcp.example.org_id + name = p0_gcp.example.access_logs.logging.sink_id + destination = "pubsub.googleapis.com/projects/${locals.logs_topic_project}/topics/${google_pubsub_topic.example.name}" + description = "P0 data access log sink" + + filter = p0_gcp.example.access_logs.logging.filter +} + +# Grants the logging service account permission to write to the access-logging Pub/Sub topic +resource "google_pubsub_topic_iam_member" "logging_example" { + project = locals.logs_topic_project + role = p0_gcp.example.access_logs.logging.role + topic = google_pubsub_topic.example.name + member = google_logging_organization_sink.example.writer_identity +} + +# Grants P0 permission to read from the access-logging Pub/Sub topic +resource "google_pubsub_topic_iam_member" "p0_example" { + project = locals.logs_topic_project + role = p0_gcp.example.access_logs.predefined_role + topic = google_pubsub_topic.example.name + member = "serviceAccount:${p0_gcp.example.serviceAccountEmail}" +} + +# Install organization access logging in P0 +resource "p0_gcp_access_logs" "example" { + topic_project_id = locals.logs_topic_project + depends_on = [ + google_logging_project_sink.example, + google_pubsub_topic_iam_member.p0_example + ] +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a95788b..4198689 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -105,14 +105,15 @@ func (p *P0Provider) Configure(ctx context.Context, req provider.ConfigureReques func (p *P0Provider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ resources.NewRoutingRules, - installaws.NewIamWriteStagedAws, installaws.NewAwsIamWrite, + installaws.NewIamWriteStagedAws, installgcp.NewGcp, installgcp.NewGcpAccessLogs, installgcp.NewGcpIamAssessment, installgcp.NewGcpIamAssessmentStaged, installgcp.NewGcpIamWrite, installgcp.NewGcpIamWriteStaged, + installgcp.NewGcpOrgAccessLogs, installgcp.NewGcpSharingRestriction, installssh.NewSshAwsIamWrite, installssh.NewSshGcpIamWrite, diff --git a/internal/provider/resources/install/gcp/common.go b/internal/provider/resources/install/gcp/common.go index b776709..6611dfe 100644 --- a/internal/provider/resources/install/gcp/common.go +++ b/internal/provider/resources/install/gcp/common.go @@ -4,10 +4,12 @@ import ( "context" "regexp" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/p0-security/terraform-provider-p0/internal" installresources "github.com/p0-security/terraform-provider-p0/internal/provider/resources/install" @@ -45,6 +47,7 @@ type gcpItemApi struct { const ( AccessLogs = "access-logs" GcpKey = "gcloud" + OrgAccessLogs = "org-access-logs" SharingRestriction = "sharing-restriction" ) @@ -71,6 +74,15 @@ var predefinedRole = schema.StringAttribute{ MarkdownDescription: `The predefined role that should be granted to P0, in order to install projects for IAM management`, } +var projectValidators = []validator.String{ + stringvalidator.RegexMatches(GcpProjectIdRegex, "GCP project IDs should consist only of alphanumeric characters and hyphens"), +} + +var stateAttribute = schema.StringAttribute{ + Computed: true, + MarkdownDescription: installresources.StateMarkdownDescription, +} + var itemAttributes = map[string]schema.Attribute{ // In P0 we would name this 'id' or 'project_id'; it is named 'project' here to align with Terraform's naming for // Google Cloud resources @@ -80,11 +92,9 @@ var itemAttributes = map[string]schema.Attribute{ PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, + Validators: projectValidators, }, - "state": schema.StringAttribute{ - Computed: true, - MarkdownDescription: installresources.StateMarkdownDescription, - }, + "state": stateAttribute, } func permissions(name string) schema.ListAttribute { @@ -143,3 +153,8 @@ func newItemInstaller(component string, providerData *internal.P0ProviderData) * ToJson: itemToJson, } } + +func singletonGetId(data any) *string { + key := installresources.SingletonKey + return &key +} diff --git a/internal/provider/resources/install/gcp/org_access_logs.go b/internal/provider/resources/install/gcp/org_access_logs.go new file mode 100644 index 0000000..c00624d --- /dev/null +++ b/internal/provider/resources/install/gcp/org_access_logs.go @@ -0,0 +1,139 @@ +package installgcp + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/p0-security/terraform-provider-p0/internal" + installresources "github.com/p0-security/terraform-provider-p0/internal/provider/resources/install" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &GcpOrgAccessLogs{} +var _ resource.ResourceWithImportState = &GcpOrgAccessLogs{} +var _ resource.ResourceWithConfigure = &GcpOrgAccessLogs{} + +func NewGcpOrgAccessLogs() resource.Resource { + return &GcpOrgAccessLogs{} +} + +type GcpOrgAccessLogs struct { + installer *installresources.Install +} + +type gcpOrgAccessLogsModel struct { + State types.String `tfsdk:"state"` + TopicProjectId types.String `tfsdk:"topic_project_id"` +} + +type gcpOrgAccessLogsJson struct { + State string `json:"state"` + TopicProjectId string `json:"topicProjectId"` +} + +type gcpOrgAccessLogsApi struct { + Item gcpOrgAccessLogsJson `json:"item"` +} + +func (r *GcpOrgAccessLogs) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_gcp_organization_access_logs" +} + +func (r *GcpOrgAccessLogs) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `An installation of P0, on an entire Google Cloud organization, for access-log collection, +which enhances IAM assessment. Note that P0 will have access to logs from all your projects, not just those +configured for IAM assessment. + +To use this resource, you must also: +- create a Pub/Sub topic, +- create an organization logging sink, publishing to this topic, +- grant your logging service account permissions to publish to this Pub/Sub topic, and +- grant P0 the ability to subscribe to this Pub/Sub topic. + +Use the read-only attributes defined on ` + "`p0_gcp`" + ` to create the requisite Google Cloud infrastructure. + +P0 recommends defining this infrastructure according to the example usage pattern.`, + Attributes: map[string]schema.Attribute{ + "state": stateAttribute, + "topic_project_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: `The project identifier where the access-logs Pub/Sub topic should reside`, + Validators: projectValidators, + }, + }, + } +} + +func (r *GcpOrgAccessLogs) getItemJson(json any) any { + inner, ok := json.(*gcpOrgAccessLogsApi) + if !ok { + return nil + } + return &inner.Item +} + +func (r *GcpOrgAccessLogs) fromJson(ctx context.Context, diags *diag.Diagnostics, id string, json any) any { + data := gcpOrgAccessLogsModel{} + jsonv, ok := json.(*gcpOrgAccessLogsJson) + if !ok { + return nil + } + + data.State = types.StringValue(jsonv.State) + data.TopicProjectId = types.StringValue(jsonv.TopicProjectId) + + return &data +} + +func (r *GcpOrgAccessLogs) toJson(data any) any { + json := gcpOrgAccessLogsJson{} + datav, ok := data.(*gcpOrgAccessLogsModel) + if !ok { + return nil + } + + // can omit state here as it's filled by the backend + json.TopicProjectId = datav.TopicProjectId.ValueString() + return json +} + +func (r *GcpOrgAccessLogs) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData := internal.Configure(&req, resp) + r.installer = &installresources.Install{ + Integration: GcpKey, + Component: OrgAccessLogs, + ProviderData: providerData, + GetId: singletonGetId, + GetItemJson: r.getItemJson, + FromJson: r.fromJson, + ToJson: r.toJson, + } +} + +func (s *GcpOrgAccessLogs) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var json gcpOrgAccessLogsApi + var data gcpOrgAccessLogsModel + s.installer.Stage(ctx, &resp.Diagnostics, &req.Plan, &resp.State, &json, &data) + s.installer.UpsertFromStage(ctx, &resp.Diagnostics, &req.Plan, &resp.State, &json, &data) +} + +func (s *GcpOrgAccessLogs) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + s.installer.Read(ctx, &resp.Diagnostics, &resp.State, &gcpOrgAccessLogsApi{}, &gcpOrgAccessLogsModel{}) +} + +func (s *GcpOrgAccessLogs) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + s.installer.Delete(ctx, &resp.Diagnostics, &req.State, &gcpOrgAccessLogsModel{}) +} + +func (s *GcpOrgAccessLogs) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + s.installer.UpsertFromStage(ctx, &resp.Diagnostics, &req.Plan, &resp.State, &gcpOrgAccessLogsApi{}, &gcpOrgAccessLogsModel{}) +} + +func (s *GcpOrgAccessLogs) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Empty(), req, resp) +}