Skip to content

Commit 49e57bc

Browse files
authored
Merge pull request #48 from auths-dev/dev-typeCleanup
feat: migrate public API from Has<P> to CapProvider<P> for transparent scope
2 parents 70a5655 + 0b6d215 commit 49e57bc

40 files changed

Lines changed: 3158 additions & 247 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ capsec provides four macros that work together:
164164

165165
| Macro | Purpose |
166166
|-------|---------|
167-
| `#[capsec::context]` | Generates `Has<P>` impls on a struct, turning it into a capability context |
167+
| `#[capsec::context]` | Generates `Has<P>` and `CapProvider<P>` impls on a struct, turning it into a capability context |
168168
| `#[capsec::main]` | Injects `CapRoot` creation into a function entry point |
169169
| `#[capsec::requires]` | Validates that a function's parameters satisfy declared permissions |
170170
| `#[capsec::deny]` | Marks a function as capability-free; violations are promoted to critical by the audit tool |

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ exclude = [
1515
]
1616

1717
[workspace.package]
18-
version = "0.1.9"
18+
version = "0.2.0"
1919
edition = "2024"
2020
rust-version = "1.85.0"
2121
readme = "README.md"
@@ -51,7 +51,7 @@ trybuild = "1"
5151
insta = { version = "1", features = ["yaml"] }
5252

5353
# Internal crates
54-
capsec-core = { path = "crates/capsec-core", version = "0.1.0" }
55-
capsec-macro = { path = "crates/capsec-macro", version = "0.1.0" }
56-
capsec-std = { path = "crates/capsec-std", version = "0.1.0" }
57-
capsec-tokio = { path = "crates/capsec-tokio", version = "0.1.0" }
54+
capsec-core = { path = "crates/capsec-core", version = "0.2.0" }
55+
capsec-macro = { path = "crates/capsec-macro", version = "0.2.0" }
56+
capsec-std = { path = "crates/capsec-std", version = "0.2.0" }
57+
capsec-tokio = { path = "crates/capsec-tokio", version = "0.2.0" }

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ cargo capsec audit
2222
capsec fills that gap with three layers:
2323

2424
1. **`cargo capsec audit`** — a static audit tool that scans your code and reports every I/O call. Drop it into CI and know exactly what your dependencies do.
25-
2. **Compile-time type system** — functions declare their I/O permissions via `Has<P>` trait bounds, and the compiler rejects anything that exceeds them. Zero runtime cost.
25+
2. **Compile-time type system** — functions declare their I/O permissions via `CapProvider<P>` trait bounds, and the compiler rejects anything that exceeds them. Zero runtime cost. Scoped capabilities (`Attenuated<P, S>`) enforce *where* a capability can act.
2626
3. **Runtime capability control**`RuntimeCap` (revocable) and `TimedCap` (expiring) wrap static capabilities with runtime validity checks for dynamic scenarios like server init or migration windows.
2727

