Anota a los miembros del rol consultando Microsoft Graph

Este contenido fue traducido mediante IA y no ha sido revisado por un editor humano. Las imágenes y los gráficos permanecen en su idioma original.

La semana pasada, nuestro CTO Daniel Otykier volvió a participar en un hilo en Twitter, donde Micah Dail sugirió crear un script para anotar los miembros del rol con información comprensible, en lugar de los GUID de AAD, que no tienen sentido para nadie.

Tengo una idea de script que sería genial, pero me está costando terminarla y no hay nada parecido en línea.
El archivo .bim contiene roles y GUID de AD asignados, no nombres de usuario principal. ¿Hay alguna forma de consultar AD para obtener los nombres y anotarlos en el rol? Cada vez es más difícil documentar los permisos.

— Micah Dail (@MicahDail) 22 de julio de 2023

Aquí tienes el script que hemos creado para ese propósito.
El objetivo del script es ayudar a documentar los miembros de los roles de seguridad de un modelo.
Así, además de esas referencias extrañas a objetos o aplicaciones como en este caso, también obtendrás una anotación de cada miembro con una descripción legible por humanos:

Un script diseñado para documentar los roles de seguridad de los miembros de un modelo

Por desgracia, el script no es de «ejecutar y listo», ya que requiere algunos permisos para poder consultar Microsoft Graph desde el código.
Así que, por favor, sigue las instrucciones del paso 1 al paso 5 (o detente después del paso 2 si solo quieres ejecutar el script una vez).

Paso 1: Configurar permisos para Microsoft Graph en una aplicación de Azure AD

Para consultar Microsoft Graph desde código, necesitaremos una aplicación de Azure AD con un conjunto específico de permisos.
Esto es necesario porque el código actúa en nombre del usuario que ha iniciado sesión, y así obtiene los permisos necesarios para actuar en tu nombre cuando ejecutas el script.
Los permisos necesarios son:

  • Microsoft Graph (Permisos delegados)
    • Application.Read.All
    • Group.Read.All
    • User.Read
    • User.ReadBasic.All

Para este paso, tienes dos opciones:

Opción 1: Usar la aplicación de Azure AD proporcionada por Tabular Editor

Para facilitarte las cosas, hemos creado una aplicación multiinquilino (multi-tenant) en nuestro tenant, que puedes usar.
Solicita el conjunto correcto de permisos de API, y lo único que tienes que hacer es conseguir que alguien de TI dé su consentimiento para que esta aplicación se use en tu tenant de Azure.
Para facilitarles el trabajo, puedes compartir un enlace a este blog y pedirles que visiten este enlace: login.microsoftonline.com
Esto abrirá este cuadro de diálogo, donde podrán aprobar la aplicación en tu inquilino:

Usar Azure AD proporcionado por Tabular Editor

NOTA

Hacer esto no permite que nadie de Tabular Editor acceda a los datos de tu organización.
Los permisos solicitados son los llamados permisos delegados; se explican en detalle aquí Microsoft Learn – Escenarios de acceso y se visualizan en su documentación así:

Visualización de permisos delegados
Con permisos delegados, la aplicación no puede acceder a nada sin que un usuario inicie sesión (en este caso, como parte de la ejecución del script)

Opción 2: Crear tu propia aplicación de Azure AD

Puedes crear tu propia aplicación yendo al Portal de Azure y accediendo a Azure Active Directory desde ahí.
Probablemente necesitarás que alguien de TI te ayude con esto, debido a las limitaciones que tienen la mayoría de las empresas.
El proceso para crear una aplicación se describe aquí: Inicio rápido: Registrar una aplicación (puedes omitir las secciones sobre URI de redireccionamiento y credenciales).
Después de crear la aplicación, hay que asignarle permisos de API, lo cual se describe aquí: Permiso delegado para Microsoft Graph.
Los permisos necesarios son:

  • Microsoft Graph (Permisos delegados)
    • Application.Read.All
    • Group.Read.All
    • User.Read
    • User.ReadBasic.All

Después de crearla, necesitarás el ID de cliente (aplicación), que debe reemplazar el clientId en los scripts.

Paso 1.1: Probar la conexión a Microsoft Graph

Independientemente de si eliges la opción 1 o la 2, valida que la aplicación se haya configurado correctamente y que se haya concedido el consentimiento necesario.
Este pequeño script te dará luz verde para continuar o instrucciones sobre lo que necesitas corregir:

#r "Azure.Identity"

using Azure.Identity;

var clientId = "a57d5fb7-7eb6-422d-9c83-5347a7a9194c";  // Replace with your applications clientId if you use your own app.

