feat(api): bind encrypted workspaces to accounts and devices

This commit is contained in:
justme committed 2026-10-13 01:38:19 +08:00
1 parent 019b15041a
commit c7d8ef15bc
49 files changed
+1904 -278

No files matched your search

@@ -20,8 +20,8 @@ public class ChannelServiceTests : IDisposable
private static void AssertCode(ErrorCode code, Action act) =>
Assert.Equal(code, Assert.Throws<WebDropException>(act).Code);
private Task<AppendResult> Append(TestChannel ch, Credentials who, byte[] frame, long? maxDownloads = null, string? label = null) =>
Svc.AppendFrameAsync(ch.Id, who, frame, maxDownloads, label);
private Task<AppendResult> Append(TestChannel ch, Credentials who, byte[] frame, long? maxDownloads = null) =>
Svc.AppendFrameAsync(ch.Id, who, frame, maxDownloads);
private Task<AppendResult> AppendSmall(TestChannel ch, Credentials who, Id16? item = null, byte fill = 0x42, FrameFlags extra = FrameFlags.None) =>
Append(ch, who, TestEnv.Frame(item ?? TestEnv.NewId(), 0, TestEnv.SmallEnc | extra, TestEnv.EncPayload, fill));
@@ -44,7 +44,7 @@ public class ChannelServiceTests : IDisposable
ReadHash = Codec.Encode(TokenHash.Compute(ch.ReadToken)),
OwnerHash = Codec.Encode(TokenHash.Compute(ch.OwnerToken)),
};
AssertCode(ErrorCode.Conflict, () => Svc.CreateChannel(dup, _ownerEp));
AssertCode(ErrorCode.Conflict, () => Svc.CreateChannel(dup, _ownerEp, "owner"));
}
[Fact]
@@ -55,9 +55,9 @@ public class ChannelServiceTests : IDisposable
var encNoRead = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Enc, OwnerHash = h() };
var noOwner = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Raw };
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(rawWithRead, _ownerEp));
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(encNoRead, _ownerEp));
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(noOwner, _ownerEp));
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(rawWithRead, _ownerEp, "owner"));
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(encNoRead, _ownerEp, "owner"));
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(noOwner, _ownerEp, "owner"));
}
[Fact]
@@ -60,7 +60,7 @@ public class EnvelopeTests
public void RejectsReservedFlagBits()
{
var bytes = TestEnv.Frame(TestEnv.NewId(), 0, FrameFlags.Final, 4);
bytes[1] |= 0x10;
bytes[1] |= 0x20;
Assert.False(Envelope.TryParse(bytes, out _));
}
@@ -110,8 +110,17 @@ public class AuthTests
public class PolicyTests
{
[Fact]
public void AuthenticatedAuthorsCanManageOwnItemsAcrossDevices()
{
var first = TestEnv.Endpoint(); var second = TestEnv.Endpoint();
var item = ItemBy(first) with { AuthorAccount = "member" };
var channel = Chan(new ChannelSettings { DeletePolicy = DeletePolicy.Own });
Assert.True(ChannelPolicy.CanDeleteItem(channel, item, new Principal(Role.Read, second, "member")));
Assert.False(ChannelPolicy.CanDeleteItem(channel, item, new Principal(Role.Read, first, "other-account")));
}
private static Channel Chan(ChannelSettings s, bool writeHash = true) => new(
TestEnv.NewId(), ChannelMode.Enc, s, new byte[32], writeHash ? new byte[32] : null, new byte[32], 0, 0, 0, 0, 0);
TestEnv.NewId(), ChannelMode.Enc, s, new byte[32], writeHash ? new byte[32] : null, new byte[32], 0, 0, 0, 0, 0, "owner");
private static Item ItemBy(byte[] author) => new(
TestEnv.NewId(), TestEnv.NewId(), author, FrameFlags.None, 0, 0, 0, -1, null, 0, 0, 0, null, null);
+6 -6
View File
@@ -22,7 +22,7 @@ public sealed class RecordingEvents : IChannelEvents
}
/// <summary>一个频道的全部凭据,模拟客户端从 K / A 派生出的令牌。</summary>
public sealed class TestChannel(Id16 id, ChannelMode mode, byte[] read, byte[] write, byte[] owner)
public sealed class TestChannel(Id16 id, ChannelMode mode, byte[] read, byte[] write, byte[] owner, byte[] ownerEndpoint)
{
public Id16 Id { get; } = id;
public ChannelMode Mode { get; } = mode;
@@ -30,9 +30,9 @@ public sealed class TestChannel(Id16 id, ChannelMode mode, byte[] read, byte[] w
public byte[] WriteToken { get; } = write;
public byte[] OwnerToken { get; } = owner;
public Credentials Owner(byte[] endpoint) => new(Role.Owner, OwnerToken, endpoint);
public Credentials Member(byte[] endpoint) => new(Role.Write, WriteToken, endpoint);
public Credentials Reader(byte[] endpoint) => new(Role.Read, ReadToken, endpoint);
public Credentials Owner(byte[] endpoint) => new(Role.Owner, OwnerToken, endpoint, "owner");
public Credentials Member(byte[] endpoint) => new(Role.Write, WriteToken, endpoint, endpoint.AsSpan().SequenceEqual(ownerEndpoint) ? "owner" : null);
public Credentials Reader(byte[] endpoint) => new(Role.Read, ReadToken, endpoint, endpoint.AsSpan().SequenceEqual(ownerEndpoint) ? "owner" : null);
}
public sealed class TestEnv : IDisposable
@@ -76,9 +76,9 @@ public sealed class TestEnv : IDisposable
ReadHash = mode == ChannelMode.Enc ? Codec.Encode(TokenHash.Compute(read)) : null,
WriteHash = withWriteHash ? Codec.Encode(TokenHash.Compute(write)) : null,
OwnerHash = Codec.Encode(TokenHash.Compute(owner)),
}, ownerEndpoint);
}, ownerEndpoint, "owner");
return new TestChannel(id, mode, read, write, owner);
return new TestChannel(id, mode, read, write, owner, ownerEndpoint);
}
/// <summary>构造一个封装。enc 模式的负载长度必须是 档位+16。</summary>
@@ -0,0 +1,24 @@
using WebDrop.Core;
namespace WebDrop.Core.Tests;
public sealed class WorkspaceControlTests
{
[Theory]
[InlineData(ChannelMode.Enc)]
[InlineData(ChannelMode.Raw)]
public async Task ThreadStructureSurvivesContentExpiryAndEviction(ChannelMode mode)
{
using var env = new TestEnv(); var endpoint = TestEnv.Endpoint();
var channel = env.Create(endpoint, mode, new ChannelSettings { TtlSeconds = 60, MaxItems = 1 });
var id = TestEnv.NewId(); var control = FrameFlags.Final | FrameFlags.Control | (mode == ChannelMode.Enc ? FrameFlags.Enc : 0);
var normal = FrameFlags.Final | (mode == ChannelMode.Enc ? FrameFlags.Enc : 0);
int payload = mode == ChannelMode.Enc ? TestEnv.EncPayload : 16;
await env.Service.AppendFrameAsync(channel.Id, channel.Owner(endpoint), TestEnv.Frame(id, 0, control, payload), null);
env.Clock.Advance(TimeSpan.FromSeconds(59));
for (int i = 0; i < 3; i++) await env.Service.AppendFrameAsync(channel.Id, channel.Owner(endpoint), TestEnv.Frame(TestEnv.NewId(), 0, normal, payload), null);
env.Clock.Advance(TimeSpan.FromSeconds(2)); env.Service.Sweep();
Assert.NotNull(env.Store.Read(tx => tx.GetItem(channel.Id, id)));
Assert.Equal(1, env.Store.Read(tx => tx.CountControlItems(channel.Id)));
Assert.Equal(1, env.Store.Read(tx => tx.CountItems(channel.Id)));
}
}