Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

refactor(config)!: re-design and clean-up configuration #979

Merged
merged 42 commits into from
Dec 23, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3043c2c
Refactor CLI
Alex6323 Dec 13, 2022
227ec8e
Remove custom Default impls from config types
Alex6323 Dec 13, 2022
5c83c33
Remove config.template.toml
Alex6323 Dec 13, 2022
34aff37
Add gen-config tool
Alex6323 Dec 14, 2022
1b8f96d
Merge branch 'main' into refactor/config/config-refactor
Alex6323 Dec 14, 2022
f989c0f
Fix CI
Alex6323 Dec 14, 2022
c9cd8a6
Fix tests
Alex6323 Dec 14, 2022
2c8dbea
Fix docs
Alex6323 Dec 14, 2022
7700dea
Use only non-default CLI commands in docker-compose.yml
Alex6323 Dec 14, 2022
c0f5793
Let's try enforcing MongoDb auth
Alex6323 Dec 14, 2022
99f9b47
Let's try again
Alex6323 Dec 14, 2022
1c55eca
What happens now?
Alex6323 Dec 14, 2022
42b0b0f
Move defaults into constants
Alex6323 Dec 16, 2022
f674fc8
Remove user Argon configuration
Alex6323 Dec 16, 2022
611f793
Improve API configuration
Alex6323 Dec 16, 2022
e68cc71
Complete default consts
Alex6323 Dec 16, 2022
5a4715c
Fix clippy box default warning
Alex6323 Dec 16, 2022
d682d2f
Merge branch 'fix/clippy/box-default' into refactor/config/config-ref…
Alex6323 Dec 16, 2022
308a361
Fix docs
Alex6323 Dec 16, 2022
05d3615
Format
Alex6323 Dec 16, 2022
72aa03d
Fix CI
Alex6323 Dec 16, 2022
697607f
Renaming
Alex6323 Dec 17, 2022
be8ff50
Simplify CLI
Alex6323 Dec 19, 2022
cd44f15
Remove config file
Alex6323 Dec 19, 2022
8d2d806
Switch toggle to disable flags
Alex6323 Dec 19, 2022
5776d56
Re-enable replica set for integration tests
Alex6323 Dec 19, 2022
2d43530
Merge branch 'main' into refactor/config/config-refactor
Alex6323 Dec 19, 2022
5f5c7e0
Fix merge
Alex6323 Dec 19, 2022
e0a7e71
(Temporarily) fix integration test CI
Alex6323 Dec 20, 2022
b013252
Merge branch 'main' into refactor/config/config-refactor
Alex6323 Dec 20, 2022
4c22547
Fix and small reorder
Alex6323 Dec 20, 2022
9f2663b
Obey the Clippy overlord
Alex6323 Dec 20, 2022
d20eccd
Impl From<*Args> for *Configs
Alex6323 Dec 20, 2022
6987b76
More details regarding replica-sets with auth
Alex6323 Dec 20, 2022
fafdc94
Fix check-all-features
Alex6323 Dec 22, 2022
492bc45
Feature headache
Alex6323 Dec 23, 2022
d071abc
Feature headache 2
Alex6323 Dec 23, 2022
ac450c6
Last fixes
Alex6323 Dec 23, 2022
dc31c2f
Load .env file in inx-chronicle docker service
Alex6323 Dec 23, 2022
44a937d
Different approach
Alex6323 Dec 23, 2022
63c234e
Add example to README
Alex6323 Dec 23, 2022
1b9fdaf
Update README.md
grtlr Dec 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/_test_int.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ jobs:
uses: supercharge/[email protected]
with:
mongodb-version: ${{ inputs.mongodb }}
mongodb-replica-set: test-rs
# FIXME: replica-set with authentication
#mongodb-replica-set: test-rs
mongodb-username: root
mongodb-password: root

- name: Test DB
uses: actions-rs/cargo@v1
Expand Down
90 changes: 0 additions & 90 deletions config.template.toml

This file was deleted.

5 changes: 1 addition & 4 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,10 @@ services:
- RUST_LOG=warn,inx_chronicle=debug
tty: true
command:
- "--config=config.toml"
- "--inx-url=http://hornet:9029"
- "--mongodb-conn-str=mongodb://mongo:27017"
- "--influxdb-url=http://influx:8086"
- "--inx-url=http://hornet:9029"
- "--loki-url=http://loki:3100"
volumes:
- ../config.template.toml:/app/config.toml:ro

influx:
image: influxdb:1.8
Expand Down
12 changes: 6 additions & 6 deletions src/bin/inx-chronicle/api/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ use axum::{
TypedHeader,
};

