Table of Contents

Class UserManagementService

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

Default IUserManagementService implementation. Directory operations require a registered IUserDirectoryService; deletion composes the storage seams — remove from all teams, then delete the user record — and only then attempts the (opt-in) directory delete, so a directory failure never leaves the local store half-deleted. Authorization and audit are applied by decorators.

public class UserManagementService : IUserManagementService
Inheritance
UserManagementService
Implements
Inherited Members

Constructors

UserManagementService(IUserService, ITeamService, IUserDirectoryService, bool, IUserIdentityDirectory, IApiKeyAdministrationService, PrivateKeyOwnerRemovedAction, ILogger<UserManagementService>)

public UserManagementService(IUserService userService, ITeamService teamService, IUserDirectoryService directoryService = null, bool writeNameToDirectory = false, IUserIdentityDirectory identityDirectory = null, IApiKeyAdministrationService apiKeys = null, PrivateKeyOwnerRemovedAction ownerRemovedAction = PrivateKeyOwnerRemovedAction.Delete, ILogger<UserManagementService> logger = null)

Parameters

userService IUserService

The user store.

teamService ITeamService

The team store, used to remove memberships and to find owned teams.

directoryService IUserDirectoryService

The external directory, when one is registered.

writeNameToDirectory bool

Whether an administrative rename also writes the name back to the directory. Default off: which side owns display names is a per-host decision, and a host federating from a corporate directory wants the directory authoritative.

identityDirectory IUserIdentityDirectory

The directory's sign-in identity reader, when one is registered.

apiKeys IApiKeyAdministrationService

The API key store, when API keys are registered, used to clean up a deleted user's personal system keys.

ownerRemovedAction PrivateKeyOwnerRemovedAction

Whether those keys are deleted or disabled; see OwnerRemovedAction.

logger ILogger<UserManagementService>

Where a failed key cleanup is reported, since it never fails the deletion.

Methods

DeleteUserAsync(string, bool, CancellationToken)

Delete a user: removes the user from every team and deletes the user record (audited). With deleteFromDirectory the user is also deleted from the external directory; a directory failure does not roll back the local delete — it is reported on the result.

public Task<UserDeleteResult> DeleteUserAsync(string userKey, bool deleteFromDirectory = false, CancellationToken cancellationToken = default)

Parameters

userKey string
deleteFromDirectory bool
cancellationToken CancellationToken

Returns

Task<UserDeleteResult>

GetDirectoryOnlyUsersAsync(CancellationToken)

List directory users that have no matching local user (matched by directory id, falling back to email), streamed as directory pages arrive.

public IAsyncEnumerable<DirectoryUser> GetDirectoryOnlyUsersAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

IAsyncEnumerable<DirectoryUser>

GetOwnedTeamsAsync(string, CancellationToken)

The teams this user owns — the teams that deleting them would leave with no owner.

public Task<IReadOnlyList<ITeam>> GetOwnedTeamsAsync(string userKey, CancellationToken cancellationToken = default)

Parameters

userKey string
cancellationToken CancellationToken

Returns

Task<IReadOnlyList<ITeam>>

Remarks

Meant to be asked before confirming a delete, so the operator can transfer ownership instead of learning afterwards that a team is unrecoverable: TransferOwnershipAsync requires the caller to be the owner, so once the owner is gone only a holder of SetOwner can repair it.

On IUserManagementService rather than ITeamService deliberately. The question is "what will deleting this user break", which is user administration; and it keeps the delete dialog off the internal team contract, which no component should inject.

GetSignInIdentitiesAsync(IEnumerable<string>, CancellationToken)

How each of the given users signs in, read from the directory in one batch. One entry per user that is linked to a directory account the directory still knows; unknown and unlinked users are left out.

public Task<UserSignInIdentities[]> GetSignInIdentitiesAsync(IEnumerable<string> userKeys, CancellationToken cancellationToken = default)

Parameters

userKeys IEnumerable<string>
cancellationToken CancellationToken

Returns

Task<UserSignInIdentities[]>

Remarks

Requires a registered IUserIdentityDirectory; without one the result is empty. Meant for a list surface reading the rows it shows, so pass a page of keys rather than every user.

