Skip to content

Commit

Permalink
fixed hashicorp#1031 adds provision on demand
Browse files Browse the repository at this point in the history
fix ci issues

wip for potential test for provision on demand job

handle the integration test not connecting to a real databricks

add azurerm_databricks_workspace to test tf

Revert "add azurerm_databricks_workspace to test tf"

This reverts commit e1cca89.
  • Loading branch information
iwarapter committed Mar 1, 2024
1 parent 79f6519 commit 6d02e70
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 2 deletions.
109 changes: 109 additions & 0 deletions docs/resources/synchronization_job_provision_on_demand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
subcategory: "Synchronization"
---

# Resource: azuread_synchronization_job_provision_on_demand

Manages synchronization job on demand provisioning associated with a service principal (enterprise application) within Azure Active Directory.

## API Permissions

The following API permissions are required in order to use this resource.

When authenticated with a service principal, this resource requires one of the following application roles: `Synchronization.ReadWrite.All`

## Example Usage

*Basic example*

```terraform
data "azuread_client_config" "current" {}
resource "azuread_group" "example" {
display_name = "example"
owners = [data.azuread_client_config.current.object_id]
security_enabled = true
}
data "azuread_application_template" "example" {
display_name = "Azure Databricks SCIM Provisioning Connector"
}
resource "azuread_application" "example" {
display_name = "example"
template_id = data.azuread_application_template.example.template_id
feature_tags {
enterprise = true
gallery = true
}
}
resource "azuread_service_principal" "example" {
application_id = azuread_application.example.application_id
use_existing = true
}
resource "azuread_synchronization_secret" "example" {
service_principal_id = azuread_service_principal.example.id
credential {
key = "BaseAddress"
value = "https://adb-example.azuredatabricks.net/api/2.0/preview/scim"
}
credential {
key = "SecretToken"
value = "some-token"
}
}
resource "azuread_synchronization_job" "example" {
service_principal_id = azuread_service_principal.example.id
template_id = "dataBricks"
enabled = true
}
resource "azuread_synchronization_job_provision_on_demand" "example" {
service_principal_id = azuread_service_principal.example.id
synchronization_job_id = azuread_synchronization_job.example.id
parameter {
# see specific synchronization schema for rule id https://learn.microsoft.com/en-us/graph/api/synchronization-synchronizationschema-get?view=graph-rest-beta
rule_id = ""
subject {
object_id = azuread_group.example.object_id
object_type_name = "Group"
}
}
}
```

## Argument Reference

The following arguments are supported:


- `synchronization_job_id` (Required) Identifier of the synchronization template this job is based on.
- `parameter` (Required) One or more `parameter` blocks as documented below.
- `service_principal_id` (Required) The object ID of the service principal for the synchronization job.

---

`parameter` block supports the following:

* `rule_id` (Required) The identifier of the synchronizationRule to be applied. This rule ID is defined in the schema for a given synchronization job or template.
* `subject` (Required) One or more `subject` blocks as documented below.

---

`subject` block supports the following:

* `object_id` (String) The identifier of an object to which a synchronizationJob is to be applied.
* `object_type_name` (String) The type of the object to which a synchronizationJob is to be applied.

## Attributes Reference

No additional attributes are exported.

## Import

This resource does not support importing.
5 changes: 3 additions & 2 deletions internal/services/synchronization/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ 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_synchronization_job": synchronizationJobResource(),
"azuread_synchronization_secret": synchronizationSecretResource(),
"azuread_synchronization_job": synchronizationJobResource(),
"azuread_synchronization_job_provision_on_demand": synchronizationJobProvisionOnDemandResource(),
"azuread_synchronization_secret": synchronizationSecretResource(),
}
}

Expand Down
35 changes: 35 additions & 0 deletions internal/services/synchronization/synchronization.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,41 @@ func expandSynchronizationSecretKeyStringValuePair(in []interface{}) *[]msgraph.
return &result
}

