The output of a security cleanup should arrive as a human-readable list of what will be revoked and why, not as a file you import. The import file comes only after the replacement is proven to be live, and the honest saving until then is zero.
The first thing a de-provisioning step produces should be a sign-off list: every original assignment the user holds, a verdict of revoke or keep, and the reason. It is deliberately not an import file. A human reads it, agrees with it, and only then does anything get removed. On one client that list ran to 6,343 revoke and 2,046 keep, with zero computed losses, and it stayed a list until a person signed it.
Why a list, and why it keeps so much
A safe revocation is conservative by construction. It will revoke an original role only when every key that role gives the user (each privilege, each restriction verb, each direct table or field grant) is still delivered, in at least the same legal entities, by the replacement package or by a role that is never revoked. If the new design drops even one privilege the old role carried, the safe move is to keep the old role until a human approves dropping that access.
That conservatism has a blunt financial consequence. With no approvals, the safe revocation keeps every original role that still carries any privilege the new design drops. The user ends up holding their new roles and their old roles, so the bill after revocation equals the bill before it. On two separate clients, the realizable like-for-like saving with nothing signed off was exactly zero.
This is the hard truth to put in front of a client. The designed saving is real. It is also unbankable until someone signs off the specific drops that produce it. A savings number quoted without that caveat is a number that cannot be collected.
Never revoke until the replacement is proven in
The protocol has one rule at its center: nothing gets revoked until the replacement is proven to be live in the actual system. "We imported the new package" is not proof. The proof is a fresh extraction taken after the import that shows the new world is really there.
Before the sign-off list is allowed to become an import file, all of this has to hold:
- - A live security export taken after the import shows every shipped role, with its duties, its sub-roles, and the tables and fields it carries direct permissions on, and shows every shipped duty unchanged.
- - A separate user-role re-extraction shows every shipped assignment as Enabled, with the legal entities it was scoped to. This matters because a single row D365 rejected on import would otherwise strip the old role without the new one ever landing.
- - The package hash matches the files that were actually imported.
- - The package is a real delivery, not a provisional or comparison build.
- - An approver is named.
- - An independent no-loss check, recomputed from scratch, passes.
If any of those fail, the revocation stays held. It does not ship.
Prove the mechanism before the first real revocation
Even with all the gates, the very first revocation against a real tenant is the riskiest one, because no revocation file has ever been imported into that system before. Before the first live release, test a small revocation in a non-production environment with negative controls: include a scoped assignment and an unknown user, and confirm the system does the right thing with each. One specific thing worth confirming directly is whether deleting a role in D365 also removes that role's user assignments, because the release logic refuses if old assignments of a package-defined role survive.
Negative controls are the point. You are not trying to show the happy path works. You are trying to show the process refuses, loudly, when something is wrong.
Bottom line
De-provisioning is a decision a human makes, so ship it as a list a human can sign, not a file a machine will run. Keep every original role that still carries a dropped privilege, which means the realizable like-for-like saving is zero until the client approves specific drops. Never turn the list into an import file until a post-import re-extraction proves the replacement is live, and never run the first real revocation without negative-control testing. The saving is real. It is just not bankable until sign-off.
Frequently asked questions
If the saving is zero until sign-off, is the design worthless?
No. The design is what makes the saving possible and quantifies it. The zero is a statement about what you can bank today, not about the design's value. It forces an honest conversation: the client collects the saving by approving specific access drops, and sees exactly which ones.
Why re-extract after import instead of trusting the import result?
Because an import can partially fail. A rejected assignment row can leave a user with the old role gone and the new one never applied. Only a fresh extraction of the live system proves the replacement actually landed, which is the precondition for safely removing anything.
What are the negative controls for?
To prove the process fails safe. A scoped assignment and an unknown user are deliberate edge cases. If the system quietly accepts them instead of refusing, the guardrails are not working, and that is exactly what you want to find in test rather than in production.