Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2025-01-31 10:08:56 +01:00
Merge remote-tracking branch 'dani/main' into sso-support
Dieser Commit ist enthalten in:
Commit
6ee5580b03
26 geänderte Dateien mit 345 neuen und 155 gelöschten Zeilen
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
vault_version: "v2025.1.0"
|
vault_version: "v2025.1.1"
|
||||||
vault_image_digest: "sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8"
|
vault_image_digest: "sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918"
|
||||||
# Cross Compile Docker Helper Scripts v1.6.1
|
# Cross Compile Docker Helper Scripts v1.6.1
|
||||||
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
# We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts
|
||||||
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
# https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags
|
||||||
|
|
|
@ -19,15 +19,15 @@
|
||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.1
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8]
|
# [docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.1.0]
|
# [docker.io/vaultwarden/web-vault:v2025.1.1]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 AS vault
|
||||||
|
|
||||||
########################## ALPINE BUILD IMAGES ##########################
|
########################## ALPINE BUILD IMAGES ##########################
|
||||||
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
## NOTE: The Alpine Base Images do not support other platforms then linux/amd64
|
||||||
|
|
|
@ -19,15 +19,15 @@
|
||||||
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
# - From https://hub.docker.com/r/vaultwarden/web-vault/tags,
|
||||||
# click the tag name to view the digest of the image it currently points to.
|
# click the tag name to view the digest of the image it currently points to.
|
||||||
# - From the command line:
|
# - From the command line:
|
||||||
# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.0
|
# $ docker pull docker.io/vaultwarden/web-vault:v2025.1.1
|
||||||
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.0
|
# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.1.1
|
||||||
# [docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8]
|
# [docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918]
|
||||||
#
|
#
|
||||||
# - Conversely, to get the tag name from the digest:
|
# - Conversely, to get the tag name from the digest:
|
||||||
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8
|
# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918
|
||||||
# [docker.io/vaultwarden/web-vault:v2025.1.0]
|
# [docker.io/vaultwarden/web-vault:v2025.1.1]
|
||||||
#
|
#
|
||||||
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:72d636334b4ad6fe9ba1d12e0cda562cd31772cf28772f6b2fe4121a537b72a8 AS vault
|
FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:cb6b2095a4afc1d9d243a33f6d09211f40e3d82c7ae829fd025df5ff175a4918 AS vault
|
||||||
|
|
||||||
########################## Cross Compile Docker Helper Scripts ##########################
|
########################## Cross Compile Docker Helper Scripts ##########################
|
||||||
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts
|
||||||
|
|
0
migrations/mysql/2025-01-09-172300_add_manage/down.sql
Normale Datei
0
migrations/mysql/2025-01-09-172300_add_manage/down.sql
Normale Datei
5
migrations/mysql/2025-01-09-172300_add_manage/up.sql
Normale Datei
5
migrations/mysql/2025-01-09-172300_add_manage/up.sql
Normale Datei
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
ALTER TABLE collections_groups
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
0
migrations/postgresql/2025-01-09-172300_add_manage/down.sql
Normale Datei
0
migrations/postgresql/2025-01-09-172300_add_manage/down.sql
Normale Datei
5
migrations/postgresql/2025-01-09-172300_add_manage/up.sql
Normale Datei
5
migrations/postgresql/2025-01-09-172300_add_manage/up.sql
Normale Datei
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
ALTER TABLE collections_groups
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
|
0
migrations/sqlite/2025-01-09-172300_add_manage/down.sql
Normale Datei
0
migrations/sqlite/2025-01-09-172300_add_manage/down.sql
Normale Datei
5
migrations/sqlite/2025-01-09-172300_add_manage/up.sql
Normale Datei
5
migrations/sqlite/2025-01-09-172300_add_manage/up.sql
Normale Datei
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
||||||
|
|
||||||
|
ALTER TABLE collections_groups
|
||||||
|
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
|
@ -99,6 +99,7 @@ const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";
|
||||||
const BASE_TEMPLATE: &str = "admin/base";
|
const BASE_TEMPLATE: &str = "admin/base";
|
||||||
|
|
||||||
const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";
|
const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";
|
||||||
|
pub const FAKE_ADMIN_UUID: &str = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
fn admin_path() -> String {
|
fn admin_path() -> String {
|
||||||
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
|
||||||
|
@ -299,7 +300,9 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
|
||||||
|
|
||||||
async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult {
|
async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult {
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_invite(user, None, None, &CONFIG.invitation_org_name(), None).await
|
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
|
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
|
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||||
} else {
|
} else {
|
||||||
let invitation = Invitation::new(&user.email);
|
let invitation = Invitation::new(&user.email);
|
||||||
invitation.save(conn).await
|
invitation.save(conn).await
|
||||||
|
@ -475,7 +478,9 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbCon
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_invite(&user, None, None, &CONFIG.invitation_org_name(), None).await
|
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
|
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
|
||||||
|
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||||
profile,
|
profile,
|
||||||
put_profile,
|
put_profile,
|
||||||
post_profile,
|
post_profile,
|
||||||
|
put_avatar,
|
||||||
get_public_keys,
|
get_public_keys,
|
||||||
post_keys,
|
post_keys,
|
||||||
post_password,
|
post_password,
|
||||||
|
@ -43,9 +44,8 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||||
post_verify_email_token,
|
post_verify_email_token,
|
||||||
post_delete_recover,
|
post_delete_recover,
|
||||||
post_delete_recover_token,
|
post_delete_recover_token,
|
||||||
post_device_token,
|
|
||||||
delete_account,
|
|
||||||
post_delete_account,
|
post_delete_account,
|
||||||
|
delete_account,
|
||||||
revision_date,
|
revision_date,
|
||||||
password_hint,
|
password_hint,
|
||||||
prelogin,
|
prelogin,
|
||||||
|
@ -53,7 +53,9 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||||
api_key,
|
api_key,
|
||||||
rotate_api_key,
|
rotate_api_key,
|
||||||
get_known_device,
|
get_known_device,
|
||||||
put_avatar,
|
get_all_devices,
|
||||||
|
get_device,
|
||||||
|
post_device_token,
|
||||||
put_device_token,
|
put_device_token,
|
||||||
put_clear_device_token,
|
put_clear_device_token,
|
||||||
post_clear_device_token,
|
post_clear_device_token,
|
||||||
|
@ -1157,6 +1159,26 @@ impl<'r> FromRequest<'r> for KnownDevice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/devices")]
|
||||||
|
async fn get_all_devices(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &mut conn).await;
|
||||||
|
let devices = devices.iter().map(|device| device.to_json()).collect::<Vec<Value>>();
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"data": devices,
|
||||||
|
"continuationToken": null,
|
||||||
|
"object": "list"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/devices/identifier/<device_id>")]
|
||||||
|
async fn get_device(device_id: DeviceId, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
let Some(device) = Device::find_by_uuid_and_user(&device_id, &headers.user.uuid, &mut conn).await else {
|
||||||
|
err!("No device found");
|
||||||
|
};
|
||||||
|
Ok(Json(device.to_json()))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct PushToken {
|
struct PushToken {
|
||||||
|
|
|
@ -4,6 +4,7 @@ use rocket::Route;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::api::admin::FAKE_ADMIN_UUID;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
|
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
|
||||||
|
@ -142,6 +143,7 @@ struct NewCollectionGroupData {
|
||||||
hide_passwords: bool,
|
hide_passwords: bool,
|
||||||
id: GroupId,
|
id: GroupId,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
|
manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -150,6 +152,7 @@ struct NewCollectionMemberData {
|
||||||
hide_passwords: bool,
|
hide_passwords: bool,
|
||||||
id: MembershipId,
|
id: MembershipId,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
|
manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -375,18 +378,13 @@ async fn get_org_collections_details(
|
||||||
|| (CONFIG.org_groups_enabled()
|
|| (CONFIG.org_groups_enabled()
|
||||||
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);
|
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);
|
||||||
|
|
||||||
// Not assigned collections should not be returned
|
|
||||||
if !assigned {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the users assigned directly to the given collection
|
// get the users assigned directly to the given collection
|
||||||
let users: Vec<Value> = col_users
|
let users: Vec<Value> = col_users
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|collection_user| collection_user.collection_uuid == col.uuid)
|
.filter(|collection_member| collection_member.collection_uuid == col.uuid)
|
||||||
.map(|collection_user| {
|
.map(|collection_member| {
|
||||||
collection_user.to_json_details_for_user(
|
collection_member.to_json_details_for_member(
|
||||||
*membership_type.get(&collection_user.membership_uuid).unwrap_or(&(MembershipType::User as i32)),
|
*membership_type.get(&collection_member.membership_uuid).unwrap_or(&(MembershipType::User as i32)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -450,7 +448,7 @@ async fn post_organization_collections(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
for group in data.groups {
|
for group in data.groups {
|
||||||
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords)
|
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
|
||||||
.save(&mut conn)
|
.save(&mut conn)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -464,12 +462,19 @@ async fn post_organization_collections(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionUser::save(&member.user_uuid, &collection.uuid, user.read_only, user.hide_passwords, &mut conn)
|
CollectionUser::save(
|
||||||
.await?;
|
&member.user_uuid,
|
||||||
|
&collection.uuid,
|
||||||
|
user.read_only,
|
||||||
|
user.hide_passwords,
|
||||||
|
user.manage,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all {
|
if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all {
|
||||||
CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, &mut conn).await?;
|
CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(collection.to_json()))
|
Ok(Json(collection.to_json()))
|
||||||
|
@ -526,7 +531,9 @@ async fn post_organization_collection_update(
|
||||||
CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?;
|
CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?;
|
||||||
|
|
||||||
for group in data.groups {
|
for group in data.groups {
|
||||||
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords).save(&mut conn).await?;
|
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
|
||||||
|
.save(&mut conn)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?;
|
CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?;
|
||||||
|
@ -540,7 +547,8 @@ async fn post_organization_collection_update(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, &mut conn).await?;
|
CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &mut conn)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await))
|
Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await))
|
||||||
|
@ -698,10 +706,10 @@ async fn get_org_collection_detail(
|
||||||
CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn)
|
CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn)
|
||||||
.await
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|collection_user| {
|
.map(|collection_member| {
|
||||||
collection_user.to_json_details_for_user(
|
collection_member.to_json_details_for_member(
|
||||||
*membership_type
|
*membership_type
|
||||||
.get(&collection_user.membership_uuid)
|
.get(&collection_member.membership_uuid)
|
||||||
.unwrap_or(&(MembershipType::User as i32)),
|
.unwrap_or(&(MembershipType::User as i32)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -771,7 +779,7 @@ async fn put_collection_users(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, &mut conn).await?;
|
CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, d.manage, &mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -891,6 +899,7 @@ struct CollectionData {
|
||||||
id: CollectionId,
|
id: CollectionId,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
hide_passwords: bool,
|
hide_passwords: bool,
|
||||||
|
manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -899,6 +908,7 @@ struct MembershipData {
|
||||||
id: MembershipId,
|
id: MembershipId,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
hide_passwords: bool,
|
hide_passwords: bool,
|
||||||
|
manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -997,8 +1007,8 @@ async fn send_invite(
|
||||||
|
|
||||||
if let Err(e) = mail::send_invite(
|
if let Err(e) = mail::send_invite(
|
||||||
&user,
|
&user,
|
||||||
Some(org_id.clone()),
|
org_id.clone(),
|
||||||
Some(new_member.uuid.clone()),
|
new_member.uuid.clone(),
|
||||||
&org_name,
|
&org_name,
|
||||||
Some(headers.user.email.clone()),
|
Some(headers.user.email.clone()),
|
||||||
)
|
)
|
||||||
|
@ -1037,6 +1047,7 @@ async fn send_invite(
|
||||||
&collection.uuid,
|
&collection.uuid,
|
||||||
col.read_only,
|
col.read_only,
|
||||||
col.hide_passwords,
|
col.hide_passwords,
|
||||||
|
col.manage,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -1124,14 +1135,7 @@ async fn _reinvite_member(
|
||||||
};
|
};
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
mail::send_invite(
|
mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_string())).await?;
|
||||||
&user,
|
|
||||||
Some(org_id.clone()),
|
|
||||||
Some(member.uuid),
|
|
||||||
&org_name,
|
|
||||||
Some(invited_by_email.to_string()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
} else if user.password_hash.is_empty() {
|
} else if user.password_hash.is_empty() {
|
||||||
let invitation = Invitation::new(&user.email);
|
let invitation = Invitation::new(&user.email);
|
||||||
invitation.save(conn).await?;
|
invitation.save(conn).await?;
|
||||||
|
@ -1163,21 +1167,24 @@ async fn accept_invite(
|
||||||
// The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead
|
// The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead
|
||||||
let data: AcceptData = data.into_inner();
|
let data: AcceptData = data.into_inner();
|
||||||
let claims = decode_invite(&data.token)?;
|
let claims = decode_invite(&data.token)?;
|
||||||
let user = headers.user;
|
|
||||||
|
// Don't allow other users from accepting an invitation.
|
||||||
|
if !claims.email.eq(&headers.user.email) {
|
||||||
|
err!("Invitation was issued to a different account", "Claim does not match user_id")
|
||||||
|
}
|
||||||
|
|
||||||
// If a claim does not have a member_id or it does not match the one in from the URI, something is wrong.
|
// If a claim does not have a member_id or it does not match the one in from the URI, something is wrong.
|
||||||
match &claims.member_id {
|
if !claims.member_id.eq(&member_id) {
|
||||||
Some(ou_id) if ou_id.eq(&member_id) => {}
|
err!("Error accepting the invitation", "Claim does not match the member_id")
|
||||||
_ => err!("Error accepting the invitation", "Claim does not match the member_id"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.email != claims.email {
|
let member = &claims.member_id;
|
||||||
err!("Invitation claim does not match the user")
|
let org = &claims.org_id;
|
||||||
}
|
|
||||||
|
|
||||||
Invitation::take(&claims.email, &mut conn).await;
|
Invitation::take(&claims.email, &mut conn).await;
|
||||||
|
|
||||||
if let (Some(member), Some(org)) = (&claims.member_id, &claims.org_id) {
|
// skip invitation logic when we were invited via the /admin panel
|
||||||
|
if **member != FAKE_ADMIN_UUID {
|
||||||
let Some(mut member) = Membership::find_by_uuid_and_org(member, org, &mut conn).await else {
|
let Some(mut member) = Membership::find_by_uuid_and_org(member, org, &mut conn).await else {
|
||||||
err!("Error accepting the invitation")
|
err!("Error accepting the invitation")
|
||||||
};
|
};
|
||||||
|
@ -1198,7 +1205,7 @@ async fn accept_invite(
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(OrgPolicyErr::TwoFactorMissing) => {
|
Err(OrgPolicyErr::TwoFactorMissing) => {
|
||||||
if CONFIG.email_2fa_auto_fallback() {
|
if CONFIG.email_2fa_auto_fallback() {
|
||||||
two_factor::email::activate_email_2fa(&user, &mut conn).await?;
|
two_factor::email::activate_email_2fa(&headers.user, &mut conn).await?;
|
||||||
} else {
|
} else {
|
||||||
err!("You cannot join this organization until you enable two-step login on your user account");
|
err!("You cannot join this organization until you enable two-step login on your user account");
|
||||||
}
|
}
|
||||||
|
@ -1219,18 +1226,16 @@ async fn accept_invite(
|
||||||
}
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
let mut org_name = CONFIG.invitation_org_name();
|
if let Some(invited_by_email) = &claims.invited_by_email {
|
||||||
if let Some(org_id) = &claims.org_id {
|
let org_name = match Organization::find_by_uuid(&claims.org_id, &mut conn).await {
|
||||||
org_name = match Organization::find_by_uuid(org_id, &mut conn).await {
|
|
||||||
Some(org) => org.name,
|
Some(org) => org.name,
|
||||||
None => err!("Organization not found."),
|
None => err!("Organization not found."),
|
||||||
};
|
};
|
||||||
};
|
|
||||||
if let Some(invited_by_email) = &claims.invited_by_email {
|
|
||||||
// User was invited to an organization, so they must be confirmed manually after acceptance
|
// User was invited to an organization, so they must be confirmed manually after acceptance
|
||||||
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name).await?;
|
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name).await?;
|
||||||
} else {
|
} else {
|
||||||
// User was invited from /admin, so they are automatically confirmed
|
// User was invited from /admin, so they are automatically confirmed
|
||||||
|
let org_name = CONFIG.invitation_org_name();
|
||||||
mail::send_invite_confirmed(&claims.email, &org_name).await?;
|
mail::send_invite_confirmed(&claims.email, &org_name).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1535,6 +1540,7 @@ async fn edit_member(
|
||||||
&collection.uuid,
|
&collection.uuid,
|
||||||
col.read_only,
|
col.read_only,
|
||||||
col.hide_passwords,
|
col.hide_passwords,
|
||||||
|
col.manage,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -1852,23 +1858,17 @@ async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn:
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/policies/token?<token>")]
|
#[get("/organizations/<org_id>/policies/token?<token>")]
|
||||||
async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbConn) -> JsonResult {
|
async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbConn) -> JsonResult {
|
||||||
// web-vault 2024.6.2 seems to send these values and cause logs to output errors
|
|
||||||
// Catch this and prevent errors in the logs
|
|
||||||
// TODO: CleanUp after 2024.6.x is not used anymore.
|
|
||||||
if org_id.as_ref() == "undefined" && token == "undefined" {
|
|
||||||
return Ok(Json(json!({})));
|
|
||||||
}
|
|
||||||
|
|
||||||
let invite = decode_invite(token)?;
|
let invite = decode_invite(token)?;
|
||||||
|
|
||||||
let Some(invite_org_id) = invite.org_id else {
|
if invite.org_id != org_id {
|
||||||
err!("Invalid token")
|
|
||||||
};
|
|
||||||
|
|
||||||
if invite_org_id != org_id {
|
|
||||||
err!("Token doesn't match request organization");
|
err!("Token doesn't match request organization");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exit early when we have been invited via /admin panel
|
||||||
|
if org_id.as_ref() == FAKE_ADMIN_UUID {
|
||||||
|
return Ok(Json(json!({})));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: We receive the invite token as ?token=<>, validate it contains the org id
|
// TODO: We receive the invite token as ?token=<>, validate it contains the org id
|
||||||
let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await;
|
let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await;
|
||||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
@ -2183,8 +2183,8 @@ async fn import(org_id: OrganizationId, data: Json<OrgImportData>, headers: Head
|
||||||
|
|
||||||
mail::send_invite(
|
mail::send_invite(
|
||||||
&user,
|
&user,
|
||||||
Some(org_id.clone()),
|
org_id.clone(),
|
||||||
Some(new_member.uuid.clone()),
|
new_member.uuid.clone(),
|
||||||
&org_name,
|
&org_name,
|
||||||
Some(headers.user.email.clone()),
|
Some(headers.user.email.clone()),
|
||||||
)
|
)
|
||||||
|
@ -2527,11 +2527,12 @@ struct SelectedCollection {
|
||||||
id: CollectionId,
|
id: CollectionId,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
hide_passwords: bool,
|
hide_passwords: bool,
|
||||||
|
manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectedCollection {
|
impl SelectedCollection {
|
||||||
pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup {
|
pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup {
|
||||||
CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords)
|
CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,14 +119,8 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
|
||||||
None => err!("Error looking up organization"),
|
None => err!("Error looking up organization"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = mail::send_invite(
|
if let Err(e) =
|
||||||
&user,
|
mail::send_invite(&user, org_id.clone(), new_member.uuid.clone(), &org_name, Some(org_email)).await
|
||||||
Some(org_id.clone()),
|
|
||||||
Some(new_member.uuid.clone()),
|
|
||||||
&org_name,
|
|
||||||
Some(org_email),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
// Upon error delete the user, invite and org member records when needed
|
// Upon error delete the user, invite and org member records when needed
|
||||||
if user_created {
|
if user_created {
|
||||||
|
|
|
@ -291,9 +291,7 @@ fn get_favicons_node(dom: Tokenizer<StringReader<'_>, FaviconEmitter>, icons: &m
|
||||||
TAG_HEAD if token.closing => {
|
TAG_HEAD if token.closing => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,6 @@ fn websockets_hub<'r>(
|
||||||
|
|
||||||
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
|
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
|
||||||
yield Message::binary(INITIAL_RESPONSE);
|
yield Message::binary(INITIAL_RESPONSE);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +224,6 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R
|
||||||
|
|
||||||
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
|
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
|
||||||
yield Message::binary(INITIAL_RESPONSE);
|
yield Message::binary(INITIAL_RESPONSE);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -272,16 +272,16 @@ pub struct InviteJwtClaims {
|
||||||
pub sub: UserId,
|
pub sub: UserId,
|
||||||
|
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub org_id: Option<OrganizationId>,
|
pub org_id: OrganizationId,
|
||||||
pub member_id: Option<MembershipId>,
|
pub member_id: MembershipId,
|
||||||
pub invited_by_email: Option<String>,
|
pub invited_by_email: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_invite_claims(
|
pub fn generate_invite_claims(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
email: String,
|
email: String,
|
||||||
org_id: Option<OrganizationId>,
|
org_id: OrganizationId,
|
||||||
member_id: Option<MembershipId>,
|
member_id: MembershipId,
|
||||||
invited_by_email: Option<String>,
|
invited_by_email: Option<String>,
|
||||||
) -> InviteJwtClaims {
|
) -> InviteJwtClaims {
|
||||||
let time_now = Utc::now();
|
let time_now = Utc::now();
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use super::{DeviceId, OrganizationId, UserId};
|
use super::{DeviceId, OrganizationId, UserId};
|
||||||
use crate::crypto::ct_eq;
|
use crate::{crypto::ct_eq, util::format_date};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use derive_more::{AsRef, Deref, Display, From};
|
use derive_more::{AsRef, Deref, Display, From};
|
||||||
use macros::UuidFromParam;
|
use macros::UuidFromParam;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
|
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
|
||||||
|
@ -64,6 +65,13 @@ impl AuthRequest {
|
||||||
authentication_date: None,
|
authentication_date: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_json_for_pending_device(&self) -> Value {
|
||||||
|
json!({
|
||||||
|
"id": self.uuid,
|
||||||
|
"creationDate": format_date(&self.creation_date),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
@ -133,6 +141,20 @@ impl AuthRequest {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_user_and_requested_device(
|
||||||
|
user_uuid: &UserId,
|
||||||
|
device_uuid: &DeviceId,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Option<Self> {
|
||||||
|
db_run! {conn: {
|
||||||
|
auth_requests::table
|
||||||
|
.filter(auth_requests::user_uuid.eq(user_uuid))
|
||||||
|
.filter(auth_requests::request_device_identifier.eq(device_uuid))
|
||||||
|
.order_by(auth_requests::creation_date.desc())
|
||||||
|
.first::<AuthRequestDb>(conn).ok().from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_created_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
|
pub async fn find_created_before(dt: &NaiveDateTime, conn: &mut DbConn) -> Vec<Self> {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
auth_requests::table
|
auth_requests::table
|
||||||
|
|
|
@ -158,16 +158,16 @@ impl Cipher {
|
||||||
|
|
||||||
// We don't need these values at all for Organizational syncs
|
// We don't need these values at all for Organizational syncs
|
||||||
// Skip any other database calls if this is the case and just return false.
|
// Skip any other database calls if this is the case and just return false.
|
||||||
let (read_only, hide_passwords) = if sync_type == CipherSyncType::User {
|
let (read_only, hide_passwords, _) = if sync_type == CipherSyncType::User {
|
||||||
match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {
|
match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {
|
||||||
Some((ro, hp)) => (ro, hp),
|
Some((ro, hp, mn)) => (ro, hp, mn),
|
||||||
None => {
|
None => {
|
||||||
error!("Cipher ownership assertion failure");
|
error!("Cipher ownership assertion failure");
|
||||||
(true, true)
|
(true, true, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(false, false)
|
(false, false, false)
|
||||||
};
|
};
|
||||||
|
|
||||||
let fields_json: Vec<_> = self
|
let fields_json: Vec<_> = self
|
||||||
|
@ -567,14 +567,14 @@ impl Cipher {
|
||||||
/// Returns the user's access restrictions to this cipher. A return value
|
/// Returns the user's access restrictions to this cipher. A return value
|
||||||
/// of None means that this cipher does not belong to the user, and is
|
/// of None means that this cipher does not belong to the user, and is
|
||||||
/// not in any collection the user has access to. Otherwise, the user has
|
/// not in any collection the user has access to. Otherwise, the user has
|
||||||
/// access to this cipher, and Some(read_only, hide_passwords) represents
|
/// access to this cipher, and Some(read_only, hide_passwords, manage) represents
|
||||||
/// the access restrictions.
|
/// the access restrictions.
|
||||||
pub async fn get_access_restrictions(
|
pub async fn get_access_restrictions(
|
||||||
&self,
|
&self,
|
||||||
user_uuid: &UserId,
|
user_uuid: &UserId,
|
||||||
cipher_sync_data: Option<&CipherSyncData>,
|
cipher_sync_data: Option<&CipherSyncData>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> Option<(bool, bool)> {
|
) -> Option<(bool, bool, bool)> {
|
||||||
// Check whether this cipher is directly owned by the user, or is in
|
// Check whether this cipher is directly owned by the user, or is in
|
||||||
// a collection that the user has full access to. If so, there are no
|
// a collection that the user has full access to. If so, there are no
|
||||||
// access restrictions.
|
// access restrictions.
|
||||||
|
@ -582,21 +582,21 @@ impl Cipher {
|
||||||
|| self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await
|
|| self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await
|
||||||
|| self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await
|
|| self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await
|
||||||
{
|
{
|
||||||
return Some((false, false));
|
return Some((false, false, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = if let Some(cipher_sync_data) = cipher_sync_data {
|
let rows = if let Some(cipher_sync_data) = cipher_sync_data {
|
||||||
let mut rows: Vec<(bool, bool)> = Vec::new();
|
let mut rows: Vec<(bool, bool, bool)> = Vec::new();
|
||||||
if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
|
if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
|
||||||
for collection in collections {
|
for collection in collections {
|
||||||
//User permissions
|
//User permissions
|
||||||
if let Some(uc) = cipher_sync_data.user_collections.get(collection) {
|
if let Some(cu) = cipher_sync_data.user_collections.get(collection) {
|
||||||
rows.push((uc.read_only, uc.hide_passwords));
|
rows.push((cu.read_only, cu.hide_passwords, cu.manage));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Group permissions
|
//Group permissions
|
||||||
if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
|
if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
|
||||||
rows.push((cg.read_only, cg.hide_passwords));
|
rows.push((cg.read_only, cg.hide_passwords, cg.manage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -623,15 +623,21 @@ impl Cipher {
|
||||||
// booleans and this behavior isn't portable anyway.
|
// booleans and this behavior isn't portable anyway.
|
||||||
let mut read_only = true;
|
let mut read_only = true;
|
||||||
let mut hide_passwords = true;
|
let mut hide_passwords = true;
|
||||||
for (ro, hp) in rows.iter() {
|
let mut manage = false;
|
||||||
|
for (ro, hp, mn) in rows.iter() {
|
||||||
read_only &= ro;
|
read_only &= ro;
|
||||||
hide_passwords &= hp;
|
hide_passwords &= hp;
|
||||||
|
manage &= mn;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((read_only, hide_passwords))
|
Some((read_only, hide_passwords, manage))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> {
|
async fn get_user_collections_access_flags(
|
||||||
|
&self,
|
||||||
|
user_uuid: &UserId,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Vec<(bool, bool, bool)> {
|
||||||
db_run! {conn: {
|
db_run! {conn: {
|
||||||
// Check whether this cipher is in any collections accessible to the
|
// Check whether this cipher is in any collections accessible to the
|
||||||
// user. If so, retrieve the access flags for each collection.
|
// user. If so, retrieve the access flags for each collection.
|
||||||
|
@ -642,13 +648,17 @@ impl Cipher {
|
||||||
.inner_join(users_collections::table.on(
|
.inner_join(users_collections::table.on(
|
||||||
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
|
||||||
.and(users_collections::user_uuid.eq(user_uuid))))
|
.and(users_collections::user_uuid.eq(user_uuid))))
|
||||||
.select((users_collections::read_only, users_collections::hide_passwords))
|
.select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
|
||||||
.load::<(bool, bool)>(conn)
|
.load::<(bool, bool, bool)>(conn)
|
||||||
.expect("Error getting user access restrictions")
|
.expect("Error getting user access restrictions")
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> {
|
async fn get_group_collections_access_flags(
|
||||||
|
&self,
|
||||||
|
user_uuid: &UserId,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Vec<(bool, bool, bool)> {
|
||||||
if !CONFIG.org_groups_enabled() {
|
if !CONFIG.org_groups_enabled() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
@ -668,15 +678,15 @@ impl Cipher {
|
||||||
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
|
||||||
))
|
))
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.select((collections_groups::read_only, collections_groups::hide_passwords))
|
.select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))
|
||||||
.load::<(bool, bool)>(conn)
|
.load::<(bool, bool, bool)>(conn)
|
||||||
.expect("Error getting group access restrictions")
|
.expect("Error getting group access restrictions")
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||||
match self.get_access_restrictions(user_uuid, None, conn).await {
|
match self.get_access_restrictions(user_uuid, None, conn).await {
|
||||||
Some((read_only, _hide_passwords)) => !read_only,
|
Some((read_only, _hide_passwords, manage)) => !read_only || manage,
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ db_object! {
|
||||||
pub collection_uuid: CollectionId,
|
pub collection_uuid: CollectionId,
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
pub hide_passwords: bool,
|
pub hide_passwords: bool,
|
||||||
|
pub manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
|
@ -83,18 +84,26 @@ impl Collection {
|
||||||
cipher_sync_data: Option<&crate::api::core::CipherSyncData>,
|
cipher_sync_data: Option<&crate::api::core::CipherSyncData>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data {
|
let (read_only, hide_passwords, manage) = if let Some(cipher_sync_data) = cipher_sync_data {
|
||||||
match cipher_sync_data.members.get(&self.org_uuid) {
|
match cipher_sync_data.members.get(&self.org_uuid) {
|
||||||
// Only for Manager types Bitwarden returns true for the can_manage option
|
// Only for Manager types Bitwarden returns true for the manage option
|
||||||
// Owners and Admins always have true
|
// Owners and Admins always have true. Users are not able to have full access
|
||||||
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
|
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
// Only let a manager manage collections when the have full read/write access
|
// Only let a manager manage collections when the have full read/write access
|
||||||
let is_manager = m.atype == MembershipType::Manager;
|
let is_manager = m.atype == MembershipType::Manager;
|
||||||
if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) {
|
if let Some(cu) = cipher_sync_data.user_collections.get(&self.uuid) {
|
||||||
(uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords)
|
(
|
||||||
|
cu.read_only,
|
||||||
|
cu.hide_passwords,
|
||||||
|
cu.manage || (is_manager && !cu.read_only && !cu.hide_passwords),
|
||||||
|
)
|
||||||
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
|
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
|
||||||
(cg.read_only, cg.hide_passwords, is_manager && !cg.read_only && !cg.hide_passwords)
|
(
|
||||||
|
cg.read_only,
|
||||||
|
cg.hide_passwords,
|
||||||
|
cg.manage || (is_manager && !cg.read_only && !cg.hide_passwords),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(false, false, false)
|
(false, false, false)
|
||||||
}
|
}
|
||||||
|
@ -104,17 +113,14 @@ impl Collection {
|
||||||
} else {
|
} else {
|
||||||
match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
|
match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
|
||||||
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
|
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
|
||||||
|
Some(_) if self.is_manageable_by_user(user_uuid, conn).await => (false, false, true),
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
let is_manager = m.atype == MembershipType::Manager;
|
let is_manager = m.atype == MembershipType::Manager;
|
||||||
let read_only = !self.is_writable_by_user(user_uuid, conn).await;
|
let read_only = !self.is_writable_by_user(user_uuid, conn).await;
|
||||||
let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await;
|
let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await;
|
||||||
(read_only, hide_passwords, is_manager && !read_only && !hide_passwords)
|
(read_only, hide_passwords, is_manager && !read_only && !hide_passwords)
|
||||||
}
|
}
|
||||||
_ => (
|
_ => (true, true, false),
|
||||||
!self.is_writable_by_user(user_uuid, conn).await,
|
|
||||||
self.hide_passwords_for_user(user_uuid, conn).await,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,7 +128,7 @@ impl Collection {
|
||||||
json_object["object"] = json!("collectionDetails");
|
json_object["object"] = json!("collectionDetails");
|
||||||
json_object["readOnly"] = json!(read_only);
|
json_object["readOnly"] = json!(read_only);
|
||||||
json_object["hidePasswords"] = json!(hide_passwords);
|
json_object["hidePasswords"] = json!(hide_passwords);
|
||||||
json_object["manage"] = json!(can_manage);
|
json_object["manage"] = json!(manage);
|
||||||
json_object
|
json_object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,6 +513,52 @@ impl Collection {
|
||||||
.unwrap_or(0) != 0
|
.unwrap_or(0) != 0
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
|
||||||
|
let user_uuid = user_uuid.to_string();
|
||||||
|
db_run! { conn: {
|
||||||
|
collections::table
|
||||||
|
.left_join(users_collections::table.on(
|
||||||
|
users_collections::collection_uuid.eq(collections::uuid).and(
|
||||||
|
users_collections::user_uuid.eq(user_uuid.clone())
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.left_join(users_organizations::table.on(
|
||||||
|
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
||||||
|
users_organizations::user_uuid.eq(user_uuid)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.left_join(groups_users::table.on(
|
||||||
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||||
|
))
|
||||||
|
.left_join(groups::table.on(
|
||||||
|
groups::uuid.eq(groups_users::groups_uuid)
|
||||||
|
))
|
||||||
|
.left_join(collections_groups::table.on(
|
||||||
|
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||||
|
collections_groups::collections_uuid.eq(collections::uuid)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.filter(collections::uuid.eq(&self.uuid))
|
||||||
|
.filter(
|
||||||
|
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::manage.eq(true)).or(// Directly accessed collection
|
||||||
|
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||||
|
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
|
||||||
|
)).or(
|
||||||
|
groups::access_all.eq(true) // access_all in groups
|
||||||
|
).or( // access via groups
|
||||||
|
groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(
|
||||||
|
collections_groups::collections_uuid.is_not_null().and(
|
||||||
|
collections_groups::manage.eq(true))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.count()
|
||||||
|
.first::<i64>(conn)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(0) != 0
|
||||||
|
}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
|
@ -537,7 +589,7 @@ impl CollectionUser {
|
||||||
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
|
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
|
||||||
.filter(collections::org_uuid.eq(org_uuid))
|
.filter(collections::org_uuid.eq(org_uuid))
|
||||||
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
|
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
|
||||||
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords))
|
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
|
||||||
.load::<CollectionUserDb>(conn)
|
.load::<CollectionUserDb>(conn)
|
||||||
.expect("Error loading users_collections")
|
.expect("Error loading users_collections")
|
||||||
.from_db()
|
.from_db()
|
||||||
|
@ -550,6 +602,7 @@ impl CollectionUser {
|
||||||
collection_uuid: &CollectionId,
|
collection_uuid: &CollectionId,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
hide_passwords: bool,
|
hide_passwords: bool,
|
||||||
|
manage: bool,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
User::update_uuid_revision(user_uuid, conn).await;
|
User::update_uuid_revision(user_uuid, conn).await;
|
||||||
|
@ -562,6 +615,7 @@ impl CollectionUser {
|
||||||
users_collections::collection_uuid.eq(collection_uuid),
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::read_only.eq(read_only),
|
||||||
users_collections::hide_passwords.eq(hide_passwords),
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
|
users_collections::manage.eq(manage),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
{
|
{
|
||||||
|
@ -576,6 +630,7 @@ impl CollectionUser {
|
||||||
users_collections::collection_uuid.eq(collection_uuid),
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::read_only.eq(read_only),
|
||||||
users_collections::hide_passwords.eq(hide_passwords),
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
|
users_collections::manage.eq(manage),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding user to collection")
|
.map_res("Error adding user to collection")
|
||||||
|
@ -590,12 +645,14 @@ impl CollectionUser {
|
||||||
users_collections::collection_uuid.eq(collection_uuid),
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::read_only.eq(read_only),
|
||||||
users_collections::hide_passwords.eq(hide_passwords),
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
|
users_collections::manage.eq(manage),
|
||||||
))
|
))
|
||||||
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
|
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
|
||||||
.do_update()
|
.do_update()
|
||||||
.set((
|
.set((
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::read_only.eq(read_only),
|
||||||
users_collections::hide_passwords.eq(hide_passwords),
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
|
users_collections::manage.eq(manage),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding user to collection")
|
.map_res("Error adding user to collection")
|
||||||
|
@ -636,7 +693,7 @@ impl CollectionUser {
|
||||||
users_collections::table
|
users_collections::table
|
||||||
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
.filter(users_collections::collection_uuid.eq(collection_uuid))
|
||||||
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
|
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
|
||||||
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords))
|
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
|
||||||
.load::<CollectionUserDb>(conn)
|
.load::<CollectionUserDb>(conn)
|
||||||
.expect("Error loading users_collections")
|
.expect("Error loading users_collections")
|
||||||
.from_db()
|
.from_db()
|
||||||
|
@ -787,15 +844,17 @@ pub struct CollectionMembership {
|
||||||
pub collection_uuid: CollectionId,
|
pub collection_uuid: CollectionId,
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
pub hide_passwords: bool,
|
pub hide_passwords: bool,
|
||||||
|
pub manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionMembership {
|
impl CollectionMembership {
|
||||||
pub fn to_json_details_for_user(&self, membership_type: i32) -> Value {
|
pub fn to_json_details_for_member(&self, membership_type: i32) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"id": self.membership_uuid,
|
"id": self.membership_uuid,
|
||||||
"readOnly": self.read_only,
|
"readOnly": self.read_only,
|
||||||
"hidePasswords": self.hide_passwords,
|
"hidePasswords": self.hide_passwords,
|
||||||
"manage": membership_type >= MembershipType::Admin
|
"manage": membership_type >= MembershipType::Admin
|
||||||
|
|| self.manage
|
||||||
|| (membership_type == MembershipType::Manager
|
|| (membership_type == MembershipType::Manager
|
||||||
&& !self.read_only
|
&& !self.read_only
|
||||||
&& !self.hide_passwords),
|
&& !self.hide_passwords),
|
||||||
|
@ -810,6 +869,7 @@ impl From<CollectionUser> for CollectionMembership {
|
||||||
collection_uuid: c.collection_uuid,
|
collection_uuid: c.collection_uuid,
|
||||||
read_only: c.read_only,
|
read_only: c.read_only,
|
||||||
hide_passwords: c.hide_passwords,
|
hide_passwords: c.hide_passwords,
|
||||||
|
manage: c.manage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,10 @@ use chrono::{NaiveDateTime, Utc};
|
||||||
|
|
||||||
use data_encoding::{BASE64, BASE64URL};
|
use data_encoding::{BASE64, BASE64URL};
|
||||||
use derive_more::{Display, From};
|
use derive_more::{Display, From};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::UserId;
|
use super::{AuthRequest, UserId};
|
||||||
use crate::crypto;
|
use crate::{crypto, util::format_date};
|
||||||
use macros::IdFromParam;
|
use macros::IdFromParam;
|
||||||
|
|
||||||
db_object! {
|
db_object! {
|
||||||
|
@ -25,7 +26,6 @@ db_object! {
|
||||||
pub push_token: Option<String>,
|
pub push_token: Option<String>,
|
||||||
|
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
|
|
||||||
pub twofactor_remember: Option<String>,
|
pub twofactor_remember: Option<String>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,18 @@ impl Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> Value {
|
||||||
|
json!({
|
||||||
|
"id": self.uuid,
|
||||||
|
"name": self.name,
|
||||||
|
"type": self.atype,
|
||||||
|
"identifier": self.push_uuid,
|
||||||
|
"creationDate": format_date(&self.created_at),
|
||||||
|
"isTrusted": false,
|
||||||
|
"object":"device"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn refresh_twofactor_remember(&mut self) -> String {
|
pub fn refresh_twofactor_remember(&mut self) -> String {
|
||||||
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
|
||||||
self.twofactor_remember = Some(twofactor_remember.clone());
|
self.twofactor_remember = Some(twofactor_remember.clone());
|
||||||
|
@ -71,6 +83,36 @@ impl Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DeviceWithAuthRequest {
|
||||||
|
pub device: Device,
|
||||||
|
pub pending_auth_request: Option<AuthRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceWithAuthRequest {
|
||||||
|
pub fn to_json(&self) -> Value {
|
||||||
|
let auth_request = match &self.pending_auth_request {
|
||||||
|
Some(auth_request) => auth_request.to_json_for_pending_device(),
|
||||||
|
None => Value::Null,
|
||||||
|
};
|
||||||
|
json!({
|
||||||
|
"id": self.device.uuid,
|
||||||
|
"name": self.device.name,
|
||||||
|
"type": self.device.atype,
|
||||||
|
"identifier": self.device.push_uuid,
|
||||||
|
"creationDate": format_date(&self.device.created_at),
|
||||||
|
"devicePendingAuthRequest": auth_request,
|
||||||
|
"isTrusted": false,
|
||||||
|
"object": "device",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from(c: Device, a: Option<AuthRequest>) -> Self {
|
||||||
|
Self {
|
||||||
|
device: c,
|
||||||
|
pending_auth_request: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
use crate::db::DbConn;
|
use crate::db::DbConn;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
|
@ -117,6 +159,16 @@ impl Device {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<DeviceWithAuthRequest> {
|
||||||
|
let devices = Self::find_by_user(user_uuid, conn).await;
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for device in devices {
|
||||||
|
let auth_request = AuthRequest::find_by_user_and_requested_device(user_uuid, &device.uuid, conn).await;
|
||||||
|
result.push(DeviceWithAuthRequest::from(device, auth_request));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
devices::table
|
devices::table
|
||||||
|
|
|
@ -29,6 +29,7 @@ db_object! {
|
||||||
pub groups_uuid: GroupId,
|
pub groups_uuid: GroupId,
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
pub hide_passwords: bool,
|
pub hide_passwords: bool,
|
||||||
|
pub manage: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Insertable)]
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
|
@ -92,7 +93,7 @@ impl Group {
|
||||||
"id": entry.collections_uuid,
|
"id": entry.collections_uuid,
|
||||||
"readOnly": entry.read_only,
|
"readOnly": entry.read_only,
|
||||||
"hidePasswords": entry.hide_passwords,
|
"hidePasswords": entry.hide_passwords,
|
||||||
"manage": !entry.read_only && !entry.hide_passwords,
|
"manage": entry.manage,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -118,12 +119,19 @@ impl Group {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionGroup {
|
impl CollectionGroup {
|
||||||
pub fn new(collections_uuid: CollectionId, groups_uuid: GroupId, read_only: bool, hide_passwords: bool) -> Self {
|
pub fn new(
|
||||||
|
collections_uuid: CollectionId,
|
||||||
|
groups_uuid: GroupId,
|
||||||
|
read_only: bool,
|
||||||
|
hide_passwords: bool,
|
||||||
|
manage: bool,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
collections_uuid,
|
collections_uuid,
|
||||||
groups_uuid,
|
groups_uuid,
|
||||||
read_only,
|
read_only,
|
||||||
hide_passwords,
|
hide_passwords,
|
||||||
|
manage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,11 +139,12 @@ impl CollectionGroup {
|
||||||
// If both read_only and hide_passwords are false, then manage should be true
|
// If both read_only and hide_passwords are false, then manage should be true
|
||||||
// You can't have an entry with read_only and manage, or hide_passwords and manage
|
// You can't have an entry with read_only and manage, or hide_passwords and manage
|
||||||
// Or an entry with everything to false
|
// Or an entry with everything to false
|
||||||
|
// For backwards compaibility and migration proposes we keep checking read_only and hide_password
|
||||||
json!({
|
json!({
|
||||||
"id": self.groups_uuid,
|
"id": self.groups_uuid,
|
||||||
"readOnly": self.read_only,
|
"readOnly": self.read_only,
|
||||||
"hidePasswords": self.hide_passwords,
|
"hidePasswords": self.hide_passwords,
|
||||||
"manage": !self.read_only && !self.hide_passwords,
|
"manage": self.manage || (!self.read_only && !self.hide_passwords),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,6 +328,7 @@ impl CollectionGroup {
|
||||||
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
||||||
collections_groups::read_only.eq(&self.read_only),
|
collections_groups::read_only.eq(&self.read_only),
|
||||||
collections_groups::hide_passwords.eq(&self.hide_passwords),
|
collections_groups::hide_passwords.eq(&self.hide_passwords),
|
||||||
|
collections_groups::manage.eq(&self.manage),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
{
|
{
|
||||||
|
@ -333,6 +343,7 @@ impl CollectionGroup {
|
||||||
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
||||||
collections_groups::read_only.eq(&self.read_only),
|
collections_groups::read_only.eq(&self.read_only),
|
||||||
collections_groups::hide_passwords.eq(&self.hide_passwords),
|
collections_groups::hide_passwords.eq(&self.hide_passwords),
|
||||||
|
collections_groups::manage.eq(&self.manage),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding group to collection")
|
.map_res("Error adding group to collection")
|
||||||
|
@ -347,12 +358,14 @@ impl CollectionGroup {
|
||||||
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
collections_groups::groups_uuid.eq(&self.groups_uuid),
|
||||||
collections_groups::read_only.eq(self.read_only),
|
collections_groups::read_only.eq(self.read_only),
|
||||||
collections_groups::hide_passwords.eq(self.hide_passwords),
|
collections_groups::hide_passwords.eq(self.hide_passwords),
|
||||||
|
collections_groups::manage.eq(self.manage),
|
||||||
))
|
))
|
||||||
.on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid))
|
.on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid))
|
||||||
.do_update()
|
.do_update()
|
||||||
.set((
|
.set((
|
||||||
collections_groups::read_only.eq(self.read_only),
|
collections_groups::read_only.eq(self.read_only),
|
||||||
collections_groups::hide_passwords.eq(self.hide_passwords),
|
collections_groups::hide_passwords.eq(self.hide_passwords),
|
||||||
|
collections_groups::manage.eq(self.manage),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_res("Error adding group to collection")
|
.map_res("Error adding group to collection")
|
||||||
|
|
|
@ -522,13 +522,13 @@ impl Membership {
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| {
|
.filter_map(|c| {
|
||||||
let (read_only, hide_passwords, can_manage) = if self.has_full_access() {
|
let (read_only, hide_passwords, manage) = if self.has_full_access() {
|
||||||
(false, false, self.atype >= MembershipType::Manager)
|
(false, false, self.atype >= MembershipType::Manager)
|
||||||
} else if let Some(cu) = cu.get(&c.uuid) {
|
} else if let Some(cu) = cu.get(&c.uuid) {
|
||||||
(
|
(
|
||||||
cu.read_only,
|
cu.read_only,
|
||||||
cu.hide_passwords,
|
cu.hide_passwords,
|
||||||
self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords,
|
cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),
|
||||||
)
|
)
|
||||||
// If previous checks failed it might be that this user has access via a group, but we should not return those elements here
|
// If previous checks failed it might be that this user has access via a group, but we should not return those elements here
|
||||||
// Those are returned via a special group endpoint
|
// Those are returned via a special group endpoint
|
||||||
|
@ -542,7 +542,7 @@ impl Membership {
|
||||||
"id": c.uuid,
|
"id": c.uuid,
|
||||||
"readOnly": read_only,
|
"readOnly": read_only,
|
||||||
"hidePasswords": hide_passwords,
|
"hidePasswords": hide_passwords,
|
||||||
"manage": can_manage,
|
"manage": manage,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -611,6 +611,7 @@ impl Membership {
|
||||||
"id": self.uuid,
|
"id": self.uuid,
|
||||||
"readOnly": col_user.read_only,
|
"readOnly": col_user.read_only,
|
||||||
"hidePasswords": col_user.hide_passwords,
|
"hidePasswords": col_user.hide_passwords,
|
||||||
|
"manage": col_user.manage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -622,11 +623,12 @@ impl Membership {
|
||||||
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn).await;
|
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn).await;
|
||||||
collections
|
collections
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| {
|
.map(|cu| {
|
||||||
json!({
|
json!({
|
||||||
"id": c.collection_uuid,
|
"id": cu.collection_uuid,
|
||||||
"readOnly": c.read_only,
|
"readOnly": cu.read_only,
|
||||||
"hidePasswords": c.hide_passwords,
|
"hidePasswords": cu.hide_passwords,
|
||||||
|
"manage": cu.manage,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -226,6 +226,7 @@ table! {
|
||||||
collection_uuid -> Text,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
hide_passwords -> Bool,
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,6 +313,7 @@ table! {
|
||||||
groups_uuid -> Text,
|
groups_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
hide_passwords -> Bool,
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,6 +226,7 @@ table! {
|
||||||
collection_uuid -> Text,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
hide_passwords -> Bool,
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,6 +313,7 @@ table! {
|
||||||
groups_uuid -> Text,
|
groups_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
hide_passwords -> Bool,
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,6 +226,7 @@ table! {
|
||||||
collection_uuid -> Text,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
hide_passwords -> Bool,
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,6 +313,7 @@ table! {
|
||||||
groups_uuid -> Text,
|
groups_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
hide_passwords -> Bool,
|
hide_passwords -> Bool,
|
||||||
|
manage -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/mail.rs
16
src/mail.rs
|
@ -259,8 +259,8 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) ->
|
||||||
|
|
||||||
pub async fn send_invite(
|
pub async fn send_invite(
|
||||||
user: &User,
|
user: &User,
|
||||||
org_id: Option<OrganizationId>,
|
org_id: OrganizationId,
|
||||||
member_id: Option<MembershipId>,
|
member_id: MembershipId,
|
||||||
org_name: &str,
|
org_name: &str,
|
||||||
invited_by_email: Option<String>,
|
invited_by_email: Option<String>,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
|
@ -272,22 +272,14 @@ pub async fn send_invite(
|
||||||
invited_by_email,
|
invited_by_email,
|
||||||
);
|
);
|
||||||
let invite_token = encode_jwt(&claims);
|
let invite_token = encode_jwt(&claims);
|
||||||
let org_id = match org_id {
|
|
||||||
Some(ref org_id) => org_id.as_ref(),
|
|
||||||
None => "_",
|
|
||||||
};
|
|
||||||
let member_id = match member_id {
|
|
||||||
Some(ref member_id) => member_id.as_ref(),
|
|
||||||
None => "_",
|
|
||||||
};
|
|
||||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||||
{
|
{
|
||||||
let mut query_params = query.query_pairs_mut();
|
let mut query_params = query.query_pairs_mut();
|
||||||
query_params
|
query_params
|
||||||
.append_pair("email", &user.email)
|
.append_pair("email", &user.email)
|
||||||
.append_pair("organizationName", org_name)
|
.append_pair("organizationName", org_name)
|
||||||
.append_pair("organizationId", org_id)
|
.append_pair("organizationId", &org_id)
|
||||||
.append_pair("organizationUserId", member_id)
|
.append_pair("organizationUserId", &member_id)
|
||||||
.append_pair("token", &invite_token);
|
.append_pair("token", &invite_token);
|
||||||
|
|
||||||
if CONFIG.sso_enabled() && CONFIG.sso_only() {
|
if CONFIG.sso_enabled() && CONFIG.sso_only() {
|
||||||
|
|
Laden …
Tabelle hinzufügen
In neuem Issue referenzieren