Skip to content

feat: environment/tenant security principal and role management commands#171

Open
TomProkop wants to merge 27 commits into
masterfrom
feat/environment-assign-user
Open

feat: environment/tenant security principal and role management commands#171
TomProkop wants to merge 27 commits into
masterfrom
feat/environment-assign-user

Conversation

@TomProkop

@TomProkop TomProkop commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Adds txc environment user/app/team/role and txc tenant role/app/user/group commands for managing Dataverse security principals (systemusers, application users/service principals, teams) and their security role assignments, at both environment (Dataverse) and tenant (Power Platform admin) scope.

By design this CLI does not mutate Entra ID — no create/delete of users, groups, or app registrations. It assumes those identities already exist and only manages the Dataverse/Power Platform side (provisioning systemusers/application users, team membership, role assignment).

Command groups

txc environment ...

  • user list/get/add/update/role list/role add/role remove/self-elevate
  • app list/get/create/update/delete/role list/role add/role remove
  • team list/get/create/delete/member list/member add/member remove/role list/role add/role remove
  • role list/get

txc tenant ...

  • role list/get
  • user role list/add/remove
  • app role list/add/remove
  • group role list/add/remove (group identified by Entra object id only, not display name — see below)

Notable decisions

  • tenant group never calls Microsoft Graph. Graph group search requires Group.Read.All, which isn't pre-consented for this CLI's Entra app registration in most tenants (confirmed via a real 403 in live testing). Following pac admin assign-group's approach, --group is always a raw object id; tenant group list/get were removed since they have no non-Graph equivalent.
  • Microsoft Graph rejects $filter entirely (400) if any clause compares a GUID-typed property to a non-GUID literal — filters are now built to only include id/appId clauses when the input parses as a GUID.
  • environment team role add/remove only supports owner/AAD-security-group/AAD-office-group teams, not access teams (Dataverse limitation, now reflected in help text).

Out of scope

Entra ID mutation (create/delete of users, groups, app registrations) — this CLI assumes those identities already exist.

TomProkop and others added 18 commits July 9, 2026 12:34
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…/roles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ole)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… build errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mmands

- Register user/app/team/role under EnvironmentCliCommand.Children
- Register the new tenant command tree (TenantCliCommand) under
  TxcCliCommand.Children and add the TALXIS.CLI.Features.Tenant project
  reference to TALXIS.CLI.csproj

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Registers App.AppCliCommand, User.UserCliCommand, and Group.GroupCliCommand
under TenantCliCommand.Children alongside the existing Role.RoleCliCommand.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Document environment user/app/team/role commands in security-roles.md
- Document tenant role/app/user/group commands and their routing between
  BAP admin-application registration and Power Platform RBAC
- Update architecture.md project map with the new Features.Tenant project
  and expanded scope of Features.Environment
- Cross-link security-roles.md from environment-management.md troubleshooting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…parsing

Live-testing against a real tenant surfaced two real bugs, both invisible
to mocked unit tests:

1. Wrong endpoint shape: used '/scopes/admin/adminApplications' with
   api-version=2020-10-01, which 404s. Verified against the decompiled
   pac CLI (Microsoft.PowerApps.CLI ClientPrimitives.PerformApplicationAction)
   that the real endpoint is '/adminApplications' (no /scopes/admin/ segment)
   with api-version=2021-04-01, for both list and register/unregister.
2. Wrong response shape: the list endpoint returns an OData-wrapped payload
   ({ "value": [...] }), not a bare JSON array. Now unwraps 'value' when
   present, falling back to a bare array for forward-compatibility.

Verified live against pct24002-sit: 'tenant role list', 'environment
role/user/app/team list/get', and 'tenant app list' all return real data.
'tenant app role list' (which exercises the fixed BAP path) got past the
404/parse errors after these fixes; further live validation is blocked
only by an expired interactive sign-in unrelated to this change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reviewed every new list/read code path for truncation risk on large
tenants/environments and fixed three real gaps:

1. PowerPlatformRbacClient (tenant role list, and role assignment
   listing that backs every 'tenant */role list' command): only read
   the first page of 'authorization/roleDefinitions' and
   'authorization/roleAssignments'. Both endpoints are OData-paged via
   '@odata.nextLink' (confirmed against the Power Platform API docs) —
   now follows the link until exhausted, same pattern as
   MicrosoftGraphClient.