func expandSynchronizationJobApplicationParameters(in []interface{}) *[]msgraph.SynchronizationJobApplicationParameters {
result := make([]msgraph.SynchronizationJobApplicationParameters, 0)

for _, raw := range in {
if raw == nil {
continue
}
item := raw.(map[string]interface{})

result = append(result, msgraph.SynchronizationJobApplicationParameters{
Subjects: expandSynchronizationJobSubject(item["subjects"].([]interface{})),
RuleId: pointer.To(item["rule_id"].(string)),
})
}

return &result
}

func expandSynchronizationJobSubject(in []interface{}) *[]msgraph.SynchronizationJobSubject {
result := make([]msgraph.SynchronizationJobSubject, 0)
for _, raw := range in {
if raw == nil {
continue
}
item := raw.(map[string]interface{})

result = append(result, msgraph.SynchronizationJobSubject{
ObjectId: pointer.To(item["object_id"].(string)),
ObjectTypeName: pointer.To(item["object_type_name"].(string)),
})
}

return &result
}

func flattenSynchronizationSchedule(in *msgraph.SynchronizationSchedule) []map[string]interface{} {
if in == nil {
return []map[string]interface{}{}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package synchronization

import (
"context"
"errors"
"github.com/hashicorp/go-azure-sdk/sdk/odata"
"net/http"
"time"

"github.com/hashicorp/go-azure-sdk/sdk/odata"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"

"github.com/hashicorp/go-uuid"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/tf/validation"
"github.com/manicminer/hamilton/msgraph"
)

func synchronizationJobProvisionOnDemandResource() *schema.Resource {
return &schema.Resource{
CreateContext: synchronizationProvisionOnDemandResourceCreate,
ReadContext: synchronizationProvisionOnDemandResourceRead,
DeleteContext: synchronizationProvisionOnDemandResourceDelete,

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(15 * time.Minute),
Read: schema.DefaultTimeout(5 * time.Minute),
Delete: schema.DefaultTimeout(5 * time.Minute),
},
SchemaVersion: 0,

Schema: map[string]*schema.Schema{
"service_principal_id": {
Description: "The object ID of the service principal for which this synchronization job should be provisioned",
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID),
},
"synchronization_job_id": {
Description: "The identifier for the synchronization jop.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"parameter": {
Description: "Represents the objects that will be provisioned and the synchronization rules executed. The resource is primarily used for on-demand provisioning.",
Type: schema.TypeList,
Required: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"rule_id": {
Description: "The identifier of the synchronizationRule to be applied. This rule ID is defined in the schema for a given synchronization job or template.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"subject": {
Description: "The identifiers of one or more objects to which a synchronizationJob is to be applied.",
Type: schema.TypeList,
Required: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"object_id": {
Description: "The identifier of an object to which a synchronization Job is to be applied. Can be one of the following: (1) An onPremisesDistinguishedName for synchronization from Active Directory to Azure AD. (2) The user ID for synchronization from Azure AD to a third-party. (3) The Worker ID of the Workday worker for synchronization from Workday to either Active Directory or Azure AD.",
Type: schema.TypeString,
Required: true,
},
"object_type_name": {
Description: "The type of the object to which a synchronization Job is to be applied. Can be one of the following: `user` for synchronizing between Active Directory and Azure AD, `User` for synchronizing a user between Azure AD and a third-party application, `Worker` for synchronization a user between Workday and either Active Directory or Azure AD, `Group` for synchronizing a group between Azure AD and a third-party application.",
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"Group", "user", "User", "Worker"}, false),
},
},
},
},
},
},
},
},
}
}

func synchronizationProvisionOnDemandResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).ServicePrincipals.SynchronizationJobClient
spClient := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient
objectId := d.Get("service_principal_id").(string)
jobId := d.Get("synchronization_job_id").(string)

tf.LockByName(servicePrincipalResourceName, objectId)
defer tf.UnlockByName(servicePrincipalResourceName, objectId)

