Public Access
Server: send each item at the revision its ciphertext is sealed at, keep team archiving with the owner, refuse suspended accounts, and stop enrolment failing after an hour
- Pull hydrated every historical change entry with the item's current ciphertext under that entry's old revision. The AAD binds the revision, so a client opened it at the wrong version, failed, and parked its own pending edit as undecryptable. Entries an item has since moved past are left out; the later entry carries the same payload under the right number, and a fresh device no longer downloads an item once per edit it ever had. - Deleting a team's last vault archived the team whoever deleted it, so an admin could archive it out from under its owner. Only the owner's delete archives it now, and TeamService.ArchiveAsync checks. - UserStatus was checked nowhere on the request path. ActiveAccountRequirement on both policies answers 403 account-not-active for a suspended or deprovisioned account. - The hourly last-seen update moves xmin under the tracked user, and enrolment then saved that user against the stale token: a 500 for anybody enrolling more than an hour after first signing in. The entity is reloaded after the update.
This commit is contained in:
1 parent
f332d9a56f
commit
6846f67c20
14 files changed
+277
-12
No files matched your search
@@ -0,0 +1,49 @@
|
||||
using DodoSSH.Domain;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace DodoSSH.Api.Authorization;
|
||||
|
||||
/// <summary>Requires the caller's account to be <see cref="UserStatus.Active"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="UserStatus.Suspended"/> is documented as "sign-in blocked", and nothing blocked it: the identity
|
||||
/// provider still issues tokens for the person, every endpoint resolved them through the current-user context,
|
||||
/// and that context looked an account up by issuer, subject and deletion alone. A suspended account could pull
|
||||
/// and push vaults, take relay tickets and hold the events socket open. This is the check, on both policies, so
|
||||
/// that no endpoint can be reached without it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A connection already open when the account is suspended — a relay tunnel, the events socket — is not
|
||||
/// reached by this; it ends on its own idle and lifetime limits.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class ActiveAccountRequirement : IAuthorizationRequirement;
|
||||
|
||||
/// <summary>Satisfies <see cref="ActiveAccountRequirement"/> from the account row.</summary>
|
||||
internal sealed class ActiveAccountHandler(ICurrentUserContext currentUser, IHttpContextAccessor accessor)
|
||||
: AuthorizationHandler<ActiveAccountRequirement>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override async Task HandleRequirementAsync(
|
||||
AuthorizationHandlerContext context,
|
||||
ActiveAccountRequirement requirement)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
// The policy's authentication requirement already refuses an anonymous caller with a 401; failing this
|
||||
// as well would turn that into a 403.
|
||||
if (context.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cancellationToken = accessor.HttpContext?.RequestAborted ?? CancellationToken.None;
|
||||
|
||||
var user = await currentUser.GetOrProvisionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (user.Status == UserStatus.Active)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user