/
resource_okta_resource_set.go
225 lines (213 loc) · 7.52 KB
/
resource_okta_resource_set.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
package okta
import (
"context"
"errors"
"fmt"
"net/url"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/okta/terraform-provider-okta/sdk"
"github.com/okta/terraform-provider-okta/sdk/query"
)
func resourceResourceSet() *schema.Resource {
return &schema.Resource{
CreateContext: resourceResourceSetCreate,
ReadContext: resourceResourceSetRead,
UpdateContext: resourceResourceSetUpdate,
DeleteContext: resourceResourceSetDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Description: `Manages Resource Sets as custom collections of resources. This resource allows the creation and manipulation of Okta Resource Sets as custom collections of Okta resources. You can use Okta Resource Sets to assign Custom Roles to administrators who are scoped to the designated resources.
The 'resources' field supports the following:
- Apps
- Groups
- All Users within a Group
- All Users within the org
- All Groups within the org
- All Apps within the org
- All Apps of the same type`,
Schema: map[string]*schema.Schema{
"label": {
Type: schema.TypeString,
Required: true,
Description: "Unique name given to the Resource Set",
},
"description": {
Type: schema.TypeString,
Required: true,
Description: "A description of the Resource Set",
},
"resources": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: "The endpoints that reference the resources to be included in the new Resource Set. At least one endpoint must be specified when creating resource set.",
},
},
}
}
func resourceResourceSetCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
set, err := buildResourceSet(d, true)
if err != nil {
return diag.Errorf("failed to create resource set: %v", err)
}
rs, _, err := getAPISupplementFromMetadata(m).CreateResourceSet(ctx, *set)
if err != nil {
return diag.Errorf("failed to create resource set: %v", err)
}
d.SetId(rs.Id)
return resourceResourceSetRead(ctx, d, m)
}
func resourceResourceSetRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
rs, resp, err := getAPISupplementFromMetadata(m).GetResourceSet(ctx, d.Id())
if err := suppressErrorOn404(resp, err); err != nil {
return diag.Errorf("failed to get resource set: %v", err)
}
if rs == nil {
d.SetId("")
return nil
}
_ = d.Set("label", rs.Label)
_ = d.Set("description", rs.Description)
resources, err := listResourceSetResources(ctx, getAPISupplementFromMetadata(m), d.Id())
if err != nil {
return diag.Errorf("failed to get list of resource set resources: %v", err)
}
_ = d.Set("resources", flattenResourceSetResources(resources))
return nil
}
func resourceResourceSetUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := getAPISupplementFromMetadata(m)
if d.HasChanges("label", "description") {
set, _ := buildResourceSet(d, false)
_, _, err := client.UpdateResourceSet(ctx, d.Id(), *set)
if err != nil {
return diag.Errorf("failed to update resource set: %v", err)
}
}
if !d.HasChange("resources") {
return nil
}
oldResources, newResources := d.GetChange("resources")
oldSet := oldResources.(*schema.Set)
newSet := newResources.(*schema.Set)
resourcesToAdd := convertInterfaceArrToStringArr(newSet.Difference(oldSet).List())
resourcesToRemove := convertInterfaceArrToStringArr(oldSet.Difference(newSet).List())
err := addResourcesToResourceSet(ctx, client, d.Id(), resourcesToAdd)
if err != nil {
return diag.FromErr(err)
}
err = removeResourcesFromResourceSet(ctx, client, d.Id(), resourcesToRemove)
if err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceResourceSetDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
resp, err := getAPISupplementFromMetadata(m).DeleteResourceSet(ctx, d.Id())
if err := suppressErrorOn404(resp, err); err != nil {
return diag.Errorf("failed to delete resource set: %v", err)
}
return nil
}
func buildResourceSet(d *schema.ResourceData, isNew bool) (*sdk.ResourceSet, error) {
rs := &sdk.ResourceSet{
Label: d.Get("label").(string),
Description: d.Get("description").(string),
}
if isNew {
rs.Resources = convertInterfaceToStringSetNullable(d.Get("resources"))
if len(rs.Resources) == 0 {
return nil, errors.New("at least one resource must be specified when creating resource set")
}
} else {
rs.Id = d.Id()
}
return rs, nil
}
func flattenResourceSetResources(resources []*sdk.ResourceSetResource) *schema.Set {
var arr []interface{}
for _, res := range resources {
if res.Links != nil {
links := res.Links.(map[string]interface{})
var url string
for _, v := range links {
for _, link := range v.(map[string]interface{}) {
url = link.(string)
break
}
}
arr = append(arr, url)
}
}
return schema.NewSet(schema.HashString, arr)
}
func listResourceSetResources(ctx context.Context, client *sdk.APISupplement, id string) ([]*sdk.ResourceSetResource, error) {
var resResources []*sdk.ResourceSetResource
resources, _, err := client.ListResourceSetResources(ctx, id, &query.Params{Limit: defaultPaginationLimit})
if err != nil {
return nil, err
}
resResources = append(resResources, resources.Resources...)
for {
// NOTE: The resources endpoint /api/v1/iam/resource-sets/%s/resources
// is not returning pagination in the headers. Make use of the _links
// object in the response body. Convert implemenation style back to
// resp.HasNextPage() if/when that endpoint starts to have pagination
// information in its headers and/or when this code is supported by
// okta-sdk-golang instead of the local SDK.
if nextURL := linksValue(resources.Links, "next", "href"); nextURL != "" {
u, err := url.Parse(nextURL)
if err != nil {
break
}
// "links": { "next": { "href": "https://host/api/v1/iam/resource-sets/{id}/resources?after={afterId}&limit=100" } }
after := u.Query().Get("after")
resources, _, err = client.ListResourceSetResources(ctx, id, &query.Params{After: after, Limit: defaultPaginationLimit})
if err != nil {
return nil, err
}
resResources = append(resResources, resources.Resources...)
} else {
break
}
}
return resResources, nil
}
func addResourcesToResourceSet(ctx context.Context, client *sdk.APISupplement, resourceSetID string, links []string) error {
if len(links) == 0 {
return nil
}
_, err := client.AddResourceSetResources(ctx, resourceSetID, sdk.AddResourceSetResourcesRequest{Additions: links})
if err != nil {
return fmt.Errorf("failed to add resources to the resource set: %v", err)
}
return nil
}
func removeResourcesFromResourceSet(ctx context.Context, client *sdk.APISupplement, resourceSetID string, urls []string) error {
resources, err := listResourceSetResources(ctx, client, resourceSetID)
if err != nil {
return fmt.Errorf("failed to get list of resource set resources: %v", err)
}
for _, res := range resources {
if res.Links == nil {
continue
}
links := res.Links.(map[string]interface{})
var url string
for _, v := range links {
for _, link := range v.(map[string]interface{}) {
url = link.(string)
break
}
}
if contains(urls, url) {
_, err := client.DeleteResourceSetResource(ctx, resourceSetID, res.Id)
if err != nil {
return fmt.Errorf("failed to remove %s resource from the resource set: %v", res.Id, err)
}
}
}
return nil
}