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

Conversation

HappyTobi
Copy link
Contributor

Add new inline property "app_password"

The app_password generate a application credential with the first request where the application is created.
The improvement here is that ist a bit faster than the external "application_password_resource" creation because there is no wait time until the credential can be used.

The feature set of the "app_password" is equal to "application_password_resource"

Regards

@manicminer
Copy link
Contributor

manicminer commented May 24, 2024

Hi @HappyTobi, thanks for submitting this PR. I agree that it would be nice for it to be quicker when adding a password credential to an application, however there are several platform limitations that necessitate the extensive checks we have put in place. Due to internal architectural and eventual consistency constraints, it can actually take up to 20 minutes for a newly created application and its password credential(s) to be usable for authentication.

Adding these properties to the azuread_application resource would not alleviate these issues, in fact we'd need to add extra checks which would be likely to take longer than it currently does. Additionally, if we were to add these, we'd need to support substituting any number of azuread_application_password resources in a declarative manner (i.e. be able to manage all password for an application). Due to the complexity involved, it's unlikely we'll add this to the azuread_application resource, particularly as the direction in which we are leaning is more towards composing the application into further separate resources.

As such, whilst I'd like to thank you again for looking at this, I'm going to close this for now since, as mentioned above, this is not the general direction we're heading in, and if we were to add this, there would be substantially more checks and tests required in order to avoid breaking existing configurations, and to ensure that the new block worked for multiple passwords in multiple scenarios.

@manicminer manicminer closed this May 24, 2024
@manicminer manicminer reopened this May 24, 2024
@github-actions github-actions bot added size/XL and removed size/L labels May 27, 2024
@github-actions github-actions bot added size/L and removed size/XL labels May 28, 2024
@manicminer manicminer self-requested a review June 17, 2024 13:24
Copy link
Contributor

@manicminer manicminer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @HappyTobi, this is looking good but there are some changes we will need to make to separate the logic for Create and Read, which I have added inline below.

