Browse Source

Implemented the API key organization and public API needed to use "Bitwarden Directory Connector"

pull/2907/head
Daniele Andrei 3 years ago
parent
commit
04a8054d73
  1. 0
      migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql
  2. 8
      migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql
  3. 0
      migrations/mysql/2022-11-08-234911_add_external_id/down.sql
  4. 2
      migrations/mysql/2022-11-08-234911_add_external_id/up.sql
  5. 0
      migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql
  6. 8
      migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql
  7. 0
      migrations/postgresql/2022-11-08-234911_add_external_id/down.sql
  8. 2
      migrations/postgresql/2022-11-08-234911_add_external_id/up.sql
  9. 0
      migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql
  10. 9
      migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql
  11. 0
      migrations/sqlite/2022-11-08-234911_add_external_id/down.sql
  12. 2
      migrations/sqlite/2022-11-08-234911_add_external_id/up.sql
  13. 2
      src/api/core/mod.rs
  14. 62
      src/api/core/organizations.rs
  15. 224
      src/api/core/public.rs
  16. 55
      src/api/identity.rs
  17. 35
      src/auth.rs
  18. 15
      src/db/models/group.rs
  19. 2
      src/db/models/mod.rs
  20. 79
      src/db/models/organization.rs
  21. 22
      src/db/models/user.rs
  22. 13
      src/db/schemas/mysql/schema.rs
  23. 13
      src/db/schemas/postgresql/schema.rs
  24. 13
      src/db/schemas/sqlite/schema.rs
  25. 2
      src/main.rs

0
migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql

8
migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql

@ -0,0 +1,8 @@
CREATE TABLE organization_api_key (
uuid CHAR(36) NOT NULL,
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
atype INTEGER NOT NULL,
api_key VARCHAR(255) NOT NULL,
revision_date DATETIME NOT NULL,
PRIMARY KEY(uuid, org_uuid)
);

0
migrations/mysql/2022-11-08-234911_add_external_id/down.sql

2
migrations/mysql/2022-11-08-234911_add_external_id/up.sql

@ -0,0 +1,2 @@
ALTER TABLE users
ADD COLUMN external_id TEXT;

0
migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql

8
migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql

@ -0,0 +1,8 @@
CREATE TABLE organization_api_key (
uuid CHAR(36) NOT NULL,
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
atype INTEGER NOT NULL,
api_key VARCHAR(255),
revision_date TIMESTAMP NOT NULL,
PRIMARY KEY(uuid, org_uuid)
);

0
migrations/postgresql/2022-11-08-234911_add_external_id/down.sql

2
migrations/postgresql/2022-11-08-234911_add_external_id/up.sql

@ -0,0 +1,2 @@
ALTER TABLE users
ADD COLUMN external_id TEXT;

0
migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql

9
migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql

@ -0,0 +1,9 @@
CREATE TABLE organization_api_key (
uuid TEXT NOT NULL,
org_uuid TEXT NOT NULL,
atype INTEGER NOT NULL,
api_key TEXT NOT NULL,
revision_date DATETIME NOT NULL,
PRIMARY KEY(uuid, org_uuid),
FOREIGN KEY(org_uuid) REFERENCES organizations(uuid)
);

0
migrations/sqlite/2022-11-08-234911_add_external_id/down.sql

2
migrations/sqlite/2022-11-08-234911_add_external_id/up.sql

@ -0,0 +1,2 @@
ALTER TABLE users
ADD COLUMN external_id TEXT;

2
src/api/core/mod.rs

