Azure AD Sync

This guide covers everything an operator needs to set up, configure, and run the Azure AD (Microsoft Entra ID) synchronization in the RegScale CLI.

Overview

The Azure AD sync provisions RegScale users and group memberships from Microsoft
Entra ID. It is built around the App Management model: each RegScale App
gets its set of admins and AppGroup members from a small number of
naming-convention-based AD groups, with optional per-App overrides.

The sync was redesigned for App Management. The redesign:

  • Replaced the legacy admin/general/read-only model with per-App sync.
  • Introduced a deterministic naming convention with override support.
  • Added a cascade-remove safety threshold to prevent accidental mass removals.
  • Added per-App locking, dry-run mode, JSON reports, and structured exit codes.
  • Deprecated the legacy adAuthUrl/adClientId/adGraphUrl/etc. config keys
    in favor of the azureEntra* family. The old keys are still read for one
    release cycle so an un-migrated init.yaml keeps working — see Migration
    from Legacy.

There are exactly four commands:

CommandPurpose
regscale ad authenticateAcquire a token; verify required Graph scopes.
regscale ad list_groupsDiscover RegScale-* groups in Entra.
regscale ad sync_app --app-id=NReconcile one App's admins and AppGroup members.
regscale ad sync_tenant_admins --tenant-id=NReconcile tenant-wide Administrator role.

The legacy commands (sync_admins, sync_general, sync_readonly) are gone.
See the Migration section at the end of this document if you are upgrading.

Concepts

Understanding the data model is the single most important part of a clean
operations experience.

ConceptWhat it isWhere it lives
Atlas userThe platform-wide user record. Identified by externalId (Entra id) and tied to a single tenant.User table in RegScale.
AppUserA row that grants a specific Atlas user access to a specific App, with a status.AppUser table; one row per (App, Atlas user).
AppGroupA named group inside an App, e.g. Editors, Reviewers. AppUsers can belong to one or more AppGroups within their App.AppGroup table per App.
Tenant AdminA user with the platform-wide Administrator role in a tenant. Independent of any App.User + role assignment.

AppUser status values

The sync drives the AppUser record into one of these states:

StatusMeaning
AdministratorFull control of the App. Sourced from RegScale-{AppName}-Administrator.
UserNormal access via AppGroup membership. Sourced from RegScale-{AppName}-{AppGroupName}.
PendingAn open access request (or manually set to shield a user from the sync); the sync will not touch them. If an AD group references a Pending user, the sync does not approve the request — the user is listed in the report's requiresReview for a human decision.
RejectedExplicitly denied; the sync will not touch them. AD-requested membership lands in requiresReview.
DeletedSoft-deleted by cascade-remove. The user is preserved for audit but is not active in the App. Reviving an audit-preserved account is a human decision: if an AD group references a Deleted user, the sync lists them in requiresReview rather than re-activating them.

How users are matched

Entra group members arrive as Entra object ids; RegScale App state is keyed
by atlas user ids. The sync joins the two through User.externalId: a tenant
user whose externalId equals the Entra object id is recognized as that
person, so they are added to the App or re-grouped — never re-created.

Two operational consequences:

  • A RegScale account created manually (no externalId) cannot be matched to
    its Entra identity. If that person is in a synced AD group, the sync plans a
    new user creation rather than reusing the manual account. Either set the
    account's externalId to the user's Entra object id, or keep them out of
    synced AD groups.
  • Users the sync creates always carry externalId (and ldapUser: true), so
    every subsequent run matches them correctly.

Cascade-remove

When an Atlas user is no longer present in any of the App's source AD
groups, the sync removes them from the App. To prevent runaway removals (e.g. a
Graph API outage returning empty membership), the cascade is gated on the
adCascadeRemoveThresholdPct config value. If the planned cascade exceeds the
threshold (as a percent of current AppUsers), the run halts with exit code 6
unless --force is passed.

Entra App Registration