Additionally, there is currently no Update implementation, so I've put together an initial suggestion with which the tests seem to pass. In the Update function, after setting the application logo, we can add something like this to ensure any password changes are effected. This is quite lengthy in order to handle eventual consistency issues.

	// Remove and/or set a new application password, if changed
	if d.HasChange("password") {
		oldPasswordRaw, newPasswordRaw := d.GetChange("password")
		oldPasswordBlock := oldPasswordRaw.(*pluginsdk.Set).List()
		oldPassword := make(map[string]interface{})
		if len(oldPasswordBlock) > 0 {
			oldPassword = oldPasswordBlock[0].(map[string]interface{})
		}

		if oldPassword["key_id"] != nil {
			keyIdToRemove := oldPassword["key_id"].(string)
			if _, err = client.RemovePassword(ctx, id.ApplicationId, keyIdToRemove); err != nil {
				return tf.ErrorDiagF(err, "Removing password credential %q from application with object ID %q", id.ApplicationId, keyIdToRemove)
			}

			// Wait for application password to be deleted
			if err = helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) {
				defer func() { client.BaseClient.DisableRetries = false }()
				client.BaseClient.DisableRetries = true

				app, _, err := client.Get(ctx, id.ApplicationId, odata.Query{})
				if err != nil {
					return nil, err
				}

				credential := helpers.GetPasswordCredential(app.PasswordCredentials, keyIdToRemove)
				if credential == nil {
					return pointer.To(false), nil
				}

				return pointer.To(true), nil
			}); err != nil {
				return tf.ErrorDiagF(err, "Waiting for deletion of password credential %q from application with object ID %q", keyIdToRemove, id.ApplicationId)
			}
		}

		newPasswordBlock := newPasswordRaw.(*pluginsdk.Set).List()
		if len(newPasswordBlock) > 1 {
			return tf.ErrorDiagPathF(errors.New("`password` must have one element"), "password", "Multiple passwords are not supported with this resource")
		}

		// Proceed to add a new password to replace the now-removed one, if the password block is present in the configuration
		if len(newPasswordBlock) > 0 {
			newPassword := newPasswordBlock[0].(map[string]interface{})

			credential, err := helpers.PasswordCredential(newPassword)
			if err != nil {
				attr := ""
				if kerr, ok := err.(helpers.CredentialError); ok {
					attr = kerr.Attr()
				}
				return tf.ErrorDiagPathF(err, attr, "Generating password credential for %s", id.ApplicationId)
			}

			newCredential, _, err := client.AddPassword(ctx, id.ApplicationId, *credential)
			if err != nil {
				return tf.ErrorDiagF(err, "Adding password for application with object ID %q", id.ApplicationId)
			}
			if newCredential == nil {
				return tf.ErrorDiagF(errors.New("nil credential received when adding password"), "API error adding password for application with object ID %q", id.ApplicationId)
			}
			if newCredential.KeyId == nil {
				return tf.ErrorDiagF(errors.New("nil or empty keyId received"), "API error adding password for application with object ID %q", id.ApplicationId)
			}
			if newCredential.SecretText == nil || len(*newCredential.SecretText) == 0 {
				return tf.ErrorDiagF(errors.New("nil or empty password received"), "API error adding password for application with object ID %q", id.ApplicationId)
			}

			// Wait for the credential to appear in the application manifest, this can take several minutes
			timeout, _ := ctx.Deadline()
			polledForCredential, err := (&pluginsdk.StateChangeConf{ //nolint:staticcheck
				Pending:                   []string{"Waiting"},
				Target:                    []string{"Done"},
				Timeout:                   time.Until(timeout),
				MinTimeout:                1 * time.Second,
				ContinuousTargetOccurence: 5,
				Refresh: func() (interface{}, string, error) {
					app, _, err := client.Get(ctx, id.ApplicationId, odata.Query{})
					if err != nil {
						return nil, "Error", err
					}

					if app.PasswordCredentials != nil {
						for _, cred := range *app.PasswordCredentials {
							if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, *newCredential.KeyId) {
								return &cred, "Done", nil
							}
						}
					}

					return nil, "Waiting", nil
				},
			}).WaitForStateContext(ctx)

			if err != nil {
				return tf.ErrorDiagF(err, "Waiting for password credential for application with object ID %q", id.ApplicationId)
			} else if polledForCredential == nil {
				return tf.ErrorDiagF(errors.New("password credential not found in application manifest"), "Waiting for password credential for application with object ID %q", id.ApplicationId)
			}

			// Ensure the new value is persisted to state
			newPassword["key_id"] = pointer.From(newCredential.KeyId)
			newPassword["value"] = pointer.From(newCredential.SecretText)
			tf.Set(d, "password", []interface{}{newPassword})
		}
	}

Additionally, in the Update function, we should add an internal lock against the application object to prevent clobbering when the azuread_application_password resource is also used. This should possibly have been added here already, but it will definitely be needed now.

	// add near the top of the Update function, after parsing the resource ID but before reading any values from config

	tf.LockByName(applicationResourceName, id.ApplicationId)
	defer tf.UnlockByName(applicationResourceName, id.ApplicationId)

We will also need to document the password property. Can you write up a proposal for explaining the use case for this? It doesn't need to be long, ideally something that will fit into an info box, for example:

Screenshot 2024-06-18 at 21 30 03

If you can look at incorporating these changes, and there are no test breakages in the applications package, this should be good to merge. Thanks!

internal/helpers/credentials.go Outdated Show resolved Hide resolved
internal/services/applications/application_resource.go Outdated Show resolved Hide resolved
internal/services/applications/applications.go Outdated Show resolved Hide resolved
internal/services/applications/application_resource.go Outdated Show resolved Hide resolved
internal/services/applications/application_resource.go Outdated Show resolved Hide resolved
internal/services/applications/applications.go Outdated Show resolved Hide resolved
@HappyTobi
Copy link
Contributor Author

