pulumi / pulumi-azuread

A Microsoft Azure Active Directory (Azure AD) Pulumi resource package, providing multi-language access to Azure AD
Apache License 2.0
18 stars 8 forks source link

When update an app registration, API permission are lost on Azure resource, but pulumi think they still there in the state file #1169

Open c-falardeau opened 5 months ago

c-falardeau commented 5 months ago

What happened?

When updating the app registration configuration after resource is already created in Azure (different run and commit), the API permissions are reset, but pulumi still contain them in the stack state file. The result is that pulumi won't put them back in the Azure resource, because it thinks they are already there because of the state file.

It happened when updating the Owners configuration. I don't know if there are other properties that trigger this. So to work around this, we need to run pulumi refresh (the API permissions will be deleted from the state file) and then pulumi up (the API permissions will be recreated)

Example

public class AppRegistrationDeployment()
{
    public void Deploy()
    {
        Application appRegistration = CreateAppRegistration();
        _ = AddRedirectUriToAppRegistration(appRegistration);
        _ = AddCertificateToAppRegistration(appRegistration);
        _ = AddApiAccessToAppRegistration(appRegistration);
    }

    private static Application CreateAppRegistration()
    {
        var application = new Application(
            "MyAppRegistration",
            new ApplicationArgs
            {
                DisplayName = "MyAppRegistrationDisplayNAme",
                SignInAudience = "AzureADMultipleOrgs",
                Owners = ["OwnersHere"], // <---- When this change, the API permission are lost, but pulumi doesn't know. So run pulumi up multiple times with different owners
            }
        );
        return application;
    }

    private static Application AddRedirectUriToAppRegistration(Application appRegistration)
    {
        _ = new ApplicationRedirectUris(
            "MyAppRegistrationRedirectUris",
            new ApplicationRedirectUrisArgs
            {
                ApplicationId = appRegistration.Id,
                RedirectUris = ["https://localhost:3000"],
                Type = "Web"
            }
        );

        return appRegistration;
    }

    private ApplicationCertificate AddCertificateToAppRegistration(Application appRegistration)
    {
        string certificateSecret = "CertificateSecretHere";

        byte[] certBytes = Convert.FromBase64String(certificateSecret);

        // Load the certificate from the byte array
        var cert = new X509Certificate2(certBytes);

        var x = new ApplicationCertificate(
            "MyAppRegistrationCertificate",
            new ApplicationCertificateArgs()
            {
                ApplicationId = appRegistration.Id,
                Value = cert.GetRawCertDataString(),
                Type = "AsymmetricX509Cert",
                Encoding = "hex",
                EndDate = cert.NotAfter.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
            },
            new CustomResourceOptions() { Parent = appRegistration, DependsOn = appRegistration }
        );
        return x;
    }

    private static ApplicationApiAccess AddApiAccessToAppRegistration(Application appRegistration)
    {
        Output<GetApplicationPublishedAppIdsResult> wellKnownApps = GetApplicationPublishedAppIds.Invoke();
        Output<string> msGraphAppClientId = wellKnownApps.Apply(x =>
            x.Result[MicrosoftGraphApiConstants.MicrosoftGraph]
        );

        Output<GetServicePrincipalResult> msGraphServicePrincipal = GetServicePrincipal.Invoke(
            new GetServicePrincipalInvokeArgs { ClientId = msGraphAppClientId }
        );

        return new ApplicationApiAccess(
            "MyAppRegistrationApiAccess",
            new ApplicationApiAccessArgs
            {
                ApplicationId = appRegistration.Id,
                ApiClientId = msGraphAppClientId,
                RoleIds = msGraphServicePrincipal.Apply(x =>
                    MicrosoftGraphApiConstants.Permissions.Select(permission => x.AppRoleIds[permission]).ToArray()
                ),
            },
            new CustomResourceOptions { Parent = appRegistration, DependsOn = appRegistration, }
        );
    }
}

Output of pulumi about

running 'dotnet build -nologo .'                                                                                                                                                                                                                                                                                          
  Determining projects to restore...                                                                                                                                                                                                                                                                                      

  Infrastructure -> C:\git\XXXXXXX\backend\XXXXXX\bin\Debug\net8.0\XXXXXX.dll                                                                                                                                                                                                              

Build succeeded.                                                                                                                                                                                                                                                                                                          

    0 Warning(s)                                                                                                                                                                                                                                                                                                          
    0 Error(s)                                                                                                                                                                                                                                                                                                            

Time Elapsed 00:00:01.26                                                                                                                                                                                                                                                                                                  