Why: the sync uses application (daemon) permissions to read groups, group
members, and user details from Microsoft Graph. You need a registered
application in Entra ID with the correct API permissions and a valid client
secret.

Step-by-step

  1. Sign in to https://portal.azure.com as a user with permission to register
    apps in your tenant.
  2. Navigate to Azure Active Directory → App registrations → New
    registration
    .
  3. Name it something recognizable (e.g. regscale-cli-ad-sync). Leave the
    default account type (single tenant) unless you have a multi-tenant need.
    No redirect URI is required.
  4. Click Register. From the Overview page, note the Application (client)
    ID
    and Directory (tenant) ID.
  5. Open API permissions → Add a permission → Microsoft Graph →
    Application permissions (NOT Delegated). Add:
    • Group.Read.All
    • GroupMember.Read.All
    • User.Read.All
  6. Click Grant admin consent for . The status column should
    show a green check next to each permission.
  7. Open Certificates & secrets → New client secret. Choose an
    expiration appropriate for your rotation policy (recommended: 12 or 24
    months). Click Add and immediately copy the Value column — it is
    only displayed once.
  8. Store the secret in your secrets manager. Schedule a calendar reminder for
    secret rotation at least 30 days before expiry.

You now have three values:

  • Client ID (a.k.a. Application ID)
  • Tenant ID (a.k.a. Directory ID)
  • Client Secret (the Value, not the Secret ID)

init.yaml Configuration Reference

All AD-related config keys live in the same init.yaml that the rest of the
CLI uses. Run regscale init once to scaffold the file.

Required keys

KeyTypeDefaultPurpose
azureEntraClientIdstringnoneApplication (client) ID from the App Registration.
azureEntraSecretstringnoneClient secret value (NOT the secret ID).
azureEntraTenantIdstringnoneDirectory (tenant) ID from the App Registration.

The azureEntra* keys are shared with the Microsoft Defender Entra evidence
commands (regscale defender collect_entra_evidence); both features
authenticate with the same Entra app registration, so grant it the union of
the Graph permissions each needs.

If any required key is missing or still holds the scaffolded placeholder value
(YOUR_CLIENT_ID, YOUR_SECRET, YOUR_TENANT_ID), the CLI exits with code
2 and a clear message.

Managed keys

KeyTypeDefaultPurpose
azureEntraAccessTokenstringemptyCached access token. The CLI manages this automatically; you do not set it manually. The sync acquires a fresh token at every invocation and never persists it.

Behavior keys

KeyTypeDefaultPurpose
adIncludeDisabledUsersboolfalseInclude users whose Entra accountEnabled=false. Set to true if you intentionally provision disabled accounts.
adFollowNestedGroupsbooltrueResolve nested group membership via Graph transitiveMembers. Set to false only if your tenant has flat groups and you want a small perf win.
adCascadeRemoveThresholdPctint (0-100)25Cascade-remove safety gate. If the planned removal percent meets or exceeds this, the sync halts and demands --force.

Mapping keys

KeyTypeDefaultPurpose
adGroupOverrideslist of mappings[]Explicit AD-group → (App, AppGroup) mappings for groups that don't follow the convention.
adTenantAdminGroupsmapping (int → string){}Per-tenant override for the AD group used by sync_tenant_admins. The scaffolded YOUR_TENANT_ID: YOUR_AD_GROUP_NAME example entry is ignored until replaced with real values.

Full example

# Required Entra credentials.
azureEntraClientId: "11111111-2222-3333-4444-555555555555"
azureEntraSecret: "your-client-secret-value"
azureEntraTenantId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"

# Managed by the CLI. Leave blank.
azureEntraAccessToken: ""

# Behavior.
adIncludeDisabledUsers: false
adFollowNestedGroups: true
adCascadeRemoveThresholdPct: 25

# Optional override map.
adGroupOverrides:
  - adGroupId: "00000000-0000-0000-0000-000000000001"
    appId: 42
    appGroupId: 7
  - adGroupId: "00000000-0000-0000-0000-000000000002"
    appId: 42
    # appGroupId omitted means "App admin"