ACC tests runs fine:

--- PASS: TestAccApplication_password (25.09s)
--- PASS: TestAccApplication_passwordUpdate (52.37s)

@manicminer
Copy link
Contributor

Test results

Screenshot 2024-06-25 at 14 52 14

@manicminer manicminer added this to the v2.53.0 milestone Jun 25, 2024
@manicminer manicminer merged commit bfaeb83 into hashicorp:main Jun 25, 2024
28 checks passed
manicminer added a commit that referenced this pull request Jun 25, 2024
dduportal referenced this pull request in jenkins-infra/azure Jun 28, 2024
<Actions>
<action
id="6d17e7acdb2f3311576150379e22805f2f9b4aa72ff00ec136aceee45cae4b98">
        <h3>Bump Terraform `azuread` provider version</h3>
<details
id="1d9343c012f5434ac9fe8a98135bae3667b399259be16d9b14302ea3bd424a24">
            <summary>Update Terraform lock file</summary>
<p>changes detected:&#xA;&#x9;&#34;hashicorp/azuread&#34; updated from
&#34;2.52.0&#34; to &#34;2.53.0&#34; in file
&#34;.terraform.lock.hcl&#34;</p>
            <details>
                <summary>2.53.0</summary>
<pre>Changelog retrieved
from:&#xA;&#x9;https://github.com/hashicorp/terraform-provider-azuread/releases/tag/v2.53.0&#xA;ENHANCEMENTS:&#xA;&#xA;*
`azuread_application` - support for the `password` block
([#1389](https://github.com/hashicorp/terraform-provider-azuread/issues/1389))&#xA;&#xA;BUG
FIXES:&#xA;&#xA;* `azuread_claims_mapping_policy` - set the correct
timeouts for this resource
([#1419](hashicorp/terraform-provider-azuread#1419
`azuread_service_principal_claims_mapping_policy_assignment` - set the
correct timeouts for this resource
([#1419](hashicorp/terraform-provider-azuread#1419
`azuread_synchronization_secret` - set the correct timeouts for this
resource
([#1419](https://github.com/hashicorp/terraform-provider-azuread/issues/1419))&#xA;&#xA;&#xA;</pre>
            </details>
        </details>
<a
href="https://infra.ci.jenkins.io/job/updatecli/job/azure/job/main/286/">Jenkins
pipeline link</a>
    </action>
</Actions>

---

<table>
  <tr>
    <td width="77">
<img src="https://www.updatecli.io/images/updatecli.png" alt="Updatecli
logo" width="50" height="50">
    </td>
    <td>
      <p>
Created automatically by <a
href="https://www.updatecli.io/">Updatecli</a>
      </p>
      <details><summary>Options:</summary>
        <br />
<p>Most of Updatecli configuration is done via <a
href="https://www.updatecli.io/docs/prologue/quick-start/">its
manifest(s)</a>.</p>
        <ul>
<li>If you close this pull request, Updatecli will automatically reopen
it, the next time it runs.</li>
<li>If you close this pull request and delete the base branch, Updatecli
will automatically recreate it, erasing all previous commits made.</li>
        </ul>
        <p>
Feel free to report any issues at <a
href="https://github.com/updatecli/updatecli/issues">github.com/updatecli/updatecli</a>.<br
/>
If you find this tool useful, do not hesitate to star <a
href="https://github.com/updatecli/updatecli/stargazers">our GitHub
repository</a> as a sign of appreciation, and/or to tell us directly on
our <a
href="https://matrix.to/#/#Updatecli_community:gitter.im">chat</a>!
        </p>
      </details>
    </td>
  </tr>
</table>

Co-authored-by: Jenkins Infra Bot (updatecli) <60776566+jenkins-infra-bot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants