use std::collections::HashSet;
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::models::{AuthUser, Role};
use crate::db::{DbPool, queries};
use crate::error::LificError;
tokio::task_local! {
static OPERATOR: bool;
}
pub async fn operator_scope<F: std::future::Future>(is_operator: bool, fut: F) -> F::Output {
OPERATOR.scope(is_operator, fut).await
}
fn operator_context() -> bool {
OPERATOR.try_with(|o| *o).unwrap_or(false) || crate::mcp::current_is_operator()
}
pub fn effective_user(conn: &Connection, auth_user: &Option<AuthUser>) -> Option<AuthUser> {
let user = auth_user.as_ref()?;
let bot_owner: Option<Option<i64>> = conn
.query_row(
"SELECT owner_id FROM users WHERE id = ?1 AND is_bot = 1",
params![user.id],
|row| row.get::<_, Option<i64>>(0),
)
.optional()
.ok()
.flatten();
match bot_owner {
Some(Some(owner_id)) => queries::users::get_user_by_id(conn, owner_id)
.ok()
.map(|owner| AuthUser {
id: owner.id,
username: owner.username,
display_name: owner.display_name,
is_admin: owner.is_admin,
})
.or_else(|| Some(user.clone())),
_ => Some(user.clone()),
}
}
fn authz_enforced_conn(conn: &Connection) -> Result<bool, LificError> {
Ok(queries::settings::get(conn)?.authz_enforced)
}
pub fn authz_enforced(db: &DbPool) -> Result<bool, LificError> {
let conn = db.read()?;
authz_enforced_conn(&conn)
}
fn insufficient_role(min: Role) -> LificError {
LificError::Forbidden(format!(
"requires at least '{}' access to this project",
min.as_str()
))
}
pub fn require_role(
db: &DbPool,
auth_user: &Option<AuthUser>,
project_id: i64,
min: Role,
) -> Result<(), LificError> {
let conn = db.read()?;
require_role_conn(&conn, auth_user, project_id, min)
}
pub(crate) fn can_view_project(
db: &DbPool,
user: &AuthUser,
project_id: i64,
) -> Result<bool, LificError> {
if user.is_admin {
return Ok(true);
}
let conn = db.read()?;
if !authz_enforced_conn(&conn)? {
return Ok(true);
}
Ok(matches!(
queries::members::get_member_role(&conn, project_id, user.id)?,
Some(role) if role >= Role::Viewer
))
}
fn require_role_conn(
conn: &Connection,
auth_user: &Option<AuthUser>,
project_id: i64,
min: Role,
) -> Result<(), LificError> {
require_role_conn_op(conn, auth_user, project_id, min, operator_context())
}
fn require_role_conn_op(
conn: &Connection,
auth_user: &Option<AuthUser>,
project_id: i64,
min: Role,
is_operator: bool,
) -> Result<(), LificError> {
let effective = effective_user(conn, auth_user);
if matches!(&effective, Some(u) if u.is_admin) {
return Ok(());
}
if authz_enforced_conn(conn)? {
if is_operator {
return Ok(());
}
require_role_enforced(conn, &effective, project_id, min)
} else {
require_role_legacy(conn, &effective, project_id, min)
}
}
fn require_role_enforced(
conn: &Connection,
effective: &Option<AuthUser>,
project_id: i64,
min: Role,
) -> Result<(), LificError> {
let Some(user) = effective else {
return Err(insufficient_role(min));
};
match queries::members::get_member_role(conn, project_id, user.id)? {
Some(role) if role >= min => Ok(()),
_ => Err(insufficient_role(min)),
}
}
fn require_role_legacy(
conn: &Connection,
effective: &Option<AuthUser>,
project_id: i64,
min: Role,
) -> Result<(), LificError> {
match min {
Role::Lead => require_lead_legacy(conn, effective, project_id),
Role::Maintainer | Role::Viewer => Ok(()),
}
}
fn require_lead_legacy(
conn: &Connection,
effective: &Option<AuthUser>,
project_id: i64,
) -> Result<(), LificError> {
const DENIED: &str = "only the project lead or an admin can do this";
const NO_LEAD: &str = "this project has no lead — only an admin can edit it";
let Some(user) = effective else {
return Err(LificError::Forbidden(DENIED.into()));
};
let project = queries::get_project(conn, project_id)?;
if project.lead_user_id == Some(user.id) {
return Ok(());
}
if queries::members::get_member_role(conn, project_id, user.id)? == Some(Role::Lead) {
return Ok(());
}
if project.lead_user_id.is_some() {
Err(LificError::Forbidden(DENIED.into()))
} else {
Err(LificError::Forbidden(NO_LEAD.into()))
}
}
pub fn require_structure_role(
db: &DbPool,
auth_user: &Option<AuthUser>,
project_id: i64,
) -> Result<(), LificError> {
if authz_enforced(db)? {
require_role(db, auth_user, project_id, Role::Maintainer)
} else {
require_role(db, auth_user, project_id, Role::Lead)
}
}
pub fn require_project_delete_role(
db: &DbPool,
auth_user: &Option<AuthUser>,
project_id: i64,
) -> Result<(), LificError> {
if authz_enforced(db)? {
require_role(db, auth_user, project_id, Role::Lead)
} else {
match auth_user {
Some(user) if user.is_admin => Ok(()),
_ => Err(LificError::Forbidden("only an admin can do this".into())),
}
}
}
pub fn require_workspace_admin(
db: &DbPool,
auth_user: &Option<AuthUser>,
) -> Result<(), LificError> {
if !authz_enforced(db)? {
return Ok(());
}
if operator_context() {
return Ok(());
}
let conn = db.read()?;
let effective = effective_user(&conn, auth_user);
match &effective {
Some(u) if u.is_admin => Ok(()),
_ => Err(LificError::Forbidden(
"only an admin can access workspace-level pages".into(),
)),
}
}
pub fn visible_project_ids(
db: &DbPool,
auth_user: &Option<AuthUser>,
) -> Result<Option<HashSet<i64>>, LificError> {
let conn = db.read()?;
let effective = effective_user(&conn, auth_user);
if matches!(&effective, Some(u) if u.is_admin) {
return Ok(None);
}
if !authz_enforced_conn(&conn)? {
return Ok(None);
}
if operator_context() {
return Ok(None);
}
let Some(user) = effective else {
return Ok(Some(HashSet::new()));
};
let ids = queries::members::list_project_ids_for_user(&conn, user.id)?;
Ok(Some(ids.into_iter().collect()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::models::{CreateProject, CreateUser};
use crate::db::queries::members::upsert_member;
use crate::db::queries::settings::{InstanceSettingsPatch, update as update_settings};
use crate::db::{self, queries};
fn test_db() -> DbPool {
db::open_memory().expect("test db")
}
fn seed_user(conn: &Connection, username: &str, is_admin: bool) -> AuthUser {
let user = queries::users::create_user(
conn,
&CreateUser {
username: username.into(),
email: format!("{username}@test.local"),
password: "testpassword1".into(),
display_name: None,
is_admin,
is_bot: false,
},
)
.unwrap();
AuthUser {
id: user.id,
username: user.username,
display_name: user.display_name,
is_admin: user.is_admin,
}
}
fn seed_bot(conn: &Connection, username: &str, owner_id: Option<i64>) -> AuthUser {
match owner_id {
Some(owner) => {
let bot = queries::users::create_bot_user(conn, owner, username, username).unwrap();
AuthUser {
id: bot.id,
username: bot.username,
display_name: bot.display_name,
is_admin: bot.is_admin,
}
}
None => {
conn.execute(
"INSERT INTO users (username, email, password_hash, display_name, is_admin, is_bot, owner_id)
VALUES (?1, ?2, 'x', ?1, 0, 1, NULL)",
rusqlite::params![username, format!("{username}@bot.local")],
)
.unwrap();
let id = conn.last_insert_rowid();
AuthUser {
id,
username: username.into(),
display_name: username.into(),
is_admin: false,
}
}
}
}
fn seed_project(conn: &Connection, ident: &str) -> i64 {
queries::create_project(
conn,
&CreateProject {
name: format!("Project {ident}"),
identifier: ident.into(),
description: String::new(),
emoji: None,
lead_user_id: None,
},
)
.unwrap()
.id
}
fn enable_enforcement(conn: &Connection) {
update_settings(
conn,
InstanceSettingsPatch { authz_enforced: Some(true), ..Default::default() },
)
.unwrap();
}
#[test]
fn enforced_non_member_denied_at_every_level() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "ENF");
let outsider = seed_user(&conn, "outsider", false);
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
let err = require_role_conn(&conn, &Some(outsider.clone()), project, min).unwrap_err();
assert!(matches!(err, LificError::Forbidden(_)), "denied at {min} got {err:?}");
}
}
#[test]
fn enforced_viewer_allowed_viewer_denied_maintainer_and_lead() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "VWR");
let viewer = seed_user(&conn, "viewer", false);
upsert_member(&conn, project, viewer.id, Role::Viewer).unwrap();
assert!(require_role_conn(&conn, &Some(viewer.clone()), project, Role::Viewer).is_ok());
assert!(require_role_conn(&conn, &Some(viewer.clone()), project, Role::Maintainer).is_err());
assert!(require_role_conn(&conn, &Some(viewer.clone()), project, Role::Lead).is_err());
}
#[test]
fn enforced_maintainer_allowed_viewer_and_maintainer_denied_lead() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "MNT");
let maintainer = seed_user(&conn, "maintainer", false);
upsert_member(&conn, project, maintainer.id, Role::Maintainer).unwrap();
assert!(require_role_conn(&conn, &Some(maintainer.clone()), project, Role::Viewer).is_ok());
assert!(require_role_conn(&conn, &Some(maintainer.clone()), project, Role::Maintainer).is_ok());
assert!(require_role_conn(&conn, &Some(maintainer.clone()), project, Role::Lead).is_err());
}
#[test]
fn enforced_lead_allowed_all_levels() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "LED");
let lead = seed_user(&conn, "lead", false);
upsert_member(&conn, project, lead.id, Role::Lead).unwrap();
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(require_role_conn(&conn, &Some(lead.clone()), project, min).is_ok());
}
}
#[test]
fn enforced_admin_non_member_allowed_all_levels() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "ADM");
let admin = seed_user(&conn, "admin", true);
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(require_role_conn(&conn, &Some(admin.clone()), project, min).is_ok());
}
}
#[test]
fn enforced_none_user_denied_all_levels() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "ANO");
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(require_role_conn(&conn, &None, project, min).is_err());
}
}
#[test]
fn enforced_operator_unbound_key_passes_all_levels() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "OPR");
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role_conn_op(&conn, &None, project, min, true).is_ok(),
"operator-trusted unbound key must pass {min} in enforced mode"
);
}
}
#[test]
fn enforced_legacy_unbound_oauth_none_stays_forbidden_all_levels() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "OAU");
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role_conn_op(&conn, &None, project, min, false).is_err(),
"a legacy unbound OAuth token (None, non-operator) must stay Forbidden at {min}"
);
}
}
#[test]
fn enforced_operator_signal_is_the_only_difference_for_a_nonmember() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "SIG");
assert!(
require_role_conn_op(&conn, &None, project, Role::Viewer, false).is_err(),
"no operator signal: denied"
);
assert!(
require_role_conn_op(&conn, &None, project, Role::Viewer, true).is_ok(),
"operator signal present: allowed"
);
}
#[test]
fn legacy_mode_operator_flag_does_not_change_lead_gate() {
let pool = test_db();
let conn = pool.write().unwrap();
let project = seed_project(&conn, "LGO");
assert!(require_role_conn_op(&conn, &None, project, Role::Lead, true).is_err());
}
#[test]
fn legacy_none_user_and_non_member_allowed_at_viewer_and_maintainer() {
let pool = test_db();
let conn = pool.write().unwrap();
let project = seed_project(&conn, "LGC");
let outsider = seed_user(&conn, "outsider", false);
for min in [Role::Viewer, Role::Maintainer] {
assert!(require_role_conn(&conn, &None, project, min).is_ok(), "None user at {min}");
assert!(
require_role_conn(&conn, &Some(outsider.clone()), project, min).is_ok(),
"non-member at {min}"
);
}
}
#[test]
fn legacy_lead_level_matches_require_project_lead_semantics() {
let pool = test_db();
let conn = pool.write().unwrap();
let admin = seed_user(&conn, "admin", true);
let lead = seed_user(&conn, "lead", false);
let regular = seed_user(&conn, "regular", false);
let project = queries::create_project(
&conn,
&CreateProject {
name: "Legacy Lead".into(),
identifier: "LLD".into(),
description: String::new(),
emoji: None,
lead_user_id: Some(lead.id),
},
)
.unwrap()
.id;
assert!(require_role_conn(&conn, &Some(lead.clone()), project, Role::Lead).is_ok());
assert!(require_role_conn(&conn, &Some(admin.clone()), project, Role::Lead).is_ok());
assert!(require_role_conn(&conn, &Some(regular.clone()), project, Role::Lead).is_err());
assert!(require_role_conn(&conn, &None, project, Role::Lead).is_err());
let co_lead = seed_user(&conn, "co_lead", false);
upsert_member(&conn, project, co_lead.id, Role::Lead).unwrap();
assert!(require_role_conn(&conn, &Some(co_lead), project, Role::Lead).is_ok());
let viewer_only = seed_user(&conn, "viewer_only", false);
upsert_member(&conn, project, viewer_only.id, Role::Viewer).unwrap();
assert!(require_role_conn(&conn, &Some(viewer_only), project, Role::Lead).is_err());
}
#[test]
fn legacy_unowned_project_denies_non_admin_with_no_lead_message() {
let pool = test_db();
let conn = pool.write().unwrap();
let regular = seed_user(&conn, "regular", false);
let project = seed_project(&conn, "UNO");
let err = require_role_conn(&conn, &Some(regular), project, Role::Lead).unwrap_err();
match err {
LificError::Forbidden(msg) => assert!(
msg.contains("no lead"),
"expected 'no lead' carve-out message, got: {msg}"
),
other => panic!("expected Forbidden, got {other:?}"),
}
}
#[test]
fn legacy_admin_can_lead_unowned_project() {
let pool = test_db();
let conn = pool.write().unwrap();
let admin = seed_user(&conn, "admin", true);
let project = seed_project(&conn, "UN2");
assert!(require_role_conn(&conn, &Some(admin), project, Role::Lead).is_ok());
}
#[test]
fn bot_inherits_owner_maintainer_role_passes_maintainer_fails_lead() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "BOT");
let owner = seed_user(&conn, "owner", false);
upsert_member(&conn, project, owner.id, Role::Maintainer).unwrap();
let bot = seed_bot(&conn, "bot1", Some(owner.id));
assert!(require_role_conn(&conn, &Some(bot.clone()), project, Role::Viewer).is_ok());
assert!(require_role_conn(&conn, &Some(bot.clone()), project, Role::Maintainer).is_ok());
assert!(
require_role_conn(&conn, &Some(bot), project, Role::Lead).is_err(),
"bot must never exceed its owner's role"
);
}
#[test]
fn bot_with_admin_owner_passes_everything() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "BAD"); let owner = seed_user(&conn, "admin_owner", true);
let bot = seed_bot(&conn, "bot2", Some(owner.id));
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(require_role_conn(&conn, &Some(bot.clone()), project, min).is_ok());
}
}
#[test]
fn ownerless_bot_is_evaluated_as_itself() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "OWL");
let bot = seed_bot(&conn, "lonebot", None);
assert!(require_role_conn(&conn, &Some(bot.clone()), project, Role::Viewer).is_err());
upsert_member(&conn, project, bot.id, Role::Viewer).unwrap();
assert!(require_role_conn(&conn, &Some(bot), project, Role::Viewer).is_ok());
}
#[test]
fn visible_project_ids_admin_returns_none() {
let pool = test_db();
let admin = {
let conn = pool.write().unwrap();
enable_enforcement(&conn);
seed_user(&conn, "admin", true)
};
assert_eq!(visible_project_ids(&pool, &Some(admin)).unwrap(), None);
}
#[test]
fn visible_project_ids_flag_off_returns_none() {
let pool = test_db();
let user = {
let conn = pool.write().unwrap();
seed_user(&conn, "someone", false)
};
assert_eq!(visible_project_ids(&pool, &Some(user)).unwrap(), None);
assert_eq!(visible_project_ids(&pool, &None).unwrap(), None);
}
#[test]
fn visible_project_ids_flag_on_member_of_two_of_three_returns_those_two() {
let pool = test_db();
let (user, p1, p2, _p3) = {
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let user = seed_user(&conn, "member", false);
let p1 = seed_project(&conn, "PA1");
let p2 = seed_project(&conn, "PA2");
let p3 = seed_project(&conn, "PA3");
upsert_member(&conn, p1, user.id, Role::Viewer).unwrap();
upsert_member(&conn, p2, user.id, Role::Lead).unwrap();
(user, p1, p2, p3)
};
let visible = visible_project_ids(&pool, &Some(user)).unwrap().unwrap();
assert_eq!(visible, HashSet::from([p1, p2]));
}
#[test]
fn visible_project_ids_flag_on_none_user_returns_empty_set() {
let pool = test_db();
{
let conn = pool.write().unwrap();
enable_enforcement(&conn);
}
assert_eq!(visible_project_ids(&pool, &None).unwrap(), Some(HashSet::new()));
}
#[tokio::test]
async fn visible_project_ids_operator_scope_returns_none() {
let pool = test_db();
{
let conn = pool.write().unwrap();
enable_enforcement(&conn);
seed_project(&conn, "V1");
seed_project(&conn, "V2");
}
assert_eq!(
visible_project_ids(&pool, &None).unwrap(),
Some(HashSet::new())
);
let got = operator_scope(true, async { visible_project_ids(&pool, &None) }).await.unwrap();
assert_eq!(got, None, "operator sees all projects (unrestricted)");
}
#[tokio::test]
async fn require_role_reads_ambient_operator_scope() {
let pool = test_db();
let project = {
let conn = pool.write().unwrap();
enable_enforcement(&conn);
seed_project(&conn, "AMB")
};
assert!(require_role(&pool, &None, project, Role::Viewer).is_err());
operator_scope(true, async {
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role(&pool, &None, project, min).is_ok(),
"ambient operator scope must pass {min}"
);
}
})
.await;
operator_scope(false, async {
assert!(
require_role(&pool, &None, project, Role::Viewer).is_err(),
"operator_scope(false) is not a bypass"
);
})
.await;
}
#[test]
fn structure_role_legacy_mode_keeps_lead_gate() {
let pool = test_db();
let conn = pool.write().unwrap();
let lead = seed_user(&conn, "lead", false);
let maintainer = seed_user(&conn, "maintainer", false);
let project = queries::create_project(
&conn,
&CreateProject {
name: "Structure Legacy".into(),
identifier: "SL1".into(),
description: String::new(),
emoji: None,
lead_user_id: Some(lead.id),
},
)
.unwrap()
.id;
upsert_member(&conn, project, maintainer.id, Role::Maintainer).unwrap();
drop(conn);
assert!(require_structure_role(&pool, &Some(lead), project).is_ok());
assert!(
require_structure_role(&pool, &Some(maintainer), project).is_err(),
"maintainer-only membership must not pass the legacy structure gate"
);
}
#[test]
fn structure_role_enforced_mode_loosens_to_maintainer() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "SE1");
let maintainer = seed_user(&conn, "maintainer", false);
let viewer = seed_user(&conn, "viewer", false);
upsert_member(&conn, project, maintainer.id, Role::Maintainer).unwrap();
upsert_member(&conn, project, viewer.id, Role::Viewer).unwrap();
drop(conn);
assert!(require_structure_role(&pool, &Some(maintainer), project).is_ok());
assert!(require_structure_role(&pool, &Some(viewer), project).is_err());
}
#[test]
fn delete_role_legacy_mode_is_admin_only_even_for_the_lead() {
let pool = test_db();
let conn = pool.write().unwrap();
let admin = seed_user(&conn, "admin", true);
let lead = seed_user(&conn, "lead", false);
let project = queries::create_project(
&conn,
&CreateProject {
name: "Delete Legacy".into(),
identifier: "DL1".into(),
description: String::new(),
emoji: None,
lead_user_id: Some(lead.id),
},
)
.unwrap()
.id;
drop(conn);
assert!(require_project_delete_role(&pool, &Some(admin), project).is_ok());
assert!(
require_project_delete_role(&pool, &Some(lead), project).is_err(),
"legacy mode's delete gate is admin-only — a lead must still be refused, matching pre-LIF-194 require_admin"
);
}
#[test]
fn delete_role_enforced_mode_allows_lead() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let project = seed_project(&conn, "DE1");
let lead = seed_user(&conn, "lead", false);
let maintainer = seed_user(&conn, "maintainer", false);
upsert_member(&conn, project, lead.id, Role::Lead).unwrap();
upsert_member(&conn, project, maintainer.id, Role::Maintainer).unwrap();
drop(conn);
assert!(require_project_delete_role(&pool, &Some(lead), project).is_ok());
assert!(require_project_delete_role(&pool, &Some(maintainer), project).is_err());
}
#[test]
fn workspace_admin_legacy_mode_is_a_no_op() {
let pool = test_db();
let conn = pool.write().unwrap();
let regular = seed_user(&conn, "regular", false);
drop(conn);
assert!(require_workspace_admin(&pool, &Some(regular)).is_ok());
assert!(require_workspace_admin(&pool, &None).is_ok());
}
#[test]
fn workspace_admin_enforced_mode_requires_admin() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let admin = seed_user(&conn, "admin", true);
let regular = seed_user(&conn, "regular", false);
drop(conn);
assert!(require_workspace_admin(&pool, &Some(admin)).is_ok());
assert!(require_workspace_admin(&pool, &Some(regular)).is_err());
assert!(require_workspace_admin(&pool, &None).is_err());
}
#[test]
fn enforced_mode_creation_seeded_lead_retains_access_second_user_has_none() {
let pool = test_db();
let conn = pool.write().unwrap();
let lead = seed_user(&conn, "creator_lead", false);
let second = seed_user(&conn, "second_user", false);
let project = queries::create_project(
&conn,
&CreateProject {
name: "Backfill".into(),
identifier: "BAK".into(),
description: String::new(),
emoji: None,
lead_user_id: Some(lead.id),
},
)
.unwrap()
.id;
enable_enforcement(&conn);
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role_conn(&conn, &Some(lead.clone()), project, min).is_ok(),
"creation-time-seeded lead must retain {min} access once enforcement is on"
);
assert!(
require_role_conn(&conn, &Some(second.clone()), project, min).is_err(),
"a second user with no membership row must have no implicit access at {min}, including read"
);
}
}
#[test]
fn toggling_authz_enforced_at_runtime_changes_behavior_without_restart() {
let pool = test_db();
let conn = pool.write().unwrap();
let project = seed_project(&conn, "TGL"); let outsider = seed_user(&conn, "outsider", false);
assert!(!authz_enforced_conn(&conn).unwrap());
assert!(require_role_conn(&conn, &Some(outsider.clone()), project, Role::Viewer).is_ok());
enable_enforcement(&conn);
assert!(authz_enforced_conn(&conn).unwrap());
assert!(
require_role_conn(&conn, &Some(outsider.clone()), project, Role::Viewer).is_err(),
"outsider must now be denied — no membership row"
);
update_settings(
&conn,
InstanceSettingsPatch { authz_enforced: Some(false), ..Default::default() },
)
.unwrap();
assert!(require_role_conn(&conn, &Some(outsider), project, Role::Viewer).is_ok());
}
}