CREATE USER [IF NOT EXISTS] WITH PASSWORD ' ' [ROLE ] [TENANT | TENANT ' ']
(
state: &SharedState,
identity: &AuthenticatedIdentity,
username: &str,
password: &str,
role_name: Option<&str>,
tenant: Option<&TenantSelector>,
if_not_exists: bool,
)
| 37 | /// CREATE USER [IF NOT EXISTS] <name> WITH PASSWORD '<password>' [ROLE <role>] |
| 38 | /// [TENANT <id> | TENANT '<name>'] |
| 39 | pub fn create_user( |
| 40 | state: &SharedState, |
| 41 | identity: &AuthenticatedIdentity, |
| 42 | username: &str, |
| 43 | password: &str, |
| 44 | role_name: Option<&str>, |
| 45 | tenant: Option<&TenantSelector>, |
| 46 | if_not_exists: bool, |
| 47 | ) -> PgWireResult<Vec<Response>> { |
| 48 | require_tenant_admin(identity, "create users")?; |
| 49 | |
| 50 | if username.is_empty() { |
| 51 | return Err(sqlstate_error( |
| 52 | "42601", |
| 53 | "syntax: CREATE USER <name> WITH PASSWORD '<password>' [ROLE <role>] [TENANT <id>]", |
| 54 | )); |
| 55 | } |
| 56 | |
| 57 | // `IF NOT EXISTS`: re-creating an existing user is a no-op success. |
| 58 | if if_not_exists && state.credentials.get_user(username).is_some() { |
| 59 | return Ok(vec![Response::Execution(Tag::new("CREATE USER"))]); |
| 60 | } |
| 61 | |
| 62 | if password.is_empty() { |
| 63 | return Err(sqlstate_error( |
| 64 | "42601", |
| 65 | "password must be a single-quoted string", |
| 66 | )); |
| 67 | } |
| 68 | |
| 69 | let role = role_name.map(parse_role).unwrap_or(Role::ReadWrite); |
| 70 | let tenant_id = if let Some(selector) = tenant { |
| 71 | if !identity.is_superuser { |
| 72 | return Err(sqlstate_error("42501", "only superuser can assign tenants")); |
| 73 | } |
| 74 | resolve_tenant_selector(state, selector)? |
| 75 | } else { |
| 76 | identity.tenant_id |
| 77 | }; |
| 78 | |
| 79 | // Build the full `StoredUser` locally (hash + salt + user_id). |
| 80 | // Followers cannot reproduce the random salt, so this step |
| 81 | // MUST happen on the proposer node. The computed record is |
| 82 | // then replicated verbatim. |
| 83 | let stored = state |
| 84 | .credentials |
| 85 | .prepare_user(username, password, tenant_id, vec![role]) |
| 86 | .map_err(|e| sqlstate_error("42710", &e.to_string()))?; |
| 87 | |
| 88 | let entry = crate::control::catalog_entry::CatalogEntry::PutUser(Box::new(stored.clone())); |
| 89 | let log_index = crate::control::metadata_proposer::propose_catalog_entry(state, &entry) |
| 90 | .map_err(|e| sqlstate_error("XX000", &format!("metadata propose: {e}")))?; |
| 91 | if log_index == 0 { |
| 92 | // Single-node / no-cluster fallback: install into the |
| 93 | // in-memory cache so subsequent reads see the user. |
| 94 | // Persist to redb when a catalog is wired up — the |
| 95 | // catalog write is best-effort durability, not a gate |
| 96 | // on the cache update. Test fixtures (and any future |
no test coverage detected