diff --git a/CHANGELOG.md b/CHANGELOG.md index c6309334..b2335e7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ FEATURES: * **New Resource:** `netapp-ontap_volume_efficiency_policies` ([#80](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/80)) * **New Resource:** `netapp-ontap_quota_rules` ([#136](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/136)) * **New Resource:** `netapp-ontap_volumes_files` ([#5](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/5)) +* **New Resource:** `netapp-ontap_security_roles` ([#140](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/140)) * **New Resource:** `netapp-ontap_storage_qtrees` ([#82](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/82)) * **New Resource:** `netapp-ontap_qos_policies` ([#76](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/76)) * **New Resource:** `netapp-security_login_messages` ([#18](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/18)) diff --git a/docs/data-sources/security_role.md b/docs/data-sources/security_role.md index a2977ca0..0251f69d 100644 --- a/docs/data-sources/security_role.md +++ b/docs/data-sources/security_role.md @@ -1,16 +1,22 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "netapp-ontap_security_role Data Source - terraform-provider-netapp-ontap" -subcategory: "Security" +subcategory: "" description: |- - Retrieves a Security role + SecurityRole data source --- # netapp-ontap_security_role (Data Source) -SecurityRole data source +Retrieve a security role + +### Related ONTAP commands +```commandline +* security login role show +``` ## Example Usage + ```terraform data "netapp-ontap_security_role" "security_role" { # required to know which system to interface with @@ -29,7 +35,7 @@ data "netapp-ontap_security_role" "security_role" { - `name` (String) SecurityRole name - `svm_name` (String) IPInterface svm name -### Optional +### Read-Only - `builtin` (Boolean) Indicates if this is a built-in (pre-defined) role which cannot be modified or deleted. - `privileges` (Attributes Set) The list of privileges that this role has been granted. (see [below for nested schema](#nestedatt--privileges)) @@ -38,7 +44,7 @@ data "netapp-ontap_security_role" "security_role" { ### Nested Schema for `privileges` -Optional: +Read-Only: - `access` (String) Access level for the REST endpoint or command/command directory path. If it denotes the access level for a command/command directory path, the only supported enum values are 'none','readonly' and 'all'. - `path` (String) Either of REST URI/endpoint OR command/command directory path. diff --git a/docs/data-sources/security_roles.md b/docs/data-sources/security_roles.md index fbe44984..021175fb 100644 --- a/docs/data-sources/security_roles.md +++ b/docs/data-sources/security_roles.md @@ -1,24 +1,33 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "netapp-ontap_security_roles Data Source - terraform-provider-netapp-ontap" -subcategory: "Security" +subcategory: "" description: |- - Retrieves Security Rules + SecurityRules data source --- # netapp-ontap_security_roles (Data Source) -SecurityRules data source +Retreive one or more security roles by filter + +### Related ONTAP commands +```commandline +* security login role show +``` + ## Example Usage + +```terraform data "netapp-ontap_security_roles" "security_roles" { + # required to know which system to interface with cx_profile_name = "cluster4" filter = { - svm_name = "svm_1" - scope = "svm" + svm_name = "acc_test" + scope = "cluster" } } - +``` ## Schema @@ -54,7 +63,7 @@ Required: - `name` (String) SecurityRule name - `svm_name` (String) IPInterface svm name -Optional: +Read-Only: - `builtin` (Boolean) Indicates if this is a built-in (pre-defined) role which cannot be modified or deleted. - `privileges` (Attributes Set) The list of privileges that this role has been granted. (see [below for nested schema](#nestedatt--security_roles--privileges)) @@ -63,7 +72,7 @@ Optional: ### Nested Schema for `security_roles.privileges` -Optional: +Read-Only: - `access` (String) Access level for the REST endpoint or command/command directory path. If it denotes the access level for a command/command directory path, the only supported enum values are 'none','readonly' and 'all'. - `path` (String) Either of REST URI/endpoint OR command/command directory path. diff --git a/docs/resources/security_roles.md b/docs/resources/security_roles.md new file mode 100644 index 00000000..94837baa --- /dev/null +++ b/docs/resources/security_roles.md @@ -0,0 +1,104 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "netapp-ontap_security_roles Resource - terraform-provider-netapp-ontap" +subcategory: "Security" +description: |- + SecurityRoles resource +--- + +# netapp-ontap_security_roles (Resource) + +Create/Modify/Delete a Security role + +## Supported Platforms +* On-perm ONTAP system 9.6 or higher + +## Example Usage + +```terraform +resource "netapp-ontap_security_roles" "security_role" { + # required to know which system to interface with + cx_profile_name = "cluster3" + name = "testme" + svm_name = "temp" + privileges = [ + { + access = "all" + path = "lun" + }, + { + access = "all" + path = "vserver" + query = "-vserver acc_test" + } + ] +} +``` + + +## Schema + +### Required + +- `cx_profile_name` (String) Connection profile name +- `name` (String) SecurityRole name + +### Optional + +- `privileges` (Attributes Set) The list of privileges that this role has been granted. (see [below for nested schema](#nestedatt--privileges)) +- `svm_name` (String) SecurityRole svm name + +### Read-Only + +- `builtin` (Boolean) Indicates if this is a built-in (pre-defined) role which cannot be modified or deleted. +- `id` (String) The unique identifier of the security role. +- `scope` (String) Scope of the entity. Set to 'cluster' for cluster owned objects and to 'svm' for SVM owned objects. + + +### Nested Schema for `privileges` + +Optional: + +- `access` (String) Access level for the REST endpoint or command/command directory path. If it denotes the access level for a command/command directory path, the only supported enum values are 'none','readonly' and 'all'. +- `path` (String) Either of REST URI/endpoint OR command/command directory path. +- `query` (String) Requires 9.11 system or above. Optional attribute that can be specified only if the 'path' attribute refers to a command/command directory path. The privilege tuple implicitly defines a set of objects the role can or cannot access at the specified access level. The query further reduces this set of objects to a subset of objects that the role is allowed to access. The query attribute must be applicable to the command/command directory specified by the 'path' attribute. It is defined using one or more parameters of the command/command directory path specified by the 'path' attribute. + +## Import +This Resource supports import, which allows you to import existing security role into the state of this resoruce. +Import require a unique ID composed of the role name, svm_name and cx_profile_name, separated by a comma. + id = `name`,`svm_name`,`cx_profile_name` + +### Terraform Import + For example + ```shell + terraform import netapp-ontap_security_roles.example role1,svm1,cluster1 + ``` + +!> The terraform import CLI command can only import resources into the state. Importing via the CLI does not generate configuration. If you want to generate the accompanying configuration for imported resources, use the import block instead. + +### Terraform Import Block +This requires Terraform 1.5 or higher, and will auto create the configuration for you + +First create the block +```terraform +import { + to = netapp-ontap_security_roles.role_import + id = "role1,svm1,cluster1" +} +``` +Next run, this will auto create the configuration for you +```shell +terraform plan -generate-config-out=generated.tf +``` +This will generate a file called generated.tf, which will contain the configuration for the imported resource +```terraform +# __generated__ by Terraform +# Please review these resources and move them into your main configuration files. +# __generated__ by Terraform from "role1,svm1,cluster1" +resource "netapp-ontap_security_roles" "role1_import" { + cx_profile_name = "cluster1" + name = "role1" + svm_name = "svm1" + ... +} +``` \ No newline at end of file diff --git a/examples/resources/netapp-ontap_security_roles/provider.tf b/examples/resources/netapp-ontap_security_roles/provider.tf new file mode 120000 index 00000000..c6b7138f --- /dev/null +++ b/examples/resources/netapp-ontap_security_roles/provider.tf @@ -0,0 +1 @@ +../../provider/provider.tf \ No newline at end of file diff --git a/examples/resources/netapp-ontap_security_roles/resource.tf b/examples/resources/netapp-ontap_security_roles/resource.tf new file mode 100644 index 00000000..b8dcd17f --- /dev/null +++ b/examples/resources/netapp-ontap_security_roles/resource.tf @@ -0,0 +1,17 @@ +resource "netapp-ontap_security_roles" "security_role" { + # required to know which system to interface with + cx_profile_name = "cluster3" + name = "testme" + svm_name = "temp" + privileges = [ + { + access = "all" + path = "lun" + }, + { + access = "all" + path = "vserver" + query = "-vserver acc_test" + } + ] +} diff --git a/examples/resources/netapp-ontap_security_roles/terraform.tfvars b/examples/resources/netapp-ontap_security_roles/terraform.tfvars new file mode 120000 index 00000000..8d9d1c96 --- /dev/null +++ b/examples/resources/netapp-ontap_security_roles/terraform.tfvars @@ -0,0 +1 @@ +../../provider/terraform.tfvars \ No newline at end of file diff --git a/examples/resources/netapp-ontap_security_roles/variables.tf b/examples/resources/netapp-ontap_security_roles/variables.tf new file mode 120000 index 00000000..395ce618 --- /dev/null +++ b/examples/resources/netapp-ontap_security_roles/variables.tf @@ -0,0 +1 @@ +../../provider/variables.tf \ No newline at end of file diff --git a/internal/interfaces/security_role.go b/internal/interfaces/security_role.go index 68c40f2c..88411125 100644 --- a/internal/interfaces/security_role.go +++ b/internal/interfaces/security_role.go @@ -2,6 +2,7 @@ package interfaces import ( "fmt" + "log" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/mitchellh/mapstructure" @@ -14,14 +15,15 @@ type SecurityRoleGetDataModelONTAP struct { Name string `mapstructure:"name"` UUID string `mapstructure:"uuid"` Owner SecurityRoleOwner `mapstructure:"owner"` - Privileges []SecurityRolePrivileges `mapstructure:"privileges"` + Privileges []SecurityRolePrivileges `mapstructure:"privileges,omitempty"` Scope string `mapstructure:"scope"` Builtin bool `mapstructure:"builtin"` } type SecurityRolePrivileges struct { - Access string `mapstructure:"access"` - Path string `mapstructure:"path"` + Access string `mapstructure:"access,omitempty"` + Path string `mapstructure:"path,omitempty"` + Query string `mapstructure:"query,omitempty"` } type SecurityRoleOwner struct { @@ -31,8 +33,21 @@ type SecurityRoleOwner struct { // SecurityRoleResourceBodyDataModelONTAP describes the body data model using go types for mapping. type SecurityRoleResourceBodyDataModelONTAP struct { - Name string `mapstructure:"name"` - SVM svm `mapstructure:"svm"` + Name string `mapstructure:"name"` + Owner svm `mapstructure:"owner"` + Privileges []SecurityRolePrivilegesListBodyDataModelONTAP `mapstructure:"privileges"` +} + +type SecurityRolePrivilegesListBodyDataModelONTAP struct { + Access string `json:"access,omitempty"` + Path string `json:"path,omitempty"` + Query string `json:"query,omitempty"` +} + +type SecurityRolePrivilegesBodyDataModelONTAP struct { + Access string `mapstructure:"access,omitempty"` + Path string `mapstructure:"path,omitempty"` + Query string `mapstructure:"query,omitempty"` } // SecurityRoleDataSourceFilterModel describes the data source data model for queries. @@ -100,11 +115,12 @@ func GetSecurityRoles(errorHandler *utils.ErrorHandler, r restclient.RestClient, // CreateSecurityRole to create security_role func CreateSecurityRole(errorHandler *utils.ErrorHandler, r restclient.RestClient, body SecurityRoleResourceBodyDataModelONTAP) (*SecurityRoleGetDataModelONTAP, error) { - api := "api_url" + api := "security/roles" var bodyMap map[string]interface{} if err := mapstructure.Decode(body, &bodyMap); err != nil { return nil, errorHandler.MakeAndReportError("error encoding security_role body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body)) } + log.Printf("body body!! %#v", bodyMap) query := r.NewQuery() query.Add("return_records", "true") statusCode, response, err := r.CallCreateMethod(api, query, bodyMap) @@ -120,12 +136,85 @@ func CreateSecurityRole(errorHandler *utils.ErrorHandler, r restclient.RestClien return &dataONTAP, nil } +// UpdateSecurityRole to update security_role +// Only privileges can be updated +func UpdateSecurityRole(errorHandler *utils.ErrorHandler, r restclient.RestClient, body SecurityRoleResourceBodyDataModelONTAP, name string, svmUUID string) error { + api := "security/roles/" + svmUUID + "/" + name + var bodyMap map[string]interface{} + if err := mapstructure.Decode(body, &bodyMap); err != nil { + return errorHandler.MakeAndReportError("error encoding security_role body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, body)) + } + statusCode, _, err := r.CallCreateMethod(api, nil, bodyMap) + if err != nil { + return errorHandler.MakeAndReportError("error creating security_role", fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + return nil +} + +func CreateSecurityRolePrivileges(errorHandler *utils.ErrorHandler, r restclient.RestClient, privileges SecurityRolePrivilegesBodyDataModelONTAP, name string, svmUUID string) error { + api := "security/roles/" + svmUUID + "/" + name + "/privileges" + var bodyMap map[string]interface{} + if err := mapstructure.Decode(privileges, &bodyMap); err != nil { + return errorHandler.MakeAndReportError("error encoding security_role privileges body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, privileges)) + } + statusCode, _, err := r.CallCreateMethod(api, nil, bodyMap) + if err != nil { + return errorHandler.MakeAndReportError("error creating security_role privileges", fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + return nil +} + +func UpdateSecurityRolePrivileges(errorHandler *utils.ErrorHandler, r restclient.RestClient, privileges SecurityRolePrivilegesBodyDataModelONTAP, name string, svmUUID string) error { + api := "security/roles/" + svmUUID + "/" + name + "/privileges/" + privileges.Path + var bodyMap map[string]interface{} + if err := mapstructure.Decode(privileges, &bodyMap); err != nil { + return errorHandler.MakeAndReportError("error encoding security_role privileges body", fmt.Sprintf("error on encoding %s body: %s, body: %#v", api, err, privileges)) + } + // path is not supported in the body of a PATCH + delete(bodyMap, "path") + statusCode, _, err := r.CallUpdateMethod(api, nil, bodyMap) + if err != nil { + return errorHandler.MakeAndReportError("error updating security_role privileges", fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + return nil +} + +func DeleteSecurityRolePrivileges(errorHandler *utils.ErrorHandler, r restclient.RestClient, path string, name string, svmUUID string) error { + api := "security/roles/" + svmUUID + "/" + name + "/privileges/" + path + statusCode, _, err := r.CallDeleteMethod(api, nil, nil) + if err != nil { + return errorHandler.MakeAndReportError("error deleting security_role privileges", fmt.Sprintf("error on POST %s: %s, statusCode %d", api, err, statusCode)) + } + + return nil +} + // DeleteSecurityRole to delete security_role -func DeleteSecurityRole(errorHandler *utils.ErrorHandler, r restclient.RestClient, uuid string) error { - api := "api_url" - statusCode, _, err := r.CallDeleteMethod(api+"/"+uuid, nil, nil) +func DeleteSecurityRole(errorHandler *utils.ErrorHandler, r restclient.RestClient, name string, svmUUID string) error { + api := "security/roles/" + svmUUID + "/" + name + statusCode, _, err := r.CallDeleteMethod(api, nil, nil) if err != nil { return errorHandler.MakeAndReportError("error deleting security_role", fmt.Sprintf("error on DELETE %s: %s, statusCode %d", api, err, statusCode)) } return nil } + +// Difference returns the difference between two slices of SecurityRolePrivileges. +// It returns a slice containing all the elements in b that are not present in a. +func Difference(a, b []SecurityRolePrivileges) (diff []SecurityRolePrivileges) { + m := make(map[SecurityRolePrivileges]bool) + + for _, s1Val := range a { + m[s1Val] = true + } + + for _, s2Val := range b { + if _, ok := m[s2Val]; !ok { + diff = append(diff, s2Val) + } + } + return diff +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index fb9ed603..ee52c092 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -236,6 +236,7 @@ func (p *ONTAPProvider) Resources(ctx context.Context) []func() resource.Resourc protocols.NewProtocolsSanIgroupResource, protocols.NewProtocolsSanLunMapsResource, security.NewSecurityAccountResource, + security.NewSecurityRolesResource, security.NewSecurityLoginMessageResource, snapmirror.NewSnapmirrorResource, snapmirror.NewSnapmirrorPolicyResource, diff --git a/internal/provider/security/security_roles_resource.go b/internal/provider/security/security_roles_resource.go new file mode 100644 index 00000000..a3b675dd --- /dev/null +++ b/internal/provider/security/security_roles_resource.go @@ -0,0 +1,561 @@ +package security + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/netapp/terraform-provider-netapp-ontap/internal/provider/connection" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/netapp/terraform-provider-netapp-ontap/internal/interfaces" + "github.com/netapp/terraform-provider-netapp-ontap/internal/utils" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &SecurityRoleResource{} +var _ resource.ResourceWithImportState = &SecurityRoleResource{} + +// NewSecurityRoleResource is a helper function to simplify the provider implementation. +func NewSecurityRolesResource() resource.Resource { + return &SecurityRoleResource{ + config: connection.ResourceOrDataSourceConfig{ + Name: "security_roles", + }, + } +} + +// SecurityRoleResource defines the resource implementation. +type SecurityRoleResource struct { + config connection.ResourceOrDataSourceConfig +} + +// SecurityRoleResourceModel describes the resource data model. +type SecurityRoleResourceModel struct { + CxProfileName types.String `tfsdk:"cx_profile_name"` + Name types.String `tfsdk:"name"` + SVMName types.String `tfsdk:"svm_name"` // if needed or relevant + Privileges types.Set `tfsdk:"privileges"` + Builtin types.Bool `tfsdk:"builtin"` + Scope types.String `tfsdk:"scope"` + ID types.String `tfsdk:"id"` +} + +type SecurityRoleResourcePrivilege struct { + Path types.String `tfsdk:"path"` + Access types.String `tfsdk:"access"` + Query types.String `tfsdk:"query"` +} + +// Metadata returns the resource type name. +func (r *SecurityRoleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + r.config.Name +} + +// Schema defines the schema for the resource. +func (r *SecurityRoleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "SecurityRole resource", + + Attributes: map[string]schema.Attribute{ + "cx_profile_name": schema.StringAttribute{ + MarkdownDescription: "Connection profile name", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "SecurityRole name", + Required: true, + }, + "svm_name": schema.StringAttribute{ + MarkdownDescription: "SecurityRole svm name", + Optional: true, + }, + "privileges": schema.SetNestedAttribute{ + MarkdownDescription: "The list of privileges that this role has been granted.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "path": schema.StringAttribute{ + MarkdownDescription: "Either of REST URI/endpoint OR command/command directory path.", + Optional: true, + }, + "access": schema.StringAttribute{ + MarkdownDescription: "Access level for the REST endpoint or command/command directory path. If it denotes the access level for a command/command directory path, the only supported enum values are 'none','readonly' and 'all'.", + Optional: true, + }, + "query": schema.StringAttribute{ + MarkdownDescription: "Optional attribute that can be specified only if the 'path' attribute refers to a command/command directory path. The privilege tuple implicitly defines a set of objects the role can or cannot access at the specified access level. The query further reduces this set of objects to a subset of objects that the role is allowed to access. The query attribute must be applicable to the command/command directory specified by the 'path' attribute. It is defined using one or more parameters of the command/command directory path specified by the 'path' attribute.", + Optional: true, + }, + }, + }, + }, + "builtin": schema.BoolAttribute{ + MarkdownDescription: "Indicates if this is a built-in (pre-defined) role which cannot be modified or deleted.", + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "scope": schema.StringAttribute{ + MarkdownDescription: "Scope of the entity. Set to 'cluster' for cluster owned objects and to 'svm' for SVM owned objects.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the security role.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *SecurityRoleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + config, ok := req.ProviderData.(connection.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + } + r.config.ProviderConfig = config +} + +// Read refreshes the Terraform state with the latest data. +func (r *SecurityRoleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data SecurityRoleResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + // we need to defer setting the client until we can read the connection profile name + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + restInfos, err := interfaces.GetSecurityRoles(errorHandler, *client, &interfaces.SecurityRoleDataSourceFilterModel{ + Name: data.Name.ValueString(), + SVMName: data.SVMName.ValueString(), + }) + + if err != nil { + // error reporting done inside GetSecurityRole + return + } + + foundRole := false + restInfo := interfaces.SecurityRoleGetDataModelONTAP{} + for _, role := range restInfos { + if role.Name == data.Name.ValueString() { + foundRole = true + restInfo = role + break + } + } + if !foundRole { + resp.Diagnostics.AddError("SecurityRole not found", fmt.Sprintf("SecurityRole %s not found", data.Name.ValueString())) + return + } + + data.Name = types.StringValue(restInfo.Name) + data.Builtin = types.BoolValue(restInfo.Builtin) + data.Scope = types.StringValue(restInfo.Scope) + + // Priviledges + setElements := []attr.Value{} + for _, privilege := range restInfo.Privileges { + nestedElementTypes := map[string]attr.Type{ + "access": types.StringType, + "path": types.StringType, + "query": types.StringType, + } + nestedElements := map[string]attr.Value{ + "access": types.StringValue(privilege.Access), + "path": types.StringValue(privilege.Path), + } + if privilege.Query != "" { + nestedElements["query"] = types.StringValue(privilege.Query) + } else { + nestedElements["query"] = basetypes.NewStringNull() + } + objectValue, diags := types.ObjectValue(nestedElementTypes, nestedElements) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + setElements = append(setElements, objectValue) + } + setValue, diags := types.SetValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "access": types.StringType, + "path": types.StringType, + "query": types.StringType, + }, + }, setElements) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + data.Privileges = setValue + data.ID = types.StringValue(restInfo.Owner.Id + "/" + restInfo.Name) + + // Write logs using the tflog package + // Documentation: https://terraform.io/plugin/log + tflog.Debug(ctx, fmt.Sprintf("read a resource: %#v", data)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Create a resource +func (r *SecurityRoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *SecurityRoleResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + var body interfaces.SecurityRoleResourceBodyDataModelONTAP + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + body.Name = data.Name.ValueString() + body.Owner.Name = data.SVMName.ValueString() + + PrivilegesList := []interfaces.SecurityRolePrivilegesListBodyDataModelONTAP{} + if !data.Privileges.IsNull() { + elements := make([]types.Object, 0, len(data.Privileges.Elements())) + diags := data.Privileges.ElementsAs(ctx, &elements, false) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + for _, element := range elements { + var privilege SecurityRoleResourcePrivilege + diags := element.As(ctx, &privilege, basetypes.ObjectAsOptions{}) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + interfacesPrivilege := interfaces.SecurityRolePrivilegesListBodyDataModelONTAP{} + interfacesPrivilege.Path = privilege.Path.ValueString() + interfacesPrivilege.Access = privilege.Access.ValueString() + interfacesPrivilege.Query = privilege.Query.ValueString() + PrivilegesList = append(PrivilegesList, interfacesPrivilege) + } + body.Privileges = PrivilegesList + } + + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + _, err = interfaces.CreateSecurityRole(errorHandler, *client, body) + if err != nil { + return + } + + tflog.Trace(ctx, "created a resource") + + restInfos, err := interfaces.GetSecurityRoles(errorHandler, *client, &interfaces.SecurityRoleDataSourceFilterModel{ + Name: data.Name.ValueString(), + SVMName: data.SVMName.ValueString(), + }) + + if err != nil { + // error reporting done inside GetSecurityRole + return + } + + foundRole := false + restInfo := interfaces.SecurityRoleGetDataModelONTAP{} + for _, role := range restInfos { + if role.Name == data.Name.ValueString() { + foundRole = true + restInfo = role + break + } + } + if !foundRole { + resp.Diagnostics.AddError("SecurityRole not found", fmt.Sprintf("SecurityRole %s not found", data.Name.ValueString())) + return + } + + data.Name = types.StringValue(restInfo.Name) + data.Builtin = types.BoolValue(restInfo.Builtin) + data.Scope = types.StringValue(restInfo.Scope) + + // Priviledges + setElements := []attr.Value{} + for _, privilege := range restInfo.Privileges { + deleteDefaultPrivileges := false + if privilege.Path == "DEFAULT" && privilege.Access == "none" && privilege.Query == "" { + for _, planedPrivilege := range PrivilegesList { + if planedPrivilege.Path == "DEFAULT" && planedPrivilege.Access == "none" && planedPrivilege.Query == "" { + deleteDefaultPrivileges = false + break + } + deleteDefaultPrivileges = true + } + } + if deleteDefaultPrivileges { + err = interfaces.DeleteSecurityRolePrivileges(errorHandler, *client, privilege.Path, data.Name.ValueString(), restInfo.Owner.Id) + if err != nil { + errorHandler.MakeAndReportError("error deleting default security_role privileges", "error on DELETE API created default privileges: {path: 'DEFAULT', access: 'none', query: ''}") + return + } + continue + } + nestedElementTypes := map[string]attr.Type{ + "access": types.StringType, + "path": types.StringType, + "query": types.StringType, + } + nestedElements := map[string]attr.Value{ + "access": types.StringValue(privilege.Access), + "path": types.StringValue(privilege.Path), + } + if privilege.Query != "" { + nestedElements["query"] = types.StringValue(privilege.Query) + } else { + nestedElements["query"] = basetypes.NewStringNull() + } + objectValue, diags := types.ObjectValue(nestedElementTypes, nestedElements) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + setElements = append(setElements, objectValue) + } + setValue, diags := types.SetValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "access": types.StringType, + "path": types.StringType, + "query": types.StringType, + }, + }, setElements) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + data.Privileges = setValue + data.ID = types.StringValue(restInfo.Owner.Id + "/" + restInfo.Name) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Update updates the resource and sets the updated Terraform state on success. +// Only the privileges can be updated by the API. +func (r *SecurityRoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan *SecurityRoleResourceModel + var config *SecurityRoleResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &config)...) + + if resp.Diagnostics.HasError() { + return + } + + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + client, err := connection.GetRestClient(errorHandler, r.config, plan.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + svm, err := interfaces.GetSvmByName(errorHandler, *client, plan.SVMName.ValueString()) + if err != nil { + return + } + + PlanPrivilegesList := []interfaces.SecurityRolePrivilegesBodyDataModelONTAP{} + ConfigPrivilegesList := []interfaces.SecurityRolePrivilegesBodyDataModelONTAP{} + + if !plan.Privileges.IsNull() { + + elements := make([]types.Object, 0, len(plan.Privileges.Elements())) + diags := plan.Privileges.ElementsAs(ctx, &elements, false) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + for _, element := range elements { + var privilege SecurityRoleResourcePrivilege + diags := element.As(ctx, &privilege, basetypes.ObjectAsOptions{}) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + log.Printf("privilegePlan : %v", privilege) + interfacesPrivilege := interfaces.SecurityRolePrivilegesBodyDataModelONTAP{} + interfacesPrivilege.Path = privilege.Path.ValueString() + interfacesPrivilege.Access = privilege.Access.ValueString() + interfacesPrivilege.Query = privilege.Query.ValueString() + PlanPrivilegesList = append(PlanPrivilegesList, interfacesPrivilege) + } + } + + if !config.Privileges.IsNull() { + elements := make([]types.Object, 0, len(config.Privileges.Elements())) + diags := config.Privileges.ElementsAs(ctx, &elements, false) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + for _, element := range elements { + var privilege SecurityRoleResourcePrivilege + diags := element.As(ctx, &privilege, basetypes.ObjectAsOptions{}) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + log.Printf("privilegeConfig : %v", privilege) + interfacesPrivilege := interfaces.SecurityRolePrivilegesBodyDataModelONTAP{} + interfacesPrivilege.Path = privilege.Path.ValueString() + interfacesPrivilege.Access = privilege.Access.ValueString() + interfacesPrivilege.Query = privilege.Query.ValueString() + ConfigPrivilegesList = append(ConfigPrivilegesList, interfacesPrivilege) + } + } + //Find the difference of paths that are in the plan but not in the config. These are the paths that need to be added. POST on these paths: /security/roles/{owner.uuid}/{name}/privileges + hasDefaultPathInPlan := false + for _, planPrivilege := range PlanPrivilegesList { + foundPathInConfig := false + if planPrivilege.Path == "DEFAULT" && planPrivilege.Access == "none" && planPrivilege.Query == "" { + hasDefaultPathInPlan = true + } + for _, configPrivilege := range ConfigPrivilegesList { + if planPrivilege.Path == configPrivilege.Path { + log.Print("hit true") + foundPathInConfig = true + // if Path is the same, but others are not. Do a PATCH on this path + if planPrivilege.Access != configPrivilege.Access || planPrivilege.Query != configPrivilege.Query { + err = interfaces.UpdateSecurityRolePrivileges(errorHandler, *client, planPrivilege, plan.Name.ValueString(), svm.UUID) + if err != nil { + return + } + } + } + } + if !foundPathInConfig { + //POST on this path + log.Printf("going to create privilege : %v", planPrivilege) + err = interfaces.CreateSecurityRolePrivileges(errorHandler, *client, planPrivilege, plan.Name.ValueString(), svm.UUID) + if err != nil { + errorHandler.MakeAndReportError("error deleting default security_role privileges", fmt.Sprint("error on DELETE API created default privileges: {path: 'DEFAULT', access: 'none', query: ''} :", err)) + return + } + if !hasDefaultPathInPlan { + // DELETE on this path + err = interfaces.DeleteSecurityRolePrivileges(errorHandler, *client, "DEFAULT", plan.Name.ValueString(), svm.UUID) + if err != nil { + return + } + } + + } + } + + //Find the difference of paths that are in the config but not in the plan. These are the paths that need to be deleted. DELETE on these paths: /security/roles/{owner.uuid}/{name}/privileges/{path} + for _, configPrivilege := range ConfigPrivilegesList { + foundPathInPlan := false + for _, planPrivilege := range PlanPrivilegesList { + if planPrivilege.Path == configPrivilege.Path { + foundPathInPlan = true + } + } + if !foundPathInPlan { + //DELETE on this path + err = interfaces.DeleteSecurityRolePrivileges(errorHandler, *client, configPrivilege.Path, plan.Name.ValueString(), svm.UUID) + if err != nil { + return + } + } + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *SecurityRoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *SecurityRoleResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics) + client, err := connection.GetRestClient(errorHandler, r.config, data.CxProfileName) + if err != nil { + // error reporting done inside NewClient + return + } + + svm, err := interfaces.GetSvmByName(errorHandler, *client, data.SVMName.ValueString()) + if err != nil { + return + } + err = interfaces.DeleteSecurityRole(errorHandler, *client, data.Name.ValueString(), svm.UUID) + if err != nil { + return + } +} + +// ImportState imports a resource using ID from terraform import command by calling the Read method. +func (r *SecurityRoleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, fmt.Sprintf("import req an security role resource: %#v", req)) + idParts := strings.Split(req.ID, ",") + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprint("Expected ID in the format 'name,svm_name,cx_profile_name', got: ", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("svm_name"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cx_profile_name"), idParts[2])...) +} diff --git a/internal/provider/security/security_roles_resource_test.go b/internal/provider/security/security_roles_resource_test.go new file mode 100644 index 00000000..4335fa6c --- /dev/null +++ b/internal/provider/security/security_roles_resource_test.go @@ -0,0 +1,162 @@ +package security_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + ntest "github.com/netapp/terraform-provider-netapp-ontap/internal/provider" +) + +func TestAccSecurityRolesResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { ntest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ntest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSecurityRoleResourceConfig("acc_test_security_role"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netapp-ontap_security_roles.security_role", "name", "acc_test_security_role"), + ), + }, + // Test adding a new priviledge to the security role + { + Config: AddPriviledge("acc_test_security_role"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netapp-ontap_security_roles.security_role", "name", "acc_test_security_role"), + ), + }, + // Test editing a priviledge and deleting a priviledge from the security role + { + Config: EditPriviledgeAndDeletePriviledge("acc_test_security_role"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netapp-ontap_security_roles.security_role", "name", "acc_test_security_role"), + ), + }, + // Test importing a security role + { + ResourceName: "netapp-ontap_security_roles.security_role", + ImportState: true, + ImportStateId: fmt.Sprintf("%s,%s,%s", "acc_test_import_security_role", "acc_test", "cluster2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netapp-ontap_security_roles.security_role", "name", "acc_test_import_security_role"), + ), + }, + }, + }) +} + +func testAccSecurityRoleResourceConfig(name string) string { + host := os.Getenv("TF_ACC_NETAPP_HOST2") + admin := os.Getenv("TF_ACC_NETAPP_USER") + password := os.Getenv("TF_ACC_NETAPP_PASS2") + if host == "" || admin == "" || password == "" { + fmt.Println("TF_ACC_NETAPP_HOST, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS must be set for acceptance tests") + os.Exit(1) + } + return fmt.Sprintf(` +provider "netapp-ontap" { + connection_profiles = [ + { + name = "cluster2" + hostname = "%s" + username = "%s" + password = "%s" + validate_certs = false + }, + ] +} + +resource "netapp-ontap_security_roles" "security_role" { + # required to know which system to interface with + cx_profile_name = "cluster2" + name = "%s" + svm_name = "acc_test" + privileges = [ + { + access = "all" + path = "lun" + } + ] + } +`, host, admin, password, name) +} + +func AddPriviledge(name string) string { + host := os.Getenv("TF_ACC_NETAPP_HOST2") + admin := os.Getenv("TF_ACC_NETAPP_USER") + password := os.Getenv("TF_ACC_NETAPP_PASS2") + if host == "" || admin == "" || password == "" { + fmt.Println("TF_ACC_NETAPP_HOST, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS must be set for acceptance tests") + os.Exit(1) + } + return fmt.Sprintf(` +provider "netapp-ontap" { + connection_profiles = [ + { + name = "cluster2" + hostname = "%s" + username = "%s" + password = "%s" + validate_certs = false + }, + ] +} + +resource "netapp-ontap_security_roles" "security_role" { + # required to know which system to interface with + cx_profile_name = "cluster2" + name = "%s" + svm_name = "acc_test" + privileges = [ + { + access = "all" + path = "lun" + }, + { + access = "all" + path = "vserver" + query = "-vserver acc_test" + } + ] + } +`, host, admin, password, name) +} + +func EditPriviledgeAndDeletePriviledge(name string) string { + host := os.Getenv("TF_ACC_NETAPP_HOST2") + admin := os.Getenv("TF_ACC_NETAPP_USER") + password := os.Getenv("TF_ACC_NETAPP_PASS2") + if host == "" || admin == "" || password == "" { + fmt.Println("TF_ACC_NETAPP_HOST, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS must be set for acceptance tests") + os.Exit(1) + } + return fmt.Sprintf(` +provider "netapp-ontap" { + connection_profiles = [ + { + name = "cluster2" + hostname = "%s" + username = "%s" + password = "%s" + validate_certs = false + }, + ] +} + +resource "netapp-ontap_security_roles" "security_role" { + # required to know which system to interface with + cx_profile_name = "cluster2" + name = "%s" + svm_name = "acc_test" + privileges = [ + { + access = "all" + path = "vserver" + query = "-vserver acc_test|temp" + } + ] + } +`, host, admin, password, name) +} diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py index 54e8f3b2..32e36f30 100755 --- a/scripts/generate_docs.py +++ b/scripts/generate_docs.py @@ -79,6 +79,9 @@ "security_account_resource.md", "security_login_message_data_source.md", "security_login_messages_data_source.md", + "security_role_data_source.md", + "security_roles_data_source.md", + "security_roles_resource.md", "security_login_message_resource.md", ], 'snaplock': [],