Skip to content

Commit 5ceddbd

Browse files
authored
Revert "BasicRoles: Remove builtin role assignments (#474)" (#496)
This reverts commit 62f1baf.
1 parent 62f1baf commit 5ceddbd

File tree

6 files changed

+704
-17
lines changed

6 files changed

+704
-17
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_builtin_role_assignment Resource - terraform-provider-grafana"
4+
subcategory: ""
5+
description: |-
6+
Note: This resource is available only with Grafana Enterprise 8.+.
7+
Official documentation https://grafana.com/docs/grafana/latest/enterprise/access-control/HTTP API https://grafana.com/docs/grafana/latest/http_api/access_control/
8+
---
9+
10+
# grafana_builtin_role_assignment (Resource)
11+
12+
**Note:** This resource is available only with Grafana Enterprise 8.+.
13+
14+
* [Official documentation](https://grafana.com/docs/grafana/latest/enterprise/access-control/)
15+
* [HTTP API](https://grafana.com/docs/grafana/latest/http_api/access_control/)
16+
17+
## Example Usage
18+
19+
```terraform
20+
resource "grafana_builtin_role_assignment" "viewer" {
21+
builtin_role = "Viewer"
22+
roles {
23+
uid = "firstuid"
24+
global = false
25+
}
26+
roles {
27+
uid = "seconduid"
28+
global = true
29+
}
30+
}
31+
```
32+
33+
<!-- schema generated by tfplugindocs -->
34+
## Schema
35+
36+
### Required
37+
38+
- `builtin_role` (String) Organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin` to assign the roles to.
39+
- `roles` (Block Set, Min: 1) Fixed or custom roles which provide granular access for specific resources within Grafana. (see [below for nested schema](#nestedblock--roles))
40+
41+
### Read-Only
42+
43+
- `id` (String) The ID of this resource.
44+
45+
<a id="nestedblock--roles"></a>
46+
### Nested Schema for `roles`
47+
48+
Required:
49+
50+
- `uid` (String) Unique identifier of the role to assign to `builtin_role`.
51+
52+
Optional:
53+
54+
- `global` (Boolean) States whether the assignment is available across all organizations or not. Defaults to `false`.
55+
56+
## Import
57+
58+
Import is supported using the following syntax:
59+
60+
```shell
61+
terraform import grafana_builtin_role_assignment.builtin_role_name {{builtin_role_name}}
62+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
terraform import grafana_builtin_role_assignment.builtin_role_name {{builtin_role_name}}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
resource "grafana_builtin_role_assignment" "viewer" {
2+
builtin_role = "Viewer"
3+
roles {
4+
uid = "firstuid"
5+
global = false
6+
}
7+
roles {
8+
uid = "seconduid"
9+
global = true
10+
}
11+
}

grafana/provider.go

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,23 +160,24 @@ func Provider(version string) func() *schema.Provider {
160160

161161
ResourcesMap: map[string]*schema.Resource{
162162
// Grafana
163-
"grafana_api_key": ResourceAPIKey(),
164-
"grafana_alert_notification": ResourceAlertNotification(),
165-
"grafana_dashboard": ResourceDashboard(),
166-
"grafana_dashboard_permission": ResourceDashboardPermission(),
167-
"grafana_data_source": ResourceDataSource(),
168-
"grafana_data_source_permission": ResourceDatasourcePermission(),
169-
"grafana_folder": ResourceFolder(),
170-
"grafana_folder_permission": ResourceFolderPermission(),
171-
"grafana_library_panel": ResourceLibraryPanel(),
172-
"grafana_organization": ResourceOrganization(),
173-
"grafana_playlist": ResourcePlaylist(),
174-
"grafana_report": ResourceReport(),
175-
"grafana_role": ResourceRole(),
176-
"grafana_team": ResourceTeam(),
177-
"grafana_team_preferences": ResourceTeamPreferences(),
178-
"grafana_team_external_group": ResourceTeamExternalGroup(),
179-
"grafana_user": ResourceUser(),
163+
"grafana_api_key": ResourceAPIKey(),
164+
"grafana_alert_notification": ResourceAlertNotification(),
165+
"grafana_builtin_role_assignment": ResourceBuiltInRoleAssignment(),
166+
"grafana_dashboard": ResourceDashboard(),
167+
"grafana_dashboard_permission": ResourceDashboardPermission(),
168+
"grafana_data_source": ResourceDataSource(),
169+
"grafana_data_source_permission": ResourceDatasourcePermission(),
170+
"grafana_folder": ResourceFolder(),
171+
"grafana_folder_permission": ResourceFolderPermission(),
172+
"grafana_library_panel": ResourceLibraryPanel(),
173+
"grafana_organization": ResourceOrganization(),
174+
"grafana_playlist": ResourcePlaylist(),
175+
"grafana_report": ResourceReport(),
176+
"grafana_role": ResourceRole(),
177+
"grafana_team": ResourceTeam(),
178+
"grafana_team_preferences": ResourceTeamPreferences(),
179+
"grafana_team_external_group": ResourceTeamExternalGroup(),
180+
"grafana_user": ResourceUser(),
180181

181182
// Cloud
182183
"grafana_cloud_api_key": ResourceCloudAPIKey(),
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
11+
12+
gapi "github.com/grafana/grafana-api-golang-client"
13+
)
14+
15+
func ResourceBuiltInRoleAssignment() *schema.Resource {
16+
return &schema.Resource{
17+
Description: `
18+
**Note:** This resource is available only with Grafana Enterprise 8.+.
19+
20+
* [Official documentation](https://grafana.com/docs/grafana/latest/enterprise/access-control/)
21+
* [HTTP API](https://grafana.com/docs/grafana/latest/http_api/access_control/)
22+
`,
23+
CreateContext: CreateBuiltInRoleAssignment,
24+
UpdateContext: UpdateBuiltInRoleAssignments,
25+
ReadContext: ReadBuiltInRole,
26+
DeleteContext: DeleteBuiltInRole,
27+
Importer: &schema.ResourceImporter{
28+
StateContext: schema.ImportStatePassthroughContext,
29+
},
30+
Schema: map[string]*schema.Schema{
31+
// Built-in roles are all organization roles and Grafana Admin
32+
"builtin_role": {
33+
Type: schema.TypeString,
34+
Required: true,
35+
ForceNew: true,
36+
ValidateFunc: validation.StringInSlice([]string{"Grafana Admin", "Admin", "Editor", "Viewer"}, false),
37+
Description: "Organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin` to assign the roles to.",
38+
},
39+
"roles": {
40+
Type: schema.TypeSet,
41+
Required: true,
42+
Description: "Fixed or custom roles which provide granular access for specific resources within Grafana.",
43+
Elem: &schema.Resource{
44+
Schema: map[string]*schema.Schema{
45+
"uid": {
46+
Type: schema.TypeString,
47+
Required: true,
48+
ForceNew: true,
49+
Description: "Unique identifier of the role to assign to `builtin_role`.",
50+
},
51+
"global": {
52+
Type: schema.TypeBool,
53+
Optional: true,
54+
Default: false,
55+
ForceNew: true,
56+
Description: "States whether the assignment is available across all organizations or not.",
57+
},
58+
},
59+
},
60+
},
61+
},
62+
}
63+
}
64+
65+
func CreateBuiltInRoleAssignment(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
66+
name := d.Get("builtin_role").(string)
67+
if dg := updateAssignments(d, meta); dg != nil {
68+
return dg
69+
}
70+
d.SetId(name)
71+
return nil
72+
}
73+
74+
func updateAssignments(d *schema.ResourceData, meta interface{}) diag.Diagnostics {
75+
stateRoles, configRoles, err := collectRoles(d)
76+
if err != nil {
77+
return diag.FromErr(err)
78+
}
79+
// compile the list of differences between current state and config
80+
changes := roleChanges(stateRoles, configRoles)
81+
brName := d.Get("builtin_role").(string)
82+
// now we can make the corresponding updates so current state matches config
83+
if err := createOrRemove(meta, brName, changes); err != nil {
84+
return diag.FromErr(err)
85+
}
86+
return nil
87+
}
88+
89+
func ReadBuiltInRole(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
90+
client := meta.(*client).gapi
91+
brName := d.Id()
92+
builtInRoles, err := client.GetBuiltInRoleAssignments()
93+
94+
if err != nil {
95+
return diag.FromErr(err)
96+
}
97+
98+
brRole := builtInRoles[brName]
99+
if builtInRoles[brName] == nil {
100+
log.Printf("[WARN] removing built-in role %s from state because it no longer exists in grafana", d.Id())
101+
d.SetId("")
102+
return nil
103+
}
104+
105+
stateRoles, configRoles, err := collectRoles(d)
106+
if err != nil {
107+
return diag.FromErr(err)
108+
}
109+
roles := make([]interface{}, 0)
110+
for _, br := range brRole {
111+
// It is possible that in the server side there are roles assigned to the built-in role which were never in Terraform state,
112+
// and are not in the current configuration. The following check ensures that we only consider roles which are either in the state or in the config.
113+
// This prevents unintended behaviour, such as destroying the assignments which were never managed by Terraform.
114+
_, isInState := stateRoles[br.UID]
115+
_, isInConfig := configRoles[br.UID]
116+
if !isInState && !isInConfig {
117+
continue
118+
}
119+
rm := map[string]interface{}{
120+
"uid": br.UID,
121+
"global": br.Global,
122+
}
123+
roles = append(roles, rm)
124+
}
125+
126+
if err = d.Set("roles", roles); err != nil {
127+
return diag.FromErr(err)
128+
}
129+
130+
if err = d.Set("builtin_role", brName); err != nil {
131+
return diag.FromErr(err)
132+
}
133+
d.SetId(brName)
134+
return nil
135+
}
136+
137+
func UpdateBuiltInRoleAssignments(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
138+
if !d.HasChange("roles") {
139+
return nil
140+
}
141+
142+
if dg := updateAssignments(d, meta); dg != nil {
143+
return dg
144+
}
145+
146+
return nil
147+
}
148+
149+
func DeleteBuiltInRole(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
150+
client := meta.(*client).gapi
151+
152+
for _, r := range d.Get("roles").(*schema.Set).List() {
153+
role := r.(map[string]interface{})
154+
bra := gapi.BuiltInRoleAssignment{
155+
RoleUID: role["uid"].(string),
156+
BuiltinRole: d.Id(),
157+
Global: role["global"].(bool),
158+
}
159+
err := client.DeleteBuiltInRoleAssignment(bra)
160+
if err != nil {
161+
return diag.FromErr(err)
162+
}
163+
}
164+
d.SetId("")
165+
return nil
166+
}
167+
168+
type RoleChange struct {
169+
Type ChangeRoleType
170+
UID string
171+
Global bool
172+
}
173+
174+
type ChangeRoleType int8
175+
176+
const (
177+
AddRole ChangeRoleType = iota
178+
RemoveRole
179+
)
180+
181+
func roleChanges(rolesInState, rolesInConfig map[string]bool) []RoleChange {
182+
var changes []RoleChange
183+
for uid, g := range rolesInConfig {
184+
if _, ok := rolesInState[uid]; !ok {
185+
changes = append(changes, RoleChange{Type: AddRole, UID: uid, Global: g})
186+
}
187+
}
188+
for uid, g := range rolesInState {
189+
if _, ok := rolesInConfig[uid]; !ok {
190+
changes = append(changes, RoleChange{Type: RemoveRole, UID: uid, Global: g})
191+
}
192+
}
193+
return changes
194+
}
195+
196+
func collectRoles(d *schema.ResourceData) (map[string]bool, map[string]bool, error) {
197+
errFn := func(uid string) error {
198+
return fmt.Errorf("error: Role '%s' cannot be specified multiple times", uid)
199+
}
200+
201+
rolesFn := func(roles interface{}) (map[string]bool, error) {
202+
output := make(map[string]bool)
203+
for _, r := range roles.(*schema.Set).List() {
204+
role := r.(map[string]interface{})
205+
uid := role["uid"].(string)
206+
if _, ok := output[uid]; ok {
207+
return nil, errFn(uid)
208+
}
209+
output[uid] = role["global"].(bool)
210+
}
211+
return output, nil
212+
}
213+
214+
state, config := d.GetChange("roles")
215+
rolesInState, err := rolesFn(state)
216+
if err != nil {
217+
return nil, nil, err
218+
}
219+
rolesInConfig, err := rolesFn(config)
220+
if err != nil {
221+
return nil, nil, err
222+
}
223+
224+
return rolesInState, rolesInConfig, nil
225+
}
226+
227+
func createOrRemove(meta interface{}, name string, changes []RoleChange) error {
228+
client := meta.(*client).gapi
229+
var err error
230+
for _, c := range changes {
231+
br := gapi.BuiltInRoleAssignment{BuiltinRole: name, RoleUID: c.UID, Global: c.Global}
232+
switch c.Type {
233+
case AddRole:
234+
_, err = client.NewBuiltInRoleAssignment(br)
235+
case RemoveRole:
236+
err = client.DeleteBuiltInRoleAssignment(br)
237+
}
238+
if err != nil {
239+
return fmt.Errorf("error with %s %w", name, err)
240+
}
241+
}
242+
return nil
243+
}

0 commit comments

Comments
 (0)