2. BapAdminApiClient.ListAdminApplicationsAsync (backs the synthetic
   'admin-application' role check in 'tenant app role'): same
   '@odata.nextLink' gap, now fixed identically.

3. DataverseSecurityPrincipalManager.RetrieveRelatedEntitiesAsync
   (backs 'environment user/app/team role list' and
   'environment team member list'): used a single RetrieveRequest
   without following MoreRecords/PagingCookie, unlike the sibling
   RetrieveAllAsync used for top-level list queries. Low real-world
   likelihood (would need >5000 roles/members on one principal) but
   was a silent-truncation risk and inconsistent with the rest of the
   codebase — now loops pages the same way RetrieveAllAsync does.

MicrosoftGraphClient (tenant app/user/group list) already paginated
correctly via '@odata.nextLink' — added a regression test since none
previously existed, matching the new tests added for the two fixed
clients.

Verified: full test suite 671/671 passing (4 new pagination
regression tests). No FakeXrmEasy-style harness exists yet for
IOrganizationServiceAsync2 in this test project, so fix #3 is
validated by code review + the existing RetrieveAllAsync precedent
rather than a new integration test — flagging this as a coverage gap
worth closing with a proper Dataverse SDK test double in a future pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssignment

Closes a gap where a user who has never signed in to an environment could
not be provisioned (no systemuser record) without first receiving JIT
provisioning via sign-in. Adds a BAP-backed provisioning path matching
pac admin assign-user's addUser call.

- BapAdminApiClient.AddUserToEnvironmentAsync: POST .../addUser with
  {"ObjectId": <guid>} body.
- New IEnvironmentUserProvisioningService / EnvironmentUserProvisioningService:
  resolves --user (UPN or GUID) via Microsoft Graph, then provisions via BAP.
- New environment user add --user <upn-or-object-id> [--role <csv>] command,
  following the same create+role-assign partial-failure pattern as
  environment app create.
- Kept separate from environment user self-elevate (different privilege
  model/endpoint - BAP addUser requires existing environment-admin access,
  self-elevate is a tenant-admin bootstrap path) with reciprocal
  cross-referencing help text on both commands.
- Updated security-roles skill doc to document environment user add as the
  provisioning step before role add/self-elevate.
- Unit tests: BapAdminApiClientTests (+2), new
  EnvironmentUserProvisioningServiceTests (+3). Full suite: 676/676 passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Discovered live against production while capturing baseline for
tenant user role list: Microsoft Graph rejects an entire $filter
expression with 400 Bad Request if any clause compares a Guid-typed
property (id, appId) to a value that is not a valid GUID literal,
even when that clause is combined with "or" against a valid string
clause (e.g. userPrincipalName, displayName).

TenantRoleResolver.BuildUserFilter/BuildGroupFilter/
BuildServicePrincipalFilter always emitted both clauses regardless of
the identifier shape, so every tenant user/group/app role list/add/
remove call with a UPN, display name, or client-id string (the
overwhelmingly common case) failed outright. Only raw-GUID lookups
worked by accident.

Fixed by only including the id/appId eq clause when the supplied
value actually parses as a Guid, mirroring the pattern already used
correctly in EnvironmentUserProvisioningService. Added 4 regression
tests to TenantRoleResolverTests. Full suite: 680/680 passing.
Re-verified live against production Graph after the fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ole help text

- DataverseSecurityPrincipalManager.CreateTeamAsync assigned raw int values
  to the teamtype/membershiptype OptionSet attributes instead of wrapping
  them in OptionSetValue, causing 'Incorrect attribute value type
  System.Int32' errors on team create. Found via live production testing.
- environment team role add/remove help text incorrectly claimed roles can
  be assigned to any team type. Dataverse rejects role assignment for
  access teams (they exist only for record sharing) — corrected the
  description to state role assignment is valid for owner,
  aad-security-group, and aad-office-group teams only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
