treetop-client 0.0.2

Typed async Rust client for Treetop policy authorization servers
Documentation
# treetop-client

A Rust client library for [Treetop](https://github.com/terjekv/treetop-rest) policy authorization servers.

Treetop is a Cedar-based policy evaluation service. This client provides a typed, async API for evaluating authorization requests, managing policies, and querying server status.

## Compatibility

`treetop-client` is pre-1.0; API compatibility is not guaranteed between `0.0.x` releases.

This version targets [treetop-rest v0.0.10](https://github.com/terjekv/treetop-rest/releases/tag/v0.0.10).
CI verifies the stable health, version, policy, and authorization contract against v0.0.4 through
v0.0.10; v0.0.10 receives the complete endpoint suite. Newer response fields use
`#[serde(default)]` for backward compatibility.

## Features

- **Type-driven design** -- strongly typed request/response types with serde, wire-compatible with the Treetop REST API
- **Capability-safe uploads** -- upload methods exist only on `Client<CanUpload>` values built with a validated token
- **Connection pooling** -- built on reqwest with configurable pool sizes and idle timeouts
- **Secure token handling** -- upload tokens backed by `SecretString` (zeroized on drop, redacted in Debug output)
- **Validated construction** -- fallible constructors preserve Cedar, entity, attribute, context, and header invariants
- **TLS by default** -- uses rustls without an OpenSSL/system-TLS dependency, with optional custom root certificates
- **Bounded I/O** -- request and successful-response bodies are capped at 16 MiB by default, with configurable limits
- **Fluent endpoint calls** -- typed detail and output transitions for authorization and user-policy queries
- **Batch authorization** -- evaluate multiple authorization requests in a single API call
- **Correlation IDs** -- clone-with-override pattern for request tracing without shared mutable state
- **Schema management** -- download and upload Cedar schema data alongside policies
- **Operational endpoints** -- typed liveness/readiness checks and access to the generated OpenAPI document

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
treetop-client = "0.0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

## Quick start

```rust
use treetop_client::{
    Action, AuthorizeRequest, Client, Request, Resource, User,
};

#[tokio::main]
async fn main() -> treetop_client::Result<()> {
    // Create a client
    let client = Client::builder("https://treetop.example.com").build()?;

    // Check server health
    client.health().await?;

    // Simple authorization check
    let allowed = client
        .is_allowed(Request::new(
            User::new("alice")?,
            Action::new("view")?,
            Resource::new("Document", "doc-42")?,
        ))
        .await?;

    println!("Allowed: {allowed}");
    Ok(())
}
```

## Usage

### Client configuration

```rust
use std::time::Duration;
use treetop_client::{Client, RequestLimits, UploadToken};

let client = Client::builder("https://treetop.example.com")
    .connect_timeout(Duration::from_secs(5))
    .request_timeout(Duration::from_secs(30))
    .pool_idle_timeout(Duration::from_secs(90))
    .pool_max_idle_per_host(10)
    .max_request_bytes(16 * 1024 * 1024)
    .max_response_bytes(16 * 1024 * 1024)
    .request_limits(RequestLimits::default())
    .upload_token(UploadToken::new("my-secret-token")?)
    .build()?;
```

Without `.upload_token(...)`, `build()` returns `Client<ReadOnly>` and upload methods are not
available. Adding the token transitions the builder and resulting client to `CanUpload`; read and
authorization methods remain available in both states.

Upload tokens require HTTPS unless the destination is loopback. For an explicitly accepted
plaintext development server, opt in with `.danger_allow_insecure_uploads(true)`. The default
HTTP client also rejects redirects so a token cannot be forwarded to a redirect target. A custom
reqwest client bypasses that redirect policy, so configure its policy deliberately.

For custom TLS configuration:

```rust
use treetop_client::Client;

let client = Client::builder("https://treetop.internal")
    .add_root_certificate(my_ca_cert)
    .build()?;
```

Or bring your own pre-configured reqwest client:

```rust
let client = Client::builder("https://treetop.example.com")
    .with_reqwest_client(my_reqwest_client)
    .build()?;
```

### Correlation IDs

Correlation IDs are managed via a clone-with-override pattern. The cloned client shares the same connection pool:

```rust
let traced = client.with_correlation_id("req-abc-123")?;
traced.authorize(&request).await?;  // sends x-correlation-id header

// Original client is unaffected
client.authorize(&request).await?;  // no correlation header

// Fluent endpoint calls can override correlation without cloning the client
client
    .authorization(&request)
    .correlation_id("req-def-456")?
    .send()
    .await?;
```

### Authorization

Request fields are private and exposed through read-only accessors. Constructors and fluent
setters validate before returning a value, while batch- and server-dependent invariants are checked
before transport. Duplicate request IDs are rejected, and response IDs, indices, ordering, counts,
policy versions, and decisions are checked against the submitted batch. `AttrValue::ip()` always
validates its IP/CIDR value.

#### Single check

```rust
use treetop_client::{Action, Request, Resource, User};

let allowed = client
    .is_allowed(Request::new(
        User::new("alice")?.with_group_names(&["admins"])?,
        Action::new("delete")?,
        Resource::new("Host", "web-01")?,
    ))
    .await?;
```

#### Batch authorization

```rust
use treetop_client::{Action, AttrValue, AuthorizeRequest, Request, Resource, User};

let batch = AuthorizeRequest::new()
    .add_request(Request::new(
        User::new("alice")?,
        Action::new("view")?,
        Resource::new("Document", "doc-1")?,
    ))
    .add_request_with_id("check-2", Request::new(
        User::new("bob")?,
        Action::new("edit")?,
        Resource::new("Document", "doc-1")?
            .with_attr("owner", AttrValue::String("alice".to_string()))?,
    ))?;

let response = client.authorization(&batch).send().await?;

println!("Successful: {}, Failed: {}", response.successes(), response.failures());

// Look up a result by client-provided ID
if let Some(result) = response.find_by_id("check-2") {
    println!("check-2 index: {}", result.index);
}
```

#### Detailed authorization (includes matching policies)

```rust
let response = client.authorization(&batch).detailed().send().await?;
```

### Resources with attributes

```rust
use treetop_client::{AttrValue, Resource};

let resource = Resource::new("Host", "web-01.example.com")?
    .with_attr("ip", AttrValue::ip("10.0.0.1")?)?
    .with_attr("environment", AttrValue::String("production".to_string()))?
    .with_attr("critical", AttrValue::Bool(true))?
    .with_attr("priority", AttrValue::Long(1))?;
```

### Namespaced types

Users, groups, and actions support Cedar namespaces:

```rust
use treetop_client::{Action, Group, User};

let user = User::new("alice")?
    .with_namespace(vec!["MyApp".to_string()])?
    .with_group_names(&["admins", "editors"])?;

let action = Action::new("delete")?
    .with_namespace(vec!["Admin".to_string()])?;

let group = Group::new("superusers")?
    .with_namespace(vec!["MyApp".to_string()])?;
```

### Policy management

```rust
// Download policies as structured data
let download = client.get_policies().await?;

// Download policies as raw Cedar DSL
let cedar_text = client.get_policies_raw().await?;

// Upload policies (requires upload token)
let metadata = client
    .upload_policies_raw("permit(principal, action, resource);")
    .await?;

// List policies for a specific user
let user_policies = client
    .user_policies("alice")?
    .group("admins")?
    .namespace("MyApp")?
    .send()
    .await?;
```

### Schema management

```rust
// Download schema as structured metadata
let schema = client.get_schema().await?;

// Download schema as raw Cedar schema JSON
let raw_schema = client.get_schema_raw().await?;

// Upload schema (requires upload token)
let metadata = client
    .upload_schema_raw(r#"{"": {"entityTypes": {}, "actions": {}}}"#)
    .await?;
```

### Server status

```rust
let version = client.version().await?;
println!("Server: {}, Cedar: {}", version.version, version.core.cedar);

let status = client.status().await?;
println!("Policies loaded: {}", status.policy_configuration.policies.entries);
println!("Context supported: {}", status.request_context.supported);
if let Some(source) = &status.policy_configuration.policies.source {
    println!("Policy source: {}", source.as_str());
}
```

The canonical operational probes and generated OpenAPI document are available directly:

```rust
client.livez().await?;
if client.readyz().await? {
    println!("Server is ready");
}

let openapi = client.openapi().await?;
println!("OpenAPI version: {}", openapi["openapi"]);
```

### Request context

Request-scoped context is serialized on the wire via `AuthRequest.context` and evaluated by
`treetop-rest v0.0.10`. The client automatically enforces `RequestLimits::default()` before
transport; configure limits reported by a differently configured server with
`ClientBuilder::request_limits()`.

```rust
use std::collections::HashMap;
use treetop_client::{Action, AttrValue, AuthRequest, AuthorizeRequest, Request, Resource, User};

let mut context = HashMap::new();
context.insert("env".to_string(), AttrValue::String("prod".to_string()));

let request = AuthRequest::new(Request::new(
    User::new("alice")?,
    Action::new("view")?,
    Resource::new("Photo", "VacationPhoto94.jpg")?,
))
    .with_context(context)?;
let batch = AuthorizeRequest::from_auth_requests([request])?;

let response = client.authorization(&batch).send().await?;
```

Inspect `status.request_context` if you need to know whether the server runtime is currently schema-backed or running in permissive fallback mode. Uploading a schema via `upload_schema_raw()` or `upload_schema_json()` lets you verify the schema-backed path explicitly.

### Prometheus metrics

```rust
let metrics_text = client.metrics().await?;
```

## Error handling

All methods return `treetop_client::Result<T>`, which uses `TreetopError`:

```rust
use treetop_client::TreetopError;

match client.health().await {
    Ok(()) => println!("Server is healthy"),
    Err(TreetopError::Transport(e)) => println!("Network error: {e}"),
    Err(TreetopError::Api { status, message }) => {
        println!("Server returned HTTP {status}: {message}");
    }
    Err(e) => println!("Other error: {e}"),
}
```

## License

MIT

## Releasing

Stable tags drive the crates.io and GitHub release workflow. See
[RELEASING.md](RELEASING.md) for the release process.