# Optional tenant-admin source group per tenant.
adTenantAdminGroups:
  1: "RegScale-Administrators"
  2: "RegScale-FedRAMP-Administrators"

Naming Convention

The sync resolves Entra groups to RegScale targets using a strict convention.
Stick to the convention and you do not need any overrides.

Format

AD group display nameResolves to
RegScale-{AppName}-AdministratorApp admin (sets AppUser status to Administrator).
RegScale-{AppName}-{AppGroupName}AppGroup membership (status User).

{AppName} matches the App's name field exactly (case-insensitive).
{AppGroupName} matches an AppGroup's name within that App (case-insensitive
exact match).

Concrete examples

For an App named FedRAMP with AppGroups Editors and Reviewers:

RegScale-FedRAMP-Administrator    -> Sets these users as App admins.
RegScale-FedRAMP-Editors          -> Adds users to the "Editors" AppGroup.
RegScale-FedRAMP-Reviewers        -> Adds users to the "Reviewers" AppGroup.
RegScale-FedRAMP-Auditors         -> UNRESOLVED (no "Auditors" AppGroup).

For an App named My Awesome App (note the space) with AppGroup Reviewers:

RegScale-My Awesome App-Reviewers    -> Adds users to "Reviewers".

If your AD provider does not allow spaces, create an adGroupOverrides entry
that maps the dashed-name AD group to the correct App.

Resolution order

The resolver follows this exact precedence for each Entra group it encounters:

  1. Override map. If adGroupOverrides contains an entry with a matching
    adGroupId, the override wins. If appGroupId is omitted, the group is
    treated as the App admin source.
  2. Convention prefix. Names not starting with RegScale- are skipped.
  3. App name match. The body must start with {AppName}- (case-insensitive)
    for the App being synced. Otherwise skipped.
  4. Administrator sentinel. If the remainder is Administrator
    (case-insensitive), the group is the App admin source.
  5. AppGroup name lookup. Otherwise the remainder is matched against the
    App's AppGroups. A hit becomes that AppGroup's membership source.
  6. Unresolved. No match — written to the report's unresolved list.
    Action: create the AppGroup in App Manager, or add an override.

Override Map

When to use it:

  • The AD provider can't represent the convention (e.g. spaces, max-length
    limits).
  • You inherited legacy AD groups that you don't want to rename.
  • One AD group needs to feed two different Apps (use two override entries with
    different appId).

Schema

Each entry in adGroupOverrides is a mapping with:

FieldTypeRequiredPurpose
adGroupIdstringyesEntra group objectId (GUID).
appIdintyesTarget App ID.
appGroupIdintnoTarget AppGroup ID. Omit to mark the override as the App admin source.

adGroupId must be unique across all entries.

Examples

App admin override (the AD group called Legacy-FedRAMP-Admins should drive
admins for App 42):

adGroupOverrides:
  - adGroupId: "00000000-0000-0000-0000-000000000001"
    appId: 42
    # appGroupId omitted -> App admin

AppGroup override (a flat AD group LegacyEditors should populate AppGroup
ID 7 in App 42):

adGroupOverrides:
  - adGroupId: "00000000-0000-0000-0000-000000000002"
    appId: 42
    appGroupId: 7

If an override targets a different appId than the one currently being
synced, the resolver returns a SKIP for that group on this run — no error,
just a quiet skip.

Per-App App.CliConfig

Why: in multi-App deployments, different Apps may need different Entra
tenants, different cascade thresholds, or different override maps.

The RegScale App entity carries an encrypted cliConfig field (a YAML
string). When regscale ad sync_app --app-id=N runs, it loads the global
init.yaml, then merges the App's cliConfig on top.

Merge semantics

  • Per-App values override global values key-by-key.
  • Lists (adGroupOverrides) replace — they do not append.
  • Maps (adTenantAdminGroups) replace at the top level.
  • Empty or unset cliConfig returns the global config unchanged.
  • Invalid YAML in cliConfig raises ConfigError (exit 2).

Example per-App YAML

A cliConfig for App ID 42 that overrides only the cascade threshold and the
override list:

adCascadeRemoveThresholdPct: 50
adGroupOverrides:
  - adGroupId: "00000000-0000-0000-0000-000000000099"
    appId: 42
    appGroupId: 12

Everything else (Entra credentials, follow-nested, etc.) inherits from
init.yaml.

Tenant Admin Sync

sync_tenant_admins is independent from sync_app. It manages the tenant-wide
Administrator role only — it does not touch any App or AppGroup.

When to use which

ScenarioCommand
User needs admin rights inside one App.sync_app --app-id=N, source group RegScale-{AppName}-Administrator.
User needs platform-wide admin in a tenant.sync_tenant_admins --tenant-id=N.

Multi-tenant deployments

--tenant-id is required because the same RegScale instance may host multiple
tenants and each tenant has its own admin pool. The adTenantAdminGroups
mapping in init.yaml declares the source AD group per tenant.

Source AD group resolution

The source group name resolves in this order:

  1. --ad-group "<name>" flag (highest precedence).
  2. adTenantAdminGroups[<tenant_id>] from config.
  3. Default: RegScale-Administrators.

--deactivate-removed

By default, removing the admin role only revokes the role — the user record is
preserved. Pass --deactivate-removed to also deactivate the user account
when the admin role is removed. Use this only if your policy is "no admin
group membership = no platform access".

Example

# Tenant 1, default AD group, dry-run first.
regscale ad sync_tenant_admins --tenant-id=1 --dry-run

# Apply, deactivating users whose admin role was revoked.
regscale ad sync_tenant_admins --tenant-id=1 --deactivate-removed

# Tenant 2, explicit AD group override.
regscale ad sync_tenant_admins --tenant-id=2 --ad-group "RegScale-FedRAMP-Administrators"

Worked End-to-End Example

A complete walkthrough for a brand-new App.

1. Pre-requisites

In RegScale:

  • Create the App. Name it FedRAMP (or whatever; the name matters because of
    the convention).
  • Inside the App, create the AppGroups you want: Editors, Reviewers,
    Auditors.

In Entra:

  • Complete the App Registration steps above.
  • Create AD groups using the convention:
    • RegScale-FedRAMP-Administrator
    • RegScale-FedRAMP-Editors
    • RegScale-FedRAMP-Reviewers
    • RegScale-FedRAMP-Auditors
  • Populate them with members. Nested groups are fine.

In init.yaml:

azureEntraClientId: "<your-client-id>"
azureEntraSecret: "<your-secret>"
azureEntraTenantId: "<your-tenant-id>"
adFollowNestedGroups: true
adCascadeRemoveThresholdPct: 25

2. Authenticate

regscale ad authenticate

Expected output:

Azure AD authentication successful.
Required Graph scopes verified: Group.Read.All, GroupMember.Read.All, User.Read.All.

If this fails, see the Troubleshooting section.

3. Discover groups

regscale ad list_groups

This writes artifacts/azure_ad_sync/groups.json and lists every group whose
display name starts with RegScale-. Confirm your four groups are listed and
have the expected objectId values.

You can scope the discovery:

regscale ad list_groups --prefix "RegScale-FedRAMP-"

4. Dry-run sync

Always dry-run first on production. Assume the App ID is 42:

regscale ad sync_app --app-id=42 --dry-run

The command:

  • Acquires a Graph token.
  • Snapshots RegScale state from four reads: the App's group list, each
    group's membership (GET /api/apps/groups/{groupId} — the only route that
    carries members), the App's pending/active users, and the App's
    administrators (GET /api/apps/{id}/detailed).
  • Fetches the tenant's user list to map Entra object ids to atlas user ids
    via User.externalId (see "How users are matched").
  • Snapshots RegScale-FedRAMP-* group members from Entra.
  • Computes a plan of creates / promotions / demotions / group adds / group
    removes / cascade-removes.
  • Writes artifacts/azure_ad_sync/FedRAMP-<timestamp>.json with the full
    plan.
  • Prints summary counts and exits 0.

