Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/build definition permissions #254

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// +build all permissions resource_build_definition_permissions
// +build !exclude_permissions !exclude_resource_build_definition_permissions

package acceptancetests

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/acceptancetests/testutils"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/datahelper"
)

func hclBuildDefinitionPermissions(projectName string, permissions map[string]string) string {
rootPermissions := datahelper.JoinMap(permissions, "=", "\n")
buildDefinitionNameFirst := testutils.GenerateResourceName()

return fmt.Sprintf(`
%s

data "azuredevops_group" "tf-project-readers" {
project_id = azuredevops_project.project.id
name = "Readers"
}

resource "azuredevops_build_definition_permissions" "permissions" {
project_id = azuredevops_project.project.id
principal = data.azuredevops_group.tf-project-readers.id

build_definition_id = azuredevops_build_definition.build.id
build_definition_path = azuredevops_build_definition.build.path

permissions = {
%s
}
}
`,
testutils.HclBuildDefinitionResourceGitHub(projectName, buildDefinitionNameFirst, `\`),
rootPermissions,
)
}

func TestAccBuildDefinitionPermissions_SetPermissions(t *testing.T) {
projectName := testutils.GenerateResourceName()
config := hclBuildDefinitionPermissions(projectName, map[string]string{
"ViewBuilds": "Allow",
"EditBuildQuality": "NotSet",
"RetainIndefinitely": "Deny",
"DeleteBuilds": "Deny",
})
tfNodeRoot := "azuredevops_build_definition_permissions.permissions"

resource.Test(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
CheckDestroy: testutils.CheckProjectDestroyed,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testutils.CheckProjectExists(projectName),
resource.TestCheckResourceAttrSet(tfNodeRoot, "project_id"),
resource.TestCheckResourceAttrSet(tfNodeRoot, "principal"),
resource.TestCheckNoResourceAttr(tfNodeRoot, "path"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.%", "4"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.ViewBuilds", "allow"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.EditBuildQuality", "notset"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.RetainIndefinitely", "deny"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.DeleteBuilds", "deny"),
),
},
},
})
}

func TestAccBuildDefinitionPermissions_UpdatePermissions(t *testing.T) {
projectName := testutils.GenerateResourceName()
config1 := hclBuildDefinitionPermissions(projectName, map[string]string{
"ViewBuilds": "Deny",
"EditBuildQuality": "NotSet",
"RetainIndefinitely": "Deny",
"DeleteBuilds": "Deny",
})
config2 := hclBuildDefinitionPermissions(projectName, map[string]string{
"ViewBuilds": "Deny",
"EditBuildQuality": "Allow",
"RetainIndefinitely": "Deny",
"DeleteBuilds": "Deny",
})
tfNodeRoot := "azuredevops_build_definition_permissions.permissions"

resource.Test(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
CheckDestroy: testutils.CheckProjectDestroyed,
Steps: []resource.TestStep{
{
Config: config1,
Check: resource.ComposeTestCheckFunc(
testutils.CheckProjectExists(projectName),
resource.TestCheckResourceAttrSet(tfNodeRoot, "project_id"),
resource.TestCheckResourceAttrSet(tfNodeRoot, "principal"),
resource.TestCheckNoResourceAttr(tfNodeRoot, "path"),
resource.TestCheckResourceAttrSet(tfNodeRoot, "project_id"),
resource.TestCheckResourceAttrSet(tfNodeRoot, "principal"),
resource.TestCheckNoResourceAttr(tfNodeRoot, "path"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.%", "4"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.ViewBuilds", "deny"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.EditBuildQuality", "notset"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.RetainIndefinitely", "deny"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.DeleteBuilds", "deny"),
),
},
{
Config: config2,
Check: resource.ComposeTestCheckFunc(
testutils.CheckProjectExists(projectName),
resource.TestCheckResourceAttrSet(tfNodeRoot, "project_id"),
resource.TestCheckResourceAttrSet(tfNodeRoot, "principal"),
resource.TestCheckNoResourceAttr(tfNodeRoot, "path"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.%", "4"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.ViewBuilds", "deny"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.EditBuildQuality", "allow"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.RetainIndefinitely", "deny"),
resource.TestCheckResourceAttr(tfNodeRoot, "permissions.DeleteBuilds", "deny"),
),
},
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package permissions

import (
"fmt"
"log"
"strconv"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/microsoft/azure-devops-go-api/azuredevops/build"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
securityhelper "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/permissions/utils"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter"
)

// ResourceBuildDefinitionPermissions schema and implementation for build permission resource
func ResourceBuildDefinitionPermissions() *schema.Resource {
return &schema.Resource{
Create: resourceBuildDefinitionPermissionsCreateOrUpdate,
Read: resourceBuildDefinitionPermissionsRead,
Update: resourceBuildDefinitionPermissionsCreateOrUpdate,
Delete: resourceBuildDefinitionPermissionsDelete,
Schema: securityhelper.CreatePermissionResourceSchema(map[string]*schema.Schema{
"project_id": {
Type: schema.TypeString,
ValidateFunc: validation.IsUUID,
Required: true,
ForceNew: true,
},
"build_definition_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"build_definition_path": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
}),
}
}

func resourceBuildDefinitionPermissionsCreateOrUpdate(d *schema.ResourceData, m interface{}) error {
clients := m.(*client.AggregatedClient)

sn, err := securityhelper.NewSecurityNamespace(d, clients, securityhelper.SecurityNamespaceIDValues.Build, createBuildToken)
if err != nil {
return err
}

if err := securityhelper.SetPrincipalPermissions(d, sn, nil, false); err != nil {
return err
}

return resourceBuildDefinitionPermissionsRead(d, m)
}

func resourceBuildDefinitionPermissionsRead(d *schema.ResourceData, m interface{}) error {
clients := m.(*client.AggregatedClient)

sn, err := securityhelper.NewSecurityNamespace(d, clients, securityhelper.SecurityNamespaceIDValues.Build, createBuildToken)
if err != nil {
return err
}

principalPermissions, err := securityhelper.GetPrincipalPermissions(d, sn)
if err != nil {
return err
}
if principalPermissions == nil {
d.SetId("")
log.Printf("[INFO] Permissions for ACL token %q not found. Removing from state", sn.GetToken())
return nil
}

d.Set("permissions", principalPermissions.Permissions)
return nil
}

func resourceBuildDefinitionPermissionsDelete(d *schema.ResourceData, m interface{}) error {
clients := m.(*client.AggregatedClient)

sn, err := securityhelper.NewSecurityNamespace(d, clients, securityhelper.SecurityNamespaceIDValues.Build, createBuildToken)
if err != nil {
return err
}

if err := securityhelper.SetPrincipalPermissions(d, sn, &securityhelper.PermissionTypeValues.NotSet, true); err != nil {
return err
}
d.SetId("")
return nil
}

func createBuildToken(d *schema.ResourceData, clients *client.AggregatedClient) (string, error) {
projectID, ok := d.GetOk("project_id")
if !ok {
return "", fmt.Errorf("Failed to get 'project_id' from schema")
}

buildDefinitionID, err := getBuildDefinitionID(d)
if err != nil {
return "", err
}

definition, err := clients.BuildClient.GetDefinition(clients.Ctx, build.GetDefinitionArgs{
Project: converter.String(projectID.(string)),
DefinitionId: converter.Int(buildDefinitionID),
})

if err != nil {
return "", err
}

var aclToken string

// The token format is Project_ID/Build_Definition_ID
// or Project_ID/Path/Build_Definition_ID

if *definition.Path != "\\" {
path, ok := d.GetOk("build_definition_path")
xuzhang3 marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return "", fmt.Errorf("Set path to %s", *definition.Path)
}

if path.(string) != *definition.Path {
return "", fmt.Errorf("Update path to %s", *definition.Path)
}

transformedPath := transformPath(*definition.Path)

aclToken = fmt.Sprintf("%s/%s/%d", projectID.(string), transformedPath, buildDefinitionID)
} else {
aclToken = fmt.Sprintf("%s/%d", projectID.(string), buildDefinitionID)
}

return aclToken, nil
}

// transformPath must return a path with forward slashes
func transformPath(path string) string {
paths := strings.Split(path, "\\")
transformedPath := strings.Join(paths, "/")

// remove slash at front of string
transformedPath = strings.TrimPrefix(transformedPath, "/")

// remove slash at end of string
if strings.HasSuffix(transformedPath, "/") {
transformedPath = transformedPath[:len(transformedPath)-1]
}

return transformedPath
}

func getBuildDefinitionID(d *schema.ResourceData) (int, error) {
buildID, ok := d.GetOk("build_definition_id")
if !ok {
return -1, fmt.Errorf("Failed to get 'build_definition_id' from schema")
}

id, err := strconv.Atoi(buildID.(string))
if err != nil {
return -1, err
}

return id, nil
}