diff --git a/docs/resources/policy_storage.md b/docs/resources/policy_storage.md new file mode 100644 index 00000000..275df600 --- /dev/null +++ b/docs/resources/policy_storage.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "boundary_policy_storage Resource - terraform-provider-boundary" +subcategory: "" +description: |- + The storage policy resource allows you to configure a Boundary storage policy. Storage policies allow an admin to configure how long session recordings must be stored and when to delete them. Storage policies must be applied to the global scope or an org scope in order to take effect. +--- + +# boundary_policy_storage (Resource) + +The storage policy resource allows you to configure a Boundary storage policy. Storage policies allow an admin to configure how long session recordings must be stored and when to delete them. Storage policies must be applied to the global scope or an org scope in order to take effect. + + + + +## Schema + +### Required + +- `scope_id` (String) The scope for this policy. + +### Optional + +- `delete_after_days` (Number) The number of days after which a session recording will be automatically deleted. Defaults to 0: never automatically delete. However, delete_after_days and retain_for_days cannot both be 0. +- `delete_after_overridable` (Boolean) Whether or not the associated delete_after_days value can be overridden by org scopes. Note: if the associated delete_after_days value is 0, overridable is ignored +- `description` (String) The policy description. +- `name` (String) The policy name. Defaults to the resource name. +- `retain_for_days` (Number) The number of days a session recording is required to be stored. Defaults to 0: allow deletions at any time. However, retain_for_days and delete_after_days cannot both be 0. +- `retain_for_overridable` (Boolean) Whether or not the associated retain_for_days value can be overridden by org scopes. Note: if the associated retain_for_days value is 0, overridable is ignored. + +### Read-Only + +- `id` (String) The ID of the policy. diff --git a/docs/resources/scope_policy_attachment.md b/docs/resources/scope_policy_attachment.md new file mode 100644 index 00000000..3c20681e --- /dev/null +++ b/docs/resources/scope_policy_attachment.md @@ -0,0 +1,25 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "boundary_scope_policy_attachment Resource - terraform-provider-boundary" +subcategory: "" +description: |- + +--- + +# boundary_scope_policy_attachment (Resource) + + + + + + +## Schema + +### Required + +- `policy_id` (String) +- `scope_id` (String) + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/resources/boundary_policy/resource.tf b/examples/resources/boundary_policy/resource.tf new file mode 100644 index 00000000..afba0584 --- /dev/null +++ b/examples/resources/boundary_policy/resource.tf @@ -0,0 +1,27 @@ +resource "boundary_scope" "global" { + global_scope = true + scope_id = "global" +} + +resource "boundary_scope" "org" { + name = "organization_one" + description = "My first scope!" + scope_id = boundary_scope.global.id + auto_create_admin_role = true + auto_create_default_role = true +} + +resource "boundary_policy_storage" "foo" { + name = "foo" + description = "Foo policy" + scope_id = boundary_scope.org.id + retain_for_days = 10 + retain_for_overridable = false + delete_after_days = 10 + delete_after_overridable = true +} + +resource "boundary_scope_policy_attachment" "foo_attachment" { + scope_id = boundary_scope.org.id + policy_id = boundary_policy_storage.foo.id +} diff --git a/go.mod b/go.mod index 02c92eb4..962a4b52 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.21.5 require ( github.com/YakDriver/regexache v0.23.0 github.com/hashicorp/boundary v0.15.0 - github.com/hashicorp/boundary/api v0.0.45 - github.com/hashicorp/boundary/sdk v0.0.42 + github.com/hashicorp/boundary/api v0.0.46-0.20240209231500-1b1c8b3bf9a6 + github.com/hashicorp/boundary/sdk v0.0.43-0.20240209231500-1b1c8b3bf9a6 github.com/hashicorp/cap v0.5.0 github.com/hashicorp/cap/ldap v0.0.0-20240206183135-ed8f24513744 github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 diff --git a/go.sum b/go.sum index 81513385..e7dcfb87 100644 --- a/go.sum +++ b/go.sum @@ -233,10 +233,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= github.com/hashicorp/boundary v0.15.0 h1:np75Igvt9ICQN3ZXJsXw91P5QFsvGDsNYoBNMFGeRcw= github.com/hashicorp/boundary v0.15.0/go.mod h1:CJxelVYlFu4fU85wtxi50swKrVg665/GiIpd1Cxez1I= -github.com/hashicorp/boundary/api v0.0.45 h1:N4ZIjsYf4cwVfcW5sv0WitWiFr8UyudjA/ozCRkCckw= -github.com/hashicorp/boundary/api v0.0.45/go.mod h1:UrsSgLDpFmG6Eq+OIdpC8BSwbHwklsxL/ZCWklr0g5A= -github.com/hashicorp/boundary/sdk v0.0.42 h1:iRDdnM6/6q9qLtBP7/cpq/FWQGf69TFSsBQFxgGK8Jw= -github.com/hashicorp/boundary/sdk v0.0.42/go.mod h1:QJhm4rky2YMtk9DS5Rj8QJhDKB/6zQbVrm3R0QgdYmQ= +github.com/hashicorp/boundary/api v0.0.46-0.20240209231500-1b1c8b3bf9a6 h1:6/GdZZDW8ChPduD9rFnr8VGZmTPGGvlMrtELWqmoz8I= +github.com/hashicorp/boundary/api v0.0.46-0.20240209231500-1b1c8b3bf9a6/go.mod h1:UrsSgLDpFmG6Eq+OIdpC8BSwbHwklsxL/ZCWklr0g5A= +github.com/hashicorp/boundary/sdk v0.0.43-0.20240209231500-1b1c8b3bf9a6 h1:DIfWtA4OOcCJlIL3LJUERF7M90ephrEgn16KWWDX2KU= +github.com/hashicorp/boundary/sdk v0.0.43-0.20240209231500-1b1c8b3bf9a6/go.mod h1:QJhm4rky2YMtk9DS5Rj8QJhDKB/6zQbVrm3R0QgdYmQ= github.com/hashicorp/cap v0.5.0 h1:YIlAYxdXXtx2IL1JDvP2OyEl55Ooi0yl573kSB9Orlw= github.com/hashicorp/cap v0.5.0/go.mod h1:IAy00Er+ZFpMo+5x6B4bkO2HgpzgrkfsuDWMmHAuKUE= github.com/hashicorp/cap/ldap v0.0.0-20240206183135-ed8f24513744 h1:6cU01ORaCUxRXRS0YYnmm4tMfAvFRjamhjgMrgmoWZs= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 56b547e1..abb0c89b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -120,6 +120,8 @@ func New() *schema.Provider { "boundary_host_set": resourceHostSet(), "boundary_host_set_static": resourceHostSetStatic(), "boundary_host_set_plugin": resourceHostSetPlugin(), + "boundary_policy_storage": resourcePolicyStorage(), + "boundary_scope_policy_attachment": resourceScopePolicyAttachment(), "boundary_role": resourceRole(), "boundary_scope": resourceScope(), "boundary_storage_bucket": resourceStorageBucket(), diff --git a/internal/provider/resource_policy_storage.go b/internal/provider/resource_policy_storage.go new file mode 100644 index 00000000..08dc7756 --- /dev/null +++ b/internal/provider/resource_policy_storage.go @@ -0,0 +1,303 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/policies" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + policyStorageRetainForDaysKey = "retain_for_days" + policyStorageRetainForOverridableKey = "retain_for_overridable" + policyStorageDeleteAfterDaysKey = "delete_after_days" + policyStorageDeleteAfterOverridableKey = "delete_after_overridable" + policyTypeStorage = "storage" + daysField = "days" + overridableField = "overridable" +) + +func resourcePolicyStorage() *schema.Resource { + return &schema.Resource{ + Description: "The storage policy resource allows you to configure a Boundary storage policy. " + + "Storage policies allow an admin to configure how long session recordings must be stored and when " + + "to delete them. Storage policies must be applied to the global scope or an org scope in order to take effect.", + CreateContext: resourcePolicyStorageCreate, + ReadContext: resourcePolicyStorageRead, + UpdateContext: resourcePolicyStorageUpdate, + DeleteContext: resourcePolicyStorageDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The ID of the policy.", + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Description: "The policy name. Defaults to the resource name.", + Type: schema.TypeString, + Optional: true, + }, + DescriptionKey: { + Description: "The policy description.", + Type: schema.TypeString, + Optional: true, + }, + ScopeIdKey: { + Description: "The scope for this policy.", + Type: schema.TypeString, + Required: true, + }, + policyStorageRetainForDaysKey: { + Description: "The number of days a session recording is required to be stored. Defaults to 0: allow deletions at any time. However, " + policyStorageRetainForDaysKey + " and " + policyStorageDeleteAfterDaysKey + " cannot both be 0.", + Type: schema.TypeInt, + Optional: true, + AtLeastOneOf: []string{policyStorageDeleteAfterDaysKey}, + }, + policyStorageRetainForOverridableKey: { + Description: "Whether or not the associated " + policyStorageRetainForDaysKey + " value can be overridden by org scopes. Note: if the associated " + policyStorageRetainForDaysKey + " value is 0, overridable is ignored.", + Type: schema.TypeBool, + Optional: true, + }, + policyStorageDeleteAfterDaysKey: { + Description: "The number of days after which a session recording will be automatically deleted. Defaults to 0: never automatically delete. However, " + policyStorageDeleteAfterDaysKey + " and " + policyStorageRetainForDaysKey + " cannot both be 0.", + Type: schema.TypeInt, + Optional: true, + AtLeastOneOf: []string{policyStorageRetainForDaysKey}, + }, + policyStorageDeleteAfterOverridableKey: { + Description: "Whether or not the associated " + policyStorageDeleteAfterDaysKey + " value can be overridden by org scopes. Note: if the associated " + policyStorageDeleteAfterDaysKey + " value is 0, overridable is ignored", + Type: schema.TypeBool, + Optional: true, + }, + }, + } +} + +func setFromPolicyItem(d *schema.ResourceData, p *policies.Policy) error { + d.SetId(p.Id) + + if err := d.Set(NameKey, p.Name); err != nil { + return err + } + + if err := d.Set(DescriptionKey, p.Description); err != nil { + return err + } + + if err := d.Set(ScopeIdKey, p.ScopeId); err != nil { + return err + } + + // parse attributes to the retain/delete values so that the tf syntax is simpler + attributes, err := p.GetStoragePolicyAttributes() + if err != nil { + return err + } + + // retain_for attribute + if attributes.RetainFor != nil { + if err := d.Set(policyStorageRetainForDaysKey, attributes.RetainFor.Days); err != nil { + return err + } + if err := d.Set(policyStorageRetainForOverridableKey, attributes.RetainFor.Overridable); err != nil { + return err + } + } else { + // no RetainFor, set to nil + if err := d.Set(policyStorageRetainForDaysKey, nil); err != nil { + return err + } + if err := d.Set(policyStorageRetainForOverridableKey, nil); err != nil { + return err + } + } + + // delete_after attribute + if attributes.DeleteAfter != nil { + if err := d.Set(policyStorageDeleteAfterDaysKey, attributes.DeleteAfter.Days); err != nil { + return err + } + if err := d.Set(policyStorageDeleteAfterOverridableKey, attributes.DeleteAfter.Overridable); err != nil { + return err + } + } else { + // no DeleteAfter, set to nil + if err := d.Set(policyStorageDeleteAfterDaysKey, nil); err != nil { + return err + } + if err := d.Set(policyStorageDeleteAfterOverridableKey, nil); err != nil { + return err + } + } + + return nil +} + +func resourcePolicyStorageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + pClient := policies.NewClient(md.client) + opts := []policies.Option{} + + var scopeId string + if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok { + scopeId = scopeIdVal.(string) + } else { + return diag.Errorf("no scope ID provided") + } + + if nameVal, ok := d.GetOk(NameKey); ok { + opts = append(opts, policies.WithName(nameVal.(string))) + } + + if descVal, ok := d.GetOk(DescriptionKey); ok { + opts = append(opts, policies.WithDescription(descVal.(string))) + } + + if retainDaysVal, ok := d.GetOk(policyStorageRetainForDaysKey); ok { + opts = append(opts, policies.WithStoragePolicyRetainForDays(int32(retainDaysVal.(int)))) + } else { + opts = append(opts, policies.DefaultStoragePolicyRetainForDays()) + } + if retainOverrideVal, ok := d.GetOk(policyStorageRetainForOverridableKey); ok { + opts = append(opts, policies.WithStoragePolicyRetainForOverridable(retainOverrideVal.(bool))) + } else { + opts = append(opts, policies.DefaultStoragePolicyRetainForOverridable()) + } + + if deleteDaysVal, ok := d.GetOk(policyStorageDeleteAfterDaysKey); ok { + opts = append(opts, policies.WithStoragePolicyDeleteAfterDays(int32(deleteDaysVal.(int)))) + } else { + opts = append(opts, policies.DefaultStoragePolicyDeleteAfterDays()) + } + if deleteOverrideVal, ok := d.GetOk(policyStorageDeleteAfterOverridableKey); ok { + opts = append(opts, policies.WithStoragePolicyDeleteAfterOverridable(deleteOverrideVal.(bool))) + } else { + opts = append(opts, policies.DefaultStoragePolicyDeleteAfterOverridable()) + } + + p, err := pClient.Create(ctx, policyTypeStorage, scopeId, opts...) + if err != nil { + return diag.Errorf("error creating storage policy: %v", err) + } + if p == nil { + return diag.Errorf("nil storage policy after create") + } + + if err := setFromPolicyItem(d, p.GetItem()); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourcePolicyStorageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + pClient := policies.NewClient(md.client) + + p, err := pClient.Read(ctx, d.Id()) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.Errorf("error reading policy: %v", err) + } + if p == nil { + return diag.Errorf("policy nil after read") + } + + if err := setFromPolicyItem(d, p.GetItem()); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourcePolicyStorageUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + pClient := policies.NewClient(md.client) + opts := []policies.Option{} + + if d.HasChange(NameKey) { + if nameVal, ok := d.GetOk(NameKey); ok { + name := nameVal.(string) + opts = append(opts, policies.WithName(name)) + } else { + opts = append(opts, policies.DefaultName()) + } + } + + if d.HasChange(DescriptionKey) { + if descriptionVal, ok := d.GetOk(DescriptionKey); ok { + description := descriptionVal.(string) + opts = append(opts, policies.WithDescription(description)) + } else { + opts = append(opts, policies.DefaultDescription()) + } + } + + if d.HasChanges(policyStorageRetainForDaysKey, policyStorageRetainForOverridableKey) { + if retainDaysVal, ok := d.GetOk(policyStorageRetainForDaysKey); ok { + opts = append(opts, policies.WithStoragePolicyRetainForDays(int32(retainDaysVal.(int)))) + } else { + opts = append(opts, policies.DefaultStoragePolicyRetainForDays()) + } + if retainOverrideVal, ok := d.GetOk(policyStorageRetainForOverridableKey); ok { + opts = append(opts, policies.WithStoragePolicyRetainForOverridable(retainOverrideVal.(bool))) + } else { + opts = append(opts, policies.DefaultStoragePolicyRetainForOverridable()) + } + } + + if d.HasChanges(policyStorageDeleteAfterDaysKey, policyStorageDeleteAfterOverridableKey) { + if deleteDaysVal, ok := d.GetOk(policyStorageDeleteAfterDaysKey); ok { + opts = append(opts, policies.WithStoragePolicyDeleteAfterDays(int32(deleteDaysVal.(int)))) + } else { + opts = append(opts, policies.DefaultStoragePolicyDeleteAfterDays()) + } + if deleteOverrideVal, ok := d.GetOk(policyStorageDeleteAfterOverridableKey); ok { + opts = append(opts, policies.WithStoragePolicyDeleteAfterOverridable(deleteOverrideVal.(bool))) + } else { + opts = append(opts, policies.DefaultStoragePolicyDeleteAfterOverridable()) + } + } + + if len(opts) > 0 { + opts = append(opts, policies.WithAutomaticVersioning(true)) + p, err := pClient.Update(ctx, d.Id(), 0, opts...) + if err != nil { + return diag.Errorf("error creating storage policy: %v", err) + } + if p == nil { + return diag.Errorf("nil storage policy after create") + } + + if err := setFromPolicyItem(d, p.GetItem()); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourcePolicyStorageDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + pClient := policies.NewClient(md.client) + + _, err := pClient.Delete(ctx, d.Id()) + if err != nil { + return diag.Errorf("error deleting policy: %v", err) + } + + return nil +} diff --git a/internal/provider/resource_scope_policy_attachment.go b/internal/provider/resource_scope_policy_attachment.go new file mode 100644 index 00000000..31778103 --- /dev/null +++ b/internal/provider/resource_scope_policy_attachment.go @@ -0,0 +1,164 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + policyIdKey = "policy_id" + storagePolicyPrefix = "pst_" +) + +func resourceScopePolicyAttachment() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceScopePolicyAttachmentCreate, + ReadWithoutTimeout: resourceScopePolicyAttachmentRead, + DeleteWithoutTimeout: resourceScopePolicyAttachmentDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + ScopeIdKey: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + policyIdKey: { + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + }, + } +} + +func resourceScopePolicyAttachmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + scp := scopes.NewClient(md.client) + opts := []scopes.Option{scopes.WithAutomaticVersioning(true)} + + var scopeId string + if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok { + scopeId = scopeIdVal.(string) + } else { + return diag.Errorf("no scope ID provided") + } + + if err := d.Set(ScopeIdKey, scopeId); err != nil { + return diag.FromErr(err) + } + + var policyId string + if policyIdVal, ok := d.GetOk(policyIdKey); ok { + policyId = policyIdVal.(string) + } else { + return diag.Errorf("no policy ID provided") + } + + // other policy types may be added here and in other switches with this file + switch { + case strings.HasPrefix(policyId, storagePolicyPrefix): + if _, err := scp.AttachStoragePolicy(ctx, scopeId, 0, policyId, opts...); err != nil { + return diag.FromErr(err) + } + default: + return diag.Errorf("unknown policy type provided.") + } + + if err := d.Set(policyIdKey, policyId); err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%s:%s", policyId, scopeId)) + + return nil +} + +func resourceScopePolicyAttachmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + scp := scopes.NewClient(md.client) + + var scopeId string + if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok { + scopeId = scopeIdVal.(string) + } else { + return diag.Errorf("no scope ID provided") + } + + var policyId string + if policyIdVal, ok := d.GetOk(policyIdKey); ok { + policyId = policyIdVal.(string) + } else { + return diag.Errorf("no policy ID provided") + } + + s, err := scp.Read(ctx, scopeId) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound { + // the scope is gone, destroy this resource + d.SetId("") + return nil + } + return diag.Errorf("error reading scope: %v", err) + } + if s == nil { + return diag.Errorf("scope nil after read") + } + + switch { + case strings.HasPrefix(policyId, storagePolicyPrefix): + if policyId != s.GetItem().StoragePolicyId { + // this policy is no longer attached to the scope, destroy this resource + d.SetId("") + return nil + } + default: + return diag.Errorf("unknown policy type provided.") + } + + return nil +} + +func resourceScopePolicyAttachmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + scp := scopes.NewClient(md.client) + opts := []scopes.Option{scopes.WithAutomaticVersioning(true)} + + var scopeId string + if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok { + scopeId = scopeIdVal.(string) + } else { + return diag.Errorf("no scope ID provided") + } + + var policyId string + if policyIdVal, ok := d.GetOk(policyIdKey); ok { + policyId = policyIdVal.(string) + } else { + return diag.Errorf("no policy ID provided") + } + + switch { + case strings.HasPrefix(policyId, storagePolicyPrefix): + if _, err := scp.DetachStoragePolicy(ctx, scopeId, 0, opts...); err != nil { + return diag.FromErr(err) + } + default: + return diag.Errorf("unknown policy type provided.") + } + + return nil +}