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:
justme committed 2026-10-16 06:46:15 +08:00
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);