<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>The Digital Meadow</title>
        <link>https://paragraph.com/@digitalmeadow</link>
        <description>A simple blog to express my simple need to communicate simple things about coding, life and random thoughts</description>
        <lastBuildDate>Tue, 27 Oct 2026 02:39:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>The Digital Meadow</title>
            <url>https://storage.googleapis.com/papyrus_images/88e939f16d9b9bfc2ac8934dd1197761</url>
            <link>https://paragraph.com/@digitalmeadow</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Lazy Man's guide to managing Azure Active Directory]]></title>
            <link>https://paragraph.com/@digitalmeadow/lazy-mans-guide-to-managing-azure-active-directory</link>
            <guid>dYIzIEkPhrgaw6O0qlWH</guid>
            <pubDate>Wed, 02 Nov 2022 02:35:18 GMT</pubDate>
            <description><![CDATA[Introduction There may come a time when the developer or admin must deal with Azure AD and all its quirks. I for instance don't get very excited at the prospect of having to go through Microsoft's documentation or interact with Azure AD questionable interface, whose user experience is less then o...]]></description>
            <content:encoded><![CDATA[<h1>Introduction</h1><p>There may come a time when the developer or admin must deal with Azure AD and all its quirks. I for instance don&apos;t get very excited at the prospect of having to go through Microsoft&apos;s documentation or interact with Azure AD questionable interface, whose user experience is less then optimal, for want of a better word.</p><p>This guide is for my future self and others too lazy or lacking the necessary inner calmness to actually use Azure AD the way it is intended.</p><h2>Scope</h2><p>This tutorial will guide you through extracting the necessary requests from the Azure AD UI and manipulating data so you can automate tasks. To illustrate this, we will migrate users from an <code>OLD APP</code> to a <code>NEW APP</code>, both of which are Enterprise Applications (although this would also work for App Registrations accessed through the Enterprise Applications tabs -- kudos for this excellent UX choice...).</p><h2>Pre-Requisites</h2><ul><li><p>A Browser with Dev Tools (I personally use <a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://www.mozilla.org/en-US/firefox/new/">Firefox</a>);</p></li><li><p>An <a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://azure.microsoft.com/en-ca/free/active-directory/">Azure Active Directory</a> subscription;</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://www.python.org/downloads/release/python-3110/">Python</a>:</p><ul><li><p><a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://ipython.org/install.html">ipython</a>;</p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://requests.readthedocs.io/en/latest/user/install/#install">requests</a>;</p></li></ul></li><li><p>An API Client (I use <a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://insomnia.rest/download">Insomnia</a>, but the same can be achieved with <a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://www.postman.com/downloads/">Postman</a>).</p></li></ul><h1>Exploring the problem</h1><p>Let&apos;s say you have two applications in you active directory:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F0f334fb34bbe009a9e641ae34130441d.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAMElEQVR4nGPg5hdWUlLU09PLy89nsLSy3rRpU1BwcFFREUNKSsqUWTOkNbX+//8PAM3GDdgJUBndAAAAAElFTkSuQmCC" width="100%" float="none" class="image-node img-center embed"><p>If you need to migrate users between them and you have an Azure AD subscription that enables <a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal">groups in Enterprise Applications </a>(which requires Active Directory Premium P1 or P2), then you may be in luck: you just have to add the necessary groups into the <code>NEW APP</code> (which hopefully aren&apos;t many). If not and you have a sizeable list of users to migrate, you may find yourself having to click through a large list of users, which is both error prone and time consuming.</p><p>You could also refer to the <a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://learn.microsoft.com/en-us/rest/api/azure/">Azure AD API documentation</a>. But where to start? You will have to understand exactly how everything translates to the API. Are you dealing with users registered to an application? Is it a service principal? Is the user assignment managed by the application? The user itself? Something else, like a role assignment? I can&apos;t really help you here, because I gave up trying to make sense of the API documentation (hence my self denominating a Lazy Man).</p><p>It turns out we can just extract the API calls made from Azure AD by interacting with the user interface and manipulate them using a programming language of choice! This lets us delegate to the UI, which is already structured around Microsoft&apos;s domain modelling, the task of correlating the many moving parts necessary to achieve our desired user migration.</p><p>The plan, therefore, is simple:</p><ol><li><p>get a list of all the users from <code>OLD APP</code>;</p></li><li><p>add a single user to <code>NEW APP</code> and see which API calls Azure ID does;</p></li><li><p>modify these calls using the list of users extracted from <code>OLD APP</code>.</p></li></ol><h1>Getting the list of users to migrate</h1><p>Let&apos;s go to OLD APP and see what we can find using the <a target="_blank" rel="noopener noreferrer nofollow" class="dont-break-out " href="https://firefox-source-docs.mozilla.org/devtools-user/index.html">dev tools</a>.</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Faf0b75732fd90e6e302d69342cc34a07.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAIAAADwyuo0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAI0lEQVR4nGNQ19ItKirJzMpet34jQ0d7e/+UKbfv3Nm4cSMAjW0NFq7pokcAAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><p>After going to <code>OLD APP</code>, open the dev tools (usually by hitting <code>F12</code>) before clicking <code>Users and groups</code>:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F033c34e96ff5b6fadd81edcb006dd006.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAMklEQVR4nAEnANj/APL09/T2+fn7/vv8/QDd3d7b3uHk5eXj5OUABgMGCgMMDAQMCgEJSZEWiyF7QigAAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><p style="text-align: center">(Azure AD didn&apos;t let me name Finn the Human for some reason... )</p><p>We can see that a <code>GET</code> request is made to <code>AppRoleAssignments</code>. This is exactly what we need. Let&apos;s just copy the <code>items</code> attribute from the response and save it to a variable in <code>ipython</code>:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Ff2b7352548c77e7685cb92b6fed5e62b.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAIAAAB2XpiaAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAFUlEQVR4nGM4dfWeg0ekgqYpA6cEACTSA/7k5TNFAAAAAElFTkSuQmCC" width="100%" float="none" class="image-node img-center embed"><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Fba38a136bcf37672e242f7825ea33aec.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAMklEQVR4nAEnANj/ANPb5/T1+MfL1JudrQCxucLa3N2rsLiEh5YAREtWWlxhLjE8AQQY/iMU6YABnAgAAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><h1>Getting the API Call to add a single user</h1><p>Now let&apos;s head to <code>NEW APP</code>, add a single user and see what we can extract.</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F61d85cd79bef198a7683492ff30cde37.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAIAAADwyuo0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAI0lEQVR4nGNgYGSZO2d2TEz0+g0bGGJjY0NjY9dv2LB9+3YAfCwLVkwTjD8AAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><p>In the <code>Users and groups</code> tab, click <code>Add user/group</code> and add a single user you would eventually migrate to the new application:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Fa83198d79db90e5daaed6fc833d6304e.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAIAAADwyuo0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAI0lEQVR4nGNwcHSaN2++oYHJhAlTGNra269du2ZtZZWcmAgAiHgK0po11LUAAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F1ed95b097f901351e199ea2415248881.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAIAAADwyuo0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAIklEQVR4nGPQ1taUk5ezs7Vtbm5i+PThfWxScm9f/6WLFwFspgssQKCBKgAAAABJRU5ErkJggg==" width="100%" float="none" class="image-node img-center embed"><p>Before actually clicking <code>Assign</code>, open up the Dev Tools again:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F32cd3fc4a0374a9dd0c0962581d4d210.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAL0lEQVR4nGN49/L5pw/v/v3++fLlK4bfv3///fPn////IA4/r7AYv5AQrwArKxcAVZQXhcli8qQAAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><p>We can see that a <code>POST</code> request is made do <code>appRoleAssignedTo</code>. Let&apos;s copy it as <code>cURL</code> and transform to python code using <code>Insomnia</code>:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Fa23ef293ea24e69e1ba442b7497b5951.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAIAAADwyuo0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAI0lEQVR4nGNwDwyVlNdeuuWwmp41Q3BMpIi4Que0Fffe/gYAaEgJ3PThK/kAAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><p>In Insomnia you just have to paste the <code>cURL</code> string and it will automatically parse it and fill out the necessary fields:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Fc646c5ce221ab46b8a918bfc39476e35.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAIAAAB2XpiaAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAFUlEQVR4nGPYevxiSkmlg39oWUMvACsBBgZa8eVMAAAAAElFTkSuQmCC" width="100%" float="none" class="image-node img-center embed"><p>Now we can generate the python code:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Fc2abe1e050e816c918dfe114d74d402e.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAP0lEQVR4nAE0AMv/ANHl69Hg6QMVHAASGAC2r7iUkJg7OEBXVVwAqrnDT11mKThALTtDADtIURonMAUWHgAUHDW7EWBzWsdNAAAAAElFTkSuQmCC" width="100%" float="none" class="image-node img-center embed"><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F064f6603cd6e7445d48edf2019d66491.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAIAAAB2XpiaAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAFUlEQVR4nGO4du/JwrWb3IJj9G1cATJzBj7ythm8AAAAAElFTkSuQmCC" width="100%" float="none" class="image-node img-center embed"><h1>Manipulating the API Calls</h1><p>Now we can use the information extracted when creating the users migration list and modify the API Calls to add other users to <code>NEW APP</code>.</p><p>In order to do this, we can study the <code>POST</code> API Call to <code>appRoleAssignedTo</code> to see that we need the <code>principalId</code>, which is fortunately included when listing users from the Application UI.</p><p>All we have to do is loop through the list of users and change the <code>principalId</code> from the request payload:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F53f69f49ad6127edf916a9e4ee3e84c2.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAIAAADwyuo0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAIklEQVR4nGP48Ojv5N6lZWWdaZm1DJVZzU4m3k4m3qYa9gCwUQrag99wGAAAAABJRU5ErkJggg==" width="100%" float="none" class="image-node img-center embed"><p>After doing it, we can execute it and see what we end up with:</p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F550331b0c2de13f7d79730f5136d98ce.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAIAAAB2XpiaAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAFUlEQVR4nGO4eOn+iUMXTPSsxEXlATHxBcTht8jeAAAAAElFTkSuQmCC" width="100%" float="none" class="image-node img-center embed"><p>We can see that we fail to add a user. That is because we previously added a single user in order to extract the API Call and the user can&apos;t be added again to this application. Instead, the API returns an error code, which we can just ignore.</p><p>The other logs tell us that we successfully added the other two users to <code>NEW APP</code>. Let&apos;s double check in the Azure AD portal: </p><img src="https://paragraph.xyz/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2F28de29d75ab538fbd3ce7ff516b85ffe.png&w=640&q=75" blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAIAAADwyuo0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAI0lEQVR4nGOoqKqeNmPmlGkz5s5bxFBQkCcuLhkVGX337j0AnjMMD+6wHjUAAAAASUVORK5CYII=" width="100%" float="none" class="image-node img-center embed"><p>Well that was easy! Easier then having to become a Microsoft Documentation expert.</p><h1>Conclusion</h1><p>Using this approach we can automate many other one-shot administrative operations without having to understand how they translate to the actual Azure AD API. For instance, this also applies, albeit with some tweaks to the API Call manipulation, to migrating users from different Azure AD groups, so even if you have a subscription that allows you to easily manage access to multiple Enterprise Application, this approach might come in handy.</p><p> Considerations to using this approach:</p><ol><li><p>The Bearer token isn&apos;t valid for long. If you have to migrate an unusually large number of users, you may have to optimize a few steps, like:</p><ol><li><p>parallelize the POST API Calls</p></li><li><p>implement continuation logic or ask for a new token, which can be extracted from a new API Call</p></li></ol></li><li><p>Azure only displays the first 200 users, so if you have to migrate more users than that, use the same approach for the POST API, and modify the GET call to set an arbitrary number of users, using the <code>top</code> header.</p></li><li><p>Azure may break the list of users in chunks. If that is the case, I couldn&apos;t force the API to retrieve the entire list using the approach described in <code>2.</code>, but clicking <code>More...</code> will make another call with the list continuation. Just do this until you end up with the complete users list and concatenate the <code>items</code> from the API Calls in <code>ipython</code>.</p></li></ol><p></p>]]></content:encoded>
            <author>digitalmeadow@newsletter.paragraph.com (Gabriel Chamon Araujo)</author>
            <category>python</category>
            <category>azure</category>
            <category>active directory</category>
            <category>dev tools</category>
            <category>ipython</category>
            <category>requests</category>
            <category>tutorial</category>
        </item>
        <item>
            <title><![CDATA[Using Mamba to manage R dependencies on AWS Lambda]]></title>
            <link>https://paragraph.com/@digitalmeadow/aws-lambda-r-mamba</link>
            <guid>H9Gfgcrc8SrgTEwfKiiK</guid>
            <pubDate>Sun, 07 Aug 2022 02:37:36 GMT</pubDate>
            <description><![CDATA[In this post we explore an approach to running R on AWS Lambda that is fast and reliable, when we need specific system dependencies not available to CentOS 7]]></description>
            <author>digitalmeadow@newsletter.paragraph.com (Gabriel Chamon Araujo)</author>
            <category>aws</category>
            <category>lambda</category>
            <category>r</category>
            <category>python</category>
            <category>mamba</category>
            <category>conda</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/fc21b7b42fbf282ec28c273013dc7c32" length="0" type="image//papyrus_images/fc21b7b42fbf282ec28c273013dc7c32"/>
        </item>
        <item>
            <title><![CDATA[[Python] SSO using Flask, requests-oauthlib and pyjwt]]></title>
            <link>https://paragraph.com/@digitalmeadow/[python]-sso-using-flask,-requests-oauthlib-and-pyjwt</link>
            <guid>xBmOxAY1zqAazKYhJaNE</guid>
            <pubDate>Sun, 28 Nov 2021 21:44:33 GMT</pubDate>
            <description><![CDATA[Using flask, requests-oauthlib, pyjwt and selenium to generate, validate, decode and retrieve JWT tokens in a complete CLI-Backend flow]]></description>
            <author>digitalmeadow@newsletter.paragraph.com (Gabriel Chamon Araujo)</author>
            <category>python</category>
            <category>oidc</category>
            <category>sso</category>
            <category>selenium</category>
        </item>
    </channel>
</rss>