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
@@ -23,6 +23,8 @@ public sealed class ServerFixture : IAsyncLifetime, IAsyncDisposable
|
||||
public HttpClient Http { get; private set; } = null!;
|
||||
public Uri BaseUri { get; private set; } = null!;
|
||||
public string OwnerAccountToken { get; private set; } = null!;
|
||||
/// <summary>非所有者的默认账号。频道只接受登录账号,所以测试里的「成员」「读者」都是这个账号。</summary>
|
||||
public string MemberAccountToken { get; private set; } = null!;
|
||||
|
||||
public ServerFixture() : this([]) { }
|
||||
|
||||
@@ -47,12 +49,20 @@ public sealed class ServerFixture : IAsyncLifetime, IAsyncDisposable
|
||||
string address = App.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!.Addresses.First();
|
||||
BaseUri = new Uri(address);
|
||||
Http = new HttpClient { BaseAddress = BaseUri, Timeout = TimeSpan.FromSeconds(30) };
|
||||
var accounts = App.Services.GetRequiredService<SqliteAccountStore>();
|
||||
OwnerAccountToken = NewAccountToken("fixture_owner");
|
||||
MemberAccountToken = NewAccountToken("fixture_member");
|
||||
}
|
||||
|
||||
/// <summary>注册一个账号并返回它的会话令牌。</summary>
|
||||
public string NewAccountToken(string username)
|
||||
{
|
||||
var accounts = App.Services.GetRequiredService<SqliteAccountStore>();
|
||||
var verifier = RandomNumberGenerator.GetBytes(32);
|
||||
using var key = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256);
|
||||
var point = key.ExportParameters(false).Q;
|
||||
accounts.Register("fixture_owner", RandomNumberGenerator.GetBytes(16), verifier, new byte[28], JsonSerializer.Serialize(new { kty = "EC", crv = "P-256", x = B64(point.X!), y = B64(point.Y!) }));
|
||||
OwnerAccountToken = accounts.Authenticate("fixture_owner", verifier)!;
|
||||
accounts.Register(username, RandomNumberGenerator.GetBytes(16), verifier, new byte[28], JsonSerializer.Serialize(new { kty = "EC", crv = "P-256", x = B64(point.X!), y = B64(point.Y!) }));
|
||||
return accounts.Authenticate(username, verifier)!;
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
@@ -82,7 +92,8 @@ public sealed class ServerFixture : IAsyncLifetime, IAsyncDisposable
|
||||
public Who Reader => new("read", ReadToken, MemberSecret);
|
||||
}
|
||||
|
||||
public sealed record Who(string Role, byte[]? Token, byte[] EndpointSecret);
|
||||
/// <summary>AccountToken 为空时:所有者用 OwnerAccountToken,其余用 MemberAccountToken。</summary>
|
||||
public sealed record Who(string Role, byte[]? Token, byte[] EndpointSecret, string? AccountToken = null);
|
||||
|
||||
public static string B64(byte[] bytes) => Codec.Encode(bytes);
|
||||
public static string NewId() => B64(RandomNumberGenerator.GetBytes(16));
|
||||
@@ -93,7 +104,10 @@ public sealed class ServerFixture : IAsyncLifetime, IAsyncDisposable
|
||||
req.Headers.Add("X-WD-Role", who.Role);
|
||||
if (who.Token is not null) req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", B64(who.Token));
|
||||
req.Headers.Add("X-WD-Endpoint", B64(who.EndpointSecret));
|
||||
if (who.Role == "owner") req.Headers.Add("X-WD-Account", OwnerAccountToken);
|
||||
// 频道和空间只接受登录账号;便捷分享(/shares)仍由未登录终端使用
|
||||
if (path.StartsWith("/api/v1/channels", StringComparison.Ordinal) || path.StartsWith("/api/v1/spaces", StringComparison.Ordinal))
|
||||
req.Headers.Add("X-WD-Account", who.AccountToken ?? (who.Role == "owner" ? OwnerAccountToken : MemberAccountToken));
|
||||
else if (who.Role == "owner") req.Headers.Add("X-WD-Account", OwnerAccountToken);
|
||||
return req;
|
||||
}
|
||||
|
||||
@@ -118,7 +132,7 @@ public sealed class ServerFixture : IAsyncLifetime, IAsyncDisposable
|
||||
};
|
||||
if (mode == ChannelMode.Enc) body["read_hash"] = B64(TokenHash.Compute(ch.ReadToken));
|
||||
// General transport tests explicitly use open spaces; creation defaults have separate coverage.
|
||||
body["settings"] = settings ?? new { name = "Test space", max_members = 2, join_policy = "open", allow_guests = true };
|
||||
body["settings"] = settings ?? new { name = "Test space", max_members = 2, join_policy = "open" };
|
||||
|
||||
using var req = Req(HttpMethod.Post, "/api/v1/channels", ch.Owner, JsonBody(body));
|
||||
if (capToken is not null) req.Headers.Add("X-WD-Cap", capToken);
|
||||
|
||||
Reference in new issue
Block a user