Skip to content

Commit 02a7824

Browse files
authored
ENG-207: Restrict namespaces (#133)
* Restrict namespaces * Add env var to restrict namespaces
1 parent 5067ab1 commit 02a7824

File tree

9 files changed

+246
-14
lines changed

9 files changed

+246
-14
lines changed

helm/mainnet-values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ service:
2121
cloud.google.com/neg: '{"ingress": true}'
2222

2323
extraEnvVars:
24+
- name: RESTRICT_NAMESPACES
25+
value: "true"
2426
- name: PEERS
2527
value: "12D3KooWR9TtbDy2hebAU2qgbP2e3UGX8BrrZUDY6eiXHvQF3Nn4,12D3KooWKnx6YDNsZhMnqr9UADPLZXtAkYkDEo6Vs7KQjAL2dWCG,12D3KooWDtEwZyL2tjW96VCE5bJgDP58w31JTkeavaYafuA7oXcD"
2628
- name: WHITELIST

helm/prenet-values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ persistence:
2020
size: 20Gi
2121

2222
extraEnvVars:
23+
- name: RESTRICT_NAMESPACES
24+
value: "true"
2325
- name: PEERS
2426
value: "12D3KooWGwYHQGP9xJ8uxrmfhZMubfAsWPrTByGgq2N8hH2atMJZ,12D3KooWKLvUcUc4h3BEKGHKMz2kxfS7Xpps2kEfaQzpchGmMgSy,12D3KooWLYByKfin35Dh37YJf8HwQfyrL2beJW67712y9kChgbja"
2527
- name: DIAL_ADDR

helm/testnet-values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ resources:
1818
additionalDomain: testnet.spacetime.xyz
1919

2020
extraEnvVars:
21+
- name: RESTRICT_NAMESPACES
22+
value: "true"
2123
- name: PEERS
2224
value: "12D3KooWSZxfWBu726XtsGumTFtmqFTTenud1HempLgHx5kSvBaM,12D3KooWNSEqaQB4ZHFrdw4A1efz3G5aTxzfRPXPjrT5z2yfGaTs,12D3KooWPBFaDpnqiPRTQZqvf1W41fcszS27qPhpkL757Z2XSkcz"
2325
- name: SNAPSHOT_CHUNK_SIZE

polybase/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ pub struct Config {
8787
/// Public key whitelist
8888
#[arg(long, env = "WHITELIST", value_parser, value_delimiter = ',')]
8989
pub whitelist: Option<Vec<String>>,
90+
91+
/// Restrict namespaces to pk/<pk>/<collection_name>
92+
#[arg(long, env = "RESTRICT_NAMESPACES", default_value = "false")]
93+
pub restrict_namespaces: bool,
9094
}
9195

9296
#[derive(Subcommand, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]

polybase/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ pub enum AppError {
4343
#[error("invalid request")]
4444
B58(#[from] bs58::decode::Error),
4545

46+
#[error("anonymous namespaces are not allowed, sign your request")]
47+
AnonNamespace,
48+
4649
#[error("public key not included in allowed whitelist")]
4750
Whitelist,
51+
52+
#[error(
53+
"namespace is invalid, must be in format pk/<public_key_hex>/<CollectionName> got {0}"
54+
)]
55+
InvalidNamespace(String),
4856
}

polybase/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ async fn main() -> Result<()> {
238238
config.rpc_laddr,
239239
Arc::clone(&db),
240240
Arc::new(config.whitelist.clone()),
241+
Arc::new(config.restrict_namespaces),
241242
logger.clone(),
242243
)?;
243244

polybase/src/rpc.rs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::{borrow::Cow, cmp::min, sync::Arc, time::Duration};
1919
struct RouteState {
2020
db: Arc<Db>,
2121
whitelist: Arc<Option<Vec<String>>>,
22+
restrict_namespaces: Arc<bool>,
2223
}
2324

