From 673e8d92afadf504e96a1ebe1b38795aa7888ae5 Mon Sep 17 00:00:00 2001 From: Deepak Gupta Date: Tue, 3 Dec 2024 11:21:04 -0500 Subject: [PATCH] implement policy v2 resource using gRPC --- cyral/client/client.go | 4 + cyral/internal/policy/v2/constants.go | 20 +-- cyral/internal/policy/v2/datasource.go | 19 +-- cyral/internal/policy/v2/model.go | 221 +++++++++++++++++-------- cyral/internal/policy/v2/resource.go | 34 ++-- go.mod | 8 +- go.sum | 12 ++ 7 files changed, 196 insertions(+), 122 deletions(-) diff --git a/cyral/client/client.go b/cyral/client/client.go index d19e1cb2..d82dfc91 100644 --- a/cyral/client/client.go +++ b/cyral/client/client.go @@ -85,6 +85,10 @@ func New(clientID, clientSecret, controlPlane string, tlsSkipVerify bool) (*Clie }, nil } +func (c *Client) GRPCClient() grpc.ClientConnInterface { + return c.grpcClient +} + // DoRequest calls the httpMethod informed and delivers the resourceData as a payload, // filling the response parameter (if not nil) with the response body. func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resourceData interface{}) ([]byte, error) { diff --git a/cyral/internal/policy/v2/constants.go b/cyral/internal/policy/v2/constants.go index 538c8ef3..761313fe 100644 --- a/cyral/internal/policy/v2/constants.go +++ b/cyral/internal/policy/v2/constants.go @@ -1,22 +1,6 @@ package policyv2 const ( - resourceName = "cyral_policy_v2" - dataSourceName = resourceName - apiPathLocal = "v2/policies/local" - apiPathGlobal = "v2/policies/global" - apiPathApproval = "v2/policies/approval" + resourceName = "cyral_policy_v2" + dataSourceName = resourceName ) - -func getAPIPath(policyType string) string { - switch policyType { - case "POLICY_TYPE_LOCAL", "local": - return apiPathLocal - case "POLICY_TYPE_GLOBAL", "global": - return apiPathGlobal - case "POLICY_TYPE_APPROVAL", "approval": - return apiPathApproval - default: - return "" - } -} diff --git a/cyral/internal/policy/v2/datasource.go b/cyral/internal/policy/v2/datasource.go index 025eac36..cc7fe42b 100644 --- a/cyral/internal/policy/v2/datasource.go +++ b/cyral/internal/policy/v2/datasource.go @@ -1,28 +1,25 @@ package policyv2 import ( - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/cyralinc/terraform-provider-cyral/cyral/client" "github.com/cyralinc/terraform-provider-cyral/cyral/core" "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" ) -var dsContextHandler = core.DefaultContextHandler{ - ResourceName: dataSourceName, - ResourceType: resourcetype.DataSource, - SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &PolicyV2{} }, - ReadUpdateDeleteURLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/%s/%s", c.ControlPlane, getAPIPath(d.Get("type").(string)), d.Get("id").(string)) - }, +var dsContextHandler = core.GenericContextHandler{ + ResourceName: dataSourceName, + ResourceType: resourcetype.DataSource, + Read: readPolicy, + Create: createPolicy, + Update: updatePolicy, + Delete: deletePolicy, } func dataSourceSchema() *schema.Resource { return &schema.Resource{ Description: "This data source provides information about a policy.", - ReadContext: dsContextHandler.ReadContext(), + ReadContext: dsContextHandler.ReadContext, Schema: map[string]*schema.Schema{ "id": { Description: "Identifier for the policy, unique within the policy type.", diff --git a/cyral/internal/policy/v2/model.go b/cyral/internal/policy/v2/model.go index f62ac39d..8b3f00ed 100644 --- a/cyral/internal/policy/v2/model.go +++ b/cyral/internal/policy/v2/model.go @@ -1,8 +1,15 @@ package policyv2 import ( + "context" "fmt" + "time" + methods "buf.build/gen/go/cyral/policy/grpc/go/policy/v1/policyv1grpc" + msg "buf.build/gen/go/cyral/policy/protocolbuffers/go/policy/v1" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" "github.com/cyralinc/terraform-provider-cyral/cyral/utils" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -14,127 +21,201 @@ type ChangeInfo struct { Timestamp string `json:"timestamp,omitempty"` } -// ToMap converts ChangeInfo to a map -func (c ChangeInfo) ToMap() map[string]interface{} { +// changeInfoToMap converts ChangeInfo to a map +func changeInfoToMap(c *msg.ChangeInfo) map[string]interface{} { return map[string]interface{}{ - "actor": c.Actor, - "actor_type": c.ActorType, - "timestamp": c.Timestamp, + "actor": c.GetActor(), + "actor_type": c.GetActorType().String(), + "timestamp": c.GetTimestamp().AsTime().Format(time.RFC3339), } } -// PolicyV2 represents the top-level policy structure -type PolicyV2 struct { - Policy Policy `json:"policy,omitempty"` -} - -type Scope struct { - RepoIds []string `json:"repoIds,omitempty"` -} - -// ToMap converts Scope to a list of maps -func (s *Scope) ToMap() []map[string]interface{} { +// scopeToMap converts Scope to a list of maps +func scopeToMap(s *msg.Scope) []map[string]interface{} { return []map[string]interface{}{ { - "repo_ids": s.RepoIds, + "repo_ids": s.GetRepoIds(), }, } } -// Policy represents the policy details -type Policy struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Enabled bool `json:"enabled,omitempty"` - Scope *Scope `json:"scope,omitempty"` - Tags []string `json:"tags,omitempty"` - ValidFrom string `json:"validFrom,omitempty"` - ValidUntil string `json:"validUntil,omitempty"` - Document string `json:"document,omitempty"` - LastUpdated ChangeInfo `json:"lastUpdated,omitempty"` - Created ChangeInfo `json:"created,omitempty"` - Enforced bool `json:"enforced,omitempty"` - Type string `json:"type,omitempty"` -} - -// WriteToSchema writes the policy data to the schema -func (r PolicyV2) WriteToSchema(d *schema.ResourceData) error { - if err := d.Set("id", r.Policy.ID); err != nil { +// updateSchema writes the policy data to the schema +func updateSchema(p *msg.Policy, ptype msg.PolicyType, d *schema.ResourceData) error { + if err := d.Set("id", p.GetId()); err != nil { return fmt.Errorf("error setting 'id' field: %w", err) } - if err := d.Set("name", r.Policy.Name); err != nil { + if err := d.Set("name", p.GetName()); err != nil { return fmt.Errorf("error setting 'name' field: %w", err) } - if err := d.Set("description", r.Policy.Description); err != nil { + if err := d.Set("description", p.GetDescription()); err != nil { return fmt.Errorf("error setting 'description' field: %w", err) } - if err := d.Set("enabled", r.Policy.Enabled); err != nil { + if err := d.Set("enabled", p.GetEnabled()); err != nil { return fmt.Errorf("error setting 'enabled' field: %w", err) } - if err := d.Set("tags", r.Policy.Tags); err != nil { + if err := d.Set("tags", p.GetTags()); err != nil { return fmt.Errorf("error setting 'tags' field: %w", err) } - if err := d.Set("valid_from", r.Policy.ValidFrom); err != nil { + if err := d.Set("valid_from", timestampFromProtobuf(p.GetValidFrom())); err != nil { return fmt.Errorf("error setting 'valid_from' field: %w", err) } - if err := d.Set("valid_until", r.Policy.ValidUntil); err != nil { + if err := d.Set("valid_until", timestampFromProtobuf(p.GetValidUntil())); err != nil { return fmt.Errorf("error setting 'valid_until' field: %w", err) } - if err := d.Set("document", r.Policy.Document); err != nil { + if err := d.Set("document", p.GetDocument()); err != nil { return fmt.Errorf("error setting 'document' field: %w", err) } - - // Use the ToMap method to set the last_updated and created fields - if err := d.Set("last_updated", r.Policy.LastUpdated.ToMap()); err != nil { + // Use the changeInfoToMap method to set the last_updated and created fields + if err := d.Set("last_updated", changeInfoToMap(p.GetLastUpdated())); err != nil { return fmt.Errorf("error setting 'last_updated' field: %w", err) } - if err := d.Set("created", r.Policy.Created.ToMap()); err != nil { + if err := d.Set("created", changeInfoToMap(p.GetCreated())); err != nil { return fmt.Errorf("error setting 'created' field: %w", err) } - if err := d.Set("enforced", r.Policy.Enforced); err != nil { + if err := d.Set("enforced", p.GetEnforced()); err != nil { return fmt.Errorf("error setting 'enforced' field: %w", err) } - if r.Policy.Type != "" { - if err := d.Set("type", r.Policy.Type); err != nil { + // policy types have aliases, so we don't want to set the policy type + // except if the new value is not an alias for the old one. + if msg.PolicyType_value[d.Get("type").(string)] != int32(ptype) { + if err := d.Set("type", ptype.String()); err != nil { return fmt.Errorf("error setting 'type' field: %w", err) } } - if r.Policy.Scope != nil { - if err := d.Set("scope", r.Policy.Scope.ToMap()); err != nil { + if p.GetScope() != nil { + if err := d.Set("scope", scopeToMap(p.GetScope())); err != nil { return fmt.Errorf("error setting 'scope' field: %w", err) } } - d.SetId(r.Policy.ID) + d.SetId(p.GetId()) return nil } -// ReadFromSchema reads the policy data from the schema -func (r *PolicyV2) ReadFromSchema(d *schema.ResourceData) error { - r.Policy.ID = d.Get("id").(string) - r.Policy.Name = d.Get("name").(string) - r.Policy.Description = d.Get("description").(string) - r.Policy.Enabled = d.Get("enabled").(bool) - r.Policy.Tags = utils.ConvertFromInterfaceList[string](d.Get("tags").([]interface{})) - r.Policy.ValidFrom = d.Get("valid_from").(string) - r.Policy.ValidUntil = d.Get("valid_until").(string) - r.Policy.Document = d.Get("document").(string) - r.Policy.Enforced = d.Get("enforced").(bool) - r.Policy.Type = d.Get("type").(string) +func timestampFromResourceData(key string, d *schema.ResourceData) (*timestamppb.Timestamp, error) { + if v, ok := d.GetOk(key); ok { + ts := v.(string) + if ts == "" { + return nil, nil + } + if t, err := time.Parse(time.RFC3339, ts); err != nil { + return nil, fmt.Errorf("invalid valid_from value: %s", ts) + } else { + return timestamppb.New(t), nil + } + } + return nil, nil +} + +func timestampFromProtobuf(ts *timestamppb.Timestamp) string { + if ts == nil { + return "" + } + return ts.AsTime().Format(time.RFC3339) +} + +func policyAndTypeFromSchema(d *schema.ResourceData) (*msg.Policy, msg.PolicyType, error) { + ptypeString := d.Get("type").(string) + ptype := msg.PolicyType(msg.PolicyType_value[ptypeString]) + if ptype == msg.PolicyType_POLICY_TYPE_UNSPECIFIED { + return nil, msg.PolicyType_POLICY_TYPE_UNSPECIFIED, fmt.Errorf( + "invalid policy type: %s", ptypeString, + ) + } + p := &msg.Policy{ + Id: d.Get("id").(string), + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Enabled: d.Get("enabled").(bool), + Tags: utils.ConvertFromInterfaceList[string](d.Get("tags").([]interface{})), + Document: d.Get("document").(string), + Enforced: d.Get("enforced").(bool), + } + var err error + if p.ValidFrom, err = timestampFromResourceData("valid_from", d); err != nil { + return nil, msg.PolicyType_POLICY_TYPE_UNSPECIFIED, nil + } + if p.ValidUntil, err = timestampFromResourceData("valid_until", d); err != nil { + return nil, msg.PolicyType_POLICY_TYPE_UNSPECIFIED, nil + } + if v, ok := d.GetOk("scope"); ok { - r.Policy.Scope = scopeFromInterface(v.([]interface{})) + p.Scope = scopeFromInterface(v.([]interface{})) } - return nil + return p, msg.PolicyType(ptype), nil } // scopeFromInterface converts the map to a Scope struct -func scopeFromInterface(s []interface{}) *Scope { +func scopeFromInterface(s []interface{}) *msg.Scope { if len(s) == 0 || s[0] == nil { return nil } m := s[0].(map[string]interface{}) - scope := Scope{ + scope := msg.Scope{ RepoIds: utils.ConvertFromInterfaceList[string](m["repo_ids"].([]interface{})), } return &scope } + +func createPolicy(ctx context.Context, cl *client.Client, rd *schema.ResourceData) error { + p, ptype, err := policyAndTypeFromSchema(rd) + if err != nil { + return err + } + req := &msg.CreatePolicyRequest{ + Type: ptype, + Policy: p, + } + grpcClient := methods.NewPolicyServiceClient(cl.GRPCClient()) + resp, err := grpcClient.CreatePolicy(ctx, req) + if err != nil { + return err + } + rd.SetId(resp.GetId()) + return nil +} + +func readPolicy(ctx context.Context, cl *client.Client, rd *schema.ResourceData) error { + p, ptype, err := policyAndTypeFromSchema(rd) + if err != nil { + return err + } + req := &msg.ReadPolicyRequest{ + Id: p.GetId(), + Type: ptype, + } + grpcClient := methods.NewPolicyServiceClient(cl.GRPCClient()) + resp, err := grpcClient.ReadPolicy(ctx, req) + if err != nil { + return err + } + return updateSchema(resp.GetPolicy(), ptype, rd) +} + +func updatePolicy(ctx context.Context, cl *client.Client, rd *schema.ResourceData) error { + p, ptype, err := policyAndTypeFromSchema(rd) + if err != nil { + return err + } + req := &msg.UpdatePolicyRequest{ + Id: p.GetId(), + Type: ptype, + Policy: p, + } + grpcClient := methods.NewPolicyServiceClient(cl.GRPCClient()) + _, err = grpcClient.UpdatePolicy(ctx, req) + return err +} + +func deletePolicy(ctx context.Context, cl *client.Client, rd *schema.ResourceData) error { + p, ptype, err := policyAndTypeFromSchema(rd) + if err != nil { + return err + } + req := &msg.DeletePolicyRequest{ + Id: p.GetId(), + Type: ptype, + } + grpcClient := methods.NewPolicyServiceClient(cl.GRPCClient()) + _, err = grpcClient.DeletePolicy(ctx, req) + return err +} diff --git a/cyral/internal/policy/v2/resource.go b/cyral/internal/policy/v2/resource.go index 8c049717..879b99a5 100644 --- a/cyral/internal/policy/v2/resource.go +++ b/cyral/internal/policy/v2/resource.go @@ -2,32 +2,22 @@ package policyv2 import ( "context" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/cyralinc/terraform-provider-cyral/cyral/client" "github.com/cyralinc/terraform-provider-cyral/cyral/core" "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" "github.com/cyralinc/terraform-provider-cyral/cyral/utils" ) -var resourceContextHandler = core.DefaultContextHandler{ - ResourceName: resourceName, - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &PolicyV2{} }, - SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &PolicyV2{} }, - BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/%s", c.ControlPlane, getAPIPath(d.Get("type").(string))) - }, - ReadUpdateDeleteURLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/%s/%s", - c.ControlPlane, - getAPIPath(d.Get("type").(string)), - d.Get("id").(string), - ) - }, +var resourceContextHandler = core.GenericContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + Create: createPolicy, + Read: readPolicy, + Update: updatePolicy, + Delete: deletePolicy, } func PolicyTypes() []string { @@ -37,10 +27,10 @@ func PolicyTypes() []string { func resourceSchema() *schema.Resource { return &schema.Resource{ Description: "This resource allows management of various types of policies in the Cyral platform. Policies can be used to define access controls, data governance rules to ensure compliance and security within your database environment.", - CreateContext: resourceContextHandler.CreateContext(), - ReadContext: resourceContextHandler.ReadContext(), - UpdateContext: resourceContextHandler.UpdateContext(), - DeleteContext: resourceContextHandler.DeleteContext(), + CreateContext: resourceContextHandler.CreateContext, + ReadContext: resourceContextHandler.ReadContext, + UpdateContext: resourceContextHandler.UpdateContext, + DeleteContext: resourceContextHandler.DeleteContext, Importer: &schema.ResourceImporter{ StateContext: importPolicyV2StateContext, }, diff --git a/go.mod b/go.mod index 9f1a5f0b..1d197412 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.22.7 toolchain go1.23.3 require ( + buf.build/gen/go/cyral/policy/grpc/go v1.5.1-00000000000000-06010c37d88b.1 + buf.build/gen/go/cyral/policy/protocolbuffers/go v1.35.2-00000000000000-06010c37d88b.1 github.com/aws/aws-sdk-go v1.55.5 github.com/google/uuid v1.6.0 github.com/hashicorp/terraform-plugin-docs v0.19.4 @@ -14,9 +16,13 @@ require ( golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f golang.org/x/oauth2 v0.24.0 google.golang.org/grpc v1.68.0 + google.golang.org/protobuf v1.35.2 ) require ( + buf.build/gen/go/cyral/utils/protocolbuffers/go v1.35.2-20221122012125-346ce30c6b6d.1 // indirect + buf.build/gen/go/envoyproxy/protoc-gen-validate/protocolbuffers/go v1.35.2-20240617172848-daf171c6cdb5.1 // indirect + buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.35.2-20240617172850-a48fcebcf8f1.1 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect github.com/BurntSushi/toml v1.2.1 // indirect github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect @@ -84,8 +90,8 @@ require ( golang.org/x/text v0.20.0 // indirect golang.org/x/tools v0.27.0 // indirect google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 60e4578b..1de47181 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,13 @@ +buf.build/gen/go/cyral/policy/grpc/go v1.5.1-00000000000000-06010c37d88b.1 h1:mDYI2bzlus/l9fZKX462fn5/GLqljrcxsRZH8nYOv68= +buf.build/gen/go/cyral/policy/grpc/go v1.5.1-00000000000000-06010c37d88b.1/go.mod h1:UyFvfFdzwg21Q8UrUaCpw2bfD5sCBwIpGtw+ee2ezQ8= +buf.build/gen/go/cyral/policy/protocolbuffers/go v1.35.2-00000000000000-06010c37d88b.1 h1:h1fllMqXEMHC0BV0KtfGSXeNquAbguBM7DZezfEmdRg= +buf.build/gen/go/cyral/policy/protocolbuffers/go v1.35.2-00000000000000-06010c37d88b.1/go.mod h1:ru47HSe8iRfxg+qM64V62vPugPohZkUJVcHH4VZI79w= +buf.build/gen/go/cyral/utils/protocolbuffers/go v1.35.2-20221122012125-346ce30c6b6d.1 h1:YQ3HGPEaSQxU9qBBNL7aJobsBDusaa6MyVELvY/1/UQ= +buf.build/gen/go/cyral/utils/protocolbuffers/go v1.35.2-20221122012125-346ce30c6b6d.1/go.mod h1:ipQzpfXSRqyrq1AY8zKzgW8titg+Thb3NGOedOvvny8= +buf.build/gen/go/envoyproxy/protoc-gen-validate/protocolbuffers/go v1.35.2-20240617172848-daf171c6cdb5.1 h1:RtDhLRivrB9Ig3/nyALj2oo5OnGNR0k9IYBakUvAxc0= +buf.build/gen/go/envoyproxy/protoc-gen-validate/protocolbuffers/go v1.35.2-20240617172848-daf171c6cdb5.1/go.mod h1:40OhQ/xCl77rWTxJGOyDLeiNNBA0obo659aSUCqi3VE= +buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.35.2-20240617172850-a48fcebcf8f1.1 h1:dMiy8/ofks7iD5NH6EUuqZuVCNgqNfjHlzSBBYrtIHk= +buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.35.2-20240617172850-a48fcebcf8f1.1/go.mod h1:f2yiWRYwu45A+gmfQpSOl+aUdluGC3qmzgkaSRq3Mgc= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= @@ -267,6 +277,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=