'dotnet build -nologo .' completed successfully                                                                                                                                                                                                                                                                           
CLI                                                                                                                                                                                                                                                                                                                       
Version      3.121.0                                                                                                                                                                                                                                                                                                      
Go Version   go1.22.4                                                                                                                                                                                                                                                                                                     
Go Compiler  gc                                                                                                                                                                                                                                                                                                           

Plugins                                                                                                                                                                                                                                                                                                                   
KIND      NAME          VERSION                                                                                                                                                                                                                                                                                           
resource  aws           5.42.0                                                                                                                                                                                                                                                                                            
resource  azure         5.43.0                                                                                                                                                                                                                                                                                            
resource  azure-native  2.30.0                                                                                                                                                                                                                                                                                            
resource  azuread       5.47.2                                                                                                                                                                                                                                                                                            
language  dotnet        unknown                                                                                                                                                                                                                                                                                           
resource  mssql         0.0.8                                                                                                                                                                                                                                                                                             
resource  random        4.13.2                                                                                                                                                                                                                                                                                            

Host                                                                                                                                                                                                                                                                                                                      
OS       Microsoft Windows 11 Pro                                                                                                                                                                                                                                                                                         
Version  10.0.22631 Build 22631                                                                                                                                                                                                                                                                                           
Arch     x86_64                                                                                                                                                                                                                                                                                                           

This project is written in dotnet: executable='C:\Program Files\dotnet\dotnet.exe' version='8.0.206'                                                                                                                                                                                                                      

......                                                                                                                                                                                                                                                                                                                          

Found no pending operations associated with dev-bt00                                                                                                                                                                                                                                                                      

Backend                                                                                                                                                                                                                                                                                                                   
Name           XXXXXXXXXXXXXXXX                                                                                                                                                                                                                                                                                         
URL            azblob://xxxxxxxx?storage_account=xxxxxxxxxxx
User           AzureAD\XXXXXXXXXXXXXXX                                                                                                                                                                                                                                                                                 
Organizations                                                                                                                                                                                                                                                                                                             
Token type     personal                                                                                                                                                                                                                                                                                                   

Dependencies:                                                                                                                                                                                                                                                                                                             
NAME                       VERSION                                                                                                                                                                                                                                                                                        
XXXXXX.Infrastructure      1.2.17                                                                                                                                                                                                                                                                                         
Microsoft.Identity.Client  4.61.2                                                                                                                                                                                                                                                                                         

Pulumi locates its logs in C:\Users\XXXXXX\AppData\Local\Temp by default                                                                                                                                                                                                                                                

Additional context

No response

Contributing

Vote on this issue by adding a 👍 reaction. To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).

justinvp commented 5 months ago

This looks specific to the azuread provider. Moving to that repo for further triage.

danielrbradley commented 4 months ago

Hi @c-falardeau I think I re-created the issue using just your code for the the Application and ApplicationRedirectUris:

using Pulumi;
using Pulumi.AzureAD;

return await Pulumi.Deployment.RunAsync(() =>
{
    var application = new Application(
        "MyAppRegistration",
        new ApplicationArgs
        {
            DisplayName = "MyAppRegistrationDisplayNAme",
            SignInAudience = "AzureADMultipleOrgs",
            Owners = new InputList<string> { "REPLACE_WITH_OBJECT_ID" },
        }
    );
    _ = new ApplicationRedirectUris(
            "MyAppRegistrationRedirectUris",
            new ApplicationRedirectUrisArgs
            {
                ApplicationId = application.Id,
                RedirectUris = new InputList<string> { "https://localhost:3000" },
                Type = "Web"
            }
        );
});

Steps:

  1. Deploy
  2. Update owner and re-deploy
  3. Refresh - see that MyAppRegistrationRedirectUris has been deleted

Does that accurately summarise your issue above?

Cause

The ApplicationRedirectUris is also a sub-property of the Application resource. The above code is equivelent to:

return await Pulumi.Deployment.RunAsync(() =>
{
    var application = new Application(
        "MyAppRegistration",
        new ApplicationArgs
        {
            DisplayName = "MyAppRegistrationDisplayNAme",
            SignInAudience = "AzureADMultipleOrgs",
            Owners = new InputList<string> { "db8fa9c3-36b4-4474-91b0-aa96bcc706bc" }, // <---- When this change, the API permission are lost, but pulumi doesn't know. So run pulumi up multiple times with different owners
            Web = new ApplicationWebArgs
            {
                RedirectUris = new InputList<string> { "https://localhost:3000" }
            }
        }
    );
});

This is a slightly odd design of the upstream provider to provide both methods of specifying the RedirectUris and results in unexpected behaviour when updating the original resource having made changes via the secondary resources (such as ApplicationRedirectUris). When you make any change to the original Application resource it entirely overwrites the state of the resource, inadvertantly removing the RedirectUris configuration which manifests as the deletion of MyAppRegistrationRedirectUris on the next refresh.

