diff --git a/Cargo.toml b/Cargo.toml index d654bdc..1051fc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ members = [ "crates/capsec-tokio", "crates/capsec-tests", ] +exclude = [ + "crates/capsec-example-db", +] [workspace.package] version = "0.1.0" diff --git a/crates/capsec-core/src/cap.rs b/crates/capsec-core/src/cap.rs index 56df44f..c57f5c2 100644 --- a/crates/capsec-core/src/cap.rs +++ b/crates/capsec-core/src/cap.rs @@ -45,6 +45,24 @@ impl Cap

{ } } + /// Creates a new capability token for use by `#[capsec::permission]` generated code. + /// + /// This constructor is public so that derive macros can create `Cap

` for + /// user-defined permission types from external crates. The `SealProof` bound + /// ensures only types with a valid seal token can use this. + /// + /// Do not call directly — use `#[capsec::permission]` instead. + #[doc(hidden)] + pub fn __capsec_new_derived() -> Self + where + P: Permission<__CapsecSeal = crate::__private::SealProof>, + { + Self { + _phantom: PhantomData, + _not_send: PhantomData, + } + } + /// Converts this capability into a [`SendCap`] that can cross thread boundaries. /// /// This is an explicit opt-in — you're acknowledging that this capability @@ -91,6 +109,19 @@ unsafe impl Send for SendCap

{} unsafe impl Sync for SendCap

{} impl SendCap