use super::{config::ApiData, error::RequestError, ApiError, AuthError};
use super::{config::ApiConfigData, error::RequestError, ApiError, AuthError};

pub struct Auth;

#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Auth
where
ApiData: FromRef<S>,
ApiConfigData: FromRef<S>,
{
type Rejection = ApiError;

async fn from_request_parts(req: &mut axum::http::request::Parts, state: &S) -> Result<Self, Self::Rejection> {
// Unwrap: <OriginalUri as FromRequest>::Rejection = Infallable
let OriginalUri(uri) = OriginalUri::from_request_parts(req, state).await.unwrap();

let config = ApiData::from_ref(state);
let config = ApiConfigData::from_ref(state);

if config.public_routes.is_match(&uri.to_string()) {
return Ok(Auth);
Expand All @@ -37,10 +37,10 @@ where

jwt.validate(
Validation::default()
.with_issuer(ApiData::ISSUER)
.with_audience(ApiData::AUDIENCE)
.with_issuer(ApiConfigData::ISSUER)
.with_audience(ApiConfigData::AUDIENCE)
.validate_nbf(true),
config.secret_key.as_ref(),
config.jwt_secret_key.as_ref(),
)
.map_err(AuthError::InvalidJwt)?;

Expand Down
100 changes: 65 additions & 35 deletions src/bin/inx-chronicle/api/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,71 +10,83 @@ use tower_http::cors::AllowOrigin;

use super::{error::ConfigError, SecretKey};

pub const DEFAULT_ENABLED: bool = true;
pub const DEFAULT_PORT: u16 = 8042;
pub const DEFAULT_ALLOW_ORIGINS: &str = "0.0.0.0";
pub const DEFAULT_PUBLIC_ROUTES: &str = "api/core/v2/*";
pub const DEFAULT_MAX_PAGE_SIZE: usize = 1000;
pub const DEFAULT_JWT_PASSWORD: &str = "password";
pub const DEFAULT_JWT_SALT: &str = "saltines";
pub const DEFAULT_JWT_EXPIRATION: &str = "72h";

/// API configuration
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ApiConfig {
pub enabled: bool,
pub port: u16,
pub allow_origins: SingleOrMultiple<String>,
pub password_hash: String,
pub password_salt: String,
#[serde(with = "humantime_serde")]
pub jwt_expiration: Duration,
pub public_routes: Vec<String>,
pub identity_path: Option<String>,
pub max_page_size: usize,
pub argon_config: ArgonConfig,
pub jwt_password: String,
pub jwt_salt: String,
pub jwt_identity_file: Option<String>,
#[serde(with = "humantime_serde")]
pub jwt_expiration: Duration,
}

impl Default for ApiConfig {
fn default() -> Self {
Self {
enabled: true,
port: 8042,
allow_origins: "*".to_string().into(),
password_hash: "c42cf2be3a442a29d8cd827a27099b0c".to_string(),
password_salt: "saltines".to_string(),
// 72 hours
jwt_expiration: Duration::from_secs(72 * 60 * 60),
public_routes: Default::default(),
identity_path: None,
max_page_size: 1000,
argon_config: Default::default(),
enabled: DEFAULT_ENABLED,
port: DEFAULT_PORT,
allow_origins: SingleOrMultiple::Single(DEFAULT_ALLOW_ORIGINS.to_string()),
public_routes: vec![DEFAULT_PUBLIC_ROUTES.to_string()],
max_page_size: DEFAULT_MAX_PAGE_SIZE,
jwt_identity_file: None,
jwt_password: DEFAULT_JWT_PASSWORD.to_string(),
jwt_salt: DEFAULT_JWT_SALT.to_string(),
jwt_expiration: DEFAULT_JWT_EXPIRATION.parse::<humantime::Duration>().unwrap().into(),
}
}
}

#[derive(Clone, Debug)]
pub struct ApiData {
pub struct ApiConfigData {
pub port: u16,
pub allow_origins: AllowOrigin,
pub password_hash: Vec<u8>,
pub password_salt: String,
pub jwt_expiration: Duration,
pub public_routes: RegexSet,
pub secret_key: SecretKey,
pub max_page_size: usize,
pub argon_config: ArgonConfig,
pub jwt_password_hash: Vec<u8>,
pub jwt_password_salt: String,
pub jwt_secret_key: SecretKey,
pub jwt_expiration: Duration,
pub jwt_argon_config: JwtArgonConfig,
}

impl ApiData {
impl ApiConfigData {
pub const ISSUER: &'static str = "chronicle";
pub const AUDIENCE: &'static str = "api";
}

impl TryFrom<ApiConfig> for ApiData {
impl TryFrom<ApiConfig> for ApiConfigData {
type Error = ConfigError;

fn try_from(config: ApiConfig) -> Result<Self, Self::Error> {
Ok(Self {
port: config.port,
allow_origins: AllowOrigin::try_from(config.allow_origins)?,
password_hash: hex::decode(config.password_hash)?,
password_salt: config.password_salt,
jwt_expiration: config.jwt_expiration,
public_routes: RegexSet::new(config.public_routes.iter().map(route_to_regex).collect::<Vec<_>>())?,
secret_key: match &config.identity_path {
max_page_size: config.max_page_size,
jwt_password_hash: argon2::hash_raw(
config.jwt_password.as_bytes(),
config.jwt_salt.as_bytes(),
&Into::into(&JwtArgonConfig::default()),
)
// TODO: Replace this once we switch to a better error lib
.expect("invalid JWT config"),
jwt_password_salt: config.jwt_salt,
jwt_secret_key: match &config.jwt_identity_file {
Some(path) => SecretKey::from_file(path)?,
None => {
if let Ok(path) = std::env::var("IDENTITY_PATH") {
Expand All @@ -84,8 +96,8 @@ impl TryFrom<ApiConfig> for ApiData {
}
}
},
max_page_size: config.max_page_size,
argon_config: config.argon_config,
jwt_expiration: config.jwt_expiration,
jwt_argon_config: JwtArgonConfig::default(),
})
}
}
Expand Down Expand Up @@ -130,9 +142,27 @@ impl TryFrom<SingleOrMultiple<String>> for AllowOrigin {
}
}

impl<T: Default> Default for SingleOrMultiple<T> {
fn default() -> Self {
Self::Single(Default::default())
}
}

impl<T: Clone> From<&Vec<T>> for SingleOrMultiple<T> {
fn from(value: &Vec<T>) -> Self {
if value.is_empty() {
unreachable!("Vec must have single or multiple elements")
} else if value.len() == 1 {
Self::Single(value[0].clone())
} else {
Self::Multiple(value.to_vec())
}
}
}

#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct ArgonConfig {
pub struct JwtArgonConfig {
/// The length of the resulting hash.
hash_length: u32,
/// The number of lanes in parallel.
Expand All @@ -149,7 +179,7 @@ pub struct ArgonConfig {
version: argon2::Version,
}

impl Default for ArgonConfig {
impl Default for JwtArgonConfig {
fn default() -> Self {
Self {
hash_length: 32,
Expand All @@ -162,8 +192,8 @@ impl Default for ArgonConfig {
}
}

impl<'a> From<&'a ArgonConfig> for argon2::Config<'a> {
fn from(val: &'a ArgonConfig) -> Self {
impl<'a> From<&'a JwtArgonConfig> for argon2::Config<'a> {
fn from(val: &'a JwtArgonConfig) -> Self {
Self {
ad: &[],
hash_length: val.hash_length,
Expand Down
14 changes: 9 additions & 5 deletions src/bin/inx-chronicle/api/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use axum::extract::{FromRef, FromRequestParts, Query};
use serde::Deserialize;

use super::{
config::ApiData,
config::ApiConfigData,
error::{ApiError, RequestError},
DEFAULT_PAGE_SIZE,
};
Expand All @@ -30,15 +30,15 @@ impl Default for Pagination {
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Pagination
where
ApiData: FromRef<S>,
ApiConfigData: FromRef<S>,
{
type Rejection = ApiError;

async fn from_request_parts(req: &mut axum::http::request::Parts, state: &S) -> Result<Self, Self::Rejection> {
let Query(mut pagination) = Query::<Pagination>::from_request_parts(req, state)
.await
.map_err(RequestError::from)?;
let config = ApiData::from_ref(state);
let config = ApiConfigData::from_ref(state);
pagination.page_size = pagination.page_size.min(config.max_page_size);
Ok(pagination)
}
Expand Down Expand Up @@ -117,14 +117,18 @@ mod test {

#[tokio::test]
async fn page_size_clamped() {
let config = ApiData::try_from(ApiConfig::default()).unwrap();
let config = ApiConfig {
max_page_size: 1000,
..Default::default()
};
let data = ApiConfigData::try_from(config).unwrap();
let req = Request::builder()
.method("GET")
.uri("/?pageSize=9999999")
.body(())
.unwrap();
assert_eq!(
Pagination::from_request(req, &config).await.unwrap(),
Pagination::from_request(req, &data).await.unwrap(),
Pagination {
page_size: 1000,
..Default::default()
Expand Down
Loading