@ -5,6 +5,7 @@ mod folders;
mod organizations;
mod sends;
pub mod two_factor;
mod public;
pub use ciphers::purge_trashed_ciphers;
pub use ciphers::{CipherSyncData, CipherSyncType};
@ -26,6 +27,7 @@ pub fn routes() -> Vec<Route> {
routes.append(&mut organizations::routes());
routes.append(&mut two_factor::routes());
routes.append(&mut sends::routes());
routes.append(&mut public::routes());
routes.append(&mut device_token_routes);
routes.append(&mut eq_domains_routes);
routes.append(&mut hibp_routes);

62
src/api/core/organizations.rs

@ -86,7 +86,9 @@ pub fn routes() -> Vec<Route> {
put_user_groups,
delete_group_user,
post_delete_group_user,
get_org_export
get_org_export,
api_key,
rotate_api_key,
]
}
@ -1887,7 +1889,7 @@ async fn add_update_group(mut group: Group, collections: Vec<SelectionReadOnly>,
"OrganizationId": group.organizations_uuid,
"Name": group.name,
"AccessAll": group.access_all,
"ExternalId": group.get_external_id()
"ExternalId": group.external_id
})))
}
@ -1909,7 +1911,7 @@ async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHea
"OrganizationId": group.organizations_uuid,
"Name": group.name,
"AccessAll": group.access_all,
"ExternalId": group.get_external_id(),
"ExternalId": group.external_id,
"Collections": collections_groups
})))
}
@ -2112,3 +2114,57 @@ async fn get_org_export(org_id: String, headers: AdminHeaders, mut conn: DbConn)
}))
}
}
async fn _api_key(
org_id: String,
data: JsonUpcase<PasswordData>,
rotate: bool,
headers: AdminHeaders,
conn: DbConn,
) -> JsonResult {
let data: PasswordData = data.into_inner().data;
let user = headers.user;
// Validate the admin users password
if !user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}
let org_api_key = match OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await {
Some(mut org_api_key) => {
if rotate {
org_api_key.api_key = crate::crypto::generate_api_key();
org_api_key.revision_date = chrono::Utc::now().naive_utc();
org_api_key.save(&conn).await.expect("Error rotating organization API Key");
}
org_api_key
}
None => {
let api_key = crate::crypto::generate_api_key();
let new_org_api_key = OrganizationApiKey::new(org_id, api_key);
new_org_api_key.save(&conn).await.expect("Error creating organization API Key");
new_org_api_key
}
};
Ok(Json(json!({
"ApiKey": org_api_key.api_key,
"RevisionDate": crate::util::format_date(&org_api_key.revision_date),
"Object": "apiKey",
})))
}
#[post("/organizations/<org_id>/api-key", data = "<data>")]
async fn api_key(org_id: String, data: JsonUpcase<PasswordData>, headers: AdminHeaders, conn: DbConn) -> JsonResult {
_api_key(org_id, data, false, headers, conn).await
}
#[post("/organizations/<org_id>/rotate-api-key", data = "<data>")]
async fn rotate_api_key(
org_id: String,
data: JsonUpcase<PasswordData>,
headers: AdminHeaders,
conn: DbConn,
) -> JsonResult {
_api_key(org_id, data, true, headers, conn).await
}

224
src/api/core/public.rs

