Skip to content

Commit f487ec3

Browse files
IevaVasiljevalinomanjulienduchesnevtorosyan
authored
RBAC: resource for RBAC role assignments (#647)
* role assignment resource * clean up * remove possibility to do global assignments for now * remove possibility to do global assignments for now * Fix variable reference * Append `.vscode` and `vendor` to `.gitignore` * Fix typo * tests attempt 1 * set id correctly * add tests * change id * Update grafana/resource_role_assignment.go Co-authored-by: Julien Duchesne <[email protected]> * Update grafana/resource_role_assignment.go Co-authored-by: Vardan Torosyan <[email protected]> * fix IDs * update API client dependency * Generate docs * Read after update * try to update api client dependency * improve the test * attempt to refactor annotation test * uncomment Co-authored-by: linoman <[email protected]> Co-authored-by: Julien Duchesne <[email protected]> Co-authored-by: Vardan Torosyan <[email protected]>
1 parent 42573f3 commit f487ec3

File tree

9 files changed

+301
-16
lines changed

9 files changed

+301
-16
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ website/vendor
3030
!command/test-fixtures/**/*.tfstate
3131
!command/test-fixtures/**/.terraform/
3232

33-
terraform-provider-grafana
33+
terraform-provider-grafana
34+
35+
.vscode
36+
vendor/

docs/resources/role_assignment.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_role_assignment Resource - terraform-provider-grafana"
4+
subcategory: "Grafana Enteprise"
5+
description: |-
6+
Note: This resource is available only with Grafana Enterprise 9.2+.
7+
* Official documentation https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/
8+
* HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/access_control/
9+
---
10+
11+
# grafana_role_assignment (Resource)
12+
13+
**Note:** This resource is available only with Grafana Enterprise 9.2+.
14+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/)
15+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/access_control/)
16+
17+
18+
19+
<!-- schema generated by tfplugindocs -->
20+
## Schema
21+
22+
### Required
23+
24+
- `role_uid` (String) Grafana RBAC role UID.
25+
26+
### Optional
27+
28+
- `service_accounts` (Set of Number) IDs of service accounts that the role should be assigned to.
29+
- `teams` (Set of Number) IDs of teams that the role should be assigned to.
30+
- `users` (Set of Number) IDs of users that the role should be assigned to.
31+
32+
### Read-Only
33+
34+
- `id` (String) The ID of this resource.
35+
36+

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.18
55
require (
66
github.com/Masterminds/semver/v3 v3.1.1
77
github.com/grafana/amixr-api-go-client v0.0.5
8-
github.com/grafana/grafana-api-golang-client v0.11.2
8+
github.com/grafana/grafana-api-golang-client v0.12.0
99
github.com/grafana/machine-learning-go-client v0.1.1
1010
github.com/grafana/synthetic-monitoring-agent v0.9.4
1111
github.com/grafana/synthetic-monitoring-api-go-client v0.6.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
115115
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
116116
github.com/grafana/amixr-api-go-client v0.0.5 h1:jqmljnd5FozuOsCNuyhZVpooxmj0BW9MmeLA7PaLK6U=
117117
github.com/grafana/amixr-api-go-client v0.0.5/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
118-
github.com/grafana/grafana-api-golang-client v0.11.2 h1:wzT/TfaPWgOA0xHJ7fdIgmcnyXJLgMgbU62zbbUmIkI=
119-
github.com/grafana/grafana-api-golang-client v0.11.2/go.mod h1:24W29gPe9yl0/3A9X624TPkAOR8DpHno490cPwnkv8E=
118+
github.com/grafana/grafana-api-golang-client v0.12.0 h1:EqoBKAeDeWQdb9PXrTc7vvVMzJTLTFYR+MbbEJhua+o=
119+
github.com/grafana/grafana-api-golang-client v0.12.0/go.mod h1:24W29gPe9yl0/3A9X624TPkAOR8DpHno490cPwnkv8E=
120120
github.com/grafana/machine-learning-go-client v0.1.1 h1:Gw6cX8xAd6IVF2LApkXOIdBK8Gzz07B3jQPukecw7fc=
121121
github.com/grafana/machine-learning-go-client v0.1.1/go.mod h1:QFfZz8NkqVF8++skjkKQXJEZfpCYd8S0yTWJUpsLLTA=
122122
github.com/grafana/synthetic-monitoring-agent v0.9.4 h1:Enx5s6gFbc/RAzL5KDX/00catAlbcY7/1IFPBe5lo/c=

grafana/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func Provider(version string) func() *schema.Provider {
6666
"grafana_playlist": ResourcePlaylist(),
6767
"grafana_report": ResourceReport(),
6868
"grafana_role": ResourceRole(),
69+
"grafana_role_assignment": ResourceRoleAssignment(),
6970
"grafana_rule_group": ResourceRuleGroup(),
7071
"grafana_team": ResourceTeam(),
7172
"grafana_team_preferences": ResourceTeamPreferences(),

grafana/resource_annotation_test.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -164,31 +164,24 @@ resource "grafana_annotation" "test_with_dashboard_id" {
164164
}
165165

166166
func testAnnotationConfigWithPanelID(text string) string {
167+
panelID := 123
168+
167169
return fmt.Sprintf(`
168170
resource "grafana_dashboard" "test_with_panel_id" {
169171
config_json = <<EOD
170172
{
171173
"title": "%s",
172174
"panels": [{
173175
"name": "%s",
174-
"id": 123
176+
"id": %d
175177
}]
176178
}
177179
EOD
178180
}
179181
180-
data "grafana_dashboard" "test_with_panel_id" {
181-
dashboard_id = grafana_dashboard.test_with_panel_id.dashboard_id
182-
}
183-
184-
locals {
185-
dashboard_json = jsondecode(data.grafana_dashboard.test_with_panel_id.config_json)
186-
panel_id = local.dashboard_json.panels[0].id
187-
}
188-
189182
resource "grafana_annotation" "test_with_panel_id" {
190183
text = "%s"
191-
panel_id = local.panel_id
184+
panel_id = %d
192185
}
193-
`, text, text, text)
186+
`, text, text, panelID, text, panelID)
194187
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
gapi "github.com/grafana/grafana-api-golang-client"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func ResourceRoleAssignment() *schema.Resource {
13+
return &schema.Resource{
14+
Description: `
15+
**Note:** This resource is available only with Grafana Enterprise 9.2+.
16+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/)
17+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/access_control/)
18+
`,
19+
CreateContext: UpdateRoleAssignments,
20+
UpdateContext: UpdateRoleAssignments,
21+
ReadContext: ReadRoleAssignments,
22+
DeleteContext: UpdateRoleAssignments,
23+
Importer: &schema.ResourceImporter{
24+
StateContext: schema.ImportStatePassthroughContext,
25+
},
26+
Schema: map[string]*schema.Schema{
27+
"role_uid": {
28+
Type: schema.TypeString,
29+
Required: true,
30+
ForceNew: true,
31+
Description: "Grafana RBAC role UID.",
32+
},
33+
"users": {
34+
Type: schema.TypeSet,
35+
Optional: true,
36+
ForceNew: false,
37+
Description: "IDs of users that the role should be assigned to.",
38+
Elem: &schema.Schema{
39+
Type: schema.TypeInt,
40+
},
41+
},
42+
"teams": {
43+
Type: schema.TypeSet,
44+
Optional: true,
45+
ForceNew: false,
46+
Description: "IDs of teams that the role should be assigned to.",
47+
Elem: &schema.Schema{
48+
Type: schema.TypeInt,
49+
},
50+
},
51+
"service_accounts": {
52+
Type: schema.TypeSet,
53+
Optional: true,
54+
ForceNew: false,
55+
Description: "IDs of service accounts that the role should be assigned to.",
56+
Elem: &schema.Schema{
57+
Type: schema.TypeInt,
58+
},
59+
},
60+
},
61+
}
62+
}
63+
64+
func ReadRoleAssignments(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
65+
client := meta.(*client).gapi
66+
uid := d.Id()
67+
assignments, err := client.GetRoleAssignments(uid)
68+
if err != nil {
69+
return diag.FromErr(err)
70+
}
71+
72+
if err := setRoleAssignments(assignments, d); err != nil {
73+
return diag.FromErr(err)
74+
}
75+
return nil
76+
}
77+
78+
func UpdateRoleAssignments(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
79+
if !d.IsNewResource() && !d.HasChange("users") && !d.HasChange("teams") && !d.HasChange("service_accounts") {
80+
return nil
81+
}
82+
83+
client := meta.(*client).gapi
84+
85+
uid := d.Get("role_uid").(string)
86+
users, err := collectRoleAssignmentsToFn(d.Get("users"))
87+
if err != nil {
88+
return diag.Errorf("invalid user IDs specified %v", err)
89+
}
90+
teams, err := collectRoleAssignmentsToFn(d.Get("teams"))
91+
if err != nil {
92+
return diag.Errorf("invalid team IDs specified %v", err)
93+
}
94+
serviceAccounts, err := collectRoleAssignmentsToFn(d.Get("service_accounts"))
95+
if err != nil {
96+
return diag.Errorf("invalid service account IDs specified %v", err)
97+
}
98+
99+
ra := &gapi.RoleAssignments{
100+
RoleUID: uid,
101+
Users: users,
102+
Teams: teams,
103+
ServiceAccounts: serviceAccounts,
104+
}
105+
assignments, err := client.UpdateRoleAssignments(ra)
106+
if err != nil {
107+
return diag.FromErr(err)
108+
}
109+
110+
if err := setRoleAssignments(assignments, d); err != nil {
111+
return diag.FromErr(err)
112+
}
113+
114+
return ReadRoleAssignments(ctx, d, meta)
115+
}
116+
117+
func setRoleAssignments(assignments *gapi.RoleAssignments, d *schema.ResourceData) error {
118+
d.SetId(assignments.RoleUID)
119+
if err := d.Set("role_uid", assignments.RoleUID); err != nil {
120+
return err
121+
}
122+
if err := d.Set("users", assignments.Users); err != nil {
123+
return err
124+
}
125+
if err := d.Set("teams", assignments.Teams); err != nil {
126+
return err
127+
}
128+
if err := d.Set("service_accounts", assignments.ServiceAccounts); err != nil {
129+
return err
130+
}
131+
132+
return nil
133+
}
134+
135+
func collectRoleAssignmentsToFn(r interface{}) ([]int, error) {
136+
output := make([]int, 0)
137+
for _, rID := range r.(*schema.Set).List() {
138+
id, ok := rID.(int)
139+
if !ok {
140+
return []int{}, fmt.Errorf("%s is not a valid id", rID)
141+
}
142+
output = append(output, id)
143+
}
144+
return output, nil
145+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package grafana
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
9+
10+
gapi "github.com/grafana/grafana-api-golang-client"
11+
)
12+
13+
func TestRoleAssignments(t *testing.T) {
14+
CheckEnterpriseTestsEnabled(t)
15+
var roleAssignment gapi.RoleAssignments
16+
17+
resource.Test(t, resource.TestCase{
18+
ProviderFactories: testAccProviderFactories,
19+
CheckDestroy: testRoleAssignmentCheckDestroy(&roleAssignment),
20+
Steps: []resource.TestStep{
21+
{
22+
Config: fmt.Sprintf(roleAssignmentConfig, roleUID),
23+
Check: resource.ComposeTestCheckFunc(
24+
testRoleAssignmentCheckExists("grafana_role_assignment.test", &roleAssignment),
25+
resource.TestCheckResourceAttr(
26+
"grafana_role_assignment.test", "role_uid", roleUID,
27+
),
28+
resource.TestCheckResourceAttr(
29+
"grafana_role_assignment.test", "users.#", "2",
30+
),
31+
resource.TestCheckResourceAttr(
32+
"grafana_role_assignment.test", "service_accounts.#", "0",
33+
),
34+
resource.TestCheckResourceAttr(
35+
"grafana_role_assignment.test", "teams.#", "1",
36+
),
37+
),
38+
},
39+
{
40+
Config: fmt.Sprintf(roleAssignmentConfig, roleUID),
41+
Destroy: true,
42+
},
43+
},
44+
})
45+
}
46+
47+
func testRoleAssignmentCheckExists(rn string, ra *gapi.RoleAssignments) resource.TestCheckFunc {
48+
return func(s *terraform.State) error {
49+
rs, ok := s.RootModule().Resources[rn]
50+
if !ok {
51+
return fmt.Errorf("resource not found: %s", rn)
52+
}
53+
54+
uid, ok := rs.Primary.Attributes["role_uid"]
55+
if !ok {
56+
return fmt.Errorf("resource UID not set")
57+
}
58+
59+
client := testAccProvider.Meta().(*client).gapi
60+
role, err := client.GetRoleAssignments(uid)
61+
if err != nil {
62+
return fmt.Errorf("error getting role assignments: %s", err)
63+
}
64+
65+
*ra = *role
66+
67+
return nil
68+
}
69+
}
70+
71+
func testRoleAssignmentCheckDestroy(ra *gapi.RoleAssignments) resource.TestCheckFunc {
72+
return func(s *terraform.State) error {
73+
client := testAccProvider.Meta().(*client).gapi
74+
role, err := client.GetRoleAssignments(ra.RoleUID)
75+
if err == nil && (len(role.Users) > 0 || len(role.ServiceAccounts) > 0 || len(role.Teams) > 0) {
76+
return fmt.Errorf("role is still assigned")
77+
}
78+
return nil
79+
}
80+
}
81+
82+
var roleUID = "terraform_test_role"
83+
84+
var roleAssignmentConfig = `
85+
resource "grafana_team" "test_team" {
86+
name = "terraform_test_team"
87+
}
88+
89+
resource "grafana_user" "test_user" {
90+
91+
92+
password = "12345"
93+
}
94+
95+
resource "grafana_user" "test_user2" {
96+
97+
98+
password = "12345"
99+
}
100+
101+
resource "grafana_role_assignment" "test" {
102+
role_uid = "%s"
103+
users = [grafana_user.test_user.id, grafana_user.test_user2.id]
104+
teams = [grafana_team.test_team.id]
105+
}
106+
`

tools/subcategories.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"resources/folder_permission": "Grafana Enteprise",
2626
"resources/report": "Grafana Enteprise",
2727
"resources/role": "Grafana Enteprise",
28+
"resources/role_assignment": "Grafana Enteprise",
2829
"resources/team_external_group": "Grafana Enteprise",
2930
"resources/cloud_api_key": "Cloud",
3031
"resources/cloud_plugin_installation": "Cloud",

0 commit comments

Comments
 (0)