From f661819975cf914e144f29d5e753245196fcd2ac Mon Sep 17 00:00:00 2001 From: Ori Shavit Date: Thu, 14 Sep 2023 18:44:39 +0300 Subject: [PATCH] UserSets and ResourceSets --- .gitignore | 1 + examples/main.tf | 37 +++ go.mod | 2 +- go.sum | 4 +- internal/provider/conditionsets/client.go | 147 +++++++++++ .../provider/conditionsets/data_source.go | 118 +++++++++ internal/provider/conditionsets/resource.go | 242 ++++++++++++++++++ internal/provider/provider.go | 4 + 8 files changed, 552 insertions(+), 3 deletions(-) create mode 100644 internal/provider/conditionsets/client.go create mode 100644 internal/provider/conditionsets/data_source.go create mode 100644 internal/provider/conditionsets/resource.go diff --git a/.gitignore b/.gitignore index fd3ad8e..f74c10a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ website/vendor # Keep windows files with windows line endings *.winfile eol=crlf +examples/.terraform.lock.hcl diff --git a/examples/main.tf b/examples/main.tf index f0de02a..4306656 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -56,6 +56,43 @@ resource "permitio_role" "admin" { ] } +resource "permitio_user_set" "privileged_users" { + key = "privileged_users" + name = "Privileged Users" + conditions = jsonencode({ + "allOf" : [ + { + "allOf" : [ + { + "subject.email" = { + contains = "@admin.com" + }, + } + ] + } + ] + }) +} + +resource "permitio_resource_set" "secret_docs" { + key = "secret_docs" + name = "Secret Docs" + resource = permitio_resource.document.key + conditions = jsonencode({ + "allOf" : [ + { + "allOf" : [ + { + "resource.title" = { + contains = "Rye" + }, + } + ] + } + ] + }) +} + output "my_resource" { value = permitio_role.admin } \ No newline at end of file diff --git a/go.mod b/go.mod index 3f87683..7e2125b 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.18.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.4.0 - github.com/permitio/permit-golang v0.0.13 + github.com/permitio/permit-golang v0.0.16 ) require ( diff --git a/go.sum b/go.sum index cc9b16e..3fbd38b 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/permitio/permit-golang v0.0.13 h1:fQWKLw9YQgu9MClsRi/6jA0wXbXZxT2o+w2hRF8VTcI= -github.com/permitio/permit-golang v0.0.13/go.mod h1:phP2AVSL3bgDKfhhmhPt/VJAN8UUDJoQtVjUKRfY5Ck= +github.com/permitio/permit-golang v0.0.16 h1:6EqJEmYzrx191H+qfyxe14j6s8G3ngZXjEivmHX62CY= +github.com/permitio/permit-golang v0.0.16/go.mod h1:phP2AVSL3bgDKfhhmhPt/VJAN8UUDJoQtVjUKRfY5Ck= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/provider/conditionsets/client.go b/internal/provider/conditionsets/client.go new file mode 100644 index 0000000..c374e1a --- /dev/null +++ b/internal/provider/conditionsets/client.go @@ -0,0 +1,147 @@ +package conditionsets + +import ( + "context" + "encoding/json" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/permitio/permit-golang/pkg/models" + "github.com/permitio/permit-golang/pkg/permit" +) + +type ConditionSetClient struct { + client *permit.Client +} + +type ConditionSetMethods interface { + Read(ctx context.Context, data ConditionSetModel) (ConditionSetModel, error) + Create(ctx context.Context, conditionSetType models.ConditionSetType, conditionSetPlan *ConditionSetModel) error + Update(ctx context.Context, conditionSetPlan *ConditionSetModel) error + Delete(ctx context.Context, id string) error +} + +func (c *ConditionSetClient) Read(ctx context.Context, data ConditionSetModel) (ConditionSetModel, error) { + var keyOrId string + + if data.Key.IsNull() { + keyOrId = data.Id.ValueString() + } else { + keyOrId = data.Key.ValueString() + } + + conditionSet, err := c.client.Api.ConditionSets.Get(ctx, keyOrId) + + if err != nil { + return ConditionSetModel{}, err + } + + conditionsMarshalled, err := json.Marshal(conditionSet.Conditions) + + if err != nil { + return ConditionSetModel{}, err + } + + var resourceKey string + + if conditionSet.Resource != nil { + resourceKey = conditionSet.Resource.Key + } + + state := ConditionSetModel{ + Id: types.StringValue(conditionSet.Id), + OrganizationId: types.StringValue(conditionSet.OrganizationId), + ProjectId: types.StringValue(conditionSet.ProjectId), + EnvironmentId: types.StringValue(conditionSet.EnvironmentId), + Key: types.StringValue(conditionSet.Key), + Name: types.StringValue(conditionSet.Name), + Description: types.StringPointerValue(conditionSet.Description), + Resource: types.StringValue(resourceKey), + Conditions: types.StringValue(string(conditionsMarshalled)), + } + + return state, nil +} + +func (c *ConditionSetClient) Create(ctx context.Context, conditionSetType models.ConditionSetType, conditionSetPlan *ConditionSetModel) error { + var conditions map[string]any + err := json.Unmarshal([]byte(conditionSetPlan.Conditions.ValueString()), &conditions) + + if err != nil { + return err + } + + conditionSetCreate := models.ConditionSetCreate{ + Key: conditionSetPlan.Key.ValueString(), + Name: conditionSetPlan.Name.ValueString(), + Type: &conditionSetType, + Conditions: conditions, + } + + if !conditionSetPlan.Resource.IsNull() { + var resourceId models.ResourceId + + err = json.Unmarshal([]byte(conditionSetPlan.Resource.String()), &resourceId) + + if err != nil { + return err + } + + conditionSetCreate.ResourceId = &resourceId + } + + + + conditionSetRead, err := c.client.Api.ConditionSets.Create(ctx, conditionSetCreate) + + if err != nil { + return err + } + + conditionSetPlan.Description = types.StringPointerValue(conditionSetRead.Description) + conditionSetPlan.Id = types.StringValue(conditionSetRead.Id) + conditionSetPlan.OrganizationId = types.StringValue(conditionSetRead.OrganizationId) + conditionSetPlan.ProjectId = types.StringValue(conditionSetRead.ProjectId) + conditionSetPlan.EnvironmentId = types.StringValue(conditionSetRead.EnvironmentId) + + return nil +} + +func (c *ConditionSetClient) Update(ctx context.Context, conditionSetPlan *ConditionSetModel) error { + var conditions map[string]any + err := json.Unmarshal([]byte(conditionSetPlan.Conditions.ValueString()), &conditions) + + if err != nil { + return err + } + + csUpdate := models.ConditionSetUpdate{ + Name: conditionSetPlan.Name.ValueStringPointer(), + Description: conditionSetPlan.Description.ValueStringPointer(), + Conditions: conditions, + } + + conditionSetRead, err := c.client.Api.ConditionSets.Update(ctx, conditionSetPlan.Key.ValueString(), csUpdate) + + if err != nil { + return err + } + + conditionsMarshalled, err := json.Marshal(conditionSetRead.Conditions) + + if err != nil { + return err + } + + conditionSetPlan.Name = types.StringValue(conditionSetRead.Name) + conditionSetPlan.Description = types.StringPointerValue(conditionSetRead.Description) + conditionSetPlan.EnvironmentId = types.StringValue(conditionSetRead.EnvironmentId) + conditionSetPlan.ProjectId = types.StringValue(conditionSetRead.ProjectId) + conditionSetPlan.Id = types.StringValue(conditionSetRead.Id) + conditionSetPlan.OrganizationId = types.StringValue(conditionSetRead.OrganizationId) + conditionSetPlan.Conditions = types.StringValue(string(conditionsMarshalled)) + + return nil +} + +func (c *ConditionSetClient) Delete(ctx context.Context, key string) error { + return c.client.Api.ConditionSets.Delete(ctx, key) +} diff --git a/internal/provider/conditionsets/data_source.go b/internal/provider/conditionsets/data_source.go new file mode 100644 index 0000000..ae9c646 --- /dev/null +++ b/internal/provider/conditionsets/data_source.go @@ -0,0 +1,118 @@ +package conditionsets + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/permitio/permit-golang/pkg/permit" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +var ( + _ datasource.DataSource = &ResourceDataSource{} + _ datasource.DataSourceWithConfigure = &ResourceDataSource{} +) + +func NewResourceDataSource() datasource.DataSource { + return &ResourceDataSource{} +} + +type ResourceDataSource struct { + client ConditionSetClient +} + +type ConditionSetModel struct { + Id types.String `tfsdk:"id"` + OrganizationId types.String `tfsdk:"organization_id"` + ProjectId types.String `tfsdk:"project_id"` + EnvironmentId types.String `tfsdk:"environment_id"` + Key types.String `tfsdk:"key"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Conditions types.String `tfsdk:"conditions"` + Resource types.String `tfsdk:"resource"` +} + +func (d *ResourceDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { + if request.ProviderData == nil { + return + } + + client, ok := request.ProviderData.(*permit.Client) + + if !ok { + response.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *permit.Client, got: %T. Please report this issue to the provider developers.", request.ProviderData), + ) + return + } + + d.client = ConditionSetClient{client: client} +} + +func (d *ResourceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_condition_set" +} + +// Schema defines the schema for the data source. +func (d *ResourceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "organization_id": schema.StringAttribute{ + Computed: true, + }, + "project_id": schema.StringAttribute{ + Computed: true, + }, + "environment_id": schema.StringAttribute{ + Computed: true, + }, + "key": schema.StringAttribute{ + Required: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "type": schema.StringAttribute{ + Required: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *ResourceDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var data ConditionSetModel + + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + state, err := d.client.Read(ctx, data) + + if err != nil { + response.Diagnostics.AddError( + "Unable to Read Resource", + fmt.Sprintf("Unable to read resource: %s, Error: %s", data.Id.String(), err.Error()), + ) + return + } + + // Set state + diags := response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/conditionsets/resource.go b/internal/provider/conditionsets/resource.go new file mode 100644 index 0000000..3db666e --- /dev/null +++ b/internal/provider/conditionsets/resource.go @@ -0,0 +1,242 @@ +package conditionsets + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource" + "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/permitio/permit-golang/pkg/models" + "github.com/permitio/permit-golang/pkg/permit" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &conditionSetResource{} + _ resource.ResourceWithConfigure = &conditionSetResource{} +) + +func NewResourceSetResource() resource.Resource { + return &ResourceSetResource{conditionSetResource{conditionSetType: models.RESOURCESET}} +} + +func NewUserSetResource() resource.Resource { + return &UserSetResource{conditionSetResource{conditionSetType: models.USERSET}} +} + +type conditionSetResource struct { + client ConditionSetClient + conditionSetType models.ConditionSetType +} + +type UserSetResource struct { + conditionSetResource +} + +type ResourceSetResource struct { + conditionSetResource +} + +func (c *UserSetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_user_set" +} + +func (c *ResourceSetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_resource_set" +} + +func (c *conditionSetResource) Configure(_ context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { + if request.ProviderData == nil { + return + } + + permitClient, ok := request.ProviderData.(*permit.Client) + + if !ok { + response.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *permit.Client, got: %T. Please report this issue to the provider developers.", request.ProviderData), + ) + return + } + + c.client = ConditionSetClient{client: permitClient} +} + +func (c *conditionSetResource) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) { + // should be completely implemented in ResourceSet/UserSet + panic("not implemented") +} + +func (c *ResourceSetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := c.baseAttributes() + attributes["resource"] = schema.StringAttribute{ + Required: true, + } + + resp.Schema = schema.Schema{ + Attributes: attributes, + } +} + +func (c *UserSetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := c.baseAttributes() + + resp.Schema = schema.Schema{ + Attributes: attributes, + } +} + +func (c *conditionSetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + panic("not implemented") +} + +func (c *conditionSetResource) baseAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "organization_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "environment_id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "key": schema.StringAttribute{ + Required: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "conditions": schema.StringAttribute{ + Required: true, + }, + "resource": schema.StringAttribute{ + Optional: true, + }, + } +} + +func (c *conditionSetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var ( + plan ConditionSetModel + ) + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + if err := c.client.Create(ctx, c.conditionSetType, &plan); err != nil { + resp.Diagnostics.AddError( + "Unable to create resource", + fmt.Sprintf("Unable to create resource: %s", err), + ) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (c *conditionSetResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data ConditionSetModel + + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + state, err := c.client.Read(ctx, data) + + if err != nil { + response.Diagnostics.AddError( + "Unable to Read Resource", + fmt.Sprintf("Unable to read resource: %s, Error: %s", data.Id.String(), err.Error()), + ) + return + } + + // Set state + diags := response.State.Set(ctx, &state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (c *conditionSetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var ( + plan ConditionSetModel + ) + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if err := c.client.Update(ctx, &plan); err != nil { + resp.Diagnostics.AddError( + "Unable to update resource", + fmt.Sprintf("Unable to update resource: %s", err), + ) + return + } + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (c *conditionSetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Retrieve values from state + var state ConditionSetModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + err := c.client.Delete(ctx, state.Key.ValueString()) + + if err != nil { + resp.Diagnostics.AddError( + "Error Deleting Condition Set", + "Could not delete resource, unexpected error: "+err.Error(), + ) + return + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 42ee53c..d93912c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" permitConfig "github.com/permitio/permit-golang/pkg/config" "github.com/permitio/permit-golang/pkg/permit" + "github.com/permitio/terraform-provider-permit-io/internal/provider/conditionsets" "github.com/permitio/terraform-provider-permit-io/internal/provider/resources" "github.com/permitio/terraform-provider-permit-io/internal/provider/roles" "os" @@ -141,6 +142,8 @@ func (p *PermitProvider) Resources(ctx context.Context) []func() resource.Resour return []func() resource.Resource{ resources.NewResourceResource, roles.NewRoleResource, + conditionsets.NewUserSetResource, + conditionsets.NewResourceSetResource, } } @@ -148,6 +151,7 @@ func (p *PermitProvider) DataSources(ctx context.Context) []func() datasource.Da return []func() datasource.DataSource{ resources.NewResourceDataSource, roles.NewRoleDataSource, + //conditionsets.NewResourceDataSource, } }