Skip to content

Commit 6703184

Browse files
saidsttmeckel
andauthored
WIP: Authorization of resources on project-level (#235)
* added resource authorization for service endpoints. upgraded to terraform-plugin-sdk-1.6.0. * Updated documentation and example. * Removed excessive reference to github.com/Azure/go-autorest/autorest/to from azuredevops/resource_resource_authorization.go * Removed excessive reference to github.com/Azure/go-autorest/autorest/to from azuredevops/resource_resource_authorization_test.go * tidy up go.mod after cherry-picking and rebasing. * externalize error strings to reduce redundancy. * fix linting issues. Co-authored-by: Thomas Meckel <tmeckel@users.noreply.github.com>
1 parent 4064813 commit 6703184

File tree

9 files changed

+385
-17
lines changed

9 files changed

+385
-17
lines changed

azuredevops/crud/serviceendpoint/crud_service_endpoint.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,12 @@ func genServiceEndpointReadFunc(flatFunc flatFunc) func(d *schema.ResourceData,
223223
return fmt.Errorf("Error looking up service endpoint given ID (%v) and project ID (%v): %v", serviceEndpointID, projectID, err)
224224
}
225225

226-
flatFunc(d, serviceEndpoint, projectID)
226+
if serviceEndpoint.Id == nil {
227+
// e.g. service endpoint has been deleted separately without TF
228+
d.SetId("")
229+
} else {
230+
flatFunc(d, serviceEndpoint, projectID)
231+
}
227232
return nil
228233
}
229234
}

