Skip to content

Commit 754e313

Browse files
Adding a resource for service account permissions (#708)
* resource for service account permissions * test * add resource example * linting * docs * docs category * docs category done correctly * terraform fmt * gen docs again after updating example * set -1 as the default ID * docs * set default team and user IDs to 0 * generate docs
1 parent 7f749e5 commit 754e313

File tree

6 files changed

+460
-27
lines changed

6 files changed

+460
-27
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_service_account_permission Resource - terraform-provider-grafana"
4+
subcategory: "Grafana OSS"
5+
description: |-
6+
Note: This resource is available from Grafana 9.2.4 onwards.
7+
Official documentation https://grafana.com/docs/grafana/latest/administration/service-accounts/#manage-users-and-teams-permissions-for-a-service-account-in-grafana
8+
---
9+
10+
# grafana_service_account_permission (Resource)
11+
12+
**Note:** This resource is available from Grafana 9.2.4 onwards.
13+
14+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/#manage-users-and-teams-permissions-for-a-service-account-in-grafana)
15+
16+
## Example Usage
17+
18+
```terraform
19+
resource "grafana_service_account" "test" {
20+
name = "sa-terraform-test"
21+
role = "Editor"
22+
is_disabled = false
23+
}
24+
25+
resource "grafana_team" "test_team" {
26+
name = "tf_test_team"
27+
}
28+
29+
resource "grafana_user" "test_user" {
30+
31+
32+
password = "password"
33+
}
34+
35+
resource "grafana_service_account_permission" "test_permissions" {
36+
service_account_id = grafana_service_account.test.id
37+
38+
permissions {
39+
user_id = grafana_user.test_user.id
40+
permission = "Edit"
41+
}
42+
permissions {
43+
team_id = grafana_team.test_team.id
44+
permission = "Admin"
45+
}
46+
}
47+
```
48+
49+
<!-- schema generated by tfplugindocs -->
50+
## Schema
51+
52+
### Required
53+
54+
- `permissions` (Block Set, Min: 1) The permission items to add/update. Items that are omitted from the list will be removed. (see [below for nested schema](#nestedblock--permissions))
55+
- `service_account_id` (Number) The id of the service account.
56+
57+
### Read-Only
58+
59+
- `id` (String) The ID of this resource.
60+
61+
<a id="nestedblock--permissions"></a>
62+
### Nested Schema for `permissions`
63+
64+
Required:
65+
66+
- `permission` (String) Permission to associate with item. Must be `Edit` or `Admin`.
67+
68+
Optional:
69+
70+
- `team_id` (Number) ID of the team to manage permissions for. Specify either this or `user_id`. Defaults to `0`.
71+
- `user_id` (Number) ID of the user to manage permissions for. Specify either this or `team_id`. Defaults to `0`.
72+
73+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
resource "grafana_service_account" "test" {
2+
name = "sa-terraform-test"
3+
role = "Editor"
4+
is_disabled = false
5+
}
6+
7+
resource "grafana_team" "test_team" {
8+
name = "tf_test_team"
9+
}
10+
11+
resource "grafana_user" "test_user" {
12+
13+
14+
password = "password"
15+
}
16+
17+
resource "grafana_service_account_permission" "test_permissions" {
18+
service_account_id = grafana_service_account.test.id
19+
20+
permissions {
21+
user_id = grafana_user.test_user.id
22+
permission = "Edit"
23+
}
24+
permissions {
25+
team_id = grafana_team.test_team.id
26+
permission = "Admin"
27+
}
28+
}

grafana/provider.go

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,34 @@ func Provider(version string) func() *schema.Provider {
4848
// Resources that require the Grafana client to exist.
4949
grafanaClientResources = addResourcesMetadataValidation(grafanaClientPresent, map[string]*schema.Resource{
5050
// Grafana
51-
"grafana_annotation": ResourceAnnotation(),
52-
"grafana_alert_notification": ResourceAlertNotification(),
53-
"grafana_builtin_role_assignment": ResourceBuiltInRoleAssignment(),
54-
"grafana_contact_point": ResourceContactPoint(),
55-
"grafana_dashboard": ResourceDashboard(),
56-
"grafana_dashboard_permission": ResourceDashboardPermission(),
57-
"grafana_data_source": ResourceDataSource(),
58-
"grafana_data_source_permission": ResourceDatasourcePermission(),
59-
"grafana_folder": ResourceFolder(),
60-
"grafana_folder_permission": ResourceFolderPermission(),
61-
"grafana_library_panel": ResourceLibraryPanel(),
62-
"grafana_message_template": ResourceMessageTemplate(),
63-
"grafana_mute_timing": ResourceMuteTiming(),
64-
"grafana_notification_policy": ResourceNotificationPolicy(),
65-
"grafana_organization": ResourceOrganization(),
66-
"grafana_organization_preferences": ResourceOrganizationPreferences(),
67-
"grafana_playlist": ResourcePlaylist(),
68-
"grafana_report": ResourceReport(),
69-
"grafana_role": ResourceRole(),
70-
"grafana_role_assignment": ResourceRoleAssignment(),
71-
"grafana_rule_group": ResourceRuleGroup(),
72-
"grafana_team": ResourceTeam(),
73-
"grafana_team_preferences": ResourceTeamPreferences(),
74-
"grafana_team_external_group": ResourceTeamExternalGroup(),
75-
"grafana_service_account_token": ResourceServiceAccountToken(),
76-
"grafana_service_account": ResourceServiceAccount(),
77-
"grafana_user": ResourceUser(),
51+
"grafana_annotation": ResourceAnnotation(),
52+
"grafana_alert_notification": ResourceAlertNotification(),
53+
"grafana_builtin_role_assignment": ResourceBuiltInRoleAssignment(),
54+
"grafana_contact_point": ResourceContactPoint(),
55+
"grafana_dashboard": ResourceDashboard(),
56+
"grafana_dashboard_permission": ResourceDashboardPermission(),
57+
"grafana_data_source": ResourceDataSource(),
58+
"grafana_data_source_permission": ResourceDatasourcePermission(),
59+
"grafana_folder": ResourceFolder(),
60+
"grafana_folder_permission": ResourceFolderPermission(),
61+
"grafana_library_panel": ResourceLibraryPanel(),
62+
"grafana_message_template": ResourceMessageTemplate(),
63+
"grafana_mute_timing": ResourceMuteTiming(),
64+
"grafana_notification_policy": ResourceNotificationPolicy(),
65+
"grafana_organization": ResourceOrganization(),
66+
"grafana_organization_preferences": ResourceOrganizationPreferences(),
67+
"grafana_playlist": ResourcePlaylist(),
68+
"grafana_report": ResourceReport(),
69+
"grafana_role": ResourceRole(),
70+
"grafana_role_assignment": ResourceRoleAssignment(),
71+
"grafana_rule_group": ResourceRuleGroup(),
72+
"grafana_team": ResourceTeam(),
73+
"grafana_team_preferences": ResourceTeamPreferences(),
74+
"grafana_team_external_group": ResourceTeamExternalGroup(),
75+
"grafana_service_account_token": ResourceServiceAccountToken(),
76+
"grafana_service_account": ResourceServiceAccount(),
77+
"grafana_service_account_permission": ResourceServiceAccountPermission(),
78+
"grafana_user": ResourceUser(),
7879

7980
// Machine Learning
8081
"grafana_machine_learning_job": ResourceMachineLearningJob(),
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"log"
6+
"strconv"
7+
"strings"
8+
9+
gapi "github.com/grafana/grafana-api-golang-client"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
13+
)
14+
15+
func ResourceServiceAccountPermission() *schema.Resource {
16+
return &schema.Resource{
17+
Description: `
18+
**Note:** This resource is available from Grafana 9.2.4 onwards.
19+
20+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/#manage-users-and-teams-permissions-for-a-service-account-in-grafana)`,
21+
22+
CreateContext: UpdateServiceAccountPermissions,
23+
ReadContext: ReadServiceAccountPermissions,
24+
UpdateContext: UpdateServiceAccountPermissions,
25+
DeleteContext: DeleteServiceAccountPermissions,
26+
Importer: &schema.ResourceImporter{
27+
StateContext: schema.ImportStatePassthroughContext,
28+
},
29+
Schema: map[string]*schema.Schema{
30+
"service_account_id": {
31+
Type: schema.TypeInt,
32+
Required: true,
33+
ForceNew: true,
34+
Description: "The id of the service account.",
35+
},
36+
"permissions": {
37+
Type: schema.TypeSet,
38+
Required: true,
39+
Description: "The permission items to add/update. Items that are omitted from the list will be removed.",
40+
Elem: &schema.Resource{
41+
Schema: map[string]*schema.Schema{
42+
"team_id": {
43+
Type: schema.TypeInt,
44+
Optional: true,
45+
Default: 0,
46+
Description: "ID of the team to manage permissions for. Specify either this or `user_id`.",
47+
},
48+
"user_id": {
49+
Type: schema.TypeInt,
50+
Optional: true,
51+
Default: 0,
52+
Description: "ID of the user to manage permissions for. Specify either this or `team_id`.",
53+
},
54+
"permission": {
55+
Type: schema.TypeString,
56+
Required: true,
57+
ValidateFunc: validation.StringInSlice([]string{"Edit", "Admin"}, false),
58+
Description: "Permission to associate with item. Must be `Edit` or `Admin`.",
59+
},
60+
},
61+
},
62+
},
63+
},
64+
}
65+
}
66+
67+
func ReadServiceAccountPermissions(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
68+
client := meta.(*client).gapi
69+
id, err := strconv.ParseInt(d.Id(), 10, 64)
70+
if err != nil {
71+
return diag.FromErr(err)
72+
}
73+
74+
saPermissions, err := client.GetServiceAccountPermissions(id)
75+
if err != nil {
76+
if strings.Contains(err.Error(), "404") {
77+
d.SetId("")
78+
log.Printf("[WARN] removing permissions for account with ID %d from state because the service account no longer exists in grafana", id)
79+
return nil
80+
}
81+
82+
return diag.FromErr(err)
83+
}
84+
85+
saPerms := make([]interface{}, 0)
86+
for _, p := range saPermissions {
87+
// Only managed service account permissions can be provisioned through this resource.
88+
if !p.IsManaged {
89+
continue
90+
}
91+
permMap := map[string]interface{}{
92+
"team_id": p.TeamID,
93+
"user_id": p.UserID,
94+
"permission": p.Permission,
95+
}
96+
saPerms = append(saPerms, permMap)
97+
}
98+
if err = d.Set("permissions", saPerms); err != nil {
99+
return diag.FromErr(err)
100+
}
101+
if err = d.Set("service_account_id", id); err != nil {
102+
return diag.FromErr(err)
103+
}
104+
d.SetId(strconv.FormatInt(id, 10))
105+
106+
return nil
107+
}
108+
109+
func UpdateServiceAccountPermissions(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
110+
client := meta.(*client).gapi
111+
112+
// Get a list of permissions from Grafana state (current permission setup)
113+
state, config := d.GetChange("permissions")
114+
oldTeamPerms := make(map[int64]string, 0)
115+
oldUserPerms := make(map[int64]string, 0)
116+
for _, p := range state.(*schema.Set).List() {
117+
perm := p.(map[string]interface{})
118+
teamID := int64(perm["team_id"].(int))
119+
userID := int64(perm["user_id"].(int))
120+
if teamID > 0 {
121+
oldTeamPerms[teamID] = perm["permission"].(string)
122+
}
123+
if userID > 0 {
124+
oldUserPerms[userID] = perm["permission"].(string)
125+
}
126+
}
127+
128+
permissionList := gapi.ServiceAccountPermissionItems{}
129+
130+
// Iterate over permissions from the configuration (the desired permission setup)
131+
for _, p := range config.(*schema.Set).List() {
132+
permission := p.(map[string]interface{})
133+
permissionItem := gapi.ServiceAccountPermissionItem{}
134+
teamID := int64(permission["team_id"].(int))
135+
userID := int64(permission["user_id"].(int))
136+
if teamID > 0 {
137+
perm, has := oldTeamPerms[teamID]
138+
if has {
139+
delete(oldTeamPerms, teamID)
140+
// Skip permissions that have not been changed
141+
if perm == permission["permission"].(string) {
142+
continue
143+
}
144+
}
145+
permissionItem.TeamID = teamID
146+
} else if userID > 0 {
147+
perm, has := oldUserPerms[userID]
148+
if has {
149+
delete(oldUserPerms, userID)
150+
if perm == permission["permission"].(string) {
151+
continue
152+
}
153+
}
154+
permissionItem.UserID = userID
155+
}
156+
permissionItem.Permission = permission["permission"].(string)
157+
permissionList.Permissions = append(permissionList.Permissions, &permissionItem)
158+
}
159+
160+
// Remove the permissions that are in the state but not in the config
161+
for teamID := range oldTeamPerms {
162+
permissionList.Permissions = append(permissionList.Permissions, &gapi.ServiceAccountPermissionItem{
163+
TeamID: teamID,
164+
Permission: "",
165+
})
166+
}
167+
for userID := range oldUserPerms {
168+
permissionList.Permissions = append(permissionList.Permissions, &gapi.ServiceAccountPermissionItem{
169+
UserID: userID,
170+
Permission: "",
171+
})
172+
}
173+
174+
saID := int64(d.Get("service_account_id").(int))
175+
err := client.UpdateServiceAccountPermissions(saID, &permissionList)
176+
if err != nil {
177+
return diag.FromErr(err)
178+
}
179+
180+
d.SetId(strconv.FormatInt(saID, 10))
181+
182+
return ReadServiceAccountPermissions(ctx, d, meta)
183+
}
184+
185+
func DeleteServiceAccountPermissions(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
186+
client := meta.(*client).gapi
187+
188+
state, _ := d.GetChange("permissions")
189+
permissionList := gapi.ServiceAccountPermissionItems{}
190+
for _, p := range state.(*schema.Set).List() {
191+
perm := p.(map[string]interface{})
192+
teamID := int64(perm["team_id"].(int))
193+
userID := int64(perm["user_id"].(int))
194+
permissionItem := gapi.ServiceAccountPermissionItem{}
195+
196+
if teamID > 0 {
197+
permissionItem.TeamID = teamID
198+
} else if userID > 0 {
199+
permissionItem.UserID = userID
200+
}
201+
permissionItem.Permission = ""
202+
permissionList.Permissions = append(permissionList.Permissions, &permissionItem)
203+
}
204+
205+
id := int64(d.Get("service_account_id").(int))
206+
err := client.UpdateServiceAccountPermissions(id, &permissionList)
207+
if err != nil {
208+
return diag.FromErr(err)
209+
}
210+
211+
return nil
212+
}

0 commit comments

Comments
 (0)