Add helpers (#2159)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailAccessHelper.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Helpers;
|
||||
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Decides who may read the mail delivered to an email alias. Every alias is always tied to a manifest.
|
||||
/// </summary>
|
||||
public static class EmailAccessHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if the user may read mail delivered to the email claim.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="claim">The email claim to check access for.</param>
|
||||
/// <param name="userId">The user requesting access.</param>
|
||||
/// <returns>True when the user holds an access key on the alias's manifest.</returns>
|
||||
public static async Task<bool> CanReadClaimAsync(AliasServerDbContext context, EmailClaim claim, string userId)
|
||||
{
|
||||
// An orphaned claim has no manifest and therefore no owner: it is a tombstone holding an address, and nobody may read mail for it.
|
||||
if (claim.VaultManifestId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Holding any access key on the manifest is proof of access: AccountKey on a root manifest, GrantKey on a shared folder (owner self-grant and recipient alike).
|
||||
var hasAccessKey = await context.VaultManifestAccessKeys.AnyAsync(k => k.UserId == userId && k.VaultManifestId == claim.VaultManifestId);
|
||||
if (hasAccessKey)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: legacy fallback — pre-KEK/VEK accounts have no AccountKey row on their root manifest yet, so owning the manifest's group is their only proof of access. Delete once all clients have migrated.
|
||||
return await context.VaultManifests.AnyAsync(m => m.ManifestId == claim.VaultManifestId && context.GroupMembers.Any(gm => gm.GroupId == m.OwnerGroupId && gm.UserId == userId && gm.Role == GroupRole.Owner));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the addresses that the user may read.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="addresses">The addresses to check access for.</param>
|
||||
/// <param name="userId">The user requesting access.</param>
|
||||
/// <returns>The addresses that the user may read.</returns>
|
||||
public static async Task<List<string>> FilterReadableAddressesAsync(AliasServerDbContext context, List<string> addresses, string userId)
|
||||
{
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get the claims that the user may read.
|
||||
var claims = await context.EmailClaims
|
||||
.Where(claim => addresses.Contains(claim.Address) && !claim.Disabled && claim.VaultManifestId != null)
|
||||
.Select(claim => new { claim.Address, ManifestId = claim.VaultManifestId!.Value, claim.VaultManifest!.OwnerGroupId })
|
||||
.ToListAsync();
|
||||
if (claims.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Holding any access key on the manifest is proof of access: AccountKey on a root manifest, GrantKey on a shared folder
|
||||
// (owner self-grant and recipient alike).
|
||||
var manifestIds = claims.Select(c => c.ManifestId).Distinct().ToList();
|
||||
var keyedManifestIds = (await context.VaultManifestAccessKeys
|
||||
.Where(k => k.UserId == userId && manifestIds.Contains(k.VaultManifestId))
|
||||
.Select(k => k.VaultManifestId)
|
||||
.ToListAsync()).ToHashSet();
|
||||
|
||||
// Legacy fallback: pre-KEK/VEK accounts have no AccountKey row on their root manifest yet
|
||||
// so owning the manifest's group is their only proof of access. TODO: delete once all clients have migrated.
|
||||
var ownedGroupIds = (await GroupHelper.GetOwnedGroupIdsAsync(context, userId)).ToHashSet();
|
||||
|
||||
return claims.Where(c => keyedManifestIds.Contains(c.ManifestId) || ownedGroupIds.Contains(c.OwnerGroupId)).Select(c => c.Address).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the ids of the encryption keys the user holds the private half of: their own personal
|
||||
/// keys, plus the keypair of every shared folder they can open.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="userId">The user requesting access.</param>
|
||||
/// <returns>The ids of the encryption keys the user can decrypt with.</returns>
|
||||
public static async Task<List<Guid>> ResolveDecryptableKeyIdsAsync(AliasServerDbContext context, string userId)
|
||||
{
|
||||
// Get the ids of the user's personal keys.
|
||||
var personalKeyIds = await context.VaultManifestDeliveryKeys
|
||||
.Where(k => context.VaultManifests.Any(m => m.ManifestId == k.VaultManifestId && m.IsRoot && context.AliasVaultUsers.Any(u => u.Id == userId && u.PersonalGroupId == m.OwnerGroupId)))
|
||||
.Select(k => k.Id)
|
||||
.ToListAsync();
|
||||
|
||||
// Get the ids of the encryption keys the user can decrypt with.
|
||||
var accessibleManifestIds = await context.VaultManifestAccessKeys
|
||||
.Where(k => k.UserId == userId && k.Type == ManifestKeyType.GrantKey)
|
||||
.Select(k => k.VaultManifestId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
if (accessibleManifestIds.Count == 0)
|
||||
{
|
||||
return personalKeyIds;
|
||||
}
|
||||
|
||||
// Get the ids of the encryption keys the user can decrypt with.
|
||||
var folderKeyIds = await context.VaultManifestDeliveryKeys
|
||||
.Where(k => accessibleManifestIds.Contains(k.VaultManifestId))
|
||||
.Select(k => k.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return [.. personalKeyIds, .. folderKeyIds];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="GroupHelper.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Helpers;
|
||||
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for group operations.
|
||||
/// </summary>
|
||||
public static class GroupHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the ids of every group the user owns.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="userId">The user.</param>
|
||||
/// <returns>The owned group ids.</returns>
|
||||
public static async Task<List<Guid>> GetOwnedGroupIdsAsync(AliasServerDbContext context, string userId)
|
||||
{
|
||||
return await context.GroupMembers.Where(m => m.UserId == userId && m.Role == GroupRole.Owner).Select(m => m.GroupId).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new user's personal group in memory.
|
||||
/// </summary>
|
||||
/// <param name="user">The user the group belongs to.</param>
|
||||
/// <param name="now">Current time.</param>
|
||||
/// <returns>The unpersisted personal group.</returns>
|
||||
public static Group CreatePersonalGroup(AliasVaultUser user, DateTime now)
|
||||
{
|
||||
var group = new Group
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = user.UserName ?? "Personal",
|
||||
Type = GroupType.Personal,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
|
||||
user.PersonalGroupId = group.Id;
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the owner's membership row for a group in memory.
|
||||
/// </summary>
|
||||
/// <param name="group">The group.</param>
|
||||
/// <param name="userId">The owning user.</param>
|
||||
/// <param name="now">Current time.</param>
|
||||
/// <returns>The unpersisted membership.</returns>
|
||||
public static GroupMember CreateOwnerMembership(Group group, string userId, DateTime now)
|
||||
{
|
||||
return new GroupMember
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GroupId = group.Id,
|
||||
UserId = userId,
|
||||
Role = GroupRole.Owner,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the user may administer the group.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="groupId">The group.</param>
|
||||
/// <param name="userId">The user.</param>
|
||||
/// <returns>True when the user is an owner or admin of the group.</returns>
|
||||
public static async Task<bool> IsGroupAdminAsync(AliasServerDbContext context, Guid groupId, string userId)
|
||||
{
|
||||
return await context.GroupMembers.AnyAsync(m => m.GroupId == groupId && m.UserId == userId && (m.Role == GroupRole.Admin || m.Role == GroupRole.Owner));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the root manifest of a personal group, i.e. the manifest a user's personal keys and
|
||||
/// personal aliases are scoped to.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="personalGroupId">The user's personal group.</param>
|
||||
/// <returns>The root manifest id, or null when the group has none.</returns>
|
||||
public static async Task<Guid?> GetRootManifestIdAsync(AliasServerDbContext context, Guid personalGroupId)
|
||||
{
|
||||
return await context.VaultManifests.Where(m => m.IsRoot && m.OwnerGroupId == personalGroupId).Select(m => (Guid?)m.ManifestId).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the user may administer the shared folder.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="manifestId">The shared folder manifest.</param>
|
||||
/// <param name="userId">The user.</param>
|
||||
/// <returns>True when the user can administer the folder.</returns>
|
||||
public static async Task<bool> CanAdministerManifestAsync(AliasServerDbContext context, Guid manifestId, string userId)
|
||||
{
|
||||
var groupId = await context.VaultManifests
|
||||
.Where(m => m.ManifestId == manifestId && !m.IsRoot)
|
||||
.Select(m => (Guid?)m.OwnerGroupId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return groupId is not null && await IsGroupAdminAsync(context, groupId.Value, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the ownership of a shared folder.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="manifestIds">The shared folder manifests to get the ownership of.</param>
|
||||
/// <returns>Manifest id to (owning group id, owning user id).</returns>
|
||||
public static async Task<Dictionary<Guid, (Guid GroupId, string OwnerUserId)>> ResolveQuotaOwnersAsync(AliasServerDbContext context, IEnumerable<Guid> manifestIds)
|
||||
{
|
||||
var ids = manifestIds.Distinct().ToList();
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var rows = await context.VaultManifests
|
||||
.Where(m => ids.Contains(m.ManifestId))
|
||||
.Join(context.GroupMembers.Where(gm => gm.Role == GroupRole.Owner), m => m.OwnerGroupId, gm => gm.GroupId, (m, gm) => new { m.ManifestId, gm.GroupId, gm.UserId, gm.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
return rows
|
||||
.GroupBy(r => r.ManifestId)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(r => r.CreatedAt).ThenBy(r => r.UserId).Select(r => (r.GroupId, r.UserId)).First());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the user to the group as a plain member when they are not already in it.
|
||||
/// </summary>
|
||||
/// <param name="context">Database context.</param>
|
||||
/// <param name="groupId">The group.</param>
|
||||
/// <param name="userId">The user to add.</param>
|
||||
/// <param name="now">Current time.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public static async Task EnsureMembershipAsync(AliasServerDbContext context, Guid groupId, string userId, DateTime now)
|
||||
{
|
||||
var exists = await context.GroupMembers.AnyAsync(m => m.GroupId == groupId && m.UserId == userId);
|
||||
if (exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.GroupMembers.Add(new GroupMember
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GroupId = groupId,
|
||||
UserId = userId,
|
||||
Role = GroupRole.Member,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user