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