Class TeamAuthorizer
Service-layer authorization primitives for team operations, read from the caller's claims via
ITeamPrincipalAccessor (so they work for HTTP/API callers and interactive Blazor circuits
alike). The authorization decorator over ITeamService composes these per operation:
- In-team scopes (Manage, MemberManage, …)
authorize only the caller's own team — the
TeamKeyclaim must equal the targetteamKey, closing the "admin of team A acts on team B" hole. - System scopes (Delete) authorize across any team — no team binding.
Claims are the source of truth: scope claims are emitted from the caller's access level / roles / overrides for their team (or from a system key's scope list), so a present scope claim already reflects the underlying membership.
public sealed class TeamAuthorizer
- Inheritance
-
TeamAuthorizer
- Inherited Members
Constructors
TeamAuthorizer(ITeamPrincipalAccessor)
public TeamAuthorizer(ITeamPrincipalAccessor principalAccessor)
Parameters
principalAccessorITeamPrincipalAccessor
Methods
GetDisplayNameAsync()
The caller's display name, for snapshotting into durable history. Falls back through the usual chain and finally to the subject, so it is never empty for an authenticated caller.
public ValueTask<string> GetDisplayNameAsync()
Returns
GetSubjectAsync()
The caller's stable authentication subject, or null when unauthenticated.
public ValueTask<string> GetSubjectAsync()
Returns
Remarks
Deliberately the same value the audit trail records as CallerUserIdentity —
NameIdentifier, with no fallback chain. Anything that identifies a person
durably has to agree with the audit trail, or two records of the same act cannot be joined. A
per-team MemberKey would be wrong here for a second reason: it stops resolving when someone
leaves the team, and the things keyed on this outlive membership.
HasSystemScopeAsync(string)
True when the caller holds the system scope (authorizes any team; no team binding).
public ValueTask<bool> HasSystemScopeAsync(string scope)
Parameters
scopestring
Returns
HasTeamScopeAsync(string, string)
True when the caller holds scope for teamKey: the scope
claim is present and the caller's TeamKey claim equals teamKey. The
scope only authorizes the caller's own team.
public ValueTask<bool> HasTeamScopeAsync(string scope, string teamKey)
Parameters
Returns
IsAuthenticatedAsync()
True when there is an authenticated caller (any identity).
public ValueTask<bool> IsAuthenticatedAsync()
Returns
IsMemberOfAsync(string)
True when the caller's TeamKey claim equals teamKey — membership, with no
scope required.
public ValueTask<bool> IsMemberOfAsync(string teamKey)
Parameters
teamKeystring
Returns
Remarks
For the operations a member may perform on their own behalf. Raising a support case about your
own team is one: gating it behind a scope would mean every host granting that scope to everybody, and
a scope everyone holds checks nothing. shared-instructions.md makes the general point — an
entry point's check need not be a scope, only a check; the invitation path is the other example.
Still a real boundary: a caller with no team selected, or one whose selected team is a different tenant, fails it.
IsOwnerOfAsync(string)
True when the caller is the Owner of teamKey: the TeamKey claim
equals it and the AccessLevel claim is Owner.
public ValueTask<bool> IsOwnerOfAsync(string teamKey)
Parameters
teamKeystring
Returns
Remarks
Both halves are load-bearing. The access level is emitted for the team the caller has resolved access to, so on its own it says "an owner" and not "the owner of this team" — checking it without the team binding would let the owner of one team act on another, which is the hole HasTeamScopeAsync(string, string) exists to close.
For the acts a scope cannot express, because no scope distinguishes the Owner. Every
registered scope is granted to Administrator as well, so team:manage cannot say "the owner
only" — which is how deleting a team came to be available to any administrator through the service
while the UI offered it to nobody but the owner.
Fails closed: an absent, empty or unparseable access level is not the Owner.