openfga_client/lib.rs
1#![warn(
2 missing_debug_implementations,
3 rust_2018_idioms,
4 unreachable_pub,
5 clippy::pedantic
6)]
7#![forbid(unsafe_code)]
8
9//! # OpenFGA Rust Client
10//!
11//! [](https://crates.io/crates/openfga-client)
12//! [](https://opensource.org/licenses/Apache-2.0)
13//! [](https://github.com/vakamo-labs/openfga-client/actions/workflows/ci.yaml)
14//!
15//! OpenFGA Rust Client is a type-safe client for OpenFGA with optional Authorization Model management and Authentication (Bearer or Client Credentials).
16//!
17//! ## Features
18//!
19//! * Type-safe client for OpenFGA (gRPC) build on `tonic`
20//! * (JSON) Serialization and deserialization for Authorization Models in addition to protobuf Messages
21//! * Uses `vendored-protoc` for well-known types - Rust files are pre-generated.
22//! * Optional Authorization Model management with Migration hooks. Ideal for stateless deployments. State is managed exclusively in OpenFGA. This enables fully automated model management by your Application without re-writing of Authorization Models on startup.
23//! * Optional Authentication (Bearer or Client Credentials) via the [Middle Crate](https://crates.io/crates/middle). (Feature: `auth-middle`)
24//! * Convenience functions like `read_all_tuples` (handles pagination), `get_store_by_name` and more.
25//!
26//! # Usage
27//!
28//! ## Basic Usage
29//! ```no_run
30//! use openfga_client::client::OpenFgaServiceClient;
31//! use tonic::transport::Channel;
32//!
33//! #[tokio::main]
34//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
35//! let endpoint = "http://localhost:8081";
36//! let service_client = OpenFgaServiceClient::connect(endpoint).await?;
37//!
38//! // Use the client to interact with OpenFGA
39//! Ok(())
40//! }
41//! ```
42//!
43//! ## Bearer Token Authentication (API-Key)
44//! ```no_run
45//! use openfga_client::{client::BasicOpenFgaServiceClient, url};
46//!
47//! fn main() -> Result<(), Box<dyn std::error::Error>> {
48//! let endpoint = url::Url::parse("http://localhost:8081")?;
49//! let token = "your-bearer-token";
50//! let service_client = BasicOpenFgaServiceClient::new_with_basic_auth(endpoint, token)?;
51//!
52//! // Use the client to interact with OpenFGA
53//! Ok(())
54//! }
55//! ```
56//!
57//! ## Client Credential Authentication
58//! ```no_run
59//! use openfga_client::client::BasicOpenFgaServiceClient;
60//! use url::Url;
61//!
62//! #[tokio::main]
63//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
64//! let endpoint = Url::parse("http://localhost:8081")?;
65//! let client_id = "your-client-id";
66//! let client_secret = "your-client-secret";
67//! let token_endpoint = Url::parse("http://localhost:8081/token")?;
68//! let scopes = vec!["scope1", "scope2"];
69//! let service_client = BasicOpenFgaServiceClient::new_with_client_credentials(endpoint, client_id, client_secret, token_endpoint, &scopes).await?;
70//!
71//! // Use the client to interact with OpenFGA
72//! Ok(())
73//! }
74//! ```
75//!
76//! ## Authorization Model Management and Migration
77//!
78//! For more details please check the [`TupleModelManager`](`migration::TupleModelManager`).
79//!
80//! Requires the following as part of the Authorization model:
81//! ```text
82//! type auth_model_id
83//! type model_version
84//! relations
85//! define openfga_id: [auth_model_id]
86//! define exists: [auth_model_id:*]
87//! ```
88//!
89//! Usage:
90//! ```no_run
91//! use openfga_client::client::{OpenFgaServiceClient, TupleKeyWithoutCondition};
92//! use openfga_client::migration::{AuthorizationModelVersion, MigrationFn, TupleModelManager};
93//! use openfga_client::tonic::codegen::StdError;
94//!
95//! /// Application specific state passed into migration functions.
96//! ///
97//! /// It must be clone so that in can be passed into *both* pre and post migration hooks.
98//! #[derive(Clone)]
99//! struct MyMigrationState {}
100//!
101//! /// An example MigrationFn.
102//! #[allow(clippy::unused_async)]
103//! async fn v1_1_migration(
104//! _client: OpenFgaServiceClient<tonic::transport::Channel>,
105//! _prev_auth_model_id: Option<String>,
106//! _active_auth_model_id: Option<String>,
107//! _state: MyMigrationState,
108//! ) -> std::result::Result<(), StdError> {
109//! // `client` and `state` can be used to read and write tuples from the store
110//! Ok(())
111//! }
112//!
113//! #[tokio::main]
114//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
115//! let endpoint = "http://localhost:8081";
116//! let mut service_client = OpenFgaServiceClient::connect(endpoint).await?;
117//!
118//! let store_name = "my-store";
119//! let model_prefix = "my-model";
120//!
121//! let mut manager = TupleModelManager::new(service_client.clone(), store_name, model_prefix)
122//! // Migrations are executed in order for models that have not been previously migrated.
123//! // First model - version 1.0
124//! .add_model(
125//! serde_json::from_str(include_str!("../tests/model-manager/v1.0/schema.json"))?,
126//! AuthorizationModelVersion::new(1, 0),
127//! // For major version upgrades, this is where tuple migrations go.
128//! None::<MigrationFn<_, _>>,
129//! None::<MigrationFn<_, _>>,
130//! )
131//! // Second model - version 1.1
132//! .add_model(
133//! serde_json::from_str(include_str!("../tests/model-manager/v1.1/schema.json"))?,
134//! AuthorizationModelVersion::new(1, 1),
135//! // For major version upgrades, this is where tuple migrations go.
136//! Some(v1_1_migration),
137//! None::<MigrationFn<_, _>>,
138//! );
139//!
140//! // Perform the migration if necessary
141//! manager.migrate(MyMigrationState {}).await?;
142//!
143//! let store_id = service_client
144//! .get_store_by_name(store_name)
145//! .await?
146//! .expect("Store found")
147//! .id;
148//! let authorization_model_id = manager
149//! .get_authorization_model_id(AuthorizationModelVersion::new(1, 1))
150//! .await?
151//! .expect("Authorization model found");
152//! let client = service_client.into_client(&store_id, &authorization_model_id);
153//!
154//! // Use the client.
155//! // `store_id` and `authorization_model_id` are stored in the client and attached to all requests.
156//! let page_size = 100;
157//! let continuation_token = None;
158//! let _tuples = client
159//! .read(
160//! page_size,
161//! TupleKeyWithoutCondition {
162//! user: "user:peter".to_string(),
163//! relation: "owner".to_string(),
164//! object: "organization:my-org".to_string(),
165//! },
166//! continuation_token,
167//! )
168//! .await?;
169//!
170//! Ok(())
171//! }
172//! ```
173//!
174//! ## License
175//! This project is licensed under the Apache-2.0 License. See the LICENSE file for details.
176//!
177//! ## Contributing
178//! Contributions are welcome! Please open an issue or submit a pull request on GitHub.
179
180pub use prost_types;
181pub use prost_wkt_types;
182pub use tonic;
183pub mod display;
184pub mod error;
185pub mod migration;
186pub use url;
187
188mod client_ext;
189mod conversions;
190mod model_client;
191
192mod generated {
193 #![allow(clippy::all)]
194 #![allow(clippy::pedantic)]
195
196 include!("gen/openfga/v1/openfga.v1.rs");
197}
198
199pub mod client {
200 //! Contains clients to connect to OpenFGA:
201 //!
202 //! * [`OpenFgaServiceClient`] is the generated client that allows full control over all parameters.
203 //! * [`OpenFgaClient`] is a wrapper around the generated client, that provides a more convenient interface and adds `store_id`, `authorization_model_id` and `consistency` to all requests.
204 //!
205 pub use open_fga_service_client::OpenFgaServiceClient;
206
207 #[cfg(feature = "auth-middle")]
208 pub use super::client_ext::{BasicAuthLayer, BasicOpenFgaServiceClient};
209 #[cfg(feature = "auth-middle")]
210 pub use super::model_client::BasicOpenFgaClient;
211 pub use super::{
212 generated::*,
213 model_client::{ConflictBehavior, OpenFgaClient, WriteOptions},
214 };
215}
216
217#[cfg(test)]
218mod test_json_serde {
219 use super::client::*;
220
221 fn test_authorization_model_serde(schema: &str) {
222 let schema_json: serde_json::Value = schema.parse().unwrap();
223 let schema: AuthorizationModel = serde_json::from_value(schema_json.clone()).unwrap();
224 let value = serde_json::to_value(&schema).unwrap();
225 assert_eq!(schema_json, value);
226 }
227 #[test]
228 fn test_serde_custom_roles() {
229 test_authorization_model_serde(include_str!(
230 "../tests/sample-store/custom-roles/schema.json"
231 ));
232 }
233
234 #[test]
235 fn test_serde_entitlements() {
236 test_authorization_model_serde(include_str!(
237 "../tests/sample-store/entitlements/schema.json"
238 ));
239 }
240
241 #[test]
242 fn test_serde_expenses() {
243 test_authorization_model_serde(include_str!("../tests/sample-store/expenses/schema.json"));
244 }
245
246 // gdrive, github, iot, issue-tracker, modular, slack
247 #[test]
248 fn test_serde_gdrive() {
249 test_authorization_model_serde(include_str!("../tests/sample-store/gdrive/schema.json"));
250 }
251
252 #[test]
253 fn test_serde_github() {
254 test_authorization_model_serde(include_str!("../tests/sample-store/github/schema.json"));
255 }
256
257 #[test]
258 fn test_serde_iot() {
259 test_authorization_model_serde(include_str!("../tests/sample-store/iot/schema.json"));
260 }
261
262 #[test]
263 fn test_serde_modular() {
264 test_authorization_model_serde(include_str!("../tests/sample-store/modular/schema.json"));
265 }
266
267 #[test]
268 fn test_serde_slack() {
269 test_authorization_model_serde(include_str!("../tests/sample-store/slack/schema.json"));
270 }
271}