Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion libsql-server/src/admin_shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,15 @@ fn run_admitted(
}) {
Ok(lease) => lease,
Err(e) => {
crate::namespace::fence::audit::denied(
&e,
crate::namespace::fence::audit::DenialSurface::AdminShell,
);
return Ok(rpc::Response {
resp: Some(Resp::Error(rpc::Error {
error: e.to_string(),
})),
})
});
}
};
let res = run_one(conn, q);
Expand Down
44 changes: 44 additions & 0 deletions libsql-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,50 @@ pub struct MetaStoreConfig {
/// How long `SetSourceReadFence` waits for running reads and streams before it cancels
/// them, when the request names no drain policy. `None` is the default of 30 seconds.
pub namespace_fence_default_read_drain: Option<Duration>,
/// The separate secret that authorises namespace fence adoption (`AdoptFence`), presented
/// in the `x-libsql-fence-adoption-key` header beside the admin credential. `None`
/// disables adoption.
pub namespace_fence_adoption_key: Option<FenceAdoptionKey>,
}

/// The secret that authorises namespace fence adoption (`docs/NAMESPACE_FENCE.md` section 12).
///
/// Only its SHA-256 digest is kept, and a presented key is compared digest to digest without
/// an early exit, so the comparison takes the same time whatever the presented key is. `Debug`
/// never prints it.
#[derive(Clone)]
pub struct FenceAdoptionKey(Arc<[u8; 32]>);

impl FenceAdoptionKey {
/// `None` for an empty key, which would authorise nothing.
pub fn new(key: &str) -> Option<Self> {
if key.is_empty() {
return None;
}
Some(Self(Arc::new(Self::digest(key.as_bytes()))))
}

/// Whether `presented` is the configured key.
pub fn matches(&self, presented: &[u8]) -> bool {
let presented = Self::digest(presented);
let difference = self
.0
.iter()
.zip(presented.iter())
.fold(0u8, |acc, (a, b)| acc | (a ^ b));
difference == 0
}

fn digest(bytes: &[u8]) -> [u8; 32] {
use sha2::Digest as _;
sha2::Sha256::digest(bytes).into()
}
}

impl std::fmt::Debug for FenceAdoptionKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("FenceAdoptionKey(<redacted>)")
}
}

#[derive(Debug, Clone)]
Expand Down
17 changes: 15 additions & 2 deletions libsql-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,13 @@ impl Error {
crate::namespace::fence::outcome::FenceError::from_proxy_stable_code(code, &e.message)
});
match fence {
Some(fence) => Error::NamespaceFence(fence),
Some(fence) => {
crate::namespace::fence::audit::denied(
&fence,
crate::namespace::fence::audit::DenialSurface::Proxy,
);
Error::NamespaceFence(fence)
}
None => Error::RpcQueryError(e),
}
}
Expand All @@ -184,7 +190,13 @@ impl Error {
/// anything else unchanged.
pub(crate) fn from_proxy_status(status: tonic::Status) -> Self {
match crate::namespace::fence::outcome::FenceError::from_grpc_status(&status) {
Some(fence) => Error::NamespaceFence(fence),
Some(fence) => {
crate::namespace::fence::audit::denied(
&fence,
crate::namespace::fence::audit::DenialSurface::Proxy,
);
Error::NamespaceFence(fence)
}
None => Error::RpcQueryExecutionError(status),
}
}
Expand All @@ -195,6 +207,7 @@ impl Error {
pub(crate) fn fence_error_response(
e: &crate::namespace::fence::outcome::FenceError,
) -> axum::response::Response {
crate::namespace::fence::audit::denied(e, crate::namespace::fence::audit::DenialSurface::Http);
let status = e.http_status();
tracing::debug!("HTTP API: {status}, {e}");
(status, axum::Json(e.http_error_body())).into_response()
Expand Down
8 changes: 7 additions & 1 deletion libsql-server/src/hrana/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,13 @@ pub fn batch_error_from_sqld_error(sqld_error: SqldError) -> Result<BatchError,
SqldError::BuilderError(QueryResultBuilderError::ResponseTooLarge(_)) => {
BatchError::ResponseTooLarge
}
SqldError::NamespaceFence(e) => BatchError::Fence(e),
SqldError::NamespaceFence(e) => {
crate::namespace::fence::audit::denied(
&e,
crate::namespace::fence::audit::DenialSurface::Hrana,
);
BatchError::Fence(e)
}
sqld_error => return Err(sqld_error),
})
}
Expand Down
8 changes: 7 additions & 1 deletion libsql-server/src/hrana/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,13 @@ pub fn stmt_error_from_sqld_error(sqld_error: SqldError) -> Result<StmtError, Sq
}
SqldError::Blocked(reason) => Ok(StmtError::Blocked { reason }),
SqldError::RpcQueryError(e) => Ok(StmtError::Proxy(e.message)),
SqldError::NamespaceFence(e) => Ok(StmtError::Fence(e)),
SqldError::NamespaceFence(e) => {
crate::namespace::fence::audit::denied(
&e,
crate::namespace::fence::audit::DenialSurface::Hrana,
);
Ok(StmtError::Fence(e))
}
SqldError::RusqliteError(rusqlite_error)
| SqldError::RusqliteErrorExtended(rusqlite_error, _) => match rusqlite_error {
rusqlite::Error::SqliteFailure(sqlite_error, Some(message)) => {
Expand Down
Loading
Loading