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
@@ -163,7 +163,7 @@ public sealed class AccountAdmissionTests(ServerFixture server) : IClassFixture<
|
||||
var firstId = Guid.NewGuid().ToString(); var secondId = Guid.NewGuid().ToString();
|
||||
var firstToken = accounts.Authenticate(username, verifier, firstId, "Desktop")!;
|
||||
var secondToken = accounts.Authenticate(username, verifier, secondId, "Phone")!;
|
||||
var ch = await server.CreateAsync(settings: new { name = "Device admission space", max_members = 2, join_policy = "open", allow_guests = true });
|
||||
var ch = await server.CreateAsync(settings: new { name = "Device admission space", max_members = 2, join_policy = "open" });
|
||||
using (var invite = await InviteAsync(ch, username, "opaque-key-package")) Assert.Equal(HttpStatusCode.NoContent, invite.StatusCode);
|
||||
var first = new Who("read", ch.ReadToken, RandomNumberGenerator.GetBytes(32)); var second = first with { EndpointSecret = RandomNumberGenerator.GetBytes(32) };
|
||||
foreach (var (who, token) in new[] { (first, firstToken), (second, secondToken) })
|
||||
@@ -198,7 +198,6 @@ public sealed class AccountAdmissionTests(ServerFixture server) : IClassFixture<
|
||||
{
|
||||
var request = JsonSerializer.Deserialize("{\"settings\":{\"max_members\":8}}", ApiJson.Default.CreateChannelRequest)!;
|
||||
Assert.Equal(JoinPolicy.Open, request.Settings!.JoinPolicy);
|
||||
Assert.True(request.Settings.AllowGuests);
|
||||
request.Settings.Validate(new ServerLimits());
|
||||
var share = JsonSerializer.Deserialize("{\"settings\":{\"max_visitors\":1}}", ApiJson.Default.CreateShareRequest)!;
|
||||
Assert.Equal(86400, share.Settings!.TtlSeconds); share.Settings.Validate(new ServerLimits());
|
||||
@@ -214,9 +213,9 @@ public sealed class AccountAdmissionTests(ServerFixture server) : IClassFixture<
|
||||
}
|
||||
}
|
||||
[Fact]
|
||||
public async Task AccountMembersNeedNoTerminalKeyAndDoNotOccupyGuestRegistrations()
|
||||
public async Task AccountMembersNeedNoTerminalKey()
|
||||
{
|
||||
var ch = await server.CreateAsync(settings: new { name = "Account only", max_members = 3, join_policy = "invite", allow_guests = false });
|
||||
var ch = await server.CreateAsync(settings: new { name = "Account only", max_members = 3, join_policy = "invite" });
|
||||
var username = "member_" + Guid.NewGuid().ToString("N")[..8];
|
||||
var account = await Register(username);
|
||||
using (var invite = await InviteAsync(ch, username, "opaque-key-package")) Assert.Equal(HttpStatusCode.NoContent, invite.StatusCode);
|
||||
@@ -230,9 +229,7 @@ public sealed class AccountAdmissionTests(ServerFixture server) : IClassFixture<
|
||||
log.Headers.Remove("X-WD-Endpoint");
|
||||
Assert.Equal(HttpStatusCode.OK, (await server.Http.SendAsync(log)).StatusCode);
|
||||
}
|
||||
// 账号成员不在访客清单里;成员清单里有它,设备数不小于在线设备数
|
||||
using (var guests = server.Req(HttpMethod.Get, $"/api/v1/channels/{ch.Id}/guests", ch.Owner))
|
||||
Assert.Equal(0, (await ReadJsonAsync(await server.Http.SendAsync(guests))).GetArrayLength());
|
||||
// 成员清单里有它,设备数不小于在线设备数
|
||||
using var members = server.Req(HttpMethod.Get, $"/api/v1/channels/{ch.Id}/members", ch.Owner);
|
||||
var list = await ReadJsonAsync(await server.Http.SendAsync(members));
|
||||
var mine = list.EnumerateArray().Single(m => m.GetProperty("username").GetString() == username);
|
||||
@@ -321,17 +318,17 @@ public sealed class AccountAdmissionTests(ServerFixture server) : IClassFixture<
|
||||
using (var patch = server.Req(HttpMethod.Patch, $"/api/v1/channels/{ch.Id}/settings", ch.Owner, JsonBody(new { join_password = "channel-secret" })))
|
||||
Assert.Equal(HttpStatusCode.OK, (await server.Http.SendAsync(patch)).StatusCode);
|
||||
var guest = new Who("read", mode == ChannelMode.Enc ? ch.ReadToken : null, RandomNumberGenerator.GetBytes(32));
|
||||
using (var missing = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest))
|
||||
var joiner = await Register("joiner_" + Guid.NewGuid().ToString("N")[..8]);
|
||||
using (var missing = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest, joiner))
|
||||
await AssertErrorAsync(await server.Http.SendAsync(missing), 403, "channel_password_required");
|
||||
using (var wrong = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest, body: new { password = "wrong" }))
|
||||
using (var wrong = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest, joiner, new { password = "wrong" }))
|
||||
await AssertErrorAsync(await server.Http.SendAsync(wrong), 403, "channel_password_invalid");
|
||||
using (var correct = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest, body: new { password = "channel-secret" }))
|
||||
using (var correct = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest, joiner, new { password = "channel-secret" }))
|
||||
Assert.Equal(HttpStatusCode.OK, (await server.Http.SendAsync(correct)).StatusCode);
|
||||
using (var closed = server.Req(HttpMethod.Patch, $"/api/v1/channels/{ch.Id}/settings", ch.Owner, JsonBody(new { allow_guests = false })))
|
||||
Assert.Equal(HttpStatusCode.OK, (await server.Http.SendAsync(closed)).StatusCode);
|
||||
using (var stranger = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest with { EndpointSecret = RandomNumberGenerator.GetBytes(32) }, body: new { password = "channel-secret" }))
|
||||
// 没有账号会话的请求一律被拒绝,哪怕链接令牌和密码都对
|
||||
using (var stranger = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", guest, body: new { password = "channel-secret" }))
|
||||
await AssertErrorAsync(await server.Http.SendAsync(stranger), 401, "account_required");
|
||||
using (var patch = server.Req(HttpMethod.Patch, $"/api/v1/channels/{ch.Id}/settings", ch.Owner, JsonBody(new { join_policy = "invite", allow_guests = false })))
|
||||
using (var patch = server.Req(HttpMethod.Patch, $"/api/v1/channels/{ch.Id}/settings", ch.Owner, JsonBody(new { join_policy = "invite" })))
|
||||
Assert.Equal(HttpStatusCode.OK, (await server.Http.SendAsync(patch)).StatusCode);
|
||||
var username = "invited_" + Guid.NewGuid().ToString("N")[..8];
|
||||
var account = await Register(username);
|
||||
@@ -341,9 +338,9 @@ public sealed class AccountAdmissionTests(ServerFixture server) : IClassFixture<
|
||||
using (var invite = await InviteAsync(ch, username, "client-encrypted-payload")) Assert.Equal(HttpStatusCode.NoContent, invite.StatusCode);
|
||||
using (var accepted = Request(HttpMethod.Post, $"/api/v1/channels/{ch.Id}/join", member, account))
|
||||
Assert.Equal(HttpStatusCode.OK, (await server.Http.SendAsync(accepted)).StatusCode);
|
||||
// 只有链接令牌与终端密钥、没有账号会话的请求没有成员身份,也没有访客登记
|
||||
// 只有链接令牌与终端密钥、没有账号会话的请求没有任何身份
|
||||
using (var withoutAccount = Request(HttpMethod.Get, $"/api/v1/channels/{ch.Id}/log", member))
|
||||
await AssertErrorAsync(await server.Http.SendAsync(withoutAccount), 403, "not_joined");
|
||||
await AssertErrorAsync(await server.Http.SendAsync(withoutAccount), 401, "account_required");
|
||||
var writer = member with { Role = "write", Token = ch.WriteToken };
|
||||
string channelId;
|
||||
using (var structureRequest = server.Req(HttpMethod.Get, $"/api/v1/spaces/{ch.Id}/structure", ch.Owner))
|
||||
|
||||
Reference in new issue
Block a user