diff --git a/docs/resources/user_permissions.md b/docs/resources/user_permissions.md new file mode 100644 index 00000000..c043f3ba --- /dev/null +++ b/docs/resources/user_permissions.md @@ -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 + +### 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. + + diff --git a/netbox/provider.go b/netbox/provider.go index 6bf97119..2d9bcb72 100644 --- a/netbox/provider.go +++ b/netbox/provider.go @@ -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(), diff --git a/netbox/resource_netbox_user_permissions.go b/netbox/resource_netbox_user_permissions.go new file mode 100644 index 00000000..00ae68ae --- /dev/null +++ b/netbox/resource_netbox_user_permissions.go @@ -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 +} diff --git a/netbox/resource_netbox_user_permissions_test.go b/netbox/resource_netbox_user_permissions_test.go new file mode 100644 index 00000000..b60e8165 --- /dev/null +++ b/netbox/resource_netbox_user_permissions_test.go @@ -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 + }, + }) +} diff --git a/netbox/util.go b/netbox/util.go index f022af3d..3264bc2c 100644 --- a/netbox/util.go +++ b/netbox/util.go @@ -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() {