Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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 all 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
7 changes: 6 additions & 1 deletion .github/workflows/_test_int.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ jobs:
uses: supercharge/[email protected]
with:
mongodb-version: ${{ inputs.mongodb }}
mongodb-replica-set: test-rs
mongodb-username: root
mongodb-password: root
# FIXME: Currently we cannot configure this action to use authentication together with replica sets as mentioned here:
# https://github.com/supercharge/mongodb-github-action#with-authentication-mongodb---auth-flag
# Apparently, the solution is to write a script that sets up the user beforehand.
#mongodb-replica-set: test-rs

- name: Test DB
uses: actions-rs/cargo@v1
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ The changelog can be created using the following command (requires the [`convent
```sh
conventional-changelog -p conventionalcommits -i CHANGELOG.md -s
```

## Docker deployment configuration of credentials through environment variables

Docker compose will automatically load credentials for different services from a `.env` file that must either be located in the same directory as the `docker-compose.yml` file, or specified using the `--env-file` flag. You therefore must create such a file before you do a `docker compose up`. An example `.env` file could look like this:

```ini
MONGODB_USERNAME=root
MONGODB_PASSWORD=root
INFLUXDB_USERNAME=root
INFLUXDB_PASSWORD=password
JWT_PASSWORD=password
JWT_SALT=saltines
```
90 changes: 0 additions & 90 deletions config.template.toml

This file was deleted.

21 changes: 11 additions & 10 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ services:
volumes:
- ./data/chronicle/mongodb:/data/db
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=root
- MONGO_INITDB_ROOT_USERNAME=${MONGODB_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGODB_PASSWORD}
ports:
- 27017:27017

Expand All @@ -29,17 +29,18 @@ services:
ports:
- "8042:8042/tcp" # REST API
- "9100:9100/tcp" # Metrics
environment:
- RUST_LOG=warn,inx_chronicle=debug
tty: true
command:
- "--config=config.toml"
- "--inx-url=http://hornet:9029"
- "--mongodb-conn-str=mongodb://mongo:27017"
- "--mongodb-username=${MONGODB_USERNAME}"
- "--mongodb-password=${MONGODB_PASSWORD}"
- "--influxdb-url=http://influx:8086"
- "--influxdb-username=${INFLUXDB_USERNAME}"
- "--influxdb-password=${INFLUXDB_PASSWORD}"
- "--inx-url=http://hornet:9029"
- "--jwt-password=${JWT_PASSWORD}"
- "--jwt-salt=${JWT_SALT}"
- "--loki-url=http://loki:3100"
volumes:
- ../config.template.toml:/app/config.toml:ro

influx:
image: influxdb:1.8
Expand All @@ -48,8 +49,8 @@ services:
- ./data/chronicle/influxdb:/var/lib/influxdb
- ./assets/influxdb/init.iql:/docker-entrypoint-initdb.d/influx_init.iql
environment:
- INFLUXDB_ADMIN_USER=root
- INFLUXDB_ADMIN_PASSWORD=password
- INFLUXDB_ADMIN_USER=${INFLUXDB_USERNAME}
- INFLUXDB_ADMIN_PASSWORD=${INFLUXDB_PASSWORD}
- INFLUXDB_HTTP_AUTH_ENABLED=true
# The following variables are used to scale InfluxDB to larger amounts of data. It remains to be seen how this
# will affect performance in the long run.
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
Loading