7 changed files with 953 additions and 13 deletions
@ -0,0 +1 @@ |
|||
run_in_transaction = false |
@ -0,0 +1,281 @@ |
|||
CREATE TABLE attachments ( |
|||
id text NOT NULL PRIMARY KEY, |
|||
cipher_uuid character varying(40) NOT NULL, |
|||
file_name text NOT NULL, |
|||
file_size bigint NOT NULL, |
|||
akey text |
|||
); |
|||
|
|||
CREATE TABLE auth_requests ( |
|||
uuid character(36) NOT NULL PRIMARY KEY, |
|||
user_uuid character(36) NOT NULL, |
|||
organization_uuid character(36), |
|||
request_device_identifier character(36) NOT NULL, |
|||
device_type integer NOT NULL, |
|||
request_ip text NOT NULL, |
|||
response_device_id character(36), |
|||
access_code text NOT NULL, |
|||
public_key text NOT NULL, |
|||
enc_key text, |
|||
master_password_hash text, |
|||
approved boolean, |
|||
creation_date timestamp without time zone NOT NULL, |
|||
response_date timestamp without time zone, |
|||
authentication_date timestamp without time zone |
|||
); |
|||
|
|||
CREATE TABLE ciphers ( |
|||
uuid character varying(40) NOT NULL PRIMARY KEY, |
|||
created_at timestamp without time zone NOT NULL, |
|||
updated_at timestamp without time zone NOT NULL, |
|||
user_uuid character varying(40), |
|||
organization_uuid character varying(40), |
|||
atype integer NOT NULL, |
|||
name text NOT NULL, |
|||
notes text, |
|||
fields text, |
|||
data text NOT NULL, |
|||
password_history text, |
|||
deleted_at timestamp without time zone, |
|||
reprompt integer, |
|||
key text |
|||
); |
|||
|
|||
CREATE TABLE ciphers_collections ( |
|||
cipher_uuid character varying(40) NOT NULL, |
|||
collection_uuid character varying(40) NOT NULL, |
|||
PRIMARY KEY (cipher_uuid, collection_uuid) |
|||
); |
|||
|
|||
CREATE TABLE collections ( |
|||
uuid character varying(40) NOT NULL PRIMARY KEY, |
|||
org_uuid character varying(40) NOT NULL, |
|||
name text NOT NULL, |
|||
external_id text |
|||
); |
|||
|
|||
CREATE TABLE collections_groups ( |
|||
collections_uuid character varying(40) NOT NULL, |
|||
groups_uuid character(36) NOT NULL, |
|||
read_only boolean NOT NULL, |
|||
hide_passwords boolean NOT NULL, |
|||
PRIMARY KEY (collections_uuid, groups_uuid) |
|||
); |
|||
|
|||
CREATE TABLE devices ( |
|||
uuid character varying(40) NOT NULL, |
|||
created_at timestamp without time zone NOT NULL, |
|||
updated_at timestamp without time zone NOT NULL, |
|||
user_uuid character varying(40) NOT NULL, |
|||
name text NOT NULL, |
|||
atype integer NOT NULL, |
|||
push_token text, |
|||
refresh_token text NOT NULL, |
|||
twofactor_remember text, |
|||
push_uuid text, |
|||
PRIMARY KEY (uuid, user_uuid) |
|||
); |
|||
|
|||
CREATE TABLE emergency_access ( |
|||
uuid character(36) NOT NULL PRIMARY KEY, |
|||
grantor_uuid character(36), |
|||
grantee_uuid character(36), |
|||
email character varying(255), |
|||
key_encrypted text, |
|||
atype integer NOT NULL, |
|||
status integer NOT NULL, |
|||
wait_time_days integer NOT NULL, |
|||
recovery_initiated_at timestamp without time zone, |
|||
last_notification_at timestamp without time zone, |
|||
updated_at timestamp without time zone NOT NULL, |
|||
created_at timestamp without time zone NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE event ( |
|||
uuid character(36) NOT NULL PRIMARY KEY, |
|||
event_type integer NOT NULL, |
|||
user_uuid character(36), |
|||
org_uuid character(36), |
|||
cipher_uuid character(36), |
|||
collection_uuid character(36), |
|||
group_uuid character(36), |
|||
org_user_uuid character(36), |
|||
act_user_uuid character(36), |
|||
device_type integer, |
|||
ip_address text, |
|||
event_date timestamp without time zone NOT NULL, |
|||
policy_uuid character(36), |
|||
provider_uuid character(36), |
|||
provider_user_uuid character(36), |
|||
provider_org_uuid character(36) |
|||
); |
|||
|
|||
CREATE TABLE favorites ( |
|||
user_uuid character varying(40) NOT NULL, |
|||
cipher_uuid character varying(40) NOT NULL, |
|||
PRIMARY KEY (user_uuid, cipher_uuid) |
|||
); |
|||
|
|||
CREATE TABLE folders ( |
|||
uuid character varying(40) NOT NULL PRIMARY KEY, |
|||
created_at timestamp without time zone NOT NULL, |
|||
updated_at timestamp without time zone NOT NULL, |
|||
user_uuid character varying(40) NOT NULL, |
|||
name text NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE folders_ciphers ( |
|||
cipher_uuid character varying(40) NOT NULL, |
|||
folder_uuid character varying(40) NOT NULL, |
|||
PRIMARY KEY (cipher_uuid, folder_uuid) |
|||
); |
|||
|
|||
CREATE TABLE groups ( |
|||
uuid character(36) NOT NULL PRIMARY KEY, |
|||
organizations_uuid character varying(40) NOT NULL, |
|||
name character varying(100) NOT NULL, |
|||
access_all boolean NOT NULL, |
|||
external_id character varying(300), |
|||
creation_date timestamp without time zone NOT NULL, |
|||
revision_date timestamp without time zone NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE groups_users ( |
|||
groups_uuid character(36) NOT NULL, |
|||
users_organizations_uuid character varying(36) NOT NULL, |
|||
PRIMARY KEY (groups_uuid, users_organizations_uuid) |
|||
); |
|||
|
|||
CREATE TABLE invitations ( |
|||
email text NOT NULL PRIMARY KEY |
|||
); |
|||
|
|||
CREATE TABLE org_policies ( |
|||
uuid character(36) NOT NULL PRIMARY KEY, |
|||
org_uuid character(36) NOT NULL, |
|||
atype integer NOT NULL, |
|||
enabled boolean NOT NULL, |
|||
data text NOT NULL, |
|||
UNIQUE (org_uuid, atype) |
|||
); |
|||
|
|||
CREATE TABLE organization_api_key ( |
|||
uuid character(36) NOT NULL, |
|||
org_uuid character(36) NOT NULL, |
|||
atype integer NOT NULL, |
|||
api_key character varying(255), |
|||
revision_date timestamp without time zone NOT NULL, |
|||
PRIMARY KEY (uuid, org_uuid) |
|||
); |
|||
|
|||
CREATE TABLE organizations ( |
|||
uuid character varying(40) NOT NULL PRIMARY KEY, |
|||
name text NOT NULL, |
|||
billing_email text NOT NULL, |
|||
private_key text, |
|||
public_key text |
|||
); |
|||
|
|||
CREATE TABLE sends ( |
|||
uuid character(36) NOT NULL PRIMARY KEY, |
|||
user_uuid character(36), |
|||
organization_uuid character(36), |
|||
name text NOT NULL, |
|||
notes text, |
|||
atype integer NOT NULL, |
|||
data text NOT NULL, |
|||
akey text NOT NULL, |
|||
password_hash bytea, |
|||
password_salt bytea, |
|||
password_iter integer, |
|||
max_access_count integer, |
|||
access_count integer NOT NULL, |
|||
creation_date timestamp without time zone NOT NULL, |
|||
revision_date timestamp without time zone NOT NULL, |
|||
expiration_date timestamp without time zone, |
|||
deletion_date timestamp without time zone NOT NULL, |
|||
disabled boolean NOT NULL, |
|||
hide_email boolean |
|||
); |
|||
|
|||
CREATE TABLE twofactor ( |
|||
uuid character varying(40) NOT NULL PRIMARY KEY, |
|||
user_uuid character varying(40) NOT NULL, |
|||
atype integer NOT NULL, |
|||
enabled boolean NOT NULL, |
|||
data text NOT NULL, |
|||
last_used bigint DEFAULT 0 NOT NULL, |
|||
UNIQUE (user_uuid, atype) |
|||
); |
|||
|
|||
CREATE TABLE twofactor_duo_ctx ( |
|||
state character varying(64) NOT NULL PRIMARY KEY, |
|||
user_email character varying(255) NOT NULL, |
|||
nonce character varying(64) NOT NULL, |
|||
exp bigint NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE twofactor_incomplete ( |
|||
user_uuid character varying(40) NOT NULL, |
|||
device_uuid character varying(40) NOT NULL, |
|||
device_name text NOT NULL, |
|||
login_time timestamp without time zone NOT NULL, |
|||
ip_address text NOT NULL, |
|||
device_type integer DEFAULT 14 NOT NULL, |
|||
PRIMARY KEY (user_uuid, device_uuid) |
|||
); |
|||
|
|||
CREATE TABLE users ( |
|||
uuid character varying(40) NOT NULL PRIMARY KEY, |
|||
created_at timestamp without time zone NOT NULL, |
|||
updated_at timestamp without time zone NOT NULL, |
|||
email text NOT NULL UNIQUE, |
|||
name text NOT NULL, |
|||
password_hash bytea NOT NULL, |
|||
salt bytea NOT NULL, |
|||
password_iterations integer NOT NULL, |
|||
password_hint text, |
|||
akey text NOT NULL, |
|||
private_key text, |
|||
public_key text, |
|||
totp_secret text, |
|||
totp_recover text, |
|||
security_stamp text NOT NULL, |
|||
equivalent_domains text NOT NULL, |
|||
excluded_globals text NOT NULL, |
|||
client_kdf_type integer DEFAULT 0 NOT NULL, |
|||
client_kdf_iter integer DEFAULT 100000 NOT NULL, |
|||
verified_at timestamp without time zone, |
|||
last_verifying_at timestamp without time zone, |
|||
login_verify_count integer DEFAULT 0 NOT NULL, |
|||
email_new character varying(255) DEFAULT NULL::character varying, |
|||
email_new_token character varying(16) DEFAULT NULL::character varying, |
|||
enabled boolean DEFAULT true NOT NULL, |
|||
stamp_exception text, |
|||
api_key text, |
|||
avatar_color text, |
|||
client_kdf_memory integer, |
|||
client_kdf_parallelism integer, |
|||
external_id text |
|||
); |
|||
|
|||
CREATE TABLE users_collections ( |
|||
user_uuid character varying(40) NOT NULL, |
|||
collection_uuid character varying(40) NOT NULL, |
|||
read_only boolean DEFAULT false NOT NULL, |
|||
hide_passwords boolean DEFAULT false NOT NULL, |
|||
PRIMARY KEY (user_uuid, collection_uuid) |
|||
); |
|||
|
|||
CREATE TABLE users_organizations ( |
|||
uuid character varying(40) NOT NULL PRIMARY KEY, |
|||
user_uuid character varying(40) NOT NULL, |
|||
org_uuid character varying(40) NOT NULL, |
|||
access_all boolean NOT NULL, |
|||
akey text NOT NULL, |
|||
status integer NOT NULL, |
|||
atype integer NOT NULL, |
|||
reset_password_key text, |
|||
external_id text, |
|||
UNIQUE (user_uuid, org_uuid) |
|||
); |
@ -0,0 +1,184 @@ |
|||
use std::sync::RwLock; |
|||
|
|||
use diesel::{ |
|||
r2d2::{ManageConnection, R2D2Connection}, |
|||
ConnectionError, |
|||
ConnectionResult, |
|||
}; |
|||
use url::Url; |
|||
|
|||
#[derive(Debug)] |
|||
pub struct ConnectionManager<T> { |
|||
inner: RwLock<diesel::r2d2::ConnectionManager<T>>, |
|||
#[cfg(dsql)] |
|||
dsql_url: Option<String>, |
|||
} |
|||
|
|||
impl<T> ConnectionManager<T> { |
|||
/// Returns a new connection manager,
|
|||
/// which establishes connections to the given database URL.
|
|||
pub fn new<S: Into<String>>(database_url: S) -> Self { |
|||
let database_url = database_url.into(); |
|||
|
|||
Self { |
|||
inner: RwLock::new(diesel::r2d2::ConnectionManager::new(&database_url)), |
|||
#[cfg(dsql)] |
|||
dsql_url: if database_url.starts_with("dsql:") { |
|||
Some(database_url) |
|||
} else { |
|||
None |
|||
}, |
|||
} |
|||
} |
|||
} |
|||
|
|||
impl<T> ManageConnection for ConnectionManager<T> |
|||
where |
|||
T: R2D2Connection + Send + 'static, |
|||
{ |
|||
type Connection = T; |
|||
type Error = diesel::r2d2::Error; |
|||
|
|||
fn connect(&self) -> Result<T, Self::Error> { |
|||
#[cfg(dsql)] |
|||
if let Some(dsql_url) = &self.dsql_url { |
|||
let url = psql_url(dsql_url).map_err(|e| Self::Error::ConnectionError(e))?; |
|||
self.inner.write().expect("Failed to lock inner connection manager to set DSQL connection URL").update_database_url(&url); |
|||
} |
|||
|
|||
self.inner.read().expect("Failed to lock inner connection manager to connect").connect() |
|||
} |
|||
|
|||
fn is_valid(&self, conn: &mut T) -> Result<(), Self::Error> { |
|||
self.inner.read().expect("Failed to lock inner connection manager to check validity").is_valid(conn) |
|||
} |
|||
|
|||
fn has_broken(&self, conn: &mut T) -> bool { |
|||
self.inner.read().expect("Failed to lock inner connection manager to check if has broken").has_broken(conn) |
|||
} |
|||
} |
|||
|
|||
// Cache the AWS SDK config, as recommended by the AWS SDK documentation. The
|
|||
// initial load is async, so we spawn a thread to load it and then join it to
|
|||
// get the result in a blocking fashion.
|
|||
static AWS_SDK_CONFIG: std::sync::LazyLock<ConnectionResult<aws_config::SdkConfig>> = std::sync::LazyLock::new(|| { |
|||
std::thread::spawn(|| { |
|||
let rt = tokio::runtime::Builder::new_current_thread() |
|||
.enable_all() |
|||
.build()?; |
|||
|
|||
std::io::Result::Ok(rt.block_on(aws_config::load_defaults(aws_config::BehaviorVersion::latest()))) |
|||
}) |
|||
.join() |
|||
.map_err(|e| ConnectionError::BadConnection(format!("Failed to load AWS config for DSQL connection: {e:#?}")))? |
|||
.map_err(|e| ConnectionError::BadConnection(format!("Failed to load AWS config for DSQL connection: {e}"))) |
|||
}); |
|||
|
|||
// Generate a Postgres libpq connection string. The input connection string has
|
|||
// the following format:
|
|||
//
|
|||
// dsql://<dsql-id>.dsql.<aws-region>.on.aws
|
|||
//
|
|||
// The generated connection string will have the form:
|
|||
//
|
|||
// postgresql://<dsql-id>.dsql.<aws-region>.on.aws/postgres?sslmode=require&user=admin&password=<auth-token>
|
|||
//
|
|||
// The auth token is a temporary token generated by the AWS SDK for DSQL. It is
|
|||
// valid for up to 15 minutes. We cache the last-generated token for each unique
|
|||
// DSQL connection URL, and reuse it if it is less than 14 minutes old.
|
|||
pub(crate) fn psql_url(url: &str) -> Result<String, ConnectionError> { |
|||
use std::{ |
|||
collections::HashMap, |
|||
sync::{Arc, LazyLock, Mutex}, |
|||
time::Duration, |
|||
}; |
|||
|
|||
struct PsqlUrl { |
|||
timestamp: std::time::Instant, |
|||
url: String, |
|||
} |
|||
|
|||
static PSQL_URLS: LazyLock<Mutex<HashMap<String, Arc<Mutex<Option<PsqlUrl>>>>>> = LazyLock::new(|| Mutex::new(HashMap::new())); |
|||
|
|||
let mut psql_urls = PSQL_URLS.lock().map_err(|e| ConnectionError::BadConnection(format!("Failed to lock PSQL URLs: {e}")))?; |
|||
|
|||
let psql_url_lock = if let Some(existing_psql_url_lock) = psql_urls.get(url) { |
|||
existing_psql_url_lock.clone() |
|||
} else { |
|||
let psql_url_lock = Arc::new(Mutex::new(None)); |
|||
psql_urls.insert(url.to_string(), psql_url_lock.clone()); |
|||
psql_url_lock |
|||
}; |
|||
|
|||
let mut psql_url_lock_guard = psql_url_lock.lock().map_err(|e| ConnectionError::BadConnection(format!("Failed to lock PSQL url: {e}")))?; |
|||
|
|||
drop(psql_urls); |
|||
|
|||
if let Some(ref psql_url) = *psql_url_lock_guard { |
|||
if psql_url.timestamp.elapsed() < Duration::from_secs(14 * 60) { |
|||
debug!("Reusing DSQL auth token for connection '{url}'"); |
|||
return Ok(psql_url.url.clone()); |
|||
} |
|||
|
|||
info!("Refreshing DSQL auth token for connection '{url}'"); |
|||
} else { |
|||
info!("Generating new DSQL auth token for connection '{url}'"); |
|||
} |
|||
|
|||
// This would be so much easier if ConnectionError implemented Clone.
|
|||
let sdk_config = match *AWS_SDK_CONFIG { |
|||
Ok(ref sdk_config) => sdk_config.clone(), |
|||
Err(ConnectionError::BadConnection(ref e)) => return Err(ConnectionError::BadConnection(e.to_owned())), |
|||
Err(ref e) => unreachable!("Unexpected error loading AWS SDK config: {e}"), |
|||
}; |
|||
|
|||
let mut psql_url = Url::parse(url).map_err(|e| { |
|||
ConnectionError::InvalidConnectionUrl(e.to_string()) |
|||
})?; |
|||
|
|||
let host = psql_url.host_str().ok_or(ConnectionError::InvalidConnectionUrl("Missing hostname in connection URL".to_string()))?.to_string(); |
|||
|
|||
static DSQL_REGION_FROM_HOST_RE: LazyLock<regex::Regex> = LazyLock::new(|| { |
|||
regex::Regex::new(r"^[a-z0-9]+\.dsql\.(?P<region>[a-z0-9-]+)\.on\.aws$").expect("Failed to compile DSQL region regex") |
|||
}); |
|||
|
|||
let region = (*DSQL_REGION_FROM_HOST_RE).captures(&host).ok_or(ConnectionError::InvalidConnectionUrl("Failed to find AWS region in DSQL hostname".to_string()))? |
|||
.name("region") |
|||
.ok_or(ConnectionError::InvalidConnectionUrl("Failed to find AWS region in DSQL hostname".to_string()))? |
|||
.as_str() |
|||
.to_string(); |
|||
|
|||
let region = aws_config::Region::new(region); |
|||
|
|||
let auth_config = aws_sdk_dsql::auth_token::Config::builder() |
|||
.hostname(host) |
|||
.region(region) |
|||
.build() |
|||
.map_err(|e| ConnectionError::BadConnection(format!("Failed to build AWS auth token signer config: {e}")))?; |
|||
|
|||
let signer = aws_sdk_dsql::auth_token::AuthTokenGenerator::new(auth_config); |
|||
|
|||
let now = std::time::Instant::now(); |
|||
|
|||
let auth_token = std::thread::spawn(move || { |
|||
let rt = tokio::runtime::Builder::new_current_thread() |
|||
.enable_all() |
|||
.build()?; |
|||
|
|||
rt.block_on(signer.db_connect_admin_auth_token(&sdk_config)) |
|||
}) |
|||
.join() |
|||
.map_err(|e| ConnectionError::BadConnection(format!("Failed to generate DSQL auth token: {e:#?}")))? |
|||
.map_err(|e| ConnectionError::BadConnection(format!("Failed to generate DSQL auth token: {e}")))?; |
|||
|
|||
psql_url.set_scheme("postgresql").expect("Failed to set 'postgresql' as scheme for DSQL connection URL"); |
|||
psql_url.set_path("postgres"); |
|||
psql_url.query_pairs_mut() |
|||
.append_pair("sslmode", "require") |
|||
.append_pair("user", "admin") |
|||
.append_pair("password", auth_token.as_str()); |
|||
|
|||
psql_url_lock_guard.replace(PsqlUrl { timestamp: now, url: psql_url.to_string() }); |
|||
|
|||
Ok(psql_url.to_string()) |
|||
} |
Loading…
Reference in new issue