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
20 changes: 17 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ spacetimedb-lib = { workspace = true, features = ["serde"] }

anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_with.workspace = true
jsonwebtoken.workspace = true

Expand Down
20 changes: 19 additions & 1 deletion crates/auth/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,27 @@ use serde::{Deserialize, Serialize};
use spacetimedb_lib::Identity;
use std::time::SystemTime;

#[derive(Debug, Clone)]
pub struct ConnectionAuthCtx {
pub claims: SpacetimeIdentityClaims,
pub jwt_payload: String,
}

impl TryFrom<SpacetimeIdentityClaims> for ConnectionAuthCtx {
type Error = anyhow::Error;
fn try_from(claims: SpacetimeIdentityClaims) -> Result<Self, Self::Error> {
let payload =
serde_json::to_string(&claims).map_err(|e| anyhow::anyhow!("Failed to serialize claims: {}", e))?;
Ok(ConnectionAuthCtx {
claims,
jwt_payload: payload,
})
}
}

// These are the claims that can be attached to a request/connection.
#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SpacetimeIdentityClaims {
#[serde(rename = "hex_identity")]
pub identity: Identity,
Expand Down
35 changes: 35 additions & 0 deletions crates/bindings-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,13 @@ pub mod raw {
///
/// - `out_ptr` is NULL or `out` is not in bounds of WASM memory.
pub fn identity(out_ptr: *mut u8);

/// Check the size of the jwt associated with the given connection.
/// Returns 0 if there is no jwt for the connection.
pub fn jwt_len(connection_id_ptr: *const u8, out_ptr: *mut u32);

/// Write the jwt payload for the given connection id to the out_ptr.
pub fn get_jwt(connection_id_ptr: *const u8, target_ptr: *mut u8, target_ptr_len: *mut u32);
}

/// What strategy does the database index use?
Expand Down Expand Up @@ -1089,6 +1096,34 @@ pub fn identity() -> [u8; 32] {
buf
}

#[inline]
pub fn jwt_length(connection_id: [u8; 16]) -> Option<u32> {
let mut v: u32 = 0;
unsafe { raw::jwt_len(connection_id.as_ptr(), &mut v) }
if v == 0 {
None
} else {
Some(v)
}
}

#[inline]
pub fn get_jwt(connection_id: [u8; 16]) -> Option<String> {
let Some(jwt_len) = jwt_length(connection_id) else {
return None; // No JWT found.
};
let mut buf = vec![0u8; jwt_len as usize];

let mut v: u32 = buf.len() as u32;
unsafe {
raw::get_jwt(connection_id.as_ptr(), buf.as_mut_ptr(), &mut v);
}
if v == 0 {
return None; // No JWT found.
}
Some(std::str::from_utf8(&buf[..v as usize]).unwrap().to_string())
}

pub struct RowIter {
raw: raw::RowIter,
}
Expand Down
4 changes: 4 additions & 0 deletions crates/bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ spacetimedb-bindings-sys.workspace = true
spacetimedb-lib.workspace = true
spacetimedb-bindings-macro.workspace = true
spacetimedb-primitives.workspace = true
serde_json.workspace = true

bytemuck.workspace = true
derive_more.workspace = true
log.workspace = true
scoped-tls.workspace = true
auto_impl = "1.3.0"

rand08 = { workspace = true, optional = true }
# we depend on getrandom and enable the `custom` feature, so that
Expand All @@ -39,6 +41,8 @@ getrandom02 = { workspace = true, optional = true, features = ["custom"] }
[dev-dependencies]
insta.workspace = true
trybuild.workspace = true
serde_json.workspace = true
auto_impl = "1.3.0"

[lints]
workspace = true
150 changes: 149 additions & 1 deletion crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ mod rng;
pub mod rt;
#[doc(hidden)]
pub mod table;
use auto_impl::auto_impl;

use spacetimedb_lib::bsatn;
use std::cell::RefCell;
use std::cell::{OnceCell, RefCell};

pub use log;
#[cfg(feature = "rand")]
Expand Down Expand Up @@ -732,18 +733,40 @@ pub struct ReducerContext {
/// See the [`#[table]`](macro@crate::table) macro for more information.
pub db: Local,

/// The authentication information for the caller of this reducer.
/// This can be accessed via the sender_auth() method.
sender_auth: Box<dyn AuthCtx>,

#[cfg(feature = "rand08")]
rng: std::cell::OnceCell<StdbRng>,
}

impl ReducerContext {
#[doc(hidden)]
pub fn new(db: Local, sender: Identity, connection_id: Option<ConnectionId>, timestamp: Timestamp) -> Self {
let sender_auth: Box<dyn AuthCtx> = match connection_id {
Some(cid) => Box::new(ConnectionIdBasedAuthCtx::new(cid)),
None => Box::new(InternalAuthCtx),
};
Self {
db,
sender,
timestamp,
connection_id,
sender_auth,
#[cfg(feature = "rand08")]
rng: std::cell::OnceCell::new(),
}
}

#[doc(hidden)]
pub fn __dummy() -> Self {
Self {
db: Local {},
sender: Identity::__dummy(),
timestamp: Timestamp::UNIX_EPOCH,
connection_id: None,
sender_auth: Box::new(InternalAuthCtx),
rng: std::cell::OnceCell::new(),
}
}
Expand All @@ -760,6 +783,14 @@ impl ReducerContext {
// which reads the module identity out of the `InstanceEnv`.
Identity::from_byte_array(spacetimedb_bindings_sys::identity())
}

pub fn sender_auth(&self) -> &dyn AuthCtx {
self.sender_auth.as_ref()
}

pub fn sender_auth_impl_version(&self) -> impl AuthCtx + '_ {
&self.sender_auth
}
}

