mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2024-12-29 00:23:23 -05:00
Merge pull request #173 from mprasil/poormans_invites
Implement poor man's invitation via Organization invitation
This commit is contained in:
commit
1b20a25514
17
README.md
17
README.md
@ -23,6 +23,7 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
|
|||||||
- [Updating the bitwarden image](#updating-the-bitwarden-image)
|
- [Updating the bitwarden image](#updating-the-bitwarden-image)
|
||||||
- [Configuring bitwarden service](#configuring-bitwarden-service)
|
- [Configuring bitwarden service](#configuring-bitwarden-service)
|
||||||
- [Disable registration of new users](#disable-registration-of-new-users)
|
- [Disable registration of new users](#disable-registration-of-new-users)
|
||||||
|
- [Disable invitations](#disable-invitations)
|
||||||
- [Enabling HTTPS](#enabling-https)
|
- [Enabling HTTPS](#enabling-https)
|
||||||
- [Enabling U2F authentication](#enabling-u2f-authentication)
|
- [Enabling U2F authentication](#enabling-u2f-authentication)
|
||||||
- [Changing persistent data location](#changing-persistent-data-location)
|
- [Changing persistent data location](#changing-persistent-data-location)
|
||||||
@ -136,6 +137,20 @@ docker run -d --name bitwarden \
|
|||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
mprasil/bitwarden:latest
|
mprasil/bitwarden:latest
|
||||||
```
|
```
|
||||||
|
Note: While users can't register on their own, they can still be invited by already registered users. Read bellow if you also want to disable that.
|
||||||
|
|
||||||
|
### Disable invitations
|
||||||
|
|
||||||
|
Even when registration is disabled, organization administrators or owners can invite users to join organization. This won't send email invitation to the users, but after they are invited, they can register with the invited email even if `SIGNUPS_ALLOWED` is actually set to `false`. You can disable this functionality completely by setting `INVITATIONS_ALLOWED` env variable to `false`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run -d --name bitwarden \
|
||||||
|
-e SIGNUPS_ALLOWED=false \
|
||||||
|
-e INVITATIONS_ALLOWED=false \
|
||||||
|
-v /bw-data/:/data/ \
|
||||||
|
-p 80:80 \
|
||||||
|
mprasil/bitwarden:latest
|
||||||
|
```
|
||||||
|
|
||||||
### Enabling HTTPS
|
### Enabling HTTPS
|
||||||
To enable HTTPS, you need to configure the `ROCKET_TLS`.
|
To enable HTTPS, you need to configure the `ROCKET_TLS`.
|
||||||
@ -365,7 +380,7 @@ We use upstream Vault interface directly without any (significant) changes, this
|
|||||||
|
|
||||||
### Inviting users into organization
|
### Inviting users into organization
|
||||||
|
|
||||||
The users must already be registered on your server to invite them, because we can't send the invitation via email. The invited users won't get the invitation email, instead they will appear in the interface as if they already accepted the invitation. Organization admin then just needs to confirm them to be proper Organization members and to give them access to the shared secrets.
|
If you have [invitations disabled](#disable-invitations), the users must already be registered on your server to invite them. The invited users won't get the invitation email, instead they will appear in the interface as if they already accepted the invitation. (if the user has already registered) Organization admin then just needs to confirm them to be proper Organization members and to give them access to the shared secrets.
|
||||||
|
|
||||||
### Running on unencrypted connection
|
### Running on unencrypted connection
|
||||||
|
|
||||||
|
1
migrations/2018-09-10-111213_add_invites/down.sql
Normal file
1
migrations/2018-09-10-111213_add_invites/down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE invitations;
|
3
migrations/2018-09-10-111213_add_invites/up.sql
Normal file
3
migrations/2018-09-10-111213_add_invites/up.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
CREATE TABLE invitations (
|
||||||
|
email TEXT NOT NULL PRIMARY KEY
|
||||||
|
);
|
@ -32,15 +32,34 @@ struct KeysData {
|
|||||||
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
|
||||||
let data: RegisterData = data.into_inner().data;
|
let data: RegisterData = data.into_inner().data;
|
||||||
|
|
||||||
if !CONFIG.signups_allowed {
|
|
||||||
err!("Signups not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if User::find_by_mail(&data.Email, &conn).is_some() {
|
let mut user = match User::find_by_mail(&data.Email, &conn) {
|
||||||
err!("Email already exists")
|
Some(mut user) => {
|
||||||
}
|
if Invitation::take(&data.Email, &conn) {
|
||||||
|
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
|
||||||
|
user_org.status = UserOrgStatus::Accepted as i32;
|
||||||
|
user_org.save(&conn);
|
||||||
|
};
|
||||||
|
user
|
||||||
|
} else {
|
||||||
|
if CONFIG.signups_allowed {
|
||||||
|
err!("Account with this email already exists")
|
||||||
|
} else {
|
||||||
|
err!("Registration not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
|
||||||
|
User::new(data.Email)
|
||||||
|
} else {
|
||||||
|
err!("Registration not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut user = User::new(data.Email, data.Key, data.MasterPasswordHash);
|
user.set_password(&data.MasterPasswordHash);
|
||||||
|
user.key = data.Key;
|
||||||
|
|
||||||
// Add extra fields if present
|
// Add extra fields if present
|
||||||
if let Some(name) = data.Name {
|
if let Some(name) = data.Name {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
use rocket_contrib::{Json, Value};
|
use rocket_contrib::{Json, Value};
|
||||||
|
use CONFIG;
|
||||||
use db::DbConn;
|
use db::DbConn;
|
||||||
use db::models::*;
|
use db::models::*;
|
||||||
|
|
||||||
@ -373,36 +373,56 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
|||||||
err!("Only Owners can invite Admins or Owners")
|
err!("Only Owners can invite Admins or Owners")
|
||||||
}
|
}
|
||||||
|
|
||||||
for user_opt in data.Emails.iter().map(|email| User::find_by_mail(email, &conn)) {
|
for email in data.Emails.iter() {
|
||||||
match user_opt {
|
let mut user_org_status = UserOrgStatus::Accepted as i32;
|
||||||
None => err!("User email does not exist"),
|
let user = match User::find_by_mail(&email, &conn) {
|
||||||
Some(user) => {
|
None => if CONFIG.invitations_allowed { // Invite user if that's enabled
|
||||||
if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() {
|
let mut invitation = Invitation::new(email.clone());
|
||||||
err!("User already in organization")
|
match invitation.save(&conn) {
|
||||||
|
Ok(()) => {
|
||||||
|
let mut user = User::new(email.clone());
|
||||||
|
if user.save(&conn) {
|
||||||
|
user_org_status = UserOrgStatus::Invited as i32;
|
||||||
|
user
|
||||||
|
} else {
|
||||||
|
err!("Failed to create placeholder for invited user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => err!(format!("Failed to invite: {}", email))
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
} else {
|
||||||
let access_all = data.AccessAll.unwrap_or(false);
|
err!(format!("User email does not exist: {}", email))
|
||||||
new_user.access_all = access_all;
|
},
|
||||||
new_user.type_ = new_type;
|
Some(user) => if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() {
|
||||||
|
err!(format!("User already in organization: {}", email))
|
||||||
|
} else {
|
||||||
|
user
|
||||||
|
}
|
||||||
|
|
||||||
// If no accessAll, add the collections received
|
};
|
||||||
if !access_all {
|
|
||||||
for col in &data.Collections {
|
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
|
||||||
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
let access_all = data.AccessAll.unwrap_or(false);
|
||||||
None => err!("Collection not found in Organization"),
|
new_user.access_all = access_all;
|
||||||
Some(collection) => {
|
new_user.type_ = new_type;
|
||||||
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
|
new_user.status = user_org_status;
|
||||||
err!("Failed saving collection access for user")
|
|
||||||
}
|
// If no accessAll, add the collections received
|
||||||
}
|
if !access_all {
|
||||||
|
for col in &data.Collections {
|
||||||
|
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
||||||
|
None => err!("Collection not found in Organization"),
|
||||||
|
Some(collection) => {
|
||||||
|
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
|
||||||
|
err!("Failed saving collection access for user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new_user.save(&conn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new_user.save(&conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -12,7 +12,7 @@ pub use self::attachment::Attachment;
|
|||||||
pub use self::cipher::Cipher;
|
pub use self::cipher::Cipher;
|
||||||
pub use self::device::Device;
|
pub use self::device::Device;
|
||||||
pub use self::folder::{Folder, FolderCipher};
|
pub use self::folder::{Folder, FolderCipher};
|
||||||
pub use self::user::User;
|
pub use self::user::{User, Invitation};
|
||||||
pub use self::organization::Organization;
|
pub use self::organization::Organization;
|
||||||
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
|
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
|
||||||
pub use self::collection::{Collection, CollectionUser, CollectionCipher};
|
pub use self::collection::{Collection, CollectionUser, CollectionCipher};
|
||||||
|
@ -27,7 +27,7 @@ pub struct UserOrganization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum UserOrgStatus {
|
pub enum UserOrgStatus {
|
||||||
_Invited = 0, // Unused, users are accepted automatically
|
Invited = 0,
|
||||||
Accepted = 1,
|
Accepted = 1,
|
||||||
Confirmed = 2,
|
Confirmed = 2,
|
||||||
}
|
}
|
||||||
@ -284,6 +284,13 @@ impl UserOrganization {
|
|||||||
.load::<Self>(&**conn).unwrap_or(vec![])
|
.load::<Self>(&**conn).unwrap_or(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
|
users_organizations::table
|
||||||
|
.filter(users_organizations::user_uuid.eq(user_uuid))
|
||||||
|
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
|
||||||
|
.load::<Self>(&**conn).unwrap_or(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
|
||||||
users_organizations::table
|
users_organizations::table
|
||||||
.filter(users_organizations::org_uuid.eq(org_uuid))
|
.filter(users_organizations::org_uuid.eq(org_uuid))
|
||||||
|
@ -39,13 +39,12 @@ pub struct User {
|
|||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
impl User {
|
impl User {
|
||||||
pub fn new(mail: String, key: String, password: String) -> Self {
|
pub fn new(mail: String) -> Self {
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
let email = mail.to_lowercase();
|
let email = mail.to_lowercase();
|
||||||
|
|
||||||
let iterations = CONFIG.password_iterations;
|
let iterations = CONFIG.password_iterations;
|
||||||
let salt = crypto::get_random_64();
|
let salt = crypto::get_random_64();
|
||||||
let password_hash = crypto::hash_password(password.as_bytes(), &salt, iterations as u32);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
uuid: Uuid::new_v4().to_string(),
|
uuid: Uuid::new_v4().to_string(),
|
||||||
@ -53,9 +52,9 @@ impl User {
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
name: email.clone(),
|
name: email.clone(),
|
||||||
email,
|
email,
|
||||||
key,
|
key: String::new(),
|
||||||
|
|
||||||
password_hash,
|
password_hash: Vec::new(),
|
||||||
salt,
|
salt,
|
||||||
password_iterations: iterations,
|
password_iterations: iterations,
|
||||||
|
|
||||||
@ -103,7 +102,7 @@ impl User {
|
|||||||
use diesel;
|
use diesel;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use db::DbConn;
|
use db::DbConn;
|
||||||
use db::schema::users;
|
use db::schema::{users, invitations};
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
impl User {
|
impl User {
|
||||||
@ -186,3 +185,47 @@ impl User {
|
|||||||
.first::<Self>(&**conn).ok()
|
.first::<Self>(&**conn).ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Identifiable, Queryable, Insertable)]
|
||||||
|
#[table_name = "invitations"]
|
||||||
|
#[primary_key(email)]
|
||||||
|
pub struct Invitation {
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Invitation {
|
||||||
|
pub fn new(email: String) -> Self {
|
||||||
|
Self {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
|
||||||
|
diesel::replace_into(invitations::table)
|
||||||
|
.values(&*self)
|
||||||
|
.execute(&**conn)
|
||||||
|
.and(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
|
||||||
|
diesel::delete(invitations::table.filter(
|
||||||
|
invitations::email.eq(self.email)))
|
||||||
|
.execute(&**conn)
|
||||||
|
.and(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
|
||||||
|
let lower_mail = mail.to_lowercase();
|
||||||
|
invitations::table
|
||||||
|
.filter(invitations::email.eq(lower_mail))
|
||||||
|
.first::<Self>(&**conn).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take(mail: &str, conn: &DbConn) -> bool {
|
||||||
|
CONFIG.invitations_allowed &&
|
||||||
|
match Self::find_by_mail(mail, &conn) {
|
||||||
|
Some(invitation) => invitation.delete(&conn).is_ok(),
|
||||||
|
None => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -113,6 +113,12 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
invitations (email) {
|
||||||
|
email -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
users_collections (user_uuid, collection_uuid) {
|
users_collections (user_uuid, collection_uuid) {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
|
@ -226,6 +226,7 @@ pub struct Config {
|
|||||||
|
|
||||||
local_icon_extractor: bool,
|
local_icon_extractor: bool,
|
||||||
signups_allowed: bool,
|
signups_allowed: bool,
|
||||||
|
invitations_allowed: bool,
|
||||||
password_iterations: i32,
|
password_iterations: i32,
|
||||||
show_password_hint: bool,
|
show_password_hint: bool,
|
||||||
|
|
||||||
@ -258,6 +259,7 @@ impl Config {
|
|||||||
|
|
||||||
local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
|
local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
|
||||||
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
|
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
|
||||||
|
invitations_allowed: util::parse_option_string(env::var("INVITATIONS_ALLOWED").ok()).unwrap_or(true),
|
||||||
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
|
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
|
||||||
show_password_hint: util::parse_option_string(env::var("SHOW_PASSWORD_HINT").ok()).unwrap_or(true),
|
show_password_hint: util::parse_option_string(env::var("SHOW_PASSWORD_HINT").ok()).unwrap_or(true),
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user