Skip to content

Commit

Permalink
Merge branch 'main' into 1449-studio-print-audit-to-modal-window
Browse files Browse the repository at this point in the history
  • Loading branch information
janavlachova authored Jan 3, 2025
2 parents a8ef478 + 968daee commit 15715b9
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 8 deletions.
107 changes: 105 additions & 2 deletions agdb_server/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::password::SALT_LEN;
use crate::server_error::ServerError;
use crate::server_error::ServerResult;
use serde::Deserialize;
Expand All @@ -21,12 +22,15 @@ pub(crate) struct ConfigImpl {
pub(crate) admin: String,
pub(crate) log_level: LogLevel,
pub(crate) data_dir: String,
pub(crate) pepper_path: String,
pub(crate) cluster_token: String,
pub(crate) cluster: Vec<Url>,
#[serde(skip)]
pub(crate) cluster_node_id: usize,
#[serde(skip)]
pub(crate) start_time: u64,
#[serde(skip)]
pub(crate) pepper: Option<[u8; SALT_LEN]>,
}

pub(crate) fn new(config_file: &str) -> ServerResult<Config> {
Expand All @@ -38,11 +42,29 @@ pub(crate) fn new(config_file: &str) -> ServerResult<Config> {
.position(|x| x == &config_impl.address)
.unwrap_or(0);
config_impl.start_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();

if !config_impl.pepper_path.is_empty() {
let pepper = std::fs::read(&config_impl.pepper_path)?;

if pepper.len() != SALT_LEN {
return Err(ServerError::from(format!(
"invalid pepper length {}, expected 16",
pepper.len()
)));
}

config_impl.pepper = Some(
pepper[0..SALT_LEN]
.try_into()
.expect("pepper length should be 16"),
);
}

let config = Config::new(config_impl);

if !config.cluster.is_empty() && !config.cluster.contains(&config.address) {
return Err(ServerError::from(format!(
"Cluster does not contain local node: {}",
"cluster does not contain local node: {}",
config.address
)));
}
Expand All @@ -57,10 +79,12 @@ pub(crate) fn new(config_file: &str) -> ServerResult<Config> {
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: String::new(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
pepper: None,
};

std::fs::write(config_file, serde_yml::to_string(&config)?)?;
Expand Down Expand Up @@ -143,15 +167,94 @@ mod tests {
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: String::new(),
cluster_token: "cluster".to_string(),
cluster: vec![Url::parse("localhost:3001").unwrap()],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};
std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();
assert_eq!(
config::new(test_file.filename).unwrap_err().description,
"cluster does not contain local node: localhost:3000"
);
}

#[test]
fn pepper_path() {
let test_file = TestFile::new("pepper_path.yaml");
let pepper_file = TestFile::new("pepper_path");
let pepper = b"abcdefghijklmnop";
std::fs::write(pepper_file.filename, pepper).unwrap();
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: pepper_file.filename.to_string(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};

std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();

let config = config::new(test_file.filename).unwrap();

assert_eq!(config.pepper.as_ref(), Some(pepper));
}

#[test]
fn pepper_missing() {
let test_file = TestFile::new("pepper_missing.yaml");
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: "missing_file".to_string(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};
std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();

assert!(config::new(test_file.filename).is_err());
}

#[test]
fn pepper_invalid_len() {
let test_file = TestFile::new("pepper_invalid_len.yaml");
let pepper_file = TestFile::new("pepper_invalid_len");
std::fs::write(pepper_file.filename, b"0123456789").unwrap();
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
data_dir: "agdb_server_data".to_string(),
pepper_path: pepper_file.filename.to_string(),
cluster_token: "cluster".to_string(),
cluster: vec![],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};
std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();

assert_eq!(
config::new(test_file.filename).unwrap_err().description,
"Cluster does not contain local node: localhost:3000"
"invalid pepper length 10, expected 16"
);
}

Expand Down
2 changes: 2 additions & 0 deletions agdb_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ async fn main() -> ServerResult {
.with_max_level(config.log_level.0)
.init();

password::init(config.pepper);

let (shutdown_sender, shutdown_receiver) = broadcast::channel::<()>(1);
let server_db = server_db::new(&config).await?;
let db_pool = db_pool::new(config.clone(), &server_db).await?;
Expand Down
23 changes: 18 additions & 5 deletions agdb_server/src/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ use ring::rand::SystemRandom;
use serde::Deserialize;
use serde::Serialize;
use std::num::NonZeroU32;
use std::sync::OnceLock;

pub(crate) const PASSWORD_LEN: usize = digest::SHA256_OUTPUT_LEN;
pub(crate) const SALT_LEN: usize = 16;

pub(crate) static BUILTIN_PEPPER: &[u8; SALT_LEN] = std::include_bytes!("../pepper");
pub(crate) static PEPPER: OnceLock<[u8; SALT_LEN]> = OnceLock::new();
static ALGORITHM: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
static PEPPER: &[u8; SALT_LEN] = std::include_bytes!("../pepper");
static DB_SALT: [u8; SALT_LEN] = [
198, 78, 119, 143, 114, 32, 22, 184, 167, 93, 196, 63, 154, 18, 14, 79,
];
Expand Down Expand Up @@ -74,19 +77,27 @@ impl Password {
}

fn salt(user: &str, user_salt: [u8; SALT_LEN]) -> Vec<u8> {
let mut salt = Vec::with_capacity(
user.as_bytes().len() + user_salt.len() + DB_SALT.len() + PEPPER.len(),
);
let mut salt = Vec::with_capacity(user.as_bytes().len() + SALT_LEN * 3);

salt.extend(DB_SALT);
salt.extend(user.as_bytes());
salt.extend(PEPPER);
salt.extend(PEPPER.get().expect("pepper should be initialized"));
salt.extend(user_salt);

salt
}
}

pub(crate) fn init(pepper: Option<[u8; SALT_LEN]>) {
PEPPER.get_or_init(|| {
if let Some(p) = pepper {
p
} else {
*BUILTIN_PEPPER
}
});
}

pub(crate) fn validate_password(password: &str) -> ServerResult {
if password.len() < 8 {
Err(ErrorCode::PasswordTooShort.into())
Expand All @@ -109,6 +120,8 @@ mod tests {

#[test]
fn verify_password() -> ServerResult {
init(None);

let password = Password::create("alice", "MyPassword123");
assert_ne!(password.password, [0_u8; PASSWORD_LEN]);
assert_ne!(password.user_salt, [0_u8; SALT_LEN]);
Expand Down
1 change: 1 addition & 0 deletions agdb_server/tests/routes/misc_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ async fn basepath_test() -> anyhow::Result<()> {
config.insert("data_dir", SERVER_DATA_DIR.into());
config.insert("basepath", "/public".into());
config.insert("log_level", "INFO".into());
config.insert("pepper_path", "".into());
config.insert("cluster_token", "test".into());
config.insert("cluster", Vec::<String>::new().into());

Expand Down
2 changes: 2 additions & 0 deletions agdb_server/tests/test_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ impl TestServerImpl {
config.insert("data_dir", SERVER_DATA_DIR.into());
config.insert("basepath", "".into());
config.insert("log_level", "INFO".into());
config.insert("pepper_path", "".into());
config.insert("cluster_token", "test".into());
config.insert("cluster", Vec::<String>::new().into());

Expand Down Expand Up @@ -376,6 +377,7 @@ pub async fn create_cluster(nodes: usize) -> anyhow::Result<Vec<TestServerImpl>>
config.insert("basepath", "".into());
config.insert("log_level", "INFO".into());
config.insert("data_dir", SERVER_DATA_DIR.into());
config.insert("pepper_path", "".into());
config.insert("cluster_token", "test".into());

configs.push(config);
Expand Down
6 changes: 5 additions & 1 deletion agdb_web/pages/en-US/docs/guides/how-to-run-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ For non-production use:
cargo install agdb_server
```

For production a manual build & install is recommended. You would build `agdb_server` from source in release mode with a custom `pepper` file. The `pepper` file located in sources as `agdb_server/pepper` contains a random 16 character value that is used internally to additionally "season" the encrypted passwords. When building for production you should change this value to a different one and keep the pepper file as secret in case you needed to rebuild the server or build a new version.
For production use there are several options:

1. Manual build & install. You would build `agdb_server` from source in release mode with a custom `pepper` file. The `pepper` file located in sources as `agdb_server/pepper` contains a random 16 character value that is used internally to additionally "season" the encrypted passwords. When building for production you should change this value to a different one and keep the pepper file as secret in case you needed to rebuild the server or build a new version.

The steps for a production/manual build (use `bash` on Unix or `git bash` on Windows):

Expand All @@ -51,6 +53,8 @@ mv target/release/agdb_server "<location available on your PATH>"
(including the old admin account).
</Callout>

2. Use default pepper value (it is still recommended to build manually and use a different value) but specify in configuration the "pepper_path" from which the pepper would be loaded in runtime. This file and location should be treated as secret and all of the caveats from step 1 still apply. On the other hand you would not need to rebuild the server in the future.

### Run the server

```bash
Expand Down
1 change: 1 addition & 0 deletions agdb_web/pages/en-US/docs/references/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ basepath: "" # base path to append to the address in case the server is to be ru
admin: admin # the admin user that will be created automatically for the server, the password will be the same as name (admin by default, recommended to change after startup)
data_dir: agdb_server_data # directory to store user data
log_level: INFO # Options are: OFF, ERROR, WARN, INFO, DEBUG, TRACE
pepper_path: "" # Optional path to a runtime secret file containing 16 bytes "pepper" value for additionally "seasoning" (hashing) passwords. If empty a built-in pepper value is used - see "How to run the server?"" guide for details
```
You can prepare it in advance in a file `agdb_server.yaml`. After the server database is created changes to the `admin` field will have no effect, but the other settings can be changed later. All config changes require server restart to take effect.
Expand Down

0 comments on commit 15715b9

Please sign in to comment.