Skip to content

Commit

Permalink
Fix External ID not set during DC Sync
Browse files Browse the repository at this point in the history
While working on the fix I realised the location where the `external_id`
is stored was wrong. It was stored in the `users` table, but it actually
should have been stored in the `users_organizations` table.

This will move the column to the right table. It will not move the
values of the `external_id` column, because if there are more
organizations, there is no way to really know which organization it is
linked to. Setups using the Directory Connector can clear the sync
cache, and sync again, that will store all the `external_id` values at
the right location.

Also changed the function to revoke,restore an org-user and set_external_id to return a boolean.
It will state if the value has been changed or not, and if not, we can
prevent a `save` call to the database.
  • Loading branch information
BlackDex committed Sep 2, 2023
1 parent bbd630f commit 43e8eb6
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 45 deletions.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE users
DROP COLUMN external_id;

ALTER TABLE users_organizations
ADD COLUMN external_id TEXT;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE users
DROP COLUMN external_id;

ALTER TABLE users_organizations
ADD COLUMN external_id TEXT;
Empty file.
48 changes: 48 additions & 0 deletions migrations/sqlite/2023-09-02-212336_move_user_external_id/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- Create new users table without the external_id column
CREATE TABLE users_new (
"uuid" TEXT NOT NULL,
"created_at" DATETIME NOT NULL,
"updated_at" DATETIME NOT NULL,
"email" TEXT NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"password_hash" BLOB NOT NULL,
"salt" BLOB 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 NOT NULL DEFAULT 0,
"client_kdf_iter" INTEGER NOT NULL DEFAULT 600000,
"verified_at" DATETIME DEFAULT NULL,
"last_verifying_at" DATETIME DEFAULT NULL,
"login_verify_count" INTEGER NOT NULL DEFAULT 0,
"email_new" TEXT DEFAULT NULL,
"email_new_token" TEXT DEFAULT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT 1,
"stamp_exception" TEXT DEFAULT NULL,
"api_key" TEXT,
"avatar_color" TEXT,
"client_kdf_memory" INTEGER DEFAULT NULL,
"client_kdf_parallelism" INTEGER DEFAULT NULL,
PRIMARY KEY ("uuid")
);

-- Transfer current data to new table
INSERT INTO "users_new" ("akey","api_key","avatar_color","client_kdf_iter","client_kdf_memory","client_kdf_parallelism","client_kdf_type","created_at","email","email_new","email_new_token","enabled","equivalent_domains","excluded_globals","last_verifying_at","login_verify_count","name","password_hash","password_hint","password_iterations","private_key","public_key","salt","security_stamp","stamp_exception","totp_recover","totp_secret","updated_at","uuid","verified_at")
SELECT "akey","api_key","avatar_color","client_kdf_iter","client_kdf_memory","client_kdf_parallelism","client_kdf_type","created_at","email","email_new","email_new_token","enabled","equivalent_domains","excluded_globals","last_verifying_at","login_verify_count","name","password_hash","password_hint","password_iterations","private_key","public_key","salt","security_stamp","stamp_exception","totp_recover","totp_secret","updated_at","uuid","verified_at"
FROM "users";

-- Drop the old table
DROP TABLE "users";

-- Rename the new table to the original name
ALTER TABLE "users_new" RENAME TO "users";

-- Add the external_id to the users_organizations table
ALTER TABLE "users_organizations" ADD COLUMN "external_id" TEXT;
48 changes: 31 additions & 17 deletions src/api/core/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,26 +56,43 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co
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?;
}
// Only revoke a user if it is not the last confirmed owner
let revoked = if user_org.atype == UserOrgType::Owner
&& user_org.status == UserOrgStatus::Confirmed as i32
{
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await
<= 1
{
warn!("Can't revoke the last owner");
false
} else {
user_org.revoke()
}
} else {
user_org.revoke()
};

