feat(enterprise): add auth TLS and recovery hardening
This commit is contained in:
@@ -98,6 +98,18 @@ pub async fn handle(
|
||||
"ok": 1.0,
|
||||
}),
|
||||
|
||||
"createUser" => handle_create_user(cmd, db, ctx).await,
|
||||
|
||||
"updateUser" => handle_update_user(cmd, db, ctx).await,
|
||||
|
||||
"dropUser" => handle_drop_user(cmd, db, ctx).await,
|
||||
|
||||
"usersInfo" => handle_users_info(cmd, db, ctx).await,
|
||||
|
||||
"grantRolesToUser" => handle_grant_roles_to_user(cmd, db, ctx).await,
|
||||
|
||||
"revokeRolesFromUser" => handle_revoke_roles_from_user(cmd, db, ctx).await,
|
||||
|
||||
"listDatabases" => handle_list_databases(cmd, ctx).await,
|
||||
|
||||
"listCollections" => handle_list_collections(cmd, db, ctx).await,
|
||||
@@ -144,15 +156,9 @@ pub async fn handle(
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
|
||||
"commitTransaction" => {
|
||||
// Stub: acknowledge.
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
|
||||
"abortTransaction" => {
|
||||
// Stub: acknowledge.
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
"commitTransaction" | "abortTransaction" => Err(CommandError::IllegalOperation(
|
||||
"Transaction numbers are only allowed on a replica set member or mongos".into(),
|
||||
)),
|
||||
|
||||
// Auth stubs - accept silently.
|
||||
"saslStart" => Ok(doc! {
|
||||
@@ -189,6 +195,166 @@ pub async fn handle(
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_create_user(
|
||||
cmd: &Document,
|
||||
db: &str,
|
||||
ctx: &CommandContext,
|
||||
) -> CommandResult<Document> {
|
||||
let username = cmd
|
||||
.get_str("createUser")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing 'createUser' field".into()))?;
|
||||
let password = cmd
|
||||
.get_str("pwd")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing 'pwd' field".into()))?;
|
||||
let roles = parse_roles(cmd, db, "roles")?;
|
||||
ctx.auth
|
||||
.create_user(db, username, password, roles)
|
||||
.map_err(auth_error_to_command_error)?;
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
|
||||
async fn handle_update_user(
|
||||
cmd: &Document,
|
||||
db: &str,
|
||||
ctx: &CommandContext,
|
||||
) -> CommandResult<Document> {
|
||||
let username = cmd
|
||||
.get_str("updateUser")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing 'updateUser' field".into()))?;
|
||||
let password = cmd.get_str("pwd").ok();
|
||||
let roles = if cmd.contains_key("roles") {
|
||||
Some(parse_roles(cmd, db, "roles")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ctx.auth
|
||||
.update_user(db, username, password, roles)
|
||||
.map_err(auth_error_to_command_error)?;
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
|
||||
async fn handle_drop_user(
|
||||
cmd: &Document,
|
||||
db: &str,
|
||||
ctx: &CommandContext,
|
||||
) -> CommandResult<Document> {
|
||||
let username = cmd
|
||||
.get_str("dropUser")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing 'dropUser' field".into()))?;
|
||||
ctx.auth
|
||||
.drop_user(db, username)
|
||||
.map_err(auth_error_to_command_error)?;
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
|
||||
async fn handle_users_info(
|
||||
cmd: &Document,
|
||||
db: &str,
|
||||
ctx: &CommandContext,
|
||||
) -> CommandResult<Document> {
|
||||
let username = match cmd.get("usersInfo") {
|
||||
Some(Bson::String(name)) => Some(name.as_str()),
|
||||
Some(Bson::Document(user_doc)) => user_doc.get_str("user").ok(),
|
||||
_ => None,
|
||||
};
|
||||
let users = ctx.auth.users_info(db, username);
|
||||
let user_docs: Vec<Bson> = users
|
||||
.into_iter()
|
||||
.map(|user| {
|
||||
let roles: Vec<Bson> = user
|
||||
.roles
|
||||
.iter()
|
||||
.map(|role| Bson::Document(role_to_document(&user.database, role)))
|
||||
.collect();
|
||||
Bson::Document(doc! {
|
||||
"user": user.username,
|
||||
"db": user.database,
|
||||
"roles": roles,
|
||||
"mechanisms": ["SCRAM-SHA-256"],
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(doc! { "users": user_docs, "ok": 1.0 })
|
||||
}
|
||||
|
||||
async fn handle_grant_roles_to_user(
|
||||
cmd: &Document,
|
||||
db: &str,
|
||||
ctx: &CommandContext,
|
||||
) -> CommandResult<Document> {
|
||||
let username = cmd
|
||||
.get_str("grantRolesToUser")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing 'grantRolesToUser' field".into()))?;
|
||||
let roles = parse_roles(cmd, db, "roles")?;
|
||||
ctx.auth
|
||||
.grant_roles(db, username, roles)
|
||||
.map_err(auth_error_to_command_error)?;
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
|
||||
async fn handle_revoke_roles_from_user(
|
||||
cmd: &Document,
|
||||
db: &str,
|
||||
ctx: &CommandContext,
|
||||
) -> CommandResult<Document> {
|
||||
let username = cmd
|
||||
.get_str("revokeRolesFromUser")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing 'revokeRolesFromUser' field".into()))?;
|
||||
let roles = parse_roles(cmd, db, "roles")?;
|
||||
ctx.auth
|
||||
.revoke_roles(db, username, roles)
|
||||
.map_err(auth_error_to_command_error)?;
|
||||
Ok(doc! { "ok": 1.0 })
|
||||
}
|
||||
|
||||
fn parse_roles(cmd: &Document, db: &str, key: &str) -> CommandResult<Vec<String>> {
|
||||
let role_values = cmd
|
||||
.get_array(key)
|
||||
.map_err(|_| CommandError::InvalidArgument(format!("missing '{key}' array")))?;
|
||||
let mut roles = Vec::with_capacity(role_values.len());
|
||||
for role_value in role_values {
|
||||
match role_value {
|
||||
Bson::String(role) => roles.push(role.clone()),
|
||||
Bson::Document(role_doc) => {
|
||||
let role = role_doc
|
||||
.get_str("role")
|
||||
.map_err(|_| CommandError::InvalidArgument("role document missing 'role'".into()))?;
|
||||
let role_db = role_doc.get_str("db").unwrap_or(db);
|
||||
if role_db == db {
|
||||
roles.push(role.to_string());
|
||||
} else {
|
||||
roles.push(format!("{role_db}.{role}"));
|
||||
}
|
||||
}
|
||||
_ => return Err(CommandError::InvalidArgument("roles must be strings or documents".into())),
|
||||
}
|
||||
}
|
||||
Ok(roles)
|
||||
}
|
||||
|
||||
fn role_to_document(default_db: &str, role: &str) -> Document {
|
||||
if let Some((role_db, role_name)) = role.split_once('.') {
|
||||
doc! { "role": role_name, "db": role_db }
|
||||
} else {
|
||||
doc! { "role": role, "db": default_db }
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_error_to_command_error(error: rustdb_auth::AuthError) -> CommandError {
|
||||
match error {
|
||||
rustdb_auth::AuthError::UserAlreadyExists(message) => CommandError::DuplicateKey(message),
|
||||
rustdb_auth::AuthError::UserNotFound(message) => CommandError::NamespaceNotFound(message),
|
||||
rustdb_auth::AuthError::Persistence(message) => CommandError::InternalError(message),
|
||||
rustdb_auth::AuthError::AuthenticationFailed => CommandError::AuthenticationFailed,
|
||||
rustdb_auth::AuthError::InvalidPayload(message) => CommandError::InvalidArgument(message),
|
||||
rustdb_auth::AuthError::UnsupportedMechanism(message) => CommandError::InvalidArgument(message),
|
||||
rustdb_auth::AuthError::Disabled => CommandError::Unauthorized("authentication is disabled".into()),
|
||||
rustdb_auth::AuthError::UnknownConversation => {
|
||||
CommandError::InvalidArgument("unknown SASL conversation".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle `listDatabases` command.
|
||||
async fn handle_list_databases(
|
||||
cmd: &Document,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
use bson::{doc, Binary, Bson, Document};
|
||||
|
||||
use crate::context::{CommandContext, ConnectionState};
|
||||
use crate::error::{CommandError, CommandResult};
|
||||
|
||||
pub async fn handle_sasl_start(
|
||||
cmd: &Document,
|
||||
db: &str,
|
||||
ctx: &CommandContext,
|
||||
connection: &mut ConnectionState,
|
||||
) -> CommandResult<Document> {
|
||||
let mechanism = cmd
|
||||
.get_str("mechanism")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing SASL mechanism".into()))?;
|
||||
if mechanism != "SCRAM-SHA-256" {
|
||||
return Err(CommandError::InvalidArgument(format!(
|
||||
"unsupported SASL mechanism: {mechanism}"
|
||||
)));
|
||||
}
|
||||
|
||||
let payload = payload_bytes(cmd)?;
|
||||
let result = ctx
|
||||
.auth
|
||||
.start_scram_sha256(db, &payload)
|
||||
.map_err(map_auth_error)?;
|
||||
let conversation_id = connection.next_conversation_id();
|
||||
connection
|
||||
.sasl_conversations
|
||||
.insert(conversation_id, result.conversation);
|
||||
|
||||
Ok(doc! {
|
||||
"conversationId": conversation_id,
|
||||
"done": false,
|
||||
"payload": Binary { subtype: bson::spec::BinarySubtype::Generic, bytes: result.payload },
|
||||
"ok": 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_sasl_continue(
|
||||
cmd: &Document,
|
||||
ctx: &CommandContext,
|
||||
connection: &mut ConnectionState,
|
||||
) -> CommandResult<Document> {
|
||||
let conversation_id = cmd
|
||||
.get_i32("conversationId")
|
||||
.map_err(|_| CommandError::InvalidArgument("missing SASL conversationId".into()))?;
|
||||
let payload = payload_bytes(cmd)?;
|
||||
let conversation = connection
|
||||
.sasl_conversations
|
||||
.remove(&conversation_id)
|
||||
.ok_or_else(|| CommandError::InvalidArgument("unknown SASL conversation".into()))?;
|
||||
let result = ctx
|
||||
.auth
|
||||
.continue_scram_sha256(conversation, &payload)
|
||||
.map_err(map_auth_error)?;
|
||||
connection.authenticate(result.user);
|
||||
|
||||
Ok(doc! {
|
||||
"conversationId": conversation_id,
|
||||
"done": true,
|
||||
"payload": Binary { subtype: bson::spec::BinarySubtype::Generic, bytes: result.payload },
|
||||
"ok": 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn payload_bytes(cmd: &Document) -> CommandResult<Vec<u8>> {
|
||||
match cmd.get("payload") {
|
||||
Some(Bson::Binary(binary)) => Ok(binary.bytes.clone()),
|
||||
Some(Bson::String(value)) => Ok(value.as_bytes().to_vec()),
|
||||
_ => Err(CommandError::InvalidArgument("missing SASL payload".into())),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_auth_error(error: rustdb_auth::AuthError) -> CommandError {
|
||||
match error {
|
||||
rustdb_auth::AuthError::InvalidPayload(message) => CommandError::InvalidArgument(message),
|
||||
rustdb_auth::AuthError::UnsupportedMechanism(message) => CommandError::InvalidArgument(message),
|
||||
rustdb_auth::AuthError::Disabled => CommandError::Unauthorized("authentication is disabled".into()),
|
||||
rustdb_auth::AuthError::UnknownConversation => {
|
||||
CommandError::InvalidArgument("unknown SASL conversation".into())
|
||||
}
|
||||
rustdb_auth::AuthError::AuthenticationFailed => CommandError::AuthenticationFailed,
|
||||
rustdb_auth::AuthError::UserAlreadyExists(message) => CommandError::DuplicateKey(message),
|
||||
rustdb_auth::AuthError::UserNotFound(message) => CommandError::NamespaceNotFound(message),
|
||||
rustdb_auth::AuthError::Persistence(message) => CommandError::InternalError(message),
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use bson::{doc, Document};
|
||||
use bson::{doc, Bson, Document};
|
||||
|
||||
use crate::context::CommandContext;
|
||||
use crate::error::CommandResult;
|
||||
@@ -7,12 +7,13 @@ use crate::error::CommandResult;
|
||||
///
|
||||
/// Returns server capabilities matching wire protocol expectations.
|
||||
pub async fn handle(
|
||||
_cmd: &Document,
|
||||
cmd: &Document,
|
||||
_db: &str,
|
||||
_ctx: &CommandContext,
|
||||
ctx: &CommandContext,
|
||||
) -> CommandResult<Document> {
|
||||
Ok(doc! {
|
||||
let mut response = doc! {
|
||||
"ismaster": true,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"maxBsonObjectSize": 16_777_216_i32,
|
||||
"maxMessageSizeBytes": 48_000_000_i32,
|
||||
@@ -24,5 +25,19 @@ pub async fn handle(
|
||||
"maxWireVersion": 21_i32,
|
||||
"readOnly": false,
|
||||
"ok": 1.0,
|
||||
})
|
||||
};
|
||||
|
||||
if ctx.auth.enabled() {
|
||||
if let Ok(namespace_user) = cmd.get_str("saslSupportedMechs") {
|
||||
let mechanisms: Vec<Bson> = ctx
|
||||
.auth
|
||||
.supported_mechanisms(namespace_user)
|
||||
.into_iter()
|
||||
.map(Bson::String)
|
||||
.collect();
|
||||
response.insert("saslSupportedMechs", Bson::Array(mechanisms));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod admin_handler;
|
||||
pub mod aggregate_handler;
|
||||
pub mod auth_handler;
|
||||
pub mod delete_handler;
|
||||
pub mod find_handler;
|
||||
pub mod hello_handler;
|
||||
|
||||
Reference in New Issue
Block a user