2828
The audit tool finds the problems. The type system prevents them at compile time. Runtime caps handle the cases where permissions need to change dynamically.
@@ -120,15 +120,15 @@ Functions declare their I/O requirements in the type signature. The compiler enf
120120
use capsec::prelude::*;
121121
122122
// Define a context with exactly the permissions your app needs.
123-
// The macro generates Cap fields, constructor, and Has<P> impls.
123+
// The macro generates Cap fields, constructor, Has<P> impls, and CapProvider<P> impls.
124124
#[capsec::context]
125125
struct AppCtx {
126126
fs: FsRead,
127127
net: NetConnect,
128128
}
129129
130-
// Leaf functions take &impl Has<P> — works with raw caps AND context structs.
131-
pub fn load_config(path: &str, cap: &impl Has<FsRead>) -> Result<String, CapSecError> {
130+
// Leaf functions take &impl CapProvider<P> — works with raw caps, context structs, AND scoped caps.
131+
pub fn load_config(path: &str, cap: &impl CapProvider<FsRead>) -> Result<String, CapSecError> {
132132
capsec::fs::read_to_string(path, cap)
133133
}
134134
@@ -157,11 +157,11 @@ let _ = capsec::fs::read_to_string("/etc/passwd", &net_cap);
157157
```
158158

159159
```
160-
error[E0277]: the trait bound `Cap<NetConnect>: Has<FsRead>` is not satisfied
160+
error[E0277]: the trait bound `Cap<NetConnect>: CapProvider<FsRead>` is not satisfied
161161
--> src/main.rs:4:55
162162
|
163163
4 | let _ = capsec::fs::read_to_string("/etc/passwd", &net_cap);
164-
| -------------------------- ^^^^^^^^ the trait `Has<FsRead>` is not implemented for `Cap<NetConnect>`
164+
| -------------------------- ^^^^^^^^ the trait `CapProvider<FsRead>` is not implemented for `Cap<NetConnect>`
165165
| |
166166
| required by a bound introduced by this call
167167
```
@@ -189,15 +189,15 @@ help: provide the argument
189189

190190
```rust
191191
let fs_all = root.grant::<FsAll>();
192-
needs_net(&fs_all); // fn needs_net(_: &impl Has<NetConnect>) {}
192+
needs_net(&fs_all); // fn needs_net(_: &impl CapProvider<NetConnect>) {}
193193
```
194194

195195
```
196-
error[E0277]: the trait bound `Cap<FsAll>: Has<NetConnect>` is not satisfied
196+
error[E0277]: the trait bound `Cap<FsAll>: CapProvider<NetConnect>` is not satisfied
197197
--> src/main.rs:3:15
198198
|
199199
3 | needs_net(&fs_all);
200-
| --------- ^^^^^^^ the trait `Has<NetConnect>` is not implemented for `Cap<FsAll>`
200+
| --------- ^^^^^^^ the trait `CapProvider<NetConnect>` is not implemented for `Cap<FsAll>`
201201
| |
202202
| required by a bound introduced by this call
203203
```
@@ -210,7 +210,7 @@ These are real `rustc` errors — no custom error framework, no runtime panics.
210210
|--|--------|-------|
211211
| Can any function read files? | Yes | Only if it has `Cap<FsRead>` |
212212
| Can any function open sockets? | Yes | Only if it has `Cap<NetConnect>` |
213-
| Can you audit who has what access? | Grep and pray | Grep for `Has<FsRead>` |
213+
| Can you audit who has what access? | Grep and pray | Grep for `CapProvider<FsRead>` |
214214
| Runtime cost? | N/A | Zero — all types are erased at compile time |
215215

216216
### Security model
@@ -347,7 +347,7 @@ fn main(root: CapRoot) -> Result<(), Box<dyn std::error::Error>> {
347347

348348
### Key properties
349349

350-
- `RuntimeCap`, `TimedCap`, `LoggedCap`, and `DualKeyCap` do **not** implement `Has<P>` — fallibility is explicit via `try_cap()` at every call site
350+
- `RuntimeCap`, `TimedCap`, `LoggedCap`, and `DualKeyCap` do **not** implement `Has<P>` but they do implement `CapProvider<P>` — so they can be passed directly to capsec-std/tokio functions. Fallibility is handled transparently by `provide_cap()`
351351
- All are `!Send` by default — use `make_send()` to opt into cross-thread transfer
352352
- Cloning a `RuntimeCap` shares the revocation flag — revoking one revokes all clones
353353
- Cloning a `LoggedCap` shares the audit log — entries from any clone appear in the same log
@@ -379,7 +379,7 @@ capsec's design draws from three foundational papers in capability-based securit
379379

380380
- **Saltzer & Schroeder (1975)**[The Protection of Information in Computer Systems](https://www.cs.virginia.edu/~evans/cs551/saltzer/). Defined the eight design principles for protection mechanisms. capsec implements six: economy of mechanism (zero-sized types), fail-safe defaults (no cap = no access), least privilege (the core mission), open design (open source + adversarial test suite), separation of privilege (`DualKeyCap`), and compromise recording (`LoggedCap`). The two partially met — complete mediation and least common mechanism — are inherent limitations of a library-level approach.
381381

382-
- **Melicher et al. (2017)**[A Capability-Based Module System for Authority Control](https://www.cs.cmu.edu/~aldrich/papers/ecoop17modules.pdf) (ECOOP 2017). Formalized non-transitive authority in the Wyvern language, proving that a module's authority can be determined by inspecting only its interface. capsec achieves the same property: `Has<P>` bounds make a function's authority visible in its signature, and `Attenuated<P, S>` / runtime cap types that don't implement `Has<P>` enforce non-transitivity.
382+
- **Melicher et al. (2017)**[A Capability-Based Module System for Authority Control](https://www.cs.cmu.edu/~aldrich/papers/ecoop17modules.pdf) (ECOOP 2017). Formalized non-transitive authority in the Wyvern language, proving that a module's authority can be determined by inspecting only its interface. capsec achieves the same property: `CapProvider<P>` bounds make a function's authority visible in its signature, and `Attenuated<P, S>` enforces non-transitivity through scope checks embedded in `provide_cap()`.
383383

384384
---
385385

crates/capsec-core/src/attenuate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ impl<P: Permission> Cap<P> {
5858

5959
impl<P: Permission, S: Scope> Attenuated<P, S> {
6060
/// Checks whether `target` is within this capability's scope.
61+
#[must_use = "ignoring a scope check silently discards scope violations"]
6162
pub fn check(&self, target: &str) -> Result<(), CapSecError> {
6263
self.scope.check(target)
6364
}

crates/capsec-core/src/cap.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::marker::PhantomData;
3030
/// // cap is a proof token — zero bytes at runtime
3131
/// assert_eq!(std::mem::size_of_val(&cap), 0);
3232
/// ```
33+
#[must_use = "capability tokens are proof of permission — discarding one wastes a grant"]
3334
pub struct Cap<P: Permission> {
3435
_phantom: PhantomData<P>,
3536
// PhantomData<*const ()> makes Cap !Send + !Sync
@@ -68,6 +69,7 @@ impl<P: Permission> Cap<P> {
6869
///
6970
/// This is an explicit opt-in — you're acknowledging that this capability
7071
/// will be used in a multi-threaded context (e.g., passed into `tokio::spawn`).
72+
#[must_use = "make_send consumes the original Cap and returns a SendCap"]
7173
pub fn make_send(self) -> SendCap<P> {
7274
SendCap {
7375
_phantom: PhantomData,
@@ -100,6 +102,7 @@ impl<P: Permission> Clone for Cap<P> {
100102
/// // use cap in this thread
101103
/// }).join().unwrap();
102104
/// ```
105+
#[must_use = "capability tokens are proof of permission — discarding one wastes a grant"]
103106
pub struct SendCap<P: Permission> {
104107
_phantom: PhantomData<P>,
105108
}

0 commit comments

Comments
 (0)