From a2bbd336c48f3fcc606054b85ebde750c743f1aa Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Mon, 15 Aug 2022 08:45:03 +0200 Subject: [PATCH] wip --- Cargo.lock | 1 + warpgate-common/src/config/defaults.rs | 71 ++++ warpgate-common/src/config/mod.rs | 213 ++---------- warpgate-common/src/config/target.rs | 122 +++++++ warpgate-common/src/error.rs | 2 + warpgate-core/src/config_providers/db.rs | 311 ++++++++++++++++++ warpgate-core/src/config_providers/mod.rs | 2 + warpgate-core/src/services.rs | 19 +- warpgate-db-entities/src/Role.rs | 27 ++ warpgate-db-entities/src/Target.rs | 66 ++++ .../src/TargetRoleAssignment.rs | 37 +++ warpgate-db-entities/src/lib.rs | 3 + warpgate-db-migrations/Cargo.toml | 1 + warpgate-db-migrations/src/lib.rs | 2 + .../src/m00007_targets_and_roles.rs | 152 +++++++++ 15 files changed, 835 insertions(+), 194 deletions(-) create mode 100644 warpgate-common/src/config/defaults.rs create mode 100644 warpgate-common/src/config/target.rs create mode 100644 warpgate-core/src/config_providers/db.rs create mode 100644 warpgate-db-entities/src/Role.rs create mode 100644 warpgate-db-entities/src/Target.rs create mode 100644 warpgate-db-entities/src/TargetRoleAssignment.rs create mode 100644 warpgate-db-migrations/src/m00007_targets_and_roles.rs diff --git a/Cargo.lock b/Cargo.lock index 8bbebd4..e1641c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4746,6 +4746,7 @@ dependencies = [ "chrono", "sea-orm", "sea-orm-migration", + "serde_json", "uuid", ] diff --git a/warpgate-common/src/config/defaults.rs b/warpgate-common/src/config/defaults.rs new file mode 100644 index 0000000..6299310 --- /dev/null +++ b/warpgate-common/src/config/defaults.rs @@ -0,0 +1,71 @@ +use std::net::ToSocketAddrs; +use std::time::Duration; + +use crate::{ListenEndpoint, Secret}; + +pub(crate) const fn _default_true() -> bool { + true +} + +pub(crate) const fn _default_false() -> bool { + false +} + +pub(crate) const fn _default_ssh_port() -> u16 { + 22 +} + +pub(crate) const fn _default_mysql_port() -> u16 { + 3306 +} + +#[inline] +pub(crate) fn _default_username() -> String { + "root".to_owned() +} + +#[inline] +pub(crate) fn _default_empty_string() -> String { + "".to_owned() +} + +#[inline] +pub(crate) fn _default_recordings_path() -> String { + "./data/recordings".to_owned() +} + +#[inline] +pub(crate) fn _default_database_url() -> Secret { + Secret::new("sqlite:data/db".to_owned()) +} + +#[inline] +pub(crate) fn _default_http_listen() -> ListenEndpoint { + #[allow(clippy::unwrap_used)] + ListenEndpoint("0.0.0.0:8888".to_socket_addrs().unwrap().next().unwrap()) +} + +#[inline] +pub(crate) fn _default_mysql_listen() -> ListenEndpoint { + #[allow(clippy::unwrap_used)] + ListenEndpoint("0.0.0.0:33306".to_socket_addrs().unwrap().next().unwrap()) +} + +#[inline] +pub(crate) fn _default_retention() -> Duration { + Duration::SECOND * 60 * 60 * 24 * 7 +} + +#[inline] +pub(crate) fn _default_empty_vec() -> Vec { + vec![] +} + +pub(crate) fn _default_ssh_listen() -> ListenEndpoint { + #[allow(clippy::unwrap_used)] + ListenEndpoint("0.0.0.0:2222".to_socket_addrs().unwrap().next().unwrap()) +} + +pub(crate) fn _default_ssh_keys_path() -> String { + "./data/keys".to_owned() +} diff --git a/warpgate-common/src/config/mod.rs b/warpgate-common/src/config/mod.rs index 82e6081..81de514 100644 --- a/warpgate-common/src/config/mod.rs +++ b/warpgate-common/src/config/mod.rs @@ -1,10 +1,12 @@ -use std::collections::HashMap; -use std::net::ToSocketAddrs; +mod defaults; +mod target; + use std::path::PathBuf; use std::time::Duration; -use poem_openapi::{Enum, Object, Union}; +use defaults::*; use serde::{Deserialize, Serialize}; +pub use target::*; use url::Url; use warpgate_sso::SsoProviderConfig; @@ -12,179 +14,6 @@ use crate::auth::CredentialKind; use crate::helpers::otp::OtpSecretKey; use crate::{ListenEndpoint, Secret, WarpgateError}; -const fn _default_true() -> bool { - true -} - -const fn _default_false() -> bool { - false -} - -const fn _default_ssh_port() -> u16 { - 22 -} - -const fn _default_mysql_port() -> u16 { - 3306 -} - -#[inline] -fn _default_username() -> String { - "root".to_owned() -} - -#[inline] -fn _default_empty_string() -> String { - "".to_owned() -} - -#[inline] -fn _default_recordings_path() -> String { - "./data/recordings".to_owned() -} - -#[inline] -fn _default_database_url() -> Secret { - Secret::new("sqlite:data/db".to_owned()) -} - -#[inline] -fn _default_http_listen() -> ListenEndpoint { - #[allow(clippy::unwrap_used)] - ListenEndpoint("0.0.0.0:8888".to_socket_addrs().unwrap().next().unwrap()) -} - -#[inline] -fn _default_mysql_listen() -> ListenEndpoint { - #[allow(clippy::unwrap_used)] - ListenEndpoint("0.0.0.0:33306".to_socket_addrs().unwrap().next().unwrap()) -} - -#[inline] -fn _default_retention() -> Duration { - Duration::SECOND * 60 * 60 * 24 * 7 -} - -#[inline] -fn _default_empty_vec() -> Vec { - vec![] -} - -#[derive(Debug, Deserialize, Serialize, Clone, Object)] -pub struct TargetSSHOptions { - pub host: String, - #[serde(default = "_default_ssh_port")] - pub port: u16, - #[serde(default = "_default_username")] - pub username: String, - #[serde(default)] - #[oai(skip)] - pub auth: SSHTargetAuth, -} - -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -#[serde(untagged)] -pub enum SSHTargetAuth { - #[serde(rename = "password")] - Password { password: Secret }, - #[serde(rename = "publickey")] - PublicKey, -} - -impl Default for SSHTargetAuth { - fn default() -> Self { - SSHTargetAuth::PublicKey - } -} - -#[derive(Debug, Deserialize, Serialize, Clone, Object)] -pub struct TargetHTTPOptions { - #[serde(default = "_default_empty_string")] - pub url: String, - - #[serde(default)] - pub tls: Tls, - - #[serde(default)] - pub headers: Option>, - - #[serde(default)] - pub external_host: Option, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Enum, PartialEq, Eq, Default)] -pub enum TlsMode { - #[serde(rename = "disabled")] - Disabled, - #[serde(rename = "preferred")] - #[default] - Preferred, - #[serde(rename = "required")] - Required, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Object)] -pub struct Tls { - #[serde(default)] - pub mode: TlsMode, - - #[serde(default = "_default_true")] - pub verify: bool, -} - -#[allow(clippy::derivable_impls)] -impl Default for Tls { - fn default() -> Self { - Self { - mode: TlsMode::default(), - verify: false, - } - } -} - -#[derive(Debug, Deserialize, Serialize, Clone, Object)] -pub struct TargetMySqlOptions { - #[serde(default = "_default_empty_string")] - pub host: String, - - #[serde(default = "_default_mysql_port")] - pub port: u16, - - #[serde(default = "_default_username")] - pub username: String, - - #[serde(default)] - pub password: Option, - - #[serde(default)] - pub tls: Tls, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Object, Default)] -pub struct TargetWebAdminOptions {} - -#[derive(Debug, Deserialize, Serialize, Clone, Object)] -pub struct Target { - pub name: String, - #[serde(default = "_default_empty_vec")] - pub allow_roles: Vec, - #[serde(flatten)] - pub options: TargetOptions, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Union)] -#[oai(discriminator_name = "kind", one_of)] -pub enum TargetOptions { - #[serde(rename = "ssh")] - Ssh(TargetSSHOptions), - #[serde(rename = "http")] - Http(TargetHTTPOptions), - #[serde(rename = "mysql")] - MySql(TargetMySqlOptions), - #[serde(rename = "web_admin")] - WebAdmin(TargetWebAdminOptions), -} - #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(tag = "type")] pub enum UserAuthCredential { @@ -239,15 +68,6 @@ pub struct Role { pub name: String, } -fn _default_ssh_listen() -> ListenEndpoint { - #[allow(clippy::unwrap_used)] - ListenEndpoint("0.0.0.0:2222".to_socket_addrs().unwrap().next().unwrap()) -} - -fn _default_ssh_keys_path() -> String { - "./data/keys".to_owned() -} - #[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Copy)] pub enum SshHostKeyVerificationMode { #[serde(rename = "prompt")] @@ -373,6 +193,15 @@ impl Default for LogConfig { } } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)] +pub enum ConfigProviderKind { + #[serde(rename = "file")] + #[default] + File, + #[serde(rename = "database")] + Database, +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct WarpgateConfigStore { pub targets: Vec, @@ -402,6 +231,9 @@ pub struct WarpgateConfigStore { #[serde(default)] pub log: LogConfig, + + #[serde(default)] + pub config_provider: ConfigProviderKind, } impl Default for WarpgateConfigStore { @@ -411,13 +243,14 @@ impl Default for WarpgateConfigStore { users: vec![], roles: vec![], sso_providers: vec![], - recordings: RecordingsConfig::default(), + recordings: <_>::default(), external_host: None, database_url: _default_database_url(), - ssh: SSHConfig::default(), - http: HTTPConfig::default(), - mysql: MySQLConfig::default(), - log: LogConfig::default(), + ssh: <_>::default(), + http: <_>::default(), + mysql: <_>::default(), + log: <_>::default(), + config_provider: <_>::default(), } } } diff --git a/warpgate-common/src/config/target.rs b/warpgate-common/src/config/target.rs new file mode 100644 index 0000000..d265eba --- /dev/null +++ b/warpgate-common/src/config/target.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; + +use poem_openapi::{Enum, Object, Union}; +use serde::{Deserialize, Serialize}; + +use super::defaults::*; +use crate::Secret; + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct TargetSSHOptions { + pub host: String, + #[serde(default = "_default_ssh_port")] + pub port: u16, + #[serde(default = "_default_username")] + pub username: String, + #[serde(default)] + #[oai(skip)] + pub auth: SSHTargetAuth, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum SSHTargetAuth { + #[serde(rename = "password")] + Password { password: Secret }, + #[serde(rename = "publickey")] + PublicKey, +} + +impl Default for SSHTargetAuth { + fn default() -> Self { + SSHTargetAuth::PublicKey + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct TargetHTTPOptions { + #[serde(default = "_default_empty_string")] + pub url: String, + + #[serde(default)] + pub tls: Tls, + + #[serde(default)] + pub headers: Option>, + + #[serde(default)] + pub external_host: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Enum, PartialEq, Eq, Default)] +pub enum TlsMode { + #[serde(rename = "disabled")] + Disabled, + #[serde(rename = "preferred")] + #[default] + Preferred, + #[serde(rename = "required")] + Required, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct Tls { + #[serde(default)] + pub mode: TlsMode, + + #[serde(default = "_default_true")] + pub verify: bool, +} + +#[allow(clippy::derivable_impls)] +impl Default for Tls { + fn default() -> Self { + Self { + mode: TlsMode::default(), + verify: false, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct TargetMySqlOptions { + #[serde(default = "_default_empty_string")] + pub host: String, + + #[serde(default = "_default_mysql_port")] + pub port: u16, + + #[serde(default = "_default_username")] + pub username: String, + + #[serde(default)] + pub password: Option, + + #[serde(default)] + pub tls: Tls, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Object, Default)] +pub struct TargetWebAdminOptions {} + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct Target { + pub name: String, + #[serde(default = "_default_empty_vec")] + pub allow_roles: Vec, + #[serde(flatten)] + pub options: TargetOptions, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Union)] +#[oai(discriminator_name = "kind", one_of)] +pub enum TargetOptions { + #[serde(rename = "ssh")] + Ssh(TargetSSHOptions), + #[serde(rename = "http")] + Http(TargetHTTPOptions), + #[serde(rename = "mysql")] + MySql(TargetMySqlOptions), + #[serde(rename = "web_admin")] + WebAdmin(TargetWebAdminOptions), +} diff --git a/warpgate-common/src/error.rs b/warpgate-common/src/error.rs index 1eeb26d..d8b7121 100644 --- a/warpgate-common/src/error.rs +++ b/warpgate-common/src/error.rs @@ -17,6 +17,8 @@ pub enum WarpgateError { UserNotFound, #[error("failed to parse url: {0}")] UrlParse(#[from] url::ParseError), + #[error("deserialization failed: {0}")] + DeserializeJson(#[from] serde_json::Error), #[error("external_url config option is not set")] ExternalHostNotSet, } diff --git a/warpgate-core/src/config_providers/db.rs b/warpgate-core/src/config_providers/db.rs new file mode 100644 index 0000000..5bce517 --- /dev/null +++ b/warpgate-core/src/config_providers/db.rs @@ -0,0 +1,311 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use async_trait::async_trait; +use data_encoding::BASE64; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, QueryOrder}; +use tokio::sync::Mutex; +use tracing::*; +use warpgate_common::auth::{ + AllCredentialsPolicy, AnySingleCredentialPolicy, AuthCredential, CredentialKind, + CredentialPolicy, PerProtocolCredentialPolicy, +}; +use warpgate_common::helpers::hash::verify_password_hash; +use warpgate_common::helpers::otp::verify_totp; +use warpgate_common::{ + Role as RoleConfig, Target as TargetConfig, User, UserAuthCredential, WarpgateConfig, + WarpgateError, +}; +use warpgate_db_entities::{Role, Target}; + +use super::ConfigProvider; +use crate::UserSnapshot; + +pub struct DatabaseConfigProvider { + db: Arc>, + config: Arc>, +} + +impl DatabaseConfigProvider { + pub async fn new( + db: &Arc>, + config: &Arc>, + ) -> Self { + Self { + db: db.clone(), + config: config.clone(), + } + } +} + +#[async_trait] +impl ConfigProvider for DatabaseConfigProvider { + async fn list_users(&mut self) -> Result, WarpgateError> { + Ok(self + .config + .lock() + .await + .store + .users + .iter() + .map(UserSnapshot::new) + .collect::>()) + } + + async fn list_targets(&mut self) -> Result, WarpgateError> { + let db = self.db.lock().await; + + let targets_and_roles = Target::Entity::find() + .order_by_desc(Target::Column::Name) + .find_with_related(Role::Entity) + .all(&*db) + .await?; + + let targets: Result, _> = targets_and_roles + .into_iter() + .map(|(t, r)| { + t.try_into().map(|mut x: TargetConfig| { + x.allow_roles = r.into_iter().map(|x| x.name.clone()).collect(); + x + }) + }) + .collect(); + + Ok(targets?) + } + + async fn get_credential_policy( + &mut self, + username: &str, + ) -> Result>, WarpgateError> { + let user = { + self.config + .lock() + .await + .store + .users + .iter() + .find(|x| x.username == username) + .map(User::to_owned) + }; + let Some(user) = user else { + error!("Selected user not found: {}", username); + return Ok(None); + }; + + let supported_credential_types: HashSet = + user.credentials.iter().map(|x| x.kind()).collect(); + let default_policy = Box::new(AnySingleCredentialPolicy { + supported_credential_types: supported_credential_types.clone(), + }) as Box; + + if let Some(req) = user.require { + let mut policy = PerProtocolCredentialPolicy { + default: default_policy, + protocols: HashMap::new(), + }; + + if let Some(p) = req.http { + policy.protocols.insert( + "HTTP", + Box::new(AllCredentialsPolicy { + supported_credential_types: supported_credential_types.clone(), + required_credential_types: p.into_iter().collect(), + }), + ); + } + if let Some(p) = req.mysql { + policy.protocols.insert( + "MySQL", + Box::new(AllCredentialsPolicy { + supported_credential_types: supported_credential_types.clone(), + required_credential_types: p.into_iter().collect(), + }), + ); + } + if let Some(p) = req.ssh { + policy.protocols.insert( + "SSH", + Box::new(AllCredentialsPolicy { + supported_credential_types, + required_credential_types: p.into_iter().collect(), + }), + ); + } + + Ok(Some( + Box::new(policy) as Box + )) + } else { + Ok(Some(default_policy)) + } + } + + async fn username_for_sso_credential( + &mut self, + client_credential: &AuthCredential, + ) -> Result, WarpgateError> { + let AuthCredential::Sso { provider: client_provider, email : client_email} = client_credential else { + return Ok(None); + }; + + Ok(self + .config + .lock() + .await + .store + .users + .iter() + .find(|x| { + for cred in x.credentials.iter() { + if let UserAuthCredential::Sso { provider, email } = cred { + if provider.as_ref().unwrap_or(client_provider) == client_provider + && email == client_email + { + return true; + } + } + } + false + }) + .map(|x| x.username.clone())) + } + + async fn validate_credential( + &mut self, + username: &str, + client_credential: &AuthCredential, + ) -> Result { + let user = { + self.config + .lock() + .await + .store + .users + .iter() + .find(|x| x.username == username) + .map(User::to_owned) + }; + let Some(user) = user else { + error!("Selected user not found: {}", username); + return Ok(false); + }; + + match client_credential { + AuthCredential::PublicKey { + kind, + public_key_bytes, + } => { + let base64_bytes = BASE64.encode(public_key_bytes); + + let client_key = format!("{} {}", kind, base64_bytes); + debug!(username = &user.username[..], "Client key: {}", client_key); + + return Ok(user.credentials.iter().any(|credential| match credential { + UserAuthCredential::PublicKey { key: ref user_key } => { + &client_key == user_key.expose_secret() + } + _ => false, + })); + } + AuthCredential::Password(client_password) => { + return Ok(user.credentials.iter().any(|credential| match credential { + UserAuthCredential::Password { + hash: ref user_password_hash, + } => verify_password_hash( + client_password.expose_secret(), + user_password_hash.expose_secret(), + ) + .unwrap_or_else(|e| { + error!( + username = &user.username[..], + "Error verifying password hash: {}", e + ); + false + }), + _ => false, + })) + } + AuthCredential::Otp(client_otp) => { + return Ok(user.credentials.iter().any(|credential| match credential { + UserAuthCredential::Totp { + key: ref user_otp_key, + } => verify_totp(client_otp.expose_secret(), user_otp_key), + _ => false, + })) + } + AuthCredential::Sso { + provider: client_provider, + email: client_email, + } => { + for credential in user.credentials.iter() { + if let UserAuthCredential::Sso { + ref provider, + ref email, + } = credential + { + if provider.as_ref().unwrap_or(client_provider) == client_provider { + return Ok(email == client_email); + } + } + } + return Ok(false); + } + _ => return Err(WarpgateError::InvalidCredentialType), + } + } + + async fn authorize_target( + &mut self, + username: &str, + target_name: &str, + ) -> Result { + let config = self.config.lock().await; + let user = config + .store + .users + .iter() + .find(|x| x.username == username) + .map(User::to_owned); + + let db = self.db.lock().await; + + let target_model: Option = Target::Entity::find() + .order_by_desc(Target::Column::Name) + .filter(Target::Column::Name.eq(target_name)) + .one(&*db) + .await?; + + let Some(user) = user else { + error!("Selected user not found: {}", username); + return Ok(false); + }; + + let Some(target_model) = target_model else { + warn!("Selected target not found: {}", target_name); + return Ok(false); + }; + + let target_roles: HashSet = target_model + .find_related(Role::Entity) + .all(&*db) + .await? + .into_iter() + .map(Into::into) + .collect(); + + let user_roles = user + .roles + .iter() + .map(|x| config.store.roles.iter().find(|y| &y.name == x)) + .filter_map(|x| x.to_owned()) + .collect::>(); + + let intersect = user_roles + .intersection(&target_roles.iter().collect()) + .count() + > 0; + + Ok(intersect) + } +} diff --git a/warpgate-core/src/config_providers/mod.rs b/warpgate-core/src/config_providers/mod.rs index dd1ffe9..0d17b46 100644 --- a/warpgate-core/src/config_providers/mod.rs +++ b/warpgate-core/src/config_providers/mod.rs @@ -1,7 +1,9 @@ +mod db; mod file; use std::sync::Arc; use async_trait::async_trait; +pub use db::DatabaseConfigProvider; pub use file::FileConfigProvider; use sea_orm::ActiveValue::Set; use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; diff --git a/warpgate-core/src/services.rs b/warpgate-core/src/services.rs index 8935c44..5d6a429 100644 --- a/warpgate-core/src/services.rs +++ b/warpgate-core/src/services.rs @@ -4,11 +4,13 @@ use std::time::Duration; use anyhow::Result; use sea_orm::DatabaseConnection; use tokio::sync::Mutex; -use warpgate_common::WarpgateConfig; +use warpgate_common::{ConfigProviderKind, WarpgateConfig}; use crate::db::{connect_to_db, sanitize_db}; use crate::recordings::SessionRecordings; -use crate::{AuthStateStore, ConfigProvider, FileConfigProvider, State}; +use crate::{AuthStateStore, ConfigProvider, FileConfigProvider, State, DatabaseConfigProvider}; + +type ConfigProviderArc = Arc>; #[derive(Clone)] pub struct Services { @@ -16,7 +18,7 @@ pub struct Services { pub recordings: Arc>, pub config: Arc>, pub state: Arc>, - pub config_provider: Arc>, + pub config_provider: ConfigProviderArc, pub auth_state_store: Arc>, } @@ -29,8 +31,17 @@ impl Services { let recordings = SessionRecordings::new(db.clone(), &config)?; let recordings = Arc::new(Mutex::new(recordings)); + let provider = config.store.config_provider.clone(); let config = Arc::new(Mutex::new(config)); - let config_provider = Arc::new(Mutex::new(FileConfigProvider::new(&config).await)); + + let config_provider = match provider { + ConfigProviderKind::File => { + Arc::new(Mutex::new(FileConfigProvider::new(&config).await)) as ConfigProviderArc + } + ConfigProviderKind::Database => { + Arc::new(Mutex::new(DatabaseConfigProvider::new(&db, &config).await)) as ConfigProviderArc + } + }; let auth_state_store = Arc::new(Mutex::new(AuthStateStore::new(config_provider.clone()))); diff --git a/warpgate-db-entities/src/Role.rs b/warpgate-db-entities/src/Role.rs new file mode 100644 index 0000000..d612122 --- /dev/null +++ b/warpgate-db-entities/src/Role.rs @@ -0,0 +1,27 @@ +use poem_openapi::Object; +use sea_orm::entity::prelude::*; +use serde::Serialize; +use uuid::Uuid; +use warpgate_common::Role; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] +#[sea_orm(table_name = "roles")] +#[oai(rename = "Role")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +impl From for Role { + fn from(model: Model) -> Self { + Self { + name: model.name, + } + } +} diff --git a/warpgate-db-entities/src/Target.rs b/warpgate-db-entities/src/Target.rs new file mode 100644 index 0000000..b95c936 --- /dev/null +++ b/warpgate-db-entities/src/Target.rs @@ -0,0 +1,66 @@ +use poem_openapi::{Enum, Object}; +use sea_orm::entity::prelude::*; +use serde::Serialize; +use uuid::Uuid; +use warpgate_common::{Target, TargetOptions}; + +#[derive(Debug, PartialEq, Eq, Serialize, Clone, Enum, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(Some(16))")] +pub enum TargetKind { + #[sea_orm(string_value = "http")] + Http, + #[sea_orm(string_value = "mysql")] + MySql, + #[sea_orm(string_value = "ssh")] + Ssh, + #[sea_orm(string_value = "web_admin")] + WebAdmin, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone, Enum, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(Some(16))")] +pub enum SshAuthKind { + #[sea_orm(string_value = "password")] + Password, + #[sea_orm(string_value = "publickey")] + PublicKey, +} + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] +#[sea_orm(table_name = "targets")] +#[oai(rename = "Target")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub kind: TargetKind, + pub options: serde_json::Value, +} + +impl Related for Entity { + fn to() -> RelationDef { + super::TargetRoleAssignment::Relation::Role.def() + } + + fn via() -> Option { + Some(super::TargetRoleAssignment::Relation::Target.def().rev()) + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +impl TryFrom for Target { + type Error = serde_json::Error; + + fn try_from(model: Model) -> Result { + let options: TargetOptions = serde_json::from_value(model.options)?; + Ok(Self { + name: model.name, + allow_roles: vec![], + options, + }) + } +} diff --git a/warpgate-db-entities/src/TargetRoleAssignment.rs b/warpgate-db-entities/src/TargetRoleAssignment.rs new file mode 100644 index 0000000..0e33424 --- /dev/null +++ b/warpgate-db-entities/src/TargetRoleAssignment.rs @@ -0,0 +1,37 @@ +use poem_openapi::Object; +use sea_orm::entity::prelude::*; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] +#[sea_orm(table_name = "target_roles")] +#[oai(rename = "Target")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: u64, + pub target_id: Uuid, + pub role_id: Uuid, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Target, + Role, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Target => Entity::belongs_to(super::Target::Entity) + .from(Column::TargetId) + .to(super::Target::Column::Id) + .into(), + Self::Role => Entity::belongs_to(super::Role::Entity) + .from(Column::RoleId) + .to(super::Role::Column::Id) + .into(), + } + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/warpgate-db-entities/src/lib.rs b/warpgate-db-entities/src/lib.rs index f98c6f5..cf7bf48 100644 --- a/warpgate-db-entities/src/lib.rs +++ b/warpgate-db-entities/src/lib.rs @@ -3,5 +3,8 @@ pub mod KnownHost; pub mod LogEntry; pub mod Recording; +pub mod Role; pub mod Session; +pub mod Target; +pub mod TargetRoleAssignment; pub mod Ticket; diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 6d44e68..2529d89 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -13,3 +13,4 @@ chrono = "0.4" sea-orm = {version = "^0.9", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros", "with-chrono", "with-uuid", "with-json"], default-features = false} sea-orm-migration = {version = "^0.9", default-features = false} uuid = {version = "1.0", features = ["v4", "serde"]} +serde_json = "1.0" diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index b5a2904..a968834 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -8,6 +8,7 @@ mod m00003_create_recording; mod m00004_create_known_host; mod m00005_create_log_entry; mod m00006_add_session_protocol; +mod m00007_targets_and_roles; pub struct Migrator; @@ -21,6 +22,7 @@ impl MigratorTrait for Migrator { Box::new(m00004_create_known_host::Migration), Box::new(m00005_create_log_entry::Migration), Box::new(m00006_add_session_protocol::Migration), + Box::new(m00007_targets_and_roles::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00007_targets_and_roles.rs b/warpgate-db-migrations/src/m00007_targets_and_roles.rs new file mode 100644 index 0000000..0a6d3b1 --- /dev/null +++ b/warpgate-db-migrations/src/m00007_targets_and_roles.rs @@ -0,0 +1,152 @@ +use sea_orm::Schema; +use sea_orm_migration::prelude::*; + +mod role { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "roles")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +mod target { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + #[derive(Debug, PartialEq, Eq, Clone, EnumIter, DeriveActiveEnum)] + #[sea_orm(rs_type = "String", db_type = "String(Some(16))")] + pub enum TargetKind { + #[sea_orm(string_value = "http")] + Http, + #[sea_orm(string_value = "mysql")] + MySql, + #[sea_orm(string_value = "ssh")] + Ssh, + #[sea_orm(string_value = "web_admin")] + WebAdmin, + } + + #[derive(Debug, PartialEq, Eq, Clone, EnumIter, DeriveActiveEnum)] + #[sea_orm(rs_type = "String", db_type = "String(Some(16))")] + pub enum SshAuthKind { + #[sea_orm(string_value = "password")] + Password, + #[sea_orm(string_value = "publickey")] + PublicKey, + } + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "targets")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub kind: TargetKind, + pub options: serde_json::Value, + } + + impl Related for Entity { + fn to() -> RelationDef { + super::target_role_assignment::Relation::Target.def() + } + + fn via() -> Option { + Some(super::target_role_assignment::Relation::Role.def().rev()) + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +mod target_role_assignment { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "target_roles")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: u64, + pub target_id: Uuid, + pub role_id: Uuid, + } + + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + Target, + Role, + } + + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Target => Entity::belongs_to(super::target::Entity) + .from(Column::TargetId) + .to(super::target::Column::Id) + .into(), + Self::Role => Entity::belongs_to(super::role::Entity) + .from(Column::RoleId) + .to(super::role::Column::Id) + .into(), + } + } + } + + impl ActiveModelBehavior for ActiveModel {} +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00007_targets_and_roles" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let builder = manager.get_database_backend(); + let schema = Schema::new(builder); + manager + .create_table(schema.create_table_from_entity(role::Entity)) + .await?; + manager + .create_table(schema.create_table_from_entity(target::Entity)) + .await?; + manager + .create_table(schema.create_table_from_entity(target_role_assignment::Entity)) + .await?; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(target_role_assignment::Entity) + .to_owned(), + ) + .await?; + manager + .drop_table(Table::drop().table(target::Entity).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(role::Entity).to_owned()) + .await?; + Ok(()) + } +}