@ -0,0 +1,224 @@
use rocket::{
Route,
Request,
request::{self, FromRequest, Outcome},
};
use chrono::Utc;
use crate::{
auth,
db::{models::*, DbConn},
api::{JsonUpcase, EmptyResult},
mail,
CONFIG
};
pub fn routes() -> Vec<Route> {
routes![
ldap_import,
]
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct OrgImportGroupData {
Name: String, // "GroupName"
ExternalId: String, // "cn=GroupName,ou=Groups,dc=example,dc=com"
MemberExternalIds: Vec<String>, // ["uid=user,ou=People,dc=example,dc=com"]
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct OrgImportUserData {
Email: String, // "user@maildomain.net"
ExternalId: String, // "uid=user,ou=People,dc=example,dc=com"
Deleted: bool,
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct OrgImportData {
Groups: Vec<OrgImportGroupData>,
Members: Vec<OrgImportUserData>,
OverwriteExisting: bool,
#[allow(dead_code)]
LargeImport: bool,
}
#[post("/public/organization/import", data = "<data>")]
async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
let _ = &conn;
let org_id = token.0 ;
let data = data.into_inner().data;
for user_data in &data.Members {
if user_data.Deleted {
// If user is marked for deletion and it exists, revoke it
if let Some(mut user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await
{
user_org.revoke();
user_org.save(&mut conn).await?;
}
// If user is part of the organization, restore it
} else if let Some(mut user_org) = UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await {
if user_org.status < UserOrgStatus::Revoked as i32
{
user_org.restore();
user_org.save(&mut conn).await?;
}
}else{ // If user is not part of the organization
let user = match User::find_by_mail(&user_data.Email, &mut conn).await {
Some(user) => user, // exists in vaultwarden
None => { // doesn't exist in vaultwarden
let mut new_user = User::new(user_data.Email.clone());
new_user.set_external_id(Some(user_data.ExternalId.clone()));
new_user.save(&mut conn).await?;
if !CONFIG.mail_enabled() {
let invitation = Invitation::new(new_user.email.clone());
invitation.save(&mut conn).await?;
}
new_user
},
};
let user_org_status = if CONFIG.mail_enabled() {
UserOrgStatus::Invited as i32
} else {
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
};
let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
new_org_user.access_all = false;
new_org_user.atype = UserOrgType::User as i32;
new_org_user.status = user_org_status;
new_org_user.save(&mut conn).await?;
if CONFIG.mail_enabled() {
let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await {
Some(org) => (org.name, org.billing_email),
None => err!("Error looking up organization"),
};
mail::send_invite(
&user_data.Email,
&user.uuid,
Some(org_id.clone()),
Some(new_org_user.uuid),
&org_name,
Some(org_email),
).await?;
}
}
}
for group_data in &data.Groups {
let group_uuid = match Group::find_by_external_id(&group_data.ExternalId, &mut conn).await {
Some(group) => group,
None => {
let mut group = Group::new( org_id.clone(), group_data.Name.clone(), false, Some(group_data.ExternalId.clone()));
group.save(&mut conn).await?;
group
}
}.uuid;
GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
for ext_id in &group_data.MemberExternalIds {
if let Some(user) = User::find_by_external_id(&ext_id, &mut conn).await {
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await {
let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
group_user.save(&mut conn).await?;
}
}
}
}
// If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
if data.OverwriteExisting {
for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await {
if let Some(user_external_id) = User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.external_id) {
if user_external_id.is_some() && !data.Members.iter().any(|u| u.ExternalId == *user_external_id.as_ref().unwrap()) {
if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 {
// Removing owner, check that there is at least one other confirmed owner
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await <= 1 {
warn!("Can't delete the last owner");
continue
}
}
user_org.delete(&mut conn).await?;
}
}
}
}
Ok(())
}
#[derive(Debug)]
pub struct PublicToken (String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for PublicToken {
type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let headers = request.headers();
// Get access_token
let access_token: &str = match headers.get_one("Authorization") {
Some(a) => match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided"),
},
None => err_handler!("No access token provided"),
};
// Check JWT token is valid and get device and user from it
let claims = match auth::decode_api_org(access_token) {
Ok(claims) => claims,
Err(_) => err_handler!("Invalid claim"),
};
// Check if time is between claims.nbf and claims.exp
let time_now = Utc::now().naive_utc().timestamp();
if time_now < claims.nbf {
err_handler!("Token issued in the future");
}
if time_now > claims.exp {
err_handler!("Token expired");
}
// Check if claims.iss is host|claims.scope[0]
let host = match auth::Host::from_request(request).await {
Outcome::Success(host) => host,
_ => err_handler!("Error getting Host"),
};
let complete_host = format!("{}|{}", host.host, claims.scope[0]);
if complete_host != claims.iss {
err_handler!("Token not issued by this server");
}
// Check if claims.sub is org_api_key.uuid
// Check if claims.client_sub is org_api_key.org_uuid
let conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
};
let org_uuid = match claims.client_id.strip_prefix("organization.") {
Some(uuid) => uuid,
None => err_handler!("Malformed client_id"),
};
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await {
Some(org_api_key) => org_api_key,
None => err_handler!("Invalid client_id"),
};
if org_api_key.org_uuid != claims.client_sub {
err_handler!("Token not issued for this org");
}
if org_api_key.uuid != claims.sub {
err_handler!("Token not issued for this client");
}
Outcome::Success(PublicToken(claims.client_sub))
}
}

55
src/api/identity.rs