2425
#[get("/")]
@@ -340,9 +341,15 @@ async fn post_record(
340341
let auth = body.auth.map(|a| a.into());
341342
let db: Arc<_> = Arc::clone(&state.db);
342343

343-
// Check whitelist
344+
// New collection is being created
344345
if collection_id == "Collection" {
345-
validate_whitelist(&state.whitelist, &auth)?;
346+
// Validate whitelist (if it exists)
347+
validate_new_collection(
348+
&body.data.args[0],
349+
&state.whitelist,
350+
&state.restrict_namespaces,
351+
&auth,
352+
)?;
346353
}
347354

348355
let txn = CallTxn::new(
@@ -437,6 +444,7 @@ pub fn create_rpc_server(
437444
rpc_laddr: String,
438445
db: Arc<Db>,
439446
whitelist: Arc<Option<Vec<String>>>,
447+
restrict_namespaces: Arc<bool>,
440448
logger: slog::Logger,
441449
) -> Result<Server, std::io::Error> {
442450
Ok(HttpServer::new(move || {
@@ -446,6 +454,7 @@ pub fn create_rpc_server(
446454
.app_data(web::Data::new(RouteState {
447455
db: Arc::clone(&db),
448456
whitelist: Arc::clone(&whitelist),
457+
restrict_namespaces: Arc::clone(&restrict_namespaces),
449458
}))
450459
.wrap(SlogMiddleware::new(logger.clone()))
451460
.wrap(cors)
@@ -464,27 +473,57 @@ pub fn create_rpc_server(
464473
.run())
465474
}
466475

467-
fn validate_whitelist(
476+
fn validate_new_collection(
477+
collection_id: &serde_json::Value,
468478
whitelist: &Option<Vec<String>>,
479+
restrict_namespaces: &bool,
469480
auth: &Option<AuthUser>,
470481
) -> Result<(), HTTPError> {
471-
// Check whitelist
482+
let pk = auth
483+
.as_ref()
484+
.map(|a| a.public_key().to_hex().unwrap_or("".to_string()))
485+
.unwrap_or("".to_string());
486+
487+
// Check collection whitelist
472488
if let Some(whitelist) = whitelist {
473-
if let Some(auth_user) = auth {
474-
// Convert the key to hex for easier comparison
475-
let pk = auth_user.public_key().to_hex().unwrap_or("".to_string());
476-
if pk.is_empty() || !whitelist.contains(&pk) {
477-
return Err(HTTPError::new(
478-
ReasonCode::Unauthorized,
479-
Some(Box::new(AppError::Whitelist)),
480-
));
481-
}
482-
} else {
489+
if pk.is_empty() || !whitelist.contains(&pk) {
483490
return Err(HTTPError::new(
484491
ReasonCode::Unauthorized,
485492
Some(Box::new(AppError::Whitelist)),
486493
));
487494
}
488495
}
496+
497+
// Check namespace is valid (only pk/<pk> currently allowed)
498+
if *restrict_namespaces {
499+
if pk.is_empty() {
500+
return Err(HTTPError::new(
501+
ReasonCode::Unauthorized,
502+
Some(Box::new(AppError::AnonNamespace)),
503+
));
504+
}
505+
506+
match collection_id {
507+
serde_json::Value::String(id) => {
508+
let parts: Vec<&str> = id.split('/').collect();
509+
if parts.len() != 3 || parts[0] != "pk" || parts[1] != pk {
510+
return Err(HTTPError::new(
511+
ReasonCode::Unauthorized,
512+
Some(Box::new(AppError::InvalidNamespace(id.clone()))),
513+
));
514+
}
515+
}
516+
_ => {
517+
return Err(HTTPError::new(
518+
ReasonCode::Unauthorized,
519+
Some(Box::new(AppError::InvalidNamespace(format!(
520+
"{:?}",
521+
collection_id
522+
)))),
523+
));
524+
}
525+
}
526+
}
527+
489528
Ok(())
490529
}

polybase/tests/api/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod index_where_sort;
1111
mod map_field;
1212
mod nested_field;
1313
mod other_collection_fns;
14+
mod restrict_namespaces;
1415
mod start_stop;
1516
mod store_other_collection_records;
1617
mod whitelist;
@@ -104,6 +105,7 @@ impl PortPool {
104105
struct ServerConfig {
105106
whitelist: Option<Vec<String>>,
106107
keep_port_after_drop: bool,
108+
restrict_namespaces: bool,
107109
}
108110

109111
#[derive(Debug)]
@@ -138,6 +140,10 @@ impl Server {
138140
if let Some(ref whitelist) = config.whitelist {
139141
command.arg("--whitelist").arg(whitelist.join(","));
140142
}
143+
144+
if config.restrict_namespaces {
145+
command.arg("--restrict-namespaces");
146+
}
141147
}
142148

143149
command.arg("--root-dir").arg(root_dir.path());
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use crate::api::{Error, ErrorData, Signature, Signer};
2+
use std::time::SystemTime;
3+
4+
use super::{Server, ServerConfig};
5+
6+
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
7+
#[serde(rename_all = "camelCase")]
8+
struct Account {
9+
id: String,
10+
}
11+
12+
#[tokio::test]
13+
async fn valid() {
14+
let schema = r#"
15+
@public
16+
collection Account {
17+
id: string;
18+
}
19+
"#;
20+
21+
let (private_key, public_key) = secp256k1::generate_keypair(&mut rand::thread_rng());
22+
let public_key = indexer::PublicKey::from_secp256k1_key(&public_key).unwrap();
23+
let pk_hex = public_key.to_hex().unwrap();
24+
25+
let signer = Signer::from(move |body: &str| {
26+
let mut signature = Signature::create(&private_key, SystemTime::now(), body);
27+
signature.public_key = Some(public_key.clone());
28+
signature
29+
});
30+
31+
let server = Server::setup_and_wait(Some(ServerConfig {
32+
restrict_namespaces: true,
33+
..Default::default()
34+
}))
35+
.await;
36+
37+
let collection_id = format!("pk/{}/Account", pk_hex);
38+
let res = server
39+
.create_collection::<Account>(collection_id.as_str(), schema, Some(&signer))
40+
.await
41+
.unwrap();
42+
43+
assert_eq!(res.id, collection_id.to_string());
44+
}
45+
46+
#[tokio::test]
47+
async fn invalid_prefix() {
48+
let schema = r#"
49+
@public
50+
collection Account {
51+
id: string;
52+
}
53+
"#;
54+
55+
let (private_key, public_key) = secp256k1::generate_keypair(&mut rand::thread_rng());
56+
let public_key = indexer::PublicKey::from_secp256k1_key(&public_key).unwrap();
57+
let pk_hex = public_key.to_hex().unwrap();
58+
59+
let signer = Signer::from(move |body: &str| {
60+
let mut signature = Signature::create(&private_key, SystemTime::now(), body);
61+
signature.public_key = Some(public_key.clone());
62+
signature
63+
});
64+
65+
let server = Server::setup_and_wait(Some(ServerConfig {
66+
restrict_namespaces: true,
67+
..Default::default()
68+
}))
69+
.await;
70+
71+
let collection_id = format!("other/{}/Account", pk_hex);
72+
73+
let res = server
74+
.create_collection::<Account>(collection_id.as_str(), schema, Some(&signer))
75+
.await
76+
.unwrap_err();
77+
78+
assert_eq!(
79+
res,
80+
Error {
81+
error: ErrorData {
82+
code: "permission-denied".to_string(),
83+
message: format!("namespace is invalid, must be in format pk/<public_key_hex>/<CollectionName> got {}", collection_id.as_str()),
84+
reason: "unauthorized".to_string(),
85+
}
86+
}
87+
);
88+
}
89+
90+
#[tokio::test]
91+
async fn mismatch_pk() {
92+
let schema = r#"
93+
@public
94+
collection Account {
95+
id: string;
96+
}
97+
"#;
98+
99+
// Key signing the request
100+
let (private_key, public_key) = secp256k1::generate_keypair(&mut rand::thread_rng());
101+
let public_key = indexer::PublicKey::from_secp256k1_key(&public_key).unwrap();
102+
103+
// Key to be used in whitelist
104+
let (_, alt_public_key) = secp256k1::generate_keypair(&mut rand::thread_rng());
105+
let alt_public_key = indexer::PublicKey::from_secp256k1_key(&alt_public_key).unwrap();
106+
let pk_hex: String = alt_public_key.to_hex().unwrap();
107+
108+
let signer = Signer::from(move |body: &str| {
109+
let mut signature = Signature::create(&private_key, SystemTime::now(), body);
110+
signature.public_key = Some(public_key.clone());
111+
signature
112+
});
113+
114+
let collection_id = format!("pk/{}/Account", pk_hex);
115+
let server = Server::setup_and_wait(Some(ServerConfig {
116+
restrict_namespaces: true,
117+
..Default::default()
118+
}))
119+
.await;
120+
121+
let res = server
122+
.create_collection::<Account>(collection_id.as_str(), schema, Some(&signer))
123+
.await
124+
.unwrap_err();
125+
126+
assert_eq!(
127+
res,
128+
Error {
129+
error: ErrorData {
130+
code: "permission-denied".to_string(),
131+
message: format!("namespace is invalid, must be in format pk/<public_key_hex>/<CollectionName> got {}", collection_id.as_str()),
132+
reason: "unauthorized".to_string(),
133+
}
134+
}
135+
);
136+
}
137+
138+
#[tokio::test]
139+
async fn missing_pk() {
140+
let schema = r#"
141+
@public
142+
collection Account {
143+
id: string;
144+
}
145+
"#;
146+
147+
let server = Server::setup_and_wait(Some(ServerConfig {
148+
restrict_namespaces: true,
149+
..ServerConfig::default()
150+
}))
151+
.await;
152+
153+
let res = server
154+
.create_collection::<Account>("test/Account", schema, None)
155+
.await
156+
.unwrap_err();
157+
158+
assert_eq!(
159+
res,
160+
Error {
161+
error: ErrorData {
162+
code: "permission-denied".to_string(),
163+
message: "anonymous namespaces are not allowed, sign your request".to_string(),
164+
reason: "unauthorized".to_string(),
165+
}
166+
}
167+
);
168+
}

0 commit comments

Comments
 (0)