Public Access
An invitation decided access from an assertion about an address. Everything else
in this model decides it from something a person did — an admin naming an
account, a key holder wrapping a vault key to a key they verified — and this was
the one place a token's email claim was the thing that let somebody in.
It was guarded as tightly as that can be guarded: the claim was refused outright
on an unverified or absent `email_verified`, with no setting to relax it. But the
guard and the risk were the same shape. The whole defence was one boolean sent by
a system the deployment does not control.
So `POST /teams/{id}/members` is the only way in, and an address with no account
is refused with `no-such-account` — which is now the end of the road rather than
the signal to invite. Both clients say the remedy: that person signs in here
once, which is what creates the account, and then they can be added. The desktop
leaves the address in the box, because a message telling you to come back later
is one you act on later.
Gone with it: the `team_invitation` table, the claim hook in the sign-in path,
and `Oidc:EmailVerifiedClaim`, which that hook was the only reader of. Nothing in
the server now reads the email claim to decide anything.
Pending invitations are dropped rather than converted. Converting one would mean
creating a membership because an address matched, which is the property being
removed — and an invitation to an address that did have an account here had
already been claimed by the hourly sweep, so what is left is offers to people who
never arrived.
Two tests carry the property rather than the feature: the endpoint inventory
asserts the three routes are absent, and the API suite adds an address that has
no account, watches the refusal, then signs that address in and checks it joined
nothing. Without the second half, a server that merely renamed the deferred path
would pass.
138 lines
6.4 KiB
C#
138 lines
6.4 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);
|
|
|
|
/// <remarks>
|
|
/// A warning rather than information, and the count is why: this is the one operation that takes a
|
|
/// vault away from everybody at once, and how many keys were withdrawn with it is the number an
|
|
/// operator asked about it will want. No name, for the reason the rename gives.
|
|
/// </remarks>
|
|
[LoggerMessage(
|
|
EventId = 2116,
|
|
Level = LogLevel.Warning,
|
|
Message = "Deleted vault {VaultId} of team {TeamId}, by {ActorId}; withdrew {GrantCount} key "
|
|
+ "grant(s). Its items are kept and unreadable; copies already synced elsewhere are unaffected.")]
|
|
internal static partial void VaultDeleted(
|
|
ILogger logger, Guid vaultId, Guid teamId, Guid actorId, int grantCount);
|
|
|
|
[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);
|
|
}
|