@ -13,7 +13,7 @@ use crate::{
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
ApiResult, EmptyResult, JsonResult, JsonUpcase,
},
auth::ClientIp,
auth::{generate_organization_api_key_login_claims, ClientIp},
db::{models::*, DbConn},
error::MapResult,
mail, util, CONFIG,
@ -188,17 +188,19 @@ async fn _password_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) ->
Ok(Json(result))
}
async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult {
// Validate scope
let scope = data.scope.as_ref().unwrap();
if scope != "api" {
err!("Scope not supported")
}
let scope_vec = vec!["api".into()];
async fn _api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
// Ratelimit the login
crate::ratelimit::check_limit_login(&ip.ip)?;
// Validate scope
match data.scope.as_ref().unwrap().as_ref() {
"api" => _user_api_key_login(data, conn, ip).await,
"api.organization" => _organization_api_key_login(data, conn, ip).await,
_ => err!("Scope not supported"),
}
}
async fn _user_api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> JsonResult {
// Get the user via the client_id
let client_id = data.client_id.as_ref().unwrap();
let user_uuid = match client_id.strip_prefix("user.") {
@ -235,6 +237,7 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J
}
// Common
let scope_vec = vec!["api".into()];
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &mut conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
device.save(&mut conn).await?;
@ -253,7 +256,39 @@ async fn _api_key_login(data: ConnectData, mut conn: DbConn, ip: &ClientIp) -> J
"Kdf": user.client_kdf_type,
"KdfIterations": user.client_kdf_iter,
"ResetMasterPassword": false, // TODO: Same as above
"scope": scope,
"scope": "api",
"unofficialServer": true,
})))
}
async fn _organization_api_key_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
// Get the org via the client_id
let client_id = data.client_id.as_ref().unwrap();
let org_uuid = match client_id.strip_prefix("organization.") {
Some(uuid) => uuid,
None => err!("Malformed client_id", format!("IP: {}.", ip.ip)),
};
let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await {
Some(org_api_key) => org_api_key,
None => err!("Invalid client_id", format!("IP: {}.", ip.ip)),
};
// Check API key.
let client_secret = data.client_secret.as_ref().unwrap();
if !org_api_key.check_valid_api_key(client_secret) {
err!("Incorrect client_secret", format!("IP: {}. Organization: {}.", ip.ip, org_api_key.org_uuid))
}
let claim = generate_organization_api_key_login_claims(org_api_key.uuid, org_api_key.org_uuid);
let access_token = crate::auth::encode_jwt(&claim);
//dbg!(&access_token);
Ok(Json(json!({
"access_token": access_token,
"expires_in": 3600,
"token_type": "Bearer",
"scope": "api.organization",
"unofficialServer": true,
})))
}

35
src/auth.rs

