From 1abf1e5ce7e91759f7d4f9794a01bc284a2ab65a Mon Sep 17 00:00:00 2001 From: John Coffey Date: Fri, 30 Oct 2026 20:54:07 -0700 Subject: [PATCH] Migrations: a Management menu link; passwords to whoever takes them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Management › Directory › Migrations, for inbuxa Admin's new page. An older console hides a page it doesn't know, so either can land first. The new accounts' passwords now go into a blob of the administrator who takes them (MG-13). They went to whoever started the migration, so another administrator taking them got a file they couldn't download, and the server's copy was gone. The system test now has a second administrator take them. --- crates/jmap/src/inbuxa/migration.rs | 24 ++++++++++++----------- crates/services/src/inbuxa_migrations.rs | 6 +++--- docs/spec/features/migrations.md | 2 +- resources/schema/schema.json.gz | Bin 155089 -> 155101 bytes resources/schema/schema.json.sha256 | 2 +- tests/src/system/migration.rs | 18 ++++++++++------- 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/crates/jmap/src/inbuxa/migration.rs b/crates/jmap/src/inbuxa/migration.rs index 1fb07c64..eec4e91b 100644 --- a/crates/jmap/src/inbuxa/migration.rs +++ b/crates/jmap/src/inbuxa/migration.rs @@ -444,17 +444,19 @@ pub async fn set( .await .map(|_| None) .map_err(refused), - Some("passwords") => runs::take_passwords(server, migration_id) - .await - .map(|blob| { - let mut out = Map::with_capacity(1); - out.insert_unchecked( - Key::Property(P::PasswordsBlobId), - Value::Str(blob.into()), - ); - Some(Value::Object(out)) - }) - .map_err(refused), + Some("passwords") => { + runs::take_passwords(server, migration_id, access_token.account_id()) + .await + .map(|blob| { + let mut out = Map::with_capacity(1); + out.insert_unchecked( + Key::Property(P::PasswordsBlobId), + Value::Str(blob.into()), + ); + Some(Value::Object(out)) + }) + .map_err(refused) + } Some(_) => Err(invalid( P::Action, "preview, move, finalPass, stop, passwords or close.", diff --git a/crates/services/src/inbuxa_migrations.rs b/crates/services/src/inbuxa_migrations.rs index d3750dc2..d8a86bf7 100644 --- a/crates/services/src/inbuxa_migrations.rs +++ b/crates/services/src/inbuxa_migrations.rs @@ -361,8 +361,8 @@ pub async fn close(server: &Server, id: u32) -> Result<(), Refusal> { } /// MG-13: the new accounts' passwords, once: the file is deleted as it's -/// read. The CSV goes into a blob of whoever started the migration. -pub async fn take_passwords(server: &Server, id: u32) -> Result { +/// read. The CSV goes into a blob of the administrator who takes them. +pub async fn take_passwords(server: &Server, id: u32, account_id: u32) -> Result { let migration = model::get(server.store(), id) .await .map_err(|err| Refusal::Failed(err.to_string()))? @@ -394,7 +394,7 @@ pub async fn take_passwords(server: &Server, id: u32) -> Result return Err(Refusal::NoPasswords); } let blob = server - .put_jmap_blob(migration.created_by_id, &csv) + .put_jmap_blob(account_id, &csv) .await .map_err(|err| Refusal::Failed(err.to_string()))?; for path in files.drain(..) { diff --git a/docs/spec/features/migrations.md b/docs/spec/features/migrations.md index f7d8661c..8eab2377 100644 --- a/docs/spec/features/migrations.md +++ b/docs/spec/features/migrations.md @@ -89,7 +89,7 @@ process of the node the administrator reached. redacts it. - **MG-13.** New accounts' passwords are in the work directory, readable by the server only. The `passwords` action hands them over once, as a blob - of whoever started the migration, and deletes the file. + of the administrator who takes them, and deletes the file. - **MG-14.** Closing a migration deletes its work directory, archives included, and forgets its secret. A closed migration stays listed; destroy is refused. diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index 951aade61ef2f0e0796dba99e2248837e2d0c93d..b5f76768d6fafc77fbeee517f65b6ef6a9e6b354 100644 GIT binary patch delta 2307 zcmV+e3H6)a|)tT zwR*)aNdMsu;cd6_4ScG*a_cqwZg<7!FF$Cmjw##~jYc_+XV`kce+@MZf9OgwHWdAa zIR{P~Gt?|Monl8&{teXF3?dLM+aep7Z#y>gt$2f-X;t#j5YbmjZV$)Hqth?w2P7@j z=OWV*-50rSQq*Imr#EldJiWW~rT-N#E4BVN6&4@2KE1HM^uHoK}}7F)iWQzx+dUT~-)+MnIW% zCV3j=D>?e|w#Djvp(a^V;;d<0o0l4VD8SZzvaZNil{RJV>P@Qh-mu<0*3eIu2dcaG zBkN>TJxn(_`$403e>n|zCunL!WVNRe3_Wrrz}aWPt3Q+@%{{QA0=vm4*9E=8kL0&A zfn-uY;6EKh5CTIM!kU!eK~#dF^r3b6H)Z-dxS(xve$4m{jAVbne>6Fm)9bcg2Zf0k z6b1hvGn{LhpgeHYiX*b2)E2%D!r3O>Mn8Nq{ou=BYWfhFe_&^q$$!Iu)o1K;)4Ck! zZg3MYVuJ+N1=sjaaw!a#ld_)m{&|A(H+1b}0}!x+}ev455h(Lh5kp zmZ0gjC_^eAe>C|8A5U$$5?il>qWzm;jPH|<(G5+z9fmWE;jsrTm_M*Bw~y7a^#zaU zbF>GPIAq6WL5Am|WAKy4s!t_GhTPOK+q6%yP{%vCOemWtFI66PxWTltFc&l~ogOi) zCpbGf?xrrLvVuqY9WAy;q|@D#PBfMuF}_RdX^{?ge|&Koo_nW@O*WzkM5*pPGQ{@+3Ds zJ5S!+TwTOL;zzNYV=%YA9Vo}mVjHQ4lj-d=E`MTtxXASgijuPCdRCIRh(kNIv+h0D z&{3%Ce<%LeG=yP@td#hAK4DXiw`3yIKpQGAZ(_yO<8(&jii{3NvejPrkOApfB3k%X zm>B?q2oX65p|c-20&AYwUS1m>Cpx3Kn3bMyxIFkUA_@j-v0;=>75o2ocD1=}!!YzJ zzCh43G{c4kONXp(``=BxEtr>hQiCBo{rDl-f3i)HYW@^xMAEV>i`3&Y6?fQ@r6?3- z^5$NSP-_}68v?c^Q$c{*%^C8DYdHkm-!8z1^X14l>Q(ZlW_(THLo_%OM@_;+dUH9KhwPm!pxcBwf ze?{N@X|8QNs!zx6ttjLAFbK{k2Dhl|FrcL8Uy?5VuJ87;K$jVZ0I3a*|K&9IG~P;@ z-j@~)o#E`LvO~qs^LS*lUy$C2ae-xfW_*Au^d2J0Wu%MxNvj62!a8}sF@SB9x<4vr zwhxuwnQb^iQig7>|IVToH{_q4r`@R;aFAWzz1lK~k|Y^3RW3 zfD#U28(3ud*>$6a3i?p8;T?SrDpsFieQ2wEe2$h-WRTF8Ny_^t1fTGq6}d(ve{FJU zvUNv)ql6NnQOQWJ*ce0<1YQbW5)&52`|-4Q*IwEvBt{Y;&ELQR4HYaM1a<-APOC|U z#sc>ap#5+r!EG^ z$baHniI5TGLEC}>q}L-eL9xx8f0^Tzr|(txS##f^21ZKN*kcOKexWGgQ&GOZckP?t z^UyYnrBeBo4J$EP_E4zE8sZfd6xIG)2`+NMy#isBHIl>Oo@*khdgXpu!njq<3<^Xe z%F`Q@TQQe=EkKm*ucW0@Fl`y43NA!Hh`X`><0e;qtCSvxeL zc7FWWnR(%J9b*8g(2Gc4#DpN5Ri+WMafgdfp8+;n7RC$-cw);_7n)g7^>)kHODM%I zrP%9X^wQCc>xqyD%F|`jgg|siNZnoq%Chl%xJNm2Crje~sM&dU)}sp+rb?h- z*1nlBO`5)k@(?HZCTL-AloxHT;biB-B&O>l^WTr-Q{V@#xzbe|sfxf|7wbx;;4g zi*Hpi?C#p z4sS2T0i=vJ#v3$I$M>6mUy^rO9Hr$!M8E+K)$6-*{tv>a|0ie?4*-riYkU9z delta 2295 zcmV|34nwFv;z55f3Q;q+uk>xHJjprmha`9ub%WzVr8*+XYiC59<`;7 zzRz>T@AFMZji9K#af;zMNxd8E|1NK&Do!Em1-J1WsEFT<^_^1?m8w-Dc0u|NcL;C0 z)oS2V-8EUSjd!~kK3Vxeb9GGN?qjsTaXiD;1O98MVL-o;vEk!4%sFt{f0&_Wx#^TK zg2HZ~#%2&TXxSFoz#Q4JnQz4#>`beFhlYs0{&9OaUih4TLEjx|p*|OxO6ZQqZIhxN zD?Pn=!{*lAoiF{bcv-3Szp1eJAob~m^`-w6=_yxpJY@B=0>1_tC3-q8zSweyb^x7gmGiAxx> zf4CcgvET5@L%Ch%O`@I>M{tOZdRVwiqGCFc_iYSLaiL5d>$DKYfAHSF_NCrhKY50R zv7c%$;G&rFUm(d~P{n&q)_L}})W_;+j%9hp_m5xxp}8(A3>PDyOgl3=jq;V8b9q}? zbtX`gtSNEUG_IXW4So+`yFFP~oRe5g=Zys3aXT}57-TQuZGO8Yvo1Fci z(Yu_6yAw1uBC^`ke+Y&i*~a7Sv*6Vq%8}+C*inJ4-WiT~;h)l4v%jCadkm)n_f4OO04s_lh207%2n7 zGj$&%iYwXxz*#xb8hjDstW?wi6usS(F>E&g5#@r?HOUZ~$RMN+r!@Obw?!FJ)1a9y z_+@I#mDqY6e-!QC3}bwsbd0WJ+U+o$VGNHwXuT2G5~u;Yu<@Z39HY_c&We*kY)eF;D}*@%;-Fj}=u;D~}M z%R`tRQa(;EaFT^|%@i)}>D%+GIK=5%$FP!X>;3K9xct-vNtY+N>DhVm=H}`m4iZ0# z-5i67^zA@7ZWh}}J)BH$r*Zic<99`_M^KcM#-h}hw}?YKwX^OG*3eO?>nHx#G=yP@ ztd#iTe?4JSj<;kY(?A<4&un7F)#G$VckVSR$J9R+t$8f(Q{g2z#?1 zI09>)*j`>63nx0GxtNunZ@4`8m>~)VYO!IIP8DlT*bAsM{~UB1S_fSMn%L9NKaw_We;qr=KPMfr-84;O=kr(R1ofc-vmszx zG8F`<-JBs?T+1Qg{&oWnn=ePcQLmCWHT`P>ACJK~HfrJ|BHy~bC}taE?pt>^V?)$T zt@geH#Y ze->p)*PY;aVsMMP4g*Sh`6cP%@A_^p3v`)r2oTZW$X-rO596(*>3ylp&>7BXDmzsC zq>e{6`_<=z7#CQ!XT}Gp9Pc5boJP8+pR{TaE3A|Ar2%ZC)cmBFS*vNGG8$la%*QNIBs@D{_rU+ThY;>xuqG2_-_Ke=?6= zvC)Yr2)q=$Bql73_v88C?ya;@NQ@*xn!kYs8mdk@2-Rk>3C}d-vPtEKsYW9Lf1jROUX7*PezE|OA&3%Wue-$ZJ#DOQrHF8&*%W z?4eMRHN-0_D1`mD5?tiiyQ4758p+{s&pRTPdgXpu!njq<3<^Xe%G2wUTQQe=DM9G6 zuG!3meFkD;eYWWqKm$13V`&#}`y42+A!Hh`X{jO64LmbhJ2atoe*D;(e|g~(8)E>e zIEzR*#DpN5Ri+U$XuFFKp8+;n7Wxbccw);_7n+$-#dXWrYbc*CrPz1f=%u3>*ApQR zl&83+34!R4kZwH-G-KoWaA|ih66Bl_-iEx+!~2$~pGPy`W1?)rBWMHR2hJnKcnu4^ zE87P6g1QFu@a9cJiKvn^e}Tm7PiX@Szw-t*|1Q=}AQsaWFdxiN-F_;>K8=a6ZBV&h zo^{9{shF+prUfh~e3K|CazvKN-rMm(Si{>ntzj}ZqB4w7HE)5X#V~GK*Pv~oq{A7$ z;f!GI zb*{trcyZ|58m4*+R2Y777X diff --git a/resources/schema/schema.json.sha256 b/resources/schema/schema.json.sha256 index 5171335f..b8ca8c92 100644 --- a/resources/schema/schema.json.sha256 +++ b/resources/schema/schema.json.sha256 @@ -1 +1 @@ -5cEvhf04ZcIdmZyse9xrcwg1YfsVnLZUo06dAhnFkio \ No newline at end of file +HXMSYoJ437ZjHoG2tQpfBeT-LXUJ_7ZoXHyOVr7623w \ No newline at end of file diff --git a/tests/src/system/migration.rs b/tests/src/system/migration.rs index 42c72107..8569a5e4 100644 --- a/tests/src/system/migration.rs +++ b/tests/src/system/migration.rs @@ -11,9 +11,10 @@ //! A migration is created and checked; a run needs the secret once, and //! the child gets it and a run token through its environment and nothing //! else of the server's. The token signs in as the administrator, from -//! loopback, and ends with the run. Preview, move, the passwords once, the -//! final pass, stop, close; destroy is refused, a tenant is refused, and -//! the audit log never holds the secret. +//! loopback, and ends with the run. Preview, move, the passwords once (to +//! whichever administrator takes them), the final pass, stop, close; +//! destroy is refused, a tenant is refused, and the audit log never holds +//! the secret. //! //! `migration_real_engine_tests` runs the real inbuxa-migrate against a //! Dovecot (`tests/resources/migrations/dovecot-source`), on demand. @@ -261,8 +262,9 @@ pub async fn test(test: &mut TestServer, dir: PathBuf) { let response = update(admin, &id, json!({"description": "Dovecot, done"})).await; assert!(response["updated"].get(&id).is_some(), "{response}"); - // The new passwords, once - let response = update(admin, &id, json!({"action": "passwords"})).await; + // The new passwords, once, to whichever administrator takes them + let other = test.account("admin2@example.com"); + let response = update(other, &id, json!({"action": "passwords"})).await; let blob = response["updated"][&id]["passwordsBlobId"] .as_str() .unwrap_or_else(|| panic!("{response}")) @@ -271,9 +273,9 @@ pub async fn test(test: &mut TestServer, dir: PathBuf) { .get(format!( "{}/jmap/download/{}/{blob}/passwords.csv", run["url"].as_str().unwrap(), - admin.id_string() + other.id_string() )) - .basic_auth(admin.name(), Some(admin.secret())) + .basic_auth(other.name(), Some(other.secret())) .send() .await .unwrap() @@ -423,6 +425,8 @@ pub async fn migration_tests() { } let admin = test.create_admin_account("admin@example.com").await; test.insert_account(admin); + let other = test.create_admin_account("admin2@example.com").await; + test.insert_account(other); self::test(&mut test, dir).await; if test.is_reset() { test.temp_dir.delete(); -- 2.54.0