Inspect the report. The counts object should match your expectations. Pay
attention to:

  • counts.cascadeRemoves — should be near zero on a healthy run.
  • unresolved — should be empty if your AppGroups are correctly named.
  • requiresReview — anything the sync flagged for human attention.

5. Apply

If the plan looks correct:

regscale ad sync_app --app-id=42

The CLI takes a per-App lock, applies the plan, writes a fresh report, and
exits.

6. Verify idempotency

Re-run immediately:

regscale ad sync_app --app-id=42

The new report should show all counts at zero (or very low — accounting for
genuine changes since the last run). Idempotency is the basic health check
for the sync.

7. Schedule

Once dry-runs and live runs both look clean, schedule the sync. Examples:

  • A cron job on a sidecar host.
  • An ROH DAG (see next section).
  • A serverless function fired daily.

ROH (Orchestration Hub) Pattern

The ROH model expects one DAG per App. The DAG passes the App ID to the CLI
and the per-App cliConfig provides any deviations from global defaults.

Why per-App DAGs

  • Independent failure domains: one App's Graph rate limit doesn't break
    another App's sync.
  • Independent schedules: a fast-moving project App syncs hourly; a stable
    archive App syncs weekly.
  • Independent on-call: each App's owner sees only their reports and alerts.

Minimal task snippet

Any scheduler that can run a shell command works. The task for App 42 is just:

regscale ad sync_app --app-id=42

The task itself does not need the Entra credentials. They live in init.yaml
on the worker; secret values can come from your usual secret store
(environment variable, AWS Secrets Manager, etc.).

If App 42 needs custom config, set its App.cliConfig field via the
RegScale API:

adCascadeRemoveThresholdPct: 40

The CLI will merge that on top of init.yaml for this App's runs only.

Operations Runbook

First run on production

Always:

  1. regscale ad authenticate to confirm token + scopes.
  2. regscale ad list_groups to confirm Entra discovery sees what you expect.
  3. regscale ad sync_app --app-id=N --dry-run and read the report.
  4. Only then run without --dry-run.

Cascade-remove gate triggered

Symptom: the run exits with code 6 and the message:

Cascade-remove gate triggered: 137 users would be removed (>= 25%).
Pass --force to proceed.

Investigation:

  1. Open the JSON report at artifacts/azure_ad_sync/<app>-<ts>.json.
  2. Inspect counts.cascadeRemoves. Are these legitimate departures?
  3. Check the AD groups directly in Entra. Did someone accidentally empty a
    group or change its name?
  4. If the change is legitimate (e.g. a real reorg), re-run with --force.
  5. If not, fix the AD-side problem first, then re-run without --force.

Unmanaged (manually added) members

The sync emits no dedicated drift log line. An AppGroup member who is not
sourced from any AD group shows up in the plan instead: they are counted as
cascade-remove candidates (counts.cascadeRemoves in the report) unless
their AppUser status is Pending or Rejected, which the sync never
touches. Common causes:

  • Manually added in App Manager.
  • Imported from a previous tool.

Action: decide policy. If everything must be AD-sourced, let the cascade
remove them (or remove them manually). If manual additions are allowed, set
their AppUser status to Pending so the sync ignores them.

Lock file held

Symptom: exit 7 with message:

Sync for App 42 is already running (lock held by PID 1234 since 2026-05-09T08:00:00Z).

Real concurrent run: another sidecar / DAG is currently syncing App 42. Wait
for it.

Stale lock: if PID 1234 is dead or the timestamp is more than 4 hours old,
the lock auto-evicts on the next invocation. If you need to force-clear it
sooner, delete the file:

