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

New resource azuread_group_license_assignment #1506

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/labeler-issue-triage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ feature/domains:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azuread_domains((.|\n)*)###'

feature/groups:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azuread_(group\W+|group_member\W+|groups\W+)((.|\n)*)###'
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azuread_(group\W+|group_license_assignment\W+|group_member\W+|groups\W+)((.|\n)*)###'

feature/identity-governance:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azuread_(access_package|privileged_access_group_)((.|\n)*)###'
Expand Down
4 changes: 2 additions & 2 deletions examples/application/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ resource "azuread_application" "widgets_app" {

resource_access {
# User.Read
id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"
id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"
type = "Scope"
}
}

required_resource_access {
resource_app_id = azuread_application.widgets_service.application_id

dynamic resource_access {
dynamic "resource_access" {
for_each = azuread_application.widgets_service.api.0.oauth2_permission_scope
iterator = scope

Expand Down
2 changes: 1 addition & 1 deletion examples/create-for-rbac/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ output "client_certificate" {
}

output "client_key" {
value = tls_private_key.example.private_key_pem
value = tls_private_key.example.private_key_pem
sensitive = true
}

Expand Down
188 changes: 188 additions & 0 deletions internal/services/groups/group_license_assignment_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package groups

import (
"context"
"strings"
"time"

"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group"
"github.com/hashicorp/go-azure-sdk/sdk/nullable"
"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation"
"github.com/hashicorp/terraform-provider-azuread/internal/services/groups/parse"
)

func groupLicenseAssignmentResource() *pluginsdk.Resource {
return &pluginsdk.Resource{
CreateContext: groupLicenseAssignmentResourceCreate,
ReadContext: groupLicenseAssignmentResourceRead,
DeleteContext: groupLicenseAssignmentResourceDelete,

Timeouts: &pluginsdk.ResourceTimeout{
Create: pluginsdk.DefaultTimeout(5 * time.Minute),
Read: pluginsdk.DefaultTimeout(5 * time.Minute),
Delete: pluginsdk.DefaultTimeout(5 * time.Minute),
},

Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error {
_, err := parse.GroupLicenseAssignmentID(id)
return err
}),

Schema: map[string]*pluginsdk.Schema{
"group_object_id": {
Description: "The object ID of the group you want to add the member to",
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsUUID,
},

"sku_id": {
Description: "The unique identifier for the SKU. Corresponds to the skuId from subscribedSkus or companySubscription.",
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsUUID,
},

"disabled_plans": {
Description: "A collection of the unique identifiers for plans that have been disabled. IDs are available in servicePlans > servicePlanId in the tenant's subscribedSkus or serviceStatus > servicePlanId in the tenant's companySubscription.",
Type: pluginsdk.TypeSet,
Optional: true,
ForceNew: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateFunc: validation.IsUUID,
},
},
},
}
}

func groupLicenseAssignmentResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics {
client := meta.(*clients.Client).Groups.GroupClientBeta

groupId := beta.NewGroupID(d.Get("group_object_id").(string))
resourceId := parse.NewGroupLicenseAssignmentID(groupId.GroupId, d.Get("sku_id").(string))

resp, err := client.GetGroup(ctx, groupId, group.GetGroupOperationOptions{
Select: &[]string{
"assignedLicenses",
},
})
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return tf.ErrorDiagPathF(nil, "group_object_id", "%s was not found", groupId)
}
return tf.ErrorDiagPathF(err, "group_object_id", "Retrieving %s", groupId)
}

license := getGroupLicense(resp.Model.AssignedLicenses, resourceId.SKUId)
if license != nil {
return tf.ImportAsExistsDiag("azuread_group_license_assignment", resourceId.String())
}

if _, err := client.AssignLicense(ctx, groupId, group.AssignLicenseRequest{
AddLicenses: &[]beta.AssignedLicense{
{
SkuId: nullable.Value(resourceId.SKUId),
DisabledPlans: tf.ExpandStringSlicePtr(d.Get("disabled_plans").(*pluginsdk.Set).List()),
},
},
}, group.DefaultAssignLicenseOperationOptions()); err != nil {
return tf.ErrorDiagF(err, "Assigning license to %s", groupId)
}

d.SetId(resourceId.String())

return groupLicenseAssignmentResourceRead(ctx, d, meta)
}

func groupLicenseAssignmentResourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics {
client := meta.(*clients.Client).Groups.GroupClientBeta

resourceId, err := parse.GroupLicenseAssignmentID(d.Id())
if err != nil {
return tf.ErrorDiagPathF(err, "id", "Parsing Group License Assignment ID %q", d.Id())
}

resp, err := client.GetGroup(ctx, beta.NewGroupID(resourceId.GroupId), group.GetGroupOperationOptions{
Select: &[]string{
"assignedLicenses",
},
})
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return tf.ErrorDiagPathF(nil, "group_object_id", "%s was not found", resourceId.GroupId)
}
return tf.ErrorDiagPathF(err, "group_object_id", "Retrieving %s", resourceId.GroupId)
}

license := getGroupLicense(resp.Model.AssignedLicenses, resourceId.SKUId)

if license == nil {
return tf.ErrorDiagF(err, "Retrieving license %s for group with object ID: %s", resourceId.SKUId, resourceId.GroupId)
}

tf.Set(d, "group_object_id", resourceId.GroupId)
tf.Set(d, "sku_id", resourceId.SKUId)
tf.Set(d, "disabled_plans", tf.FlattenStringSlicePtr(license.DisabledPlans))

return nil
}

