TCM.InAppAuth
1.0.23
dotnet add package TCM.InAppAuth --version 1.0.23
NuGet\Install-Package TCM.InAppAuth -Version 1.0.23
<PackageReference Include="TCM.InAppAuth" Version="1.0.23" />
<PackageVersion Include="TCM.InAppAuth" Version="1.0.23" />
<PackageReference Include="TCM.InAppAuth" />
paket add TCM.InAppAuth --version 1.0.23
#r "nuget: TCM.InAppAuth, 1.0.23"
#:package TCM.InAppAuth@1.0.23
#addin nuget:?package=TCM.InAppAuth&version=1.0.23
#tool nuget:?package=TCM.InAppAuth&version=1.0.23
TCM.InAppAuth
Shared library for in-app RBAC (feature permissions) and data scope (org tree + row-level filtering) across TCM services. Identity comes from Keycloak SSO; roles, org structure, and permission matrices are managed in the InAppAuth database.
Features
- Dynamic permission matrix: Role × Resource slug × Action (
[AppPermission]) - Multi-app isolation via
AppIdin resource slug (pre-planning:buyer) - Org unit tree (admin CRUD, user assignment, merge)
- Data scope per role/resource (
IDataScopeService) with kill switch per resource GET /permissions/mefor client-side menu guards (server still enforces everything)- Permission audit log for sensitive admin changes
Installation
<ProjectReference Include="..\..\shareds\TCM.InAppAuth\TCM.InAppAuth.csproj" />
Configuration
Environment / appsettings
| Key | Description |
|---|---|
IN_APP_AUTH_CONNTECTION_STRING |
SQL Server connection string (Development: appsettings key of the same name) |
IN_APP_AUTH_ENABLED |
true to enforce [AppPermission] (default off in Development unless set) |
IN_APP_AUTH_APP_ID |
App id for resource slug prefix (e.g. PrePlanning → pre-planning:controller) |
Example appsettings.Development.json in the host API:
{
"IN_APP_AUTH_CONNTECTION_STRING": "Server=...;Database=InAppAuth;...",
"IN_APP_AUTH_ENABLED": true,
"IN_APP_AUTH_APP_ID": "PrePlanning"
}
Service registration
In Program.cs:
using TCM.InAppAuth.Extensions;
builder.Services.AddInAppAuthentication(builder.Configuration);
// If the host already calls AddControllers():
builder.Services.AddControllers()
.AddInAppAuthControllers();
// Pipeline (same as other TCM APIs)
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
AddInAppAuthentication registers DbContext, MediatR, AutoMapper, IPermissionService, IDataScopeService, IOrgTreeService, repositories, and InAppAuth API controllers.
Database migration
From the TCM.InAppAuth project folder:
dotnet ef database update --context InAppAuthDbContext
Latest migration adds: OrgUnit, AppUserOrgUnit, OrgTreeMeta, RoleResourceDataScope, ResourceDataScopeConfig, PermissionAuditLog, and AppUser.PermissionVersion.
Feature permissions (RBAC)
Controller
[AppPermission] // auto: GET list → index, GET {id} → show, etc.
[AppPermission("import-excel")] // explicit action
[AppPermission("PrePlanning", "sync")] // app-scoped slug + action (if not using env AppId)
Admin realm role admin in Keycloak JWT bypasses all checks. Internal role slug admin in InAppAuth DB also bypasses.
Sync resources from host assembly
Call POST /api/v1/Resource/sync (with appropriate permission) after deploy so new controllers appear in the admin matrix.
Data scope (row-level)
Concepts
| Scope type | Meaning |
|---|---|
All |
All rows (admin / reporting) |
Own |
CreatedUserId == current user (Guid from JWT sub) |
PrimaryOrgUnit |
Rows with OrgUnitId = user's primary assignment |
AssignedOrgUnits |
Union of all assigned org units |
OrgUnitAndDescendants |
Assigned/primary units + subtree (by OrgUnit.Path) |
ExplicitOrgUnits |
Fixed list in ExplicitOrgUnitIds JSON |
Union across roles: any role with All wins; otherwise org ids and Own are unioned. Rows with OrgUnitId and CreatedUserId both null are visible only to All.
Domain entity contract (TCM.Core)
public class Order : BaseGuidEntity, IScopedEntity
{
public Guid? OrgUnitId { get; set; }
// CreatedUserId inherited — same Keycloak subject as AppUser.UserId
}
Enforcement modes (ResourceDataScopeConfig)
| Mode | Behavior |
|---|---|
Off (default) |
No filtering; IDataScopeService returns AllowAll |
LogOnly |
Resolve scope but do not filter (for impact analysis) |
Enforce |
Apply ApplyFilter / CanAccess rules |
Set per resource slug via PUT /api/v1/DataScope/resources/Enforcement.
Handler integration pattern
using TCM.Core.Abstractions;
using TCM.Core.Services;
using TCM.InAppAuth.Extensions;
using TCM.InAppAuth.Models.DataScope;
using TCM.InAppAuth.Services;
public class GetOrderQueryHandler
{
private readonly IDataScopeService _dataScope;
private readonly IAuthenticatedUserService _auth;
private readonly IOrderRepository _repo;
public async Task<OrderDto?> Handle(Guid id, CancellationToken ct)
{
var slug = AppPermissionAttribute.BuildResourceSlug(_appId, "order");
var scope = HttpContext?.GetDataScope()
?? await _dataScope.ResolveAsync(slug, PermissionAction.Show, cancellationToken: ct);
var order = await _repo.GetByIdAsync(id);
if (order == null || !_dataScope.CanAccess(order, scope, _auth.UserId))
return null; // return 404 from controller — do not leak existence
return Map(order);
}
public async Task<PagedList<OrderDto>> List(CancellationToken ct)
{
var slug = AppPermissionAttribute.BuildResourceSlug(_appId, "order");
var scope = await _dataScope.ResolveAsync(slug, PermissionAction.Index, cancellationToken: ct);
var query = _dataScope.ApplyFilter(_repo.Query(), scope, _auth.UserId);
return await query.ToPagedListAsync(ct);
}
}
After [AppPermission], scope is also in HttpContext.Items under IDataScopeService.HttpContextItemKey — use HttpContext.GetDataScope() extension.
Create
Default: set OrgUnitId from user's primary org. If user has no primary org and scope is not All, return 400 with a clear message. Frontend can use GET /api/v1/AppUser/me/org-units (MissingPrimaryOrgWarning flag).
Bulk update/delete
Always call ApplyFilter on IQueryable before ExecuteUpdate / ExecuteDelete.
Cross-resource FKs
Scope-check only the root entity for the API resource. Do not re-check FK entities in the same response (use projection if you need to hide fields).
Domain-specific scope augmenter
IDataScopeAugmenter<T> is an extension hook for services whose access rules go beyond OrgUnitId + CreatedUserId — typically workflow approval (assignee, approvers, delegates) or any rule computed from domain-specific data InAppAuth does not own.
Augmenter predicates are OR-combined with the base scope. They run only when the base scope is actually filtering (skipped when Off, when AllowAll = true, and when LogOnly forces full access). Return null from BuildPredicate when no rule applies for the current user/context.
Contract
using System.Linq.Expressions;
using TCM.Core.Abstractions;
using TCM.InAppAuth.Models.DataScope;
using TCM.InAppAuth.Services;
public interface IDataScopeAugmenter<T> where T : class, IScopedEntity
{
Expression<Func<T, bool>>? BuildPredicate(DataScopeContext context, Guid? currentUserId);
bool CanAccess(T record, DataScopeContext context, Guid? currentUserId);
}
Example: workflow participant table
public class PurchaseRequestWorkflowAugmenter : IDataScopeAugmenter<PurchaseRequest>
{
public Expression<Func<PurchaseRequest, bool>>? BuildPredicate(
DataScopeContext context, Guid? currentUserId)
{
if (currentUserId == null) return null;
var userId = currentUserId.Value;
return pr => pr.Participants.Any(p => p.UserId == userId && p.IsActive);
}
public bool CanAccess(PurchaseRequest record, DataScopeContext context, Guid? currentUserId)
=> currentUserId != null
&& record.Participants.Any(p => p.UserId == currentUserId.Value && p.IsActive);
}
Registration
builder.Services.AddInAppAuthentication(builder.Configuration);
builder.Services.AddDataScopeAugmenter<PurchaseRequest, PurchaseRequestWorkflowAugmenter>();
Constraints
BuildPredicatemust be EF Core translatable — no client-side method calls inside the expression.- Return
null(not_ => false) when the augmenter has no rule to contribute. - Multiple augmenters per entity are allowed; predicates are OR-combined in registration order.
- Augmenters are not consulted when scope is
Off/AllowAll = true/LogOnly; use this only to expand the base scope, not to restrict it.
See documents/inappauth-workflow-participant-scope-guide.md for patterns (participant table, workflow engine join, derived field, snapshot list), checklist, and pitfalls.
Admin APIs (InAppAuth host)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/OrgUnit/tree |
Org tree for UI |
| POST | /api/v1/OrgUnit |
Create node |
| PUT | /api/v1/OrgUnit/{id} |
Update name/sort/active |
| PATCH | /api/v1/OrgUnit/{id}/move |
Change parent (updates paths) |
| POST | /api/v1/OrgUnit/merge |
Merge assignments into target (domain rows unchanged) |
| POST | /api/v1/OrgUnit/split |
Split source into new sibling nodes + move selected users |
| GET | /api/v1/OrgUnit/{id}/users |
List users in org unit (paginated; optional includeDescendants) |
| POST | /api/v1/OrgUnit/{id}/users |
Add multiple users to an org unit (skips already assigned) |
| PUT | /api/v1/AppUser/{id}/org-units |
Replace user org assignments |
| GET | /api/v1/AppUser/me/org-units |
Current user's orgs + warning flag |
| GET | /api/v1/Permission/me |
Permissions + dataScope + org list |
| POST | /api/v1/DataScope/roles/Get |
Role data scopes |
| PUT | /api/v1/DataScope/roles/Update |
Replace role data scopes |
| GET | /api/v1/DataScope/resources/Enforcement |
List enforcement configs |
| PUT | /api/v1/DataScope/resources/Enforcement |
Kill switch per resource |
| POST | /api/v1/DataScope/MigrateAllowOwnOnly |
Copy AllowOwnOnly permissions → RoleResourceDataScope (Own) |
| GET | /api/v1/DataScope/admin/audit-logs |
Permission change audit |
Client login flow
- User authenticates with Keycloak.
POST /api/v1/AppUser/sync— upsertAppUserfrom token.GET /api/v1/Permission/me— cache matrix + org units +MissingPrimaryOrgWarning.- On each API call, server runs
[AppPermission]then optional data scope in handlers.
Example /permissions/me fragment:
{
"isAdmin": false,
"missingPrimaryOrgWarning": true,
"orgUnits": [{ "id": "...", "name": "Sales HCM", "isPrimary": true }],
"permissions": [
{
"resourceSlug": "pre-planning:buyer",
"action": "show",
"allowOwnOnly": false,
"dataScope": "OrgUnitAndDescendants"
}
]
}
Caching
- Org tree version:
OrgTreeMeta.LastTreeChangeAt(bumped on tree changes). - Per user:
AppUser.PermissionVersion(bumped on role/org assignment changes). - Include these in cache keys when caching resolved org ids or scope outside a request.
Migrating from AllowOwnOnly
- Configure
RoleResourceDataScopewithScopeType = Ownfor the same role/resource rows. - Keep feature
Permissionrows granted as today. - Set
ResourceDataScopeConfig.EnforcementModetoLogOnly, thenEnforcewhen ready. HttpContext.Items["OwnOnly"]remains set when scope includesOwn(backward compatible).
Keycloak scope
- Used: SSO user + realm role
admin. - Not used: Keycloak groups for data scope (
AuthorizePath/ViewPath/Groupsare obsolete — useIDataScopeService).
Project layout
TCM.InAppAuth/
├── Attributes/AppPermissionAttribute.cs
├── Controllers/v1/ # AppUser, Role, Permission, Resource, OrgUnit, DataScope
├── Entities/ # AppUser, OrgUnit, RoleResourceDataScope, ...
├── Services/ # PermissionService, DataScopeService, OrgTreeService
├── Extensions/ # AddInAppAuthentication, ApplyDataScope, HttpContext helpers
└── Migrations/
Pilot: TCM.Tasks
ScheduleTask implements IScopedEntity (OrgUnitId). Host must register both AddInAppAuthentication and AddTCMTasks, with matching IN_APP_AUTH_APP_ID / task AppId.
Resource slug: {app-id-kebab}:schedule-task (e.g. tasks:schedule-task).
After deploy:
POST /Resource/sync— registersschedule-taskactions includingrun-now.- Grant role permissions + optional
RoleResourceDataScope. POST /DataScope/MigrateAllowOwnOnlyif migrating from legacyAllowOwnOnly.- Set
ResourceDataScopeConfigtoLogOnlythenEnforcewhen ready.
See TCM.Tasks/README.md for task-specific setup.
Further reading
Architecture decisions and phase 5 (HR sync): documents/inappauth-data-permission-and-org-structure-plan.md in the shareds repo.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net7.0
- Microsoft.Data.SqlClient (>= 5.2.2)
- TCM.Core (>= 1.0.21)
-
net9.0
- Microsoft.Data.SqlClient (>= 6.1.2)
- TCM.Core (>= 1.0.21)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on TCM.InAppAuth:
| Package | Downloads |
|---|---|
|
TCM.Tasks
Task scheduling library for TCM projects, built on Quartz.NET. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.23 | 0 | 6/8/2026 |
| 1.0.22 | 46 | 6/6/2026 |
| 1.0.21 | 122 | 6/2/2026 |
| 1.0.20 | 112 | 6/2/2026 |
| 1.0.19 | 93 | 5/28/2026 |
| 1.0.18 | 92 | 5/28/2026 |
| 1.0.17 | 96 | 5/26/2026 |
| 1.0.16 | 126 | 5/26/2026 |
| 1.0.15 | 111 | 5/24/2026 |
| 1.0.14 | 88 | 5/24/2026 |
| 1.0.13 | 85 | 5/24/2026 |
| 1.0.12 | 90 | 5/24/2026 |
| 1.0.11 | 90 | 5/24/2026 |
| 1.0.10 | 98 | 5/24/2026 |
| 1.0.9 | 103 | 5/23/2026 |
| 1.0.8 | 98 | 5/23/2026 |
| 1.0.7 | 98 | 5/23/2026 |
| 1.0.6 | 99 | 5/19/2026 |
| 1.0.5 | 90 | 5/18/2026 |
| 1.0.4 | 92 | 5/16/2026 |