Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

DXCDT-431: auth0_role_permission resource #582

Merged
merged 12 commits into from
May 18, 2023
100 changes: 97 additions & 3 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

## Upgrading from v0.46.0 → v0.47.0

There are deprecations in this update. Please ensure you read this guide thoroughly and prepare your potential
There are deprecations in this update. Please ensure you read this guide thoroughly and prepare your potential
automated workflows before upgrading.

### Deprecations

- [User Roles](#user-roles)
- [Role Permissions](#role-permissions)

### User Roles

Expand Down Expand Up @@ -66,14 +67,14 @@ resource "auth0_user" "user" {
}
}

# Use the auth0_user_roles to manage a 1:many
# Use the auth0_user_roles to manage a 1:many
# relationship between the user and its roles.
resource auth0_user_roles user_roles {
user_id = auth0_user.user.id
roles = [auth0_role.admin.id]
}

# Or the auth0_user_roles to manage a 1:1
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Quick fix

# Or the auth0_user_role to manage a 1:1
# relationship between the user and its role.
resource auth0_user_role user_roles {
user_id = auth0_user.user.id
Expand All @@ -84,3 +85,96 @@ resource auth0_user_role user_roles {
</td>
</tr>
</table>

### Role Permissions

The `permissions` field on the `auth0_role` resource will continue to be available for managing role permissions. However, to ensure
a smooth transition when we eventually remove the capability to manage permissions through this field, we recommend
proactively migrating to the newly introduced `auth0_role_permission` resource. This will help you stay
prepared for future changes.

<table>
<tr>
<th>Before (v0.46.0)</th>
<th>After (v0.47.0)</th>
</tr>
<tr>
<td>

```terraform
resource auth0_resource_server api {
name = "Example API"
identifier = "https://api.travel0.com/"

scopes {
value = "read:posts"
description = "Can read posts"
}
scopes {
value = "write:posts"
description = "Can write posts"
}
}

resource auth0_role content_editor {
name = "Content Editor"
description = "Elevated roles for editing content"
permissions {
name = "read:posts"
resource_server_identifier = auth0_resource_server.api.identifier
}
permissions {
name = "write:posts"
resource_server_identifier = auth0_resource_server.api.identifier
}
}
```

</td>
<td>

```terraform
resource auth0_resource_server api {
name = "Example API"
identifier = "https://api.travel0.com/"

scopes {
value = "read:posts"
description = "Can read posts"
}
scopes {
value = "write:posts"
description = "Can write posts"
}
}

resource auth0_role content_editor {
name = "Content Editor"
description = "Elevated roles for editing content"

# Until we remove the ability to operate changes on
# the permissions field it is important to have this
# block in the config, to avoid diffing issues.
lifecycle {
ignore_changes = [ permissions ]
}
}

# Use the auth0_role_permission resource to manage a 1:1
# relationship between a role and its permissions.
resource "auth0_role_permission" "read_posts_permission" {
role_id = auth0_role.content_editor.id
resource_server_identifier = auth0_resource_server.api.identifier
permission = "read:posts"
}

resource "auth0_role_permission" "write_posts_permission" {
role_id = auth0_role.content_editor.id
resource_server_identifier = auth0_resource_server.api.identifier
permission = "write:posts"
}
```

</td>
</tr>
</table>
2 changes: 1 addition & 1 deletion docs/resources/role.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ resource "auth0_role" "my_role" {
### Optional

- `description` (String) Description of the role.
- `permissions` (Block Set) Configuration settings for permissions (scopes) attached to the role. (see [below for nested schema](#nestedblock--permissions))
- `permissions` (Block Set, Deprecated) Configuration settings for permissions (scopes) attached to the role. (see [below for nested schema](#nestedblock--permissions))

### Read-Only

Expand Down
28 changes: 28 additions & 0 deletions docs/resources/role_permission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
page_title: "Resource: auth0_role_permission"
description: |-
With this resource, you can manage role permissions (1-1).
---

# Resource: auth0_role_permission

With this resource, you can manage role permissions (1-1).



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `permission` (String) Name of the permission.
- `resource_server_identifier` (String) Identifier of the resource server that the permission is associated with.
- `role_id` (String) ID of the role to associate the permission to.

### Read-Only

- `description` (String) Description of the permission.
- `id` (String) The ID of this resource.
- `resource_server_name` (String) Name of the resource server that the permission is associated with.


2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/auth0/go-auth0 v0.0.0-20230511144613-965c83f9cd67 h1:YqVvrABIHIyH201WH9zpWOJYmn9wt3EqCZBKJzVz5zc=
github.com/auth0/go-auth0 v0.0.0-20230511144613-965c83f9cd67/go.mod h1:CMHBK8TF30dmqCItdcDHVyXg0UbYxT0laf4MGDMseN0=
github.com/auth0/go-auth0 v0.17.1 h1:xfk6Zuit4Tigg985RPZ6tpue5VHako8KfVkjjC+6X5g=
github.com/auth0/go-auth0 v0.17.1/go.mod h1:CMHBK8TF30dmqCItdcDHVyXg0UbYxT0laf4MGDMseN0=
github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48=
Expand Down
3 changes: 3 additions & 0 deletions internal/auth0/role/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func NewResource() *schema.Resource {
Type: schema.TypeSet,
Optional: true,
Description: "Configuration settings for permissions (scopes) attached to the role.",
Deprecated: "Managing permissions through the `permissions` attribute is deprecated and it will be changed to read-only in a future version. " +
"Migrate to the `auth0_role_permission` resource to manage role permissions instead. " +
"Check the [MIGRATION GUIDE](https://github.com/auth0/terraform-provider-auth0/blob/main/MIGRATION_GUIDE.md) for more info.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Expand Down
172 changes: 172 additions & 0 deletions internal/auth0/role/resource_permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package role

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/auth0/go-auth0/management"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/auth0/terraform-provider-auth0/internal/config"
)

// NewPermissionResource will return a new auth0_role_permission resource.
func NewPermissionResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"role_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "ID of the role to associate the permission to.",
},
"permission": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Name of the permission.",
},
"resource_server_identifier": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Identifier of the resource server that the permission is associated with.",
},
"description": {
Type: schema.TypeString,
Computed: true,
Description: "Description of the permission.",
},
"resource_server_name": {
Type: schema.TypeString,
Computed: true,
Description: "Name of the resource server that the permission is associated with.",
},
},
CreateContext: createRolePermission,
ReadContext: readRolePermission,
DeleteContext: deleteRolePermission,
Importer: &schema.ResourceImporter{
StateContext: importRolePermission,
},
Description: "With this resource, you can manage role permissions (1-1).",
}
}

func createRolePermission(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()
mutex := meta.(*config.Config).GetMutex()

roleID := data.Get("role_id").(string)
resourceServerID := data.Get("resource_server_identifier").(string)
permissionName := data.Get("permission").(string)

mutex.Lock(roleID)
defer mutex.Unlock(roleID)

if err := api.Role.AssociatePermissions(roleID, []*management.Permission{
{
ResourceServerIdentifier: &resourceServerID,
Name: &permissionName,
},
}); err != nil {
return diag.FromErr(err)
}

data.SetId(fmt.Sprintf(`%s::%s::%s`, roleID, resourceServerID, permissionName))

return readRolePermission(ctx, data, meta)
}

func readRolePermission(_ context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()

roleID := data.Get("role_id").(string)
permissionName := data.Get("permission").(string)
resourceServerID := data.Get("resource_server_identifier").(string)

existingPermissions, err := api.Role.Permissions(roleID)
if err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}
return diag.FromErr(err)
}

for _, p := range existingPermissions.Permissions {
if p.GetName() == permissionName && p.GetResourceServerIdentifier() == resourceServerID {
result := multierror.Append(
data.Set("description", p.GetDescription()),
data.Set("resource_server_name", p.GetResourceServerName()),
)
return diag.FromErr(result.ErrorOrNil())
}
}

data.SetId("")
return nil
}

