1
1
//! Shows basic setup of a custom api server.
2
//!
3
//! This example has a section in the User Guide: https://dev.bonsaidb.io/main/guide/about/access-models/custom-api-server.html
4

            
5
use std::time::Duration;
6

            
7
use bonsaidb::{
8
    client::{url::Url, ApiError, Client},
9
    core::{
10
        actionable::Permissions,
11
        api::{Api, Infallible},
12
        async_trait::async_trait,
13
        connection::{AsyncStorageConnection, Authentication, SensitiveString},
14
        keyvalue::AsyncKeyValue,
15
        permissions::{
16
            bonsai::{AuthenticationMethod, BonsaiAction, ServerAction},
17
            Action, Identifier, Statement,
18
        },
19
        schema::{ApiName, Qualified},
20
    },
21
    local::config::Builder,
22
    server::{
23
        api::{Handler, HandlerResult, HandlerSession},
24
        Backend, CustomServer, ServerConfiguration,
25
    },
26
};
27
use serde::{Deserialize, Serialize};
28

            
29
/// The `Backend` for the BonsaiDb server.
30
#[derive(Debug)]
31
pub struct ExampleBackend;
32

            
33
// ANCHOR: api-types
34
4
#[derive(Serialize, Deserialize, Debug)]
35
pub struct Ping;
36

            
37
impl Api for Ping {
38
    type Response = Pong;
39
    type Error = Infallible;
40

            
41
3
    fn name() -> ApiName {
42
3
        ApiName::private("ping")
43
3
    }
44
}
45

            
46
4
#[derive(Serialize, Deserialize, Debug, Clone)]
47
pub struct Pong;
48

            
49
24
#[derive(Serialize, Deserialize, Debug)]
50
pub struct IncrementCounter {
51
    amount: u64,
52
}
53

            
54
impl Api for IncrementCounter {
55
    type Response = Counter;
56
    type Error = Infallible;
57

            
58
9
    fn name() -> ApiName {
59
9
        ApiName::private("increment")
60
9
    }
61
}
62

            
63
4
#[derive(Serialize, Deserialize, Debug, Clone)]
64
pub struct Counter(pub u64);
65
// ANCHOR_END: api-types
66

            
67
// ANCHOR: server-traits
68
impl Backend for ExampleBackend {
69
    type Error = Infallible;
70
    type ClientData = ();
71
}
72

            
73
/// Dispatches Requests and returns Responses.
74
#[derive(Debug)]
75
pub struct ExampleHandler;
76

            
77
/// The Request::Ping variant has `#[actionable(protection = "none")]`, which
78
/// causes `PingHandler` to be generated with a single method and no implicit
79
/// permission handling.
80
#[async_trait]
81
impl Handler<ExampleBackend, Ping> for ExampleHandler {
82
2
    async fn handle(
83
2
        _session: HandlerSession<'_, ExampleBackend>,
84
2
        _request: Ping,
85
2
    ) -> HandlerResult<Ping> {
86
2
        Ok(Pong)
87
2
    }
88
}
89
// ANCHOR_END: server-traits
90

            
91
// ANCHOR: permission-handles
92
/// The permissible actions that can be granted for this example api.
93
17
#[derive(Debug, Action)]
94
#[action(actionable = bonsaidb::core::actionable)]
95
pub enum ExampleActions {
96
    Increment,
97
    DoSomethingCustom,
98
}
99

            
100
8
pub async fn increment_counter<S: AsyncStorageConnection<Database = C>, C: AsyncKeyValue>(
101
8
    storage: &S,
102
8
    as_client: &S,
103
8
    amount: u64,
104
8
) -> Result<u64, bonsaidb::core::Error> {
105
8
    as_client.check_permission(&[Identifier::from("increment")], &ExampleActions::Increment)?;
106
4
    let database = storage.database::<()>("counter").await?;
107
4
    database.increment_key_by("counter", amount).await
108
8
}
109

            
110
#[async_trait]
111
impl Handler<ExampleBackend, IncrementCounter> for ExampleHandler {
112
8
    async fn handle(
113
8
        session: HandlerSession<'_, ExampleBackend>,
114
8
        request: IncrementCounter,
115
8
    ) -> HandlerResult<IncrementCounter> {
116
        Ok(Counter(
117
8
            increment_counter(session.server, &session.as_client, request.amount).await?,
118
        ))
119
16
    }
120
}
121
// ANCHOR_END: permission-handles
122

            
123
#[tokio::main]
124
1
async fn main() -> anyhow::Result<()> {
125
1
    env_logger::init();
126
    // ANCHOR: server-init
127
1
    let server = CustomServer::<ExampleBackend>::open(
128
1
        ServerConfiguration::new("custom-api.bonsaidb")
129
1
            .default_permissions(Permissions::from(
130
1
                Statement::for_any()
131
1
                    .allowing(&BonsaiAction::Server(ServerAction::Connect))
132
1
                    .allowing(&BonsaiAction::Server(ServerAction::Authenticate(
133
1
                        AuthenticationMethod::PasswordHash,
134
1
                    ))),
135
1
            ))
136
1
            .authenticated_permissions(Permissions::from(vec![
137
1
                Statement::for_any().allowing(&ExampleActions::Increment)
138
1
            ]))
139
1
            .with_api::<ExampleHandler, Ping>()?
140
1
            .with_api::<ExampleHandler, IncrementCounter>()?
141
1
            .with_schema::<()>()?,
142
3
    )
143
3
    .await?;
144
    // ANCHOR_END: server-init
145

            
146
2
    server.create_database::<()>("counter", true).await?;
147

            
148
    // Create a user to allow testing authenticated permissions
149
1
    match server.create_user("test-user").await {
150
1
        Ok(_) | Err(bonsaidb::core::Error::UniqueKeyViolation { .. }) => {}
151
        Err(other) => anyhow::bail!(other),
152
    }
153

            
154
1
    server
155
1
        .set_user_password("test-user", SensitiveString("hunter2".to_string()))
156
1
        .await?;
157

            
158
2
    if server.certificate_chain().await.is_err() {
159
10
        server.install_self_signed_certificate(true).await?;
160
    }
161
1
    let certificate = server
162
2
        .certificate_chain()
163
2
        .await?
164
1
        .into_end_entity_certificate();
165
1

            
166
1
    // If websockets are enabled, we'll also listen for websocket traffic.
167
1
    #[cfg(feature = "websockets")]
168
1
    {
169
1
        let server = server.clone();
170
1
        tokio::spawn(async move {
171
1
            server
172
3
                .listen_for_websockets_on("localhost:8080", false)
173
3
                .await
174
1
        });
175
1
    }
176
1

            
177
1
    // Spawn our QUIC-based protocol listener.
178
1
    let task_server = server.clone();
179
7
    tokio::spawn(async move { task_server.listen_on(5645).await });
180
1

            
181
1
    // Give a moment for the listeners to start.
182
1
    tokio::time::sleep(Duration::from_millis(10)).await;
183

            
184
    // To allow this example to run both websockets and QUIC, we're going to gather the clients
185
    // into a collection and use join_all to wait until they finish.
186
1
    let mut tasks = Vec::new();
187
1
    #[cfg(feature = "websockets")]
188
1
    {
189
1
        // To connect over websockets, use the websocket scheme.
190
1
        tasks.push(invoke_apis(
191
1
            Client::build(Url::parse("ws://localhost:8080")?).finish()?,
192
1
            "websockets",
193
1
        ));
194
1
    }
195
1

            
196
1
    // To connect over QUIC, use the bonsaidb scheme.
197
1
    tasks.push(invoke_apis(
198
1
        Client::build(Url::parse("bonsaidb://localhost")?)
199
1
            .with_certificate(certificate)
200
1
            .finish()?,
201
1
        "bonsaidb",
202
1
    ));
203
1

            
204
1
    // Wait for the clients to finish
205
12
    futures::future::join_all(tasks)
206
12
        .await
207
1
        .into_iter()
208
1
        .collect::<Result<_, _>>()?;
209

            
210
    // Shut the server down gracefully (or forcefully after 5 seconds).
211
1
    server.shutdown(Some(Duration::from_secs(5))).await?;
212

            
213
1
    Ok(())
214
1
}
215
2
async fn invoke_apis(client: Client, client_name: &str) -> Result<(), bonsaidb::core::Error> {
216
5
    ping_the_server(&client, client_name).await?;
217

            
218
    // Calling DoSomethingSimple and DoSomethingCustom will check permissions, which our client currently doesn't have access to.
219
    assert!(matches!(
220
2
        client
221
2
            .send_api_request_async(&IncrementCounter { amount: 1 })
222
2
            .await,
223
        Err(ApiError::Client(bonsaidb::client::Error::Core(
224
            bonsaidb::core::Error::PermissionDenied(_)
225
        )))
226
    ));
227
    assert!(matches!(
228
2
        client
229
2
            .send_api_request_async(&IncrementCounter { amount: 1 })
230
2
            .await,
231
        Err(ApiError::Client(bonsaidb::client::Error::Core(
232
            bonsaidb::core::Error::PermissionDenied(_)
233
        )))
234
    ));
235

            
236
    // Now, let's authenticate and try calling the APIs that previously were denied permissions
237
2
    let authenticated_client = client
238
2
        .authenticate(
239
2
            "test-user",
240
2
            Authentication::Password(SensitiveString(String::from("hunter2"))),
241
8
        )
242
8
        .await
243
2
        .unwrap();
244
2
    assert!(matches!(
245
2
        authenticated_client
246
2
            .send_api_request_async(&IncrementCounter { amount: 1 })
247
2
            .await,
248
        Ok(Counter(_))
249
    ));
250
    assert!(matches!(
251
2
        authenticated_client
252
2
            .send_api_request_async(&IncrementCounter { amount: 1 })
253
2
            .await,
254
        Ok(Counter(_))
255
    ));
256

            
257
2
    Ok(())
258
2
}
259

            
260
// ANCHOR: api-call
261
2
async fn ping_the_server(client: &Client, client_name: &str) -> Result<(), bonsaidb::core::Error> {
262
5
    match client.send_api_request_async(&Ping).await {
263
2
        Ok(Pong) => {
264
2
            println!("Received Pong from server on {}", client_name);
265
2
        }
266
        other => println!(
267
            "Unexpected response from API call on {}: {:?}",
268
            client_name, other
269
        ),
270
    }
271

            
272
2
    Ok(())
273
2
}
274
// ANCHOR_END: api-call
275

            
276
1
#[test]
277
1
fn runs() {
278
1
    main().unwrap()
279
1
}