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

[WIP] Add support Extension properties #802

Open
wants to merge 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package applications

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"regexp"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"

"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/services/applications/parse"
"github.com/hashicorp/terraform-provider-azuread/internal/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/utils"
)

const applicationExtensionPropertyResourceName = "azuread_application_extension_property"

func applicationExtensionPropertyResource() *schema.Resource {
return &schema.Resource{
CreateContext: applicationExtensionPropertyResourceCreate,
ReadContext: applicationExtensionPropertyResourceRead,
UpdateContext: applicationExtensionPropertyResourceUpdate,
DeleteContext: applicationExtensionPropertyResourceDelete,

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

Importer: tf.ValidateResourceIDPriorToImport(func(id string) error {
_, err := parse.ApplicationExtensionPropertyID(id)
return err
}),

Schema: map[string]*schema.Schema{
"id": {
Description: "The ID",
Type: schema.TypeString,
Computed: true,
},

"application_id": {
Description: "The application ID to be used to link the extension property",
Type: schema.TypeString,
Required: true,
},

"name": {
Description: "The extension property name",
Type: schema.TypeString,
Required: true,
DiffSuppressFunc: applicationExtensionPropertyNameDiffSuppress,
},

"app_display_name": {
Description: "The display name for the application",
Type: schema.TypeString,
Computed: true,
},

"data_type": {
Description: "The extension property data type",
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
msgraph.ApplicationExtensionDataTypeBinary,
msgraph.ApplicationExtensionDataTypeBoolean,
msgraph.ApplicationExtensionDataTypeDateTime,
msgraph.ApplicationExtensionDataTypeInteger,
msgraph.ApplicationExtensionDataTypeLargeInteger,
msgraph.ApplicationExtensionDataTypeString,
}, false),
},

"target_objects": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice([]string{
msgraph.ExtensionSchemaTargetTypeAdministrativeUnit,
msgraph.ExtensionSchemaTargetTypeContact,
msgraph.ExtensionSchemaTargetTypeDevice,
msgraph.ExtensionSchemaTargetTypeEvent,
msgraph.ExtensionSchemaTargetTypeGroup,
msgraph.ExtensionSchemaTargetTypeMessage,
msgraph.ExtensionSchemaTargetTypeOrganization,
msgraph.ExtensionSchemaTargetTypePost,
msgraph.ExtensionSchemaTargetTypeUser,
}, false),
},
},

"is_synced_from_on_premises": {
Type: schema.TypeBool,
Computed: true,
},
},
}
}

func applicationExtensionPropertyNameDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
res, _ := regexp.MatchString("extension_.*_"+new, old)
return res
}

func applicationExtensionPropertyResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).Applications.ApplicationsClient
applicationId := d.Get("application_id").(string)
targetObjects := d.Get("target_objects").([]interface{})

// Create a new application
properties := msgraph.ApplicationExtension{
Name: utils.String(d.Get("name").(string)),
DataType: d.Get("data_type").(msgraph.ApplicationExtensionDataType),
TargetObjects: tf.ExpandStringSlicePtr(targetObjects),
}

appExt, _, err := client.CreateExtension(ctx, properties, applicationId)
if err != nil {
return tf.ErrorDiagF(err, "Could not create extension property")
}

if appExt.Id == nil || *appExt.Id == "" {
return tf.ErrorDiagF(errors.New("Bad API response"), "ID returned for extension property is nil/empty")
}

d.SetId(parse.NewApplicationExtensionPropertyID(applicationId, *appExt.Id).String())

return applicationExtensionPropertyResourceRead(ctx, d, meta)
}

func applicationExtensionPropertyResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// Update isn't supported
err := errors.New("Extension property can't be updated once created")
return tf.ErrorDiagF(err, "Could not update application with object ID: %q", d.Id())
}

func applicationExtensionPropertyResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).Applications.ApplicationsClient
extId, err := parse.ApplicationExtensionPropertyID(d.Id())
if err != nil {
return tf.ErrorDiagPathF(err, "id", "Parsing Extension Property ID %q", d.Id())
}

appExts, status, err := client.ListExtensions(ctx, extId.ApplicationId, odata.Query{
Filter: "id eq '" + extId.ExtensionPropertyId + "'",
})
if err != nil {
if status == http.StatusNotFound {
log.Printf("[DEBUG] Extension property with ID %q was not found in Application %q - removing from state", extId.ExtensionPropertyId, extId.ApplicationId)
d.SetId("")
return nil
}

return tf.ErrorDiagPathF(err, "id", "Retrieving extension property with ID %q in application id %q", extId.ExtensionPropertyId, extId.ApplicationId)
}