{ + /// Creates a new send-capable token for use by `#[capsec::permission]` generated code. + /// + /// Do not call directly — use `#[capsec::permission]` instead. + #[doc(hidden)] + pub fn __capsec_new_send_derived() -> Self + where + P: Permission<__CapsecSeal = crate::__private::SealProof>, + { + Self { + _phantom: PhantomData, + } + } + /// Returns a new `Cap

` from this send-capable token. /// /// This creates a fresh `Cap` (not a reference cast) — safe because diff --git a/crates/capsec-core/src/lib.rs b/crates/capsec-core/src/lib.rs index 57377e6..c909421 100644 --- a/crates/capsec-core/src/lib.rs +++ b/crates/capsec-core/src/lib.rs @@ -6,7 +6,7 @@ //! This crate provides the foundational types that the rest of the `capsec` //! ecosystem builds on: //! -//! - [`Permission`](permission::Permission) — sealed marker trait for capability categories +//! - [`Permission`](permission::Permission) — marker trait for capability categories //! - [`Cap

`](cap::Cap) — zero-sized proof token that the holder has permission `P` //! - [`Has

`](has::Has) — trait for checking and composing capabilities //! - [`CapRoot`](root::CapRoot) — the singleton root of all capability grants @@ -45,3 +45,8 @@ pub mod error; pub mod has; pub mod permission; pub mod root; + +/// Re-export of the seal token module for use by `#[capsec::permission]` macro. +/// Do not use directly. +#[doc(hidden)] +pub use permission::__private; diff --git a/crates/capsec-core/src/permission.rs b/crates/capsec-core/src/permission.rs index 6035b85..fa6d135 100644 --- a/crates/capsec-core/src/permission.rs +++ b/crates/capsec-core/src/permission.rs @@ -1,8 +1,9 @@ -//! The sealed [`Permission`] trait and all built-in permission types. +//! The [`Permission`] trait and all built-in permission types. //! //! Permissions are zero-sized marker types that encode what kind of I/O a -//! capability token grants. They form a sealed hierarchy — external crates -//! cannot define new permissions, ensuring the set is auditable. +//! capability token grants. Built-in permissions cover filesystem, network, +//! environment, and process operations. Library authors can define custom +//! permissions using `#[capsec::permission]`. //! //! # Built-in permissions //! @@ -19,6 +20,18 @@ //! | [`Spawn`] | Process | Execute subprocesses | //! | [`Ambient`] | Everything | Full ambient authority — the "god token" | //! +//! # Custom permissions +//! +//! Use `#[capsec::permission]` to define domain-specific permissions: +//! +//! ```rust,ignore +//! #[capsec::permission] +//! pub struct DbRead; +//! +//! #[capsec::permission(subsumes = [DbRead])] +//! pub struct DbAll; +//! ``` +//! //! # Tuples //! //! Two permissions can be bundled via a tuple: `(FsRead, NetConnect)` is itself @@ -32,12 +45,23 @@ //! meaning a `Cap` can be used anywhere a `Cap` is required. //! [`Ambient`] subsumes everything. -/// Marker trait for all capability permissions. Sealed to prevent external implementation. +/// Marker trait for all capability permissions. /// /// Every permission type is a zero-sized struct that implements this trait. -/// The sealed pattern ensures that only the permissions defined in this crate -/// can be used as capability tokens — external crates cannot forge new permissions. -pub trait Permission: sealed::Sealed + 'static {} +/// Built-in permissions are defined in this module. Custom permissions can be +/// defined using the `#[capsec::permission]` derive macro, which generates the +/// required seal token. +/// +/// # Direct implementation +/// +/// Do not implement this trait manually. Use `#[capsec::permission]` instead. +/// The `__CapsecSeal` associated type is `#[doc(hidden)]` and may change +/// without notice. +pub trait Permission: 'static { + /// Seal token preventing manual implementation. Do not use directly. + #[doc(hidden)] + type __CapsecSeal: __private::SealToken; +} // Filesystem @@ -84,20 +108,42 @@ pub struct Ambient; // Permission impls -impl Permission for FsRead {} -impl Permission for FsWrite {} -impl Permission for FsAll {} -impl Permission for NetConnect {} -impl Permission for NetBind {} -impl Permission for NetAll {} -impl Permission for EnvRead {} -impl Permission for EnvWrite {} -impl Permission for Spawn {} -impl Permission for Ambient {} +impl Permission for FsRead { + type __CapsecSeal = __private::SealProof; +} +impl Permission for FsWrite { + type __CapsecSeal = __private::SealProof; +} +impl Permission for FsAll { + type __CapsecSeal = __private::SealProof; +} +impl Permission for NetConnect { + type __CapsecSeal = __private::SealProof; +} +impl Permission for NetBind { + type __CapsecSeal = __private::SealProof; +} +impl Permission for NetAll { + type __CapsecSeal = __private::SealProof; +} +impl Permission for EnvRead { + type __CapsecSeal = __private::SealProof; +} +impl Permission for EnvWrite { + type __CapsecSeal = __private::SealProof; +} +impl Permission for Spawn { + type __CapsecSeal = __private::SealProof; +} +impl Permission for Ambient { + type __CapsecSeal = __private::SealProof; +} // Tuple permissions -impl Permission for (A, B) {} +impl Permission for (A, B) { + type __CapsecSeal = __private::SealProof; +} // Subsumption @@ -114,21 +160,19 @@ impl Subsumes for NetAll {} impl Subsumes for NetAll {} impl Subsumes

for Ambient {} -// Sealed - -mod sealed { - pub trait Sealed {} - impl Sealed for super::FsRead {} - impl Sealed for super::FsWrite {} - impl Sealed for super::FsAll {} - impl Sealed for super::NetConnect {} - impl Sealed for super::NetBind {} - impl Sealed for super::NetAll {} - impl Sealed for super::EnvRead {} - impl Sealed for super::EnvWrite {} - impl Sealed for super::Spawn {} - impl Sealed for super::Ambient {} - impl Sealed for (A, B) {} +// Seal token — prevents manual Permission implementation. +// The #[capsec::permission] macro generates the correct seal. +// Users could write this by hand (it's #[doc(hidden)], not private), +// but that's equivalent to writing `unsafe` — they own the consequences. + +#[doc(hidden)] +pub mod __private { + /// Proof token that a permission was registered via the capsec derive macro. + pub struct SealProof; + + /// Trait bound for the seal associated type. + pub trait SealToken {} + impl SealToken for SealProof {} } #[cfg(test)] diff --git a/crates/capsec-example-db/Cargo.toml b/crates/capsec-example-db/Cargo.toml new file mode 100644 index 0000000..c51a10c --- /dev/null +++ b/crates/capsec-example-db/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "capsec-example-db" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Example: user-defined database permissions with capsec" +publish = false + +[dependencies] +capsec = { path = "../capsec" } +capsec-core = { path = "../capsec-core" } +duckdb = { version = "1", features = ["bundled"] } + +[dev-dependencies] +capsec-core = { path = "../capsec-core" } diff --git a/crates/capsec-example-db/README.md b/crates/capsec-example-db/README.md new file mode 100644 index 0000000..691f86c --- /dev/null +++ b/crates/capsec-example-db/README.md @@ -0,0 +1,110 @@ +# capsec-example-db + +A real-world example of capsec's user-defined permissions, gating DuckDB operations at compile time. + +## What this demonstrates + +Library authors can define domain-specific permissions with `#[capsec::permission]` and enforce them through the type system — zero runtime cost, zero chance of forgetting a check. + +```rust +#[capsec::permission] +pub struct DbRead; + +#[capsec::permission] +pub struct DbWrite; + +#[capsec::permission(subsumes = [DbRead, DbWrite])] +pub struct DbAll; +``` + +Functions declare what they need: + +```rust +// This function literally cannot write to the database. +// The compiler rejects it — not a runtime check. +fn run_analytics(db: &CapDb, cap: &impl Has) { + db.query("SELECT * FROM players", cap)?; + + // COMPILE ERROR: Has is not implemented for impl Has + // db.execute("DELETE FROM players", cap); +} +``` + +## Building and running + +This crate is **excluded from the default workspace** because it depends on `duckdb` with the `bundled` feature, which compiles the entire DuckDB C++ engine from source (~500k lines of C++). This keeps `cargo build --workspace`, `cargo test --workspace`, and `cargo clippy --workspace` fast for day-to-day development. + +To build, test, or run this crate, target it explicitly with `-p`: + +```bash +# Run the example +cargo run -p capsec-example-db --example duck_db_app + +# Run the tests +cargo test -p capsec-example-db +``` + +The first build will be slow (DuckDB compilation). Subsequent builds use the cached artifacts and are fast. + +Output: + +``` +=== DuckDB + capsec: Compile-Time Capability Gating === +Schema created. + +--- Ingestion (write-only) --- + inserted: Alice (score: 950) + inserted: Bob (score: 870) + ... + +--- Analytics (read-only) --- +ID Name Score +----- --------------- ------ +5 Eve 1100 +3 Charlie 1020 +... + +--- Admin (full access) --- +Players below 900: 2 +Removed 2 player(s) below threshold +Remaining players: 3 +``` + +## Architecture + +| Module | Capability | Can read? | Can write? | +|--------|-----------|-----------|------------| +| `ingest_scores` | `DbWrite` | No | Yes | +| `run_analytics` | `DbRead` | Yes | No | +| `admin_cleanup` | `DbAll` | Yes | Yes | + +`DbAll` subsumes `DbRead` and `DbWrite` — extract a `Cap` and it satisfies both `Has` and `Has` bounds. + +## Key patterns shown + +- **`#[capsec::permission]`** — define custom permissions in your own crate +- **`subsumes = [...]`** — build permission hierarchies +- **`Cap` subsumption** — extract the concrete token to use both read and write APIs +- **`#[capsec::context]`** — bundle custom permissions into a context struct +- **`#[capsec::requires]`** — annotate functions with custom permission requirements +- **Mixed built-in + custom** — combine `FsRead` and `DbRead` in one context + +## Connection to auths + +These compile-time permissions mirror the runtime capabilities used by [auths](https://github.com/bordumb/auths). The same vocabulary spans both layers: + +| capsec (compile-time) | auths (runtime) | +|----------------------|-----------------| +| `Cap` | `HasCapability("db:read")` | +| Type system: compiles/doesn't | Policy evaluation: Allow/Deny | +| `Subsumes for DbAll` | Delegation narrowing | + +capsec prevents the code from exceeding its authority. auths prevents unauthorized callers from reaching the code. Together: defense in depth. + +## Tests + +```bash +cargo test -p capsec-example-db +``` + +> **Note:** This crate is in the workspace `exclude` list, not `members`. Use `-p capsec-example-db` explicitly — `--workspace` will not include it. diff --git a/crates/capsec-example-db/examples/duck_db_app.rs b/crates/capsec-example-db/examples/duck_db_app.rs new file mode 100644 index 0000000..700765a --- /dev/null +++ b/crates/capsec-example-db/examples/duck_db_app.rs @@ -0,0 +1,165 @@ +//! A small DuckDB application demonstrating compile-time capability gating. +//! +//! This example shows how capsec prevents unauthorized database operations +//! at compile time — not at runtime. If you uncomment the "COMPILE ERROR" +//! lines, the program won't build. +//! +//! Run with: +//! cargo run -p capsec-example-db --example duck_db_app + +use capsec::Has; +use capsec_example_db::{CapDb, DbAll, DbRead, DbWrite}; + +/// Analytics module: read-only access to the database. +/// +/// This function only accepts `DbRead` capability. It literally cannot +/// execute INSERT/UPDATE/DELETE — the compiler rejects it. +fn run_analytics(db: &CapDb, cap: &impl Has) { + println!("\n--- Analytics (read-only) ---"); + + let rows = db.query( + "SELECT CAST(id AS VARCHAR), name, CAST(score AS VARCHAR) FROM players ORDER BY score DESC", + cap, + ).unwrap(); + + println!("{:<5} {:<15} {:<6}", "ID", "Name", "Score"); + println!("{:-<5} {:-<15} {:-<6}", "", "", ""); + for row in &rows { + println!("{:<5} {:<15} {:<6}", row[0], row[1], row[2]); + } + + let top = db + .query_one("SELECT name FROM players ORDER BY score DESC LIMIT 1", cap) + .unwrap(); + println!("\nTop player: {top}"); + + let avg = db + .query_one( + "SELECT CAST(ROUND(AVG(score), 1) AS VARCHAR) FROM players", + cap, + ) + .unwrap(); + println!("Average score: {avg}"); + + // COMPILE ERROR: uncomment to see capsec in action + // db.execute("DELETE FROM players", cap); + // ^^^ the trait `Has` is not + // implemented for `impl Has` +} + +/// Ingestion module: write-only access to the database. +/// +/// This function only accepts `DbWrite` capability. It can insert data +/// but cannot query it back. +fn ingest_scores(db: &CapDb, cap: &impl Has) { + println!("\n--- Ingestion (write-only) ---"); + + let players = [ + ("Alice", 950), + ("Bob", 870), + ("Charlie", 1020), + ("Diana", 890), + ("Eve", 1100), + ]; + + for (name, score) in &players { + db.execute( + &format!("INSERT INTO players (name, score) VALUES ('{name}', {score})"), + cap, + ) + .unwrap(); + println!(" inserted: {name} (score: {score})"); + } + + // COMPILE ERROR: uncomment to see capsec in action + // db.query("SELECT * FROM players", cap); + // ^^^ the trait `Has` is not + // implemented for `impl Has` +} + +/// Admin module: full access (read + write via DbAll). +/// +/// `DbAll` subsumes both `DbRead` and `DbWrite`, so this function can +/// do everything — but it must be explicitly granted `DbAll`. +/// +/// Note the pattern: we extract `Cap` from the capability holder, +/// then use it for both reads and writes. `Cap` implements both +/// `Has` and `Has` via subsumption. +fn admin_cleanup(db: &CapDb, cap: &impl Has) { + println!("\n--- Admin (full access) ---"); + + // Extract a Cap — this concrete token satisfies Has + // and Has via the subsumption impls generated by + // #[capsec::permission(subsumes = [DbRead, DbWrite])]. + let all_cap: capsec::Cap = cap.cap_ref(); + + // Cap satisfies Has — can query + let count = db + .query_one( + "SELECT CAST(COUNT(*) AS VARCHAR) FROM players WHERE score < 900", + &all_cap, + ) + .unwrap(); + println!("Players below 900: {count}"); + + // Cap satisfies Has — can delete + let removed = db + .execute("DELETE FROM players WHERE score < 900", &all_cap) + .unwrap(); + println!("Removed {removed} player(s) below threshold"); + + // Verify + let remaining = db + .query_one("SELECT CAST(COUNT(*) AS VARCHAR) FROM players", &all_cap) + .unwrap(); + println!("Remaining players: {remaining}"); +} + +#[capsec::main] +fn main(root: capsec::CapRoot) { + println!("=== DuckDB + capsec: Compile-Time Capability Gating ==="); + + // Open an in-memory database + let db = CapDb::open_in_memory().unwrap(); + + // Grant DbAll for setup (migration needs read + write) + let db_all = root.grant::(); + db.migrate( + "CREATE SEQUENCE players_seq START 1; + CREATE TABLE players ( + id INTEGER DEFAULT nextval('players_seq'), + name VARCHAR NOT NULL, + score INTEGER NOT NULL + );", + &db_all, + ) + .unwrap(); + println!("Schema created."); + + // Grant narrow capabilities to each module. + // Each module can ONLY do what its capability allows. + let read_cap = root.grant::(); + let write_cap = root.grant::(); + + // Ingest: can write but not read + ingest_scores(&db, &write_cap); + + // Analytics: can read but not write + run_analytics(&db, &read_cap); + + // Admin: can do both (DbAll subsumes DbRead + DbWrite) + admin_cleanup(&db, &db_all); + + // Final leaderboard + println!("\n--- Final Leaderboard ---"); + let rows = db.query( + "SELECT CAST(id AS VARCHAR), name, CAST(score AS VARCHAR) FROM players ORDER BY score DESC", + &read_cap, + ).unwrap(); + for row in &rows { + println!(" #{}: {} ({})", row[0], row[1], row[2]); + } + + println!("\nAll operations completed with compile-time capability enforcement."); + println!("No runtime permission checks. Zero overhead. Just types."); +} diff --git a/crates/capsec-example-db/src/lib.rs b/crates/capsec-example-db/src/lib.rs new file mode 100644 index 0000000..ce8f31c --- /dev/null +++ b/crates/capsec-example-db/src/lib.rs @@ -0,0 +1,211 @@ +//! Capability-gated DuckDB wrapper. +//! +//! Demonstrates how library authors can define domain-specific permissions +//! using `#[capsec::permission]` and gate real database operations behind them. +//! +//! The core idea: if you only hold `Cap`, the compiler won't let you +//! call `db_execute` — you literally cannot write to the database without the +//! type system proving you have `DbWrite` permission. + +use capsec::{Cap, Has}; +use duckdb::Connection; + +/// Permission to execute database read queries (SELECT). +#[capsec::permission] +pub struct DbRead; + +/// Permission to execute database write statements (INSERT, UPDATE, DELETE, DDL). +#[capsec::permission] +pub struct DbWrite; + +/// Full database access. Subsumes both [`DbRead`] and [`DbWrite`]. +#[capsec::permission(subsumes = [DbRead, DbWrite])] +pub struct DbAll; + +/// A capability-gated DuckDB connection. +/// +/// Wraps a `duckdb::Connection` and gates all operations behind capsec +/// permission tokens. The connection itself has no capability — you must +/// pass a capability proof to every operation. +pub struct CapDb { + conn: Connection, +} + +impl CapDb { + /// Open an in-memory DuckDB database. + pub fn open_in_memory() -> Result { + Ok(Self { + conn: Connection::open_in_memory()?, + }) + } + + /// Execute a read query and collect results as string rows. + /// + /// All columns are returned as strings (caller should CAST in SQL). + /// Requires `DbRead` capability. Will not compile if you only hold `DbWrite`. + pub fn query>( + &self, + sql: &str, + cap: &C, + ) -> Result>, duckdb::Error> { + let _proof: Cap = cap.cap_ref(); + let mut stmt = self.conn.prepare(sql)?; + let mut result_rows = Vec::new(); + let mut rows = stmt.query([])?; + while let Some(row) = rows.next()? { + let mut vals = Vec::new(); + let mut i = 0; + loop { + match row.get::<_, String>(i) { + Ok(val) => vals.push(val), + Err(_) => break, + } + i += 1; + } + result_rows.push(vals); + } + Ok(result_rows) + } + + /// Execute a write statement (INSERT, UPDATE, DELETE, DDL). + /// + /// Requires `DbWrite` capability. Will not compile if you only hold `DbRead`. + pub fn execute>(&self, sql: &str, cap: &C) -> Result { + let _proof: Cap = cap.cap_ref(); + self.conn.execute(sql, []) + } + + /// Execute a batch of DDL/DML statements. + /// + /// Requires `DbWrite` capability. + pub fn execute_batch>(&self, sql: &str, cap: &C) -> Result<(), duckdb::Error> { + let _proof: Cap = cap.cap_ref(); + self.conn.execute_batch(sql) + } + + /// Run a migration: create schema then seed data. + /// + /// Requires `DbAll` — proves you can both read and write. + pub fn migrate>(&self, ddl: &str, cap: &C) -> Result<(), duckdb::Error> { + let _proof: Cap = cap.cap_ref(); + self.conn.execute_batch(ddl) + } + + /// Query a single scalar value. + /// + /// Requires `DbRead` capability. + pub fn query_one>(&self, sql: &str, cap: &C) -> Result { + let _proof: Cap = cap.cap_ref(); + self.conn.query_row(sql, [], |row| row.get(0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use capsec_core::root::test_root; + + #[test] + fn grant_custom_permission() { + let root = test_root(); + let db = CapDb::open_in_memory().unwrap(); + let write_cap = root.grant::(); + db.execute("CREATE TABLE t (x INTEGER)", &write_cap) + .unwrap(); + db.execute("INSERT INTO t VALUES (42)", &write_cap).unwrap(); + + let read_cap = root.grant::(); + let rows = db + .query("SELECT CAST(x AS VARCHAR) FROM t", &read_cap) + .unwrap(); + assert_eq!(rows, vec![vec!["42".to_string()]]); + } + + #[test] + fn db_all_subsumes_read_and_write() { + let root = test_root(); + let db = CapDb::open_in_memory().unwrap(); + let cap = root.grant::(); + + db.execute("CREATE TABLE t (x INTEGER)", &cap).unwrap(); + db.execute("INSERT INTO t VALUES (1)", &cap).unwrap(); + let rows = db.query("SELECT CAST(x AS VARCHAR) FROM t", &cap).unwrap(); + assert_eq!(rows.len(), 1); + } + + #[test] + fn migrate_requires_db_all() { + let root = test_root(); + let db = CapDb::open_in_memory().unwrap(); + let cap = root.grant::(); + db.migrate("CREATE TABLE users (id INTEGER, name VARCHAR)", &cap) + .unwrap(); + let count = db + .query_one("SELECT CAST(COUNT(*) AS VARCHAR) FROM users", &cap) + .unwrap(); + assert_eq!(count, "0"); + } + + #[test] + fn custom_permission_is_zst() { + assert_eq!(std::mem::size_of::>(), 0); + assert_eq!(std::mem::size_of::>(), 0); + assert_eq!(std::mem::size_of::>(), 0); + } + + #[test] + fn context_macro_with_custom_permissions() { + #[capsec::context] + struct DbCtx { + read: DbRead, + write: DbWrite, + } + + let root = test_root(); + let db = CapDb::open_in_memory().unwrap(); + let ctx = DbCtx::new(&root); + db.execute("CREATE TABLE t (x INTEGER)", &ctx).unwrap(); + db.execute("INSERT INTO t VALUES (1)", &ctx).unwrap(); + let rows = db.query("SELECT CAST(x AS VARCHAR) FROM t", &ctx).unwrap(); + assert_eq!(rows.len(), 1); + } + + #[test] + fn requires_macro_with_custom_permissions() { + #[capsec::context] + struct QueryCtx { + read: DbRead, + } + + #[capsec::requires(DbRead, on = ctx)] + fn checked_query(db: &CapDb, ctx: &QueryCtx) -> Vec> { + db.query("SELECT 'hello'", ctx).unwrap() + } + + let root = test_root(); + let db = CapDb::open_in_memory().unwrap(); + let ctx = QueryCtx::new(&root); + let results = checked_query(&db, &ctx); + assert_eq!(results, vec![vec!["hello".to_string()]]); + } + + #[test] + fn context_with_mixed_builtin_and_custom() { + use capsec::FsRead; + + #[capsec::context] + struct MixedCtx { + fs: FsRead, + db: DbRead, + } + + let root = test_root(); + let db = CapDb::open_in_memory().unwrap(); + let ctx = MixedCtx::new(&root); + let rows = db.query("SELECT 'mixed'", &ctx).unwrap(); + assert_eq!(rows, vec![vec!["mixed".to_string()]]); + + fn needs_fs(_: &impl Has) {} + needs_fs(&ctx); + } +} diff --git a/crates/capsec-macro/src/lib.rs b/crates/capsec-macro/src/lib.rs index 1b1e088..ab99a2b 100644 --- a/crates/capsec-macro/src/lib.rs +++ b/crates/capsec-macro/src/lib.rs @@ -33,6 +33,167 @@ const KNOWN_PERMISSIONS: &[&str] = &[ "Ambient", ]; +/// Defines a user-defined permission type for capability-based security. +/// +/// Generates the `Permission` trait impl (with seal token), `Has

` impls for +/// `Cap

` and `SendCap

`, and optionally `Subsumes` impls for permission hierarchies. +/// +/// # Usage +/// +/// ```rust,ignore +/// #[capsec::permission] +/// pub struct DbRead; +/// +/// #[capsec::permission] +/// pub struct DbWrite; +/// +/// #[capsec::permission(subsumes = [DbRead, DbWrite])] +/// pub struct DbAll; +/// ``` +/// +/// # What it generates +/// +/// For `#[capsec::permission] pub struct DbRead;`: +/// - `impl Permission for DbRead` with the seal token +/// - `impl Has for Cap` +/// - `impl Has for SendCap` +/// +/// For `#[capsec::permission(subsumes = [DbRead, DbWrite])] pub struct DbAll;`: +/// - All of the above, plus: +/// - `impl Subsumes for DbAll` +/// - `impl Has for Cap` and `SendCap` +/// - Same for `DbWrite` +#[proc_macro_attribute] +pub fn permission(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr2: proc_macro2::TokenStream = attr.into(); + let input = parse_macro_input!(item as ItemStruct); + + match permission_inner(attr2, &input) { + Ok(tokens) => tokens.into(), + Err(e) => e.into_compile_error().into(), + } +} + +fn permission_inner( + attr: proc_macro2::TokenStream, + input: &ItemStruct, +) -> syn::Result { + // Validate: must be a unit struct (no fields) + match &input.fields { + syn::Fields::Unit => {} + _ => { + return Err(syn::Error::new_spanned( + input, + "#[capsec::permission] requires a unit struct (no fields)", + )); + } + } + + // Reject generics + if !input.generics.params.is_empty() { + return Err(syn::Error::new_spanned( + &input.generics, + "#[capsec::permission] does not support generic structs", + )); + } + + let struct_name = &input.ident; + let struct_vis = &input.vis; + let struct_attrs = &input.attrs; + + // Parse subsumes = [...] from attribute + let subsumes = parse_subsumes(attr)?; + + // Generate Permission impl with seal token + let permission_impl = quote! { + impl capsec_core::permission::Permission for #struct_name { + type __CapsecSeal = capsec_core::__private::SealProof; + } + }; + + // Note: Has for Cap and SendCap are already covered by + // the blanket impls in capsec_core::has: + // impl Has

for Cap

+ // impl Has

for SendCap

+ // So we only need to generate Subsumes-related Has impls. + + // Generate Subsumes impls + Has for Cap/SendCap + let subsumes_impls: Vec<_> = subsumes + .iter() + .map(|sub| { + quote! { + impl capsec_core::permission::Subsumes<#sub> for #struct_name {} + + impl capsec_core::has::Has<#sub> for capsec_core::cap::Cap<#struct_name> { + fn cap_ref(&self) -> capsec_core::cap::Cap<#sub> { + capsec_core::cap::Cap::__capsec_new_derived() + } + } + + impl capsec_core::has::Has<#sub> for capsec_core::cap::SendCap<#struct_name> { + fn cap_ref(&self) -> capsec_core::cap::Cap<#sub> { + capsec_core::cap::Cap::__capsec_new_derived() + } + } + } + }) + .collect(); + + Ok(quote! { + #(#struct_attrs)* + #struct_vis struct #struct_name; + + #permission_impl + #(#subsumes_impls)* + }) +} + +/// Parses `subsumes = [A, B, C]` from macro attribute tokens. +fn parse_subsumes(attr: proc_macro2::TokenStream) -> syn::Result> { + if attr.is_empty() { + return Ok(Vec::new()); + } + + // Parse as Meta::NameValue: subsumes = [...] + let meta: Meta = syn::parse2(attr)?; + match &meta { + Meta::NameValue(nv) if nv.path.is_ident("subsumes") => { + // The value should be an array expression: [A, B, C] + if let syn::Expr::Array(arr) = &nv.value { + let mut paths = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for elem in &arr.elems { + if let syn::Expr::Path(ep) = elem { + let path_str = quote::quote!(#ep).to_string(); + if !seen.insert(path_str.clone()) { + return Err(syn::Error::new_spanned( + elem, + format!("duplicate subsumes entry: {}", path_str), + )); + } + paths.push(ep.path.clone()); + } else { + return Err(syn::Error::new_spanned( + elem, + "expected a permission type path in subsumes list", + )); + } + } + Ok(paths) + } else { + Err(syn::Error::new_spanned( + &nv.value, + "expected an array [A, B, C] for subsumes", + )) + } + } + _ => Err(syn::Error::new_spanned( + &meta, + "expected `subsumes = [...]`", + )), + } +} + /// Declares the capability requirements of a function. /// /// When all parameters use `impl Has

` bounds, the compiler already enforces @@ -465,7 +626,8 @@ fn context_inner( }; // Validate fields and collect permission info - let mut field_infos: Vec<(syn::Ident, syn::Ident)> = Vec::new(); // (field_name, perm_ident) + // Each entry: (field_name, resolved_perm_type_tokens) + let mut field_infos: Vec<(syn::Ident, proc_macro2::TokenStream)> = Vec::new(); let mut seen_perms: std::collections::HashSet = std::collections::HashSet::new(); for field in &fields.named { @@ -480,21 +642,23 @@ fn context_inner( )); } - // Extract type ident (last segment of path) - let perm_ident = match ty { + // Extract type path + let (perm_key, perm_tokens) = match ty { Type::Path(tp) => { if let Some(seg) = tp.path.segments.last() { - seg.ident.clone() + let ident_str = seg.ident.to_string(); + // Known built-in? Qualify with capsec_core::permission:: + if KNOWN_PERMISSIONS.contains(&ident_str.as_str()) { + let ident = &seg.ident; + (ident_str, quote! { capsec_core::permission::#ident }) + } else { + // Custom permission — pass through original type path + (ident_str, quote! { #tp }) + } } else { return Err(syn::Error::new_spanned( ty, - format!( - "field '{}' has type '{}', which is not a capsec permission type. \ - Expected one of: {}", - field_name, - quote!(#ty), - KNOWN_PERMISSIONS.join(", ") - ), + format!("field '{}' has an empty type path", field_name,), )); } } @@ -502,44 +666,26 @@ fn context_inner( return Err(syn::Error::new_spanned( ty, format!( - "field '{}' has type '{}', which is not a capsec permission type. \ - Expected one of: {}", + "field '{}' has type '{}', which is not a valid permission type", field_name, quote!(#ty), - KNOWN_PERMISSIONS.join(", ") ), )); } }; - let perm_str = perm_ident.to_string(); - - // Validate against known permissions - if !KNOWN_PERMISSIONS.contains(&perm_str.as_str()) { - return Err(syn::Error::new_spanned( - ty, - format!( - "field '{}' has type '{}', which is not a capsec permission type. \ - Expected one of: {}", - field_name, - perm_str, - KNOWN_PERMISSIONS.join(", ") - ), - )); - } - // Check for duplicates - if !seen_perms.insert(perm_str.clone()) { + if !seen_perms.insert(perm_key.clone()) { return Err(syn::Error::new_spanned( ty, format!( "duplicate permission type '{}' — each permission can only appear once in a context struct", - perm_str + perm_key ), )); } - field_infos.push((field_name, perm_ident)); + field_infos.push((field_name, perm_tokens)); } let struct_name = &input.ident; @@ -551,9 +697,9 @@ fn context_inner( .iter() .map(|(name, perm)| { if is_send { - quote! { #name: capsec_core::cap::SendCap } + quote! { #name: capsec_core::cap::SendCap<#perm> } } else { - quote! { #name: capsec_core::cap::Cap } + quote! { #name: capsec_core::cap::Cap<#perm> } } }) .collect(); @@ -563,9 +709,9 @@ fn context_inner( .iter() .map(|(name, perm)| { if is_send { - quote! { #name: root.grant::().make_send() } + quote! { #name: root.grant::<#perm>().make_send() } } else { - quote! { #name: root.grant::() } + quote! { #name: root.grant::<#perm>() } } }) .collect(); @@ -575,8 +721,8 @@ fn context_inner( .iter() .map(|(name, perm)| { quote! { - impl capsec_core::has::Has for #struct_name { - fn cap_ref(&self) -> capsec_core::cap::Cap { + impl capsec_core::has::Has<#perm> for #struct_name { + fn cap_ref(&self) -> capsec_core::cap::Cap<#perm> { self.#name.cap_ref() } } diff --git a/crates/capsec-macro/src/resolve.rs b/crates/capsec-macro/src/resolve.rs index ec69dca..07a688a 100644 --- a/crates/capsec-macro/src/resolve.rs +++ b/crates/capsec-macro/src/resolve.rs @@ -27,11 +27,11 @@ pub fn meta_to_permission_type(meta: &Meta) -> Result { "env::write" | "EnvWrite" => Ok(quote! { capsec_core::permission::EnvWrite }), "spawn" | "Spawn" => Ok(quote! { capsec_core::permission::Spawn }), "all" | "Ambient" => Ok(quote! { capsec_core::permission::Ambient }), - _ => Err(syn::Error::new_spanned( - meta.path(), - format!( - "Unknown capsec permission: `{path_str}`. Expected one of: fs::read, fs::write, fs::all, net::connect, net::bind, net::all, env::read, env::write, spawn, all" - ), - )), + _ => { + // Pass through as-is — custom permission types are checked by the + // compiler via Permission trait bounds at the use site. + let path = &meta.path(); + Ok(quote! { #path }) + } } } diff --git a/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.rs b/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.rs index e762713..09171b4 100644 --- a/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.rs +++ b/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.rs @@ -1,13 +1,10 @@ -/// Attempt to implement the private Sealed trait from an external crate. +/// Attempt to implement Permission directly without the derive macro. /// -/// The `Permission` trait requires `sealed::Sealed`, which lives in a private -/// module inside capsec-core. External crates cannot name it, so they cannot -/// implement Permission for custom types. -/// -/// This test proves that even if you try to reach into the sealed module -/// path, the compiler rejects it. +/// The `Permission` trait requires a `__CapsecSeal` associated type that +/// satisfies `__private::SealToken`. Without the `#[capsec::permission]` +/// macro, you must provide this type — but leaving it out means the impl +/// is incomplete and the compiler rejects it. -// Approach 1: try to implement Permission directly (requires Sealed) use capsec::prelude::*; struct EvilPerm; diff --git a/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.stderr b/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.stderr index 1226a01..20d4691 100644 --- a/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.stderr +++ b/crates/capsec-tests/tests/compile_fail/sealed_impl_from_outside.stderr @@ -1,37 +1,7 @@ -error[E0277]: the trait bound `EvilPerm: capsec_core::permission::sealed::Sealed` is not satisfied - --> tests/compile_fail/sealed_impl_from_outside.rs:14:21 +error[E0046]: not all trait items implemented, missing: `__CapsecSeal` + --> tests/compile_fail/sealed_impl_from_outside.rs:11:1 | -14 | impl Permission for EvilPerm {} - | ^^^^^^^^ unsatisfied trait bound +11 | impl Permission for EvilPerm {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `__CapsecSeal` in implementation | -help: the trait `capsec_core::permission::sealed::Sealed` is not implemented for `EvilPerm` - --> tests/compile_fail/sealed_impl_from_outside.rs:13:1 - | -13 | struct EvilPerm; - | ^^^^^^^^^^^^^^^ - = help: the following other types implement trait `capsec_core::permission::sealed::Sealed`: - (A, B) - Ambient - EnvRead - EnvWrite - FsAll - FsRead - FsWrite - NetAll - and $N others -note: required by a bound in `capsec::Permission` - --> $WORKSPACE/crates/capsec-core/src/permission.rs - | - | pub trait Permission: sealed::Sealed + 'static {} - | ^^^^^^^^^^^^^^ required by this bound in `Permission` - = note: `Permission` is a "sealed trait", because to implement it you also need to implement `capsec_core::permission::sealed::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it - = help: the following types implement the trait: - capsec::FsRead - capsec::FsWrite - capsec::FsAll - capsec::NetConnect - capsec::NetBind - capsec::NetAll - capsec::EnvRead - capsec::EnvWrite - and $N others + = help: implement the missing item: `type __CapsecSeal = /* Type */;` diff --git a/crates/capsec/src/lib.rs b/crates/capsec/src/lib.rs index dc84369..2aabd82 100644 --- a/crates/capsec/src/lib.rs +++ b/crates/capsec/src/lib.rs @@ -54,7 +54,7 @@ pub fn run(f: impl FnOnce(CapRoot) -> T) -> T { // Re-exports from capsec-macro -pub use capsec_macro::{context, deny, main, requires}; +pub use capsec_macro::{context, deny, main, permission, requires}; // Capability-gated std wrappers diff --git a/crates/capsec/tests/compile_fail/context_bad_field.stderr b/crates/capsec/tests/compile_fail/context_bad_field.stderr index a2a893e..73403ac 100644 --- a/crates/capsec/tests/compile_fail/context_bad_field.stderr +++ b/crates/capsec/tests/compile_fail/context_bad_field.stderr @@ -1,13 +1,91 @@ -error: field 'x' has type 'String', which is not a capsec permission type. Expected one of: FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn, Ambient +error[E0277]: the trait bound `String: Permission` is not satisfied + --> tests/compile_fail/context_bad_field.rs:4:1 + | +4 | #[capsec::context] + | ^^^^^^^^^^^^^^^^^^ the trait `Permission` is not implemented for `String` + | + = help: the following other types implement trait `Permission`: + (A, B) + Ambient + EnvRead + EnvWrite + FsAll + FsRead + FsWrite + NetAll + and $N others +note: required by a bound in `Cap` + --> $WORKSPACE/crates/capsec-core/src/cap.rs + | + | pub struct Cap { + | ^^^^^^^^^^ required by this bound in `Cap` + = note: this error originates in the attribute macro `capsec::context` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `String: Permission` is not satisfied + --> tests/compile_fail/context_bad_field.rs:4:1 + | +4 | #[capsec::context] + | ^^^^^^^^^^^^^^^^^^ the trait `Permission` is not implemented for `String` + | + = help: the following other types implement trait `Permission`: + (A, B) + Ambient + EnvRead + EnvWrite + FsAll + FsRead + FsWrite + NetAll + and $N others +note: required by a bound in `capsec::Has` + --> $WORKSPACE/crates/capsec-core/src/has.rs + | + | pub trait Has { + | ^^^^^^^^^^ required by this bound in `Has` + = note: this error originates in the attribute macro `capsec::context` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `String: Permission` is not satisfied --> tests/compile_fail/context_bad_field.rs:6:8 | +4 | #[capsec::context] + | ------------------ required by a bound introduced by this call +5 | struct Bad { 6 | x: String, - | ^^^^^^ + | ^^^^^^ the trait `Permission` is not implemented for `String` + | + = help: the following other types implement trait `Permission`: + (A, B) + Ambient + EnvRead + EnvWrite + FsAll + FsRead + FsWrite + NetAll + and $N others +note: required by a bound in `CapRoot::grant` + --> $WORKSPACE/crates/capsec-core/src/root.rs + | + | pub fn grant(&self) -> Cap

{ + | ^^^^^^^^^^ required by this bound in `CapRoot::grant` -warning: unused import: `capsec::prelude::*` - --> tests/compile_fail/context_bad_field.rs:2:5 +error[E0599]: the method `cap_ref` exists for struct `Cap`, but its trait bounds were not satisfied + --> tests/compile_fail/context_bad_field.rs:4:1 + | +4 | #[capsec::context] + | ^^^^^^^^^^^^^^^^^^ method cannot be called on `Cap` due to unsatisfied trait bounds + | + ::: $WORKSPACE/crates/capsec-core/src/cap.rs + | + | pub struct Cap { + | ----------------------------- doesn't satisfy `Cap: capsec::Has` + | + ::: $RUST/alloc/src/string.rs | -2 | use capsec::prelude::*; - | ^^^^^^^^^^^^^^^^^^ + | pub struct String { + | ----------------- doesn't satisfy `String: Permission` | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: the following trait bounds were not satisfied: + `String: Permission` + which is required by `Cap: capsec::Has` + = note: this error originates in the attribute macro `capsec::context` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.rs b/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.rs index 1cd16ab..b44c3c5 100644 --- a/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.rs +++ b/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.rs @@ -1,5 +1,5 @@ -/// The Permission trait is sealed — external crates cannot implement it. -/// This prevents forgery of new permission types outside capsec-core. +/// The Permission trait requires a seal token — external crates cannot +/// implement it without the `#[capsec::permission]` derive macro. use capsec::prelude::*; struct MyPerm; diff --git a/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.stderr b/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.stderr index db4b07e..0764b01 100644 --- a/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.stderr +++ b/crates/capsec/tests/compile_fail/sealed_permission_no_external_impl.stderr @@ -1,37 +1,7 @@ -error[E0277]: the trait bound `MyPerm: capsec_core::permission::sealed::Sealed` is not satisfied - --> tests/compile_fail/sealed_permission_no_external_impl.rs:6:21 +error[E0046]: not all trait items implemented, missing: `__CapsecSeal` + --> tests/compile_fail/sealed_permission_no_external_impl.rs:6:1 | 6 | impl Permission for MyPerm {} - | ^^^^^^ unsatisfied trait bound + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `__CapsecSeal` in implementation | -help: the trait `capsec_core::permission::sealed::Sealed` is not implemented for `MyPerm` - --> tests/compile_fail/sealed_permission_no_external_impl.rs:5:1 - | -5 | struct MyPerm; - | ^^^^^^^^^^^^^ - = help: the following other types implement trait `capsec_core::permission::sealed::Sealed`: - (A, B) - Ambient - EnvRead - EnvWrite - FsAll - FsRead - FsWrite - NetAll - and $N others -note: required by a bound in `capsec::Permission` - --> $WORKSPACE/crates/capsec-core/src/permission.rs - | - | pub trait Permission: sealed::Sealed + 'static {} - | ^^^^^^^^^^^^^^ required by this bound in `Permission` - = note: `Permission` is a "sealed trait", because to implement it you also need to implement `capsec_core::permission::sealed::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it - = help: the following types implement the trait: - capsec::FsRead - capsec::FsWrite - capsec::FsAll - capsec::NetConnect - capsec::NetBind - capsec::NetAll - capsec::EnvRead - capsec::EnvWrite - and $N others + = help: implement the missing item: `type __CapsecSeal = /* Type */;`