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
This commit is contained in:
1 parent
ccb2c074d6
commit
17638b6e20
36 files changed
+926
-589
No files matched your search
@@ -159,25 +159,36 @@ public class ChannelServiceTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileGroupQuotaIsPerWorkspaceChannelAndFreedOnDelete()
|
||||
public async Task FileObjectBytesAreNeverStoredByTheHostAWholeFileIsRegisteredByDigestsInOneStep()
|
||||
{
|
||||
_env.Limits.MaxWorkspaceChannelBytes = 200;
|
||||
var space = _env.Create(_ownerEp, ChannelMode.Raw);
|
||||
var owner = space.Owner(_ownerEp);
|
||||
var channelId = Svc.GetWorkspace(space.Id, owner).DefaultChannelId!;
|
||||
var first = TestEnv.NewId(); var second = TestEnv.NewId();
|
||||
Svc.DeclareObject(space.Id, owner, new ObjectEnvelope { ItemId = first.ToBase64Url(), ChannelId = channelId, Domain = CryptoDomain.Files, Epoch = 0, KeyEnvelope = "" });
|
||||
Svc.DeclareObject(space.Id, owner, new ObjectEnvelope { ItemId = second.ToBase64Url(), ChannelId = channelId, Domain = CryptoDomain.Files, Epoch = 0, KeyEnvelope = "" });
|
||||
var item = TestEnv.NewId();
|
||||
Svc.DeclareObject(space.Id, owner, new ObjectEnvelope { ItemId = item.ToBase64Url(), ChannelId = channelId, Domain = CryptoDomain.Files, Epoch = 0, KeyEnvelope = "" });
|
||||
|
||||
byte[] firstFrame = TestEnv.Frame(first, 0, FrameFlags.Final, 120);
|
||||
await Svc.AppendFrameAsync(space.Id, owner, firstFrame, null);
|
||||
Assert.Equal(firstFrame.Length, Svc.GetWorkspace(space.Id, owner).Channels.Single().FileBytes);
|
||||
await AssertCode(ErrorCode.TooLarge, () => Svc.AppendFrameAsync(space.Id, owner, TestEnv.Frame(second, 0, FrameFlags.Final, 60), null));
|
||||
// 文件域的对象不能把字节直接交给主后端
|
||||
byte[] first = TestEnv.Frame(item, 0, FrameFlags.Gated, 120);
|
||||
await AssertCode(ErrorCode.UseFileApi, () => Svc.AppendFrameAsync(space.Id, owner, first, null));
|
||||
|
||||
Svc.DeleteItem(space.Id, owner, first);
|
||||
Assert.Equal(0, Svc.GetWorkspace(space.Id, owner).Channels.Single().FileBytes);
|
||||
await Svc.AppendFrameAsync(space.Id, owner, TestEnv.Frame(second, 0, FrameFlags.Final, 60), null);
|
||||
Assert.Equal(86, Svc.GetWorkspace(space.Id, owner).Channels.Single().FileBytes);
|
||||
// 整个文件一次登记:末帧头部 + 每一帧的(负载长度,整帧摘要)
|
||||
byte[][] frames = [first, TestEnv.Frame(item, 1, FrameFlags.Gated, 80), TestEnv.Frame(item, 2, FrameFlags.Gated | FrameFlags.Final, 40)];
|
||||
ChannelService.FrameEntry[] entries = frames.Select(f => new ChannelService.FrameEntry(f.Length - Envelope.HeaderSize, System.Security.Cryptography.SHA256.HashData(f))).ToArray();
|
||||
var header = frames[^1].AsMemory(0, Envelope.HeaderSize);
|
||||
Assert.True(Svc.RegisterFileItem(space.Id, owner, header, entries, 3).Created);
|
||||
Assert.False(Svc.RegisterFileItem(space.Id, owner, header, entries, 3).Created); // 同一个文件重复登记幂等
|
||||
var other = entries.ToArray(); other[1] = new ChannelService.FrameEntry(other[1].PayloadLength, new byte[32]);
|
||||
Assert.Equal(ErrorCode.Conflict, Assert.Throws<WebDropException>(() => Svc.RegisterFileItem(space.Id, owner, header, other, 3)).Code); // 摘要不同就是冲突
|
||||
Assert.Equal(ErrorCode.BadRequest, Assert.Throws<WebDropException>(() => Svc.RegisterFileItem(space.Id, owner, frames[0].AsMemory(0, Envelope.HeaderSize), entries, 3)).Code); // 头部必须是末帧
|
||||
|
||||
// 聊天域的对象不能这样登记(它们的字节就存在主后端)
|
||||
var chatItem = TestEnv.NewId();
|
||||
Svc.DeclareObject(space.Id, owner, new ObjectEnvelope { ItemId = chatItem.ToBase64Url(), ChannelId = channelId, Domain = CryptoDomain.Chat, Epoch = 0, KeyEnvelope = "" });
|
||||
var chatFrame = TestEnv.Frame(chatItem, 0, FrameFlags.Final, 40);
|
||||
Assert.Equal(ErrorCode.BadRequest, Assert.Throws<WebDropException>(() => Svc.RegisterFileItem(space.Id, owner, chatFrame.AsMemory(0, Envelope.HeaderSize),
|
||||
[new ChannelService.FrameEntry(40, System.Security.Cryptography.SHA256.HashData(chatFrame))], null)).Code);
|
||||
|
||||
Svc.DeleteItem(space.Id, owner, item);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in new issue
Block a user