This commit is contained in:
Eugene Pankov 2022-08-15 08:45:03 +02:00
parent 6b805e686f
commit a2bbd336c4
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
15 changed files with 835 additions and 194 deletions

1
Cargo.lock generated
View file

@ -4746,6 +4746,7 @@ dependencies = [
"chrono",
"sea-orm",
"sea-orm-migration",
"serde_json",
"uuid",
]

View file

@ -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<String> {
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<T>() -> Vec<T> {
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()
}

View file

@ -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<String> {
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<T>() -> Vec<T> {
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<String> },
#[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<HashMap<String, String>>,
#[serde(default)]
pub external_host: Option<String>,
}
#[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<String>,
#[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<String>,
#[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<Target>,
@ -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(),
}
}
}

View file

@ -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<String> },
#[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<HashMap<String, String>>,
#[serde(default)]
pub external_host: Option<String>,
}
#[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<String>,
#[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<String>,
#[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),
}

View file

@ -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,
}

View file

@ -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<Mutex<DatabaseConnection>>,
config: Arc<Mutex<WarpgateConfig>>,
}
impl DatabaseConfigProvider {
pub async fn new(
db: &Arc<Mutex<DatabaseConnection>>,
config: &Arc<Mutex<WarpgateConfig>>,
) -> Self {
Self {
db: db.clone(),
config: config.clone(),
}
}
}
#[async_trait]
impl ConfigProvider for DatabaseConfigProvider {
async fn list_users(&mut self) -> Result<Vec<UserSnapshot>, WarpgateError> {
Ok(self
.config
.lock()
.await
.store
.users
.iter()
.map(UserSnapshot::new)
.collect::<Vec<_>>())
}
async fn list_targets(&mut self) -> Result<Vec<TargetConfig>, 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<Vec<TargetConfig>, _> = 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<Option<Box<dyn CredentialPolicy + Sync + Send>>, 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<CredentialKind> =
user.credentials.iter().map(|x| x.kind()).collect();
let default_policy = Box::new(AnySingleCredentialPolicy {
supported_credential_types: supported_credential_types.clone(),
}) as Box<dyn CredentialPolicy + Sync + Send>;
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<dyn CredentialPolicy + Sync + Send>
))
} else {
Ok(Some(default_policy))
}
}
async fn username_for_sso_credential(
&mut self,
client_credential: &AuthCredential,
) -> Result<Option<String>, 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<bool, 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(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<bool, WarpgateError> {
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::Model> = 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<RoleConfig> = 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::<HashSet<_>>();
let intersect = user_roles
.intersection(&target_roles.iter().collect())
.count()
> 0;
Ok(intersect)
}
}

View file

@ -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};

View file

@ -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<Mutex<dyn ConfigProvider + Send + 'static>>;
#[derive(Clone)]
pub struct Services {
@ -16,7 +18,7 @@ pub struct Services {
pub recordings: Arc<Mutex<SessionRecordings>>,
pub config: Arc<Mutex<WarpgateConfig>>,
pub state: Arc<Mutex<State>>,
pub config_provider: Arc<Mutex<dyn ConfigProvider + Send + 'static>>,
pub config_provider: ConfigProviderArc,
pub auth_state_store: Arc<Mutex<AuthStateStore>>,
}
@ -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())));

View file

@ -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<Model> for Role {
fn from(model: Model) -> Self {
Self {
name: model.name,
}
}
}

View file

@ -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<super::Role::Entity> for Entity {
fn to() -> RelationDef {
super::TargetRoleAssignment::Relation::Role.def()
}
fn via() -> Option<RelationDef> {
Some(super::TargetRoleAssignment::Relation::Target.def().rev())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl TryFrom<Model> for Target {
type Error = serde_json::Error;
fn try_from(model: Model) -> Result<Self, Self::Error> {
let options: TargetOptions = serde_json::from_value(model.options)?;
Ok(Self {
name: model.name,
allow_roles: vec![],
options,
})
}
}

View file

@ -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 {}

View file

@ -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;

View file

@ -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"

View file

@ -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),
]
}
}

View file

@ -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<super::role::Entity> for Entity {
fn to() -> RelationDef {
super::target_role_assignment::Relation::Target.def()
}
fn via() -> Option<RelationDef> {
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(())
}
}