<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
 <channel>
  <title>Entitlements as Code</title>
  <link>https://entitlementsascode.dev/blog/</link>
  <description>Posts on managing access as code: who approves an access request, drift in Microsoft Entra ID, observe and enforce, access reviews and end dates.</description>
  <language>en</language>
  <lastBuildDate>Fri, 06 Nov 2026 08:00:00 +0000</lastBuildDate>
  <atom:link href="https://entitlementsascode.dev/blog/feed.xml" rel="self" type="application/rss+xml"/>
  <item>
   <title>Drift: access granted by hand in Entra</title>
   <link>https://entitlementsascode.dev/blog/drift-access-granted-by-hand-in-entra</link>
   <guid isPermaLink="true">https://entitlementsascode.dev/blog/drift-access-granted-by-hand-in-entra</guid>
   <pubDate>Fri, 06 Nov 2026 08:00:00 +0000</pubDate>
   <description>Access added by hand in Entra is either reported, so a person decides, or put back to what Git says. And access that should end, ends.</description>
   <content:encoded><![CDATA[<p>Git is only the source of truth for access if Entra agrees with it. Sooner or later, someone with admin rights adds a person to a group directly in the Azure portal: a quick fix during an incident, a favour for a colleague, a step skipped under pressure. That change has no request, no reason and no approver. This post follows one such change and shows what Entitlements as Code does with it, depending on how the organization is set up.</p>
<h2 id="the-scenario">The scenario</h2>
<p>Ana works at ACME, a fictional company. She has asked for Contributor on the web-platform project, and her request is waiting for Marco, the group's owner. Someone decides not to wait and adds Ana to the WebPlatform Contributors group by hand in the Azure portal. Through the group, she now has the access.</p>
<p>Git does not list her. Entra and Git now disagree, and that difference is drift.</p>
<p><img alt="Animation: in the Azure portal someone adds Ana by hand to the WebPlatform Contributors group; then two outcomes side by side: if the organization enforces, Ana is removed and the audit log shows the removal by Entitlements as Code about a minute later; if it observes, nothing changes and the web app's Drift page lists Ana as added by hand, with Bring into Git and Remove" src="https://entitlementsascode.dev/media/hand-change-two-modes.svg" title="One change made by hand, two outcomes: put back to what Git says, or reported for a person to decide."></p>
<h2 id="if-the-organization-enforces-back-to-what-git-says">If the organization enforces: back to what Git says</h2>
<p>In an organization in enforce mode, Git is the record, and anything else is a mistake by definition. The membership Ana got by hand is removed on the next check of the group, about a minute in our tests, and the audit log in Entra shows that Entitlements as Code removed it.</p>
<p>Ana is not stuck. The way to get the access is the one already open: her request, approved by Marco.</p>
<p>Owners work the same way. An owner added by hand is removed, and the group's owners go back to the ones on record. A group with no owners on record is the exception: its Entra owners are left alone, so a team can bring owners under Git one group at a time.</p>
<h2 id="if-the-organization-observes-reported-and-a-person-decides">If the organization observes: reported, and a person decides</h2>
<p>Observe mode is read-only. Nothing is written to Entra, so Ana keeps the access she was given by hand. What changes is that it is no longer invisible: the web app's Drift page lists it, with who added it, to which group, and when.</p>
<p>From there, a person decides, with one click:</p>
<ul>
<li><strong>Bring into Git</strong>, if the access is right. It opens a pull request that adds Ana to the group's file, and the group's owner approves it like any request. From then on the access has a record.</li>
<li><strong>Remove</strong>, if it is not. It opens a pull request that removes her, approved the same way.</li>
</ul>
<p>Either way, nothing is removed by a job on its own. Taking access away from someone is a decision, so it stays with the owner, made with a request and recorded like any other.</p>
<p>Drift is not only additions. A member removed by hand, or a group created in Entra that Git does not know about, shows up too.</p>
<h2 id="side-by-side">Side by side</h2>
<table>
<thead>
<tr>
<th>Situation</th>
<th>A member added by hand</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Situation">The organization enforces</td>
<td data-label="A member added by hand">Removed, back to what Git says, about a minute in our tests</td>
</tr>
<tr>
<td data-label="Situation">The organization observes</td>
<td data-label="A member added by hand">Kept for now, listed on the Drift page, brought into Git or removed by its owner</td>
</tr>
</tbody>
</table>
<p>An organization can move from one to the other over time. Observe suits the period when access is still partly granted by hand and Git is catching up. Enforce suits an organization whose records are trusted.</p>
<h2 id="access-that-should-end-ends">Access that should end, ends</h2>
<p>Drift covers access granted outside Git. The other half of keeping access clean is access that was granted properly but is no longer needed.</p>
<ul>
<li><strong>End dates.</strong> A request can say until when. When the date comes, the access ends on its own. The approval already covered the end date, so nobody has to approve it again or remember it, and a pull request tidies the expired line out of Git.</li>
<li><strong>Access reviews.</strong> Owners review their groups on a schedule. For each member they choose to keep or remove, and every removal is a request with a reason, recorded like any other.</li>
</ul>
<p><img alt="Animation: Marco reviews the WebPlatform Contributors group in the web app, keeps Ana, Chen and Dana and removes Ben, who has not used the access for five months; Ana's access reaches its end date and a removal pull request is opened; in the Azure portal the group no longer has Ben or Ana, and the audit log shows both removals by Entitlements as Code" src="https://entitlementsascode.dev/media/reviews-and-expiry.svg" title="Reviews and end dates: access that is no longer needed is removed through Git, like any other change."></p>
<p>Together they answer the questions an audit asks: who has access, who approved it, until when, and who checked that it is still needed.</p>]]></content:encoded>
  </item>
  <item>
   <title>Start read-only: observe first, then enforce</title>
   <link>https://entitlementsascode.dev/blog/start-read-only-observe-then-enforce</link>
   <guid isPermaLink="true">https://entitlementsascode.dev/blog/start-read-only-observe-then-enforce</guid>
   <pubDate>Thu, 05 Nov 2026 08:00:00 +0000</pubDate>
   <description>Adopt Entitlements as Code on a live directory without changing it. Import what exists, observe with nothing written, then enforce one organization at a time.</description>
   <content:encoded><![CDATA[<p>A new access tool should not start by writing to a live directory. Your Entra groups already have members and owners, and your platforms already grant access through those groups. If the tool enforced its files on day one and the files were wrong, every mistake would become a change in someone's access. So Entitlements as Code starts read-only, and enforcing is something you switch on later, one organization at a time.</p>
<p>The examples use a fictional company, ACME, with two Azure DevOps organizations: ACME, where its product teams work, and LABS, a smaller one for experiments.</p>
<p><img alt="Diagram: the groups that exist today in Microsoft Entra ID are imported into Entitlements as Code; the files are reviewed and the organization observes, read only; an EaC admin switches ACME to enforce and the changes are applied to the ACME groups, while the LABS groups stay read only" src="https://entitlementsascode.dev/media/adoption-journey.svg" title="Import what exists, observe with nothing written, then enforce one organization at a time."></p>
<h2 id="step-1-import-what-already-exists">Step 1: import what already exists</h2>
<p>Nobody should have to write the access files by hand for a directory that already exists. Entitlements as Code imports the groups you have today, with their members and owners, and writes one file per group into Git. Each file is linked to its existing group in Entra, so nothing is created twice.</p>
<p>From then on, the files describe the access people have today, not the access someone thinks they have. That is the right starting point: first Git has to match reality, and only then do you change reality through Git.</p>
<p>Read the import with the group owners. It records what exists; it does not judge it. Some memberships will look wrong. Note them, but resist fixing everything in the first change. Fixes are easier to review as separate, small requests, each with its own reason.</p>
<h2 id="step-2-observe-with-nothing-written">Step 2: observe, with nothing written</h2>
<p>Every organization starts in observe mode. Nothing is written: no members, no owners, no groups. Users are a special case in every mode: they are read from Entra and never changed.</p>
<p>What you get while nothing changes:</p>
<ul>
<li><strong>Requests and approvals, on real data.</strong> People ask in the web app, owners approve, and every change is reviewed and recorded as a pull request.</li>
<li><strong>Checks on your actual groups.</strong> Naming, duplicate members, circular nesting and sensitive groups with no owner are flagged before anyone has to spot them.</li>
<li><strong>A drift report.</strong> The web app lists the access granted outside Git. During observe, that is usually access still granted the old way, by hand. Each one can be brought into Git for review, or removed.</li>
</ul>
<p>Since nothing is written, an approved request in an organization that observes is recorded but not applied. Observe is for getting Git and Entra to agree, not for granting access yet.</p>
<h3 id="what-to-check-before-enforcing">What to check before enforcing</h3>
<p>Use the observe period to get three things right:</p>
<ol>
<li><strong>Drift close to zero.</strong> Each difference is either access to bring into Git or access to question. When the report is empty, Git and Entra agree.</li>
<li><strong>Owners on every group.</strong> In enforce mode, the owners on record become the group's owners in Entra, and owners added by hand are removed. A group with no owners on record leaves its Entra owners alone, so you can bring owners in one group at a time.</li>
<li><strong>Owners who will approve.</strong> Approvals come from the owners. Make sure each group has owners who know they now review the requests.</li>
</ol>
<h2 id="step-3-enforce-one-organization-at-a-time">Step 3: enforce one organization at a time</h2>
<p>When an organization is ready, an EaC admin switches it to enforce in the web app. The switch is itself a request: it opens a pull request, another EaC admin approves it, and it has an author and a history like every other change.</p>
<p><img alt="Animation: in the web app's Organizations page, ACME and LABS both observe; an EaC admin switches ACME to enforce, which opens a pull request; in Azure DevOps another EaC admin approves it and it completes; back in the web app ACME shows Enforce and LABS still shows Observe" src="https://entitlementsascode.dev/media/switch-to-enforce.svg" title="One request switches ACME to enforce. LABS keeps observing."></p>
<p>From then on, for that organization only:</p>
<ul>
<li><strong>Approved requests are applied.</strong> The change reaches Entra and every platform that follows the groups, with no ticket and nobody in an admin console.</li>
<li><strong>Changes made by hand go back to what Git says.</strong> A member or owner added outside Git is removed; one removed outside Git is put back.</li>
<li><strong>Users stay read-only.</strong> They are still only read from Entra.</li>
<li><strong>Other organizations keep their own mode.</strong> One that still observes stays read-only until it is switched.</li>
</ul>
<h3 id="why-one-organization-at-a-time">Why one organization at a time</h3>
<p>Each organization has its own groups, owners and habits. Switching one keeps the effect small and easy to watch: you learn how the first one behaves, fix what the observe period missed, and then move to the next. An organization whose owners are not ready can wait in observe mode without holding back the others.</p>
<p>The order is the same for every organization: import, observe until Git matches Entra, then enforce.</p>]]></content:encoded>
  </item>
  <item>
   <title>Who approves an access request?</title>
   <link>https://entitlementsascode.dev/blog/who-approves-an-access-pull-request</link>
   <guid isPermaLink="true">https://entitlementsascode.dev/blog/who-approves-an-access-pull-request</guid>
   <pubDate>Wed, 04 Nov 2026 08:00:00 +0000</pubDate>
   <description>The owner of a group approves who joins it, a change of owners needs two people, and the person asking never approves their own access.</description>
   <content:encoded><![CDATA[<p>When someone needs access, two questions matter more than how fast it happens: who said yes, and could that person say yes? Entitlements as Code answers both for every request. Each one is a pull request in your Git, approved by the right person in the web app, and applied to your platforms once it is approved. This post walks through who approves what, who never counts, and why.</p>
<h2 id="one-request-start-to-finish">One request, start to finish</h2>
<p>Ana works at ACME, a fictional company, and needs to work on the web-platform project in its Azure DevOps organization. She opens the Entitlements as Code web app, picks the project and the access level, writes why she needs it and until when, and sends the request.</p>
<p>Marco owns the group that grants that access. He sees Ana's request in his Approvals page, with her reason and the end date, and approves it there. Behind the scenes the request is a pull request: Marco's approval is recorded on it, the checks pass, and it is merged. The group in Microsoft Entra ID is updated, Azure DevOps follows the group, and Ana's My access page shows the new access and when it ends.</p>
<p><img alt="Animation: Ana requests Contributor on web-platform in the web app, with a reason and an end date; Marco approves it in his Approvals page; in Azure DevOps the pull request shows his approval and completes; in the Azure portal Ana is now a member of the group; Ana's My access page lists the access and its end date" src="https://entitlementsascode.dev/media/request-to-access.svg" title="One request: Ana asks, Marco approves, and the access is applied, with a pull request behind every step."></p>
<p>Nobody opened an admin console, and nobody had to remember to write anything down. The pull request holds who asked, what for, why, until when, and who approved it.</p>
<h2 id="who-approves-what">Who approves what</h2>
<p>The person who approves depends on what the change does.</p>
<table>
<thead>
<tr>
<th>Change</th>
<th>Who approves</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Change">Someone joins or leaves a group</td>
<td data-label="Who approves">One owner of that group</td>
</tr>
<tr>
<td data-label="Change">A group's owners change</td>
<td data-label="Who approves">An owner and an EaC admin, two different people</td>
</tr>
<tr>
<td data-label="Change">A group is created or deleted</td>
<td data-label="Who approves">An EaC admin</td>
</tr>
<tr>
<td data-label="Change">An organization's settings change</td>
<td data-label="Who approves">An EaC admin</td>
</tr>
</tbody>
</table>
<p>In the demo, Marco owns the group and the request only adds a member, so his approval is enough.</p>
<p>If the same request had made Ana an owner, Marco's approval would not be enough on its own. Owners decide who gets in, so a change to the owners needs a second person: an EaC admin who is not Marco.</p>
<p>An owner can also say no. A rejected request is not applied, and the rejection stays on record with the request.</p>
<h2 id="who-never-counts">Who never counts</h2>
<p>Three kinds of people never count as approvers, even when they own the group:</p>
<ol>
<li><strong>The person asking.</strong> Ana cannot approve her own request.</li>
<li><strong>Anyone who edited the request.</strong> They helped write the change, so they are not independent of it.</li>
<li><strong>The people getting the access.</strong> Some people can ask on behalf of a colleague. If Ana asks for Marco, Marco cannot approve his own access, even though he owns the group.</li>
</ol>
<p>The reason is separation of duties. A record that says "Ana asked, Ana approved" answers nothing. When an auditor or a customer's security questionnaire asks who approved a grant, the answer has to be someone other than the person who asked for it, wrote it or received it.</p>
<p>That is also why EaC admins step in when no owner can vote. Say Marco is the only owner and he needs Contributor himself. He is the one asking, so he cannot approve, and an EaC admin approves instead. Marco still gets his access through the same flow, and the record still shows two different people.</p>
<p>The rules are enforced, not just suggested. The pull request cannot be merged until the right approval is there, and the owners that count are the ones already on record, so nobody can make themselves an owner in the same request and then approve it.</p>
<h2 id="the-same-flow-for-every-platform">The same flow, for every platform</h2>
<p>Azure DevOps is the example here, but the flow does not depend on it. Access lives in groups in Microsoft Entra ID, and the platforms you use map those groups to their own access: a project role, a team, an account, a Jira project. A module connects each platform.</p>
<p><img alt="Diagram: Ana asks and Marco approves in the web app; every change is a pull request in Git; once applied, the groups in Microsoft Entra ID are updated, and Azure DevOps, GitHub, GitLab, AWS, Jira and more platforms follow the groups" src="https://entitlementsascode.dev/media/one-flow-every-platform.svg" title="One flow for every platform: ask and approve in the web app, a pull request in Git, groups in Entra, and every platform follows."></p>
<p>So the questions never change from one platform to the next: who asked, who approved, and until when.</p>
<h2 id="taking-access-away">Taking access away</h2>
<p>Removing access follows the same path. Leaving a group is a request like any other, approved by the group's owner. Offboarding someone from every group is a single request. Access with an end date ends on its own, and periodic access reviews let owners confirm who should keep it. Every removal has an author, a reason and an approver, like every grant.</p>
<p>The result is a history where every grant and every removal has an author, a reason, an approver who is not the author, and a date. That is the evidence, and it lives in your own Git.</p>]]></content:encoded>
  </item>
 </channel>
</rss>
