feat(server)!: require a logged-in account for every channel request
Drop unauthenticated guests from channels: the allow_guests setting, the /guests endpoints and the guest seat table are gone, and requests without an account session get 401 account_required. Share links are unaffected. Contract version is now 8.0.0.
This commit is contained in:
1 parent
b1ac11e1d7
commit
c858bd2a09
31 files changed
+194
-358
No files matched your search
@@ -45,7 +45,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, "owner"));
|
||||
AssertCode(ErrorCode.Conflict, () => Svc.CreateChannel(dup, "owner"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -56,9 +56,9 @@ public class ChannelServiceTests : IDisposable
|
||||
var encNoRead = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Enc, Settings = new ChannelSettings { Name = "Enc space" }, OwnerHash = h() };
|
||||
var noOwner = new CreateChannelRequest { Id = TestEnv.NewId().ToBase64Url(), Mode = ChannelMode.Raw, Settings = new ChannelSettings { Name = "No owner space" } };
|
||||
|
||||
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(rawWithRead, _ownerEp, "owner"));
|
||||
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(encNoRead, _ownerEp, "owner"));
|
||||
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(noOwner, _ownerEp, "owner"));
|
||||
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(rawWithRead, "owner"));
|
||||
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(encNoRead, "owner"));
|
||||
AssertCode(ErrorCode.BadRequest, () => Svc.CreateChannel(noOwner, "owner"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -78,10 +78,10 @@ public class ChannelServiceTests : IDisposable
|
||||
public void WrongTokenIsUnauthorized()
|
||||
{
|
||||
var ch = _env.Create(_ownerEp);
|
||||
AssertCode(ErrorCode.Unauthorized, () => Svc.GetChannel(ch.Id, new Credentials(Role.Owner, TestEnv.Random(32), _ownerEp)));
|
||||
AssertCode(ErrorCode.Unauthorized, () => Svc.GetChannel(ch.Id, new Credentials(Role.Read, null, _ownerEp)));
|
||||
AssertCode(ErrorCode.Unauthorized, () => Svc.GetChannel(ch.Id, new Credentials(Role.Owner, TestEnv.Random(32), _ownerEp, "owner")));
|
||||
AssertCode(ErrorCode.Unauthorized, () => Svc.GetChannel(ch.Id, new Credentials(Role.Read, null, _ownerEp, "owner")));
|
||||
// 用读令牌冒充所有者
|
||||
AssertCode(ErrorCode.Unauthorized, () => Svc.GetChannel(ch.Id, new Credentials(Role.Owner, ch.ReadToken, _ownerEp)));
|
||||
AssertCode(ErrorCode.Unauthorized, () => Svc.GetChannel(ch.Id, new Credentials(Role.Owner, ch.ReadToken, _ownerEp, "owner")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -119,13 +119,10 @@ public class ChannelServiceTests : IDisposable
|
||||
Assert.Equal(2, Svc.Join(ch.Id, alice(memberOne, "alice-1")).MembersUsed);
|
||||
Assert.Equal(2, Svc.Join(ch.Id, alice(memberTwo, "alice-2")).MembersUsed);
|
||||
Assert.Equal(2, Svc.Join(ch.Id, ch.Owner(ownerTwo)).MembersUsed);
|
||||
Assert.Empty(Svc.ListGuests(ch.Id, ch.Owner(_ownerEp))); // 账号的设备不对外列出,也不计入成员人数
|
||||
|
||||
var bob = new Credentials(Role.Read, ch.ReadToken, TestEnv.Endpoint(), "bob", "bob-1");
|
||||
AssertCode(ErrorCode.MembersFull, () => Svc.Join(ch.Id, bob));
|
||||
|
||||
AssertCode(ErrorCode.NotFound, () => Svc.ReleaseGuest(ch.Id, ch.Owner(_ownerEp), memberOne)); // 账号设备不能按设备移除,只能撤销用户
|
||||
|
||||
Svc.RevokeInvitation(ch.Id, ch.Owner(_ownerEp), "alice");
|
||||
Assert.Equal(1, Svc.GetChannel(ch.Id, ch.Owner(_ownerEp)).MembersUsed);
|
||||
Assert.Equal(2, Svc.Join(ch.Id, bob with { EndpointHash = TestEnv.Endpoint() }).MembersUsed);
|
||||
@@ -151,7 +148,7 @@ public class ChannelServiceTests : IDisposable
|
||||
Settings = new ChannelSettings { Name = name },
|
||||
OwnerHash = Codec.Encode(TokenHash.Compute(TestEnv.Random(32))),
|
||||
};
|
||||
AssertCode(ErrorCode.SpaceNameRequired, () => Svc.CreateChannel(request, _ownerEp, "owner"));
|
||||
AssertCode(ErrorCode.SpaceNameRequired, () => Svc.CreateChannel(request, "owner"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -204,28 +201,36 @@ public class ChannelServiceTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReleasedSeatLosesAccess()
|
||||
public async Task RevokedMemberLosesAccess()
|
||||
{
|
||||
var ch = _env.Create(_ownerEp);
|
||||
Svc.Join(ch.Id, ch.Member(_memberEp));
|
||||
await AppendSmall(ch, ch.Member(_memberEp));
|
||||
|
||||
Svc.ReleaseGuest(ch.Id, ch.Owner(_ownerEp), _memberEp);
|
||||
Svc.RevokeInvitation(ch.Id, ch.Owner(_ownerEp), ch.AccountOf(_memberEp));
|
||||
await AssertCode(ErrorCode.NotJoined, () => AppendSmall(ch, ch.Member(_memberEp)));
|
||||
AssertCode(ErrorCode.Forbidden, () => Svc.Join(ch.Id, ch.Member(_memberEp))); // 被移除的账号不能凭链接再进来
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyTheOwnerManagesGuestsAndSettings()
|
||||
public async Task ChannelsRequireALoggedInAccount()
|
||||
{
|
||||
var ch = _env.Create(_ownerEp);
|
||||
var anonymous = new Credentials(Role.Read, ch.ReadToken, _memberEp);
|
||||
AssertCode(ErrorCode.AccountRequired, () => Svc.GetChannel(ch.Id, anonymous));
|
||||
AssertCode(ErrorCode.AccountRequired, () => Svc.Join(ch.Id, anonymous));
|
||||
await AssertCode(ErrorCode.AccountRequired, () => LogOf(ch, anonymous));
|
||||
await AssertCode(ErrorCode.AccountRequired, () => AppendSmall(ch, anonymous with { ClaimedRole = Role.Write, Token = ch.WriteToken }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyTheOwnerManagesSettings()
|
||||
{
|
||||
var ch = _env.Create(_ownerEp);
|
||||
Svc.Join(ch.Id, ch.Member(_memberEp));
|
||||
|
||||
AssertCode(ErrorCode.Forbidden, () => Svc.ListGuests(ch.Id, ch.Member(_memberEp)));
|
||||
AssertCode(ErrorCode.Forbidden, () => Svc.UpdateSettings(ch.Id, ch.Member(_memberEp), new ChannelSettingsPatch { MaxMembers = 5 }));
|
||||
AssertCode(ErrorCode.Forbidden, () => Svc.DeleteChannel(ch.Id, ch.Member(_memberEp)));
|
||||
AssertCode(ErrorCode.Forbidden, () => Svc.ReleaseGuest(ch.Id, ch.Member(_memberEp), _ownerEp));
|
||||
|
||||
Assert.Single(Svc.ListGuests(ch.Id, ch.Owner(_ownerEp))); // 访客只有成员那一台
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -238,8 +243,6 @@ public class ChannelServiceTests : IDisposable
|
||||
// Owner management uses A; an owner on a new device can manage a full channel without first taking a seat.
|
||||
Assert.Equal(ch.Id.ToBase64Url(), Svc.GetChannel(ch.Id, unseatedOwner).Id);
|
||||
Assert.Equal(3, Svc.UpdateSettings(ch.Id, unseatedOwner, new ChannelSettingsPatch { MaxMembers = 3 }).Settings.MaxMembers);
|
||||
Assert.Single(Svc.ListGuests(ch.Id, unseatedOwner));
|
||||
AssertCode(ErrorCode.NotFound, () => Svc.ReleaseGuest(ch.Id, unseatedOwner, TestEnv.Endpoint()));
|
||||
Svc.DeleteChannel(ch.Id, unseatedOwner);
|
||||
AssertCode(ErrorCode.NotFound, () => Svc.GetChannel(ch.Id, ch.Owner(_ownerEp)));
|
||||
}
|
||||
@@ -346,7 +349,7 @@ public class ChannelServiceTests : IDisposable
|
||||
var enc = _env.Create(_ownerEp, ChannelMode.Enc);
|
||||
await Append(raw, raw.Owner(_ownerEp), TestEnv.Frame(TestEnv.NewId(), 0, FrameFlags.Final, 10));
|
||||
|
||||
var anonymous = new Credentials(Role.Read, null, _memberEp);
|
||||
var anonymous = new Credentials(Role.Read, null, _memberEp, "reader"); // raw 频道知道 id 即可读,但仍要登录
|
||||
Svc.Join(raw.Id, anonymous);
|
||||
Assert.Single((await Svc.ReadLogAsync(raw.Id, anonymous, 0, 10)).Records);
|
||||
|
||||
|
||||
@@ -116,8 +116,8 @@ public class PolicyTests
|
||||
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")));
|
||||
Assert.True(ChannelPolicy.CanDeleteItem(channel, item, new Principal(Role.Read, "member")));
|
||||
Assert.False(ChannelPolicy.CanDeleteItem(channel, item, new Principal(Role.Read, "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, "owner");
|
||||
@@ -129,12 +129,12 @@ public class PolicyTests
|
||||
public void WritePermission()
|
||||
{
|
||||
var ep = TestEnv.Endpoint();
|
||||
Assert.True(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Owner, ep)));
|
||||
Assert.True(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Write, ep)));
|
||||
Assert.False(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Read, ep)));
|
||||
Assert.False(ChannelPolicy.CanWrite(Chan(new() { MembersCanUpload = false }), new Principal(Role.Write, ep)));
|
||||
Assert.False(ChannelPolicy.CanWrite(Chan(new(), writeHash: false), new Principal(Role.Write, ep)));
|
||||
Assert.True(ChannelPolicy.CanWrite(Chan(new() { MembersCanUpload = false }), new Principal(Role.Owner, ep)));
|
||||
Assert.True(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Owner, "someone")));
|
||||
Assert.True(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Write, "someone")));
|
||||
Assert.False(ChannelPolicy.CanWrite(Chan(new()), new Principal(Role.Read, "someone")));
|
||||
Assert.False(ChannelPolicy.CanWrite(Chan(new() { MembersCanUpload = false }), new Principal(Role.Write, "someone")));
|
||||
Assert.False(ChannelPolicy.CanWrite(Chan(new(), writeHash: false), new Principal(Role.Write, "someone")));
|
||||
Assert.True(ChannelPolicy.CanWrite(Chan(new() { MembersCanUpload = false }), new Principal(Role.Owner, "someone")));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -146,10 +146,9 @@ public class PolicyTests
|
||||
[InlineData(DeletePolicy.Anyone, Role.Read, false, true)]
|
||||
public void DeletePermission(DeletePolicy policy, Role role, bool isAuthor, bool expected)
|
||||
{
|
||||
var me = TestEnv.Endpoint();
|
||||
var item = ItemBy(isAuthor ? me : TestEnv.Endpoint());
|
||||
var item = ItemBy(TestEnv.Endpoint()) with { AuthorAccount = isAuthor ? "me" : "someone-else" };
|
||||
var channel = Chan(new() { DeletePolicy = policy });
|
||||
Assert.Equal(expected, ChannelPolicy.CanDeleteItem(channel, item, new Principal(role, me)));
|
||||
Assert.Equal(expected, ChannelPolicy.CanDeleteItem(channel, item, new Principal(role, "me")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -30,9 +30,11 @@ public sealed class TestChannel(Id16 id, ChannelMode mode, byte[] read, byte[] w
|
||||
public byte[] WriteToken { get; } = write;
|
||||
public byte[] OwnerToken { get; } = owner;
|
||||
|
||||
/// <summary>测试里用 endpoint 当作「哪一个人」:同一个 endpoint 对应同一个账号。</summary>
|
||||
public string AccountOf(byte[] endpoint) => endpoint.AsSpan().SequenceEqual(ownerEndpoint) ? "owner" : "u" + Convert.ToHexString(endpoint)[..8].ToLowerInvariant();
|
||||
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 Credentials Member(byte[] endpoint) => new(Role.Write, WriteToken, endpoint, AccountOf(endpoint));
|
||||
public Credentials Reader(byte[] endpoint) => new(Role.Read, ReadToken, endpoint, AccountOf(endpoint));
|
||||
}
|
||||
|
||||
public sealed class TestEnv : IDisposable
|
||||
@@ -78,7 +80,7 @@ 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, "owner");
|
||||
}, "owner");
|
||||
|
||||
return new TestChannel(id, mode, read, write, owner, ownerEndpoint);
|
||||
}
|
||||
|
||||
@@ -57,15 +57,15 @@ public sealed class WorkspaceStructureTests
|
||||
Assert.Equal("新频道", added.Channels.Single().Name); Assert.Equal(1, added.Revision);
|
||||
}
|
||||
[Fact]
|
||||
public async Task AllowedGuestsCanPlaceTheirUploadsButCannotCreateBusinessStructure()
|
||||
public async Task MembersCanPlaceTheirUploadsButCannotCreateBusinessStructure()
|
||||
{
|
||||
using var env = new TestEnv(); var ownerEp = TestEnv.Endpoint(); var guestEp = TestEnv.Endpoint(); var ch = env.Create(ownerEp);
|
||||
var guest = ch.Member(guestEp); env.Service.Join(ch.Id, guest);
|
||||
using var env = new TestEnv(); var ownerEp = TestEnv.Endpoint(); var memberEp = TestEnv.Endpoint(); var ch = env.Create(ownerEp);
|
||||
var guest = ch.Member(memberEp); env.Service.Join(ch.Id, guest);
|
||||
var item = TestEnv.NewId();
|
||||
await env.Service.AppendFrameAsync(ch.Id, guest, TestEnv.Frame(item, 0, TestEnv.SmallEnc, TestEnv.EncPayload), null);
|
||||
var channelId = env.Service.GetWorkspace(ch.Id, ch.Owner(ownerEp)).Channels.Single().Id;
|
||||
Assert.Single(env.Service.MoveWorkspaceItems(ch.Id, guest, [item.ToBase64Url()], channelId, null).Locations);
|
||||
Assert.Equal(ErrorCode.AccountRequired, Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceGroup(ch.Id, guest, "访客分组")).Code);
|
||||
Assert.Equal(ErrorCode.Forbidden, Assert.Throws<WebDropException>(() => env.Service.CreateWorkspaceGroup(ch.Id, guest, "成员分组")).Code);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user