txc tenant group role add/list/remove previously resolved --group by calling
Microsoft Graph (list + filter by displayName/id), which requires the
Group.Read.All delegated permission. That permission is not pre-consented
for this CLI's Entra app registration (the same public client id pac cli
uses) in most tenants, and this CLI intentionally never prompts tenant
admins for extra consent.

Investigated how pac admin assign-group avoids this: it never calls Graph
for groups at all - it requires the caller to already supply the group's
raw object id (and display name, for the Dataverse team record it creates).
Adopted the same approach here:

- TenantRoleResolver no longer calls Graph to resolve groups. --group must
  be a valid GUID; a display name now throws a clear validation error
  explaining why (no consent prompts) and how to find the object id
  (Entra admin center or az ad group show).
- Removed tenant group list/tenant group get, which fundamentally required
  Graph group search/enumeration and have no pac cli equivalent.
- Removed the now-dead Graph group list/parse code (GraphGroup record,
  MicrosoftGraphClient.ListGroupsAsync) and TenantRoleResolver's
  BuildGroupFilter/MatchesGroup helpers.
- Updated command help text and the security-roles skill doc accordingly.
- Updated/added tests confirming zero Graph HTTP calls occur for group
  operations, and that a non-GUID --group value fails fast with a clear
  error instead of attempting a Graph call.

Live-verified against production: tenant group role add/list/remove now
succeed end-to-end with the existing delegated sign-in that previously hit
a 403 on Graph's groups endpoint, since no Graph call is made anymore.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new txc tenant ... command group for tenant-wide Power Platform RBAC role discovery/assignment (users, groups, apps), and expands txc environment ... with Dataverse security-principal management (users, application users, teams) including role assignment and self-elevation/user provisioning. This fits into the CLI by extending the existing command hierarchy and introducing Power Platform control-layer strategies/services plus corresponding test coverage and docs updates.

Changes:

  • Introduces TALXIS.CLI.Features.Tenant with role, user, group, and app subcommands for tenant-scope RBAC.
  • Adds Power Platform role-assignment strategies (PP-RBAC + synthetic admin-application) and BAP/Graph/BAP admin transport enhancements.
  • Expands txc environment with Dataverse principal CRUD-ish workflows (user/app/team/role), including user self-elevate and environment user provisioning via Graph + BAP.

Reviewed changes