/// A handle on a database with a particular table schema.
Expand Down Expand Up @@ -871,6 +902,123 @@ impl std::ops::DerefMut for IterBuf {
}
}

#[non_exhaustive]
pub struct JwtClaims {
payload: String,
parsed: OnceCell<serde_json::Value>,
audience: OnceCell<Vec<String>>,
}

impl JwtClaims {
fn new(jwt: String) -> Self {
Self {
payload: jwt,
parsed: OnceCell::new(),
audience: OnceCell::new(),
}
}

fn get_parsed(&self) -> &serde_json::Value {
self.parsed
.get_or_init(|| serde_json::from_str(&self.payload).expect("Failed to parse JWT payload"))
}

pub fn subject(&self) -> &str {
// TODO: Add more error messages here.
self.get_parsed().get("sub").unwrap().as_str().unwrap()
}

pub fn issuer(&self) -> &str {
self.get_parsed().get("iss").unwrap().as_str().unwrap()
}

fn extract_audience(&self) -> Vec<String> {
let aud = self.get_parsed().get("aud").unwrap();
match aud {
serde_json::Value::String(s) => vec![s.clone()],
serde_json::Value::Array(arr) => arr.iter().filter_map(|v| v.as_str().map(String::from)).collect(),
_ => panic!("Unexpected type for 'aud' claim in JWT"),
}
}

pub fn audience(&self) -> &[String] {
self.audience.get_or_init(|| self.extract_audience())
}

// A convenience method, since this may not be in the token.
pub fn identity(&self) -> Identity {
Identity::from_claims(self.issuer(), self.subject())
}

// We can expose the whole payload for users that want to parse custom claims.
pub fn raw_payload(&self) -> &str {
&self.payload
}
}

/// AuthCtx represents the authentication information for a caller.
#[auto_impl(&, Box)]
pub trait AuthCtx {
// True if this reducer was spawned from inside the database.
fn is_internal(&self) -> bool;
// Check if there is a JWT without loading it.
// If is_internal is true, this will be false.
fn has_jwt(&self) -> bool;
// Load the jwt.
fn jwt(&self) -> Option<&JwtClaims>;
}

/// The auth information is fetched from system tables for the connection id.
struct ConnectionIdBasedAuthCtx {
connection_id: ConnectionId,
claims: std::cell::OnceCell<Option<JwtClaims>>,
}

impl ConnectionIdBasedAuthCtx {
fn new(connection_id: ConnectionId) -> Self {
Self {
connection_id,
claims: std::cell::OnceCell::new(),
}
}
}

struct InternalAuthCtx;
impl AuthCtx for InternalAuthCtx {
fn is_internal(&self) -> bool {
true
}

fn has_jwt(&self) -> bool {
false
}

fn jwt(&self) -> Option<&JwtClaims> {
None
}
}

impl AuthCtx for ConnectionIdBasedAuthCtx {
fn is_internal(&self) -> bool {
false
}

fn has_jwt(&self) -> bool {
if let Some(maybe_claims) = self.claims.get() {
return maybe_claims.is_some();
}
spacetimedb_bindings_sys::jwt_length(self.connection_id.as_le_byte_array()).is_some()
}

fn jwt(&self) -> Option<&JwtClaims> {
self.claims
.get_or_init(|| {
spacetimedb_bindings_sys::get_jwt(self.connection_id.as_le_byte_array()).map(JwtClaims::new)
})
.as_ref()
}
}

#[cfg(feature = "unstable")]
#[macro_export]
macro_rules! volatile_nonatomic_schedule_immediate {
Expand Down
4 changes: 4 additions & 0 deletions crates/bindings/src/rt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,8 @@ extern "C" fn __call_reducer__(

// Assemble the `ReducerContext`.
let timestamp = Timestamp::from_micros_since_unix_epoch(timestamp as i64);
let ctx = ReducerContext::new(crate::Local {}, sender, conn_id, timestamp);
/*
let ctx = ReducerContext {
db: crate::Local {},
sender,
Expand All @@ -492,6 +494,8 @@ extern "C" fn __call_reducer__(
rng: std::cell::OnceCell::new(),
};

*/

// Fetch reducer function.
let reducers = REDUCERS.get().unwrap();
// Dispatch to it with the arguments read.
Expand Down
1 change: 1 addition & 0 deletions crates/client-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ spacetimedb-lib = { workspace = true, features = ["serde"] }
spacetimedb-paths.workspace = true
spacetimedb-schema.workspace = true

base64.workspace = true
tokio = { version = "1.2", features = ["full"] }
lazy_static = "1.4.0"
log = "0.4.4"
Expand Down
Loading
Loading