let ext_modified = user_org.set_external_id(Some(user_data.ExternalId.clone()));
if revoked || ext_modified {
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();
let restored = user_org.restore();
let ext_modified = user_org.set_external_id(Some(user_data.ExternalId.clone()));
if restored || ext_modified {
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
// User does not exist yet
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() {
Expand All @@ -92,6 +109,7 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co
};

let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
new_org_user.set_external_id(Some(user_data.ExternalId.clone()));
new_org_user.access_all = false;
new_org_user.atype = UserOrgType::User as i32;
new_org_user.status = user_org_status;
Expand Down Expand Up @@ -132,12 +150,10 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co
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 let Some(user_org) = UserOrganization::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await
{
let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
group_user.save(&mut conn).await?;
}
}
}
Expand All @@ -150,10 +166,8 @@ async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut co
// Generate a HashSet to quickly verify if a member is listed or not.
let sync_members: HashSet<String> = data.Members.into_iter().map(|m| m.ExternalId).collect();
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() && !sync_members.contains(&user_external_id.unwrap()) {
if let Some(ref user_external_id) = user_org.external_id {
if !sync_members.contains(user_external_id) {
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)
Expand Down
36 changes: 33 additions & 3 deletions src/db/models/organization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ db_object! {
pub status: i32,
pub atype: i32,
pub reset_password_key: Option<String>,
pub external_id: Option<String>,
}

#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
Expand Down Expand Up @@ -208,19 +209,37 @@ impl UserOrganization {
status: UserOrgStatus::Accepted as i32,
atype: UserOrgType::User as i32,
reset_password_key: None,
external_id: None,
}
}

pub fn restore(&mut self) {
pub fn restore(&mut self) -> bool {
if self.status < UserOrgStatus::Accepted as i32 {
self.status += ACTIVATE_REVOKE_DIFF;
return true;
}
false
}

pub fn revoke(&mut self) {
pub fn revoke(&mut self) -> bool {
if self.status > UserOrgStatus::Revoked as i32 {
self.status -= ACTIVATE_REVOKE_DIFF;
return true;
}
false
}

pub fn set_external_id(&mut self, external_id: Option<String>) -> bool {
//Check if external id is empty. We don't want to have
//empty strings in the database
if self.external_id != external_id {
self.external_id = match external_id {
Some(external_id) if !external_id.is_empty() => Some(external_id),
_ => None,
};
return true;
}
false
}
}

Expand Down Expand Up @@ -434,7 +453,7 @@ impl UserOrganization {
"UserId": self.user_uuid,
"Name": user.name,
"Email": user.email,
"ExternalId": user.external_id,
"ExternalId": self.external_id,
"Groups": groups,
"Collections": collections,

Expand Down Expand Up @@ -778,6 +797,17 @@ impl UserOrganization {
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
}}
}

pub async fn find_by_external_id_and_org(ext_id: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
users_organizations::table
.filter(
users_organizations::external_id.eq(ext_id)
.and(users_organizations::org_uuid.eq(org_uuid))
)
.first::<UserOrganizationDb>(conn).ok().from_db()
}}
}
}

impl OrganizationApiKey {
Expand Down
22 changes: 0 additions & 22 deletions src/db/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ db_object! {
pub api_key: Option<String>,

pub avatar_color: Option<String>,

pub external_id: Option<String>,
}

#[derive(Identifiable, Queryable, Insertable)]
Expand Down Expand Up @@ -128,8 +126,6 @@ impl User {
api_key: None,

avatar_color: None,

external_id: None,
}
}

Expand All @@ -154,18 +150,6 @@ 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<String>) {
//Check if external id is empty. We don't want to have
//empty strings in the database
let mut ext_id: Option<String> = None;
if let Some(external_id) = external_id {
if !external_id.is_empty() {
ext_id = Some(external_id);
}
}
self.external_id = ext_id;
}

/// Set the password hash generated
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
///
Expand Down Expand Up @@ -392,12 +376,6 @@ impl User {
}}
}

pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
users::table.filter(users::external_id.eq(id)).first::<UserDb>(conn).ok().from_db()
}}
}

pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
Expand Down
2 changes: 1 addition & 1 deletion src/db/schemas/mysql/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ table! {
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

Expand All @@ -228,6 +227,7 @@ table! {
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/db/schemas/postgresql/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ table! {
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

Expand All @@ -228,6 +227,7 @@ table! {
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/db/schemas/sqlite/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ table! {
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

Expand All @@ -228,6 +227,7 @@ table! {
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

Expand Down

0 comments on commit 43e8eb6

Please sign in to comment.