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

In Azure MS Entra ID custom application registration AzureAD.AppRoleAssignment disappears on the first run of the Azure DevOps deployment pipeline #1785

Open
rkolmakov337 opened this issue Jan 7, 2025 · 7 comments
Labels
kind/bug Some behavior is incorrect or out of spec

Comments

@rkolmakov337
Copy link

I use MS Azure DevOps pipeline to deploy our web API and one of the tasks is to deploy IoC using Pulumi. I created customer application registration in Microsoft Entra ID using AzureAD.Application. I assigned roles to the app registration using AzureAD.AppRoleAssignment.

The issue is when I run the Azure DevOps pipeline again to deploy new code to production sometimes the roles disappear from the custom application registration in Microsoft Entra ID. I need to rerun the pipeline for the roles to reappear in the custom app registration. The infrastructure code was written in C# .NET 8.0. I did not make any changes to it.

Why do the roles (AzureAD.AppRoleAssignment) disappear from the custom app registration (AzureAD.Application)? Why do I need to rerun the pipeline for them to reappear? Is there a way to fix this?

Thank you very much.

@pulumi-bot pulumi-bot added the needs-triage Needs attention from the triage team label Jan 7, 2025
@thomas11
Copy link
Contributor

thomas11 commented Jan 8, 2025

Hi @rkolmakov337, it seems like you might be hitting the same problem as pulumi/pulumi-azure-native#1169. Could you try what Daniel wrote there under "Workaround"?

@thomas11 thomas11 added kind/bug Some behavior is incorrect or out of spec awaiting-feedback Blocked on input from the author and removed needs-triage Needs attention from the triage team labels Jan 8, 2025
@rkolmakov337
Copy link
Author

Hello @thomas11, thank you for the reply. I was not able to locate the workaround. Could you provide a link to it? Thank you.

@pulumi-bot pulumi-bot added needs-triage Needs attention from the triage team and removed awaiting-feedback Blocked on input from the author labels Jan 8, 2025
@thomas11
Copy link
Contributor

thomas11 commented Jan 8, 2025

Sorry, I linked to the wrong issue. This is the comment containing the workaround..

@rkolmakov337
Copy link
Author

rkolmakov337 commented Jan 8, 2025

I appreciated the quick reply @thomas11 . Please view below the configuration of the Azure AD custom application for my web API with two AppRoles ("Todo.Read" and "Todo.Write"). The AppRoles do not disappear from the application configuration. The issue is that the Pulumi.AzureAD.AppRoleAssignment disappears for the client who consumes my web API. The consumer gets 401 (Unauthorized). Please view the code below.

var application = new Pulumi.AzureAD.Application($"appreg-myapp-dev", new AzureAD.ApplicationArgs
{
    DisplayName = $"API Server dev",
    IdentifierUris =
    {
        $"api://app-myapp-dev-server"
    },
    Api = new ApplicationApiArgs
    {
        RequestedAccessTokenVersion = 2
    },
    SinglePageApplication = new ApplicationSinglePageApplicationArgs
    {
        RedirectUris =
        {
            Output.Format($"https://{server.DefaultHostName}/swagger/oauth2-redirect.html"),
            "https://localhost:7167/swagger/oauth2-redirect.html"
        }
    },
    AppRoles =
    {
        CreateRole(todoReadRoleUuid.Result, "Todo.Read"),
        CreateRole(todoWriteRoleUuid.Result, "Todo.Write")
    }
});

private static ApplicationAppRoleArgs CreateRole(Output<string> id, string name)
{
    return new ApplicationAppRoleArgs
    {
        AllowedMemberTypes =
        {
            "Application"
        },
        DisplayName = name,
        Enabled = true,
        Value = name,
        Description = name,
        Id = id
    };
}

Pulumi.AzureAD.AppRoleAssignment configuration:

 private Pulumi.AzureAD.AppRoleAssignment AssignRead(string prefix, string environment, string assigneeName, Output<string> principalObjectId)
 {
     return new Pulumi.AzureAD.AppRoleAssignment(
         $"appra-{prefix}-{assigneeName}-read-server-{environment}",
         new AzureAD.AppRoleAssignmentArgs
         {
             AppRoleId = TodoReadRoleUuid,
             PrincipalObjectId = principalObjectId,
             ResourceObjectId = ServicePrincipalObjectId,
         });
 }

 private Pulumi.AzureAD.AppRoleAssignment AssignWrite(string prefix, string environmnent, string assigneeName, Output<string> principalObjectId)
 {
     return new Pulumi.AzureAD.AppRoleAssignment(
         $"appra-{prefix}-{assigneeName}-write-server-{environmnent}",
         new AzureAD.AppRoleAssignmentArgs
         {
             AppRoleId = TodoWriteRoleUuid,
             PrincipalObjectId = principalObjectId,
             ResourceObjectId = ServicePrincipalObjectId
         });
 }

 public void AssignRoles(string prefix, string assigneeName, string environment, Output<string> principalObjectId)
 {
     AssignRead(prefix, environment, assigneeName, principalObjectId);

     AssignWrite(prefix, environment, assigneeName, principalObjectId);
 }

