Merge NEXT: planning, generation map and portable configuration on top of the OIDC access model

NEXT predates the access work, so the identity layer follows master: access comes from
configured groups, instance administrators replace the site administration, and accounts are
created by SSO or by an administrator. Kept from NEXT: manual publication with the Review
changes panel and shared generator drafts, portable import/export (meguri/v2), the layered
generation map with binding provenance, JSM provider changes, contact/approval semantics and
the account-owned Person. Dropped: site administration, local sign-up, and the mandatory
unique email with its contact/account migrations. The manual-publication migration is
renumbered m_0010 after m_0009_access.

Accounts without an email still get a Person in every workspace they reach, a changed sign-in
email updates the account's person, and an account that loses access keeps its linked person so
schedules stay valid. Instance administrators may use Sync and shared drafts.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-07 16:06:10 +01:00
commit 38105c65c2
57 files changed
+6890 -431

No files matched your search

+32 -2
View File
@@ -6,7 +6,7 @@ use meguri::{
admin::{self, NewUser},
domain::*,
identity,
models::UserRow,
models::{HistoryRow, UserRow},
service,
store::{Store, WorkspaceRecord},
};
@@ -59,7 +59,7 @@ pub async fn account(db: &Store, config: &Config, name: &str, groups: &[String])
username: format!("test-{}", Uuid::new_v4().simple()),
display_name: name.into(),
password: "test-only-password".into(),
email: None,
email: Some(format!("{}@example.com", Uuid::new_v4())),
groups: groups.to_vec(),
country: None,
},
@@ -123,6 +123,13 @@ impl Fixture {
config,
}
}
/// Makes `user` a workspace administrator of the fixture workspace through the group mapping.
pub async fn make_admin(&self, user: &User) {
let groups = self.config.workspaces[0].admin_groups.clone();
identity::apply_access(&self.db, &self.config, user.id, Some(groups))
.await
.unwrap();
}
/// A second workspace whose administrator group is granted to `admin` only.
pub async fn extra_workspace(&self, name: &str, admin: &User) -> WorkspaceRecord {
let extra = unique_workspace(name);
@@ -175,6 +182,29 @@ impl Fixture {
}
}
pub async fn prioritize_publication(&self) {
let revision = self.db.workspace(self.team.id).await.unwrap().revision;
self.db
.retry(self.team.id, self.people[0].id, revision)
.await
.unwrap();
// PostgreSQL and the worker can run on different host clocks. Publish only
// once this test's latest committed revision has actually become effective.
let workspace = self.team.id.to_string();
let effective = query!(HistoryRow, $workspace_id == workspace)
.all(&self.db.db)
.await
.unwrap()
.into_iter()
.map(|row| row.effective_at)
.max()
.unwrap();
if let Ok(delay) = (effective - Utc::now()).to_std() {
assert!(
delay < std::time::Duration::from_secs(2),
"Database clock is too far ahead of the test worker"
);
tokio::time::sleep(delay + std::time::Duration::from_millis(1)).await;
}
// The worker takes a bounded batch. Repeated runs leave other test tenants
// in this disposable database, so explicitly put this test's job first.
self.db