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 951aade6..b5f76768 100644 Binary files a/resources/schema/schema.json.gz and b/resources/schema/schema.json.gz differ 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();