try
{
	var options = new DefaultAzureCredentialOptions
	{
		ExcludeAzureCliCredential = true,
		ExcludeAzurePowerShellCredential = true,
		ExcludeEnvironmentCredential = true,
		ExcludeInteractiveBrowserCredential = false,
		ExcludeManagedIdentityCredential = true,
		ExcludeSharedTokenCacheCredential = true,
		ExcludeVisualStudioCodeCredential = true,
		ExcludeVisualStudioCredential = true,
		InteractiveBrowserCredentialClientId = clientId
	};
	var credential = new DefaultAzureCredential(options);
	var tokenRequest = credential.GetTokenAsync(
		new Azure.Core.TokenRequestContext(
			new[] { "https://graph.microsoft.com/Application.Read.All", "https://graph.microsoft.com/Group.Read.All", "https://graph.microsoft.com/User.ReadBasic.All" }
			));
	var token = await tokenRequest.ConfigureAwait(false);
	var accessToken = token.Token;

	Info("Your Azure AD App is configured correctly and you can proceed");
}
catch (Exception ex)
{
	Error("There was an error while validating your Azure AD App.\r\nPlease validate all its settings and try again.\r\n\r\nException was:\r\n" + ex.Message);
}

Paso 2: Copia/pega el script en un nuevo C# Script en Tabular Editor

Ahora ya puedes empezar a trabajar con el script en sí. Puedes ejecutarlo tal cual o guardarlo como una macro, lo cual se explica en el siguiente paso.
El script procesará todos los roles seleccionados (si hay alguno) o, si no hay ninguno, todos los roles del modelo; recorrerá los miembros de esos roles e intentará anotarlos con el tipo de objeto (Service Principal, Group o User) y el DisplayName.
Puedes copiar el script que aparece abajo con el botón de la esquina superior derecha y pegarlo en un nuevo C# Script en Tabular Editor 3.

#r "Azure.Identity"

using Azure.Identity;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

// We need an Azure AD App to be able to connect to Microsoft Graph.
// You can either stick with this one, which is created in the Tabular Editor tenant as a multi tenant app,
// or you can create your own in your tenant. Necesita los siguientes permisos de API:
// Microsoft Graph:
// Application.Read.All
// Group.Read.All
// User.Read
// User.ReadBasic.All
var clientId = "a57d5fb7-7eb6-422d-9c83-5347a7a9194c";
// ¿Cómo quieres que se llame la anotación?
var annotationName = "MemberDescription";

// You probably shouldn't edit anything below this line...
class MicrosoftGraphHelper
{
    HttpClient client;
    string _clientId;

    public MicrosoftGraphHelper(string clientId)
    {
        _clientId = clientId;
    }

    private async Task AuthenticateAsync()
    {
        // If not already authenticate, get a token by using interactive authentication
        if (client == null)
        {
            var options = new DefaultAzureCredentialOptions
                {
                    ExcludeAzureCliCredential = true,
                    ExcludeAzurePowerShellCredential = true,
                    ExcludeEnvironmentCredential = true,
                    ExcludeInteractiveBrowserCredential = false,
                    ExcludeManagedIdentityCredential = true,
                    ExcludeSharedTokenCacheCredential = true,
                    ExcludeVisualStudioCodeCredential = true,
                    ExcludeVisualStudioCredential = true,
                    InteractiveBrowserCredentialClientId = _clientId
                };
            var credential = new DefaultAzureCredential(options);
            var tokenRequest = credential.GetTokenAsync(
                new Azure.Core.TokenRequestContext(
                    new[] { "https://graph.microsoft.com/.default" }));
            var token = await tokenRequest.ConfigureAwait(false);
            var accessToken = token.Token;

            // Append token to httpclient to be able to call the Microsoft Graph API
            client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json"));
        }
    }

    private async Task<HttpResponseMessage> CallRestApiAsync(string url)
    {
        // Helper function for invoking REST API
        await AuthenticateAsync();
        return await client.GetAsync(url).ConfigureAwait(false);
    }

    public async Task<JObject> GetAADUserAsync(string objectId)
    {
        var url = $"https://graph.microsoft.com/v1.0/users/{objectId}";
        var response = await CallRestApiAsync(url).ConfigureAwait(false);
        var jsonResult = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        return JObject.Parse(jsonResult);
    }

    public async Task<JObject> GetAADGroupAsync(string objectId)
    {
        var url = $"https://graph.microsoft.com/v1.0/groups/{objectId}";
        var response = await CallRestApiAsync(url).ConfigureAwait(false);
        var jsonResult = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        return JObject.Parse(jsonResult);
    }

    public async Task<JObject> GetAADApplicationAsync(string objectId)
    {
        var url = $"https://graph.microsoft.com/v1.0/applications/{objectId}";
        var response = await CallRestApiAsync(url).ConfigureAwait(false);
        var jsonResult = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        return JObject.Parse(jsonResult);
    }

