Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/cloud-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ impl From<NewUser> for User {
};

User {
username: user_data.username.to_lowercase(),
username: user_data.username,
hash,
salt,
email: user_data.email,
group_id: user_data.group_id,
created_at: DateTime::from_system_time(SystemTime::now()),
linked_accounts: std::vec::Vec::new(),
linked_accounts: Vec::new(),
role: user_data.role.unwrap_or(UserRole::User),
services_hosts: None,
service_settings: HashMap::new(),
Expand Down
171 changes: 168 additions & 3 deletions crates/cloud/src/app_data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ use lettre::{Address, Message, SmtpTransport, Transport};
use log::{info, warn};
use lru::LruCache;
use mongodb::bson::{doc, DateTime, Document};
use mongodb::options::{FindOneAndUpdateOptions, IndexOptions, ReturnDocument};
use mongodb::options::{
Collation, CollationStrength, FindOneAndDeleteOptions, FindOneAndUpdateOptions, FindOneOptions,
IndexOptions, ReturnDocument,
};
use rusoto_core::credential::StaticProvider;
use rusoto_core::Region;
use serde::{Deserialize, Serialize};
Expand All @@ -33,7 +36,7 @@ use crate::libraries;
use crate::network::topology::{self, SetStorage, TopologyActor};
use actix::{Actor, Addr};
use futures::TryStreamExt;
use mongodb::{Client, Collection, IndexModel};
use mongodb::{Client, Collection, Cursor, IndexModel};
use rusoto_s3::{
CreateBucketRequest, DeleteObjectRequest, GetObjectRequest, PutObjectOutput, PutObjectRequest,
S3Client, S3,
Expand All @@ -52,7 +55,7 @@ pub struct AppData {
pub(crate) settings: Settings,
pub(crate) network: Addr<TopologyActor>,
pub(crate) groups: Collection<Group>,
pub(crate) users: Collection<User>,
users: Collection<User>,
pub(crate) banned_accounts: Collection<BannedAccount>,
pub(crate) friends: Collection<FriendLink>,
pub(crate) project_metadata: Collection<ProjectMetadata>,
Expand Down Expand Up @@ -211,6 +214,23 @@ impl AppData {
.await
.map_err(InternalError::DatabaseConnectionError)?;

let case_insensitive_col = Collation::builder()
.locale("en_US")
.strength(CollationStrength::Primary)
.build();
let user_index = IndexModel::builder()
.keys(doc! {"username": 1})
.options(
IndexOptions::builder()
.collation(case_insensitive_col)
.build(),
)
.build();
self.users
.create_index(user_index, None)
.await
.map_err(InternalError::DatabaseConnectionError)?;

self.project_metadata
.create_indexes(
vec![
Expand Down Expand Up @@ -294,6 +314,151 @@ impl AppData {
});
}

pub async fn create_user(&self, user: User) -> Result<(), UserError> {
let query = doc! {"email": &user.email};
if let Some(_account) = self
.banned_accounts
.find_one(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
{
return Err(UserError::InvalidEmailAddress);
}

let query = doc! {"username": &user.username};
let update = doc! {"$setOnInsert": &user};
let options = mongodb::options::FindOneAndUpdateOptions::builder()
.return_document(ReturnDocument::Before)
.upsert(true)
.collation(
Collation::builder()
.locale("en_US")
.strength(CollationStrength::Primary)
.build(),
)
.build();
let existing_user = self
.users
.find_one_and_update(query, update, options)
.await
.map_err(InternalError::DatabaseConnectionError)?;

if existing_user.is_some() {
Err(UserError::UserExistsError)
} else {
Ok(())
}
}

pub async fn update_user(
&self,
username: &str,
update: Document,
) -> Result<Option<User>, InternalError> {
let query = doc! {"username": username};
let collation = Collation::builder()
.locale("en_US")
.strength(CollationStrength::Primary)
.build();

let options = FindOneAndUpdateOptions::builder()
.return_document(ReturnDocument::Before)
.upsert(true)
.collation(collation)
.build();

let updated_user = self
.users
.find_one_and_update(query, update, options)
.await
.map_err(InternalError::DatabaseConnectionError)?;

Ok(updated_user)
}

pub async fn find_user(&self, username: &str) -> Result<Option<User>, InternalError> {
let query = doc! {"username": &username};
let collation = Collation::builder()
.locale("en_US")
.strength(CollationStrength::Primary)
.build();
let options = FindOneOptions::builder().collation(collation).build();
self.users
.find_one(query, Some(options))
.await
.map_err(InternalError::DatabaseConnectionError)
}

pub async fn delete_user(&self, username: &str) -> Result<Option<User>, UserError> {
let query = doc! {"username": &username};
let collation = Collation::builder()
.locale("en_US")
.strength(CollationStrength::Primary)
.build();
let options = FindOneAndDeleteOptions::builder()
.collation(collation)
.build();
let deleted_user = self
.users
.find_one_and_delete(query, Some(options))
.await
.map_err(InternalError::DatabaseConnectionError)?;

Ok(deleted_user)
}

/// Find users using an arbitrary query. Do not use this for username-based lookups!
pub async fn find_users_where(&self, query: Document) -> Result<Cursor<User>, InternalError> {
let cursor = self
.users
.find(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?;

Ok(cursor)
}

pub async fn all_users(&self) -> Result<Cursor<User>, InternalError> {
let query = doc! {};
self.find_users_where(query).await
}

/// Look up a user using a custom query (such as by a linked account).
///
/// Do not use this to look up a user by username! Usernames are case-insensitive
/// and require specific collation options (handled by `find_user` and `update_user`).
pub async fn find_user_where(&self, query: Document) -> Result<Option<User>, InternalError> {
self.users
.find_one(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)
}

pub async fn usernames_with_prefix(
&self,
prefix: &str,
) -> Result<HashSet<String>, InternalError> {
let prefix_query = mongodb::bson::Regex {
pattern: format!("^{}", &prefix),
options: String::new(),
};
let query = doc! {"username": {"$regex": prefix_query}};
// TODO: this could be optimized to map on the stream...
let names = self
.users
.find(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
.try_collect::<Vec<_>>()
.await
.map_err(InternalError::DatabaseConnectionError)?
.into_iter()
.map(|user| user.username)
.collect::<HashSet<String>>();

Ok(names)
}

pub async fn get_project_metadatum(
&self,
id: &ProjectId,
Expand Down
6 changes: 2 additions & 4 deletions crates/cloud/src/friends.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ async fn list_friends(
let is_universal_friend = matches!(get_user_role(&app, &owner).await?, UserRole::Admin);

let friend_names = if is_universal_friend {
app.users
.find(doc! {}, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
app.all_users()
.await?
.try_collect::<Vec<User>>()
.await
.map_err(InternalError::DatabaseConnectionError)?
Expand Down
6 changes: 1 addition & 5 deletions crates/cloud/src/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,7 @@ async fn list_members(

ensure_can_edit_group(&app, &session, &id).await?;
let query = doc! {"groupId": id};
let cursor = app
.users
.find(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?;
let cursor = app.find_users_where(query).await?;
let members: Vec<api::User> = cursor
.try_collect::<Vec<_>>()
.await
Expand Down
29 changes: 8 additions & 21 deletions crates/cloud/src/services/hosts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,9 @@ async fn list_user_hosts(

ensure_can_edit_user(&app, &session, &username).await?;

let query = doc! {"username": username};
let user = app
.users
.find_one(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
.find_user(&username)
.await?
.ok_or(UserError::UserNotFoundError)?;

Ok(HttpResponse::Ok().json(user.services_hosts.unwrap_or_default()))
Expand All @@ -109,18 +106,11 @@ async fn set_user_hosts(

let service_hosts: Vec<ServiceHost> = hosts.into_inner();
let update = doc! {"$set": {"servicesHosts": &service_hosts }};
let filter = doc! {"username": username};
let result = app
.users
.update_one(filter, update, None)
.await
.map_err(InternalError::DatabaseConnectionError)?;
app.update_user(&username, update)
.await?
.ok_or(UserError::UserNotFoundError)?;

if result.modified_count == 1 {
Ok(HttpResponse::Ok().finish())
} else {
Ok(HttpResponse::NotFound().finish())
}
Ok(HttpResponse::Ok().finish())
}

#[get("/all/{username}")]
Expand All @@ -132,12 +122,9 @@ async fn list_all_hosts(
let (username,) = path.into_inner();
ensure_can_edit_user(&app, &session, &username).await?;

let query = doc! {"username": &username};
let user = app
.users
.find_one(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
.find_user(&username)
.await?
.ok_or(UserError::UserNotFoundError)?;

let mut groups = app
Expand Down
52 changes: 14 additions & 38 deletions crates/cloud/src/services/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,9 @@ async fn list_user_hosts_with_settings(
let (username,) = path.into_inner();
ensure_can_edit_user(&app, &session, &username).await?;

let query = doc! {"username": &username};
let user = app
.users
.find_one(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
.find_user(&username)
.await?
.ok_or(UserError::UserNotFoundError)?;

let hosts: Vec<_> = user.service_settings.keys().collect();
Expand All @@ -42,12 +39,9 @@ async fn get_user_settings(
let (username, host) = path.into_inner();
ensure_can_edit_user(&app, &session, &username).await?;

let query = doc! {"username": &username};
let user = app
.users
.find_one(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
.find_user(&username)
.await?
.ok_or(UserError::UserNotFoundError)?;

let default_settings = String::from("");
Expand All @@ -71,12 +65,9 @@ async fn get_all_settings(
ensure_can_edit_user(&app, &session, &username).await?;
}

let query = doc! {"username": &username};
let user = app
.users
.find_one(query, None)
.await
.map_err(InternalError::DatabaseConnectionError)?
.find_user(&username)
.await?
.ok_or(UserError::UserNotFoundError)?;

let query = match user.group_id {
Expand Down Expand Up @@ -126,21 +117,13 @@ async fn set_user_settings(
ensure_can_edit_user(&app, &session, &username).await?;

let settings = std::str::from_utf8(&body).map_err(|_err| UserError::InternalError)?;

let query = doc! {"username": &username};
let update = doc! {"$set": {format!("serviceSettings.{}", &host): settings}};

let result = app
.users
.update_one(query, update, None)
.await
.map_err(InternalError::DatabaseConnectionError)?;
app.update_user(&username, update)
.await?
.ok_or(UserError::UserNotFoundError)?;

if result.matched_count == 0 {
Err(UserError::UserNotFoundError)
} else {
Ok(HttpResponse::Ok().finish())
}
Ok(HttpResponse::Ok().finish())
}

#[delete("/user/{username}/{host}")]
Expand All @@ -152,20 +135,13 @@ async fn delete_user_settings(
let (username, host) = path.into_inner();
ensure_can_edit_user(&app, &session, &username).await?;

let query = doc! {"username": &username};
let update = doc! {"$unset": {format!("serviceSettings.{}", &host): true}};

let result = app
.users
.update_one(query, update, None)
.await
.map_err(InternalError::DatabaseConnectionError)?;
app.update_user(&username, update)
.await?
.ok_or(UserError::UserNotFoundError)?;

if result.matched_count == 0 {
Err(UserError::UserNotFoundError)
} else {
Ok(HttpResponse::Ok().finish())
}
Ok(HttpResponse::Ok().finish())
}

#[get("/group/{group_id}/")]
Expand Down
Loading