Table of Contents

Class TeamAuthorizer

Namespace
Tharga.Team.Service
Assembly
Tharga.Team.Service.dll

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 TeamKey claim must equal the target teamKey, 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

principalAccessor ITeamPrincipalAccessor

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

ValueTask<string>

GetSubjectAsync()

The caller's stable authentication subject, or null when unauthenticated.

public ValueTask<string> GetSubjectAsync()

Returns

ValueTask<string>

Remarks

Deliberately the same value the audit trail records as CallerUserIdentityNameIdentifier, 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

scope string

Returns

ValueTask<bool>

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

scope string
teamKey string

Returns

ValueTask<bool>

IsAuthenticatedAsync()

True when there is an authenticated caller (any identity).

public ValueTask<bool> IsAuthenticatedAsync()

Returns

ValueTask<bool>

IsMemberOfAsync(string)

True when the caller's TeamKey claim equals teamKey — membership, with no scope required.

public ValueTask<bool> IsMemberOfAsync(string teamKey)

Parameters

teamKey string

Returns

ValueTask<bool>

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

teamKey string

Returns

ValueTask<bool>

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.