@ -23,6 +23,7 @@ static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFI
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
static PRIVATE_RSA_KEY_VEC: Lazy<Vec<u8>> = Lazy::new(|| {
std::fs::read(CONFIG.private_rsa_key()).unwrap_or_else(|e| panic!("Error loading private RSA Key.\n{}", e))
@ -96,6 +97,10 @@ pub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> {
decode_jwt(token, JWT_SEND_ISSUER.to_string())
}
pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> {
decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginJwtClaims {
// Not before
@ -203,6 +208,36 @@ pub fn generate_emergency_access_invite_claims(
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OrgApiKeyLoginJwtClaims {
// Not before
pub nbf: i64,
// Expiration time
pub exp: i64,
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub client_id: String,
pub client_sub: String,
// [ "api.organization" ]
pub scope: Vec<String>,
}
pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims {
let time_now = Utc::now().naive_utc();
OrgApiKeyLoginJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + Duration::hours(1)).timestamp(),
iss: JWT_ORG_API_KEY_ISSUER.to_string(),
sub: uuid,
client_id: format!("organization.{org_id}"),
client_sub: org_id,
scope: vec!["api.organization".into()],
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BasicJwtClaims {
// Not before

15
src/db/models/group.rs

@ -10,7 +10,7 @@ db_object! {
pub organizations_uuid: String,
pub name: String,
pub access_all: bool,
external_id: Option<String>,
pub external_id: Option<String>,
pub creation_date: NaiveDateTime,
pub revision_date: NaiveDateTime,
}
@ -82,10 +82,6 @@ impl Group {
None => self.external_id = None,
}
}
pub fn get_external_id(&self) -> Option<String> {
self.external_id.clone()
}
}
impl CollectionGroup {
@ -171,6 +167,15 @@ impl Group {
}}
}
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
groups::table
.filter(groups::external_id.eq(id))
.first::<GroupDb>(conn)
.ok()
.from_db()
}}
}
//Returns all organizations the user has full access to
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
db_run! { conn: {

2
src/db/models/mod.rs

@ -22,7 +22,7 @@ pub use self::favorite::Favorite;
pub use self::folder::{Folder, FolderCipher};
pub use self::group::{CollectionGroup, Group, GroupUser};
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::send::{Send, SendType};
pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_incomplete::TwoFactorIncomplete;

79
src/db/models/organization.rs

@ -1,3 +1,4 @@
use chrono::{NaiveDateTime, Utc};
use num_traits::FromPrimitive;
use serde_json::Value;
use std::cmp::Ordering;
@ -29,6 +30,17 @@ db_object! {
pub status: i32,
pub atype: i32,
}
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = organization_api_key)]
#[diesel(primary_key(uuid, org_uuid))]
pub struct OrganizationApiKey {
pub uuid: String,
pub org_uuid: String,
pub atype: i32,
pub api_key: String,
pub revision_date: NaiveDateTime,
}
}
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
@ -155,7 +167,7 @@ impl Organization {
"UseSso": false, // Not supported
// "UseKeyConnector": false, // Not supported
"SelfHost": true,
"UseApi": false, // Not supported
"UseApi": true,
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
"UseResetPassword": false, // Not supported
@ -209,6 +221,23 @@ impl UserOrganization {
}
}
impl OrganizationApiKey {
pub fn new(org_uuid: String, api_key: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
org_uuid,
atype: 0, // Type 0 is the default and only type we support currently
api_key,
revision_date: Utc::now().naive_utc(),
}
}
pub fn check_valid_api_key(&self, api_key: &str) -> bool {
crate::crypto::ct_eq(&self.api_key, api_key)
}
}
use crate::db::DbConn;
use crate::api::EmptyResult;
@ -308,7 +337,7 @@ impl UserOrganization {
"UseTotp": true,
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
"UsePolicies": true,
"UseApi": false, // Not supported
"UseApi": true,
"SelfHost": true,
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
"ResetPasswordEnrolled": false, // Not supported
@ -442,7 +471,7 @@ impl UserOrganization {
.set(UserOrganizationDb::to_db(self))
.execute(conn)
.map_res("Error adding user to organization")
}
},
Err(e) => Err(e.into()),
}.map_res("Error adding user to organization")
}
@ -688,6 +717,50 @@ impl UserOrganization {
}
}
impl OrganizationApiKey {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(organization_api_key::table)
.values(OrganizationApiKeyDb::to_db(self))
.execute(conn)
{
Ok(_) => Ok(()),
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(organization_api_key::table)
.filter(organization_api_key::uuid.eq(&self.uuid))
.set(OrganizationApiKeyDb::to_db(self))
.execute(conn)
.map_res("Error saving organization")
}
Err(e) => Err(e.into()),
}.map_res("Error saving organization")
}
postgresql {
let value = OrganizationApiKeyDb::to_db(self);
diesel::insert_into(organization_api_key::table)
.values(&value)
.on_conflict(organization_api_key::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving organization")
}
}
}
pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
organization_api_key::table
.filter(organization_api_key::org_uuid.eq(org_uuid))
.first::<OrganizationApiKeyDb>(conn)
.ok().from_db()
}}
}
}
#[cfg(test)]
mod tests {
use super::*;

22
src/db/models/user.rs

@ -46,6 +46,7 @@ db_object! {
pub client_kdf_iter: i32,
pub api_key: Option<String>,
pub external_id: Option<String>,
}
#[derive(Identifiable, Queryable, Insertable)]
@ -113,6 +114,7 @@ impl User {
client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT,
api_key: None,
external_id: None,
}
}
@ -137,6 +139,21 @@ impl User {
matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key))
}
pub fn set_external_id(&mut self, external_id: Option<String>) {
//Check if external id is empty. We don't want to have
//empty strings in the database
match external_id {
Some(external_id) => {
if external_id.is_empty() {
self.external_id = None;
} else {
self.external_id = Some(external_id)
}
}
None => self.external_id = None,
}
}
/// Set the password hash generated
/// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different.
///
@ -349,6 +366,11 @@ impl User {
}}
}
pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
users::table.filter(users::external_id.eq(id)).first::<UserDb>(conn).ok().from_db()
}}
}
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()

