Skip to content

Commit

Permalink
feat: Add Netbox User Permission Resource (#390)
Browse files Browse the repository at this point in the history
This feature allows management of netbox user permissions with
terraform. It follows the pattern of testing and design similar to that
of other resources.

A unique feature of this resource is the handling of the `constraints`
field. Since this field is a JSON blob in the netbox API, we had to
find a nice way to convert a JSON string into a JSON blob. And since
this field can be either a JSON list or a JSON object, we had to handle
this accordingly. By using `json.Unmarshal()` on the string, and then
doing a type switch, we could detect the correct type, and set that in
the struct for sending it to the API.

Ref: #387
  • Loading branch information
tagur87 authored May 7, 2023
1 parent d2ce14a commit 74b786e
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 0 deletions.
41 changes: 41 additions & 0 deletions docs/resources/user_permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
# generated by https://github.com/fbreckle/terraform-plugin-docs
page_title: "netbox_user_permissions Resource - terraform-provider-netbox"
subcategory: "Authentication"
description: |-
This resource manages the object-based permissions for Netbox users, built into the application.
Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type.
For more information, see the Netbox Object-Based Permissions Docs. https://docs.netbox.dev/en/stable/administration/permissions/
---

# netbox_user_permissions (Resource)

This resource manages the object-based permissions for Netbox users, built into the application.

> Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type.
> For more information, see the [Netbox Object-Based Permissions Docs.](https://docs.netbox.dev/en/stable/administration/permissions/)


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

### Required

- `actions` (Set of Number) A list actions that are allowed on the object types. Acceptable values are `view`, `add`, `change`, or `delete`.
- `name` (String) The name of the permissions object.
- `object_types` (Set of String) A list of object types that the permission object allows access to. Should be in a form the API can accept. For example: `circuits.provider`, `dcim.inventoryitem`, etc.

### Optional

- `constraints` (String) A JSON string of an arbitrary filter used to limit the granted action(s) to a specific subset of objects. For more information on correct syntax, see https://docs.netbox.dev/en/stable/administration/permissions/#constraints. Defaults to `""`.
- `description` (String) The description of the permissions object.
- `enabled` (Boolean) Whether the permissions object is enabled or not. Defaults to `true`.
- `groups` (Set of Number) A list of group IDs that have been assigned to this permissions object.
- `users` (Set of Number) A list of user IDs that have been assigned to this permissions object.

### Read-Only

- `id` (String) The ID of this resource.


1 change: 1 addition & 0 deletions netbox/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func Provider() *schema.Provider {
"netbox_circuit_provider": resourceNetboxCircuitProvider(),
"netbox_circuit_termination": resourceNetboxCircuitTermination(),
"netbox_user": resourceNetboxUser(),
"netbox_user_permissions": resourceNetboxUserPermissions(),
"netbox_token": resourceNetboxToken(),
"netbox_custom_field": resourceCustomField(),
"netbox_asn": resourceNetboxAsn(),
Expand Down
231 changes: 231 additions & 0 deletions netbox/resource_netbox_user_permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package netbox

import (
"encoding/json"
"strconv"

"github.com/fbreckle/go-netbox/netbox/client"
"github.com/fbreckle/go-netbox/netbox/client/users"
"github.com/fbreckle/go-netbox/netbox/models"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

// a
func resourceNetboxUserPermissions() *schema.Resource {
return &schema.Resource{
Create: resourceNetboxUserPermissionsCreate,
Read: resourceNetboxUserPermissionsRead,
Update: resourceNetboxUserPermissionsUpdate,
Delete: resourceNetboxUserPermissionsDelete,
Description: `:meta:subcategory:Authentication:This resource manages the object-based permissions for Netbox users, built into the application.
> Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type.
> For more information, see the [Netbox Object-Based Permissions Docs.](https://docs.netbox.dev/en/stable/administration/permissions/)`,

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "The name of the permissions object.",
Required: true,
},
"description": {
Type: schema.TypeString,
Description: "The description of the permissions object.",
Optional: true,
},
"enabled": {
Type: schema.TypeBool,
Description: "Whether the permissions object is enabled or not.",
Optional: true,
Default: true,
},
"object_types": {
Type: schema.TypeSet,
Description: "A list of object types that the permission object allows access to. Should be in a form " +
"the API can accept. For example: `circuits.provider`, `dcim.inventoryitem`, etc.",
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"groups": {
Type: schema.TypeSet,
Optional: true,
Description: "A list of group IDs that have been assigned to this permissions object.",
Elem: &schema.Schema{
Type: schema.TypeInt,
},
},
"users": {
Type: schema.TypeSet,
Optional: true,
Description: "A list of user IDs that have been assigned to this permissions object.",
Elem: &schema.Schema{
Type: schema.TypeInt,
},
},
"actions": {
Type: schema.TypeSet,
Required: true,
Description: "A list actions that are allowed on the object types. Acceptable values are `view`, `add`, `change`, or `delete`.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"constraints": {
Type: schema.TypeString,
Description: "A JSON string of an arbitrary filter used to limit the granted action(s) to a specific subset of objects. " +
"For more information on correct syntax, see https://docs.netbox.dev/en/stable/administration/permissions/#constraints ",
Optional: true,
Default: nil,
ValidateFunc: validation.StringIsJSON,
},
},
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
}
}
func resourceNetboxUserPermissionsCreate(d *schema.ResourceData, m interface{}) error {
api := m.(*client.NetBoxAPI)
data := models.WritableObjectPermission{}

name := d.Get("name").(string)
data.Name = &name
data.Description = d.Get("description").(string)
data.Enabled = d.Get("enabled").(bool)

data.ObjectTypes = toStringList(d.Get("object_types"))
data.Groups = toInt64List(d.Get("groups"))
data.Users = toInt64List(d.Get("users"))
data.Actions = toStringList(d.Get("actions"))

var constraints interface{}
c := d.Get("constraints").(string)
if c == "" {
data.Constraints = nil
} else {
err := json.Unmarshal([]byte(c), &constraints)
if err != nil {
return err
}
switch v := constraints.(type) {
case []interface{}:
data.Constraints = v
case map[string]interface{}:
data.Constraints = v

}
}

params := users.NewUsersPermissionsCreateParams().WithData(&data)
res, err := api.Users.UsersPermissionsCreate(params, nil)
if err != nil {
return err
}
d.SetId(strconv.FormatInt(res.GetPayload().ID, 10))

return resourceNetboxUserPermissionsRead(d, m)
}

func resourceNetboxUserPermissionsRead(d *schema.ResourceData, m interface{}) error {
api := m.(*client.NetBoxAPI)
id, _ := strconv.ParseInt(d.Id(), 10, 64)
params := users.NewUsersPermissionsReadParams().WithID(id)

res, err := api.Users.UsersPermissionsRead(params, nil)
if err != nil {
errorcode := err.(*users.UsersPermissionsReadDefault).Code()
if errorcode == 404 {
// If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html
d.SetId("")
return nil
}
return err
}

d.Set("name", res.GetPayload().Name)
d.Set("description", res.GetPayload().Description)
d.Set("enabled", res.GetPayload().Enabled)
d.Set("object_types", res.GetPayload().ObjectTypes)

var groups []int
for _, v := range res.GetPayload().Groups {
groups = append(groups, int(v.ID))
}
d.Set("groups", groups)

var users []int
for _, v := range res.GetPayload().Users {
users = append(users, int(v.ID))
}
d.Set("users", users)

d.Set("actions", res.GetPayload().Actions)

b, err := json.Marshal(res.GetPayload().Constraints)
if err != nil {
return err
}
d.Set("constraints", string(b))

return nil
}

func resourceNetboxUserPermissionsUpdate(d *schema.ResourceData, m interface{}) error {
api := m.(*client.NetBoxAPI)
id, _ := strconv.ParseInt(d.Id(), 10, 64)
data := models.WritableObjectPermission{}

name := d.Get("name").(string)
data.Name = &name
data.Description = d.Get("description").(string)
data.Enabled = d.Get("enabled").(bool)

data.ObjectTypes = toStringList(d.Get("object_types"))
data.Groups = toInt64List(d.Get("groups"))
data.Users = toInt64List(d.Get("users"))
data.Actions = toStringList(d.Get("actions"))

var constraints interface{}
c := d.Get("constraints").(string)
if c == "" {
data.Constraints = nil
} else {
err := json.Unmarshal([]byte(c), &constraints)
if err != nil {
return err
}
switch v := constraints.(type) {
case []interface{}:
data.Constraints = v
case map[string]interface{}:
data.Constraints = v
}
}
params := users.NewUsersPermissionsUpdateParams().WithID(id).WithData(&data)
_, err := api.Users.UsersPermissionsUpdate(params, nil)
if err != nil {
return err
}
return resourceNetboxUserPermissionsRead(d, m)
}

func resourceNetboxUserPermissionsDelete(d *schema.ResourceData, m interface{}) error {
api := m.(*client.NetBoxAPI)
id, _ := strconv.ParseInt(d.Id(), 10, 64)
params := users.NewUsersPermissionsDeleteParams().WithID(id)
_, err := api.Users.UsersPermissionsDelete(params, nil)
if err != nil {
if errresp, ok := err.(*users.UsersPermissionsDeleteDefault); ok {
if errresp.Code() == 404 {
d.SetId("")
return nil
}
}
return err
}
d.SetId("")
return nil
}
86 changes: 86 additions & 0 deletions netbox/resource_netbox_user_permissions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package netbox

import (
"fmt"
"log"
"strings"
"testing"

"github.com/fbreckle/go-netbox/netbox/client"
"github.com/fbreckle/go-netbox/netbox/client/users"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccNetboxUsePermissions_basic(t *testing.T) {
testSlug := "user_permissions"
testName := testAccGetTestName(testSlug)
resource.ParallelTest(t, resource.TestCase{
Providers: testAccProviders,
PreCheck: func() { testAccPreCheck(t) },
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "netbox_user_permissions" "test_basic" {
name = "%s"
description = "This is a terraform test."
enabled = true
object_types = ["ipam.prefix"]
actions = ["add", "change"]
users = [1]
constraints = jsonencode([{
"status" = "active"
}]
)
}`, testName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "name", testName),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "description", "This is a terraform test."),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "enabled", "true"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "object_types.#", "1"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "object_types.0", "ipam.prefix"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "actions.#", "2"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "actions.0", "add"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "actions.1", "change"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "users.#", "1"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "users.0", "1"),
resource.TestCheckResourceAttr("netbox_user_permissions.test_basic", "constraints", "[{\"status\":\"active\"}]"),
),
},
{
ResourceName: "netbox_user_permissions.test_basic",
ImportState: true,
ImportStateVerify: false,
},
},
})
}

func init() {
resource.AddTestSweepers("netbox_user_permissions", &resource.Sweeper{
Name: "netbox_user_permissions",
Dependencies: []string{},
F: func(region string) error {
m, err := sharedClientForRegion(region)
if err != nil {
return fmt.Errorf("Error getting client: %s", err)
}
api := m.(*client.NetBoxAPI)
params := users.NewUsersPermissionsListParams()
res, err := api.Users.UsersPermissionsList(params, nil)
if err != nil {
return err
}
for _, perm := range res.GetPayload().Results {
if strings.HasPrefix(*perm.Name, testPrefix) {
deleteParams := users.NewUsersPermissionsDeleteParams().WithID(perm.ID)
_, err := api.Users.UsersPermissionsDelete(deleteParams, nil)
if err != nil {
return err
}
log.Print("[DEBUG] Deleted a user")
}
}
return nil
},
})
}
8 changes: 8 additions & 0 deletions netbox/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ func float64ToPtr(i float64) *float64 {
return &i
}

func toStringList(a interface{}) []string {
strList := []string{}
for _, str := range a.(*schema.Set).List() {
strList = append(strList, str.(string))
}
return strList
}

func toInt64List(a interface{}) []int64 {
intList := []int64{}
for _, number := range a.(*schema.Set).List() {
Expand Down

0 comments on commit 74b786e

Please sign in to comment.