appExt := (*appExts)[0]

tf.Set(d, "id", appExt.Id)
tf.Set(d, "name", appExt.Name)
tf.Set(d, "app_display_name", appExt.AppDisplayName)
tf.Set(d, "data_type", appExt.DataType)
tf.Set(d, "is_synced_from_on_premises", appExt.IsSyncedFromOnPremises)
tf.Set(d, "target_objects", appExt.TargetObjects)

return nil
}

func applicationExtensionPropertyResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).Applications.ApplicationsClient
extId, err := parse.ApplicationExtensionPropertyID(d.Id())
if err != nil {
return tf.ErrorDiagPathF(err, "id", "Parsing Extension Property ID %q", d.Id())
}

status, err := client.DeleteExtension(ctx, extId.ApplicationId, extId.ExtensionPropertyId)
if err != nil {
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(fmt.Errorf("Extension property was not found"), "id", "Retrieving Extension property with ID %q in Application ID %q", extId.ExtensionPropertyId, extId.ApplicationId)
}

return tf.ErrorDiagPathF(err, "id", "Retrieving extension property with ID %q on application ID %q", extId.ExtensionPropertyId, extId.ApplicationId)
}

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package applications_test

import (
"context"
"fmt"
"net/http"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/manicminer/hamilton/odata"

"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/applications/parse"
"github.com/hashicorp/terraform-provider-azuread/internal/utils"
)

type ApplicationExtensionPropertyResource struct{}

func TestAccApplicationExtensionProperty_complete(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_application_extension_property", "test")
r := ApplicationExtensionPropertyResource{}

data.ResourceTest(t, r, []resource.TestStep{
{
Config: r.complete(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("application_id").Exists(),
check.That(data.ResourceName).Key("data_type").Exists(),
//check.That(data.ResourceName).Key("target_objects").Exists(),
),
},
data.ImportStep(),
})
}

func (r ApplicationExtensionPropertyResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) {
client := clients.Applications.ApplicationsClient
client.BaseClient.DisableRetries = true
extId, err := parse.ApplicationExtensionPropertyID(state.ID)
if err != nil {
return nil, err
}

appExts, status, err := client.ListExtensions(ctx, extId.ApplicationId, odata.Query{
Filter: "id eq '" + extId.ExtensionPropertyId + "'",
})
if err != nil {
if status == http.StatusNotFound {
return nil, fmt.Errorf("Extension property with ID %q does not exist in Application ID %q", extId.ExtensionPropertyId, extId.ApplicationId)
}
return nil, fmt.Errorf("failed to retrieve Extension property ID %q Application ID %q: %+v", extId.ExtensionPropertyId, extId.ApplicationId, err)
}
appExt := (*appExts)[0]
return utils.Bool(appExt.Id != nil && *appExt.Id == extId.ExtensionPropertyId), nil
}

func (ApplicationExtensionPropertyResource) complete(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azuread" {}

resource "azuread_application" "testapp" {
display_name = "acctest-APP-%[1]d"
}

resource "azuread_application_extension_property" "test" {
application_id = azuread_application.testapp.id
name = "acctest_APP_complete_%[1]d"
data_type = "String"
target_objects = [ "User" ]
}
`, data.RandomInteger)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package parse

import "fmt"

type ApplicationExtensionPropertyId struct {
ObjectSubResourceId
ApplicationId string
ExtensionPropertyId string
}

func NewApplicationExtensionPropertyID(appId, extAttrId string) ApplicationExtensionPropertyId {
return ApplicationExtensionPropertyId{
ObjectSubResourceId: NewObjectSubResourceID(appId, "extensionProperty", extAttrId),
ApplicationId: appId,
ExtensionPropertyId: extAttrId,
}
}

func ApplicationExtensionPropertyID(idString string) (*ApplicationExtensionPropertyId, error) {
id, err := ObjectSubResourceID(idString, "extensionProperty")
if err != nil {
return nil, fmt.Errorf("unable to parse ExtensionProperty ID: %v", err)
}

return &ApplicationExtensionPropertyId{
ObjectSubResourceId: *id,
ApplicationId: id.objectId,
ExtensionPropertyId: id.subId,
}, nil
}
1 change: 1 addition & 0 deletions internal/services/applications/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ func (r Registration) SupportedResources() map[string]*schema.Resource {
"azuread_application_federated_identity_credential": applicationFederatedIdentityCredentialResource(),
"azuread_application_password": applicationPasswordResource(),
"azuread_application_pre_authorized": applicationPreAuthorizedResource(),
"azuread_application_extension_property": applicationExtensionPropertyResource(),
}
}