func groupLicenseAssignmentResourceDelete(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics {
client := meta.(*clients.Client).Groups.GroupClientBeta

resourceId, err := parse.GroupLicenseAssignmentID(d.Id())
if err != nil {
return tf.ErrorDiagPathF(err, "id", "Parsing Group License Assignment ID %q", d.Id())
}

resp, err := client.GetGroup(ctx, beta.NewGroupID(resourceId.GroupId), group.GetGroupOperationOptions{
Select: &[]string{
"assignedLicenses",
},
})
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
// Group is already deleted
return nil
}
return tf.ErrorDiagPathF(err, "group_object_id", "Retrieving %s", resourceId.GroupId)
}
license := getGroupLicense(resp.Model.AssignedLicenses, resourceId.SKUId)

if license == nil {
// License is already removed
return nil
}

if _, err := client.AssignLicense(ctx, beta.NewGroupID(resourceId.GroupId), group.AssignLicenseRequest{
RemoveLicenses: &[]string{resourceId.SKUId},
}, group.DefaultAssignLicenseOperationOptions()); err != nil {
return tf.ErrorDiagF(err, "Removing license %s to %s", resourceId.SKUId, resourceId.GroupId)
}

return nil
}

func getGroupLicense(licenses *[]beta.AssignedLicense, skuId string) *beta.AssignedLicense {
if licenses != nil {
for _, v := range *licenses {
if strings.EqualFold(v.SkuId.GetOrZero(), skuId) {
return &v
}
}
}

return nil
}
118 changes: 118 additions & 0 deletions internal/services/groups/group_license_assignment_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package groups_test

import (
"context"
"fmt"
"testing"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/beta"
"github.com/hashicorp/go-azure-sdk/microsoft-graph/groups/beta/group"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-provider-azuread/internal/acceptance"
"github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check"
"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/services/groups/parse"
)

type GroupLicenseAssignmentResource struct{}

func TestAccGrouplicenseassignment_license(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_group_license_assignment", "testA")
r := GroupLicenseAssignmentResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.license(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("group_object_id").IsUuid(),
check.That(data.ResourceName).Key("sku_id").IsUuid(),
check.That(data.ResourceName).Key("disabled_plans").IsEmpty(),
),
},
data.ImportStep(),
})
}

func TestAccGroupLicenseAssignment_requiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_group_license_assignment", "test")
r := GroupLicenseAssignmentResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.license(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.RequiresImportErrorStep(r.requiresImport(data)),
})
}

func (r GroupLicenseAssignmentResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) {
client := clients.Groups.GroupClientBeta

id, err := parse.GroupLicenseAssignmentID(state.ID)
if err != nil {
return nil, fmt.Errorf("parsing Group License Assignment ID: %v", err)
}

resp, err := client.GetGroup(ctx, beta.NewGroupID(id.GroupId), group.GetGroupOperationOptions{
Select: &[]string{
"assignedLicenses",
},
})
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return pointer.To(false), nil
}
return nil, fmt.Errorf("failed to retrieve license %q (group ID: %q): %+v", id.SKUId, id.GroupId, err)
}

if resp.Model != nil {
for _, license := range *resp.Model.AssignedLicenses {
if license.SkuId.GetOrZero() == id.SKUId {
return pointer.To(true), nil
}
}
}

return pointer.To(false), nil
}

func (GroupLicenseAssignmentResource) template(data acceptance.TestData) string {
return fmt.Sprintf(`
resource "azuread_group" "test" {
display_name = "acctestGroup-%[1]d"
security_enabled = true
}
`, data.RandomInteger)
}

func (r GroupLicenseAssignmentResource) license(data acceptance.TestData) string {
return fmt.Sprintf(`
%[1]s

resource "azuread_group_license_assignment" "test" {
group_object_id = azuread_group.test.object_id
sku_id = "90d8b3f8-712e-4f7b-aa1e-62e7ae6cbe96" # SMB_APPS
}
`, r.template(data))
}

func (r GroupLicenseAssignmentResource) requiresImport(data acceptance.TestData) string {
return fmt.Sprintf(`
%[1]s

resource "azuread_group_license_assignment" "import" {
group_object_id = azuread_group_license_assignment.test.group_object_id
sku_id = azuread_group_license_assignment.test.sku_id
disabled_plans = azuread_group_license_assignment.test.disabled_plans
}
`, r.license(data))
}
33 changes: 33 additions & 0 deletions internal/services/groups/parse/group_license_assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package parse

import "fmt"

type GroupLicenseAssignmentId struct {
ObjectSubResourceId
GroupId string
SKUId string
}

func NewGroupLicenseAssignmentID(groupId, skuId string) GroupLicenseAssignmentId {
return GroupLicenseAssignmentId{
ObjectSubResourceId: NewObjectSubResourceID(groupId, "license", skuId),
GroupId: groupId,
SKUId: skuId,
}
}

func GroupLicenseAssignmentID(idString string) (*GroupLicenseAssignmentId, error) {
id, err := ObjectSubResourceID(idString, "license")
if err != nil {
return nil, fmt.Errorf("unable to parse Group License Assignment ID: %v", err)
}

return &GroupLicenseAssignmentId{
ObjectSubResourceId: *id,
GroupId: id.objectId,
SKUId: id.subId,
}, nil
}
5 changes: 3 additions & 2 deletions internal/services/groups/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ func (r Registration) SupportedDataSources() map[string]*pluginsdk.Resource {
// SupportedResources returns the supported Resources supported by this Service
func (r Registration) SupportedResources() map[string]*pluginsdk.Resource {
return map[string]*pluginsdk.Resource{
"azuread_group": groupResource(),
"azuread_group_member": groupMemberResource(),
"azuread_group": groupResource(),
"azuread_group_member": groupMemberResource(),
"azuread_group_license_assignment": groupLicenseAssignmentResource(),
}
}
Loading