Skip to content

Commit 8a3885a

Browse files
committed
teams: API authorization, CLI, smoketests
This adds authorization to the relevant API endpoints, updates the CLI commands and adds smoketests for the teams feature. Depends-on: #3519
1 parent da137a8 commit 8a3885a

File tree

16 files changed

+875
-120
lines changed

16 files changed

+875
-120
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
run: python -m pip install psycopg2-binary xmltodict
8989
- name: Run smoketests
9090
# Note: clear_database and replication only work in private
91-
run: python -m smoketests ${{ matrix.smoketest_args }} -x clear_database replication
91+
run: python -m smoketests ${{ matrix.smoketest_args }} -x clear_database replication teams
9292
- name: Stop containers (Linux)
9393
if: always() && runner.os == 'Linux'
9494
run: docker compose down

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ tar = "0.4"
270270
tempdir = "0.3.7"
271271
tempfile = "3.20"
272272
termcolor = "1.2.0"
273+
termtree = "0.5.1"
273274
thin-vec = "0.2.13"
274275
thiserror = "1.0.37"
275276
tokio = { version = "1.37", features = ["full"] }

crates/cli/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ tabled.workspace = true
6464
tar.workspace = true
6565
tempfile.workspace = true
6666
termcolor.workspace = true
67+
termtree.workspace = true
6768
thiserror.workspace = true
6869
tokio.workspace = true
6970
tokio-tungstenite.workspace = true
@@ -83,6 +84,9 @@ quick-xml.workspace = true
8384
names.workspace = true
8485
notify.workspace = true
8586

87+
[dev-dependencies]
88+
pretty_assertions.workspace = true
89+
8690
[target.'cfg(not(target_env = "msvc"))'.dependencies]
8791
tikv-jemallocator = { workspace = true }
8892
tikv-jemalloc-ctl = { workspace = true }

crates/cli/src/subcommands/delete.rs

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
use std::io;
2+
13
use crate::common_args;
24
use crate::config::Config;
3-
use crate::util::{add_auth_header_opt, database_identity, get_auth_header};
5+
use crate::util::{add_auth_header_opt, database_identity, get_auth_header, y_or_n, AuthHeader};
46
use clap::{Arg, ArgMatches};
7+
use http::StatusCode;
8+
use itertools::Itertools as _;
9+
use reqwest::Response;
10+
use spacetimedb_client_api_messages::http::{DatabaseDeleteConfirmationResponse, DatabaseTree, DatabaseTreeNode};
11+
use spacetimedb_lib::Hash;
12+
use tokio::io::AsyncWriteExt as _;
513

