Files
WebDrop/server/src/WebDrop.Core/Channels/ChannelPolicy.cs
T
justme 17638b6e20 refactor(host): single-source ChannelPolicy, space-signed file grants, whole-file registration
- all authorization rules (owner/creator, per-channel read/write grants, who may
  remove whom, delete rules, structure management) live in ChannelPolicy; a test
  fails if they are reimplemented elsewhere
- ChannelInfo carries what the caller may do (can_*), member and invitation lists
  say who can be removed; clients display it and never derive it
- the host no longer stores or calls the file service: it only signs short-lived
  tickets from space permissions (create/put/get/delete/domain), and keeps an opaque
  fileDomain reference on the channel
- a whole file is registered in one request (last frame header + per-frame length and
  digest) instead of one request per 1 MiB frame; the signed manifest still has to
  match every digest
- drop file quota/threshold, receipts, reconcile and delete-queue code from the host
- contract generator emits constants for enums, error codes, token roles, permissions
  and share states
2026-10-19 13:38:40 +08:00

104 lines
6.5 KiB
C#

namespace WebDrop.Core;
/// <summary>谁能做什么。规则只写在这里,服务和测试都调用它。</summary>
public static class ChannelPolicy
{
public static bool IsOwner(Principal p) => p.Role == Role.Owner;
public static bool CanRead(Principal p) => p.Role >= Role.Read;
/// <summary>成员写入需要频道允许上传,且服务端持有写令牌哈希。</summary>
public static bool CanWrite(Channel channel, Principal p) =>
p.Role == Role.Owner
|| (p.Role == Role.Write && channel.Settings.MembersCanUpload && channel.WriteHash is not null);
// ───── 空间所有者与频道访问授权(ChannelAccess):谁能读、写哪个频道的哪个数据域。其他地方只调用这里,不再各写一遍
/// <summary>空间的所有者:持有所有者令牌的人,或者空间的创建账号</summary>
public static bool IsSpaceOwner(Channel space, Principal p) => p.Role == Role.Owner || p.AccountName == space.CreatorAccount;
/// <summary>某个账号在某个频道上的访问授权;没有记录返回 null</summary>
public static ChannelAccess? AccessOf(CryptoState crypto, string? account, string channelId) =>
crypto.Access.Find(a => a.ChannelId == channelId && a.Username == account);
/// <summary>这条授权允许读这个数据域</summary>
public static bool CanReadDomain(ChannelAccess? access, CryptoDomain domain) =>
access is not null && (domain == CryptoDomain.Chat ? access.ChatRead : access.FilesRead);
/// <summary>这条授权允许写这个数据域(聊天域写消息,文件域上传)</summary>
public static bool CanWriteDomain(ChannelAccess? access, CryptoDomain domain) =>
access is not null && (domain == CryptoDomain.Chat ? access.ChatWrite : access.FilesUpload);
/// <summary>读一个对象:能读它所在的数据域,并且对象不早于授权生效的版本(关闭历史可读时,新成员看不到加入前的内容)</summary>
public static bool CanReadObject(ChannelAccess? access, CryptoDomain domain, long objectRevision) =>
CanReadDomain(access, domain) && objectRevision >= (domain == CryptoDomain.Chat ? access!.ChatFromRevision : access!.FilesFromRevision);
/// <summary>频道对这个账号可见:聊天或文件任一域能读</summary>
public static bool CanSeeChannel(ChannelAccess access) => access.ChatRead || access.FilesRead;
/// <summary>能碰这个频道的文件域:能读或能上传</summary>
public static bool CanUseFiles(ChannelAccess access) => access.FilesRead || access.FilesUpload;
/// <summary>能往频道里放东西:聊天或文件任一域能写</summary>
public static bool CanWriteAnyDomain(ChannelAccess? access) => access is not null && (access.ChatWrite || access.FilesUpload);
/// <summary>这个频道里能读某个数据域的账号</summary>
public static IEnumerable<ChannelAccess> Readers(CryptoState crypto, string channelId, CryptoDomain domain) =>
crypto.Access.Where(a => a.ChannelId == channelId && CanReadDomain(a, domain));
/// <summary>
/// 这个空间是否已经按授权记录来约束成员:加密空间有了密钥信封,明文空间有了授权记录或对象登记之后才约束。
/// 之前(刚建好、还没初始化)所有成员的行为和旧版一致。
/// </summary>
public static bool GrantsEnforced(Channel space, CryptoState crypto) =>
space.Mode == ChannelMode.Enc ? crypto.Envelopes.Count > 0 : crypto.Access.Count > 0 || crypto.Objects.Count > 0;
// ───── 空间权限(身份组授予)与结构管理
/// <summary>有没有这项空间权限:所有者拥有全部,其余看身份组授予</summary>
public static bool HasPermission(WorkspaceState? state, Principal principal, string permission) =>
principal.Role == Role.Owner
|| principal.AccountName is { } account && state?.Roles.Any(r => r.Members.Contains(account) && r.Permissions.Contains(permission)) == true;
/// <summary>重命名频道、调整频道所属分组、管理分组(所有者,或被授予「管理频道和分组」)</summary>
public static bool CanManageStructure(WorkspaceState? state, Principal p) => HasPermission(state, p, WorkspacePermission.ManageStructure);
/// <summary>移除成员、撤销邀请的权限(所有者,或被授予「移除成员」)。具体能移除谁见 <see cref="CanRemoveMember"/></summary>
public static bool CanRemoveMembers(WorkspaceState? state, Principal p) => HasPermission(state, p, WorkspacePermission.RemoveMembers);
/// <summary>
/// 谁能移除谁:所有者能移除除自己外的任何人;被授权的成员不能移除自己、空间主,也不能移除同样有移除权限的人。
/// 执行(RevokeInvitation)和给前端的可移除标记(MembersView)都用这一处。
/// </summary>
public static bool CanRemoveMember(WorkspaceState? state, Principal acting, string target, bool targetIsOwner)
{
if (targetIsOwner) return false;
if (acting.Role == Role.Owner) return true;
return acting.AccountName is { } actor && actor != target
&& CanRemoveMembers(state, acting)
&& !CanRemoveMembers(state, new Principal(Role.Write, target));
}
// 下面这几项目前都只有所有者:新建频道(涉及密钥)、文件夹、邀请、改空间设置。要放开时只改这里
public static bool CanCreateChannel(Principal p) => IsOwner(p);
public static bool CanManageFolders(Principal p) => IsOwner(p);
public static bool CanInvite(Principal p) => IsOwner(p);
public static bool CanEditSettings(Principal p) => IsOwner(p);
public static bool CanManageRoles(Principal p) => IsOwner(p);
// ───── 删除条目
/// <summary>能删任何人发的条目:所有者,或空间的删除策略是 anyone</summary>
public static bool CanDeleteAnyItem(Channel channel, Principal p) =>
IsOwner(p) || p.Role >= Role.Read && channel.Settings.DeletePolicy == DeletePolicy.Anyone;
/// <summary>能删自己发的条目:所有者,或删除策略是 own / anyone</summary>
public static bool CanDeleteOwnItem(Channel channel, Principal p) =>
IsOwner(p) || p.Role >= Role.Read && channel.Settings.DeletePolicy is DeletePolicy.Own or DeletePolicy.Anyone;
/// <summary>某个具体条目能不能删:能删任何人的,或者是自己的并且能删自己的</summary>
public static bool CanDeleteItem(Channel channel, Item item, Principal p) =>
CanDeleteAnyItem(channel, p) || item.AuthorAccount == p.AccountName && CanDeleteOwnItem(channel, p);
}