    public async Task AnnotateRoleMembersAsync(string annotationName)
    {
        if (ScriptHost.Model.Roles.Count == 0)
        {
            Info("Your model doesn't contain any roles");
            return;
        }

        if (!ScriptHost.Model.Roles.Any(r => r.Members.Count > 0))
        {
            Info("None of your roles contains members");
            return;
        }

        if (!ScriptHost.Model.Roles.Any(r => !r.Members.Any(m => m.HasAnnotation(annotationName))))
        {
            Info("All of your role members are already annotated");
            return;
        }

        // Now we start to work through roles and their members
        int roleCount = 0;
        int roleMemberCount = 0;
        int roleMemberEligableCount = 0;
        int roleMemberAnnotatedCount = 0;

        var objRegEx = "obj:(?<objId>[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})@(?<tenenatId>[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})";
        var appRegEx = "app:(?<appId>[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})@(?<tenenatId>[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})";

        // If invoked as a macro on one/more role(s), only process those, otherwise process all of models roles
        var rolesToProcess = ScriptHost.Selected.Roles.Count != 0 ? ScriptHost.Selected.Roles.ToList() : ScriptHost.Model.Roles.ToList();

        foreach (var role in rolesToProcess)
        {
            roleCount++;
            foreach (var member in role.Members)
            {
                roleMemberCount++;

                // Don't attempt to annotate members, that already have the annotation
                var existingAnnotation = member.GetAnnotation(annotationName);
                if (existingAnnotation == null)
                {
                    roleMemberEligableCount++;
                    if (member.MemberName.StartsWith("app:"))
                    {
                        var appMatch = Regex.Match(member.MemberName, appRegEx);
                        if (appMatch.Success == true)
                        {
                            var appNameObject = await GetAADApplicationAsync(appMatch.Groups["appId"].Value).ConfigureAwait(false);
                            if (appNameObject.ContainsKey("displayName"))
                            {
                                member.SetAnnotation(annotationName, "Azure AD Service Principal: " + appNameObject["displayName"].ToString().Trim());
                                roleMemberAnnotatedCount++;
                            }
                        }
                    }
                    else
                    {
                        var searchValue = member.MemberName;
                        var objMatch = Regex.Match(member.MemberName, objRegEx);
                        if (objMatch.Success == true)
                        {
                            searchValue = objMatch.Groups["objId"].Value;
                        }

                        var groupNameObject = await GetAADGroupAsync(searchValue).ConfigureAwait(false);
                        if (groupNameObject.ContainsKey("displayName"))
                        {
                            member.SetAnnotation(annotationName, "Azure AD Group: " + groupNameObject["displayName"].ToString().Trim());
                            roleMemberAnnotatedCount++;
                        }
                        else
                        {
                            var userNameObject = await GetAADUserAsync(searchValue).ConfigureAwait(false);
                            if (userNameObject.ContainsKey("displayName"))
                            {
                                member.SetAnnotation(annotationName, "Azure AD User: " + userNameObject["displayName"].ToString().Trim());
                                roleMemberAnnotatedCount++;
                            }
                        }
                    }
                }
            }
        }

        // Report result
        Info(
            "Role member annotating completed\r\n" +
            "Processed:\r\n" +
            $"\t{roleCount} Roles with " + $"{roleMemberCount} members.\r\n" +
            $"\t{roleMemberEligableCount} of those members wasn't already annotated.\r\n" +
            $"\t{roleMemberAnnotatedCount} was annotated, while the rest was unsuccesful."
        );
    }

    public void AnnotateRoleMembers(string annotationName)
    {
        AnnotateRoleMembersAsync(annotationName).Wait();
    }
}

var graphHelper = new MicrosoftGraphHelper(clientId);
graphHelper.AnnotateRoleMembersAsync(annotationName);

If you just want to run the script once, you are now ready to do so, and you do not need to complete the rest of the steps.

Step 3: Save the script as a macro

To save the script as a reusable macro, click C# Script > Save as Macro.

Script as a macro

This will make it available in the context menu for the selected types.
By selecting both Model and Role, you’ll be able to right click one or more roles to do a selective processing or the model to just iterate over all roles.

Enable role and model

NOTE

We have a known issue running asynchronous code as a macro, therefore the last line of the script is not awaited.
This means that the script will appear finished before it actually is. Solo cuando recibas una ventana emergente con un mensaje habrá terminado de ejecutarse el script.
Abordaremos este problema en una próxima versión de Tabular Editor.

Paso 4: Probar la macro

Para probar el script, he creado un modelo con 3 roles. Cada rol contiene un miembro que es un grupo, un usuario o un service principal:

Probando la macro_parte I
Probando la macro_II

Cuando invoco la macro haciendo clic con el botón derecho en mi modelo

Probando la macro_III

Se me pide iniciar sesión (para poder consultar Microsoft Graph) y, tras iniciar sesión, se procesan los roles y los miembros del rol.
Al final, o bien obtienes un mensaje de error o un buen resumen de lo que se corrigió:

Probando la macro_IV

Las anotaciones ya se pueden guardar en el modelo.

Paso 5: Validar el resultado

Para validar el resultado, uso “guardar en carpeta” para escribir mis cambios en una carpeta habilitada para Git, revisar los cambios y comprobar que todos mis miembros del rol han quedado anotados con una descripción:

Validando el resultado

Para terminar

Espero que hayas disfrutado de esta entrada. El potencial de usar Microsoft Graph mediante C# Scripting es enorme, y este es solo un pequeño ejemplo de lo que se puede lograr.
No dudes en dejar un comentario a continuación con preguntas o comentarios.

Related articles