Skip to content

Commit

Permalink
Permissions: Support service account in user_id field (#1051)
Browse files Browse the repository at this point in the history
* Permissions: Support service account in `user_id` field
Closes #994
This seems like an undocumented feature that's known to users.
You can set a service account ID in the `user_id` field of permissions resources

In this PR:
 - Make this behavior work with the new ID format (`<org>:<id>`)
 - Document that the field supports an SA
 - Add tests to make sure we don't regress it again

* Do not test Grafana 8
  • Loading branch information
julienduchesne authored Sep 25, 2023
1 parent 1ce86ab commit 76c2e8f
Show file tree
Hide file tree
Showing 12 changed files with 90 additions and 41 deletions.
2 changes: 1 addition & 1 deletion docs/resources/dashboard_permission.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Optional:

- `role` (String) Manage permissions for `Viewer` or `Editor` roles.
- `team_id` (String) ID of the team to manage permissions for. Defaults to `0`.
- `user_id` (Number) ID of the user to manage permissions for. Defaults to `0`.
- `user_id` (String) ID of the user or service account to manage permissions for. Defaults to `0`.

## Import

Expand Down
11 changes: 10 additions & 1 deletion docs/resources/data_source_permission.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ resource "grafana_user" "user" {
password = "hunter2"
}
resource "grafana_service_account" "sa" {
name = "test-ds-permissions"
role = "Viewer"
}
resource "grafana_data_source_permission" "fooPermissions" {
datasource_id = grafana_data_source.foo.id
permissions {
Expand All @@ -53,6 +58,10 @@ resource "grafana_data_source_permission" "fooPermissions" {
built_in_role = "Viewer"
permission = "Query"
}
permissions {
user_id = grafana_service_account.sa.id
permission = "Query"
}
}
```

Expand Down Expand Up @@ -83,4 +92,4 @@ Optional:

- `built_in_role` (String) Name of the basic role to manage permissions for. Options: `Viewer`, `Editor` or `Admin`. Can only be set from Grafana v9.2.3+. Defaults to ``.
- `team_id` (String) ID of the team to manage permissions for. Defaults to `0`.
- `user_id` (Number) ID of the user to manage permissions for. Defaults to `0`.
- `user_id` (String) ID of the user or service account to manage permissions for. Defaults to `0`.
2 changes: 1 addition & 1 deletion docs/resources/folder_permission.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ Optional:

- `role` (String) Manage permissions for `Viewer` or `Editor` roles.
- `team_id` (String) ID of the team to manage permissions for. Defaults to `0`.
- `user_id` (Number) ID of the user to manage permissions for. Defaults to `0`.
- `user_id` (String) ID of the user or service account to manage permissions for. Defaults to `0`.
2 changes: 1 addition & 1 deletion docs/resources/service_account_permission.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ Required:
Optional:

- `team_id` (String) ID of the team to manage permissions for. Specify either this or `user_id`. Defaults to `0`.
- `user_id` (Number) ID of the user to manage permissions for. Specify either this or `team_id`. Defaults to `0`.
- `user_id` (String) ID of the user or service account to manage permissions for. Specify either this or `team_id`. Defaults to `0`.
9 changes: 9 additions & 0 deletions examples/resources/grafana_data_source_permission/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ resource "grafana_user" "user" {
password = "hunter2"
}

resource "grafana_service_account" "sa" {
name = "test-ds-permissions"
role = "Viewer"
}

resource "grafana_data_source_permission" "fooPermissions" {
datasource_id = grafana_data_source.foo.id
permissions {
Expand All @@ -38,4 +43,8 @@ resource "grafana_data_source_permission" "fooPermissions" {
built_in_role = "Viewer"
permission = "Query"
}
permissions {
user_id = grafana_service_account.sa.id
permission = "Query"
}
}
19 changes: 11 additions & 8 deletions internal/resources/grafana/resource_dashboard_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ func ResourceDashboardPermission() *schema.Resource {
Type: schema.TypeSet,
Required: true,
Description: "The permission items to add/update. Items that are omitted from the list will be removed.",
// Ignore the org ID of the team when hashing. It works with or without it.
// Ignore the org ID of the team/SA when hashing. It works with or without it.
Set: func(i interface{}) int {
m := i.(map[string]interface{})
_, teamID := SplitOrgResourceID(m["team_id"].(string))
return schema.HashString(m["role"].(string) + teamID + strconv.Itoa(m["user_id"].(int)) + m["permission"].(string))
_, userID := SplitOrgResourceID((m["user_id"].(string)))
return schema.HashString(m["role"].(string) + teamID + userID + m["permission"].(string))
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand All @@ -72,10 +73,10 @@ func ResourceDashboardPermission() *schema.Resource {
Description: "ID of the team to manage permissions for.",
},
"user_id": {
Type: schema.TypeInt,
Type: schema.TypeString,
Optional: true,
Default: 0,
Description: "ID of the user to manage permissions for.",
Default: "0",
Description: "ID of the user or service account to manage permissions for.",
},
"permission": {
Type: schema.TypeString,
Expand Down Expand Up @@ -109,8 +110,10 @@ func UpdateDashboardPermissions(ctx context.Context, d *schema.ResourceData, met
if teamID > 0 {
permissionItem.TeamID = teamID
}
if permission["user_id"].(int) != -1 {
permissionItem.UserID = int64(permission["user_id"].(int))
_, userIDStr := SplitOrgResourceID(permission["user_id"].(string))
userID, _ := strconv.ParseInt(userIDStr, 10, 64)
if userID > 0 {
permissionItem.UserID = userID
}
permissionItem.Permission = mapPermissionStringToInt64(permission["permission"].(string))
permissionList.Items = append(permissionList.Items, &permissionItem)
Expand Down Expand Up @@ -161,7 +164,7 @@ func ReadDashboardPermissions(ctx context.Context, d *schema.ResourceData, meta
permissionItem := make(map[string]interface{})
permissionItem["role"] = permission.Role
permissionItem["team_id"] = strconv.FormatInt(permission.TeamID, 10)
permissionItem["user_id"] = permission.UserID
permissionItem["user_id"] = strconv.FormatInt(permission.UserID, 10)
permissionItem["permission"] = mapPermissionInt64ToString(permission.Permission)

permissionItems[count] = permissionItem
Expand Down
11 changes: 10 additions & 1 deletion internal/resources/grafana/resource_dashboard_permission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestAccDashboardPermission_basic(t *testing.T) {
Config: testAccDashboardPermissionConfig_Basic,
Check: resource.ComposeAggregateTestCheckFunc(
testAccDashboardPermissionsCheckExistsUID("grafana_dashboard_permission.testPermission", &dashboardUID),
resource.TestCheckResourceAttr("grafana_dashboard_permission.testPermission", "permissions.#", "4"),
resource.TestCheckResourceAttr("grafana_dashboard_permission.testPermission", "permissions.#", "5"),
),
},
{
Expand Down Expand Up @@ -159,6 +159,11 @@ resource "grafana_user" "testAdminUser" {
password = "zyx987"
}
resource "grafana_service_account" "test" {
name = "terraform-test-service-account-dashboard-perms"
role = "Editor"
}
resource "grafana_dashboard_permission" "testPermission" {
dashboard_uid = grafana_dashboard.testDashboard.uid
permissions {
Expand All @@ -177,6 +182,10 @@ resource "grafana_dashboard_permission" "testPermission" {
user_id = grafana_user.testAdminUser.id
permission = "Admin"
}
permissions {
user_id = grafana_service_account.test.id
permission = "Admin"
}
}
`
const testAccDashboardPermissionConfig_Remove = `
Expand Down
19 changes: 11 additions & 8 deletions internal/resources/grafana/resource_data_source_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ func ResourceDatasourcePermission() *schema.Resource {
Type: schema.TypeSet,
Required: true,
Description: "The permission items to add/update. Items that are omitted from the list will be removed.",
// Ignore the org ID of the team when hashing. It works with or without it.
// Ignore the org ID of the team/SA when hashing. It works with or without it.
Set: func(i interface{}) int {
m := i.(map[string]interface{})
_, teamID := SplitOrgResourceID(m["team_id"].(string))
return schema.HashString(m["built_in_role"].(string) + teamID + strconv.Itoa(m["user_id"].(int)) + m["permission"].(string))
_, userID := SplitOrgResourceID((m["user_id"].(string)))
return schema.HashString(m["built_in_role"].(string) + teamID + userID + m["permission"].(string))
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand All @@ -52,10 +53,10 @@ func ResourceDatasourcePermission() *schema.Resource {
Description: "ID of the team to manage permissions for.",
},
"user_id": {
Type: schema.TypeInt,
Type: schema.TypeString,
Optional: true,
Default: 0,
Description: "ID of the user to manage permissions for.",
Default: "0",
Description: "ID of the user or service account to manage permissions for.",
},
"built_in_role": {
Type: schema.TypeString,
Expand Down Expand Up @@ -97,8 +98,10 @@ func UpdateDatasourcePermissions(ctx context.Context, d *schema.ResourceData, me
if teamID > 0 {
permissionItem.TeamID = teamID
}
if permission["user_id"].(int) != -1 {
permissionItem.UserID = int64(permission["user_id"].(int))
_, userIDStr := SplitOrgResourceID(permission["user_id"].(string))
userID, _ := strconv.ParseInt(userIDStr, 10, 64)
if userID > 0 {
permissionItem.UserID = userID
}
if permission["built_in_role"].(string) != "" {
permissionItem.BuiltInRole = permission["built_in_role"].(string)
Expand Down Expand Up @@ -137,7 +140,7 @@ func ReadDatasourcePermissions(ctx context.Context, d *schema.ResourceData, meta
permissionItem := make(map[string]interface{})
permissionItem["built_in_role"] = permission.BuiltInRole
permissionItem["team_id"] = strconv.FormatInt(permission.TeamID, 10)
permissionItem["user_id"] = permission.UserID
permissionItem["user_id"] = strconv.FormatInt(permission.UserID, 10)

if permissionItem["permission"], err = mapDatasourcePermissionTypeToString(permission.Permission); err != nil {
return diag.FromErr(err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestAccDatasourcePermission_basic(t *testing.T) {
Config: testutils.TestAccExample(t, "resources/grafana_data_source_permission/resource.tf"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccDatasourcePermissionsCheckExists("grafana_data_source_permission.fooPermissions", &datasourceID),
resource.TestCheckResourceAttr("grafana_data_source_permission.fooPermissions", "permissions.#", "3"),
resource.TestCheckResourceAttr("grafana_data_source_permission.fooPermissions", "permissions.#", "4"),
),
},
{
Expand All @@ -35,7 +35,6 @@ func TestAccDatasourcePermission_basic(t *testing.T) {
})
}

//nolint:unused
func testAccDatasourcePermissionsCheckExists(rn string, datasourceID *int64) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[rn]
Expand Down Expand Up @@ -66,7 +65,6 @@ func testAccDatasourcePermissionsCheckExists(rn string, datasourceID *int64) res
}
}

//nolint:unused
func testAccDatasourcePermissionCheckDestroy(datasourceID *int64) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testutils.Provider.Meta().(*common.Client).GrafanaAPI
Expand Down
19 changes: 11 additions & 8 deletions internal/resources/grafana/resource_folder_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ func ResourceFolderPermission() *schema.Resource {
Type: schema.TypeSet,
Required: true,
Description: "The permission items to add/update. Items that are omitted from the list will be removed.",
// Ignore the org ID of the team when hashing. It works with or without it.
// Ignore the org ID of the team/SA when hashing. It works with or without it.
Set: func(i interface{}) int {
m := i.(map[string]interface{})
_, teamID := SplitOrgResourceID(m["team_id"].(string))
return schema.HashString(m["role"].(string) + teamID + strconv.Itoa(m["user_id"].(int)) + m["permission"].(string))
_, userID := SplitOrgResourceID(m["user_id"].(string))
return schema.HashString(m["role"].(string) + teamID + userID + m["permission"].(string))
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand All @@ -58,10 +59,10 @@ func ResourceFolderPermission() *schema.Resource {
Description: "ID of the team to manage permissions for.",
},
"user_id": {
Type: schema.TypeInt,
Type: schema.TypeString,
Optional: true,
Default: 0,
Description: "ID of the user to manage permissions for.",
Default: "0",
Description: "ID of the user or service account to manage permissions for.",
},
"permission": {
Type: schema.TypeString,
Expand Down Expand Up @@ -95,8 +96,10 @@ func UpdateFolderPermissions(ctx context.Context, d *schema.ResourceData, meta i
if teamID > 0 {
permissionItem.TeamID = teamID
}
if permission["user_id"].(int) != -1 {
permissionItem.UserID = int64(permission["user_id"].(int))
_, userIDStr := SplitOrgResourceID(permission["user_id"].(string))
userID, _ := strconv.ParseInt(userIDStr, 10, 64)
if userID > 0 {
permissionItem.UserID = userID
}
permissionItem.Permission = mapPermissionStringToInt64(permission["permission"].(string))
permissionList.Items = append(permissionList.Items, &permissionItem)
Expand Down Expand Up @@ -129,7 +132,7 @@ func ReadFolderPermissions(ctx context.Context, d *schema.ResourceData, meta int
permissionItem := make(map[string]interface{})
permissionItem["role"] = permission.Role
permissionItem["team_id"] = strconv.FormatInt(permission.TeamID, 10)
permissionItem["user_id"] = permission.UserID
permissionItem["user_id"] = strconv.FormatInt(permission.UserID, 10)
permissionItem["permission"] = mapPermissionInt64ToString(permission.Permission)

permissionItems[count] = permissionItem
Expand Down
13 changes: 12 additions & 1 deletion internal/resources/grafana/resource_folder_permission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

func TestAccFolderPermission_basic(t *testing.T) {
testutils.CheckOSSTestsEnabled(t)
testutils.CheckOSSTestsSemver(t, ">=9.0.0") // Folder permissions only work for service accounts in Grafana 9+, so we're just not testing versions before 9.

folderUID := "uninitialized"

Expand All @@ -24,7 +25,7 @@ func TestAccFolderPermission_basic(t *testing.T) {
Config: testAccFolderPermissionConfig_Basic,
Check: resource.ComposeAggregateTestCheckFunc(
testAccFolderPermissionsCheckExists("grafana_folder_permission.testPermission", &folderUID),
resource.TestCheckResourceAttr("grafana_folder_permission.testPermission", "permissions.#", "4"),
resource.TestCheckResourceAttr("grafana_folder_permission.testPermission", "permissions.#", "5"),
),
},
{
Expand Down Expand Up @@ -100,6 +101,12 @@ resource "grafana_user" "testAdminUser" {
password = "zyx987"
}
resource "grafana_service_account" "test" {
name = "terraform-test-service-account-folder-perms"
role = "Editor"
is_disabled = false
}
resource "grafana_folder_permission" "testPermission" {
folder_uid = grafana_folder.testFolder.uid
permissions {
Expand All @@ -118,6 +125,10 @@ resource "grafana_folder_permission" "testPermission" {
user_id = grafana_user.testAdminUser.id
permission = "Admin"
}
permissions {
user_id = grafana_service_account.test.id
permission = "Admin"
}
}
`
const testAccFolderPermissionConfig_Remove = `
Expand Down
20 changes: 12 additions & 8 deletions internal/resources/grafana/resource_service_account_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ func ResourceServiceAccountPermission() *schema.Resource {
Set: func(i interface{}) int {
m := i.(map[string]interface{})
_, teamID := SplitOrgResourceID(m["team_id"].(string))
return schema.HashString(teamID + strconv.Itoa(m["user_id"].(int)) + m["permission"].(string))
_, userID := SplitOrgResourceID((m["user_id"].(string)))
return schema.HashString(teamID + userID + m["permission"].(string))
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand All @@ -52,10 +53,10 @@ func ResourceServiceAccountPermission() *schema.Resource {
Description: "ID of the team to manage permissions for. Specify either this or `user_id`.",
},
"user_id": {
Type: schema.TypeInt,
Type: schema.TypeString,
Optional: true,
Default: 0,
Description: "ID of the user to manage permissions for. Specify either this or `team_id`.",
Default: "0",
Description: "ID of the user or service account to manage permissions for. Specify either this or `team_id`.",
},
"permission": {
Type: schema.TypeString,
Expand Down Expand Up @@ -91,7 +92,7 @@ func ReadServiceAccountPermissions(ctx context.Context, d *schema.ResourceData,
}
permMap := map[string]interface{}{
"team_id": strconv.FormatInt(p.TeamID, 10),
"user_id": p.UserID,
"user_id": strconv.FormatInt(p.UserID, 10),
"permission": p.Permission,
}
saPerms = append(saPerms, permMap)
Expand All @@ -117,7 +118,8 @@ func UpdateServiceAccountPermissions(ctx context.Context, d *schema.ResourceData
perm := p.(map[string]interface{})
_, teamIDStr := SplitOrgResourceID(perm["team_id"].(string))
teamID, _ := strconv.ParseInt(teamIDStr, 10, 64)
userID := int64(perm["user_id"].(int))
_, userIDStr := SplitOrgResourceID(perm["user_id"].(string))
userID, _ := strconv.ParseInt(userIDStr, 10, 64)
if teamID > 0 {
oldTeamPerms[teamID] = perm["permission"].(string)
}
Expand All @@ -134,7 +136,8 @@ func UpdateServiceAccountPermissions(ctx context.Context, d *schema.ResourceData
permissionItem := gapi.ServiceAccountPermissionItem{}
_, teamIDStr := SplitOrgResourceID(permission["team_id"].(string))
teamID, _ := strconv.ParseInt(teamIDStr, 10, 64)
userID := int64(permission["user_id"].(int))
_, userIDStr := SplitOrgResourceID(permission["user_id"].(string))
userID, _ := strconv.ParseInt(userIDStr, 10, 64)
if teamID > 0 {
perm, has := oldTeamPerms[teamID]
if has {
Expand Down Expand Up @@ -197,7 +200,8 @@ func DeleteServiceAccountPermissions(ctx context.Context, d *schema.ResourceData
perm := p.(map[string]interface{})
_, teamIDStr := SplitOrgResourceID(perm["team_id"].(string))
teamID, _ := strconv.ParseInt(teamIDStr, 10, 64)
userID := int64(perm["user_id"].(int))
_, userIDStr := SplitOrgResourceID(perm["user_id"].(string))
userID, _ := strconv.ParseInt(userIDStr, 10, 64)
permissionItem := gapi.ServiceAccountPermissionItem{}

if teamID > 0 {
Expand Down

0 comments on commit 76c2e8f

Please sign in to comment.