614
pub fn cli() -> clap::Command {
715
clap::Command::new("delete")
@@ -22,11 +30,143 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
2230
let force = args.get_flag("force");
2331

2432
let identity = database_identity(&config, database, server).await?;
25-
26-
let builder = reqwest::Client::new().delete(format!("{}/v1/database/{}", config.get_host_url(server)?, identity));
33+
let host_url = config.get_host_url(server)?;
34+
let request_path = format!("{host_url}/v1/database/{identity}");
2735
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
28-
let builder = add_auth_header_opt(builder, &auth_header);
29-
builder.send().await?.error_for_status()?;
36+
let client = reqwest::Client::new();
37+
38+
let response = send_request(&client, &request_path, &auth_header, None).await?;
39+
match response.status() {
40+
StatusCode::PRECONDITION_REQUIRED => {
41+
let confirm = response.json::<DatabaseDeleteConfirmationResponse>().await?;
42+
println!("WARNING: Deleting the database {identity} will also delete its children!");
43+
if !force {
44+
print_database_tree_info(&confirm.database_tree).await?;
45+
}
46+
if y_or_n(force, "Do you want to proceed deleting above databases?")? {
47+
send_request(&client, &request_path, &auth_header, Some(confirm.confirmation_token))
48+
.await?
49+
.error_for_status()?;
50+
} else {
51+
println!("Aborting");
52+
}
53+
54+
Ok(())
55+
}
56+
StatusCode::OK => Ok(()),
57+
_ => response.error_for_status().map(drop).map_err(Into::into),
58+
}
59+
}
60+
61+
async fn send_request(
62+
client: &reqwest::Client,
63+
request_path: &str,
64+
auth: &AuthHeader,
65+
confirmation_token: Option<Hash>,
66+
) -> Result<Response, reqwest::Error> {
67+
let mut builder = client.delete(request_path);
68+
builder = add_auth_header_opt(builder, auth);
69+
if let Some(token) = confirmation_token {
70+
builder = builder.query(&[("token", token)]);
71+
}
72+
builder.send().await
73+
}
74+
75+
async fn print_database_tree_info(tree: &DatabaseTree) -> io::Result<()> {
76+
tokio::io::stdout()
77+
.write_all(as_termtree(tree).to_string().as_bytes())
78+
.await
79+
}
80+
81+
fn as_termtree(tree: &DatabaseTree) -> termtree::Tree<String> {
82+
let mut stack: Vec<(&DatabaseTree, bool)> = vec![];
83+
stack.push((tree, false));
84+
85+
let mut built: Vec<termtree::Tree<String>> = <_>::default();
86+
87+
while let Some((node, visited)) = stack.pop() {
88+
if visited {
89+
let mut term_node = termtree::Tree::new(fmt_tree_node(&node.root));
90+
term_node.leaves = built.drain(built.len() - node.children.len()..).collect();
91+
term_node.leaves.reverse();
92+
built.push(term_node);
93+
} else {
94+
stack.push((node, true));
95+
stack.extend(node.children.iter().rev().map(|child| (child, false)));
96+
}
97+
}
98+
99+
built
100+
.pop()
101+
.expect("database tree contains a root and we pushed it last")
102+
}
103+
104+
fn fmt_tree_node(node: &DatabaseTreeNode) -> String {
105+
format!(
106+
"{}{}",
107+
node.database_identity,
108+
if node.database_names.is_empty() {
109+
<_>::default()
110+
} else {
111+
format!(": {}", node.database_names.iter().join(", "))
112+
}
113+
)
114+
}
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use super::*;
119+
use spacetimedb_client_api_messages::http::{DatabaseTree, DatabaseTreeNode};
120+
use spacetimedb_lib::{sats::u256, Identity};
30121

31-
Ok(())
122+
#[test]
123+
fn render_termtree() {
124+
let tree = DatabaseTree {
125+
root: DatabaseTreeNode {
126+
database_identity: Identity::ONE,
127+
database_names: ["parent".into()].into(),
128+
},
129+
children: vec![
130+
DatabaseTree {
131+
root: DatabaseTreeNode {
132+
database_identity: Identity::from_u256(u256::new(2)),
133+
database_names: ["child".into()].into(),
134+
},
135+
children: vec![
136+
DatabaseTree {
137+
root: DatabaseTreeNode {
138+
database_identity: Identity::from_u256(u256::new(3)),
139+
database_names: ["grandchild".into()].into(),
140+
},
141+
children: vec![],
142+
},
143+
DatabaseTree {
144+
root: DatabaseTreeNode {
145+
database_identity: Identity::from_u256(u256::new(5)),
146+
database_names: [].into(),
147+
},
148+
children: vec![],
149+
},
150+
],
151+
},
152+
DatabaseTree {
153+
root: DatabaseTreeNode {
154+
database_identity: Identity::from_u256(u256::new(4)),
155+
database_names: ["sibling".into(), "bro".into()].into(),
156+
},
157+
children: vec![],
158+
},
159+
],
160+
};
161+
pretty_assertions::assert_eq!(
162+
"\
163+
0000000000000000000000000000000000000000000000000000000000000001: parent
164+
├── 0000000000000000000000000000000000000000000000000000000000000004: bro, sibling
165+
└── 0000000000000000000000000000000000000000000000000000000000000002: child
166+
├── 0000000000000000000000000000000000000000000000000000000000000005
167+
└── 0000000000000000000000000000000000000000000000000000000000000003: grandchild
168+
",
169+
&as_termtree(&tree).to_string()
170+
);
171+
}
32172
}

0 commit comments

Comments
 (0)