servicePrincipal, status, err := spClient.Get(ctx, objectId, odata.Query{})
if err != nil {
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(nil, "service_principal_id", "Service principal with object ID %q was not found", objectId)
}
return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", objectId)
}
if servicePrincipal == nil || servicePrincipal.ID() == nil {
return tf.ErrorDiagF(errors.New("nil service principal or service principal with nil ID was returned"), "API error retrieving service principal with object ID %q", objectId)
}

job, status, err := client.Get(ctx, jobId, objectId)
if err != nil {
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(nil, "job_id", "Job with object ID %q was not found for service principle %q", jobId, objectId)
}
return tf.ErrorDiagPathF(err, "job_id", "Retrieving job with object ID %q for service principle %q", jobId, objectId)
}
if job == nil || job.ID == nil {
return tf.ErrorDiagF(errors.New("nil job or job with nil ID was returned"), "API error retrieving job with object ID %q/%s", objectId, jobId)
}
// Create a new synchronization job
synchronizationProvisionOnDemand := &msgraph.SynchronizationJobProvisionOnDemand{
Parameters: expandSynchronizationJobApplicationParameters(d.Get("parameter").([]interface{})),
}

_, err = client.ProvisionOnDemand(ctx, jobId, synchronizationProvisionOnDemand, *servicePrincipal.ID())
if err != nil {
return tf.ErrorDiagF(err, "Creating synchronization job for service principal ID %q", *servicePrincipal.ID())
}

id, _ := uuid.GenerateUUID()
d.SetId(id)

return synchronizationProvisionOnDemandResourceRead(ctx, d, meta)
}

func synchronizationProvisionOnDemandResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
}

func synchronizationProvisionOnDemandResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package synchronization_test

import (
"context"
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/hashicorp/terraform-provider-azuread/internal/acceptance"
"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/utils"
)

type SynchronizationJobProvisionOnDemandResource struct{}

func TestAccSynchronizationJobProvisionOnDemand_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_synchronization_job_provision_on_demand", "test")
r := SynchronizationJobProvisionOnDemandResource{}

data.ResourceTest(t, r, []resource.TestStep{
{
//The provisioned app isn't actually integrated so this will never work
Config: r.basic(data),
ExpectError: regexp.MustCompile("CredentialsMissing: Please configure provisioning by providing your admin credentials then retry the provision on-demand."),
},
})
}

func (r SynchronizationJobProvisionOnDemandResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) {
return utils.Bool(true), nil
}

func (SynchronizationJobProvisionOnDemandResource) template(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azuread" {}
data "azuread_client_config" "test" {}
data "azuread_application_template" "test" {
display_name = "Azure Databricks SCIM Provisioning Connector"
}
resource "azuread_application" "test" {
display_name = "acctestSynchronizationJob-%[1]d"
owners = [data.azuread_client_config.test.object_id]
template_id = data.azuread_application_template.test.template_id
}
resource "azuread_service_principal" "test" {
application_id = azuread_application.test.application_id
owners = [data.azuread_client_config.test.object_id]
use_existing = true
}
resource "azuread_synchronization_job" "test" {
service_principal_id = azuread_service_principal.test.id
template_id = "dataBricks"
}
resource "azuread_group" "test" {
display_name = "acctestGroup-%[1]d"
security_enabled = true
}
`, data.RandomInteger)
}

func (r SynchronizationJobProvisionOnDemandResource) basic(data acceptance.TestData) string {
return fmt.Sprintf(`
%[1]s
resource "azuread_synchronization_job_provision_on_demand" "test" {
service_principal_id = azuread_service_principal.test.id
synchronization_job_id = trimprefix(azuread_synchronization_job.test.id, "${azuread_service_principal.test.id}/job/")
parameter {
rule_id = "03f7d90d-bf71-41b1-bda6-aaf0ddbee5d8" //no api to check this so assuming the rule id is the same globally :finger_crossed:
subject {
object_id = azuread_group.test.id
object_type_name = "Group"
}
}
}
`, r.template(data))
}

0 comments on commit 6d02e70

Please sign in to comment.