mod common;
use common::pgwire_auth_helpers::{
assert_readonly_denied, ddl_err, ddl_ok, make_state, make_state_with_catalog, superuser,
};
use nodedb::control::security::credential::store::CredentialStore;
use nodedb::control::security::identity::Role;
use nodedb::types::TenantId;
#[tokio::test]
async fn create_user() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER alice WITH PASSWORD 'secret123' ROLE readwrite TENANT 1",
)
.await;
let user = state.credentials.get_user("alice").unwrap();
assert_eq!(user.tenant_id, TenantId::new(1));
assert!(user.roles.contains(&Role::ReadWrite));
assert!(!user.is_superuser);
}
#[tokio::test]
async fn create_user_duplicate_rejected() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER bob WITH PASSWORD 'pass'").await;
let err = ddl_err(&state, &su, "CREATE USER bob WITH PASSWORD 'pass2'").await;
assert!(
err.contains("already exists"),
"expected duplicate error: {err}"
);
}
#[tokio::test]
async fn create_user_default_role_and_tenant() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER carol WITH PASSWORD 'pass'").await;
let user = state.credentials.get_user("carol").unwrap();
assert!(user.roles.contains(&Role::ReadWrite));
}
#[tokio::test]
async fn drop_user() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER dave WITH PASSWORD 'pass'").await;
ddl_ok(&state, &su, "DROP USER dave").await;
assert!(state.credentials.get_user("dave").is_none());
}
#[tokio::test]
async fn drop_self_rejected() {
let state = make_state();
let su = superuser();
let err = ddl_err(&state, &su, "DROP USER nodedb").await;
assert!(err.contains("cannot drop your own"), "{err}");
}
#[tokio::test]
async fn drop_nonexistent_user() {
let state = make_state();
let su = superuser();
let err = ddl_err(&state, &su, "DROP USER nobody").await;
assert!(err.contains("does not exist"), "{err}");
}
#[tokio::test]
async fn alter_user_password() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER eve WITH PASSWORD 'old'").await;
ddl_ok(&state, &su, "ALTER USER eve SET PASSWORD 'new'").await;
assert!(state.credentials.verify_password("eve", "new"));
assert!(!state.credentials.verify_password("eve", "old"));
}
#[tokio::test]
async fn alter_user_role() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER frank WITH PASSWORD 'pass' ROLE readonly",
)
.await;
ddl_ok(&state, &su, "ALTER USER frank SET ROLE readwrite").await;
let user = state.credentials.get_user("frank").unwrap();
assert!(user.roles.contains(&Role::ReadWrite));
}
#[tokio::test]
async fn drop_then_recreate_same_name() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER demo2 WITH PASSWORD 'oldpass' ROLE readwrite TENANT 2",
)
.await;
ddl_ok(&state, &su, "DROP USER demo2").await;
assert!(state.credentials.get_user("demo2").is_none());
ddl_ok(
&state,
&su,
"CREATE USER demo2 WITH PASSWORD 'newpass' ROLE readwrite TENANT 2",
)
.await;
let user = state
.credentials
.get_user("demo2")
.expect("recreated user must be visible");
assert!(user.is_active);
assert!(
state.credentials.verify_password("demo2", "newpass"),
"recreated user must carry the new password"
);
assert!(
!state.credentials.verify_password("demo2", "oldpass"),
"stale credentials from the dropped user must not survive"
);
}
#[test]
fn dropped_username_is_free_after_restart() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("system.redb");
{
let store = CredentialStore::open(&path).unwrap();
store
.create_user("demo2", "oldpass", TenantId::new(2), vec![Role::ReadWrite])
.unwrap();
assert!(store.drop_user("demo2").unwrap());
}
let store = CredentialStore::open(&path).unwrap();
store
.create_user("demo2", "newpass", TenantId::new(2), vec![Role::ReadWrite])
.expect("recreating a dropped user after restart must succeed");
let user = store
.get_user("demo2")
.expect("recreated user must be visible after restart");
assert!(user.is_active);
}
#[test]
fn dropped_username_is_free_for_service_account() {
let store = CredentialStore::new();
store
.create_user("demo2", "oldpass", TenantId::new(2), vec![Role::ReadWrite])
.unwrap();
assert!(store.drop_user("demo2").unwrap());
store
.create_service_account("demo2", TenantId::new(2), vec![Role::ReadWrite], vec![])
.expect("a dropped user's name must be free for a service account");
}
#[tokio::test]
async fn readonly_cannot_create_user() {
let state = make_state();
assert_readonly_denied(&state, "CREATE USER hacker WITH PASSWORD 'x'").await;
}
#[tokio::test]
async fn readonly_cannot_drop_user() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER target WITH PASSWORD 'pass'").await;
assert_readonly_denied(&state, "DROP USER target").await;
}
#[tokio::test]
async fn drop_user_if_exists_missing_is_noop() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "DROP USER IF EXISTS ghost").await;
}
#[tokio::test]
async fn drop_user_if_exists_existing_drops() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER target WITH PASSWORD 'pass'").await;
ddl_ok(&state, &su, "DROP USER IF EXISTS target").await;
assert!(
state.credentials.get_user("target").is_none(),
"DROP USER IF EXISTS must drop an existing user"
);
}
#[tokio::test]
async fn create_user_tenant_by_name() {
let state = make_state_with_catalog();
let su = superuser();
ddl_ok(&state, &su, "CREATE TENANT acme ID 42").await;
ddl_ok(
&state,
&su,
"CREATE USER alice WITH PASSWORD 'secret123' TENANT 'acme'",
)
.await;
let user = state.credentials.get_user("alice").unwrap();
assert_eq!(
user.tenant_id,
TenantId::new(42),
"TENANT '<name>' must resolve to the named tenant's id"
);
}
#[tokio::test]
async fn create_user_if_not_exists_names_real_user() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER IF NOT EXISTS alice WITH PASSWORD 'pw'",
)
.await;
assert!(
state.credentials.get_user("alice").is_some(),
"user must be created under its real name"
);
assert!(
state.credentials.get_user("IF").is_none(),
"clause keyword must not be created as a user"
);
}
#[tokio::test]
async fn create_user_if_not_exists_is_idempotent() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER alice WITH PASSWORD 'pw'").await;
ddl_ok(
&state,
&su,
"CREATE USER IF NOT EXISTS alice WITH PASSWORD 'pw2'",
)
.await;
}
#[tokio::test]
async fn alter_user_role_without_set_keyword_changes_role() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER eman WITH PASSWORD 'pw' ROLE readonly",
)
.await;
ddl_ok(&state, &su, "ALTER USER eman ROLE tenant_admin").await;
let user = state.credentials.get_user("eman").unwrap();
assert!(
user.roles.contains(&Role::TenantAdmin),
"ALTER USER ... ROLE <role> must update the role: {:?}",
user.roles
);
assert!(!user.roles.contains(&Role::ReadOnly));
}
#[tokio::test]
async fn alter_user_with_role_changes_role() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER eman WITH PASSWORD 'pw' ROLE readonly",
)
.await;
ddl_ok(&state, &su, "ALTER USER eman WITH ROLE tenant_admin").await;
let user = state.credentials.get_user("eman").unwrap();
assert!(user.roles.contains(&Role::TenantAdmin));
assert!(!user.roles.contains(&Role::ReadOnly));
}
#[tokio::test]
async fn alter_user_role_alias_without_role_name_rejected_cleanly() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER eman WITH PASSWORD 'pw' ROLE readonly",
)
.await;
let err = ddl_err(&state, &su, "ALTER USER eman ROLE").await;
assert!(
!err.to_lowercase()
.contains("expected role name after set role"),
"must not surface the misleading legacy wording: {err}"
);
let user = state.credentials.get_user("eman").unwrap();
assert!(user.roles.contains(&Role::ReadOnly));
}
#[tokio::test]
async fn alter_user_set_unknown_action_rejected_cleanly() {
let state = make_state();
let su = superuser();
ddl_ok(
&state,
&su,
"CREATE USER eman WITH PASSWORD 'pw' ROLE readonly",
)
.await;
let err = ddl_err(&state, &su, "ALTER USER eman SET FOO bar").await;
assert!(
err.to_uppercase().contains("FOO") || err.to_uppercase().contains("UNKNOWN"),
"error must name the unrecognized token, not silently route to SET ROLE: {err}"
);
let user = state.credentials.get_user("eman").unwrap();
assert!(user.roles.contains(&Role::ReadOnly));
}
#[tokio::test]
async fn alter_user_password_unknown_subform_does_not_silently_execute() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER eman WITH PASSWORD 'pw'").await;
ddl_ok(&state, &su, "ALTER USER eman PASSWORD EXPIRES IN 30 DAYS").await;
let before = state.credentials.get_user("eman").unwrap();
let expiry_before = before.password_expires_at;
assert!(
expiry_before != 0,
"test precondition: expiry should be set to a finite value, got {expiry_before}"
);
let err = ddl_err(&state, &su, "ALTER USER eman PASSWORD WHATEVER").await;
assert!(
err.to_uppercase().contains("WHATEVER") || err.to_uppercase().contains("UNKNOWN"),
"error must name the unrecognized token: {err}"
);
let after = state.credentials.get_user("eman").unwrap();
assert_eq!(
after.password_expires_at, expiry_before,
"rejected ALTER USER PASSWORD must not silently overwrite expiry to 'never' (0)"
);
}
#[tokio::test]
async fn alter_user_no_subcommand_rejected_cleanly() {
let state = make_state();
let su = superuser();
ddl_ok(&state, &su, "CREATE USER eman WITH PASSWORD 'pw'").await;
let err = ddl_err(&state, &su, "ALTER USER eman").await;
assert!(
!err.to_lowercase()
.contains("expected role name after set role"),
"bare ALTER USER must not be misreported as a SET ROLE failure: {err}"
);
}