use std::collections::HashSet;
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::models::{AuthUser, Role};
use crate::db::{DbPool, queries};
use crate::error::LificError;
use crate::resolve_caller::ResolvedIdentity;
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 user_of(identity: &Option<ResolvedIdentity>) -> Option<AuthUser> {
identity.as_ref().map(|i| i.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,
identity: &Option<ResolvedIdentity>,
project_id: i64,
min: Role,
) -> Result<(), LificError> {
let conn = db.read()?;
require_role_conn(&conn, identity, project_id, min)
}
pub(crate) fn can_view_project(
db: &DbPool,
identity: &ResolvedIdentity,
project_id: i64,
) -> Result<bool, LificError> {
if identity.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, identity.user.id)?,
Some(role) if role >= Role::Viewer
))
}
fn require_role_conn(
conn: &Connection,
identity: &Option<ResolvedIdentity>,
project_id: i64,
min: Role,
) -> Result<(), LificError> {
let auth_user = user_of(identity);
let effective = effective_user(conn, &auth_user);
if matches!(&effective, Some(u) if u.is_admin) {
return Ok(());
}
if authz_enforced_conn(conn)? {
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,
identity: &Option<ResolvedIdentity>,
project_id: i64,
) -> Result<(), LificError> {
if authz_enforced(db)? {
require_role(db, identity, project_id, Role::Maintainer)
} else {
require_role(db, identity, project_id, Role::Lead)
}
}
pub fn require_project_delete_role(
db: &DbPool,
identity: &Option<ResolvedIdentity>,
project_id: i64,
) -> Result<(), LificError> {
if authz_enforced(db)? {
require_role(db, identity, project_id, Role::Lead)
} else {
match identity {
Some(i) if i.user.is_admin => Ok(()),
_ => Err(LificError::Forbidden("only an admin can do this".into())),
}
}
}
pub fn require_workspace_admin(
db: &DbPool,
identity: &Option<ResolvedIdentity>,
) -> Result<(), LificError> {
if !authz_enforced(db)? {
return Ok(());
}
let conn = db.read()?;
let effective = effective_user(&conn, &user_of(identity));
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,
identity: &Option<ResolvedIdentity>,
) -> Result<Option<HashSet<i64>>, LificError> {
let conn = db.read()?;
let effective = effective_user(&conn, &user_of(identity));
if matches!(&effective, Some(u) if u.is_admin) {
return Ok(None);
}
if !authz_enforced_conn(&conn)? {
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()))
}
pub fn filter_visible<T>(
items: Vec<T>,
visible: &Option<HashSet<i64>>,
project_id_of: impl Fn(&T) -> Option<i64>,
) -> Vec<T> {
match visible {
None => items,
Some(ids) => items
.into_iter()
.filter(|it| project_id_of(it).is_some_and(|pid| ids.contains(&pid)))
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actor::Transport;
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 id(user: AuthUser) -> Option<ResolvedIdentity> {
Some(ResolvedIdentity {
user,
transport: Transport::Api,
})
}
fn first_admin_identity(conn: &Connection) -> Option<ResolvedIdentity> {
queries::users::first_admin(conn)
.unwrap()
.map(|admin| ResolvedIdentity {
user: AuthUser {
id: admin.id,
username: admin.username,
display_name: admin.display_name,
is_admin: admin.is_admin,
},
transport: Transport::Api,
})
}
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(),
..Default::default()
},
)
.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, &id(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, &id(viewer.clone()), project, Role::Viewer).is_ok());
assert!(require_role_conn(&conn, &id(viewer.clone()), project, Role::Maintainer).is_err());
assert!(require_role_conn(&conn, &id(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, &id(maintainer.clone()), project, Role::Viewer).is_ok());
assert!(require_role_conn(&conn, &id(maintainer.clone()), project, Role::Maintainer).is_ok());
assert!(require_role_conn(&conn, &id(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, &id(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, &id(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_resolved_first_admin_passes_all_levels_like_operator_key_did() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
let _admin = seed_user(&conn, "admin", true); let project = seed_project(&conn, "OPR");
let identity = first_admin_identity(&conn).expect("first admin resolves");
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role_conn(&conn, &Some(identity.clone()), project, min).is_ok(),
"resolved-first-admin (unbound key) must pass {min} in enforced mode"
);
}
}
#[test]
fn enforced_resolved_non_admin_non_member_stays_forbidden_all_levels() {
let pool = test_db();
let conn = pool.write().unwrap();
enable_enforcement(&conn);
seed_user(&conn, "someone", true); let project = seed_project(&conn, "ONA");
let regular = seed_user(&conn, "regular", false);
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role_conn(&conn, &id(regular.clone()), project, min).is_err(),
"a resolved non-admin non-member must stay Forbidden at {min}"
);
}
}
#[test]
fn legacy_mode_lead_gate_admits_resolved_admin_denies_none() {
let pool = test_db();
let conn = pool.write().unwrap();
seed_user(&conn, "admin", true);
let project = seed_project(&conn, "LGO");
let identity = first_admin_identity(&conn).expect("first admin resolves");
assert!(require_role_conn(&conn, &Some(identity.clone()), project, Role::Lead).is_ok());
assert!(require_role_conn(&conn, &None, project, Role::Lead).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, &id(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(),
lead_user_id: Some(lead.id),
..Default::default()
},
)
.unwrap()
.id;
assert!(require_role_conn(&conn, &id(lead.clone()), project, Role::Lead).is_ok());
assert!(require_role_conn(&conn, &id(admin.clone()), project, Role::Lead).is_ok());
assert!(require_role_conn(&conn, &id(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, &id(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, &id(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, &id(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, &id(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, &id(bot.clone()), project, Role::Viewer).is_ok());
assert!(require_role_conn(&conn, &id(bot.clone()), project, Role::Maintainer).is_ok());
assert!(
require_role_conn(&conn, &id(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, &id(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, &id(bot.clone()), project, Role::Viewer).is_err());
upsert_member(&conn, project, bot.id, Role::Viewer).unwrap();
assert!(require_role_conn(&conn, &id(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, &id(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, &id(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, &id(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_resolved_first_admin_returns_none() {
let pool = test_db();
let identity = {
let conn = pool.write().unwrap();
enable_enforcement(&conn);
seed_user(&conn, "admin", true);
seed_project(&conn, "V1");
seed_project(&conn, "V2");
first_admin_identity(&conn).expect("first admin resolves")
};
assert_eq!(
visible_project_ids(&pool, &None).unwrap(),
Some(HashSet::new())
);
assert_eq!(
visible_project_ids(&pool, &Some(identity.clone())).unwrap(),
None,
"resolved-first-admin (unbound key) sees all projects"
);
}
#[tokio::test]
async fn require_role_admits_resolved_first_admin_denies_none() {
let pool = test_db();
let (project, identity, regular) = {
let conn = pool.write().unwrap();
enable_enforcement(&conn);
seed_user(&conn, "admin", true);
let project = seed_project(&conn, "AMB");
let identity = first_admin_identity(&conn).expect("first admin resolves");
let regular = seed_user(&conn, "regular", false);
(project, identity, regular)
};
assert!(require_role(&pool, &None, project, Role::Viewer).is_err());
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role(&pool, &Some(identity.clone()), project, min).is_ok(),
"resolved-first-admin must pass {min}"
);
}
assert!(
require_role(&pool, &id(regular), project, Role::Viewer).is_err(),
"resolved non-admin is not a bypass"
);
}
#[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(),
lead_user_id: Some(lead.id),
..Default::default()
},
)
.unwrap()
.id;
upsert_member(&conn, project, maintainer.id, Role::Maintainer).unwrap();
drop(conn);
assert!(require_structure_role(&pool, &id(lead), project).is_ok());
assert!(
require_structure_role(&pool, &id(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, &id(maintainer), project).is_ok());
assert!(require_structure_role(&pool, &id(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(),
lead_user_id: Some(lead.id),
..Default::default()
},
)
.unwrap()
.id;
drop(conn);
assert!(require_project_delete_role(&pool, &id(admin), project).is_ok());
assert!(
require_project_delete_role(&pool, &id(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, &id(lead), project).is_ok());
assert!(require_project_delete_role(&pool, &id(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, &id(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, &id(admin)).is_ok());
assert!(require_workspace_admin(&pool, &id(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(),
lead_user_id: Some(lead.id),
..Default::default()
},
)
.unwrap()
.id;
enable_enforcement(&conn);
for min in [Role::Viewer, Role::Maintainer, Role::Lead] {
assert!(
require_role_conn(&conn, &id(lead.clone()), project, min).is_ok(),
"creation-time-seeded lead must retain {min} access once enforcement is on"
);
assert!(
require_role_conn(&conn, &id(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, &id(outsider.clone()), project, Role::Viewer).is_ok());
enable_enforcement(&conn);
assert!(authz_enforced_conn(&conn).unwrap());
assert!(
require_role_conn(&conn, &id(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, &id(outsider), project, Role::Viewer).is_ok());
}
}