Restrict workspace settings to administrators, add demo countries, Helm chart and instance event log

Employees keep schedule editing but can no longer change workspace preferences or existing
scheduling-directory entries (enforced by the server, hidden in the UI). Demo accounts live in
Cyprus, the United Kingdom and Armenia with matching bank holidays. A Helm chart deploys the
service with a pre-upgrade migrate hook. Refused SSO sign-ins are stored with their support
reference as instance events and searchable on the instance administration page.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-07 15:06:12 +01:00
1 parent 7afdc4be97
commit 548494f12e
11 files changed
+413 -18

No files matched your search

+80
View File
@@ -15,6 +15,7 @@ use meguri::{
service,
store::Store,
};
use serde_json::json;
use std::collections::BTreeSet;
use support::{IsolatedDatabase, account, config_with};
use uuid::Uuid;
@@ -1357,3 +1358,82 @@ async fn employees_plan_schedules_but_only_administrators_change_preferences_and
assert_eq!(changed.document.horizon_days, 45);
assert_eq!(changed.document.people[0].display_name, "Dana Renamed");
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL and CREATEDB"]
async fn the_instance_event_log_is_searchable_newest_first_and_failed_logins_are_capped() {
let iso = IsolatedDatabase::new().await;
let db = &iso.store;
let mut config = config_with(vec![ws("alpha", "Alpha")]);
config.instance_admin_groups = vec!["root".into()];
sync(db, &config, at(9, 0)).await;
let root = account(db, &config, "Root", &["root".into()]).await;
let plain = account(db, &config, "Plain", &["alpha-staff".into()]).await;
assert!(matches!(
admin::events(db, plain.id, "").await,
Err(Error::Forbidden)
));
db.instance_event(
None,
"login.failed",
json!({"reference":"deadbeef","code":"no_access","groups":["/unknown"]}),
)
.await
.unwrap();
admin::create_user(
db,
&config,
root.id,
NewUser {
username: "casey".into(),
display_name: "Casey".into(),
password: "long-enough-password".into(),
email: None,
groups: vec![],
country: None,
},
)
.await
.unwrap();
let all = admin::events(db, root.id, "").await.unwrap();
let actions: Vec<_> = all["events"]
.as_array()
.unwrap()
.iter()
.map(|e| e["action"].as_str().unwrap().to_owned())
.collect();
assert_eq!(actions[0], "user.created", "newest first");
assert!(actions.contains(&"workspace.created".to_owned()));
assert_eq!(all["events"][0]["actor"], root.username);
let by_reference = admin::events(db, root.id, " DEADBEEF ").await.unwrap();
assert_eq!(by_reference["events"].as_array().unwrap().len(), 1);
assert_eq!(
by_reference["events"][0]["details"]["groups"][0],
"/unknown"
);
assert!(
admin::events(db, root.id, "no-such-text").await.unwrap()["events"]
.as_array()
.unwrap()
.is_empty()
);
// Anonymous sign-in failures cannot grow the table without bound, nor push out other events.
for n in 0..2010 {
db.instance_event(None, "login.failed", json!({"reference":format!("{n:08}")}))
.await
.unwrap();
}
let remaining = InstanceAuditRow::objects()
.all(&db.db)
.await
.unwrap()
.into_iter()
.filter(|e| e.action == "login.failed")
.count();
assert!((2000..=2002).contains(&remaining), "{remaining}");
assert!(
!events(db, "workspace.created").await.is_empty()
&& !events(db, "user.created").await.is_empty()
);
iso.drop().await;
}