Table of Contents

Class AuthorizationTeamServiceDecorator

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

Decorator over Tharga.Team.ITeamService that enforces the team-operation authorization model in the service layer (so the same checks protect the Blazor circuit and any consumer's REST controller). Reads the caller's claims via TeamAuthorizer:

  • Create — authenticated AND AllowTeamCreation (no scope; self-service).
  • Delete — (Owner of the team AND AllowTeamCreation) OR teams:delete (system).
  • Rename / Consent — team:manage on the team.
  • Custom-role CRUD — the configurable custom-role manage scope on the team (default team:manage).
  • Member invite/remove/role/scope-overrides/display-name — member:manage on the team.
  • Leave — no scope; the operation names no user but the caller. See LeaveTeamAsync(string).
  • Remove user from all teams — users:manage (system; backs user deletion).
  • Transfer ownership — passed through (Owner-only is enforced by the inner service).

Reads, consent-team lookup, last-seen touch, invite-code lookup and invitation responses pass through (self-service / not gated here).

public sealed class AuthorizationTeamServiceDecorator
Inheritance
AuthorizationTeamServiceDecorator
Inherited Members

Constructors

AuthorizationTeamServiceDecorator(ITeamService, TeamAuthorizer, TeamLifecycleOptions, IScopeRegistry, ITenantRoleRegistry, string, TeamPurgeCascade)

public AuthorizationTeamServiceDecorator(ITeamService inner, TeamAuthorizer authorizer, TeamLifecycleOptions lifecycle, IScopeRegistry scopeRegistry = null, ITenantRoleRegistry tenantRoleRegistry = null, string customRoleManageScope = null, TeamPurgeCascade purgeCascade = null)

Parameters

inner ITeamService
authorizer TeamAuthorizer
lifecycle TeamLifecycleOptions
scopeRegistry IScopeRegistry
tenantRoleRegistry ITenantRoleRegistry
customRoleManageScope string
purgeCascade TeamPurgeCascade

Methods

AddMemberAsync(string, InviteUserModel)

public Task AddMemberAsync(string teamKey, InviteUserModel model)

Parameters

teamKey string
model InviteUserModel

Returns

Task

ClearTeamIconAsync(string)

Clears the team's icon and deletes the stored bytes. Gated by team:manage.

public Task ClearTeamIconAsync(string teamKey)

Parameters

teamKey string

Returns

Task

CreateTeamAsync(string)

public Task<ITeam> CreateTeamAsync(string name)

Parameters

name string

Returns

Task<ITeam>

DeleteTeamAsync<TMember>(string)

public Task DeleteTeamAsync<TMember>(string teamKey) where TMember : ITeamMember

Parameters

teamKey string

Returns

Task

Type Parameters

TMember

ExtendInvitationAsync(string, string)

Gives an outstanding invitation a fresh lifetime, keeping its code.

public Task ExtendInvitationAsync(string teamKey, string inviteKey)

Parameters

teamKey string
inviteKey string

Returns

Task

Remarks

The point is what it does not do: mint a new code. A link that has already been mailed keeps working, so extending an invitation costs the recipient nothing and needs no second message. That is only possible because the expiry lives on the invitation record rather than being derived from its creation time — see ExpiresAt.

An operation rather than a settable expiry, so it can be authorized and audited as one fact.

The new expiry is now plus the configured Lifetime. Where no lifetime is configured invitations do not expire, and extending clears any expiry the invitation was carrying.

GetAllTeamsAsync()

Every team, regardless of membership. Requires the Read system scope.

public IAsyncEnumerable<ITeam> GetAllTeamsAsync()

Returns

IAsyncEnumerable<ITeam>

Remarks

Discovery only — the returned teams carry no implied access. Acting inside a team the caller is not a member of still depends on that team's consent. Use Tharga.Team.ITeamService.GetTeamsAsync() for the caller's own teams; this method is for oversight surfaces (support, administration).

GetAllTeamsAsync<TMember>()

Every team, regardless of membership. Requires the Read system scope.

public IAsyncEnumerable<ITeam<TMember>> GetAllTeamsAsync<TMember>() where TMember : ITeamMember

Returns

IAsyncEnumerable<ITeam<TMember>>

Type Parameters

TMember

Remarks

Discovery only — the returned teams carry no implied access. Acting inside a team the caller is not a member of still depends on that team's consent. Use Tharga.Team.ITeamService.GetTeamsAsync() for the caller's own teams; this method is for oversight surfaces (support, administration).

GetConsentedTeamsAsync(string[])

public IAsyncEnumerable<ITeam> GetConsentedTeamsAsync(string[] userRoles)

Parameters

userRoles string[]

Returns

IAsyncEnumerable<ITeam>

GetMembersAsync(string)

public IAsyncEnumerable<ITeamMember> GetMembersAsync(string teamKey)

Parameters

teamKey string

Returns

IAsyncEnumerable<ITeamMember>

GetTeamAsync<TMember>(string)

public Task<ITeam<TMember>> GetTeamAsync<TMember>(string teamKey) where TMember : ITeamMember

Parameters

teamKey string

Returns

Task<ITeam<TMember>>

Type Parameters

TMember

GetTeamByKeyAsync(string)

A team by key, regardless of the caller's membership — a non-generic exact read for call sites with no TMember to hand (e.g. the audit decorator capturing a "before" value for a consent change made by a non-member acting through consent). Returns null when the team does not exist.

public Task<ITeam> GetTeamByKeyAsync(string teamKey)

Parameters

teamKey string

Returns

Task<ITeam>

GetTeamCustomRolesAsync(string)

public Task<IReadOnlyList<TenantRoleDefinition>> GetTeamCustomRolesAsync(string teamKey)

Parameters

teamKey string

Returns

Task<IReadOnlyList<TenantRoleDefinition>>

GetTeamKeyByInviteKeyAsync(string)

Unscoped, like SetInvitationResponseAsync(string, string, string, bool): the invite code is the check, and whoever follows an invitation link holds nothing on the team yet.

public Task<string> GetTeamKeyByInviteKeyAsync(string inviteKey)

Parameters

inviteKey string

Returns

Task<string>

GetTeamMemberAsync(string, string)

public Task<ITeamMember> GetTeamMemberAsync(string teamKey, string userKey)

Parameters

teamKey string
userKey string

Returns

Task<ITeamMember>

GetTeamsAsync()

public IAsyncEnumerable<ITeam> GetTeamsAsync()

Returns

IAsyncEnumerable<ITeam>

GetTeamsAsync<TMember>()

public IAsyncEnumerable<ITeam<TMember>> GetTeamsAsync<TMember>() where TMember : ITeamMember

Returns

IAsyncEnumerable<ITeam<TMember>>

Type Parameters

TMember

GetTeamsForUserWithAccessLevelAsync(string, AccessLevel)

public Task<IReadOnlyList<ITeam>> GetTeamsForUserWithAccessLevelAsync(string userKey, AccessLevel accessLevel)

Parameters

userKey string
accessLevel AccessLevel

Returns

Task<IReadOnlyList<ITeam>>

Remarks

Gated on users:manage, the scope that already authorizes removing this user from every one of these teams. Requiring teams:read instead would hide the warning from exactly the caller about to cause the damage.

LeaveTeamAsync(string)

Deliberately unscoped, unlike RemoveMemberAsync(string, string) directly above it.

public Task LeaveTeamAsync(string teamKey)

Parameters

teamKey string

Returns

Task

Remarks

The signature carries no user key, so the caller can only remove themselves — the check is the shape of the operation rather than a grant, which is the same reason an invitation is authorized by its code. Requiring member:manage here is what stopped an ordinary member leaving a team at all: it is registered at Administrator, so User and Viewer never hold it.

No scope would serve. A suspended member holds none at all, and refusing them would make "suspended people cannot leave" an accident of the gate rather than a decision — while stranding nothing, since the Owner cannot be suspended.

The refusals live in the inner service and are the only ones: the Owner must transfer ownership first, and the last administrator of an ownerless team stays.

PurgeTeamAsync<TMember>(string)

Purging is the irreversible one and needs its own system scope, never a team-level grant.

public Task PurgeTeamAsync<TMember>(string teamKey) where TMember : ITeamMember

Parameters

teamKey string

Returns

Task

Type Parameters

TMember

Remarks

No AllowTeamCreation self-service path here, unlike RequireDeleteAsync(string). A team administrator deleting their own team is recoverable and reasonable; destroying its storage outright is not something a tenant should reach by holding team:manage.

RemoveMemberAsync(string, string)

public Task RemoveMemberAsync(string teamKey, string userKey)

Parameters

teamKey string
userKey string

Returns

Task

RemoveUserFromAllTeamsAsync(string)

Removes the user from every team they appear in, regardless of membership state. Backs user deletion (DeleteUserAsync(string, bool, CancellationToken)); requires the Manage system scope. Returns the number of teams the user was removed from.

public Task<int> RemoveUserFromAllTeamsAsync(string userKey)

Parameters

userKey string

Returns

Task<int>

RenameTeamAsync<TMember>(string, string)

public Task RenameTeamAsync<TMember>(string teamKey, string name) where TMember : ITeamMember

Parameters

teamKey string
name string

Returns

Task

Type Parameters

TMember

RestoreTeamAsync<TMember>(string)

Restoring is authorized by the same rule as deleting — it undoes it.

public Task RestoreTeamAsync<TMember>(string teamKey) where TMember : ITeamMember

Parameters

teamKey string

Returns

Task

Type Parameters

TMember

SetInvitationResponseAsync(string, string, string, bool)

public Task SetInvitationResponseAsync(string teamKey, string userKey, string inviteCode, bool accept)

Parameters

teamKey string
userKey string
inviteCode string
accept bool

Returns

Task

SetMemberLastSeenAsync(string)

public Task SetMemberLastSeenAsync(string teamKey)

Parameters

teamKey string

Returns

Task

SetMemberNameAsync(string, string, string)

public Task SetMemberNameAsync(string teamKey, string userKey, string name)

Parameters

teamKey string
userKey string
name string

Returns

Task

SetMemberRoleAsync(string, string, AccessLevel)

public Task SetMemberRoleAsync(string teamKey, string userKey, AccessLevel accessLevel)

Parameters

teamKey string
userKey string
accessLevel AccessLevel

Returns

Task

SetMemberScopeOverridesAsync(string, string, string[])

public Task SetMemberScopeOverridesAsync(string teamKey, string userKey, string[] scopeOverrides)

Parameters

teamKey string
userKey string
scopeOverrides string[]

Returns

Task

SetMemberSuspendedAsync(string, string, bool)

Suspends a member's access to the team, or restores it. The member keeps their membership, access level, roles and history, and still sees the team in the selector — they are simply granted no team scopes, so every scoped operation refuses.

public Task SetMemberSuspendedAsync(string teamKey, string userKey, bool suspended)

Parameters

teamKey string
userKey string
suspended bool

Returns

Task

Remarks

Reuses MemberManage, which already authorizes RemoveMemberAsync(string, string) — strictly more destructive, so a separate grant would guard the lesser act more carefully than the greater one.

The Owner cannot be suspended, and a member cannot suspend themselves. Both are refused by the service, not merely hidden in the UI.

Distinct from IUserManagementService.SetUserDisabledAsync, which blocks a person from the whole application. This one is bounded to a single team.

SetMemberTenantRolesAsync(string, string, string[])

public Task SetMemberTenantRolesAsync(string teamKey, string userKey, string[] tenantRoles)

Parameters

teamKey string
userKey string
tenantRoles string[]

Returns

Task

SetOwnerAsync<TMember>(string, string)

public Task<SetOwnerResult> SetOwnerAsync<TMember>(string teamKey, string newOwnerUserKey) where TMember : ITeamMember

Parameters

teamKey string
newOwnerUserKey string

Returns

Task<SetOwnerResult>

Type Parameters

TMember

Remarks

A system grant only — there is no in-team fallback, deliberately, and now for two reasons. On an ownerless team no in-team caller can exist. On a team that has an owner, the in-team caller who should move ownership is the owner, and TransferOwnershipAsync<TMember>(string, string) is already their path — an in-team fallback here would let an Administrator depose the owner, which SetMemberRoleAsync exists to refuse.

An in-team scope of the same name must not satisfy it either, which is why this asks HasSystemScopeAsync rather than checking a claim.

SetTeamConsentAsync(string, string[], AccessLevel?)

public Task SetTeamConsentAsync(string teamKey, string[] consentedRoles, AccessLevel? accessLevel = null)

Parameters

teamKey string
consentedRoles string[]
accessLevel AccessLevel?

Returns

Task

SetTeamCustomRolesAsync(string, IReadOnlyList<TenantRoleDefinition>)

public Task SetTeamCustomRolesAsync(string teamKey, IReadOnlyList<TenantRoleDefinition> customRoles)

Parameters

teamKey string
customRoles IReadOnlyList<TenantRoleDefinition>

Returns

Task

SetTeamIconAsync(string, byte[], string)

Sets the team's icon from raw image bytes: stores them via the registered IIconStore, persists the reference on the team, and deletes any previously-stored icon. Requires a registered icon store. Gated by team:manage.

public Task SetTeamIconAsync(string teamKey, byte[] data, string contentType)

Parameters

teamKey string
data byte[]
contentType string

Returns

Task

TransferOwnershipAsync<TMember>(string, string)

public Task TransferOwnershipAsync<TMember>(string teamKey, string newOwnerUserKey) where TMember : ITeamMember

Parameters

teamKey string
newOwnerUserKey string

Returns

Task

Type Parameters

TMember

Events

SelectTeamEvent

public event EventHandler<SelectTeamEventArgs> SelectTeamEvent

Event Type

EventHandler<SelectTeamEventArgs>

TeamsListChangedEvent

public event EventHandler<TeamsListChangedEventArgs> TeamsListChangedEvent

Event Type

EventHandler<TeamsListChangedEventArgs>