rm artifacts/azure_ad_sync/.lock-app-42

Only do this if you have confirmed no real run is in flight.

Report files

Location: artifacts/azure_ad_sync/<app-name>-<ISO-timestamp>.json.

Written by sync_app only. sync_tenant_admins prints its plan summary and
error count to stdout and does not write a JSON report.

Top-level fields:

FieldMeaning
kindAlways azure_ad_sync_app.
appId, appName, tenantIdIdentity of the target.
correlationIdUUID for this run; include in support tickets.
startedAt, completedAtUTC timestamps.
dryRun, forceRun-mode flags.
countsMap of phase → count: usersCreated, usersAddedToApp, promotedToAdmin, demotedToUser, groupAdds, groupRemoves, cascadeRemoves, unresolved, errors.
unresolvedAD groups that did not resolve (adGroupId, displayName, reason).
requiresReviewItems that need manual attention (atlasUserId, reason).
errorsPer-item apply failures (always empty in dry-run).

The --redact flag hashes Atlas user IDs (SHA-256, first 12 chars) — useful
when shipping reports to a non-trusted analytics pipeline.

SIGTERM during apply

The apply phase is per-item with per-item error capture. If the process is
killed mid-apply:

  • Already-applied changes persist (the platform writes them as it goes).
  • The lock file is left behind. The next run evicts it as stale or
    force-removes it as described above.
  • Re-running the sync converges — anything that didn't apply on the killed
    run is included in the next run's plan.

There is no "rollback" mode. The reconciler is designed to be idempotent.

Troubleshooting

Exit codes

These codes predate the CLI-wide contract in exit-codes.md and are kept for compatibility.

CodeClassCauseFix
0successClean run.—
2ConfigErrorMissing or malformed config; App ID not found.Check init.yaml, confirm App ID exists, validate adGroupOverrides schema.
3AuthErrorToken acquisition failed; missing Graph scopes.Verify azureEntraClientId/Secret/TenantId. Check secret hasn't expired. Re-grant admin consent.
4EntraApiError / genericGraph API non-2xx after retries.Check rate limits, scope grants, network egress to Graph.
5RegScaleApiError / AppApiErrorRegScale platform non-2xx after retries, or a platform response whose shape the CLI's models could not parse.Check platform connectivity, auth token, server logs. For a shape error, the message names the endpoint and fields — the platform contract may have drifted; regenerate the schema snapshot (make azure-ad-sync-regenerate-platform-schema) and compare.
6PartialApplyError / thresholdSome items failed to apply, OR cascade threshold tripped.Open the report. Fix per-item errors and re-run; or pass --force if the cascade is intentional.
7LockHeldErrorAnother sync is running for this App.Wait or clear stale lock.

Common errors

Configuration error: Required key 'azureEntraSecret' is missing or unconfigured

The required Entra key is empty or set to its sentinel default. Open
init.yaml and populate the value, or run regscale init and follow the
prompts.

Authentication failed: ...

Most common causes:

  • Wrong client ID, tenant ID, or secret value (vs. secret ID).
  • Secret expired. Issue a fresh secret in Azure Portal and update
    azureEntraSecret.
  • Admin consent not granted. Re-open the App Registration → API permissions
    → Grant admin consent.

EntraApiError: Microsoft Graph returned 429

Rate limited. The CLI retries automatically. If it still fails, reduce sync
frequency or split the work across more Apps.

EntraApiError: 403 Forbidden

A required Graph scope is missing. Verify the App Registration has
Group.Read.All, GroupMember.Read.All, User.Read.All as Application
permissions (not Delegated) and that admin consent is granted.

AppGroup 'X' not found in app 'Y' (in unresolved)

The convention found RegScale-Y-X but App Y has no AppGroup named X.
Fix:

  • Create the AppGroup X in App Manager, or
  • Rename the AD group to a valid AppGroup name, or
  • Add an adGroupOverrides entry mapping X's objectId to a different
    AppGroup ID.

