Repository navigation
A delegated advisor is a leader in timeplanning - #1769
Merged
Merged
Conversation
…aller A partner advisor who opens a customer's tenant from my.microting.com is signed in as a credential-less "delegated principal": no role, no security group, and a synthetic email (delegated-<digest>@delegated.invalid). SiteScopeResolver matched none of its first three rules in a real tenant — customers do configure managers, so the degenerate-state fallback does not fire — and then looked the caller up in sdkDbContext.Workers by email, which a synthetic address can never match. The planning page answered ErrorWhileObtainingPlannings and Sentry was sent a message on every single request. An advisor acts across the customer's whole organisation on the partner's behalf and is never employed at one of its sites, so they are a leader here. Adding them to "eForm admins" was rejected: that group carries far wider privileges than this needs. The marker is the one the host already uses in UserManagerLockoutExtensions.IsDelegatedPrincipalAsync — a UserLogins row for the delegated provider — not the email domain and not a null PasswordHash, both of which are wrong for accounts that merely look alike. The provider literal is repeated here because the plugin references Microting.EformAngularFrontendBase rather than eFormAPI.Web, and it is static readonly so a test can pin it; a const would be folded into the test at compile time and agree with any drift. The check sits immediately above the worker lookup it guards, so a delegated caller returns before it and neither the error nor the Sentry message can be reached. Every other caller takes exactly the path they took before: the query runs only after the admin branches have already returned. Tests: SiteScopeResolverTests covers the delegated case (unrestricted even with managers present and with no group membership; wins over a worker row that would otherwise narrow it; no Sentry event, proven against a live control that still produces one), both negatives (a delegated-looking email with no login row, and a row for the ordinary my.microting.com link provider), the pinned literal, and all four pre-existing branches as regressions. Sharded into the fastest CI shard in both workflows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8CRdEseXEmtevf5bPHHL6
There was a problem hiding this comment.
🟡 Changes recommended
The test fixture explicitly repeats NUnit’s inherited setup, duplicating database reset and migration work and overwriting a database context without disposing it.
1 open issue
What changed in this PR
This PR gives delegated partner advisors unrestricted site scope in timeplanning, avoiding failed worker lookups and repeated Sentry messages while preserving existing access rules.
Changes:
- Recognizes delegated principals by their external-login provider before resolving workers by email.
- Adds delegated-access, negative-case, Sentry, and regression tests.
- Registers the new fixture in shard
gof both CI workflows.
| File | Description |
|---|---|
eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/SiteScopeResolver.cs |
Adds the delegated-provider marker and unrestricted-scope branch. |
eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/SiteScopeResolverTests.cs |
Adds coverage for delegated access and existing scope rules. |
.github/workflows/dotnet-core-pr.yml |
Includes the fixture in PR CI shard g. |
.github/workflows/dotnet-core-master.yml |
Includes the fixture in master CI shard g. |
Review effort: Balanced
Comment on lines
+71
to
+73
| await base.Setup(); | ||
|
|
||
| _userService = Substitute.For<IUserService>(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The bug
A partner advisor opens a customer's eForm tenant from my.microting.com already signed in. The tenant mints a credential-less delegated principal for them just in time: no role, no security group, and a synthetic email of the shape
delegated-<digest>@delegated.invalid.SiteScopeResolver.ResolveForCurrentUserAsyncdecides site scope in four steps, and a delegated principal fell through all of them:admin— the advisor holds none"eForm admins"— the advisor is in none"eForm users", not in"Kun tid", and noAssignedSites.IsManagerrow anywhere — a degenerate-state fallback that never fires in a real tenant, because customers do configure managerssdkDbContext.WorkersbyEmail == currentUser.Email— a synthetic address can never matchSo every advisor request ended at step 4's failure branch: the planning page answered
ErrorWhileObtainingPlannings, and Sentry was sent a message on every single request.The fix
An advisor acts across the customer's whole organisation on the partner's behalf and is never employed at one of its sites, so in timeplanning they are a leader — unrestricted site scope. Putting them in
"eForm admins"was rejected: that group carries far wider privileges than this needs. It is therefore decided here, in the resolver.The marker is the same one the host already uses in
UserManagerLockoutExtensions.IsDelegatedPrincipalAsync: aUserLoginsrow whoseLoginProvideris the delegated provider. Deliberately not the email domain (a convention, and spoofable) and not a nullPasswordHash(plenty of ordinary accounts have none — the host already had to fix that same over-broad assumption once).The check sits immediately above the worker-email lookup it guards, inside the same method, and returns. That is what removes both the error and the per-request Sentry noise, and it means the lookup is unreachable for a delegated caller.
Behaviour for everyone else is unchanged
The query runs only after the admin cascade has already returned, so an
admin, an"eForm admins"member and an"eForm users"member in the no-manager fallback never execute it at all. A plain worker and a"Kun tid"device user execute one extraAnyAsyncagainstUserLogins(indexed byUserId) that comes back false, and then take exactly the path they took before. No rule was altered.The provider literal
"my.microting.com/delegated", fromExternalLoginProviders.MyMicrotingDelegatedin the host'seFormAPI.Web. This plugin referencesMicroting.EformAngularFrontendBase, not the host web project, so the literal is repeated in a single named constant whose doc comment names the source of truth. It isstatic readonly, notconst, on purpose: aconstis folded into the pinning test at compile time and would silently agree with whatever the value had been changed to.A shared package would be the better long-term home. This is now the second byte-identical copy of a host provider literal (
eFormAPI.Web's own comment notes the plugin already holds a copy of the plainmy.microting.comone), and the only thing keeping them together is a pair of comments and a pinning test in each repo. The natural home isMicroting.eFormApi.BasePnorMicroting.EformAngularFrontendBase, both of which the host and every plugin already reference — a smallExternalLoginProviderstype there would let the host, this plugin and the next one import the same symbol instead of restating it. Out of scope for a fix, but worth doing before a third copy appears.Tests
eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/SiteScopeResolverTests.cs— the resolver had no fixture of its own; this one covers the new case and keeps the four pre-existing branches beside it, so the new case cannot be shipped by loosening one of them.Delegated:
ErrorWhileObtainingPlannings, asserted against a live control in the same hub — an ordinary caller with no worker row must still be reported, so a hub that failed to install fails the test instead of making it pass vacuously. Nothing leaves the process:BeforeSenddrops every event and the transport is a no-op.Negatives — only the
UserLoginsrow counts:my.microting.comprovider is not delegatedRegressions — unchanged behaviour:
adminrole is still unrestricted"eForm admins"member is still unrestricted"eForm users"/ no-manager fallback still grants the whole board"eForm users"member with a manager present is still restricted to their own site"Kun tid"member is still restricted even with no manager anywhere (guarded, so only the exclusion can be what restricts them)Plus
TheDelegatedLoginProviderLiteralIsPinned.The fixture is registered in shard
gof bothdotnet-core-pr.ymlanddotnet-core-master.yml(required byShardCoverageTests);gwas the fastest shard on the last green master run at 216s.dotnet buildis clean, with no new warnings from either changed file. Tests were not run locally — CI is the executor.🤖 Generated with Claude Code
https://claude.ai/code/session_01P8CRdEseXEmtevf5bPHHL6