The default implementation returns nothing, so a host's own implementation of this interface keeps compiling; such a host shows no sign-in methods until it implements this.

SetUserDisabledAsync(string, bool, CancellationToken)

public Task SetUserDisabledAsync(string userKey, bool disabled, CancellationToken cancellationToken = default)

Parameters

userKey string
disabled bool
cancellationToken CancellationToken

Returns

Task

Remarks

The self-disable refusal lives here, not only in UserAdminGate. A host can inject its own entry through ActionItems and dispatch straight to this method, so a UI-only guard is one the server never applies — the same lesson the delete guard learned.

SetUserEmailAsync(string, string, CancellationToken)

Set a user's email address. It stays local: a directory-backed user's address is usually owned by the organisation, so it is never written back to the directory.

public Task<UserEmailChangeResult> SetUserEmailAsync(string userKey, string email, CancellationToken cancellationToken = default)

Parameters

userKey string
email string
cancellationToken CancellationToken

Returns

Task<UserEmailChangeResult>

Remarks

The address is trimmed and must be a single, plain address. Nothing is sent to it, and it is not checked against other users: more than one user may hold the same address. It is what the user is mailed at, what an invitation to them matches, and what their avatar and fallback name come from, so the change records the address it replaced.

A host that refreshes the profile from trusted sign-ins (UserServiceBase.ShouldRefreshProfile) will have the address set back to the one the identity provider sends the next time they sign in.

Exceptions

ArgumentException

The address is empty or not a single plain address.

InvalidOperationException

The user was not found.

NotSupportedException

The host's user service does not implement it.

SetUserNameAsync(string, string, bool, CancellationToken)

Set a user's display name, and, when writeToDirectory is true, write it to the external directory too.

public Task<UserNameChangeResult> SetUserNameAsync(string userKey, string name, bool writeToDirectory, CancellationToken cancellationToken = default)

Parameters

userKey string

The user to rename.

name string

The new name. Null or empty clears the local name, and cannot be written to the directory.

writeToDirectory bool

Whether to write the name to the directory as well. The caller decides on every rename; o.Blazor.WriteNameToDirectory only allows it.

cancellationToken CancellationToken

Cancels the directory write.

Returns

Task<UserNameChangeResult>

Remarks

A write to the directory that cannot be honoured is refused before anything changes: the host has not allowed it, no directory is registered, the user is not linked to a directory account, or the name is a placeholder (IsPlaceholder(string)). Once the local write has happened, the directory write is best-effort and its outcome is reported on the result rather than rolled back into the local one: they fail independently, and coupling them would let a directory outage block renaming a user here.

Administrative rename only. The self-service path (IUserService.SetUserNameAsync) stays local deliberately — a user editing their own display name in this application should not silently rewrite the organization's directory.

Exceptions

InvalidOperationException

The user was not found, or writeToDirectory is true and the write cannot be honoured.

NotSupportedException

writeToDirectory is true and no directory is registered.

UseDirectoryNameAsync(string, CancellationToken)

Replace the user's local name with the display name the directory holds for them, and return it.

public Task<string> UseDirectoryNameAsync(string userKey, CancellationToken cancellationToken = default)

Parameters

userKey string
cancellationToken CancellationToken

Returns

Task<string>

Remarks

The directory is read here rather than the name being passed in, so the operation can only ever take what the directory actually says. Refused when the directory name is a placeholder (IsPlaceholder(string)): taking "unknown" would overwrite a real name with none. Local only — the name came from the directory, so nothing is written back.

Exceptions

InvalidOperationException

The user is not found in the directory, or the directory holds only a placeholder name.

NotSupportedException

The host's user management service does not implement it.

VerifyAllAsync(CancellationToken)

Verify all local users against the external directory, streamed as results arrive.

public IAsyncEnumerable<UserVerificationResult> VerifyAllAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

IAsyncEnumerable<UserVerificationResult>

VerifyUserAsync(string, CancellationToken)

Verify a local user against the external directory. When the user resolves via email fallback, the found directory id is persisted on the user (relink).

public Task<DirectoryVerificationResult> VerifyUserAsync(string userKey, CancellationToken cancellationToken = default)

Parameters

userKey string
cancellationToken CancellationToken

Returns

Task<DirectoryVerificationResult>