Lock held: Sync for App N is already running

See the Lock File Held runbook entry.

App N not found

The provided --app-id does not exist or your auth token can't see it. Verify
the App ID and check that your RegScale credentials in init.yaml are
correct.

Migration from Legacy

If you ran the pre-redesign sync, your init.yaml likely contains the old
keys and your scheduled jobs likely call the old commands.

Config key mapping

The legacy ad* keys are deprecated but still honored for one release
cycle
. If an azureEntra* key is missing or still holds its scaffolded
YOUR_* placeholder, the CLI copies the value from the matching legacy key so
an un-migrated init.yaml keeps working. A populated azureEntra* key always
wins over its legacy counterpart. Either way the CLI emits a deprecation
warning naming every legacy key it found and the rename to perform.

This is a compatibility shim, not the supported schema — rename the keys
now.
The shim will be removed, and when it is, an init.yaml that still
carries only ad* keys will fail every regscale ad command with
Configuration error: Required key 'azureEntraClientId' is missing or
unconfigured.

Legacy keyNew keyNotes
adAuthUrl—Removed. The CLI hard-codes the Microsoft identity platform URL. Delete it.
adAccessTokenazureEntraAccessTokenNow managed automatically; do not set.
adClientIdazureEntraClientIdSame value, new key name.
adClientSecretazureEntraSecretSame value, new key name.
adGraphUrl—Removed. Hard-coded to https://graph.microsoft.com/v1.0. Delete it.
adTenantIdazureEntraTenantIdSame value, new key name.

Action: copy the values, rename the keys, delete the old keys. This applies to
any external tooling or CI pipeline that generates init.yaml as well — the
generator must emit the new key names before the shim is removed.

Command mapping

The legacy commands are removed. Invoking them produces a stub that prints
the replacement command and exits with code 2, so scripts fail fast with
guidance instead of a bare "no such command". The stubs stay registered for as
long as the legacy ad* config keys are honored above — they serve the same
operators, and retiring one without the other strands half the migration.

Only sync_admins has a like-for-like successor. sync_general and
sync_readonly assigned tenant-wide SystemRoles; sync_app reconciles
membership of a single App's AppGroups. That is a different model, and
swapping the command alone will not reproduce the old behavior — the App and
its AppGroups have to exist in RegScale first (migration checklist step 3
below).

Legacy commandNew command(s)Drop-in?
regscale ad sync_adminsregscale ad sync_tenant_admins --tenant-id=N for each tenant.Yes, once per tenant instead of one tenant-wide pass.
regscale ad sync_generalregscale ad sync_app --app-id=N for each App.No. The tenant-wide GeneralUser role is replaced by per-App AppGroup membership; create the App and its AppGroups first.
regscale ad sync_readonlyregscale ad sync_app --app-id=N for each App.No. Read-only is now an AppGroup role on a specific App, not a tenant-wide ReadOnly role; create the App and a read-only AppGroup first.

Operators running sync_general or sync_readonly from CI therefore have
platform configuration work to do, not just a command rename.

Migration checklist

  1. Update init.yaml: rename keys, remove deprecated keys.
  2. Re-run regscale ad authenticate to confirm credentials still work.
  3. List Apps and decide which need AD-driven membership. For each:
    • Confirm AppGroups exist in App Manager with the names you want.
    • Create matching RegScale-{AppName}-{AppGroupName} AD groups in Entra.
    • Add an App.cliConfig only if this App needs deviations from the
      global config.
  4. Update each scheduled job:
    • One job per App: regscale ad sync_app --app-id=N.
    • One job per tenant: regscale ad sync_tenant_admins --tenant-id=N.
  5. Run all new jobs in --dry-run first; review the reports; then go live.
  6. Decommission the legacy job definitions.

If you have many Apps and are not yet sure they're all wired up, the override
map is your friend during transition. You can map the old monolithic
RegScale-Users group to specific AppGroups across multiple Apps without
renaming a single AD group.


Did this page help you?