Copilot reviewed 115 out of 115 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/TALXIS.CLI.Tests/Tenant/User/UserCliCommandTests.cs Adds CLI-level tests for tenant user list/get and role add validation behavior.
tests/TALXIS.CLI.Tests/Tenant/TenantPrincipalCommandTestHost.cs Introduces shared test host wiring for tenant principal CLI tests (Graph + TenantRoleResolver).
tests/TALXIS.CLI.Tests/Tenant/Role/TenantRoleCommandTestHost.cs Test host wiring for tenant role catalog CLI tests.
tests/TALXIS.CLI.Tests/Tenant/Role/RoleListCliCommandTests.cs Tests tenant role catalog listing and filtering.
tests/TALXIS.CLI.Tests/Tenant/Role/RoleGetCliCommandTests.cs Tests tenant role catalog “get” behavior and validation.
tests/TALXIS.CLI.Tests/Tenant/Group/GroupCliCommandTests.cs Adds CLI-level tests for tenant group list and role remove flow.
tests/TALXIS.CLI.Tests/Tenant/App/TenantAppCommandTestHost.cs Test host wiring for tenant app CLI commands (Graph + TenantRoleResolver).
tests/TALXIS.CLI.Tests/Tenant/App/TenantAppCommandSupportTests.cs Unit-tests tenant app validation exception handling output.
tests/TALXIS.CLI.Tests/Tenant/App/AppRoleRemoveCliCommandTests.cs Tests removing the synthetic admin-application assignment.
tests/TALXIS.CLI.Tests/Tenant/App/AppRoleListCliCommandTests.cs Tests listing merged tenant role + synthetic admin-application assignments.
tests/TALXIS.CLI.Tests/Tenant/App/AppRoleAddCliCommandTests.cs Tests ambiguous role selection behavior for tenant app role add.
tests/TALXIS.CLI.Tests/Tenant/App/AppListCliCommandTests.cs Tests tenant app listing and Graph filter shaping.
tests/TALXIS.CLI.Tests/Tenant/App/AppGetCliCommandTests.cs Tests tenant app get behavior and ambiguity handling.
tests/TALXIS.CLI.Tests/TALXIS.CLI.Tests.csproj Adds project reference to the new Tenant feature project.
tests/TALXIS.CLI.Tests/Environment/Platforms/Dataverse/DataverseSecurityPrincipalManagerTests.cs Adds unit tests for Dataverse security principal helper logic (ambiguity + option mapping).
tests/TALXIS.CLI.Tests/Config/Providers/PowerPlatform/PowerPlatformRoleStrategiesTests.cs Adds tests for PP-RBAC and BAP strategy validation behavior and synthetic assignment listing.
tests/TALXIS.CLI.Tests/Config/Providers/PowerPlatform/PowerPlatformRbacClientTests.cs Adds PP-RBAC client tests for audience usage, paging, and assignment body shape.
tests/TALXIS.CLI.Tests/Config/Providers/PowerPlatform/MicrosoftGraphClientTests.cs Adds Graph client tests (audience, paging, and permission error shaping).
tests/TALXIS.CLI.Tests/Architecture/LayeringTests.cs Extends destructive-command architecture checks to include the Tenant feature assembly.
tests/TALXIS.CLI.Tests/Architecture/CommandConventionTests.cs Extends command convention checks to include the Tenant feature assembly.
TALXIS.CLI.sln Adds the new TALXIS.CLI.Features.Tenant project to the solution.
src/TALXIS.CLI/TxcCliCommand.cs Registers txc tenant as a top-level command group.
src/TALXIS.CLI/TALXIS.CLI.csproj References the new Tenant feature project from the main CLI tool.
src/TALXIS.CLI.Platform.PowerPlatform.Control/Strategies/PowerPlatformRbacRoleStrategy.cs Implements PP-RBAC role assignment strategy (list/add/remove + tenant-role catalog resolution).
src/TALXIS.CLI.Platform.PowerPlatform.Control/Strategies/BapAdminApplicationRoleStrategy.cs Adds synthetic admin-application role support via BAP adminApplications registration.
src/TALXIS.CLI.Platform.PowerPlatform.Control/EnvironmentUserProvisioningService.cs Adds environment user provisioning via Graph lookup + BAP addUser call.
src/TALXIS.CLI.Platform.PowerPlatform.Control/EnvironmentSettingsClient.cs Adds environment self-elevate call via the usermanagement applyAdminRole endpoint.
src/TALXIS.CLI.Platform.PowerPlatform.Control/Bap/BapAdminApiClient.cs Extends BAP client with adminApplications list/register/unregister and addUser support (incl. paging).
src/TALXIS.CLI.Platform.Dataverse.Runtime/DependencyInjection/DataverseProviderServiceCollectionExtensions.cs Wires Graph/RBAC/BAP strategies + TenantRoleResolver + environment user provisioning into DI.
src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataverseUserService.cs Adds Dataverse user service façade over DataverseSecurityPrincipalManager.
src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataverseTeamService.cs Adds Dataverse team service façade (CRUD + membership + role assignment).
src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataverseRoleService.cs Adds Dataverse role lookup façade.
src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataverseAppUserService.cs Adds Dataverse application user service façade (CRUD + role assignment).
src/TALXIS.CLI.Platform.Dataverse.Application/DependencyInjection/DataverseApplicationServiceCollectionExtensions.cs Registers new Dataverse principal services in DI.
src/TALXIS.CLI.Features.Tenant/User/UserRoleRemoveCliCommand.cs Adds txc tenant user role remove destructive command.
src/TALXIS.CLI.Features.Tenant/User/UserRoleListCliCommand.cs Adds txc tenant user role list.
src/TALXIS.CLI.Features.Tenant/User/UserRoleAddCliCommand.cs Adds txc tenant user role add.
src/TALXIS.CLI.Features.Tenant/User/UserListCliCommand.cs Adds txc tenant user list.
src/TALXIS.CLI.Features.Tenant/User/UserGetCliCommand.cs Adds txc tenant user get.
src/TALXIS.CLI.Features.Tenant/User/UserCommandSupport.cs Implements Graph-backed user discovery + TenantRoleResolver role assignment helpers.
src/TALXIS.CLI.Features.Tenant/User/UserCliCommand.cs Defines tenant user and user role routing commands.
src/TALXIS.CLI.Features.Tenant/User/TenantPrincipalCommandSupport.cs Shared helpers for tenant principal commands (escaping, validation error handling, tabular output).
src/TALXIS.CLI.Features.Tenant/TenantCliCommand.cs Defines the top-level txc tenant routing command and registers subcommands.
src/TALXIS.CLI.Features.Tenant/TALXIS.CLI.Features.Tenant.csproj Adds the new Tenant feature project.
src/TALXIS.CLI.Features.Tenant/Role/TenantRoleCommandSupport.cs Implements tenant role catalog list/get helpers via TenantRoleResolver.
src/TALXIS.CLI.Features.Tenant/Role/RoleOutput.cs Implements text output formatting for tenant role list/get.
src/TALXIS.CLI.Features.Tenant/Role/RoleListCliCommand.cs Adds txc tenant role list.
src/TALXIS.CLI.Features.Tenant/Role/RoleGetCliCommand.cs Adds txc tenant role get.
src/TALXIS.CLI.Features.Tenant/Role/RoleCliCommand.cs Defines tenant role routing command.
src/TALXIS.CLI.Features.Tenant/Group/GroupRoleRemoveCliCommand.cs Adds txc tenant group role remove destructive command.
src/TALXIS.CLI.Features.Tenant/Group/GroupRoleListCliCommand.cs Adds txc tenant group role list.
src/TALXIS.CLI.Features.Tenant/Group/GroupRoleAddCliCommand.cs Adds txc tenant group role add.
src/TALXIS.CLI.Features.Tenant/Group/GroupListCliCommand.cs Adds txc tenant group list.
src/TALXIS.CLI.Features.Tenant/Group/GroupGetCliCommand.cs Adds txc tenant group get.
src/TALXIS.CLI.Features.Tenant/Group/GroupCommandSupport.cs Implements Graph-backed group discovery + TenantRoleResolver role assignment helpers.
src/TALXIS.CLI.Features.Tenant/Group/GroupCliCommand.cs Defines tenant group and group role routing commands.
src/TALXIS.CLI.Features.Tenant/App/AssemblyInfo.cs Exposes tenant app internals to the test project.
src/TALXIS.CLI.Features.Tenant/App/AppRoleRemoveCliCommand.cs Adds txc tenant app role remove destructive command (incl. synthetic role handling).
src/TALXIS.CLI.Features.Tenant/App/AppRoleListCliCommand.cs Adds txc tenant app role list.
src/TALXIS.CLI.Features.Tenant/App/AppRoleAddCliCommand.cs Adds txc tenant app role add.
src/TALXIS.CLI.Features.Tenant/App/AppListCliCommand.cs Adds txc tenant app list.
src/TALXIS.CLI.Features.Tenant/App/AppGetCliCommand.cs Adds txc tenant app get.
src/TALXIS.CLI.Features.Tenant/App/AppCliCommand.cs Defines tenant app and app role routing commands.
src/TALXIS.CLI.Features.Environment/User/UserUpdateCliCommand.cs Adds txc environment user update (enable/disable) command.
src/TALXIS.CLI.Features.Environment/User/UserSelfElevateCliCommand.cs Adds txc environment user self-elevate command.
src/TALXIS.CLI.Features.Environment/User/UserRoleRemoveCliCommand.cs Adds txc environment user role remove.
src/TALXIS.CLI.Features.Environment/User/UserRoleListCliCommand.cs Adds txc environment user role list.
src/TALXIS.CLI.Features.Environment/User/UserRoleAddCliCommand.cs Adds txc environment user role add.
src/TALXIS.CLI.Features.Environment/User/UserListCliCommand.cs Adds txc environment user list.
src/TALXIS.CLI.Features.Environment/User/UserGetCliCommand.cs Adds txc environment user get.
src/TALXIS.CLI.Features.Environment/User/UserCliCommand.cs Adds environment user routing updates (list/get/add/update/role/self-elevate).
src/TALXIS.CLI.Features.Environment/Team/TeamRoleRemoveCliCommand.cs Adds txc environment team role remove.
src/TALXIS.CLI.Features.Environment/Team/TeamRoleListCliCommand.cs Adds txc environment team role list.
src/TALXIS.CLI.Features.Environment/Team/TeamRoleAddCliCommand.cs Adds txc environment team role add.
src/TALXIS.CLI.Features.Environment/Team/TeamMemberRemoveCliCommand.cs Adds txc environment team member remove.
src/TALXIS.CLI.Features.Environment/Team/TeamMemberListCliCommand.cs Adds txc environment team member list.
src/TALXIS.CLI.Features.Environment/Team/TeamMemberAddCliCommand.cs Adds txc environment team member add.
src/TALXIS.CLI.Features.Environment/Team/TeamListCliCommand.cs Adds txc environment team list.
src/TALXIS.CLI.Features.Environment/Team/TeamGetCliCommand.cs Adds txc environment team get.
src/TALXIS.CLI.Features.Environment/Team/TeamDeleteCliCommand.cs Adds txc environment team delete.
src/TALXIS.CLI.Features.Environment/Team/TeamCreateCliCommand.cs Adds txc environment team create.
src/TALXIS.CLI.Features.Environment/Team/TeamCliCommand.cs Adds environment team routing command (list/get/create/delete/member/role).
src/TALXIS.CLI.Features.Environment/Role/RoleListCliCommand.cs Adds txc environment role list.
src/TALXIS.CLI.Features.Environment/Role/RoleGetCliCommand.cs Adds txc environment role get.
src/TALXIS.CLI.Features.Environment/Role/RoleCliCommand.cs Adds environment role routing command.
src/TALXIS.CLI.Features.Environment/EnvironmentCliCommand.cs Registers new environment subcommands (user/app/team/role).
src/TALXIS.CLI.Features.Environment/App/AppUpdateCliCommand.cs Adds txc environment app update (enable/disable).
src/TALXIS.CLI.Features.Environment/App/AppRoleRemoveCliCommand.cs Adds txc environment app role remove.
src/TALXIS.CLI.Features.Environment/App/AppRoleListCliCommand.cs Adds txc environment app role list.
src/TALXIS.CLI.Features.Environment/App/AppRoleAddCliCommand.cs Adds txc environment app role add.
src/TALXIS.CLI.Features.Environment/App/AppListCliCommand.cs Adds txc environment app list.
src/TALXIS.CLI.Features.Environment/App/AppGetCliCommand.cs Adds txc environment app get.
src/TALXIS.CLI.Features.Environment/App/AppDeleteCliCommand.cs Adds txc environment app delete.
src/TALXIS.CLI.Features.Environment/App/AppCreateCliCommand.cs Adds txc environment app create (with optional initial role assignment).
src/TALXIS.CLI.Features.Environment/App/AppCliCommand.cs Adds environment app routing command.
src/TALXIS.CLI.Features.Docs/Skills/security-roles.md Documents environment and tenant role assignment workflows and command sequences.
src/TALXIS.CLI.Features.Docs/Skills/environment-management.md Links environment-management troubleshooting to the security-role workflow doc.
src/TALXIS.CLI.Core/Platforms/PowerPlatform/IEnvironmentUserProvisioningService.cs Adds provisioning service contract + result model.
src/TALXIS.CLI.Core/Contracts/PowerPlatform/IPowerPlatformRoleAssignmentStrategy.cs Adds tenant role strategy abstractions + principal/role/assignment contracts.
src/TALXIS.CLI.Core/Contracts/Dataverse/IDataverseUserService.cs Adds Dataverse user service contract.
src/TALXIS.CLI.Core/Contracts/Dataverse/IDataverseTeamService.cs Adds Dataverse team service contract.
src/TALXIS.CLI.Core/Contracts/Dataverse/IDataverseRoleService.cs Adds Dataverse role service contract.
src/TALXIS.CLI.Core/Contracts/Dataverse/IDataverseAppUserService.cs Adds Dataverse application user service contract.
src/TALXIS.CLI.Core/Contracts/Dataverse/DataverseSecurityPrincipalContracts.cs Adds Dataverse security principal/role/team/user contracts and exception model.
docs/architecture.md Updates architecture documentation to include Tenant feature layer and expanded Environment scope.
Comments suppressed due to low confidence (1)

