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
#![allow(clippy::vec_init_then_push)] // Only true if websockets is disabled
5

            
6
use std::time::Duration;
7

            
8
use bonsaidb::client::url::Url;
9
use bonsaidb::client::{ApiError, AsyncClient};
10
use bonsaidb::core::actionable::Permissions;
11
use bonsaidb::core::api::Api;
12
use bonsaidb::core::async_trait::async_trait;
13
use bonsaidb::core::connection::{
14
    AsyncStorageConnection, Authentication, AuthenticationMethod, SensitiveString,
15
};
16
use bonsaidb::core::keyvalue::AsyncKeyValue;
17
use bonsaidb::core::permissions::bonsai::{BonsaiAction, ServerAction};
18
use bonsaidb::core::permissions::{Action, Identifier, Statement};
19
use bonsaidb::local::config::Builder;
20
use bonsaidb::server::api::{Handler, HandlerResult, HandlerSession};
21
use bonsaidb::server::{Server, ServerConfiguration};
22
use serde::{Deserialize, Serialize};
23

            
24
// ANCHOR: api-types
25
4
#[derive(Serialize, Deserialize, Debug, Api)]
26
#[api(name = "ping", response = Pong)]
27
pub struct Ping;
28

            
29
4
#[derive(Serialize, Deserialize, Debug, Clone)]
30
pub struct Pong;
31

            
32
24
#[derive(Serialize, Deserialize, Debug, Api)]
33
#[api(name = "increment", response = Counter)]
34
pub struct IncrementCounter {
35
    amount: u64,
36
}
37

            
38
4
#[derive(Serialize, Deserialize, Debug, Clone)]
39
pub struct Counter(pub u64);
40
// ANCHOR_END: api-types
41

            
42
// ANCHOR: server-traits
43
/// Dispatches Requests and returns Responses.
44
#[derive(Debug)]
45
pub struct ExampleHandler;
46

            
47
/// The Request::Ping variant has `#[actionable(protection = "none")]`, which
48
/// causes `PingHandler` to be generated with a single method and no implicit
49
/// permission handling.
50
#[async_trait]
51
impl Handler<Ping> for ExampleHandler {
52
2
    async fn handle(_session: HandlerSession<'_>, _request: Ping) -> HandlerResult<Ping> {
53
2
        Ok(Pong)
54
2
    }
55
}
56
// ANCHOR_END: server-traits
57

            
58
// ANCHOR: permission-handles
59
/// The permissible actions that can be granted for this example api.
60
17
#[derive(Debug, Action)]
61
#[action(actionable = bonsaidb::core::actionable)]
62
pub enum ExampleActions {
63
    Increment,
64
    DoSomethingCustom,
65
}
66

            
67
8
pub async fn increment_counter<S: AsyncStorageConnection<Database = C>, C: AsyncKeyValue>(
68
8
    storage: &S,
69
8
    as_client: &S,
70
8
    amount: u64,
71
8
) -> Result<u64, bonsaidb::core::Error> {
72
8
    as_client.check_permission([Identifier::from("increment")], &ExampleActions::Increment)?;
73
4
    let database = storage.database::<()>("counter").await?;
74
4
    database.increment_key_by("counter", amount).await
75
8
}
76

            
77
#[async_trait]
78
impl Handler<IncrementCounter> for ExampleHandler {
79
8
    async fn handle(
80
8
        session: HandlerSession<'_>,
81
8
        request: IncrementCounter,
82
8
    ) -> HandlerResult<IncrementCounter> {
83
        Ok(Counter(
84
8
            increment_counter(session.server, &session.as_client, request.amount).await?,
85
        ))
86
16
    }
87
}
88
// ANCHOR_END: permission-handles
89

            
90
#[tokio::main]
91
1
async fn main() -> anyhow::Result<()> {
92
1
    env_logger::init();
93
    // ANCHOR: server-init
94
1
    let server = Server::open(
95
1
        ServerConfiguration::new("custom-api.bonsaidb")
96
1
            .default_permissions(Permissions::from(
97
1
                Statement::for_any()
98
1
                    .allowing(&BonsaiAction::Server(ServerAction::Connect))
99
1
                    .allowing(&BonsaiAction::Server(ServerAction::Authenticate(
100
1
                        AuthenticationMethod::PasswordHash,
101
1
                    ))),
102
1
            ))
103
1
            .authenticated_permissions(Permissions::from(vec![
104
1
                Statement::for_any().allowing(&ExampleActions::Increment)
105
1
            ]))
106
1
            .with_api::<ExampleHandler, Ping>()?
107
1
            .with_api::<ExampleHandler, IncrementCounter>()?
108
1
            .with_schema::<()>()?,
109
    )
110
3
    .await?;
111
    // ANCHOR_END: server-init
112

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

            
115
    // Create a user to allow testing authenticated permissions
116
1
    match server.create_user("test-user").await {
117
1
        Ok(_) | Err(bonsaidb::core::Error::UniqueKeyViolation { .. }) => {}
118
        Err(other) => anyhow::bail!(other),
119
    }
120

            
121
1
    server
122
1
        .set_user_password("test-user", SensitiveString::from("hunter2"))
123
1
        .await?;
124

            
125
2
    if server.certificate_chain().await.is_err() {
126
10
        server.install_self_signed_certificate(true).await?;
127
    }
128
1
    let certificate = server
129
1
        .certificate_chain()
130
2
        .await?
131
1
        .into_end_entity_certificate();
132
1

            
133
1
    // If websockets are enabled, we'll also listen for websocket traffic.
134
1
    #[cfg(feature = "websockets")]
135
1
    {
136
1
        let server = server.clone();
137
1
        tokio::spawn(async move {
138
1
            server
139
1
                .listen_for_websockets_on("localhost:8080", false)
140
3
                .await
141
1
        });
142
1
    }
143
1

            
144
1
    // Spawn our QUIC-based protocol listener.
145
1
    let task_server = server.clone();
146
7
    tokio::spawn(async move { task_server.listen_on(5645).await });
147
1

            
148
1
    // Give a moment for the listeners to start.
149
1
    tokio::time::sleep(Duration::from_millis(10)).await;
150

            
151
    // To allow this example to run both websockets and QUIC, we're going to gather the clients
152
    // into a collection and use join_all to wait until they finish.
153
1
    let mut tasks = Vec::new();
154
1
    #[cfg(feature = "websockets")]
155
1
    {
156
1
        // To connect over websockets, use the websocket scheme.
157
1
        tasks.push(invoke_apis(
158
1
            AsyncClient::build(Url::parse("ws://localhost:8080")?).build()?,
159
1
            "websockets",
160
1
        ));
161
1
    }
162
1

            
163
1
    // To connect over QUIC, use the bonsaidb scheme.
164
1
    tasks.push(invoke_apis(
165
1
        AsyncClient::build(Url::parse("bonsaidb://localhost")?)
166
1
            .with_certificate(certificate)
167
1
            .build()?,
168
1
        "bonsaidb",
169
1
    ));
170
1

            
171
1
    // Wait for the clients to finish
172
1
    futures::future::join_all(tasks)
173
12
        .await
174
1
        .into_iter()
175
1
        .collect::<Result<_, _>>()?;
176

            
177
    // Shut the server down gracefully (or forcefully after 5 seconds).
178
1
    server.shutdown(Some(Duration::from_secs(5))).await?;
179

            
180
1
    Ok(())
181
}
182
2
async fn invoke_apis(client: AsyncClient, client_name: &str) -> Result<(), bonsaidb::core::Error> {
183
5
    ping_the_server(&client, client_name).await?;
184

            
185
    // Calling DoSomethingSimple and DoSomethingCustom will check permissions, which our client currently doesn't have access to.
186
    assert!(matches!(
187
2
        client
188
2
            .send_api_request(&IncrementCounter { amount: 1 })
189
2
            .await,
190
        Err(ApiError::Client(bonsaidb::client::Error::Core(
191
            bonsaidb::core::Error::PermissionDenied(_)
192
        )))
193
    ));
194
    assert!(matches!(
195
2
        client
196
2
            .send_api_request(&IncrementCounter { amount: 1 })
197
2
            .await,
198
        Err(ApiError::Client(bonsaidb::client::Error::Core(
199
            bonsaidb::core::Error::PermissionDenied(_)
200
        )))
201
    ));
202

            
203
    // Now, let's authenticate and try calling the APIs that previously were denied permissions
204
2
    let authenticated_client = client
205
2
        .authenticate(Authentication::password(
206
2
            "test-user",
207
2
            SensitiveString(String::from("hunter2")),
208
2
        )?)
209
8
        .await
210
2
        .unwrap();
211
2
    assert!(matches!(
212
2
        authenticated_client
213
2
            .send_api_request(&IncrementCounter { amount: 1 })
214
2
            .await,
215
        Ok(Counter(_))
216
    ));
217
    assert!(matches!(
218
2
        authenticated_client
219
2
            .send_api_request(&IncrementCounter { amount: 1 })
220
2
            .await,
221
        Ok(Counter(_))
222
    ));
223

            
224
2
    Ok(())
225
2
}
226

            
227
// ANCHOR: api-call
228
2
async fn ping_the_server(
229
2
    client: &AsyncClient,
230
2
    client_name: &str,
231
2
) -> Result<(), bonsaidb::core::Error> {
232
5
    match client.send_api_request(&Ping).await {
233
2
        Ok(Pong) => {
234
2
            println!("Received Pong from server on {client_name}");
235
2
        }
236
        other => println!("Unexpected response from API call on {client_name}: {other:?}"),
237
    }
238

            
239
2
    Ok(())
240
2
}
241
// ANCHOR_END: api-call
242

            
243
1
#[test]
244
1
fn runs() {
245
1
    main().unwrap()
246
1
}