From f3d766cbfae1d2acace624578d0e19841c75c39f Mon Sep 17 00:00:00 2001 From: Daniel Loader Date: Mon, 9 Mar 2026 23:19:39 +0000 Subject: [PATCH] feat: add permission resource, data source, and organization role permission resource Add three new Terraform types for managing WorkOS permissions: - workos_permission: CRUD resource for environment-level permissions - workos_permission (data source): lookup existing permissions by slug - workos_organization_role_permission: join resource assigning a permission to an org role Co-Authored-By: Claude Opus 4.6 --- README.md | 42 ++- docs/data-sources/permission.md | 52 +++ .../resources/organization_role_permission.md | 69 ++++ docs/resources/permission.md | 76 ++++ .../workos_permission/data-source.tf | 3 + .../resource.tf | 5 + .../resources/workos_permission/resource.tf | 5 + internal/client/models.go | 38 ++ .../client/organization_role_permissions.go | 31 ++ internal/client/permissions.go | 48 +++ internal/provider/data_source_permission.go | 165 +++++++++ .../provider/data_source_permission_test.go | 54 +++ internal/provider/provider.go | 3 + .../resource_organization_role_permission.go | 298 +++++++++++++++ ...ource_organization_role_permission_test.go | 78 ++++ internal/provider/resource_permission.go | 349 ++++++++++++++++++ internal/provider/resource_permission_test.go | 90 +++++ 17 files changed, 1405 insertions(+), 1 deletion(-) create mode 100644 docs/data-sources/permission.md create mode 100644 docs/resources/organization_role_permission.md create mode 100644 docs/resources/permission.md create mode 100644 examples/data-sources/workos_permission/data-source.tf create mode 100644 examples/resources/workos_organization_role_permission/resource.tf create mode 100644 examples/resources/workos_permission/resource.tf create mode 100644 internal/client/organization_role_permissions.go create mode 100644 internal/client/permissions.go create mode 100644 internal/provider/data_source_permission.go create mode 100644 internal/provider/data_source_permission_test.go create mode 100644 internal/provider/resource_organization_role_permission.go create mode 100644 internal/provider/resource_organization_role_permission_test.go create mode 100644 internal/provider/resource_permission.go create mode 100644 internal/provider/resource_permission_test.go diff --git a/README.md b/README.md index 855b2e7..e2abf0d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WorkOS Terraform Provider -Terraform provider for managing [WorkOS](https://workos.com) resources including organizations, users, organization memberships, and roles. +Terraform provider for managing [WorkOS](https://workos.com) resources including organizations, users, organization memberships, roles, and permissions. ## Requirements @@ -95,6 +95,38 @@ resource "workos_organization_role" "viewer" { } ``` +### Managing Permissions + +```hcl +resource "workos_permission" "billing_read" { + slug = "billing:read" + name = "Read Billing" + description = "Allows reading billing data" +} + +resource "workos_permission" "billing_write" { + slug = "billing:write" + name = "Write Billing" + description = "Allows modifying billing data" +} +``` + +### Assigning Permissions to Organization Roles + +```hcl +resource "workos_organization_role_permission" "billing_admin_read" { + organization_id = workos_organization.example.id + role_slug = workos_organization_role.billing_admin.slug + permission = workos_permission.billing_read.slug +} + +resource "workos_organization_role_permission" "billing_admin_write" { + organization_id = workos_organization.example.id + role_slug = workos_organization_role.billing_admin.slug + permission = workos_permission.billing_write.slug +} +``` + ### Data Sources ```hcl @@ -118,6 +150,11 @@ data "workos_organization_role" "billing" { organization_id = workos_organization.example.id slug = "org-billing-admin" } + +# Look up permission by slug +data "workos_permission" "billing_read" { + slug = "billing:read" +} ``` ## Resources @@ -128,6 +165,8 @@ data "workos_organization_role" "billing" { | `workos_user` | Manages AuthKit users | | `workos_organization_membership` | Manages user-organization memberships | | `workos_organization_role` | Manages organization authorization roles | +| `workos_permission` | Manages environment-level permissions | +| `workos_organization_role_permission` | Assigns a permission to an organization role | ## Data Sources @@ -140,6 +179,7 @@ data "workos_organization_role" "billing" { | `workos_directory_group` | Retrieves directory-synced group | | `workos_user` | Retrieves AuthKit user by ID or email | | `workos_organization_role` | Retrieves organization role by slug or ID | +| `workos_permission` | Retrieves permission by slug | ## Development diff --git a/docs/data-sources/permission.md b/docs/data-sources/permission.md new file mode 100644 index 0000000..8371df9 --- /dev/null +++ b/docs/data-sources/permission.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "workos_permission Data Source - workos" +subcategory: "" +description: |- + Use this data source to get information about a WorkOS Permission. + You can look up a permission by its slug. + Example Usage + + data "workos_permission" "billing_read" { + slug = "billing:read" + } +--- + +# workos_permission (Data Source) + +Use this data source to get information about a WorkOS Permission. + +You can look up a permission by its slug. + +## Example Usage + +```hcl +data "workos_permission" "billing_read" { + slug = "billing:read" +} +``` + +## Example Usage + +```terraform +data "workos_permission" "billing_read" { + slug = "billing:read" +} +``` + + +## Schema + +### Required + +- `slug` (String) The slug identifier of the permission to look up. + +### Read-Only + +- `created_at` (String) The timestamp when the permission was created (RFC3339 format). +- `description` (String) A description of the permission. +- `id` (String) The unique identifier of the permission. +- `name` (String) The display name of the permission. +- `resource_type_slug` (String) The slug of the resource type this permission applies to. +- `system` (Boolean) Whether this is a system-managed permission. +- `updated_at` (String) The timestamp when the permission was last updated (RFC3339 format). diff --git a/docs/resources/organization_role_permission.md b/docs/resources/organization_role_permission.md new file mode 100644 index 0000000..09bf018 --- /dev/null +++ b/docs/resources/organization_role_permission.md @@ -0,0 +1,69 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "workos_organization_role_permission Resource - workos" +subcategory: "" +description: |- + Assigns a permission to a WorkOS Organization Role. + This resource manages the assignment of a permission to an organization role. All fields + are immutable — changing any field will destroy and recreate the assignment. + Example Usage + + resource "workos_organization_role_permission" "assign" { + organization_id = workos_organization.acme.id + role_slug = workos_organization_role.billing.slug + permission = workos_permission.billing_read.slug + } + + Import + Organization role permissions can be imported using a composite key of organization ID, role slug, and permission slug: + + terraform import workos_organization_role_permission.example org_01HXYZ.../billing-admin/billing:read +--- + +# workos_organization_role_permission (Resource) + +Assigns a permission to a WorkOS Organization Role. + +This resource manages the assignment of a permission to an organization role. All fields +are immutable — changing any field will destroy and recreate the assignment. + +## Example Usage + +```hcl +resource "workos_organization_role_permission" "assign" { + organization_id = workos_organization.acme.id + role_slug = workos_organization_role.billing.slug + permission = workos_permission.billing_read.slug +} +``` + +## Import + +Organization role permissions can be imported using a composite key of organization ID, role slug, and permission slug: + +```shell +terraform import workos_organization_role_permission.example org_01HXYZ.../billing-admin/billing:read +``` + +## Example Usage + +```terraform +resource "workos_organization_role_permission" "billing_admin_read" { + organization_id = workos_organization.example.id + role_slug = workos_organization_role.billing_admin.slug + permission = workos_permission.billing_read.slug +} +``` + + +## Schema + +### Required + +- `organization_id` (String) The ID of the organization the role belongs to (e.g., `org_01HXYZ...`). +- `permission` (String) The slug of the permission to assign to the role. +- `role_slug` (String) The slug of the organization role to assign the permission to. + +### Read-Only + +- `id` (String) The composite identifier of the organization role permission (`organization_id/role_slug/permission`). diff --git a/docs/resources/permission.md b/docs/resources/permission.md new file mode 100644 index 0000000..553c9ac --- /dev/null +++ b/docs/resources/permission.md @@ -0,0 +1,76 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "workos_permission Resource - workos" +subcategory: "" +description: |- + Manages a WorkOS Permission. + Permissions are environment-level resources that can be assigned to both environment roles + and organization roles. Permissions are identified by their slug. + Example Usage + + resource "workos_permission" "billing_read" { + slug = "billing:read" + name = "Read Billing" + description = "Allows reading billing data" + } + + Import + Permissions can be imported using their slug: + + terraform import workos_permission.example billing:read +--- + +# workos_permission (Resource) + +Manages a WorkOS Permission. + +Permissions are environment-level resources that can be assigned to both environment roles +and organization roles. Permissions are identified by their slug. + +## Example Usage + +```hcl +resource "workos_permission" "billing_read" { + slug = "billing:read" + name = "Read Billing" + description = "Allows reading billing data" +} +``` + +## Import + +Permissions can be imported using their slug: + +```shell +terraform import workos_permission.example billing:read +``` + +## Example Usage + +```terraform +resource "workos_permission" "billing_read" { + slug = "billing:read" + name = "Read Billing" + description = "Allows reading billing data" +} +``` + + +## Schema + +### Required + +- `name` (String) The display name of the permission. +- `slug` (String) The slug identifier for the permission. Must be unique within the environment. + +### Optional + +- `description` (String) A description of the permission. +- `resource_type_slug` (String) The slug of the resource type this permission applies to. + +### Read-Only + +- `created_at` (String) The timestamp when the permission was created (RFC3339 format). +- `id` (String) The unique identifier of the permission. +- `system` (Boolean) Whether this is a system-managed permission. +- `updated_at` (String) The timestamp when the permission was last updated (RFC3339 format). diff --git a/examples/data-sources/workos_permission/data-source.tf b/examples/data-sources/workos_permission/data-source.tf new file mode 100644 index 0000000..e137b38 --- /dev/null +++ b/examples/data-sources/workos_permission/data-source.tf @@ -0,0 +1,3 @@ +data "workos_permission" "billing_read" { + slug = "billing:read" +} diff --git a/examples/resources/workos_organization_role_permission/resource.tf b/examples/resources/workos_organization_role_permission/resource.tf new file mode 100644 index 0000000..ad95e3e --- /dev/null +++ b/examples/resources/workos_organization_role_permission/resource.tf @@ -0,0 +1,5 @@ +resource "workos_organization_role_permission" "billing_admin_read" { + organization_id = workos_organization.example.id + role_slug = workos_organization_role.billing_admin.slug + permission = workos_permission.billing_read.slug +} diff --git a/examples/resources/workos_permission/resource.tf b/examples/resources/workos_permission/resource.tf new file mode 100644 index 0000000..f9484ee --- /dev/null +++ b/examples/resources/workos_permission/resource.tf @@ -0,0 +1,5 @@ +resource "workos_permission" "billing_read" { + slug = "billing:read" + name = "Read Billing" + description = "Allows reading billing data" +} diff --git a/internal/client/models.go b/internal/client/models.go index c97395b..e8d06d7 100644 --- a/internal/client/models.go +++ b/internal/client/models.go @@ -212,3 +212,41 @@ type OrganizationRoleListResponse struct { Data []OrganizationRole `json:"data"` ListMetadata ListMetadata `json:"list_metadata"` } + +// Permission represents a WorkOS Permission +type Permission struct { + ID string `json:"id"` + Object string `json:"object"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + System bool `json:"system"` + ResourceTypeSlug string `json:"resource_type_slug,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// PermissionCreateRequest represents the request to create a permission +type PermissionCreateRequest struct { + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + ResourceTypeSlug string `json:"resource_type_slug,omitempty"` +} + +// PermissionUpdateRequest represents the request to update a permission +type PermissionUpdateRequest struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` +} + +// PermissionListResponse represents the response from listing permissions +type PermissionListResponse struct { + Data []Permission `json:"data"` + ListMetadata ListMetadata `json:"list_metadata"` +} + +// AddPermissionRequest represents the request to add a permission to a role +type AddPermissionRequest struct { + Slug string `json:"slug"` +} diff --git a/internal/client/organization_role_permissions.go b/internal/client/organization_role_permissions.go new file mode 100644 index 0000000..930e9c4 --- /dev/null +++ b/internal/client/organization_role_permissions.go @@ -0,0 +1,31 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package client + +import ( + "context" + "fmt" +) + +// AddOrganizationRolePermission adds a permission to an organization role +func (c *Client) AddOrganizationRolePermission(ctx context.Context, orgID, roleSlug, permSlug string) (*OrganizationRole, error) { + req := &AddPermissionRequest{ + Slug: permSlug, + } + var role OrganizationRole + err := c.Post(ctx, fmt.Sprintf("/authorization/organizations/%s/roles/%s/permissions", orgID, roleSlug), req, &role) + if err != nil { + return nil, fmt.Errorf("failed to add permission to organization role: %w", err) + } + return &role, nil +} + +// RemoveOrganizationRolePermission removes a permission from an organization role +func (c *Client) RemoveOrganizationRolePermission(ctx context.Context, orgID, roleSlug, permSlug string) error { + err := c.Delete(ctx, fmt.Sprintf("/authorization/organizations/%s/roles/%s/permissions/%s", orgID, roleSlug, permSlug)) + if err != nil { + return fmt.Errorf("failed to remove permission from organization role: %w", err) + } + return nil +} diff --git a/internal/client/permissions.go b/internal/client/permissions.go new file mode 100644 index 0000000..d5cfb4e --- /dev/null +++ b/internal/client/permissions.go @@ -0,0 +1,48 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package client + +import ( + "context" + "fmt" +) + +// CreatePermission creates a new permission +func (c *Client) CreatePermission(ctx context.Context, req *PermissionCreateRequest) (*Permission, error) { + var perm Permission + err := c.Post(ctx, "/authorization/permissions", req, &perm) + if err != nil { + return nil, fmt.Errorf("failed to create permission: %w", err) + } + return &perm, nil +} + +// GetPermission retrieves a permission by slug +func (c *Client) GetPermission(ctx context.Context, slug string) (*Permission, error) { + var perm Permission + err := c.Get(ctx, fmt.Sprintf("/authorization/permissions/%s", slug), &perm) + if err != nil { + return nil, fmt.Errorf("failed to get permission: %w", err) + } + return &perm, nil +} + +// UpdatePermission updates an existing permission +func (c *Client) UpdatePermission(ctx context.Context, slug string, req *PermissionUpdateRequest) (*Permission, error) { + var perm Permission + err := c.Patch(ctx, fmt.Sprintf("/authorization/permissions/%s", slug), req, &perm) + if err != nil { + return nil, fmt.Errorf("failed to update permission: %w", err) + } + return &perm, nil +} + +// DeletePermission deletes a permission by slug +func (c *Client) DeletePermission(ctx context.Context, slug string) error { + err := c.Delete(ctx, fmt.Sprintf("/authorization/permissions/%s", slug)) + if err != nil { + return fmt.Errorf("failed to delete permission: %w", err) + } + return nil +} diff --git a/internal/provider/data_source_permission.go b/internal/provider/data_source_permission.go new file mode 100644 index 0000000..980bab1 --- /dev/null +++ b/internal/provider/data_source_permission.go @@ -0,0 +1,165 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/osodevops/terraform-provider-workos/internal/client" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &PermissionDataSource{} + +func NewPermissionDataSource() datasource.DataSource { + return &PermissionDataSource{} +} + +// PermissionDataSource defines the data source implementation. +type PermissionDataSource struct { + client *client.Client +} + +// PermissionDataSourceModel describes the data source data model. +type PermissionDataSourceModel struct { + ID types.String `tfsdk:"id"` + Slug types.String `tfsdk:"slug"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + System types.Bool `tfsdk:"system"` + ResourceTypeSlug types.String `tfsdk:"resource_type_slug"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + +func (d *PermissionDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_permission" +} + +func (d *PermissionDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Use this data source to get information about a WorkOS Permission.", + MarkdownDescription: ` +Use this data source to get information about a WorkOS Permission. + +You can look up a permission by its slug. + +## Example Usage + +` + "```hcl" + ` +data "workos_permission" "billing_read" { + slug = "billing:read" +} +` + "```" + ` +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier of the permission.", + MarkdownDescription: "The unique identifier of the permission.", + Computed: true, + }, + "slug": schema.StringAttribute{ + Description: "The slug identifier of the permission to look up.", + MarkdownDescription: "The slug identifier of the permission to look up.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "The display name of the permission.", + MarkdownDescription: "The display name of the permission.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "A description of the permission.", + MarkdownDescription: "A description of the permission.", + Computed: true, + }, + "system": schema.BoolAttribute{ + Description: "Whether this is a system-managed permission.", + MarkdownDescription: "Whether this is a system-managed permission.", + Computed: true, + }, + "resource_type_slug": schema.StringAttribute{ + Description: "The slug of the resource type this permission applies to.", + MarkdownDescription: "The slug of the resource type this permission applies to.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "The timestamp when the permission was created.", + MarkdownDescription: "The timestamp when the permission was created (RFC3339 format).", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + Description: "The timestamp when the permission was last updated.", + MarkdownDescription: "The timestamp when the permission was last updated (RFC3339 format).", + Computed: true, + }, + }, + } +} + +func (d *PermissionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client +} + +func (d *PermissionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config PermissionDataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if resp.Diagnostics.HasError() { + return + } + + slug := config.Slug.ValueString() + + tflog.Debug(ctx, "Reading permission by slug", map[string]any{ + "slug": slug, + }) + + perm, err := d.client.GetPermission(ctx, slug) + if err != nil { + resp.Diagnostics.AddError( + "Error Reading Permission", + "Could not read permission with slug "+slug+": "+err.Error(), + ) + return + } + + config.ID = types.StringValue(perm.ID) + config.Slug = types.StringValue(perm.Slug) + config.Name = types.StringValue(perm.Name) + config.Description = types.StringValue(perm.Description) + config.System = types.BoolValue(perm.System) + config.ResourceTypeSlug = types.StringValue(perm.ResourceTypeSlug) + config.CreatedAt = types.StringValue(perm.CreatedAt.Format(time.RFC3339)) + config.UpdatedAt = types.StringValue(perm.UpdatedAt.Format(time.RFC3339)) + + tflog.Info(ctx, "Read permission", map[string]any{ + "id": perm.ID, + "slug": perm.Slug, + "name": perm.Name, + }) + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} diff --git a/internal/provider/data_source_permission_test.go b/internal/provider/data_source_permission_test.go new file mode 100644 index 0000000..c4649cf --- /dev/null +++ b/internal/provider/data_source_permission_test.go @@ -0,0 +1,54 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccPermissionDataSource_BySlug(t *testing.T) { + slug := fmt.Sprintf("tf-acc-test-%d", time.Now().UnixNano()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccPermissionDataSourceConfigBySlug(slug), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair( + "data.workos_permission.test", "id", + "workos_permission.test", "id", + ), + resource.TestCheckResourceAttrPair( + "data.workos_permission.test", "name", + "workos_permission.test", "name", + ), + resource.TestCheckResourceAttrPair( + "data.workos_permission.test", "slug", + "workos_permission.test", "slug", + ), + resource.TestCheckResourceAttrSet("data.workos_permission.test", "created_at"), + ), + }, + }, + }) +} + +func testAccPermissionDataSourceConfigBySlug(slug string) string { + return fmt.Sprintf(` +resource "workos_permission" "test" { + slug = %[1]q + name = "Test Permission" +} + +data "workos_permission" "test" { + slug = workos_permission.test.slug +} +`, slug) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index eef2306..f650e42 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -176,6 +176,8 @@ func (p *WorkOSProvider) Resources(ctx context.Context) []func() resource.Resour NewUserResource, NewOrganizationMembershipResource, NewOrganizationRoleResource, + NewPermissionResource, + NewOrganizationRolePermissionResource, } } @@ -188,6 +190,7 @@ func (p *WorkOSProvider) DataSources(ctx context.Context) []func() datasource.Da NewDirectoryGroupDataSource, NewUserDataSource, NewOrganizationRoleDataSource, + NewPermissionDataSource, } } diff --git a/internal/provider/resource_organization_role_permission.go b/internal/provider/resource_organization_role_permission.go new file mode 100644 index 0000000..4c7bd6d --- /dev/null +++ b/internal/provider/resource_organization_role_permission.go @@ -0,0 +1,298 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "strings" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/osodevops/terraform-provider-workos/internal/client" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &OrganizationRolePermissionResource{} +var _ resource.ResourceWithImportState = &OrganizationRolePermissionResource{} + +func NewOrganizationRolePermissionResource() resource.Resource { + return &OrganizationRolePermissionResource{} +} + +// OrganizationRolePermissionResource defines the resource implementation. +type OrganizationRolePermissionResource struct { + client *client.Client +} + +// OrganizationRolePermissionResourceModel describes the resource data model. +type OrganizationRolePermissionResourceModel struct { + ID types.String `tfsdk:"id"` + OrganizationID types.String `tfsdk:"organization_id"` + RoleSlug types.String `tfsdk:"role_slug"` + Permission types.String `tfsdk:"permission"` +} + +func (r *OrganizationRolePermissionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_organization_role_permission" +} + +func (r *OrganizationRolePermissionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Assigns a permission to a WorkOS Organization Role.", + MarkdownDescription: ` +Assigns a permission to a WorkOS Organization Role. + +This resource manages the assignment of a permission to an organization role. All fields +are immutable — changing any field will destroy and recreate the assignment. + +## Example Usage + +` + "```hcl" + ` +resource "workos_organization_role_permission" "assign" { + organization_id = workos_organization.acme.id + role_slug = workos_organization_role.billing.slug + permission = workos_permission.billing_read.slug +} +` + "```" + ` + +## Import + +Organization role permissions can be imported using a composite key of organization ID, role slug, and permission slug: + +` + "```shell" + ` +terraform import workos_organization_role_permission.example org_01HXYZ.../billing-admin/billing:read +` + "```" + ` +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The composite identifier of the organization role permission (organization_id/role_slug/permission).", + MarkdownDescription: "The composite identifier of the organization role permission (`organization_id/role_slug/permission`).", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "organization_id": schema.StringAttribute{ + Description: "The ID of the organization the role belongs to.", + MarkdownDescription: "The ID of the organization the role belongs to (e.g., `org_01HXYZ...`).", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "role_slug": schema.StringAttribute{ + Description: "The slug of the organization role to assign the permission to.", + MarkdownDescription: "The slug of the organization role to assign the permission to.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "permission": schema.StringAttribute{ + Description: "The slug of the permission to assign to the role.", + MarkdownDescription: "The slug of the permission to assign to the role.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *OrganizationRolePermissionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *OrganizationRolePermissionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan OrganizationRolePermissionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + orgID := plan.OrganizationID.ValueString() + roleSlug := plan.RoleSlug.ValueString() + permSlug := plan.Permission.ValueString() + + tflog.Debug(ctx, "Adding permission to organization role", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + "permission": permSlug, + }) + + _, err := r.client.AddOrganizationRolePermission(ctx, orgID, roleSlug, permSlug) + if err != nil { + resp.Diagnostics.AddError( + "Error Adding Permission to Organization Role", + "Could not add permission to organization role, unexpected error: "+err.Error(), + ) + return + } + + plan.ID = types.StringValue(fmt.Sprintf("%s/%s/%s", orgID, roleSlug, permSlug)) + + tflog.Info(ctx, "Added permission to organization role", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + "permission": permSlug, + }) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *OrganizationRolePermissionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state OrganizationRolePermissionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + orgID := state.OrganizationID.ValueString() + roleSlug := state.RoleSlug.ValueString() + permSlug := state.Permission.ValueString() + + tflog.Debug(ctx, "Reading organization role permission", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + "permission": permSlug, + }) + + // Get the role and check if the permission is present + role, err := r.client.GetOrganizationRole(ctx, orgID, roleSlug) + if err != nil { + if client.IsNotFound(err) { + tflog.Info(ctx, "Organization role not found, removing permission assignment from state", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.AddError( + "Error Reading Organization Role Permission", + "Could not read organization role "+roleSlug+": "+err.Error(), + ) + return + } + + // Check if the permission is in the role's permissions list + found := false + for _, p := range role.Permissions { + if p == permSlug { + found = true + break + } + } + + if !found { + tflog.Info(ctx, "Permission not found on role, removing from state", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + "permission": permSlug, + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *OrganizationRolePermissionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // All fields have RequiresReplace, so Update should never be called. + resp.Diagnostics.AddError( + "Unexpected Update", + "This resource does not support in-place updates. All changes require replacement.", + ) +} + +func (r *OrganizationRolePermissionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state OrganizationRolePermissionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + orgID := state.OrganizationID.ValueString() + roleSlug := state.RoleSlug.ValueString() + permSlug := state.Permission.ValueString() + + tflog.Debug(ctx, "Removing permission from organization role", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + "permission": permSlug, + }) + + err := r.client.RemoveOrganizationRolePermission(ctx, orgID, roleSlug, permSlug) + if err != nil { + if client.IsNotFound(err) { + tflog.Info(ctx, "Organization role permission already removed", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + "permission": permSlug, + }) + return + } + + resp.Diagnostics.AddError( + "Error Removing Permission from Organization Role", + "Could not remove permission from organization role, unexpected error: "+err.Error(), + ) + return + } + + tflog.Info(ctx, "Removed permission from organization role", map[string]any{ + "organization_id": orgID, + "role_slug": roleSlug, + "permission": permSlug, + }) +} + +func (r *OrganizationRolePermissionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, "Importing organization role permission", map[string]any{ + "id": req.ID, + }) + + // Parse composite key: organization_id/role_slug/permission + parts := strings.SplitN(req.ID, "/", 3) + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + resp.Diagnostics.AddError( + "Invalid Import ID", + fmt.Sprintf("Expected import ID in the format 'organization_id/role_slug/permission', got: %s", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("role_slug"), parts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("permission"), parts[2])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) +} diff --git a/internal/provider/resource_organization_role_permission_test.go b/internal/provider/resource_organization_role_permission_test.go new file mode 100644 index 0000000..308d5c3 --- /dev/null +++ b/internal/provider/resource_organization_role_permission_test.go @@ -0,0 +1,78 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccOrganizationRolePermissionResource_Basic(t *testing.T) { + orgName := fmt.Sprintf("tf-acc-test-%d", time.Now().UnixNano()) + roleSlug := fmt.Sprintf("org-test-role-%d", time.Now().UnixNano()) + permSlug := fmt.Sprintf("tf-acc-perm-%d", time.Now().UnixNano()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccOrganizationRolePermissionResourceConfig(orgName, roleSlug, permSlug), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("workos_organization_role_permission.test", "id"), + resource.TestCheckResourceAttrSet("workos_organization_role_permission.test", "organization_id"), + resource.TestCheckResourceAttr("workos_organization_role_permission.test", "role_slug", roleSlug), + resource.TestCheckResourceAttr("workos_organization_role_permission.test", "permission", permSlug), + ), + }, + // ImportState testing + { + ResourceName: "workos_organization_role_permission.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["workos_organization_role_permission.test"] + if !ok { + return "", fmt.Errorf("resource not found: workos_organization_role_permission.test") + } + return fmt.Sprintf("%s/%s/%s", + rs.Primary.Attributes["organization_id"], + rs.Primary.Attributes["role_slug"], + rs.Primary.Attributes["permission"], + ), nil + }, + }, + }, + }) +} + +func testAccOrganizationRolePermissionResourceConfig(orgName, roleSlug, permSlug string) string { + return fmt.Sprintf(` +resource "workos_organization" "test" { + name = %[1]q +} + +resource "workos_organization_role" "test" { + organization_id = workos_organization.test.id + slug = %[2]q + name = "Test Role" +} + +resource "workos_permission" "test" { + slug = %[3]q + name = "Test Permission" +} + +resource "workos_organization_role_permission" "test" { + organization_id = workos_organization.test.id + role_slug = workos_organization_role.test.slug + permission = workos_permission.test.slug +} +`, orgName, roleSlug, permSlug) +} diff --git a/internal/provider/resource_permission.go b/internal/provider/resource_permission.go new file mode 100644 index 0000000..d85ca1f --- /dev/null +++ b/internal/provider/resource_permission.go @@ -0,0 +1,349 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "time" + + "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-log/tflog" + "github.com/osodevops/terraform-provider-workos/internal/client" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &PermissionResource{} +var _ resource.ResourceWithImportState = &PermissionResource{} + +func NewPermissionResource() resource.Resource { + return &PermissionResource{} +} + +// PermissionResource defines the resource implementation. +type PermissionResource struct { + client *client.Client +} + +// PermissionResourceModel describes the resource data model. +type PermissionResourceModel struct { + ID types.String `tfsdk:"id"` + Slug types.String `tfsdk:"slug"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + System types.Bool `tfsdk:"system"` + ResourceTypeSlug types.String `tfsdk:"resource_type_slug"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + +func (r *PermissionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_permission" +} + +func (r *PermissionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages a WorkOS Permission.", + MarkdownDescription: ` +Manages a WorkOS Permission. + +Permissions are environment-level resources that can be assigned to both environment roles +and organization roles. Permissions are identified by their slug. + +## Example Usage + +` + "```hcl" + ` +resource "workos_permission" "billing_read" { + slug = "billing:read" + name = "Read Billing" + description = "Allows reading billing data" +} +` + "```" + ` + +## Import + +Permissions can be imported using their slug: + +` + "```shell" + ` +terraform import workos_permission.example billing:read +` + "```" + ` +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier of the permission.", + MarkdownDescription: "The unique identifier of the permission.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "slug": schema.StringAttribute{ + Description: "The slug identifier for the permission. Must be unique within the environment.", + MarkdownDescription: "The slug identifier for the permission. Must be unique within the environment.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: "The display name of the permission.", + MarkdownDescription: "The display name of the permission.", + Required: true, + }, + "description": schema.StringAttribute{ + Description: "A description of the permission.", + MarkdownDescription: "A description of the permission.", + Optional: true, + Computed: true, + }, + "system": schema.BoolAttribute{ + Description: "Whether this is a system-managed permission.", + MarkdownDescription: "Whether this is a system-managed permission.", + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "resource_type_slug": schema.StringAttribute{ + Description: "The slug of the resource type this permission applies to.", + MarkdownDescription: "The slug of the resource type this permission applies to.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "created_at": schema.StringAttribute{ + Description: "The timestamp when the permission was created.", + MarkdownDescription: "The timestamp when the permission was created (RFC3339 format).", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "updated_at": schema.StringAttribute{ + Description: "The timestamp when the permission was last updated.", + MarkdownDescription: "The timestamp when the permission was last updated (RFC3339 format).", + Computed: true, + }, + }, + } +} + +func (r *PermissionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *PermissionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PermissionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Creating permission", map[string]any{ + "slug": plan.Slug.ValueString(), + "name": plan.Name.ValueString(), + }) + + createReq := &client.PermissionCreateRequest{ + Slug: plan.Slug.ValueString(), + Name: plan.Name.ValueString(), + } + + if !plan.Description.IsNull() && !plan.Description.IsUnknown() { + createReq.Description = plan.Description.ValueString() + } + + if !plan.ResourceTypeSlug.IsNull() && !plan.ResourceTypeSlug.IsUnknown() { + createReq.ResourceTypeSlug = plan.ResourceTypeSlug.ValueString() + } + + perm, err := r.client.CreatePermission(ctx, createReq) + if err != nil { + resp.Diagnostics.AddError( + "Error Creating Permission", + "Could not create permission, unexpected error: "+err.Error(), + ) + return + } + + plan.ID = types.StringValue(perm.ID) + plan.Description = types.StringValue(perm.Description) + plan.System = types.BoolValue(perm.System) + plan.ResourceTypeSlug = types.StringValue(perm.ResourceTypeSlug) + plan.CreatedAt = types.StringValue(perm.CreatedAt.Format(time.RFC3339)) + plan.UpdatedAt = types.StringValue(perm.UpdatedAt.Format(time.RFC3339)) + + tflog.Info(ctx, "Created permission", map[string]any{ + "id": perm.ID, + "slug": perm.Slug, + "name": perm.Name, + }) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PermissionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PermissionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Reading permission", map[string]any{ + "slug": state.Slug.ValueString(), + }) + + perm, err := r.client.GetPermission(ctx, state.Slug.ValueString()) + if err != nil { + if client.IsNotFound(err) { + tflog.Info(ctx, "Permission not found, removing from state", map[string]any{ + "slug": state.Slug.ValueString(), + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.AddError( + "Error Reading Permission", + "Could not read permission "+state.Slug.ValueString()+": "+err.Error(), + ) + return + } + + state.ID = types.StringValue(perm.ID) + state.Slug = types.StringValue(perm.Slug) + state.Name = types.StringValue(perm.Name) + state.Description = types.StringValue(perm.Description) + state.System = types.BoolValue(perm.System) + state.ResourceTypeSlug = types.StringValue(perm.ResourceTypeSlug) + state.CreatedAt = types.StringValue(perm.CreatedAt.Format(time.RFC3339)) + state.UpdatedAt = types.StringValue(perm.UpdatedAt.Format(time.RFC3339)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PermissionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PermissionResourceModel + var state PermissionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Updating permission", map[string]any{ + "slug": state.Slug.ValueString(), + "name": plan.Name.ValueString(), + }) + + // Skip update if no user-configurable attributes changed + if plan.Name.Equal(state.Name) && plan.Description.Equal(state.Description) { + plan.ID = state.ID + plan.System = state.System + plan.ResourceTypeSlug = state.ResourceTypeSlug + plan.CreatedAt = state.CreatedAt + plan.UpdatedAt = state.UpdatedAt + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + updateReq := &client.PermissionUpdateRequest{ + Name: plan.Name.ValueString(), + Description: plan.Description.ValueString(), + } + + perm, err := r.client.UpdatePermission(ctx, state.Slug.ValueString(), updateReq) + if err != nil { + resp.Diagnostics.AddError( + "Error Updating Permission", + "Could not update permission, unexpected error: "+err.Error(), + ) + return + } + + plan.ID = state.ID + plan.System = state.System + plan.ResourceTypeSlug = state.ResourceTypeSlug + plan.CreatedAt = state.CreatedAt + plan.Description = types.StringValue(perm.Description) + plan.UpdatedAt = types.StringValue(perm.UpdatedAt.Format(time.RFC3339)) + + tflog.Info(ctx, "Updated permission", map[string]any{ + "id": perm.ID, + "slug": perm.Slug, + "name": perm.Name, + }) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PermissionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PermissionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Deleting permission", map[string]any{ + "slug": state.Slug.ValueString(), + }) + + err := r.client.DeletePermission(ctx, state.Slug.ValueString()) + if err != nil { + if client.IsNotFound(err) { + tflog.Info(ctx, "Permission already deleted", map[string]any{ + "slug": state.Slug.ValueString(), + }) + return + } + + resp.Diagnostics.AddError( + "Error Deleting Permission", + "Could not delete permission, unexpected error: "+err.Error(), + ) + return + } + + tflog.Info(ctx, "Deleted permission", map[string]any{ + "slug": state.Slug.ValueString(), + }) +} + +func (r *PermissionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, "Importing permission", map[string]any{ + "id": req.ID, + }) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("slug"), req.ID)...) +} diff --git a/internal/provider/resource_permission_test.go b/internal/provider/resource_permission_test.go new file mode 100644 index 0000000..edd3a87 --- /dev/null +++ b/internal/provider/resource_permission_test.go @@ -0,0 +1,90 @@ +// Copyright (c) OSO DevOps +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccPermissionResource_Basic(t *testing.T) { + slug := fmt.Sprintf("tf-acc-test-%d", time.Now().UnixNano()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccPermissionResourceConfig(slug, "Test Permission", "A test permission"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("workos_permission.test", "slug", slug), + resource.TestCheckResourceAttr("workos_permission.test", "name", "Test Permission"), + resource.TestCheckResourceAttr("workos_permission.test", "description", "A test permission"), + resource.TestCheckResourceAttrSet("workos_permission.test", "id"), + resource.TestCheckResourceAttrSet("workos_permission.test", "created_at"), + resource.TestCheckResourceAttrSet("workos_permission.test", "updated_at"), + ), + }, + // ImportState testing + { + ResourceName: "workos_permission.test", + ImportState: true, + ImportStateVerify: true, + ImportStateId: slug, + }, + // Update testing + { + Config: testAccPermissionResourceConfig(slug, "Updated Permission", "An updated test permission"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("workos_permission.test", "name", "Updated Permission"), + resource.TestCheckResourceAttr("workos_permission.test", "description", "An updated test permission"), + resource.TestCheckResourceAttr("workos_permission.test", "slug", slug), + ), + }, + }, + }) +} + +func TestAccPermissionResource_NoDescription(t *testing.T) { + slug := fmt.Sprintf("tf-acc-test-%d", time.Now().UnixNano()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create without description + { + Config: testAccPermissionResourceConfigNoDescription(slug, "Test Permission"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("workos_permission.test", "slug", slug), + resource.TestCheckResourceAttr("workos_permission.test", "name", "Test Permission"), + resource.TestCheckResourceAttrSet("workos_permission.test", "id"), + ), + }, + }, + }) +} + +func testAccPermissionResourceConfig(slug, name, description string) string { + return fmt.Sprintf(` +resource "workos_permission" "test" { + slug = %[1]q + name = %[2]q + description = %[3]q +} +`, slug, name, description) +} + +func testAccPermissionResourceConfigNoDescription(slug, name string) string { + return fmt.Sprintf(` +resource "workos_permission" "test" { + slug = %[1]q + name = %[2]q +} +`, slug, name) +}