Skip to content

Commit aad9ed2

Browse files
committed
feat(network-group): introduce Network Group API
1 parent 12d2ef1 commit aad9ed2

24 files changed

+1952
-121
lines changed

Cargo.toml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ keywords = ["clevercloud", "sdk", "logging", "metrics", "jsonschemas"]
1313
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1414

1515
[features]
16-
default = ["logging"]
16+
default = ["logging", "network-group"]
1717
jsonschemas = ["dep:schemars"]
1818
logging = ["dep:log", "oauth10a/logging", "tracing/log-always"]
1919
metrics = ["oauth10a/metrics"]
2020
tracing = ["oauth10a/tracing", "dep:tracing"]
21-
network-group = ["dep:base64", "x25519-dalek", "dep:rand_core", "dep:zeroize"]
21+
network-group = ["dep:base64", "x25519-dalek", "dep:rand_core", "dep:cidr"]
2222

2323
[dependencies]
2424
base64 = { version = "^0.22.1", optional = true }
2525
chrono = { version = "^0.4.41", features = ["serde"] }
26-
oauth10a = { version = "^3.0.0", default-features = false, features = [
26+
cidr = { version = "^0.3.1", features = ["serde"], optional = true }
27+
oauth10a = { path = "../oauth10a-rust", default-features = false, features = [
2728
"client",
2829
"serde",
2930
"rest",
@@ -49,7 +50,10 @@ x25519-dalek = { version = "^2.0.1", features = [
4950
"zeroize",
5051
"static_secrets",
5152
], optional = true }
52-
zeroize = { version = "^1.8.1", features = [
53+
zeroize = { version = "^1.8.1", features = ["serde", "derive"] }
54+
env-capture = { git = "https://gitlab.corp.clever-cloud.com/CedricLG/env-capture", features = [
5355
"serde",
54-
"derive",
55-
], optional = true }
56+
] }
57+
58+
[dev-dependencies]
59+
tracing-subscriber = { version = "^0.3.19", features = ["env-filter"] }

src/clever_env.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
use std::{borrow::Cow, fs, io, path::Path};
2+
3+
use env_capture::{Env, IgnoreAsciiCase};
4+
use oauth10a::{credentials::Credentials, url::Url};
5+
6+
use crate::{
7+
DEFAULT_SSH_GATEWAY, PartialCredentials,
8+
clever_tools::{CleverTools, CleverToolsConfig, CleverToolsConfigError},
9+
default_api_host, default_auth_bridge_host,
10+
};
11+
12+
// PARTIAL OAUTH ERROR /////////////////////////////////////////////////////////
13+
14+
#[derive(Debug, thiserror::Error)]
15+
pub enum PartialOAuthError {
16+
#[error("missing token")]
17+
Token,
18+
#[error("missing secret")]
19+
Secret,
20+
#[error("missing consumer token")]
21+
ConsumerKey,
22+
#[error("missing consumer secret")]
23+
ConsumerSecret,
24+
}
25+
26+
// CLEVER ENV //////////////////////////////////////////////////////////////////
27+
28+
#[derive(Debug, thiserror::Error)]
29+
pub enum CleverEnvError {
30+
#[error("failed to capture environnement, {0}")]
31+
Capture(#[from] env_capture::Error),
32+
#[error(transparent)]
33+
CleverToolsConfigFile(#[from] CleverToolsConfigError),
34+
#[error("partial OAuth credentials: {0}")]
35+
PartialOAuth(#[from] PartialOAuthError),
36+
#[error("failed to create configuration directory")]
37+
ConfigDir(io::Error),
38+
}
39+
40+
/// Snapshot of the clever environment variables, hydrated with the OAuth
41+
/// configuration from the main configuration file of the `clever-tools`, if any.
42+
#[derive(Debug, serde::Deserialize)]
43+
pub struct CleverEnv {
44+
#[serde(rename = "API_HOST")]
45+
pub(crate) api_host: Option<Url>,
46+
#[serde(rename = "AUTH_BRIDGE_API")]
47+
pub(crate) auth_bridge_host: Option<Url>,
48+
#[serde(rename = "SSH_GATEWAY")]
49+
pub(crate) ssh_gateway: Option<Box<str>>,
50+
#[serde(rename = "", flatten, default)]
51+
pub(crate) credentials: Option<PartialCredentials>,
52+
#[serde(skip)]
53+
pub(crate) config_dir: Option<Box<Path>>,
54+
}
55+
56+
impl CleverEnv {
57+
pub fn from_env() -> Result<Self, CleverEnvError> {
58+
let env = Env::<IgnoreAsciiCase>::from_env();
59+
60+
let mut env = env.with_prefix("CLEVER_").parse::<Self>()?;
61+
62+
match &mut env.credentials {
63+
credentials @ None => {
64+
trace!("credentials not found in current process environment");
65+
66+
let config_dir = CleverToolsConfig::default_config_dir()?;
67+
68+
let config_path = CleverToolsConfig::config_path_in(&config_dir);
69+
70+
if config_path.exists() {
71+
env.config_dir.replace(config_dir.into());
72+
73+
let CleverToolsConfig {
74+
oauth_token,
75+
oauth_secret,
76+
} = CleverToolsConfig::from_path(&config_path)?;
77+
78+
*credentials = Some(Credentials::OAuth1 {
79+
token: oauth_token,
80+
secret: oauth_secret,
81+
consumer_key: None,
82+
consumer_secret: None,
83+
});
84+
85+
trace!("using credentials from `clever-tools` configuration file");
86+
}
87+
}
88+
Some(Credentials::OAuth1 {
89+
consumer_key: Some(_),
90+
consumer_secret: None,
91+
..
92+
}) => {
93+
return Err(CleverEnvError::PartialOAuth(
94+
PartialOAuthError::ConsumerSecret,
95+
));
96+
}
97+
Some(Credentials::OAuth1 {
98+
consumer_key: None,
99+
consumer_secret: Some(_),
100+
..
101+
}) => return Err(CleverEnvError::PartialOAuth(PartialOAuthError::ConsumerKey)),
102+
Some(_) => trace!("using credentials from environment"),
103+
}
104+
105+
Ok(env)
106+
}
107+
108+
pub fn env_api_host(&self) -> Option<&Url> {
109+
self.api_host.as_ref()
110+
}
111+
112+
pub fn api_host(&self) -> &Url {
113+
match &self.api_host {
114+
None => default_api_host(),
115+
Some(v) => v,
116+
}
117+
}
118+
119+
pub fn env_auth_bridge_host(&self) -> Option<&Url> {
120+
self.auth_bridge_host.as_ref()
121+
}
122+
123+
pub fn auth_bridge_host(&self) -> &Url {
124+
match &self.auth_bridge_host {
125+
None => default_auth_bridge_host(),
126+
Some(v) => v,
127+
}
128+
}
129+
130+
pub fn env_ssh_gateway(&self) -> Option<&str> {
131+
self.ssh_gateway.as_deref()
132+
}
133+
134+
pub const fn ssh_gateway(&self) -> &str {
135+
match &self.ssh_gateway {
136+
None => DEFAULT_SSH_GATEWAY,
137+
Some(v) => v,
138+
}
139+
}
140+
141+
pub const fn env_oauth_consumer_key(&self) -> Option<&str> {
142+
match self.credentials {
143+
Some(Credentials::OAuth1 {
144+
consumer_key: Some(ref v),
145+
..
146+
}) => Some(v),
147+
_ => None,
148+
}
149+
}
150+
151+
pub const fn oauth_consumer_key(&self) -> &str {
152+
match self.env_oauth_consumer_key() {
153+
Some(x) => x,
154+
None => CleverTools::CONSUMER_KEY,
155+
}
156+
}
157+
158+
pub const fn env_oauth_consumer_secret(&self) -> Option<&str> {
159+
match self.credentials {
160+
Some(Credentials::OAuth1 {
161+
consumer_secret: Some(ref v),
162+
..
163+
}) => Some(v),
164+
_ => None,
165+
}
166+
}
167+
168+
pub const fn oauth_consumer_secret(&self) -> &str {
169+
match self.env_oauth_consumer_secret() {
170+
Some(v) => v,
171+
None => CleverTools::CONSUMER_SECRET,
172+
}
173+
}
174+
175+
/// Returns the path to the directory where configuration files of clever apps are stored.
176+
pub fn config_dir(&self) -> Result<Cow<'_, Path>, CleverEnvError> {
177+
let path = match self.config_dir {
178+
Some(ref config_dir) => Cow::Borrowed(&**config_dir),
179+
None => Cow::Owned(CleverToolsConfig::default_config_dir()?),
180+
};
181+
182+
fs::create_dir_all(&*path).map_err(CleverEnvError::ConfigDir)?;
183+
184+
Ok(path)
185+
}
186+
187+
pub fn credentials(&self) -> Option<&PartialCredentials> {
188+
self.credentials.as_ref()
189+
}
190+
}
191+
192+
#[cfg(test)]
193+
mod tests {
194+
use env_capture::set_tmp_var;
195+
use oauth10a::credentials::Credentials;
196+
197+
use crate::clever_env::CleverEnv;
198+
199+
#[test]
200+
fn test_env() {
201+
let _ = unsafe { set_tmp_var("RUST_LOG", "trace") };
202+
203+
tracing_subscriber::fmt::fmt()
204+
.with_level(true)
205+
.with_line_number(true)
206+
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
207+
.init();
208+
209+
let _ = unsafe { set_tmp_var("CLEVER_TOKEN", "my_token") };
210+
let _ = unsafe { set_tmp_var("CLEVER_SECRET", "my_secret") };
211+
212+
if let Credentials::OAuth1 {
213+
token,
214+
secret,
215+
consumer_key,
216+
consumer_secret,
217+
} = CleverEnv::from_env().unwrap().credentials().unwrap()
218+
{
219+
dbg!(token, secret, consumer_key, consumer_secret);
220+
}
221+
}
222+
}

src/clever_tools.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Clever Tools
2+
3+
use std::{
4+
env, fs,
5+
io::{self, Read},
6+
path::{Path, PathBuf},
7+
};
8+
9+
// CLEVER TOOLS ////////////////////////////////////////////////////////////////
10+
11+
/// Default OAuth1 consumer.
12+
#[derive(Debug)]
13+
pub struct CleverTools;
14+
15+
impl CleverTools {
16+
// Consumer key and secret of the clever-tools are publicly available.
17+
// The disclosure of these tokens is not considered a vulnerability.
18+
// Do not report this to our security service.
19+
//
20+
// See:
21+
// - <https://github.com/CleverCloud/clever-tools/blob/fed085e2ba0339f55e966d7c8c6439d4dac71164/src/models/configuration.js#L128>
22+
23+
pub const CONSUMER_KEY: &'static str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e";
24+
pub const CONSUMER_SECRET: &'static str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT";
25+
}
26+
27+
// CLEVER TOOLS CONFIG ERROR ///////////////////////////////////////////////////
28+
29+
#[derive(Debug, thiserror::Error)]
30+
pub enum CleverToolsConfigError {
31+
#[error("failed to resolve home directory")]
32+
HomeDir,
33+
#[error("failed to open clever-tools configuration file")]
34+
Open(io::Error),
35+
#[error("failed to read clever-tools configuration file's contents")]
36+
Read(io::Error),
37+
#[error("failed to parse clever-tools configuration file's contents")]
38+
Json(serde_json::Error),
39+
}
40+
41+
// CLEVER TOOLS CONFIG /////////////////////////////////////////////////////////
42+
43+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44+
pub struct CleverToolsConfig {
45+
#[serde(rename = "token")]
46+
pub oauth_token: Box<str>,
47+
#[serde(rename = "secret")]
48+
pub oauth_secret: Box<str>,
49+
}
50+
51+
fn config_dir() -> Result<PathBuf, CleverToolsConfigError> {
52+
Ok(match env::var_os("XDG_CONFIG_HOME") {
53+
Some(config_dir) => PathBuf::from(config_dir),
54+
None => env::home_dir()
55+
.ok_or(CleverToolsConfigError::HomeDir)?
56+
.join(".config"),
57+
})
58+
}
59+
60+
impl CleverToolsConfig {
61+
pub fn default_config_dir() -> Result<PathBuf, CleverToolsConfigError> {
62+
Ok(config_dir()?.join("clever-cloud"))
63+
}
64+
65+
pub fn config_path_in(config_dir: &Path) -> PathBuf {
66+
config_dir.join("clever-tools.json")
67+
}
68+
69+
pub fn config_path() -> Result<PathBuf, CleverToolsConfigError> {
70+
Ok(Self::config_path_in(&Self::default_config_dir()?))
71+
}
72+
73+
pub fn from_path(path: &Path) -> Result<Self, CleverToolsConfigError> {
74+
let buf = {
75+
let mut buf = String::new();
76+
77+
let _ = fs::File::open(path)
78+
.map_err(CleverToolsConfigError::Open)?
79+
.read_to_string(&mut buf)
80+
.map_err(CleverToolsConfigError::Read)?;
81+
82+
buf
83+
};
84+
85+
serde_json::from_str(&buf).map_err(CleverToolsConfigError::Json)
86+
}
87+
}

0 commit comments

Comments
 (0)