I call these methods in my stack constructor:

server.AssignRoles("myapp", "flowgear", "dev", principalObjectId);

@thomas11 thomas11 transferred this issue from pulumi/pulumi-azure-native Jan 10, 2025
@thomas11
Copy link
Contributor

Hi @rkolmakov337, thanks for the added context. Unfortunately, I'm still not 100% clear on how everything fits together. What's the definition of todoReadRoleUuid, and is it different from uppercase TodoReadRoleUuid?

Ideally, could you post a complete runnable sample program? Otherwise it might be hard to diagnose your problem.

@thomas11 thomas11 added awaiting-feedback Blocked on input from the author and removed needs-triage Needs attention from the triage team labels Jan 13, 2025
@rkolmakov337
Copy link
Author

rkolmakov337 commented Jan 14, 2025

Hello @thomas11,

Thank you for your reply. I appreciate your help. Please view the code below for web server, pulumi stack, and external client. The issue is that Pulumi.AzureAD.AppRoleAssignment(s) gets deleted on the first run of the Azure DevOps pipeline. The second run of the Azure DevOps pipeline adds the Pulumi.AzureAD.AppRoleAssignment(s) back. I have to manually verify that Role Assignments for each external client have not been deleted. It is fine for one or two external clients but for many it becomes an issue.

I thought that there was a problem with the permissions of the service principal of the Azure DevOps pipeline but the Pulumi.AzureAD.AppRoleAssignment(s) get deleted and on the second attempt they get added back. So the principal has sufficient permissions to execute operations in the Azure AD.

