feat!: drop the backend-side elevation windows; password and passkey sessions share capabilities, passkeys guard what nodes verify; the trust page lists added and removed passkeys [CI]
This commit is contained in:
1 parent
f704c66dd9
commit
4106f831dc
34 files changed
+191
-1364
No files matched your search
@@ -43,11 +43,10 @@ public sealed class TrustTests
|
||||
return existing;
|
||||
}
|
||||
|
||||
// 操作员:有 passkey、用 passkey 登录的会话(密码会话什么能力都没有)
|
||||
// 操作员:有 passkey、用 passkey 登录的会话
|
||||
(HttpClient session, SoftwarePasskey passkey, _) = await _backend.CreateStrongSessionAsync("operator");
|
||||
|
||||
// 创世要凭证窗口(立根的人要先证明自己),再用 passkey 签集合本身
|
||||
await ElevateAsync(session, passkey);
|
||||
// 创世:用 passkey 对集合本身签名(节点据此验链)
|
||||
JsonNode state = JsonNode.Parse(await session.GetStringAsync("/api/admin/trust"))!;
|
||||
Assert.False(state["initialized"]!.GetValue<bool>());
|
||||
string statement = state["proposal"]!["statement"]!.GetValue<string>();
|
||||
@@ -83,15 +82,6 @@ public sealed class TrustTests
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task ElevateAsync(HttpClient session, SoftwarePasskey passkey)
|
||||
{
|
||||
using HttpResponseMessage options = await session.PostAsync("/api/admin/auth/elevation/options", BackendFixture.Json(new JsonObject { ["class"] = "credential" }));
|
||||
JsonObject assertion = passkey.Assert(JsonNode.Parse(await options.Content.ReadAsStringAsync())!["challenge"]!.GetValue<string>());
|
||||
assertion["class"] = "credential";
|
||||
using HttpResponseMessage elevated = await session.PostAsync("/api/admin/auth/elevation", BackendFixture.Json(assertion));
|
||||
Assert.Equal(HttpStatusCode.OK, elevated.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>创建一个加入码(普通 API key 就行,不需要 passkey),返回响应里的码和三条安装命令。</summary>
|
||||
private async Task<(string Code, JsonNode Commands, HttpStatusCode Status)> CreateCodeAsync(string role = "probe", string[]? controllers = null)
|
||||
{
|
||||
@@ -235,9 +225,9 @@ public sealed class TrustTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task APasswordSession_CanOnlyRegisterAPasskey_AndCreatingACodeNeedsNoPasskeyTouch()
|
||||
public async Task APasswordSession_ManagesAsUsual_AndCreatingACodeNeedsNoPasskey()
|
||||
{
|
||||
// 密码登录的会话没有任何能力:连看节点列表都不行,只能注册 passkey
|
||||
// 密码登录保留:日常管理(看列表、创建加入码)不需要 passkey;passkey 只用在节点自己会验证的事上(操作员集合的签名)
|
||||
string name = _backend.UniqueName("pwonly");
|
||||
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/users", BackendFixture.Json(new JsonObject { ["username"] = name, ["password"] = "correct horse battery" }));
|
||||
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
||||
@@ -245,14 +235,14 @@ public sealed class TrustTests
|
||||
using HttpResponseMessage login = await password.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = name, ["password"] = "correct horse battery", ["return_session"] = true }));
|
||||
password.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", JsonNode.Parse(await login.Content.ReadAsStringAsync())!["session_token"]!.GetValue<string>());
|
||||
JsonNode me = JsonNode.Parse(await password.GetStringAsync("/api/admin/auth/me"))!;
|
||||
Assert.Empty(me["capabilities"]!.AsArray());
|
||||
Assert.NotEmpty(me["capabilities"]!.AsArray());
|
||||
Assert.Equal("password", me["auth_method"]!.GetValue<string>());
|
||||
Assert.Equal(HttpStatusCode.Forbidden, (await password.GetAsync("/api/admin/agents")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.Forbidden, (await password.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent" }))).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, (await password.GetAsync("/api/admin/agents")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.Created, (await password.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "agent" }))).StatusCode);
|
||||
using HttpResponseMessage options = await password.PostAsync("/api/admin/auth/passkeys/options", null);
|
||||
Assert.Equal(HttpStatusCode.OK, options.StatusCode);
|
||||
|
||||
// passkey 登录的会话创建码:不需要提权窗口、不需要再触碰(创建码不是信任动作,信任在属主固定里)
|
||||
// passkey 登录的会话创建码:同样不需要再触碰(创建码不是信任动作,信任在属主固定里)
|
||||
(HttpClient strong, SoftwarePasskey passkey, _) = await _backend.CreateStrongSessionAsync("creator");
|
||||
using HttpClient strongScope = strong;
|
||||
using SoftwarePasskey passkeyScope = passkey;
|
||||
@@ -260,6 +250,24 @@ public sealed class TrustTests
|
||||
Assert.Equal(HttpStatusCode.Created, code.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheProposal_ListsEveryNewPasskey_SoTheSignerSeesWhatTheyAreAuthorizing()
|
||||
{
|
||||
// 往账号里塞一把 passkey 再等人签名是这个流程唯一的风险:提案要明确列出新增的凭证
|
||||
Operator op = await OperatorAsync();
|
||||
(HttpClient other, SoftwarePasskey added, _) = await _backend.CreateStrongSessionAsync("added");
|
||||
using HttpClient otherScope = other;
|
||||
using SoftwarePasskey addedScope = added;
|
||||
|
||||
JsonNode proposal = JsonNode.Parse(await op.Session.GetStringAsync("/api/admin/trust"))!["proposal"]!;
|
||||
JsonArray list = proposal["added"]!.AsArray();
|
||||
Assert.Contains(list, a => a!["id"]!.GetValue<string>() == added.Id);
|
||||
Assert.All(list, a => Assert.False(string.IsNullOrWhiteSpace(a!["name"]!.GetValue<string>())));
|
||||
Assert.DoesNotContain(list, a => a!["id"]!.GetValue<string>() == op.Passkey.Id);
|
||||
Assert.Empty(proposal["removed"]!.AsArray());
|
||||
Assert.False(proposal["server_key_changed"]!.GetValue<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheServerKey_IsSignedIntoTheOperatorSet_AndOnlyThatServerPassesTheNodesCheck()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user