func deleteRolePermission(_ context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()
mutex := meta.(*config.Config).GetMutex()

roleID := data.Get("role_id").(string)
permissionName := data.Get("permission").(string)
resourceServerID := data.Get("resource_server_identifier").(string)

mutex.Lock(roleID)
defer mutex.Unlock(roleID)

if err := api.Role.RemovePermissions(
roleID,
[]*management.Permission{
{
ResourceServerIdentifier: &resourceServerID,
Name: &permissionName,
},
},
); err != nil {
if mErr, ok := err.(management.Error); ok && mErr.Status() == http.StatusNotFound {
data.SetId("")
return nil
}
return diag.FromErr(err)
}

data.SetId("")
return nil
}

func importRolePermission(
_ context.Context,
data *schema.ResourceData,
_ interface{},
) ([]*schema.ResourceData, error) {
rawID := data.Id()
if rawID == "" {
return nil, fmt.Errorf("ID cannot be empty")
}

if !strings.Contains(rawID, "::") {
return nil, fmt.Errorf("ID must be formatted as <roleID>::<resourceServerIdentifier>::<permission>")
}

idPair := strings.Split(rawID, "::")
if len(idPair) != 3 {
return nil, fmt.Errorf("ID must be formatted as <roleID>::<resourceServerIdentifier>::<permission>")
}

result := multierror.Append(
data.Set("role_id", idPair[0]),
data.Set("resource_server_identifier", idPair[1]),
data.Set("permission", idPair[2]),
)

return []*schema.ResourceData{data}, result.ErrorOrNil()
}
Loading