13
src/db/schemas/mysql/schema.rs

@ -179,6 +179,7 @@ table! {
client_kdf_type -> Integer,
client_kdf_iter -> Integer,
api_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}
@ -203,6 +204,16 @@ table! {
}
}
table! {
organization_api_key (uuid, org_uuid) {
uuid -> Text,
org_uuid -> Text,
atype -> Integer,
api_key -> Text,
revision_date -> Timestamp,
}
}
table! {
emergency_access (uuid) {
uuid -> Text,
@ -266,6 +277,7 @@ joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid));
joinable!(organization_api_key -> organizations (org_uuid));
joinable!(emergency_access -> users (grantor_uuid));
joinable!(groups -> organizations (organizations_uuid));
joinable!(groups_users -> users_organizations (users_organizations_uuid));
@ -289,6 +301,7 @@ allow_tables_to_appear_in_same_query!(
users,
users_collections,
users_organizations,
organization_api_key,
emergency_access,
groups,
groups_users,

13
src/db/schemas/postgresql/schema.rs

@ -179,6 +179,7 @@ table! {
client_kdf_type -> Integer,
client_kdf_iter -> Integer,
api_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}
@ -203,6 +204,16 @@ table! {
}
}
table! {
organization_api_key (uuid, org_uuid) {
uuid -> Text,
org_uuid -> Text,
atype -> Integer,
api_key -> Text,
revision_date -> Timestamp,
}
}
table! {
emergency_access (uuid) {
uuid -> Text,
@ -266,6 +277,7 @@ joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid));
joinable!(organization_api_key -> organizations (org_uuid));
joinable!(emergency_access -> users (grantor_uuid));
joinable!(groups -> organizations (organizations_uuid));
joinable!(groups_users -> users_organizations (users_organizations_uuid));
@ -289,6 +301,7 @@ allow_tables_to_appear_in_same_query!(
users,
users_collections,
users_organizations,
organization_api_key,
emergency_access,
groups,
groups_users,

13
src/db/schemas/sqlite/schema.rs

@ -179,6 +179,7 @@ table! {
client_kdf_type -> Integer,
client_kdf_iter -> Integer,
api_key -> Nullable<Text>,
external_id -> Nullable<Text>,
}
}
@ -203,6 +204,16 @@ table! {
}
}
table! {
organization_api_key (uuid, org_uuid) {
uuid -> Text,
org_uuid -> Text,
atype -> Integer,
api_key -> Text,
revision_date -> Timestamp,
}
}
table! {
emergency_access (uuid) {
uuid -> Text,
@ -266,6 +277,7 @@ joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid));
joinable!(organization_api_key -> organizations (org_uuid));
joinable!(emergency_access -> users (grantor_uuid));
joinable!(groups -> organizations (organizations_uuid));
joinable!(groups_users -> users_organizations (users_organizations_uuid));
@ -289,6 +301,7 @@ allow_tables_to_appear_in_same_query!(
users,
users_collections,
users_organizations,
organization_api_key,
emergency_access,
groups,
groups_users,

2
src/main.rs

@ -34,7 +34,7 @@
// The more key/value pairs there are the more recursion occurs.
// We want to keep this as low as possible, but not higher then 128.
// If you go above 128 it will cause rust-analyzer to fail,
#![recursion_limit = "94"]
#![recursion_limit = "97"]
// When enabled use MiMalloc as malloc instead of the default malloc
#[cfg(feature = "enable_mimalloc")]

Loading…
Cancel
Save