You can also see this by:

  1. Performing a clean deployment of the original code
  2. Run a refresh. This will show the property being populated on the Application resource to reflect the change made by the standalone resource.

Workaround

Where there's conflicts betwen a feature being managed as part of the parent and as it's own resource, it's recommended to only manage the feature via the property on the parent and not as an external resource. This will ensure the lifecycle isn't broken

Addressing the core issue

This would need to be fixed in the core provider implementation. You can search for existing issues or open a new issue if one doesn't yet exist here: https://github.com/hashicorp/terraform-provider-azuread

We've had similar issues in the Azure Native provider historically and have been able to resolve this issue by introducing resource parent-child awareness to allow parts of the parent resource to be managed via standalone resources: https://github.com/pulumi/pulumi-azure-native/issues/1112. Unfortunately, given we don't mange the implementation of the resource behaviours directly for this provider, we would be unable to address this problem directly.

Please let me know if I've mis-understood your problem or if you have any follow-up questions.

c-falardeau commented 4 months ago

Hi @c-falardeau I think I re-created the issue using just your code for the the Application and ApplicationRedirectUris:

using Pulumi;
using Pulumi.AzureAD;

return await Pulumi.Deployment.RunAsync(() =>
{
    var application = new Application(
        "MyAppRegistration",
        new ApplicationArgs
        {
            DisplayName = "MyAppRegistrationDisplayNAme",
            SignInAudience = "AzureADMultipleOrgs",
            Owners = new InputList<string> { "REPLACE_WITH_OBJECT_ID" },
        }
    );
    _ = new ApplicationRedirectUris(
            "MyAppRegistrationRedirectUris",
            new ApplicationRedirectUrisArgs
            {
                ApplicationId = application.Id,
                RedirectUris = new InputList<string> { "https://localhost:3000" },
                Type = "Web"
            }
        );
});

Steps:

  1. Deploy
  2. Update owner and re-deploy
  3. Refresh - see that MyAppRegistrationRedirectUris has been deleted

Does that accurately summarise your issue above?

Cause

The ApplicationRedirectUris is also a sub-property of the Application resource. The above code is equivelent to:

return await Pulumi.Deployment.RunAsync(() =>
{
    var application = new Application(
        "MyAppRegistration",
        new ApplicationArgs
        {
            DisplayName = "MyAppRegistrationDisplayNAme",
            SignInAudience = "AzureADMultipleOrgs",
            Owners = new InputList<string> { "db8fa9c3-36b4-4474-91b0-aa96bcc706bc" }, // <---- When this change, the API permission are lost, but pulumi doesn't know. So run pulumi up multiple times with different owners
            Web = new ApplicationWebArgs
            {
                RedirectUris = new InputList<string> { "https://localhost:3000" }
            }
        }
    );
});

This is a slightly odd design of the upstream provider to provide both methods of specifying the RedirectUris and results in unexpected behaviour when updating the original resource having made changes via the secondary resources (such as ApplicationRedirectUris). When you make any change to the original Application resource it entirely overwrites the state of the resource, inadvertantly removing the RedirectUris configuration which manifests as the deletion of MyAppRegistrationRedirectUris on the next refresh.

You can also see this by:

  1. Performing a clean deployment of the original code
  2. Run a refresh. This will show the property being populated on the Application resource to reflect the change made by the standalone resource.

Workaround

Where there's conflicts betwen a feature being managed as part of the parent and as it's own resource, it's recommended to only manage the feature via the property on the parent and not as an external resource. This will ensure the lifecycle isn't broken

Addressing the core issue

This would need to be fixed in the core provider implementation. You can search for existing issues or open a new issue if one doesn't yet exist here: https://github.com/hashicorp/terraform-provider-azuread

We've had similar issues in the Azure Native provider historically and have been able to resolve this issue by introducing resource parent-child awareness to allow parts of the parent resource to be managed via standalone resources: pulumi/pulumi-azure-native#1112. Unfortunately, given we don't mange the implementation of the resource behaviours directly for this provider, we would be unable to address this problem directly.

Please let me know if I've mis-understood your problem or if you have any follow-up questions.

Hello danielrbradley, thanks you for you response.

Yes it matches the behavior well, but I can only confirm reproducing the behavior for the Owners fields. Other child resources or fields we did not tested thoroughly

Do I need to do something else at this stage ?

Best regards.

thomas11 commented 4 months ago

Hi @c-falardeau, there's nothing else to do here at this point except the two points Workaround and Addressing the core issue outlined by Daniel above. Should the workaround not work for you, please report back here.