fix(msa): 浏览器登录按钮在生产路径真的置位 browser_first(复审 r1 收口)
复审 r1 的 P1-1:「用浏览器登录」这颗按钮在生产二进制里是哑的——`start_browser_login` 用的是
共享的 `MicrosoftAuth::from_env()`(`browser_first=false`),于是 `browser_then_device_login`
里整段预检 / 回退 / 说明编排不执行,按钮静默退化成设备码,任务③(authcode 可选加速路)
实际未交付。
- P1-1:入口改成 `msa_auth(env, MsaEntry::{DeviceCode,Browser})`,浏览器入口走
`.with_browser_first(true)`。守卫钉两段:①调用点(按 `LoginJob` 分桶记下生产入口实际请求的
模式,避开同进程并发用例串台)②模式真的落到 `MicrosoftAuth::browser_first`。变异实测:
把浏览器入口改回 `DeviceCode` ⇒ 该用例红。核心侧另有报文级证据:浏览器优先路必然对
`/authorize` 发一次预检(`unregistered_redirect_falls_back_to_the_device_code` 断言 GET 计数 == 1)。
- P2-1:`listeners_opened` 改成**绑定现场**计数——`CallbackServer::bind_counting` 在
`TcpListener::bind` 前 +1,`login_counting` 把 per-login 的 `AtomicUsize` 传下去;
删掉「进了授权码分支就记 1」的代理指标。复做变异(预检之前偷绑一个监听口)⇒ 3 条
msalogin 用例红,包括两条 `listeners_opened == 0`。两处 stale 文档(引用不存在的
`callback::bind_attempts()`)改成引用 `listeners_opened` / `bind_counting`。
- P2-2:预检加整体 deadline(10s,`PREFLIGHT_TIMEOUT`),超时归 `PreflightUnavailable` 并
回退设备码;新增「接了连接却不出响应」的假服务器单测(200ms deadline 内必须收场)。
(下载引擎的 HTTP 客户端只设了 connect_timeout,这条兜住「header 之后停住」。)
- P2-3:client_id 守卫加严到三种拼法(`concat!` / 跨行两段 / `format!` 逐段拼),做法是
「先收字符串字面量再拼」+「连续纯 hex 字面量累计 ≥32 且 ≥2 段」,并跳过注释(否则本模块
文档里的示例会自指假红);每条一个自检向量,且不误杀 `session.rs` 的 profile UUID。
- P2-4:页面级断言补中文(`Lang::ZhCn` 走 `Page::build`),并把回退说明的文案出口收敛成
视图自己的 `login::browser_note_text`(视图与取样共用,不再测试另写一份映射)。
- P2-5:主按钮文案改成「使用 Microsoft 账户登录」/「Sign in with your Microsoft account」,
下面补一行「浏览器打不开就用这个:它会给一串用户码,在任何设备上输入即可。」。
门禁:`cargo fmt --check`、`cargo clippy --workspace --all-targets -- -D warnings`、
`cargo test --workspace --all-targets`(51 个 test 目标全绿)通过;`client_id.rs` 的判据拆到
`client_id/tests.rs` 守 400 行红线。截图三张(入口 / 设备码 / 回退)已重出。
This commit is contained in:
1 parent
f82bd395f8
commit
a40b3a5940
15 files changed
+745
-294
No files matched your search
@@ -188,12 +188,11 @@ impl LoginJob {
|
||||
|
||||
/// 起一次微软登录(M178:**GUI 的默认路**——设备码)。
|
||||
///
|
||||
/// 设备码流不依赖任何应用注册项(Azure 那边只需要一个公共客户端 id),所以它是唯一
|
||||
/// 「在官方产物上开箱可用」的路。它**一个本地监听口都不开**,也就不会撞上
|
||||
/// `redirect_uri is not valid` 那张 AAD 错误页(守卫断言见 `callback::bind_attempts`)。
|
||||
/// 设备码流不依赖任何应用注册项,是唯一「在官方产物上开箱可用」的路;它**一个本地监听口都不开**,
|
||||
/// 也就不会撞上 `redirect_uri is not valid`(守卫断言 `listeners_opened == 0`,在 bind 现场加)。
|
||||
pub fn start_login(env: HostEnv, tx: JobSender, job: LoginJob) {
|
||||
spawn("msa-login", move || {
|
||||
let auth = match microsoft_auth(&env) {
|
||||
let auth = match msa_auth(&env, MsaEntry::DeviceCode, &job) {
|
||||
Ok(auth) => auth,
|
||||
Err(error) => return fail(&tx, auth_failure(&error), error.to_string()),
|
||||
};
|
||||
@@ -212,16 +211,13 @@ pub fn start_login(env: HostEnv, tx: JobSender, job: LoginJob) {
|
||||
|
||||
/// 起一次「浏览器优先、失败自动回退设备码」的登录(M178:可选加速路)。
|
||||
///
|
||||
/// 只在用户**显式要求**时才走到这里(账户页的「用浏览器登录」)。即使如此也不盲试:
|
||||
/// `core` 先做回环重定向 URI 的注册预检,没登记就**不绑端口、不弹浏览器**,直接设备码;
|
||||
/// 试了但失败(超时/被拒/端口绑不上)同样回退。两条路都不满意时用户还能在面板上看到
|
||||
/// 一句「为什么改用设备码 + 怎么修」(结构化码 → i18n,原文只进日志)。
|
||||
///
|
||||
/// 与设备码流共用同一个 [`LoginJob`] 取消槽位、同一段账户入库逻辑,所以两条流的
|
||||
/// 凭据/刷新语义完全一致(`core` 侧也共用 [`unlucky_core::auth::MicrosoftSession`])。
|
||||
/// 只在用户**显式要求**时才走到这里(账户页的「用浏览器登录」)。即使如此也不盲试:`core` 先做
|
||||
/// 回环注册预检,没登记就**不绑端口、不弹浏览器**,直接设备码;试了但失败同样回退并给一句说明。
|
||||
pub fn start_browser_login(env: HostEnv, tx: JobSender, job: LoginJob) {
|
||||
spawn("msa-browser", move || {
|
||||
let auth = match microsoft_auth(&env) {
|
||||
// **P1-1 的那一行**:这里是「用户点了用浏览器登录」,必须把 browser_first 置位,
|
||||
// 否则下面这段预检/回退编排整段不执行(复审 r1 实测的洞)。
|
||||
let auth = match msa_auth(&env, MsaEntry::Browser, &job) {
|
||||
Ok(auth) => auth,
|
||||
Err(error) => return fail(&tx, auth_failure(&error), error.to_string()),
|
||||
};
|
||||
@@ -273,16 +269,75 @@ fn browser_note(reason: BrowserFallbackReason) -> BrowserLoginNote {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生产入口的 `MicrosoftAuth`:client id 取环境/构建期,失败日志落 `<数据根>/logs/`。
|
||||
fn microsoft_auth(env: &HostEnv) -> AuthResult<MicrosoftAuth> {
|
||||
/// 微软登录的两个入口(决定 `MicrosoftAuth::browser_first`,M178)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum MsaEntry {
|
||||
/// 默认路:设备码。**一个监听口都不开**,也永远不做回环预检。
|
||||
DeviceCode,
|
||||
/// 用户显式点了「用浏览器登录」:浏览器优先(预检 → 授权码 → 失败自动回退设备码)。
|
||||
Browser,
|
||||
}
|
||||
|
||||
impl MsaEntry {
|
||||
/// 这个入口要不要把 `browser_first` 置位。
|
||||
const fn browser_first(self) -> bool {
|
||||
matches!(self, Self::Browser)
|
||||
}
|
||||
}
|
||||
|
||||
/// 生产入口的 `MicrosoftAuth`(client id 取环境/构建期,失败日志落 `<数据根>/logs/`)。
|
||||
///
|
||||
/// **`browser_first` 必须在这里按入口置位**(复审 r1 P1-1):只构造默认 `MicrosoftAuth` 的话,
|
||||
/// 浏览器按钮会静默退化成设备码——预检/回退码/说明全不可达。
|
||||
fn msa_auth(env: &HostEnv, entry: MsaEntry, _job: &LoginJob) -> AuthResult<MicrosoftAuth> {
|
||||
#[cfg(test)]
|
||||
test_seam::record(_job, entry);
|
||||
MicrosoftAuth::from_env().map(|auth| {
|
||||
auth.with_journal(Some(unlucky_core::auth::AuthJournal::in_logs_dir(
|
||||
env.layout.logs(),
|
||||
)))
|
||||
auth.with_browser_first(entry.browser_first())
|
||||
.with_journal(Some(unlucky_core::auth::AuthJournal::in_logs_dir(
|
||||
env.layout.logs(),
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// `microsoft_auth` 失败 → 原因码。
|
||||
/// 测试缝:记下生产入口**实际请求**的模式,让守卫能钉住「按钮 → browser_first=true」这条线。
|
||||
///
|
||||
/// 为什么需要它:两条入口在没有 client id 的测试构建里都会在网络之前早退,拿不到
|
||||
/// `MicrosoftAuth` 实例去问 `browser_first()`;而 P1-1 的 bug 恰恰在**调用点**。这里在被调用
|
||||
/// 的一刻就把模式记下来,把调用点变成可断言的事实。
|
||||
///
|
||||
/// 为什么按 `LoginJob` 的取消槽指针分桶:同一进程里别的用例也在调这两个入口,一个全局
|
||||
/// 「最后一次」记录会串台(实测过:并行跑时读到别人的 Browser ⇒ 假红)。
|
||||
#[cfg(test)]
|
||||
mod test_seam {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::{LoginJob, MsaEntry};
|
||||
|
||||
static ENTRIES: Mutex<Vec<(usize, bool)>> = Mutex::new(Vec::new());
|
||||
|
||||
fn key(job: &LoginJob) -> usize {
|
||||
Arc::as_ptr(&job.cancelled) as usize
|
||||
}
|
||||
|
||||
pub(super) fn record(job: &LoginJob, entry: MsaEntry) {
|
||||
let key = key(job);
|
||||
let mut entries = ENTRIES.lock().unwrap_or_else(|error| error.into_inner());
|
||||
entries.push((key, entry.browser_first()));
|
||||
}
|
||||
|
||||
/// 这个 job 的生产构造请求的模式(`None` = 还没调用过)。
|
||||
pub(super) fn mode_for(job: &LoginJob) -> Option<bool> {
|
||||
let key = key(job);
|
||||
let entries = ENTRIES.lock().unwrap_or_else(|error| error.into_inner());
|
||||
entries
|
||||
.iter()
|
||||
.find(|(recorded, _)| *recorded == key)
|
||||
.map(|(_, browser_first)| *browser_first)
|
||||
}
|
||||
}
|
||||
|
||||
/// `msa_auth` 失败 → 原因码。
|
||||
///
|
||||
/// **只有「两处都没配 client id」单独成码**(M132):`from_env` 的 `Unsupported` 是它唯一的
|
||||
/// 产出点,且这条路径零网络。上屏的正文是 catalog 里的固定文案(原文照旧只进日志),理由是
|
||||
|
||||
Reference in new issue
Block a user