Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-08-18 09:55:25 +02:00
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
Dieser Commit ist enthalten in:
Ursprung
4219249e11
Commit
8e34495e73
16 geänderte Dateien mit 282 neuen und 7 gelöschten Zeilen
|
@ -6,3 +6,5 @@ CREATE TABLE organization_api_key (
|
||||||
revision_date DATETIME NOT NULL,
|
revision_date DATETIME NOT NULL,
|
||||||
PRIMARY KEY(uuid, org_uuid)
|
PRIMARY KEY(uuid, org_uuid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN external_id TEXT;
|
|
@ -6,3 +6,5 @@ CREATE TABLE organization_api_key (
|
||||||
revision_date TIMESTAMP NOT NULL,
|
revision_date TIMESTAMP NOT NULL,
|
||||||
PRIMARY KEY(uuid, org_uuid)
|
PRIMARY KEY(uuid, org_uuid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN external_id TEXT;
|
|
@ -7,3 +7,5 @@ CREATE TABLE organization_api_key (
|
||||||
PRIMARY KEY(uuid, org_uuid),
|
PRIMARY KEY(uuid, org_uuid),
|
||||||
FOREIGN KEY(org_uuid) REFERENCES organizations(uuid)
|
FOREIGN KEY(org_uuid) REFERENCES organizations(uuid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN external_id TEXT;
|
|
@ -4,6 +4,7 @@ mod emergency_access;
|
||||||
mod events;
|
mod events;
|
||||||
mod folders;
|
mod folders;
|
||||||
mod organizations;
|
mod organizations;
|
||||||
|
mod public;
|
||||||
mod sends;
|
mod sends;
|
||||||
pub mod two_factor;
|
pub mod two_factor;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
routes.append(&mut sends::routes());
|
routes.append(&mut sends::routes());
|
||||||
|
routes.append(&mut public::routes());
|
||||||
routes.append(&mut eq_domains_routes);
|
routes.append(&mut eq_domains_routes);
|
||||||
routes.append(&mut hibp_routes);
|
routes.append(&mut hibp_routes);
|
||||||
routes.append(&mut meta_routes);
|
routes.append(&mut meta_routes);
|
||||||
|
|
|
@ -2382,7 +2382,7 @@ async fn add_update_group(
|
||||||
"OrganizationId": group.organizations_uuid,
|
"OrganizationId": group.organizations_uuid,
|
||||||
"Name": group.name,
|
"Name": group.name,
|
||||||
"AccessAll": group.access_all,
|
"AccessAll": group.access_all,
|
||||||
"ExternalId": group.get_external_id()
|
"ExternalId": group.external_id
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
231
src/api/core/public.rs
Normale Datei
231
src/api/core/public.rs
Normale Datei
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -94,6 +94,10 @@ pub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> {
|
||||||
decode_jwt(token, JWT_SEND_ISSUER.to_string())
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginJwtClaims {
|
pub struct LoginJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
|
|
@ -10,7 +10,7 @@ db_object! {
|
||||||
pub organizations_uuid: String,
|
pub organizations_uuid: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub access_all: bool,
|
pub access_all: bool,
|
||||||
external_id: Option<String>,
|
pub external_id: Option<String>,
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub revision_date: NaiveDateTime,
|
pub revision_date: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
@ -107,10 +107,6 @@ impl Group {
|
||||||
None => self.external_id = None,
|
None => self.external_id = None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_external_id(&self) -> Option<String> {
|
|
||||||
self.external_id.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionGroup {
|
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
|
//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> {
|
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
|
|
|
@ -510,7 +510,7 @@ impl UserOrganization {
|
||||||
.set(UserOrganizationDb::to_db(self))
|
.set(UserOrganizationDb::to_db(self))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding user to organization")
|
.map_res("Error adding user to organization")
|
||||||
}
|
},
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}.map_res("Error adding user to organization")
|
}.map_res("Error adding user to organization")
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,8 @@ db_object! {
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
pub avatar_color: Option<String>,
|
pub avatar_color: Option<String>,
|
||||||
|
|
||||||
|
pub external_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
|
@ -126,6 +128,8 @@ impl User {
|
||||||
api_key: None,
|
api_key: None,
|
||||||
|
|
||||||
avatar_color: 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))
|
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
|
/// Set the password hash generated
|
||||||
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
|
/// 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> {
|
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
|
||||||
|
|
|
@ -204,6 +204,7 @@ table! {
|
||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> Nullable<Text>,
|
avatar_color -> Nullable<Text>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,7 @@ table! {
|
||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> Nullable<Text>,
|
avatar_color -> Nullable<Text>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,7 @@ table! {
|
||||||
client_kdf_parallelism -> Nullable<Integer>,
|
client_kdf_parallelism -> Nullable<Integer>,
|
||||||
api_key -> Nullable<Text>,
|
api_key -> Nullable<Text>,
|
||||||
avatar_color -> Nullable<Text>,
|
avatar_color -> Nullable<Text>,
|
||||||
|
external_id -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Laden …
In neuem Issue referenzieren