azuredevops/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
func Provider() *schema.Provider {
1010
p := &schema.Provider{
1111
ResourcesMap: map[string]*schema.Resource{
12+
"azuredevops_resource_authorization": resourceResourceAuthorization(),
1213
"azuredevops_build_definition": resourceBuildDefinition(),
1314
"azuredevops_project": resourceProject(),
1415
"azuredevops_variable_group": resourceVariableGroup(),

azuredevops/provider_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var provider = Provider()
1515

1616
func TestAzureDevOpsProvider_HasChildResources(t *testing.T) {
1717
expectedResources := []string{
18+
"azuredevops_resource_authorization",
1819
"azuredevops_build_definition",
1920
"azuredevops_project",
2021
"azuredevops_serviceendpoint_github",
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package azuredevops
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
8+
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
9+
"github.com/microsoft/azure-devops-go-api/azuredevops/build"
10+
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/config"
11+
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/converter"
12+
)
13+
14+
const msgErrorFailedResourceCreate = "error creating authorized resource: %+v"
15+
const msgErrorFailedResourceUpdate = "error updating authorized resource: %+v"
16+
const msgErrorFailedResourceDelete = "error deleting authorized resource: %+v"
17+
18+
func resourceResourceAuthorization() *schema.Resource {
19+
return &schema.Resource{
20+
Create: resourceResourceAuthorizationCreate,
21+
Read: resourceResourceAuthorizationRead,
22+
Update: resourceResourceAuthorizationUpdate,
23+
Delete: resourceResourceAuthorizationDelete,
24+
25+
Schema: map[string]*schema.Schema{
26+
"project_id": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
ForceNew: true,
30+
},
31+
"resource_id": {
32+
Type: schema.TypeString,
33+
Optional: true,
34+
Description: "id of the resource",
35+
ValidateFunc: validation.IsUUID,
36+
},
37+
"type": {
38+
Type: schema.TypeString,
39+
Optional: true,
40+
Default: "endpoint",
41+
Description: "type of the resource",
42+
ValidateFunc: validation.StringInSlice([]string{"endpoint"}, false),
43+
},
44+
"authorized": {
45+
Type: schema.TypeBool,
46+
Required: true,
47+
Description: "indicates whether the resource is authorized for use",
48+
},
49+
},
50+
}
51+
}
52+
53+
func resourceResourceAuthorizationCreate(d *schema.ResourceData, m interface{}) error {
54+
clients := m.(*config.AggregatedClient)
55+
authorizedResource, projectID, err := expandAuthorizedResource(d)
56+
if err != nil {
57+
return fmt.Errorf(msgErrorFailedResourceCreate, err)
58+
}
59+
60+
_, err = sendAuthorizedResourceToAPI(clients, authorizedResource, projectID)
61+
if err != nil {
62+
return fmt.Errorf(msgErrorFailedResourceCreate, err)
63+
}
64+
65+
return resourceResourceAuthorizationRead(d, m)
66+
}
67+
68+
func resourceResourceAuthorizationRead(d *schema.ResourceData, m interface{}) error {
69+
ctx := context.Background()
70+
clients := m.(*config.AggregatedClient)
71+
72+
authorizedResource, projectID, err := expandAuthorizedResource(d)
73+
if err != nil {
74+
return err
75+
}
76+
77+
if !*authorizedResource.Authorized {
78+
// flatten structure provided by user-configuration and not read from ado
79+
flattenAuthorizedResource(d, authorizedResource, projectID)
80+
} else {
81+
// (attempt) flatten read result from ado
82+
resourceRefs, err := clients.BuildClient.GetProjectResources(ctx, build.GetProjectResourcesArgs{
83+
Project: &projectID,
84+
Type: authorizedResource.Type,
85+
Id: authorizedResource.Id,
86+
})
87+
88+
if err != nil {
89+
return err
90+
}
91+
92+
// the authorization does no longer exist
93+
if len(*resourceRefs) == 0 {
94+
d.SetId("")
95+
return nil
96+
}
97+
98+
flattenAuthorizedResource(d, &(*resourceRefs)[0], projectID)
99+
return nil
100+
}
101+
return nil
102+
}
103+
104+
func resourceResourceAuthorizationDelete(d *schema.ResourceData, m interface{}) error {
105+
clients := m.(*config.AggregatedClient)
106+
authorizedResource, projectID, err := expandAuthorizedResource(d)
107+
if err != nil {
108+
return fmt.Errorf(msgErrorFailedResourceDelete, err)
109+
}
110+
111+
// deletion works only by setting authorized to false
112+
// because the resource to delete might have had this parameter set to true, we overwrite it
113+
authorizedResource.Authorized = converter.Bool(false)
114+
115+
_, err = sendAuthorizedResourceToAPI(clients, authorizedResource, projectID)
116+
if err != nil {
117+
return fmt.Errorf(msgErrorFailedResourceDelete, err)
118+
}
119+
120+
return err
121+
}
122+
123+
func resourceResourceAuthorizationUpdate(d *schema.ResourceData, m interface{}) error {
124+
clients := m.(*config.AggregatedClient)
125+
authorizedResource, projectID, err := expandAuthorizedResource(d)
126+
if err != nil {
127+
return fmt.Errorf(msgErrorFailedResourceUpdate, err)
128+
}
129+
130+
_, err = sendAuthorizedResourceToAPI(clients, authorizedResource, projectID)
131+
if err != nil {
132+
return fmt.Errorf(msgErrorFailedResourceUpdate, err)
133+
}
134+
135+
return resourceResourceAuthorizationRead(d, m)
136+
}
137+
138+
func flattenAuthorizedResource(d *schema.ResourceData, authorizedResource *build.DefinitionResourceReference, projectID string) {
139+
d.SetId(*authorizedResource.Id)
140+
d.Set("resource_id", *authorizedResource.Id)
141+
d.Set("type", *authorizedResource.Type)
142+
d.Set("authorized", *authorizedResource.Authorized)
143+
d.Set("project_id", projectID)
144+
}
145+
146+
func expandAuthorizedResource(d *schema.ResourceData) (*build.DefinitionResourceReference, string, error) {
147+
resourceRef := build.DefinitionResourceReference{
148+
Authorized: converter.Bool(d.Get("authorized").(bool)),
149+
Id: converter.String(d.Get("resource_id").(string)),
150+
Name: nil,
151+
Type: converter.String(d.Get("type").(string)),
152+
}
153+
154+
return &resourceRef, d.Get("project_id").(string), nil
155+
}
156+
157+
func sendAuthorizedResourceToAPI(clients *config.AggregatedClient, resourceRef *build.DefinitionResourceReference, project string) (*build.DefinitionResourceReference, error) {
158+
ctx := context.Background()
159+
160+
createdResourceRefs, err := clients.BuildClient.AuthorizeProjectResources(ctx, build.AuthorizeProjectResourcesArgs{
161+
Resources: &[]build.DefinitionResourceReference{*resourceRef},
162+
Project: &project,
163+
})
164+
165+
if err != nil {
166+
return nil, err
167+
} else if len(*createdResourceRefs) == 0 {
168+
return nil, fmt.Errorf("no project resources have been authorized")
169+
}
170+
171+
return &(*createdResourceRefs)[0], err
172+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// +build all resource_resource_authorization
2+
3+
package azuredevops
4+
5+
// The tests in this file use the mock clients in mock_client.go to mock out
6+
// the Azure DevOps client operations.
7+
8+
import (
9+
"context"
10+
"errors"
11+
"testing"
12+
13+
"github.com/microsoft/terraform-provider-azuredevops/azdosdkmocks"
14+
15+
"github.com/golang/mock/gomock"
16+
"github.com/google/uuid"
17+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
18+
"github.com/microsoft/azure-devops-go-api/azuredevops/build"
19+
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/config"
20+
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/converter"
21+
"github.com/stretchr/testify/require"
22+
)
23+
24+
var projectId = "projectid"
25+
var endpointId = uuid.New()
26+
27+
var resourceReferenceAuthorized = build.DefinitionResourceReference{
28+
Authorized: converter.Bool(true),
29+
Id: converter.String(endpointId.String()),
30+
Name: nil,
31+
Type: converter.String("endpoint"),
32+
}
33+
34+
var resourceReferenceNotAuthorized = build.DefinitionResourceReference{
35+
Authorized: converter.Bool(false),
36+
Id: converter.String(endpointId.String()),
37+
Name: nil,
38+
Type: converter.String("endpoint"),
39+
}
40+
41+
func init() {
42+
InitProvider()
43+
}
44+
45+
/**
46+
* Begin unit tests
47+
*/
48+
49+
func TestAzureDevOpsResourceAuthorization_FlattenExpand_RoundTripTestAzureDevOpsResourceAuthorization_FlattenExpand_RoundTrip(t *testing.T) {
50+
resourceData := schema.TestResourceDataRaw(t, resourceResourceAuthorization().Schema, nil)
51+
flattenAuthorizedResource(resourceData, &resourceReferenceAuthorized, projectId)
52+
53+
resourceReferenceAfterRoundtrip, projectIdAfterRoundtrip, err := expandAuthorizedResource(resourceData)
54+
require.Nil(t, err)
55+
require.Equal(t, resourceReferenceAuthorized, *resourceReferenceAfterRoundtrip)
56+
require.Equal(t, projectId, projectIdAfterRoundtrip)
57+
}
58+
59+
func TestAzureDevOpsResourceAuthorization_Create_DoesNotSwallowError(t *testing.T) {
60+
ctrl := gomock.NewController(t)
61+
defer ctrl.Finish()
62+
63+
r, resourceData, clients := prepareForCreateOrUpdate(t, ctrl, "CreateResourceAuthorization() Failed")
64+
65+
err := r.Create(resourceData, clients)
66+
require.Contains(t, err.Error(), "CreateResourceAuthorization() Failed")
67+
}
68+
69+
func TestAzureDevOpsResourceAuthorization_Update_DoesNotSwallowError(t *testing.T) {
70+
ctrl := gomock.NewController(t)
71+
defer ctrl.Finish()
72+
73+
r, resourceData, clients := prepareForCreateOrUpdate(t, ctrl, "UpdateResourceAuthorization() Failed")
74+
75+
err := r.Update(resourceData, clients)
76+
require.Contains(t, err.Error(), "UpdateResourceAuthorization() Failed")
77+
}
78+
79+
func prepareForCreateOrUpdate(t *testing.T, ctrl *gomock.Controller, expectedMessage string) (*schema.Resource, *schema.ResourceData, *config.AggregatedClient) {
80+
r := resourceResourceAuthorization()
81+
resourceData := schema.TestResourceDataRaw(t, r.Schema, nil)
82+
flattenAuthorizedResource(resourceData, &resourceReferenceAuthorized, projectId)
83+
84+
buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
85+
clients := &config.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
86+
87+
expectedArgs := build.AuthorizeProjectResourcesArgs{
88+
Resources: &[]build.DefinitionResourceReference{resourceReferenceAuthorized},
89+
Project: &projectId,
90+
}
91+
buildClient.
92+
EXPECT().
93+
AuthorizeProjectResources(clients.Ctx, expectedArgs).
94+
Return(nil, errors.New(expectedMessage)).
95+
Times(1)
96+
return r, resourceData, clients
97+
}
98+
99+
func TestAzureDevOpsResourceAuthorization_Read_DoesNotSwallowError(t *testing.T) {
100+
ctrl := gomock.NewController(t)
101+
defer ctrl.Finish()
102+
103+
r := resourceResourceAuthorization()
104+
resourceData := schema.TestResourceDataRaw(t, r.Schema, nil)
105+
flattenAuthorizedResource(resourceData, &resourceReferenceAuthorized, projectId)
106+
107+
buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
108+
clients := &config.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
109+
110+
expectedArgs := build.GetProjectResourcesArgs{
111+
Project: &projectId,
112+
Type: resourceReferenceAuthorized.Type,
113+
Id: resourceReferenceAuthorized.Id,
114+
}
115+
buildClient.
116+
EXPECT().
117+
GetProjectResources(clients.Ctx, expectedArgs).
118+
Return(nil, errors.New("ReadResourceAuthorization() Failed")).
119+
Times(1)
120+
121+
err := r.Read(resourceData, clients)
122+
require.Contains(t, err.Error(), "ReadResourceAuthorization() Failed")
123+
}
124+
125+
func TestAzureDevOpsResourceAuthorization_Delete_DoesNotSwallowError(t *testing.T) {
126+
ctrl := gomock.NewController(t)
127+
defer ctrl.Finish()
128+
129+
r := resourceResourceAuthorization()
130+
resourceData := schema.TestResourceDataRaw(t, r.Schema, nil)
131+
flattenAuthorizedResource(resourceData, &resourceReferenceNotAuthorized, projectId)
132+
133+
buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
134+
clients := &config.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
135+
136+
expectedArgs := build.AuthorizeProjectResourcesArgs{
137+
Resources: &[]build.DefinitionResourceReference{resourceReferenceNotAuthorized},
138+
Project: &projectId,
139+
}
140+
buildClient.
141+
EXPECT().
142+
AuthorizeProjectResources(clients.Ctx, expectedArgs).
143+
Return(nil, errors.New("DeleteResourceAuthorization() Failed")).
144+
Times(1)
145+
146+
err := r.Delete(resourceData, clients)
147+
require.Contains(t, err.Error(), "DeleteResourceAuthorization() Failed")
148+
}

examples/azdo-based-cicd/main.tf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ resource "azuredevops_serviceendpoint_azurerm" "endpoint1" {
9696
azurerm_scope = "/subscriptions/1da42ac9-xxxx-xxxxx-xxxx-xxxxxxxxxxx"
9797
}
9898

99+
resource "azuredevops_serviceendpoint_bitbucket" "bitbucket_account" {
100+
project_id = "vanilla-sky"
101+
username = "xxxx"
102+
password = "xxxx"
103+
service_endpoint_name = "test-bitbucket"
104+
description = "test"
105+
}
106+
107+
resource "azuredevops_resource_authorization" "bitbucket_account_authorization" {
108+
project_id = azuredevops_project.project.id
109+
resource_id = azuredevops_serviceendpoint_bitbucket.bitbucket_account.id
110+
authorized = true
111+
}
112+
99113
#
100114
# https://github.com/microsoft/terraform-provider-azuredevops/issues/83
101115
# resource "azuredevops_policy_build" "p1" {

go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ require (
1111
github.com/hashicorp/terraform-plugin-sdk v1.8.0
1212
github.com/microsoft/azure-devops-go-api/azuredevops v0.0.0-20200327121006-543de4815ec2
1313
github.com/stretchr/testify v1.3.0
14-
github.com/yuin/goldmark v1.1.30 // indirect
1514
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc
1615
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
1716
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect
18-
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect
1917
golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect
20-
golang.org/x/tools v0.0.0-20200428211048-dbf5ce1eac26 // indirect
18+
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 // indirect
2119
)

0 commit comments

Comments
 (0)