Skip to content

Commit

Permalink
identity: implement config file and http redirect
Browse files Browse the repository at this point in the history
  • Loading branch information
TheButlah committed Oct 4, 2024
1 parent a21b61a commit 39db399
Show file tree
Hide file tree
Showing 8 changed files with 660 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

*.swp
.DS_Store

# For local development
.direnv
.envrc
my_config.toml

# SQLite
*.db


56 changes: 56 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ clap = { version = "4.4.11", features = ["derive"] }
color-eyre = "0.6"
did-simple.path = "did-simple"
eyre = "0.6"
futures = "0.3.30"
header-parsing.path = "header-parsing"
hex-literal = "0.4.1"
http = "1.1.0"
Expand All @@ -35,11 +36,13 @@ serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.114"
thiserror = "1.0.64"
tokio = { version = "1.35.1", default-features = false }
toml = "0.8.19"
tower = "0.4.13"
tower-http = "0.5.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-test = "0.2.5"
url = "2.5.2"
uuid = "1.7.0"
wiremock = "0.6.2"

Expand Down
3 changes: 3 additions & 0 deletions identity-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ clap = { workspace = true, features = ["derive", "env"] }
color-eyre.workspace = true
derive_more = { workspace = true, features = ["debug"] }
did-simple.workspace = true
futures.workspace = true
header-parsing.workspace = true
http-body-util.workspace = true
jose-jwk = { workspace = true, default-features = false }
Expand All @@ -28,9 +29,11 @@ serde_json.workspace = true
sqlx = { version = "0.8.0", features = ["runtime-tokio", "tls-rustls", "sqlite", "uuid", "migrate"] }
thiserror.workspace = true
tokio = { workspace = true, features = ["full"] }
toml.workspace = true
tower-http = { workspace = true, features = ["trace", "fs"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing.workspace = true
url = { workspace = true, features = ["serde"] }
uuid = { workspace = true, features = ["std", "v4", "serde"] }

[dev-dependencies]
Expand Down
34 changes: 34 additions & 0 deletions identity-server/default_config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Default settings. Alternative options are commented out.

[cache]
# By default, we use the cache directory on your machine (from
# `$XDG_CACHE_HOME/vr_identity` or `~/.config/cache/vr_identity`
# dir = "path/to/my/cache/dir"

# Note: If both HTTP and HTTPS are enabled, the HTTP server will always redirect to the
# HTTPS endpoints.
[http]
port = 80 # also supports 0 to mean random

# Note: Enabling HTTPS always will force HSTS on.
[https]
port = 443 # also supports 0 to mean random

[https.tls]
type = "acme" # requires publicly visible port to be 443, otherwise the challenge fails
domains = [] # The public domain name(s). Should not include the url scheme.
# domains = ["socialvr.net", "socialvr.net:1337", "10.11.12.13"]

# [https.tls]
# type = "self_signed"
# domains = ["socialvr.net"]

# [https.tls]
# type = "file"
# cert_path = "path/to/cert.pem"
# private_key_path = "another/path/key.pem"

[third_party.google]
# To get the client id, follow the instructions at:
# https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#get_your_google_api_client_id
oauth2_client_id = ""
201 changes: 201 additions & 0 deletions identity-server/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
//! Structs representing deserialized config file
//!
//! See [`Config`].
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

pub const DEFAULT_CONFIG_CONTENTS: &str = include_str!("../default_config.toml");

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub enum DatabaseConfig {
Sqlite { db_file: PathBuf },
}

impl Default for DatabaseConfig {
fn default() -> Self {
Self::Sqlite {
db_file: PathBuf::from(".").join("identities.db"),
}
}
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct CacheSettings {
/// If `None`, relies on `XDG_CACHE_HOME` instead.
pub dir: Option<PathBuf>,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct HttpConfig {
/// If `0`, uses a random available port.
pub port: u16,
}

impl Default for HttpConfig {
fn default() -> Self {
Self { port: 80 }
}
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct HttpsConfig {
/// If `0`, uses a random available port.
pub port: u16,
#[serde(default)]
pub tls: TlsConfig,
}

impl Default for HttpsConfig {
fn default() -> Self {
Self {
port: 443,
tls: TlsConfig::default(),
}
}
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct ThirdPartySettings {
#[serde(default = "default_some")]
pub google: Option<GoogleSettings>,
}

impl Default for ThirdPartySettings {
fn default() -> Self {
Self {
google: Some(GoogleSettings::default()),
}
}
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct GoogleSettings {
/// The Google API OAuth2 Client ID.
/// See https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid
#[serde(default)]
pub oauth2_client_id: String,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")]
pub enum TlsConfig {
/// LetsEncrypt's certificate authoriy and the TLS-ALPN-01 challenge type to get a
/// valid signed certificate.
/// Read more at https://letsencrypt.org/docs/challenge-types/#tls-alpn-01
Acme {
domains: Vec<String>,
},
/// Creates a self-signed certificate
SelfSigned {
domains: Vec<String>,
},
File {
path: PathBuf,
},
}

impl Default for TlsConfig {
fn default() -> Self {
Self::Acme {
domains: Vec::new(),
}
}
}

/// Helper function to construct `Some(T::default())`.
fn default_some<T: Default>() -> Option<T> {
Some(T::default())
}

#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("error in toml file: {0}")]
Toml(#[from] toml::de::Error),
}

/// The contents of the config file. Contains all settings customizeable during
/// deployment.
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub database: DatabaseConfig,
#[serde(default = "default_some")]
pub http: Option<HttpConfig>,
#[serde(default = "default_some")]
pub https: Option<HttpsConfig>,
#[serde(default)]
pub cache: CacheSettings,
#[serde(default)]
pub third_party: ThirdPartySettings,
}

impl Config {
pub fn from_str(str: &str) -> Result<Self, ConfigError> {
let config: Self = toml::from_str(str)?;

Ok(config)
}
}

impl Default for Config {
fn default() -> Self {
Self {
database: DatabaseConfig::default(),
http: Some(HttpConfig::default()),
https: Some(HttpsConfig::default()),
cache: CacheSettings::default(),
third_party: ThirdPartySettings::default(),
}
}
}

#[cfg(test)]
mod test {
use super::*;

/// We could have used Config::default, but I wanted to explicitly write it all out
/// in case something is messed up.
fn default_config() -> Config {
Config {
database: DatabaseConfig::Sqlite {
db_file: PathBuf::from("./identities.db"),
},
http: Some(HttpConfig { port: 80 }),
https: Some(HttpsConfig {
port: 443,
tls: TlsConfig::Acme {
domains: Vec::new(),
},
}),
cache: CacheSettings { dir: None },
third_party: ThirdPartySettings {
google: Some(GoogleSettings {
oauth2_client_id: String::new(),
}),
},
}
}

#[test]
fn test_empty_config_file_deserializes_to_default() {
let config = Config::from_str("").unwrap();
assert_eq!(config, default_config());
assert_eq!(config, Config::default());
}

#[test]
fn test_default_config_deserializes_correctly() {
let deserialized: Config = toml::from_str(DEFAULT_CONFIG_CONTENTS)
.expect("default config file should always deserialize");
assert_eq!(deserialized, Config::default());
}
}
Loading

0 comments on commit 39db399

Please sign in to comment.