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

azuread_application: add password property #1389

Merged
merged 12 commits into from
Jun 25, 2024
71 changes: 71 additions & 0 deletions internal/helpers/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,77 @@ func KeyCredentialForResource(d *pluginsdk.ResourceData) (*msgraph.KeyCredential
return &credential, nil
}

func FlattenCredential(in *msgraph.PasswordCredential, exist map[string]interface{}) []map[string]interface{} {
if in == nil {
return []map[string]interface{}{}
}

startDate := exist["start_date"]
if in.StartDateTime != nil {
startDate = in.StartDateTime.Format(time.RFC3339)
}

endDate := exist["start_date"]
if in.EndDateTime != nil {
endDate = in.EndDateTime.Format(time.RFC3339)
}

value := exist["value"]
if in.SecretText != nil {
value = *in.SecretText
}

keyId := exist["key_id"]
if in.KeyId != nil {
keyId = *in.KeyId
}

return []map[string]interface{}{{
"display_name": exist["display_name"],
"start_date": startDate,
"end_date": endDate,
"value": value,
"key_id": keyId,
"end_date_relative": exist["end_date_relative"],
}}
}

HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
func PasswordCredential(d map[string]interface{}) (*msgraph.PasswordCredential, error) {
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
credential := msgraph.PasswordCredential{}

if v, ok := d["display_name"]; ok {
credential.DisplayName = pointer.To(v.(string))
}

if v, ok := d["start_date"]; ok && v.(string) != "" {
startDate, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return nil, CredentialError{str: fmt.Sprintf("Unable to parse the provided start date %q: %+v", v, err), attr: "start_date"}
}
credential.StartDateTime = &startDate
}

if v, ok := d["end_date"]; ok && v.(string) != "" {
var err error
expiry, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return nil, CredentialError{str: fmt.Sprintf("Unable to parse the provided end date %q: %+v", v, err), attr: "end_date"}
}

credential.EndDateTime = &expiry
}

if v, ok := d["key_id"]; ok && v.(string) != "" {
credential.KeyId = pointer.To(v.(string))
}

if v, ok := d["value"]; ok && v.(string) != "" {
credential.SecretText = pointer.To(v.(string))
}

return &credential, nil
}

func PasswordCredentialForResource(d *pluginsdk.ResourceData) (*msgraph.PasswordCredential, error) {
credential := msgraph.PasswordCredential{}

Expand Down
89 changes: 88 additions & 1 deletion internal/services/applications/application_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,58 @@
},
},

"password": {

Check failure on line 269 in internal/services/applications/application_resource.go

View workflow job for this annotation

GitHub Actions / tflint

S018: schema should use TypeList with MaxItems 1
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
Description: "App password definition",
Type: pluginsdk.TypeSet,
Optional: true,
MaxItems: 1,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"display_name": {
Description: "A display name for the password",
Type: pluginsdk.TypeString,
Required: true,
},

"start_date": {
Description: "The start date from which the password is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`). If this isn't specified, the current date is used",
Type: pluginsdk.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.IsRFC3339Time,
},

"end_date": {
Description: "The end date until which the password is valid, formatted as an RFC3339 date string (e.g. `2018-01-01T01:02:03Z`)",
Type: pluginsdk.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.IsRFC3339Time,
},

"end_date_relative": {
Description: "A relative duration for which the password is valid until, for example `240h` (10 days) or `2400h30m`. Changing this field forces a new resource to be created",
Type: pluginsdk.TypeString,
Optional: true,
ValidateFunc: validation.StringIsNotEmpty,
},

HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
"key_id": {
Description: "A UUID used to uniquely identify this password credential",
Type: pluginsdk.TypeString,
Computed: true,
},

"value": {
Description: "The password for this application, which is generated by Azure Active Directory",
Type: pluginsdk.TypeString,
Computed: true,
Sensitive: true,
},
},
},
},

