Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENG-12949: Policy engine providers #547

Merged
merged 17 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
22 changes: 22 additions & 0 deletions cyral/internal/policyv2/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package policyv2

const (
resourceName = "cyral_policy_v2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like cyral_custom_policy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep as cyral_policy_v2 we will replace cyral_policy by this new resource in the next major version of the provider. Please also mark the cyral_policy as deprecated as we should encourage the move.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just rename the policy package with deprecated? Looks like that is what we've done before? Moving the files to deprecated folder won't work with this new model (as it will eventually lead to filename collisions)?

dataSourceName = resourceName
apiPathLocal = "v2/policies/local"
apiPathGlobal = "v2/policies/global"
apiPathApproval = "v2/policies/approval"
)

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 ""
}
}
111 changes: 111 additions & 0 deletions cyral/internal/policyv2/datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package policyv2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a package policyv2, it seems to me that creating a v2 package inside policy is better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


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

func dataSourceSchema() *schema.Resource {
return &schema.Resource{
Description: "This data source provides information about a policy.",
ReadContext: dsContextHandler.ReadContext(),
Schema: map[string]*schema.Schema{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This schema is identical to the resource. You may simplify this code by calling utils.ConvertSchemaFieldsToComputed as shown here. The resource for token settings may help you simplify your code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not, is it. I mean for the resources, you have to have some four fields, the rest are optional. For datasource you have to have two fields (type, id) - but not name & document or any other field.
Simplification/re-use can still be done but I kept as-is.

"id": {
Description: "Identifier for the policy, unique within the policy type.",
Type: schema.TypeString,
Required: true,
},
"type": {
Description: "Type of the policy, one of [`local`, `global`, `approval`]",
Type: schema.TypeString,
Required: true,
},
"name": {
Description: "Name of the policy.",
Type: schema.TypeString,
Computed: true,
},
"description": {
Description: "Description of the policy.",
Type: schema.TypeString,
Computed: true,
},
"enabled": {
Description: "Indicates if the policy is enabled.",
Type: schema.TypeBool,
Computed: true,
},
"tags": {
Description: "Tags associated with the policy for categorization.",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"scope": {
Description: "Scope of the policy. If empty or omitted, all repositories are in scope.",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"repo_ids": {
Description: "List of repository IDs that are in scope.",
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Computed: true,
},
},
},
},
"valid_from": {
Description: "Time when the policy comes into effect. If omitted, the policy is in effect immediately.",
Type: schema.TypeString,
Computed: true,
},
"valid_until": {
Description: "Time after which the policy is no longer in effect. If omitted, the policy is in effect indefinitely.",
Type: schema.TypeString,
Computed: true,
},
"document": {
Description: "The actual policy document in JSON format. It must conform to the schema for the policy type.",
Type: schema.TypeString,
Computed: true,
},
"last_updated": {
Description: "Information about when and by whom the policy was last updated.",
Type: schema.TypeMap,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"created": {
Description: "Information about when and by whom the policy was created.",
Type: schema.TypeMap,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"enforced": {
Description: "Indicates if the policy is enforced. If not enforced, no action is taken based on the policy, but alerts are triggered for violations.",
Type: schema.TypeBool,
Computed: true,
},
},
}
}
158 changes: 158 additions & 0 deletions cyral/internal/policyv2/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package policyv2

import (
"fmt"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// ChangeInfo represents information about changes to the policy
type ChangeInfo struct {
Actor string `json:"actor,omitempty"`
ActorType string `json:"actorType,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
}

// PolicyV2 represents the top-level policy structure
type PolicyV2 struct {
Policy Policy `json:"policy,omitempty"`
}

type Scope struct {
RepoIds []string `json:"repoIds,omitempty"`
}

// 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 {
return fmt.Errorf("error setting 'id' field: %w", err)
}
if err := d.Set("name", r.Policy.Name); err != nil {
return fmt.Errorf("error setting 'name' field: %w", err)
}
if err := d.Set("description", r.Policy.Description); err != nil {
return fmt.Errorf("error setting 'description' field: %w", err)
}
if err := d.Set("enabled", r.Policy.Enabled); err != nil {
return fmt.Errorf("error setting 'enabled' field: %w", err)
}
if err := d.Set("tags", r.Policy.Tags); err != nil {
return fmt.Errorf("error setting 'tags' field: %w", err)
}
if err := d.Set("valid_from", r.Policy.ValidFrom); err != nil {
return fmt.Errorf("error setting 'valid_from' field: %w", err)
}
if err := d.Set("valid_until", r.Policy.ValidUntil); err != nil {
return fmt.Errorf("error setting 'valid_until' field: %w", err)
}

if err := d.Set("document", r.Policy.Document); err != nil {
return fmt.Errorf("error setting 'document' field: %w", err)
}

if err := d.Set("last_updated", map[string]interface{}{
"actor": r.Policy.LastUpdated.Actor,
"actor_type": r.Policy.LastUpdated.ActorType,
"timestamp": r.Policy.LastUpdated.Timestamp,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Teach ChangeInfo to perform this operation, then you will call r.Policy.LastUpdated.ToMap() and r.Policy.Created.ToMap() on line 77.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}); err != nil {
return fmt.Errorf("error setting 'last_updated' field: %w", err)
}
if err := d.Set("created", map[string]interface{}{
"actor": r.Policy.Created.Actor,
"actor_type": r.Policy.Created.ActorType,
"timestamp": r.Policy.Created.Timestamp,
}); err != nil {
return fmt.Errorf("error setting 'created' field: %w", err)
}
if err := d.Set("enforced", r.Policy.Enforced); 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 {
return fmt.Errorf("error setting 'type' field: %w", err)
}
}
if err := d.Set("scope", flattenScope(r.Policy.Scope)); err != nil {
return fmt.Errorf("error setting 'scope' field: %w", err)
}
d.SetId(r.Policy.ID)
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 = expandStringList(d.Get("tags").([]interface{}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
r.Policy.Tags = expandStringList(d.Get("tags").([]interface{}))
r.Policy.Tags = utils.ConvertFromInterfaceList(d.Get("tags").([]interface{}))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

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)
if v, ok := d.GetOk("scope"); ok {
r.Policy.Scope = expandScope(v.([]interface{}))
}
return nil
}

// expandStringList converts a list of interface{} to a list of strings
func expandStringList(list []interface{}) []string {
result := make([]string, len(list))
for i, v := range list {
result[i] = v.(string)
}
return result
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use utils.ConvertFromInterfaceList instead.

Suggested change
// expandStringList converts a list of interface{} to a list of strings
func expandStringList(list []interface{}) []string {
result := make([]string, len(list))
for i, v := range list {
result[i] = v.(string)
}
return result
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


// getStringOrDefault returns the string value or an empty string if nil or not a string
func getStringOrDefault(v interface{}) string {
str, ok := v.(string)
if !ok {
return ""
}
return str
}

// flattenScope converts the Scope struct to a list of maps
func flattenScope(scope *Scope) []map[string]interface{} {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope should be taught how to perform this operation itself so you would call it like scope.ToMap() or some other more go-lang appropriate name. :-)

Suggested change
func flattenScope(scope *Scope) []map[string]interface{} {
func (s *Scope) ToMap() []map[string]interface{} {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

if scope == nil {
return nil
}
scopeMap := []map[string]interface{}{
{
"repo_ids": scope.RepoIds,
},
}
return scopeMap
}

// expandScope converts the map to a Scope struct
func expandScope(s []interface{}) *Scope {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New name seems better IMO, but feel free to not use it.

Suggested change
func expandScope(s []interface{}) *Scope {
func scopeFromInterface(s []interface{}) *Scope {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

if len(s) == 0 || s[0] == nil {
return nil
}
m := s[0].(map[string]interface{})
scope := Scope{
RepoIds: expandStringList(m["repo_ids"].([]interface{})),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RepoIds: expandStringList(m["repo_ids"].([]interface{})),
RepoIds: utils.ConvertFromInterfaceList(m["repo_ids"].([]interface{})),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}
return &scope
}
Loading
Loading