Public Access
Main built vault key rotation while this branch was reshaping the screen that would drive it, so the two met in the same three files. Every other conflict was textual and resolved by taking both; these are the ones where a decision had to be made. **The view model.** Main taught TeamsViewModel three things and this branch had renamed and rewritten it into VaultsViewModel. All three are ported rather than dropped, because each is a behaviour rather than wording: adding somebody now wraps the vault to them on the spot instead of leaving SHARE KEY to be pressed, removing somebody rotates the vault and hands the new key to whoever is left, and a share reports how many generations were wrapped. The session calls they reach — ShareTeamVaultsAsync and RekeyTeamVaultsAsync — are scoped to a membership list rather than to one vault, and they are called that way here rather than narrowed: adding somebody is a change to the list, so every vault the list carries is one they can now fetch. This screen makes lists that carry one vault, so the sentences name one; where a list carries several, naming them all is the honest report, and the members section already says the list is shared. AddMemberAsync ran two lines over the length limit once the sharing was in it, so the calls behind it moved to AddOrInviteAsync and the three-way refusal to WhyNobodyCanBeAdded — the command reads as its guards now, which is what it was before the sharing arrived. **The tests.** Main's four new cases are ported to the vault-first API, including the one that matters most: the tampered key log is corrupted *before* the add, because the add is now a route to a wrap and a test that corrupted it afterwards would be asserting about the manual route only. SelectingAVault_ListsWhoHoldsAKey now expects two holders rather than one — main's fake records the creator's own self-grant, and a key-holder list that omitted it would show the one person who can certainly open a new vault as somebody who cannot. **The README.** The limits list is six rather than four or five: main's rotation entries and this branch's "a vault cannot be deleted" describe different things and both are true. "The rekey is flagged, never performed" is gone, since it is now performed, and M3 reads *Done* rather than *Done, except rekey*. One thing worth writing down that neither side had. An invitation claimed at sign-in still leaves the key owed, where an add does not: at the moment an invitation is issued there is no account and no published key to wrap to, and the claim happens on the invitee's machine, which holds nothing. Manual check 12.1 says so, because a reader who knows adding shares would otherwise read that step as stale. 1561 tests pass.
170 lines
7.9 KiB
C#
170 lines
7.9 KiB
C#
namespace DodoSSH.Api.Features.Teams;
|
|
|
|
/// <summary>
|
|
/// Source-generated log events for teams, membership and vault key grants.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Ids, roles and outcomes only. Never a wrapped key, a signature or a fingerprint: the sharing graph
|
|
/// is already visible to the operator (docs/crypto.md §10) and there is nothing to gain by adding key
|
|
/// material to what a log aggregator keeps.
|
|
/// </remarks>
|
|
internal static partial class TeamLog
|
|
{
|
|
[LoggerMessage(
|
|
EventId = 2101,
|
|
Level = LogLevel.Information,
|
|
Message = "Created team {TeamId} for user {UserId}.")]
|
|
internal static partial void TeamCreated(ILogger logger, Guid teamId, Guid userId);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2102,
|
|
Level = LogLevel.Information,
|
|
Message = "Added user {MemberId} to team {TeamId} as {Role}, by {ActorId}.")]
|
|
internal static partial void MemberAdded(
|
|
ILogger logger, Guid teamId, Guid memberId, Domain.TeamRole role, Guid actorId);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2103,
|
|
Level = LogLevel.Information,
|
|
Message = "Changed user {MemberId} in team {TeamId} to {Role}, by {ActorId}.")]
|
|
internal static partial void MemberRoleChanged(
|
|
ILogger logger, Guid teamId, Guid memberId, Domain.TeamRole role, Guid actorId);
|
|
|
|
/// <remarks>
|
|
/// Warning rather than information, and it names the grant count. Removal is the operation whose
|
|
/// consequences are least like what the word implies — it blocks future reads and returns nothing
|
|
/// already downloaded — so it is the one worth being able to find in a log afterwards.
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2104,
|
|
Level = LogLevel.Warning,
|
|
Message = "Removed user {MemberId} from team {TeamId} by {ActorId}; revoked {GrantCount} vault "
|
|
+ "key grant(s). Vaults are flagged for rekey; already-downloaded data is unaffected.")]
|
|
internal static partial void MemberRemoved(
|
|
ILogger logger, Guid teamId, Guid memberId, Guid actorId, int grantCount);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2105,
|
|
Level = LogLevel.Information,
|
|
Message = "Created team vault {VaultId} for team {TeamId}, by {ActorId}.")]
|
|
internal static partial void TeamVaultCreated(
|
|
ILogger logger, Guid vaultId, Guid teamId, Guid actorId);
|
|
|
|
/// <remarks>
|
|
/// The vault and its team, and no names. A vault name is plaintext on this server, which is not a
|
|
/// reason to copy it into everything a log aggregator keeps for a year.
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2115,
|
|
Level = LogLevel.Information,
|
|
Message = "Renamed vault {VaultId} of team {TeamId}.")]
|
|
internal static partial void VaultRenamed(ILogger logger, Guid vaultId, Guid? teamId);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2106,
|
|
Level = LogLevel.Information,
|
|
Message = "Issued a key grant on vault {VaultId} generation {KeyGeneration} to {RecipientId}, "
|
|
+ "by {ActorId}.")]
|
|
internal static partial void GrantIssued(
|
|
ILogger logger, Guid vaultId, int keyGeneration, Guid recipientId, Guid actorId);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2107,
|
|
Level = LogLevel.Warning,
|
|
Message = "Revoked the key grant on vault {VaultId} held by {RecipientId}, by {ActorId}. "
|
|
+ "Blocks future reads only; see ADR 0001.")]
|
|
internal static partial void GrantRevoked(
|
|
ILogger logger, Guid vaultId, Guid recipientId, Guid actorId);
|
|
|
|
/// <remarks>
|
|
/// Warning, because a rotation is the one operation that changes what every other member's key is
|
|
/// worth: until each of them is wrapped the new generation, they hold the vault's history and
|
|
/// cannot read anything written since. An operator seeing members report an unreadable vault needs
|
|
/// this line and its timestamp to explain it.
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2115,
|
|
Level = LogLevel.Warning,
|
|
Message = "Rotated the key of vault {VaultId} to generation {KeyGeneration}, by {ActorId}. "
|
|
+ "Earlier grants are kept so stored items stay readable; every other member needs the new "
|
|
+ "generation wrapped to them before they can read anything written from now on.")]
|
|
internal static partial void VaultRekeyed(
|
|
ILogger logger, Guid vaultId, int keyGeneration, Guid actorId);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2108,
|
|
Level = LogLevel.Information,
|
|
Message = "Renamed team {TeamId}, by {ActorId}.")]
|
|
internal static partial void TeamUpdated(ILogger logger, Guid teamId, Guid actorId);
|
|
|
|
/// <remarks>
|
|
/// Warning, and it names the member count, for the reason removal does: an archive takes a team
|
|
/// out of every member's list at once and only an operator can put it back.
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2109,
|
|
Level = LogLevel.Warning,
|
|
Message = "Archived team {TeamId} and its {MemberCount} membership(s), by {ActorId}. "
|
|
+ "Recoverable only by an operator clearing deleted_at_utc.")]
|
|
internal static partial void TeamArchived(
|
|
ILogger logger, Guid teamId, Guid actorId, int memberCount);
|
|
|
|
/// <remarks>
|
|
/// Warning rather than information: it is the only operation that takes administrative control of
|
|
/// a team away from the account that had it, and the account it is taken from is not the one
|
|
/// asking afterwards.
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2110,
|
|
Level = LogLevel.Warning,
|
|
Message = "Transferred ownership of team {TeamId} from {FormerOwnerId} to {NewOwnerId}. "
|
|
+ "The former owner is now an admin.")]
|
|
internal static partial void OwnershipTransferred(
|
|
ILogger logger, Guid teamId, Guid formerOwnerId, Guid newOwnerId);
|
|
|
|
/// <remarks>
|
|
/// The invitation id, never the address. TeamLog's rule is ids and outcomes only, and an email is
|
|
/// exactly the kind of personal detail a log aggregator would then keep for its whole retention.
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2111,
|
|
Level = LogLevel.Information,
|
|
Message = "Issued invitation {InvitationId} to team {TeamId} as {Role}, by {ActorId}.")]
|
|
internal static partial void InvitationIssued(
|
|
ILogger logger, Guid invitationId, Guid teamId, Domain.TeamRole role, Guid actorId);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2112,
|
|
Level = LogLevel.Information,
|
|
Message = "Revoked invitation {InvitationId} to team {TeamId}, by {ActorId}.")]
|
|
internal static partial void InvitationRevoked(
|
|
ILogger logger, Guid invitationId, Guid teamId, Guid actorId);
|
|
|
|
[LoggerMessage(
|
|
EventId = 2113,
|
|
Level = LogLevel.Information,
|
|
Message = "User {UserId} claimed invitation {InvitationId} and joined team {TeamId} as {Role}.")]
|
|
internal static partial void InvitationClaimed(
|
|
ILogger logger, Guid userId, Guid invitationId, Guid teamId, Domain.TeamRole role);
|
|
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Warning, and the one log line an operator will need when invitations appear not to work at all.
|
|
/// A provider that does not assert <c>email_verified</c> leaves every invitation pending for ever
|
|
/// with nothing else to show for it, and this is the only place that difference is visible.
|
|
/// </para>
|
|
/// <para>
|
|
/// It names the count and the account, never the address — the address is the thing being refused
|
|
/// as untrustworthy, and writing it to a log would be keeping a claim the server just rejected.
|
|
/// </para>
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2114,
|
|
Level = LogLevel.Warning,
|
|
Message = "Left {InvitationCount} invitation(s) unclaimed for user {UserId}: the access token "
|
|
+ "does not assert that their email address is verified. Check the identity provider "
|
|
+ "sends the email_verified claim.")]
|
|
internal static partial void InvitationNotClaimedUnverified(
|
|
ILogger logger, int invitationCount, Guid userId);
|
|
}
|