"description": {
Description: "Description of the application as shown to end users",
Type: pluginsdk.TypeString,
Expand Down Expand Up @@ -817,7 +869,6 @@

func applicationDiffSuppress(k, old, new string, d *pluginsdk.ResourceData) bool {
suppress := false

switch {
case k == "api.#" && old == "1" && new == "0":
apiRaw := d.Get("api").([]interface{})
Expand Down Expand Up @@ -1018,6 +1069,19 @@
Web: expandApplicationWeb(d.Get("web").([]interface{})),
}

// Create application passwords, the first is created within the application request, rest is added later.
if v, ok := d.GetOk("password"); ok {
credentials := make([]msgraph.PasswordCredential, 0)
for _, cred := range v.(*pluginsdk.Set).List() {
credential, err := helpers.PasswordCredential(cred.(map[string]interface{}))
if err != nil {
return tf.ErrorDiagPathF(err, "password", "Could not flatten application password credentials")
}
credentials = append(credentials, *credential)
}
properties.PasswordCredentials = &credentials
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
}

// Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice
// The calling principal should always be in the first slice of owners
callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{})
Expand Down Expand Up @@ -1080,6 +1144,18 @@
id := parse.NewApplicationID(*app.ID())
d.SetId(id.ID())

// set the pw credentials to state
if app.PasswordCredentials != nil {
var cred map[string]interface{}
for _, password := range d.Get("password").(*pluginsdk.Set).List() {
cred = password.(map[string]interface{})
}

if credentials := flattenApplicationPasswordCredentials(app.PasswordCredentials, cred); credentials != nil {
tf.Set(d, "password", credentials)
}
}
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved

// Attempt to patch the newly created application and set the display name, which will tell us whether it exists yet, then set it back to the desired value.
// The SDK handles retries for us here in the event of 404, 429 or 5xx, then returns after giving up.
uuid, err := uuid.GenerateUUID()
Expand Down Expand Up @@ -1334,6 +1410,17 @@
tf.Set(d, "terms_of_service_url", app.Info.TermsOfServiceUrl)
}

if app.PasswordCredentials != nil {
var cred map[string]interface{}
for _, password := range d.Get("password").(*pluginsdk.Set).List() {
cred = password.(map[string]interface{})
}

if credentials := flattenApplicationPasswordCredentials(app.PasswordCredentials, cred); credentials != nil {
tf.Set(d, "password", credentials)
}
}
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved

logoImage := ""
if v := d.Get("logo_image").(string); v != "" {
logoImage = v
Expand Down
39 changes: 39 additions & 0 deletions internal/services/applications/application_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestAccApplication_basic(t *testing.T) {
check.That(data.ResourceName).Key("application_id").Exists(),
check.That(data.ResourceName).Key("client_id").Exists(),
check.That(data.ResourceName).Key("object_id").Exists(),
check.That(data.ResourceName).Key("app_password").DoesNotExist(),
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("acctest-APP-%d", data.RandomInteger)),
),
},
Expand Down Expand Up @@ -594,6 +595,27 @@ func TestAccApplication_logo(t *testing.T) {
})
}

func TestAccApplication_password(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_application", "test")
r := ApplicationResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.password(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("app_password.#").HasValue("1"),
check.That(data.ResourceName).Key("app_password.0.key_id").Exists(),
check.That(data.ResourceName).Key("app_password.0.value").Exists(),
check.That(data.ResourceName).Key("app_password.0.start_date").Exists(),
check.That(data.ResourceName).Key("app_password.0.end_date").Exists(),
check.That(data.ResourceName).Key("app_password.0.end_data_relative").Exists(),
check.That(data.ResourceName).Key("app_password.0.display_name").HasValue(data.ResourceName),
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
),
},
})
}

HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
func (r ApplicationResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) {
client := clients.Applications.ApplicationsClientBeta
client.BaseClient.DisableRetries = true
Expand Down Expand Up @@ -1572,3 +1594,20 @@ resource "azuread_application" "test" {
}
`, data.RandomInteger)
}

func (r ApplicationResource) password(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azuread" {}

data "azuread_client_config" "current" {}

resource "azuread_application" "test" {
display_name = "acctest-APP-%[1]d"
owners = [ data.azuread_client_config.current.object_id ]
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved

password {
display_name = "acctest-APP-%[1]d"
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
}
}
`, data.RandomInteger)
}
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved
80 changes: 80 additions & 0 deletions internal/services/applications/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,66 @@
return
}

func expandApplicationPasswordCredentials(input []interface{}) (*[]msgraph.PasswordCredential, error) {

Check failure on line 408 in internal/services/applications/applications.go

View workflow job for this annotation

GitHub Actions / golint

func `expandApplicationPasswordCredentials` is unused (unused)
if len(input) == 0 {
return nil, nil
}

result := make([]msgraph.PasswordCredential, 0)

for _, appPasswordRaw := range input {
if appPasswordRaw == nil {
continue
}

appPassword := appPasswordRaw.(map[string]interface{})

credential := msgraph.PasswordCredential{
DisplayName: pointer.To(appPassword["display_name"].(string)),
}

if v, ok := appPassword["start_date"]; ok && v.(string) != "" {
startDate, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return nil, fmt.Errorf("foo") //CredentialError{str: fmt.Sprintf("Unable to parse the provided start date %q: %+v", v, err), attr: "start_date"}

Check failure on line 429 in internal/services/applications/applications.go

View workflow job for this annotation

GitHub Actions / golint

commentFormatting: put a space between `//` and comment text (gocritic)
}
credential.StartDateTime = &startDate
}

var endDate *time.Time

if v, ok := appPassword["end_date"]; ok && v.(string) != "" {
var err error
expiry, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return nil, fmt.Errorf("foo") //CredentialError{str: fmt.Sprintf("Unable to parse the provided end date %q: %+v", v, err), attr: "end_date"}

Check failure on line 440 in internal/services/applications/applications.go

View workflow job for this annotation

GitHub Actions / golint

commentFormatting: put a space between `//` and comment text (gocritic)
}
endDate = &expiry
} else if v, ok := appPassword["end_date_relative"]; ok && v.(string) != "" {
d, err := time.ParseDuration(v.(string))
if err != nil {
return nil, fmt.Errorf("foo") //CredentialError{str: fmt.Sprintf("Unable to parse `end_date_relative` (%q) as a duration", v), attr: "end_date_relative"}

Check failure on line 446 in internal/services/applications/applications.go

View workflow job for this annotation

GitHub Actions / golint

commentFormatting: put a space between `//` and comment text (gocritic)
}

if credential.StartDateTime == nil {
expiry := time.Now().Add(d)
endDate = &expiry
} else {
expiry := credential.StartDateTime.Add(d)
endDate = &expiry
}
}

if endDate != nil {
credential.EndDateTime = endDate
}
result = append(result, credential)
}
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved

return &result, nil

}

func expandApplicationAppRoles(input []interface{}) *[]msgraph.AppRole {
result := make([]msgraph.AppRole, 0)

Expand Down Expand Up @@ -793,6 +853,26 @@
}}
}

func flattenApplicationPasswordCredentials(passwordCredentials *[]msgraph.PasswordCredential, d map[string]interface{}) []map[string]interface{} {
if d == nil {
return []map[string]interface{}{}
}

// update, read case
if d["key_id"] != nil {
if credRead := helpers.GetPasswordCredential(passwordCredentials, d["key_id"].(string)); credRead != nil {
return helpers.FlattenCredential(credRead, d)
}
}

// create case
for _, appCred := range *passwordCredentials {
return helpers.FlattenCredential(&appCred, d)
}

return []map[string]interface{}{}
}
HappyTobi marked this conversation as resolved.
Show resolved Hide resolved

func flattenApplicationWeb(in *msgraph.ApplicationWeb) []map[string]interface{} {
if in == nil {
return []map[string]interface{}{}
Expand Down
Loading