Merge and modify PR from @Kurnihil

Merging a PR from @Kurnihil into the already rebased branch.
Made some small changes to make it work with newer changes.

Some finetuning is probably still needed.

Co-authored-by: Daniele Andrei <daniele.andrei@geo-satis.com>
Co-authored-by: Kurnihil
This commit is contained in:
BlackDex 2023-06-02 22:28:30 +02:00
parent 4219249e11
commit 8e34495e73
No known key found for this signature in database
GPG key ID: 58C80A2AA6C765E1
16 changed files with 282 additions and 7 deletions

View file

@ -6,3 +6,5 @@ CREATE TABLE organization_api_key (
revision_date DATETIME NOT NULL,
PRIMARY KEY(uuid, org_uuid)
);
ALTER TABLE users ADD COLUMN external_id TEXT;

View file

@ -6,3 +6,5 @@ CREATE TABLE organization_api_key (
revision_date TIMESTAMP NOT NULL,
PRIMARY KEY(uuid, org_uuid)
);
ALTER TABLE users ADD COLUMN external_id TEXT;

View file

@ -7,3 +7,5 @@ CREATE TABLE organization_api_key (
PRIMARY KEY(uuid, org_uuid),
FOREIGN KEY(org_uuid) REFERENCES organizations(uuid)
);
ALTER TABLE users ADD COLUMN external_id TEXT;

View file

@ -4,6 +4,7 @@ mod emergency_access;
mod events;
mod folders;
mod organizations;
mod public;
mod sends;
pub mod two_factor;
@ -27,6 +28,7 @@ pub fn routes() -> Vec<Route> {
routes.append(&mut organizations::routes());
routes.append(&mut two_factor::routes());
routes.append(&mut sends::routes());
routes.append(&mut public::routes());
routes.append(&mut eq_domains_routes);
routes.append(&mut hibp_routes);
routes.append(&mut meta_routes);

View file

@ -2382,7 +2382,7 @@ async fn add_update_group(
"OrganizationId": group.organizations_uuid,
"Name": group.name,
"AccessAll": group.access_all,
"ExternalId": group.get_external_id()
"ExternalId": group.external_id
})))
}

231
src/api/core/public.rs Normal file
View file

@ -0,0 +1,231 @@
use chrono::Utc;
use rocket::{
request::{self, FromRequest, Outcome},
Request, Route,
};
use crate::{
api::{EmptyResult, JsonUpcase},
auth,
db::{models::*, DbConn},
mail, CONFIG,
};
pub fn routes() -> Vec<Route> {
routes![ldap_import]
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct OrgImportGroupData {
Name: String,
ExternalId: String,
MemberExternalIds: Vec<String>,
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct OrgImportUserData {
Email: String,
ExternalId: String,
Deleted: bool,
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct OrgImportData {
Groups: Vec<OrgImportGroupData>,
Members: Vec<OrgImportUserData>,
OverwriteExisting: bool,
#[allow(dead_code)]
LargeImport: bool,
}
#[post("/public/organization/import", data = "<data>")]
async fn ldap_import(data: JsonUpcase<OrgImportData>, 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);
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.uuid,
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<Self, Self::Error> {
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))
}
}

View file

@ -94,6 +94,10 @@ pub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> {
decode_jwt(token, JWT_SEND_ISSUER.to_string())
}
pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> {
decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginJwtClaims {
// Not before

View file

@ -10,7 +10,7 @@ db_object! {
pub organizations_uuid: String,
pub name: String,
pub access_all: bool,
external_id: Option<String>,
pub external_id: Option<String>,
pub creation_date: NaiveDateTime,
pub revision_date: NaiveDateTime,
}
@ -107,10 +107,6 @@ impl Group {
None => self.external_id = None,
}
}
pub fn get_external_id(&self) -> Option<String> {
self.external_id.clone()
}
}
impl CollectionGroup {
@ -214,6 +210,15 @@ impl Group {
}}
}
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
groups::table
.filter(groups::external_id.eq(id))
.first::<GroupDb>(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<String> {
db_run! { conn: {

View file

@ -510,7 +510,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")
}

View file

@ -50,6 +50,8 @@ db_object! {
pub api_key: Option<String>,
pub avatar_color: Option<String>,
pub external_id: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable)]
@ -126,6 +128,8 @@ impl User {
api_key: None,
avatar_color: None,
external_id: None,
}
}
@ -150,6 +154,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<String>) {
//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.
///
@ -376,6 +395,11 @@ 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()

View file

@ -204,6 +204,7 @@ table! {
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

View file

@ -204,6 +204,7 @@ table! {
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}

View file

@ -204,6 +204,7 @@ table! {
client_kdf_parallelism -> Nullable<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}