mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2024-11-26 14:56:27 +03:00
feature: Support single organization policy
This adds back-end support for the [single organization policy](https://bitwarden.com/help/article/policies/#single-organization).
This commit is contained in:
parent
9930a0d752
commit
d014eede9a
11 changed files with 97 additions and 13 deletions
|
@ -105,7 +105,7 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> Json<Value> {
|
||||||
let collections_json: Vec<Value> =
|
let collections_json: Vec<Value> =
|
||||||
collections.iter().map(|c| c.to_json_details(&headers.user.uuid, &conn)).collect();
|
collections.iter().map(|c| c.to_json_details(&headers.user.uuid, &conn)).collect();
|
||||||
|
|
||||||
let policies = OrgPolicy::find_by_user(&headers.user.uuid, &conn);
|
let policies = OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn);
|
||||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
|
||||||
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn);
|
||||||
|
|
|
@ -683,7 +683,7 @@ fn policies_emergency_access(emer_id: String, headers: Headers, conn: DbConn) ->
|
||||||
None => err!("Grantor user not found."),
|
None => err!("Grantor user not found."),
|
||||||
};
|
};
|
||||||
|
|
||||||
let policies = OrgPolicy::find_by_user(&grantor_user.uuid, &conn);
|
let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &conn);
|
||||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
|
|
|
@ -102,6 +102,11 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
|
||||||
if !CONFIG.is_org_creation_allowed(&headers.user.email) {
|
if !CONFIG.is_org_creation_allowed(&headers.user.email) {
|
||||||
err!("User not allowed to create organizations")
|
err!("User not allowed to create organizations")
|
||||||
}
|
}
|
||||||
|
if OrgPolicy::is_applicable_to_user(&headers.user.uuid, OrgPolicyType::SingleOrg, &conn) {
|
||||||
|
err!(
|
||||||
|
"You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let data: OrgData = data.into_inner().data;
|
let data: OrgData = data.into_inner().data;
|
||||||
let (private_key, public_key) = if data.Keys.is_some() {
|
let (private_key, public_key) = if data.Keys.is_some() {
|
||||||
|
@ -747,6 +752,30 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase<AcceptD
|
||||||
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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enforce Single Organization Policy of organization user is trying to join
|
||||||
|
let single_org_policy_enabled =
|
||||||
|
match OrgPolicy::find_by_org_and_type(&user_org.org_uuid, OrgPolicyType::SingleOrg as i32, &conn) {
|
||||||
|
Some(p) => p.enabled,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
if single_org_policy_enabled && user_org.atype < UserOrgType::Admin {
|
||||||
|
let is_member_of_another_org = UserOrganization::find_any_state_by_user(&user_org.user_uuid, &conn)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|uo| uo.org_uuid != user_org.org_uuid)
|
||||||
|
.count()
|
||||||
|
> 1;
|
||||||
|
if is_member_of_another_org {
|
||||||
|
err!("You may not join this organization until you leave or remove all other organizations.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce Single Organization Policy of other organizations user is a member of
|
||||||
|
if OrgPolicy::is_applicable_to_user(&user_org.user_uuid, OrgPolicyType::SingleOrg, &conn) {
|
||||||
|
err!(
|
||||||
|
"You cannot join this organization because you are a member of an organization which forbids it"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
user_org.status = UserOrgStatus::Accepted as i32;
|
user_org.status = UserOrgStatus::Accepted as i32;
|
||||||
user_org.save(&conn)?;
|
user_org.save(&conn)?;
|
||||||
}
|
}
|
||||||
|
@ -1219,6 +1248,33 @@ fn put_policy(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If enabling the SingleOrg policy, remove this org's members that are members of other orgs
|
||||||
|
if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled {
|
||||||
|
let org_members = UserOrganization::find_by_org(&org_id, &conn);
|
||||||
|
|
||||||
|
for member in org_members.into_iter() {
|
||||||
|
// Policy only applies to non-Owner/non-Admin members who have accepted joining the org
|
||||||
|
if member.atype < UserOrgType::Admin && member.status != UserOrgStatus::Invited as i32 {
|
||||||
|
let is_member_of_another_org = UserOrganization::find_any_state_by_user(&member.user_uuid, &conn)
|
||||||
|
.into_iter()
|
||||||
|
// Other UserOrganization's where they have accepted being a member of
|
||||||
|
.filter(|uo| uo.uuid != member.uuid && uo.status != UserOrgStatus::Invited as i32)
|
||||||
|
.count()
|
||||||
|
> 1;
|
||||||
|
|
||||||
|
if is_member_of_another_org {
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
let org = Organization::find_by_uuid(&member.org_uuid, &conn).unwrap();
|
||||||
|
let user = User::find_by_uuid(&member.user_uuid, &conn).unwrap();
|
||||||
|
|
||||||
|
mail::send_single_org_removed_from_org(&user.email, &org.name)?;
|
||||||
|
}
|
||||||
|
member.delete(&conn)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) {
|
let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()),
|
None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()),
|
||||||
|
|
|
@ -56,7 +56,7 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
|
||||||
|
|
||||||
// COMMON
|
// COMMON
|
||||||
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
|
||||||
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn);
|
||||||
|
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn);
|
||||||
|
|
||||||
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
|
||||||
device.save(&conn)?;
|
device.save(&conn)?;
|
||||||
|
|
|
@ -874,6 +874,7 @@ where
|
||||||
reg!("email/pw_hint_none", ".html");
|
reg!("email/pw_hint_none", ".html");
|
||||||
reg!("email/pw_hint_some", ".html");
|
reg!("email/pw_hint_some", ".html");
|
||||||
reg!("email/send_2fa_removed_from_org", ".html");
|
reg!("email/send_2fa_removed_from_org", ".html");
|
||||||
|
reg!("email/send_single_org_removed_from_org", ".html");
|
||||||
reg!("email/send_org_invite", ".html");
|
reg!("email/send_org_invite", ".html");
|
||||||
reg!("email/send_emergency_access_invite", ".html");
|
reg!("email/send_emergency_access_invite", ".html");
|
||||||
reg!("email/twofactor_email", ".html");
|
reg!("email/twofactor_email", ".html");
|
||||||
|
|
|
@ -27,7 +27,7 @@ pub enum OrgPolicyType {
|
||||||
TwoFactorAuthentication = 0,
|
TwoFactorAuthentication = 0,
|
||||||
MasterPassword = 1,
|
MasterPassword = 1,
|
||||||
PasswordGenerator = 2,
|
PasswordGenerator = 2,
|
||||||
// SingleOrg = 3, // Not currently supported.
|
SingleOrg = 3,
|
||||||
// RequireSso = 4, // Not currently supported.
|
// RequireSso = 4, // Not currently supported.
|
||||||
PersonalOwnership = 5,
|
PersonalOwnership = 5,
|
||||||
DisableSend = 6,
|
DisableSend = 6,
|
||||||
|
@ -143,7 +143,7 @@ impl OrgPolicy {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
org_policies::table
|
org_policies::table
|
||||||
.inner_join(
|
.inner_join(
|
||||||
|
@ -184,8 +184,8 @@ impl OrgPolicy {
|
||||||
/// and the user is not an owner or admin of that org. This is only useful for checking
|
/// and the user is not an owner or admin of that org. This is only useful for checking
|
||||||
/// applicability of policy types that have these particular semantics.
|
/// applicability of policy types that have these particular semantics.
|
||||||
pub fn is_applicable_to_user(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
|
pub fn is_applicable_to_user(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
|
||||||
// Returns confirmed users only.
|
// TODO: Should check confirmed and accepted users
|
||||||
for policy in OrgPolicy::find_by_user(user_uuid, conn) {
|
for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn) {
|
||||||
if policy.enabled && policy.has_type(policy_type) {
|
if policy.enabled && policy.has_type(policy_type) {
|
||||||
let org_uuid = &policy.org_uuid;
|
let org_uuid = &policy.org_uuid;
|
||||||
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
|
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
|
||||||
|
@ -201,8 +201,7 @@ impl OrgPolicy {
|
||||||
/// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
|
/// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
|
||||||
/// option of the `Send Options` policy, and the user is not an owner or admin of that org.
|
/// option of the `Send Options` policy, and the user is not an owner or admin of that org.
|
||||||
pub fn is_hide_email_disabled(user_uuid: &str, conn: &DbConn) -> bool {
|
pub fn is_hide_email_disabled(user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
// Returns confirmed users only.
|
for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn) {
|
||||||
for policy in OrgPolicy::find_by_user(user_uuid, conn) {
|
|
||||||
if policy.enabled && policy.has_type(OrgPolicyType::SendOptions) {
|
if policy.enabled && policy.has_type(OrgPolicyType::SendOptions) {
|
||||||
let org_uuid = &policy.org_uuid;
|
let org_uuid = &policy.org_uuid;
|
||||||
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
|
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn) {
|
||||||
|
|
|
@ -477,7 +477,7 @@ impl UserOrganization {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
users_organizations::table
|
users_organizations::table
|
||||||
.filter(users_organizations::user_uuid.eq(user_uuid))
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
|
|
|
@ -185,7 +185,7 @@ use crate::error::MapResult;
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl User {
|
impl User {
|
||||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||||
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
let orgs = UserOrganization::find_confirmed_by_user(&self.uuid, conn);
|
||||||
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(conn)).collect();
|
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(conn)).collect();
|
||||||
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ impl User {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
pub fn delete(self, conn: &DbConn) -> EmptyResult {
|
||||||
for user_org in UserOrganization::find_by_user(&self.uuid, conn) {
|
for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn) {
|
||||||
if user_org.atype == UserOrgType::Owner {
|
if user_org.atype == UserOrgType::Owner {
|
||||||
let owner_type = UserOrgType::Owner as i32;
|
let owner_type = UserOrgType::Owner as i32;
|
||||||
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, conn).len() <= 1 {
|
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, conn).len() <= 1 {
|
||||||
|
|
12
src/mail.rs
12
src/mail.rs
|
@ -195,6 +195,18 @@ pub fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyResult {
|
||||||
send_email(address, &subject, body_html, body_text)
|
send_email(address, &subject, body_html, body_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_single_org_removed_from_org(address: &str, org_name: &str) -> EmptyResult {
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/send_single_org_removed_from_org",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"org_name": org_name,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(address, &subject, body_html, body_text)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_invite(
|
pub fn send_invite(
|
||||||
address: &str,
|
address: &str,
|
||||||
uuid: &str,
|
uuid: &str,
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
You have been removed from {{{org_name}}}
|
||||||
|
<!---------------->
|
||||||
|
Your user account has been removed from the *{{org_name}}* organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account.
|
||||||
|
===
|
||||||
|
Github: https://github.com/dani-garcia/vaultwarden
|
|
@ -0,0 +1,11 @@
|
||||||
|
You have been removed from {{{org_name}}}
|
||||||
|
<!---------------->
|
||||||
|
{{> email/email_header }}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
Your user account has been removed from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{> email/email_footer }}
|
Loading…
Reference in a new issue