The log of Azure DevOps pipeline has these lines (Attempt #1):

~ azuread:index/application:Application: (update) <-- I did not make any changes for Azure AD Application

Resources:
~ 2 to update
21 unchanged

using Pulumi;
using Pulumi.AzureAD.Inputs;
using Pulumi.AzureNative.Resources;
using Pulumi.AzureNative.Web;
using Pulumi.AzureNative.Web.Inputs;
using Pulumi.Random;
using AzureAD = Pulumi.AzureAD;

namespace EnterpriseSubscriber.Infrastructure;

public class Server
{
    private const string AppType = "server";

    public Server(string prefix, string environment, ResourceGroup resourceGroup, ServerServicePlan servicePlan)
    {
        var appName = $"app-qsr-{prefix}-{environment}";

        var server = new WebApp(appName, new()
        {
            Name = appName,
            ResourceGroupName = resourceGroup.Name,
            ServerFarmId = servicePlan.Id,
            Location = resourceGroup.Location,
            HttpsOnly = true,
            SiteConfig = new SiteConfigArgs
            {
                AlwaysOn = false,
                AppSettings =
                [
                    new NameValuePairArgs
                    { 
                        Name = "ASPNETCORE_ENVIRONMENT", 
                        Value = "Development"
                    }
                ],
                NetFrameworkVersion = "v6.0",
                HealthCheckPath = "/health"
            },
            Identity = new ManagedServiceIdentityArgs
            {
                Type = Pulumi.AzureNative.Web.ManagedServiceIdentityType.SystemAssigned
            }
        });

        var todoReadRoleUuid = new RandomUuid($"uuid-{prefix}-todo-read-role-{AppType}-{environment}");
        var todoWriteRoleUuid = new RandomUuid($"uuid-{prefix}-todo-write-role-{AppType}-{environment}");

        var application = new AzureAD.Application($"appreg-{prefix}-{AppType}-{environment}", new AzureAD.ApplicationArgs
        {
            DisplayName = $"My Web API Server {environment}",
            IdentifierUris =
            {
                $"api://{appName}-{AppType}"
            },
            Api = new ApplicationApiArgs
            {
                RequestedAccessTokenVersion = 2
            },
            SinglePageApplication = new ApplicationSinglePageApplicationArgs
            {
                RedirectUris =
                {
                    Output.Format($"https://{server.DefaultHostName}/swagger/oauth2-redirect.html"),
                    "https://localhost:7167/swagger/oauth2-redirect.html"
                }
            },
            AppRoles =
            {
                CreateRole(todoReadRoleUuid.Result, "Todo.Read"),
                CreateRole(todoWriteRoleUuid.Result, "Todo.Write")
            }
        });

        var principal = new AzureAD.ServicePrincipal($"sp-{prefix}-{AppType}-{environment}", new AzureAD.ServicePrincipalArgs
        {
            ClientId = application.ClientId
        });

        ApplicationClientId = application.ClientId;
        ServicePrincipalObjectId = principal.ObjectId;
        TodoReadRoleUuid = todoReadRoleUuid.Result;
        TodoWriteRoleUuid = todoWriteRoleUuid.Result;
    }

    public Output<string> ApplicationClientId { get; private set; }

    public Output<string> ServicePrincipalObjectId { get; set; }

    public Output<string> TodoReadRoleUuid { get; set; }

    public Output<string> TodoWriteRoleUuid { get; set; }

    private static ApplicationAppRoleArgs CreateRole(Output<string> id, string name)
    {
        return new ApplicationAppRoleArgs
        {
            AllowedMemberTypes =
            {
                "Application"
            },
            DisplayName = name,
            Enabled = true,
            Value = name,
            Description = name,
            Id = id
        };
    }

    private AzureAD.AppRoleAssignment AssignRead(string prefix, string environment, string assigneeName, Output<string> principalObjectId)
    {
        return new AzureAD.AppRoleAssignment(
            $"appra-{prefix}-{assigneeName}-read-{AppType}-{environment}",
            new AzureAD.AppRoleAssignmentArgs
            {
                AppRoleId = TodoReadRoleUuid,
                PrincipalObjectId = principalObjectId,
                ResourceObjectId = ServicePrincipalObjectId,
            });
    }

    private AzureAD.AppRoleAssignment AssignWrite(string prefix, string environmnent, string assigneeName, Output<string> principalObjectId)
    {
        return new AzureAD.AppRoleAssignment(
            $"appra-{prefix}-{assigneeName}-write-{AppType}-{environmnent}",
            new AzureAD.AppRoleAssignmentArgs
            {
                AppRoleId = TodoWriteRoleUuid,
                PrincipalObjectId = principalObjectId,
                ResourceObjectId = ServicePrincipalObjectId
            });
    }

    public void AssignRoles(string prefix, string assigneeName, string environment, Output<string> principalObjectId)
    {
        AssignRead(prefix, environment, assigneeName, principalObjectId);

        AssignWrite(prefix, environment, assigneeName, principalObjectId);
    }

    public void AssignRoles(string prefix, string environment, ExternalClient client)
    {
        AssignRead(prefix, environment, client.AppName, client.PrincipalObjectId);

        AssignWrite(prefix, environment, client.AppName, client.PrincipalObjectId);
    }
}

Here is my app pulumi stack:

using Microsoft.Extensions.Options;
using Pulumi;
using Pulumi.AzureNative.Authorization;
using Pulumi.AzureNative.Resources;
using System;

namespace MyApp.Infrastructure;

public class MyAppStack : Stack
{
    const string Prefix = "my-app";

    public MyAppStack()
    {
        var environment = "Development";

        var resourceGroupName = $"rg-{Prefix}-{environment}";
        var resourceGroup = new ResourceGroup(resourceGroupName, new()
        {
            ResourceGroupName = resourceGroupName,
            Location = location
        });


	var externalClient = new ExternalClient(Prefix, "Flowgear", environment);

        var server = new Server(Prefix, environment, resourceGroup, serverServicePlan);

        server.AssignRoles(Prefix, environment, externalClient);
    }
}

Here is the external client code:

using Pulumi;
using AzureAD = Pulumi.AzureAD;

namespace MyApp.Infrastructure;

public class ExternalClient
{
    private const string AppType = "external-client";

    public ExternalClient(string prefix, string assigneeName, string environment)
    {
        AppName = $"{assigneeName.Replace(" ", "").ToLower()}-{AppType}";

        var name = $"appreg-{prefix}-{AppName}-{environment}";

        var application = new AzureAD.Application(name,
            new AzureAD.ApplicationArgs
            {
                DisplayName = $"My App {assigneeName} {environment}",
                Api = new AzureAD.Inputs.ApplicationApiArgs
                {
                    RequestedAccessTokenVersion = 2
                }
            });

        var applicationSecret = new AzureAD.ApplicationPassword($"apppwd-{prefix}-{AppName}-{environment}",
            new AzureAD.ApplicationPasswordArgs
            {
                ApplicationId = application.Id
            },
            new CustomResourceOptions
            {
                AdditionalSecretOutputs =
                {
                    "value"
                }
            });

        var servicePrincipal = new AzureAD.ServicePrincipal($"sp-{prefix}-{AppName}-{environment}",
            new AzureAD.ServicePrincipalArgs
            {
                ClientId = application.ClientId
            });

        ClientId = application.ClientId;
        ClientSecret = applicationSecret.Value;
        PrincipalObjectId = servicePrincipal.ObjectId;
    }

    public Output<string> ClientId { get; set; }

    public Output<string> ClientSecret { get; set; }

    public Output<string> PrincipalObjectId { get; set; }

    public string AppName { get; set; }
}

@pulumi-bot pulumi-bot added needs-triage Needs attention from the triage team and removed awaiting-feedback Blocked on input from the author labels Jan 14, 2025
@thomas11 thomas11 removed the needs-triage Needs attention from the triage team label Jan 17, 2025
@thomas11
Copy link
Contributor

Thank you for the sample program! We'll take a deeper look when we can.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/bug Some behavior is incorrect or out of spec
Projects
None yet
Development

No branches or pull requests

3 participants