src/TALXIS.CLI.Features.Tenant/Group/GroupCommandSupport.cs:156

  • Graph $filter currently always includes an id eq '<input>' clause even when --group is not a GUID. Microsoft Graph rejects the entire filter with 400 when a GUID-typed property (id) is compared to a non-GUID literal, so txc tenant group get will fail for display-name inputs. Align this with TenantRoleResolver.BuildGroupFilter by only emitting the id eq clause when the input parses as a GUID.

Comment on lines +154 to +159
private static string BuildGetFilter(string user)
{
ArgumentException.ThrowIfNullOrWhiteSpace(user);
var escaped = TenantPrincipalCommandSupport.EscapeODataString(user.Trim());
return $"id eq '{escaped}' or userPrincipalName eq '{escaped}'";
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0d83122 — this call site now guards the id/appId clauses to only appear when the input parses as a GUID, mirroring TenantRoleResolver's pattern. Added regression tests covering both GUID and non-GUID inputs, and live-verified against production that the fixed commands no longer 400.

Comment thread src/TALXIS.CLI.Features.Tenant/App/TenantAppCommandSupport.cs Outdated
Comment thread tests/TALXIS.CLI.Tests/Tenant/User/UserCliCommandTests.cs Outdated
TomProkop and others added 8 commits July 9, 2026 15:35
Code review on PR #171 caught that the GUID-typed-filter bug I'd already
fixed in TenantRoleResolver (used by role add/remove/list) was still
present in the separate filter-building code backing "tenant user get"
and "tenant app get" (UserCommandSupport.BuildGetFilter and
TenantAppCommandSupport.BuildExactAppFilter) - I had missed these two
call sites when fixing the bug originally.

Both unconditionally included an "id eq"/"appId eq" clause even when the
input wasn't a GUID (e.g. a UPN or display name), and Microsoft Graph
rejects the entire $filter with 400 when any clause compares a GUID-typed
property to a non-GUID literal, even inside an "or". Fixed both to only
include the GUID-typed clauses when the input actually parses as a GUID,
mirroring TenantRoleResolver.BuildUserFilter/BuildServicePrincipalFilter.

Updated the existing test that asserted the buggy filter shape, and added
regression tests confirming: GUID input still includes the id/appId
clauses, and non-GUID input omits them.

Live-verified against production: "tenant user get --user <upn>" and
"tenant app get --app <display-name>" both resolve correctly (no more 400).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The security-roles skill doc only said "--role admin-application authorizes
the app to call environment admin commands" without explaining that this is
implemented completely differently from every other tenant role (BAP's
adminApplications endpoint instead of Power Platform RBAC), that role list
marks it with "isSynthetic": true so scripted consumers can tell it apart
from real RBAC assignments, or that it's application-only. Expanded that
bullet with all three points and a link to the Microsoft doc this mirrors
(Create a service principal to create and manage environments and other
resources for Power Platform).

Added a matching XML doc summary to BapAdminApplicationRoleStrategy linking
the same Microsoft doc, so the code and skill markdown stay consistent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
UserRoleAddCliCommand and UserRoleRemoveCliCommand resolve the user and
role once, then call userService.AddRoleAsync/RemoveRoleAsync, which
re-resolve both by string identifier internally and can throw
DataverseAmbiguousMatchException again (e.g. a race between resolve and
mutate). The App/Team role commands already wrap this call in a
try/catch routed through TryHandleValidationException (exit code 2,
candidate listing) but the User equivalents did not, so the exception
fell through to the generic exit-code-1 handler with no candidate list.

Wrapped the final AddRoleAsync/RemoveRoleAsync calls in the same
try/catch pattern, moving the ExecuteAsync body into a private helper
method (ExecuteAddRoleAsync/ExecuteRemoveRoleAsync) since the TXC006
analyzer forbids try/catch directly inside ExecuteAsync — mirroring how
AppRoleAddCliCommand already structures this.

Added UserRoleCliCommandTests.cs (Environment/User) with fake
IDataverseUserService/IDataverseRoleService, since no CLI-level test
coverage existed for these commands previously. Full suite: 684/684.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…port classes

GraphODataFilterSupport (Platform.PowerPlatform.Control/Graph/) now owns the
"only include a GUID-typed OData clause when the input actually parses as a
GUID" rule and OData string escaping, previously copy-pasted independently
in TenantRoleResolver.BuildServicePrincipalFilter/BuildUserFilter,
TenantAppCommandSupport.BuildExactAppFilter, and
UserCommandSupport.BuildGetFilter (one comment even admitted it "mirrors"
another without sharing code). All four call sites now delegate to the
single implementation.

TenantAppCommandSupport and TenantRoleCommandSupport also redefined their
own private ResolveContextAsync/TryHandleValidationException/Truncate
instead of reusing the existing shared TenantPrincipalCommandSupport,
unlike GroupCommandSupport which already reused it correctly. Migrated both
to the shared class, widening TenantPrincipalCommandSupport.
TryHandleValidationException with the ArgumentException/InvalidOperationException
fallback that only TenantAppCommandSupport previously had (a strict
superset - no existing caller's behavior changes for the exceptions they
already handled).

Moved TenantAppCommandSupportTests.cs to TenantPrincipalCommandSupportTests.cs
targeting the now-shared method, and added coverage for the
ArgumentException/InvalidOperationException/unrelated-exception branches
that had no direct test before. Full suite: 692/692.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract `EnvironmentPrincipalCommandSupport` (mirroring the existing
`TenantPrincipalCommandSupport` on the tenant side) with the state-filter
parsing, role-identifier parsing, and column-truncation logic that was
duplicated across `UserCliCommandSupport`, `AppCommandSupport`, and
`TeamCommandSupport`. All three now delegate to the shared implementation
instead of keeping private copies.

`TryHandleValidationException`/`LogAmbiguousMatch` were deliberately left
as-is in each class: their candidate-logging output format genuinely
differs between User and App/Team, and unifying it would change
user-visible CLI output for a should-consider (non-bug) finding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`UserCliCommandSupport.SelfElevateAsync` used `Type.GetType` +
`MethodInfo.Invoke` to reach `EnvironmentSettingsClient` in
`Platform.PowerPlatform.Control`, because `Features.Environment` had no
project reference to that assembly. This bypassed compile-time checking
and was untested.

Add `SelfElevateAsync` to `IEnvironmentUserProvisioningService` (Core) and
implement it in `EnvironmentUserProvisioningService` (Control) by
injecting `EnvironmentSettingsClient`, mirroring how the service already
injects `MicrosoftGraphClient`/`BapAdminApiClient`. `UserCliCommandSupport`
now resolves the interface via `TxcServices.Get<T>()` like every other
environment command, with no new project reference and no reflection.

Add direct unit test coverage for `SelfElevateAsync` (success + failure
paths) — there was previously zero coverage for this code path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract `ODataPagingSupport.FetchAllPagesAsync`/`TryReadNextLink`
implementing the `@odata.nextLink`-following loop and "value" array unwrap
once, instead of four independent copies:

- `BapAdminApiClient.ListAdminApplicationsAsync`
- `MicrosoftGraphClient.GetPagedAsync`
- `PowerPlatformRbacClient.ListRoleDefinitionsAsync`
- `PowerPlatformRbacClient.ListTenantRoleAssignmentsAsync`

Each client now supplies only its own page-fetch delegate (token
acquisition, headers, and HTTP-level error handling) and item projector;
the shared helper owns the page-following loop, "value"/bare-array
unwrapping, and next-link parsing. Behavior is unchanged — verified by the
existing multi-page pagination tests for all three clients, which continue
to pass unmodified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collapse `TenantAppCommandTestHost`, `TenantRoleCommandTestHost`, and
`TenantPrincipalCommandTestHost` — three ~90%-identical HTTP-mocked test
host fixtures — into a single `TenantCommandTestHost` in
`tests/TALXIS.CLI.Tests/Tenant/`. All tenant-scope CLI command tests
(user, group, app, role) now share the same fake `MicrosoftGraphClient`/
`TenantRoleResolver` wiring instead of each maintaining its own copy of
the fake token service, HTTP handler queue, and connection fixture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants