From 04a8054d73debeb318232e10a392c6753f4a087b Mon Sep 17 00:00:00 2001 From: Daniele Andrei Date: Thu, 10 Nov 2022 13:10:21 +0100 Subject: [PATCH] Implemented the API key organization and public API needed to use "Bitwarden Directory Connector" --- .../down.sql | 0 .../up.sql | 8 + .../down.sql | 0 .../2022-11-08-234911_add_external_id/up.sql | 2 + .../down.sql | 0 .../up.sql | 8 + .../down.sql | 0 .../2022-11-08-234911_add_external_id/up.sql | 2 + .../down.sql | 0 .../up.sql | 9 + .../down.sql | 0 .../2022-11-08-234911_add_external_id/up.sql | 2 + src/api/core/mod.rs | 2 + src/api/core/organizations.rs | 62 ++++- src/api/core/public.rs | 224 ++++++++++++++++++ src/api/identity.rs | 55 ++++- src/auth.rs | 35 +++ src/db/models/group.rs | 15 +- src/db/models/mod.rs | 2 +- src/db/models/organization.rs | 79 +++++- src/db/models/user.rs | 22 ++ src/db/schemas/mysql/schema.rs | 13 + src/db/schemas/postgresql/schema.rs | 13 + src/db/schemas/sqlite/schema.rs | 13 + src/main.rs | 2 +- 25 files changed, 545 insertions(+), 23 deletions(-) create mode 100644 migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql create mode 100644 migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql create mode 100644 migrations/mysql/2022-11-08-234911_add_external_id/down.sql create mode 100644 migrations/mysql/2022-11-08-234911_add_external_id/up.sql create mode 100644 migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql create mode 100644 migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql create mode 100644 migrations/postgresql/2022-11-08-234911_add_external_id/down.sql create mode 100644 migrations/postgresql/2022-11-08-234911_add_external_id/up.sql create mode 100644 migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql create mode 100644 migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql create mode 100644 migrations/sqlite/2022-11-08-234911_add_external_id/down.sql create mode 100644 migrations/sqlite/2022-11-08-234911_add_external_id/up.sql create mode 100644 src/api/core/public.rs diff --git a/migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql b/migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql b/migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql new file mode 100644 index 00000000..5e429597 --- /dev/null +++ b/migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE organization_api_key ( + uuid CHAR(36) NOT NULL, + org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid), + atype INTEGER NOT NULL, + api_key VARCHAR(255) NOT NULL, + revision_date DATETIME NOT NULL, + PRIMARY KEY(uuid, org_uuid) +); \ No newline at end of file diff --git a/migrations/mysql/2022-11-08-234911_add_external_id/down.sql b/migrations/mysql/2022-11-08-234911_add_external_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/mysql/2022-11-08-234911_add_external_id/up.sql b/migrations/mysql/2022-11-08-234911_add_external_id/up.sql new file mode 100644 index 00000000..f21a51b1 --- /dev/null +++ b/migrations/mysql/2022-11-08-234911_add_external_id/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN external_id TEXT; diff --git a/migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql b/migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql b/migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql new file mode 100644 index 00000000..3c37bb5c --- /dev/null +++ b/migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE organization_api_key ( + uuid CHAR(36) NOT NULL, + org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid), + atype INTEGER NOT NULL, + api_key VARCHAR(255), + revision_date TIMESTAMP NOT NULL, + PRIMARY KEY(uuid, org_uuid) +); diff --git a/migrations/postgresql/2022-11-08-234911_add_external_id/down.sql b/migrations/postgresql/2022-11-08-234911_add_external_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2022-11-08-234911_add_external_id/up.sql b/migrations/postgresql/2022-11-08-234911_add_external_id/up.sql new file mode 100644 index 00000000..f21a51b1 --- /dev/null +++ b/migrations/postgresql/2022-11-08-234911_add_external_id/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN external_id TEXT; diff --git a/migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql b/migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql b/migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql new file mode 100644 index 00000000..78934f27 --- /dev/null +++ b/migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE organization_api_key ( + uuid TEXT NOT NULL, + org_uuid TEXT NOT NULL, + atype INTEGER NOT NULL, + api_key TEXT NOT NULL, + revision_date DATETIME NOT NULL, + PRIMARY KEY(uuid, org_uuid), + FOREIGN KEY(org_uuid) REFERENCES organizations(uuid) +); \ No newline at end of file diff --git a/migrations/sqlite/2022-11-08-234911_add_external_id/down.sql b/migrations/sqlite/2022-11-08-234911_add_external_id/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2022-11-08-234911_add_external_id/up.sql b/migrations/sqlite/2022-11-08-234911_add_external_id/up.sql new file mode 100644 index 00000000..f21a51b1 --- /dev/null +++ b/migrations/sqlite/2022-11-08-234911_add_external_id/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN external_id TEXT; diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 0df9a9dc..d910e0c4 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -5,6 +5,7 @@ mod folders; mod organizations; mod sends; pub mod two_factor; +mod public; pub use ciphers::purge_trashed_ciphers; pub use ciphers::{CipherSyncData, CipherSyncType}; @@ -26,6 +27,7 @@ pub fn routes() -> Vec { routes.append(&mut organizations::routes()); routes.append(&mut two_factor::routes()); routes.append(&mut sends::routes()); + routes.append(&mut public::routes()); routes.append(&mut device_token_routes); routes.append(&mut eq_domains_routes); routes.append(&mut hibp_routes); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index a7eb8db5..bce47c84 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -86,7 +86,9 @@ pub fn routes() -> Vec { put_user_groups, delete_group_user, post_delete_group_user, - get_org_export + get_org_export, + api_key, + rotate_api_key, ] } @@ -1887,7 +1889,7 @@ async fn add_update_group(mut group: Group, collections: Vec, "OrganizationId": group.organizations_uuid, "Name": group.name, "AccessAll": group.access_all, - "ExternalId": group.get_external_id() + "ExternalId": group.external_id }))) } @@ -1909,7 +1911,7 @@ async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHea "OrganizationId": group.organizations_uuid, "Name": group.name, "AccessAll": group.access_all, - "ExternalId": group.get_external_id(), + "ExternalId": group.external_id, "Collections": collections_groups }))) } @@ -2112,3 +2114,57 @@ async fn get_org_export(org_id: String, headers: AdminHeaders, mut conn: DbConn) })) } } + +async fn _api_key( + org_id: String, + data: JsonUpcase, + rotate: bool, + headers: AdminHeaders, + conn: DbConn, +) -> JsonResult { + let data: PasswordData = data.into_inner().data; + let user = headers.user; + + // Validate the admin users password + if !user.check_valid_password(&data.MasterPasswordHash) { + err!("Invalid password") + } + + let org_api_key = match OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await { + Some(mut org_api_key) => { + if rotate { + org_api_key.api_key = crate::crypto::generate_api_key(); + org_api_key.revision_date = chrono::Utc::now().naive_utc(); + org_api_key.save(&conn).await.expect("Error rotating organization API Key"); + } + org_api_key + } + None => { + let api_key = crate::crypto::generate_api_key(); + let new_org_api_key = OrganizationApiKey::new(org_id, api_key); + new_org_api_key.save(&conn).await.expect("Error creating organization API Key"); + new_org_api_key + } + }; + + Ok(Json(json!({ + "ApiKey": org_api_key.api_key, + "RevisionDate": crate::util::format_date(&org_api_key.revision_date), + "Object": "apiKey", + }))) +} + +#[post("/organizations//api-key", data = "")] +async fn api_key(org_id: String, data: JsonUpcase, headers: AdminHeaders, conn: DbConn) -> JsonResult { + _api_key(org_id, data, false, headers, conn).await +} + +#[post("/organizations//rotate-api-key", data = "")] +async fn rotate_api_key( + org_id: String, + data: JsonUpcase, + headers: AdminHeaders, + conn: DbConn, +) -> JsonResult { + _api_key(org_id, data, true, headers, conn).await +} diff --git a/src/api/core/public.rs b/src/api/core/public.rs new file mode 100644 index 00000000..631f1225 --- /dev/null +++ b/src/api/core/public.rs @@ -0,0 +1,224 @@ +use rocket::{ + Route, + Request, + request::{self, FromRequest, Outcome}, +}; +use chrono::Utc; + +use crate::{ + auth, + db::{models::*, DbConn}, + api::{JsonUpcase, EmptyResult}, + mail, + CONFIG +}; + +pub fn routes() -> Vec { + routes![ + ldap_import, + ] +} + + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct OrgImportGroupData { + Name: String, // "GroupName" + ExternalId: String, // "cn=GroupName,ou=Groups,dc=example,dc=com" + MemberExternalIds: Vec, // ["uid=user,ou=People,dc=example,dc=com"] +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct OrgImportUserData { + Email: String, // "user@maildomain.net" + ExternalId: String, // "uid=user,ou=People,dc=example,dc=com" + Deleted: bool, +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct OrgImportData { + Groups: Vec, + Members: Vec, + OverwriteExisting: bool, + #[allow(dead_code)] + LargeImport: bool, +} + +#[post("/public/organization/import", data = "")] +async fn ldap_import(data: JsonUpcase, token: PublicToken, mut conn: DbConn) -> EmptyResult { + let _ = &conn; + let org_id = token.0 ; + let data = data.into_inner().data; + + for user_data in &data.Members { + if user_data.Deleted { + // If user is marked for deletion and it exists, revoke it + if let Some(mut user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await + { + user_org.revoke(); + user_org.save(&mut conn).await?; + } + + // If user is part of the organization, restore it + } else if let Some(mut user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await { + if user_org.status < UserOrgStatus::Revoked as i32 + { + user_org.restore(); + user_org.save(&mut conn).await?; + } + }else{ // If user is not part of the organization + let user = match User::find_by_mail(&user_data.Email, &mut conn).await { + Some(user) => user, // exists in vaultwarden + None => { // doesn't exist in vaultwarden + let mut new_user = User::new(user_data.Email.clone()); + new_user.set_external_id(Some(user_data.ExternalId.clone())); + new_user.save(&mut conn).await?; + + if !CONFIG.mail_enabled() { + let invitation = Invitation::new(new_user.email.clone()); + invitation.save(&mut conn).await?; + } + new_user + }, + }; + let user_org_status = if CONFIG.mail_enabled() { + UserOrgStatus::Invited as i32 + } else { + UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites + }; + + let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); + new_org_user.access_all = false; + new_org_user.atype = UserOrgType::User as i32; + new_org_user.status = user_org_status; + + new_org_user.save(&mut conn).await?; + + if CONFIG.mail_enabled() { + let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await { + Some(org) => (org.name, org.billing_email), + None => err!("Error looking up organization"), + }; + + mail::send_invite( + &user_data.Email, + &user.uuid, + Some(org_id.clone()), + Some(new_org_user.uuid), + &org_name, + Some(org_email), + ).await?; + } + } + + } + + for group_data in &data.Groups { + let group_uuid = match Group::find_by_external_id(&group_data.ExternalId, &mut conn).await { + Some(group) => group, + None => { + let mut group = Group::new( org_id.clone(), group_data.Name.clone(), false, Some(group_data.ExternalId.clone())); + group.save(&mut conn).await?; + group + } + }.uuid; + + GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?; + + for ext_id in &group_data.MemberExternalIds { + if let Some(user) = User::find_by_external_id(&ext_id, &mut conn).await { + if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await { + let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone()); + group_user.save(&mut conn).await?; + } + } + } + } + + // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) + if data.OverwriteExisting { + for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await { + if let Some(user_external_id) = User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.external_id) { + if user_external_id.is_some() && !data.Members.iter().any(|u| u.ExternalId == *user_external_id.as_ref().unwrap()) { + if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 { + // Removing owner, check that there is at least one other confirmed owner + if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await <= 1 { + warn!("Can't delete the last owner"); + continue + } + } + user_org.delete(&mut conn).await?; + } + } + } + } + + Ok(()) +} + +#[derive(Debug)] +pub struct PublicToken (String); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for PublicToken { + type Error = &'static str; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + let headers = request.headers(); + // Get access_token + let access_token: &str = match headers.get_one("Authorization") { + Some(a) => match a.rsplit("Bearer ").next() { + Some(split) => split, + None => err_handler!("No access token provided"), + }, + None => err_handler!("No access token provided"), + }; + // Check JWT token is valid and get device and user from it + let claims = match auth::decode_api_org(access_token) { + Ok(claims) => claims, + Err(_) => err_handler!("Invalid claim"), + }; + // Check if time is between claims.nbf and claims.exp + let time_now = Utc::now().naive_utc().timestamp(); + if time_now < claims.nbf { + err_handler!("Token issued in the future"); + } + if time_now > claims.exp { + err_handler!("Token expired"); + } + // Check if claims.iss is host|claims.scope[0] + let host = match auth::Host::from_request(request).await { + Outcome::Success(host) => host, + _ => err_handler!("Error getting Host"), + }; + let complete_host = format!("{}|{}", host.host, claims.scope[0]); + if complete_host != claims.iss { + err_handler!("Token not issued by this server"); + } + + // Check if claims.sub is org_api_key.uuid + // Check if claims.client_sub is org_api_key.org_uuid + let conn = match DbConn::from_request(request).await { + Outcome::Success(conn) => conn, + _ => err_handler!("Error getting DB"), + }; + let org_uuid = match claims.client_id.strip_prefix("organization.") { + Some(uuid) => uuid, + None => err_handler!("Malformed client_id"), + }; + let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await { + Some(org_api_key) => org_api_key, + None => err_handler!("Invalid client_id"), + }; + if org_api_key.org_uuid != claims.client_sub { + err_handler!("Token not issued for this org"); + } + if org_api_key.uuid != claims.sub { + err_handler!("Token not issued for this client"); + } + + Outcome::Success(PublicToken(claims.client_sub)) + } +} diff --git a/src/api/identity.rs b/src/api/identity.rs index a509df87..4b09cc6f 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -13,7 +13,7 @@ use crate::{ core::two_factor::{duo, email, email::EmailTokenData, yubikey}, ApiResult, EmptyResult, JsonResult, JsonUpcase, }, - auth::ClientIp, + auth::{generate_organization_api_key_login_claims, ClientIp}, db::{models::*, DbConn}, error::MapResult, mail, util, CONFIG, @@ -188,17 +188,19 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> Ok(Json(result)) } -async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult { - // Validate scope - let scope = data.scope.as_ref().unwrap(); - if scope != "api" { - err!("Scope not supported") - } - let scope_vec = vec!["api".into()]; - +async fn _api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult { // Ratelimit the login crate::ratelimit::check_limit_login(&ip.ip)?; + // Validate scope + match data.scope.as_ref().unwrap().as_ref() { + "api" => _user_api_key_login(data, conn, ip).await, + "api.organization" => _organization_api_key_login(data, conn, ip).await, + _ => err!("Scope not supported"), + } +} + +async fn _user_api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult { // Get the user via the client_id let client_id = data.client_id.as_ref().unwrap(); let user_uuid = match client_id.strip_prefix("user.") { @@ -235,6 +237,7 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J } // Common + let scope_vec = vec!["api".into()]; let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await; let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec); device.save(&mut conn).await?; @@ -253,7 +256,39 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J "Kdf": user.client_kdf_type, "KdfIterations": user.client_kdf_iter, "ResetMasterPassword": false, // TODO: Same as above - "scope": scope, + "scope": "api", + "unofficialServer": true, + }))) +} + +async fn _organization_api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult { + // Get the org via the client_id + let client_id = data.client_id.as_ref().unwrap(); + let org_uuid = match client_id.strip_prefix("organization.") { + Some(uuid) => uuid, + None => err!("Malformed client_id", format!("IP: {}.", ip.ip)), + }; + let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await { + Some(org_api_key) => org_api_key, + None => err!("Invalid client_id", format!("IP: {}.", ip.ip)), + }; + + // Check API key. + let client_secret = data.client_secret.as_ref().unwrap(); + if !org_api_key.check_valid_api_key(client_secret) { + err!("Incorrect client_secret", format!("IP: {}. Organization: {}.", ip.ip, org_api_key.org_uuid)) + } + + let claim = generate_organization_api_key_login_claims(org_api_key.uuid, org_api_key.org_uuid); + let access_token = crate::auth::encode_jwt(&claim); + + //dbg!(&access_token); + + Ok(Json(json!({ + "access_token": access_token, + "expires_in": 3600, + "token_type": "Bearer", + "scope": "api.organization", "unofficialServer": true, }))) } diff --git a/src/auth.rs b/src/auth.rs index 0db2d95a..04405d3f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -23,6 +23,7 @@ static JWT_DELETE_ISSUER: Lazy = Lazy::new(|| format!("{}|delete", CONFI static JWT_VERIFYEMAIL_ISSUER: Lazy = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin())); static JWT_ADMIN_ISSUER: Lazy = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin())); static JWT_SEND_ISSUER: Lazy = Lazy::new(|| format!("{}|send", CONFIG.domain_origin())); +static JWT_ORG_API_KEY_ISSUER: Lazy = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin())); static PRIVATE_RSA_KEY_VEC: Lazy> = Lazy::new(|| { std::fs::read(CONFIG.private_rsa_key()).unwrap_or_else(|e| panic!("Error loading private RSA Key.\n{}", e)) @@ -96,6 +97,10 @@ pub fn decode_send(token: &str) -> Result { decode_jwt(token, JWT_SEND_ISSUER.to_string()) } +pub fn decode_api_org(token: &str) -> Result { + decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string()) +} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginJwtClaims { // Not before @@ -203,6 +208,36 @@ pub fn generate_emergency_access_invite_claims( } } +#[derive(Debug, Serialize, Deserialize)] +pub struct OrgApiKeyLoginJwtClaims { + // Not before + pub nbf: i64, + // Expiration time + pub exp: i64, + // Issuer + pub iss: String, + // Subject + pub sub: String, + + pub client_id: String, + pub client_sub: String, + // [ "api.organization" ] + pub scope: Vec, +} + +pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims { + let time_now = Utc::now().naive_utc(); + OrgApiKeyLoginJwtClaims { + nbf: time_now.timestamp(), + exp: (time_now + Duration::hours(1)).timestamp(), + iss: JWT_ORG_API_KEY_ISSUER.to_string(), + sub: uuid, + client_id: format!("organization.{org_id}"), + client_sub: org_id, + scope: vec!["api.organization".into()], + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct BasicJwtClaims { // Not before diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 1d2e6062..9139d947 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -10,7 +10,7 @@ db_object! { pub organizations_uuid: String, pub name: String, pub access_all: bool, - external_id: Option, + pub external_id: Option, pub creation_date: NaiveDateTime, pub revision_date: NaiveDateTime, } @@ -82,10 +82,6 @@ impl Group { None => self.external_id = None, } } - - pub fn get_external_id(&self) -> Option { - self.external_id.clone() - } } impl CollectionGroup { @@ -171,6 +167,15 @@ impl Group { }} } + pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option { + db_run! { conn: { + groups::table + .filter(groups::external_id.eq(id)) + .first::(conn) + .ok() + .from_db() + }} + } //Returns all organizations the user has full access to pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 20e659c6..11bb9df5 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -22,7 +22,7 @@ pub use self::favorite::Favorite; pub use self::folder::{Folder, FolderCipher}; pub use self::group::{CollectionGroup, Group, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; -pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; +pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization}; pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_incomplete::TwoFactorIncomplete; diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 0c4cadc4..38cdaa12 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1,3 +1,4 @@ +use chrono::{NaiveDateTime, Utc}; use num_traits::FromPrimitive; use serde_json::Value; use std::cmp::Ordering; @@ -29,6 +30,17 @@ db_object! { pub status: i32, pub atype: i32, } + + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[diesel(table_name = organization_api_key)] + #[diesel(primary_key(uuid, org_uuid))] + pub struct OrganizationApiKey { + pub uuid: String, + pub org_uuid: String, + pub atype: i32, + pub api_key: String, + pub revision_date: NaiveDateTime, + } } // https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs @@ -155,7 +167,7 @@ impl Organization { "UseSso": false, // Not supported // "UseKeyConnector": false, // Not supported "SelfHost": true, - "UseApi": false, // Not supported + "UseApi": true, "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), "UseResetPassword": false, // Not supported @@ -209,6 +221,23 @@ impl UserOrganization { } } +impl OrganizationApiKey { + pub fn new(org_uuid: String, api_key: String) -> Self { + Self { + uuid: crate::util::get_uuid(), + + org_uuid, + atype: 0, // Type 0 is the default and only type we support currently + api_key, + revision_date: Utc::now().naive_utc(), + } + } + + pub fn check_valid_api_key(&self, api_key: &str) -> bool { + crate::crypto::ct_eq(&self.api_key, api_key) + } +} + use crate::db::DbConn; use crate::api::EmptyResult; @@ -308,7 +337,7 @@ impl UserOrganization { "UseTotp": true, // "UseScim": false, // Not supported (Not AGPLv3 Licensed) "UsePolicies": true, - "UseApi": false, // Not supported + "UseApi": true, "SelfHost": true, "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), "ResetPasswordEnrolled": false, // Not supported @@ -442,7 +471,7 @@ impl UserOrganization { .set(UserOrganizationDb::to_db(self)) .execute(conn) .map_res("Error adding user to organization") - } + }, Err(e) => Err(e.into()), }.map_res("Error adding user to organization") } @@ -688,6 +717,50 @@ impl UserOrganization { } } +impl OrganizationApiKey { + pub async fn save(&self, conn: &DbConn) -> EmptyResult { + db_run! { conn: + sqlite, mysql { + match diesel::replace_into(organization_api_key::table) + .values(OrganizationApiKeyDb::to_db(self)) + .execute(conn) + { + Ok(_) => Ok(()), + // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. + Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + diesel::update(organization_api_key::table) + .filter(organization_api_key::uuid.eq(&self.uuid)) + .set(OrganizationApiKeyDb::to_db(self)) + .execute(conn) + .map_res("Error saving organization") + } + Err(e) => Err(e.into()), + }.map_res("Error saving organization") + + } + postgresql { + let value = OrganizationApiKeyDb::to_db(self); + diesel::insert_into(organization_api_key::table) + .values(&value) + .on_conflict(organization_api_key::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving organization") + } + } + } + + pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option { + db_run! { conn: { + organization_api_key::table + .filter(organization_api_key::org_uuid.eq(org_uuid)) + .first::(conn) + .ok().from_db() + }} + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 826e00fa..6c4ec167 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -46,6 +46,7 @@ db_object! { pub client_kdf_iter: i32, pub api_key: Option, + pub external_id: Option, } #[derive(Identifiable, Queryable, Insertable)] @@ -113,6 +114,7 @@ impl User { client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT, api_key: None, + external_id: None, } } @@ -137,6 +139,21 @@ impl User { matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key)) } + pub fn set_external_id(&mut self, external_id: Option) { + //Check if external id is empty. We don't want to have + //empty strings in the database + match external_id { + Some(external_id) => { + if external_id.is_empty() { + self.external_id = None; + } else { + self.external_id = Some(external_id) + } + } + None => self.external_id = None, + } + } + /// Set the password hash generated /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different. /// @@ -349,6 +366,11 @@ impl User { }} } + pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option { + db_run! {conn: { + users::table.filter(users::external_id.eq(id)).first::(conn).ok().from_db() + }} + } pub async fn get_all(conn: &mut DbConn) -> Vec { db_run! {conn: { users::table.load::(conn).expect("Error loading users").from_db() diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 514bc67a..f606157b 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -179,6 +179,7 @@ table! { client_kdf_type -> Integer, client_kdf_iter -> Integer, api_key -> Nullable, + external_id -> Nullable, } } @@ -203,6 +204,16 @@ table! { } } +table! { + organization_api_key (uuid, org_uuid) { + uuid -> Text, + org_uuid -> Text, + atype -> Integer, + api_key -> Text, + revision_date -> Timestamp, + } +} + table! { emergency_access (uuid) { uuid -> Text, @@ -266,6 +277,7 @@ joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> users (user_uuid)); +joinable!(organization_api_key -> organizations (org_uuid)); joinable!(emergency_access -> users (grantor_uuid)); joinable!(groups -> organizations (organizations_uuid)); joinable!(groups_users -> users_organizations (users_organizations_uuid)); @@ -289,6 +301,7 @@ allow_tables_to_appear_in_same_query!( users, users_collections, users_organizations, + organization_api_key, emergency_access, groups, groups_users, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 23f9af7e..fa0f316a 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -179,6 +179,7 @@ table! { client_kdf_type -> Integer, client_kdf_iter -> Integer, api_key -> Nullable, + external_id -> Nullable, } } @@ -203,6 +204,16 @@ table! { } } +table! { + organization_api_key (uuid, org_uuid) { + uuid -> Text, + org_uuid -> Text, + atype -> Integer, + api_key -> Text, + revision_date -> Timestamp, + } +} + table! { emergency_access (uuid) { uuid -> Text, @@ -266,6 +277,7 @@ joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> users (user_uuid)); +joinable!(organization_api_key -> organizations (org_uuid)); joinable!(emergency_access -> users (grantor_uuid)); joinable!(groups -> organizations (organizations_uuid)); joinable!(groups_users -> users_organizations (users_organizations_uuid)); @@ -289,6 +301,7 @@ allow_tables_to_appear_in_same_query!( users, users_collections, users_organizations, + organization_api_key, emergency_access, groups, groups_users, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 23f9af7e..fa0f316a 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -179,6 +179,7 @@ table! { client_kdf_type -> Integer, client_kdf_iter -> Integer, api_key -> Nullable, + external_id -> Nullable, } } @@ -203,6 +204,16 @@ table! { } } +table! { + organization_api_key (uuid, org_uuid) { + uuid -> Text, + org_uuid -> Text, + atype -> Integer, + api_key -> Text, + revision_date -> Timestamp, + } +} + table! { emergency_access (uuid) { uuid -> Text, @@ -266,6 +277,7 @@ joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> users (user_uuid)); +joinable!(organization_api_key -> organizations (org_uuid)); joinable!(emergency_access -> users (grantor_uuid)); joinable!(groups -> organizations (organizations_uuid)); joinable!(groups_users -> users_organizations (users_organizations_uuid)); @@ -289,6 +301,7 @@ allow_tables_to_appear_in_same_query!( users, users_collections, users_organizations, + organization_api_key, emergency_access, groups, groups_users, diff --git a/src/main.rs b/src/main.rs index 83b3b64d..7a87d591 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ // The more key/value pairs there are the more recursion occurs. // We want to keep this as low as possible, but not higher then 128. // If you go above 128 it will cause rust-analyzer to fail, -#![recursion_limit = "94"] +#![recursion_limit = "97"] // When enabled use MiMalloc as malloc instead of the default malloc #[cfg(feature = "enable_mimalloc")]