Browse Source

Squash commits

pull/2449/head
Pablo Ovelleiro Corral 3 years ago
parent
commit
cc58bbe1f5
No known key found for this signature in database GPG Key ID: F7C1D57C8464E825
  1. 1
      Cargo.lock
  2. 4
      Cargo.toml
  3. 2
      migrations/mysql/2021-09-16-133000_add_sso/down.sql
  4. 18
      migrations/mysql/2021-09-16-133000_add_sso/up.sql
  5. 2
      migrations/postgresql/2021-09-16-133000_add_sso/down.sql
  6. 18
      migrations/postgresql/2021-09-16-133000_add_sso/up.sql
  7. 2
      migrations/sqlite/2021-09-16-133000_add_sso/down.sql
  8. 18
      migrations/sqlite/2021-09-16-133000_add_sso/up.sql
  9. 94
      src/api/core/organizations.rs
  10. 246
      src/api/identity.rs
  11. 4
      src/db/models/mod.rs
  12. 2
      src/db/models/org_policy.rs
  13. 2417
      src/db/models/organization.rs
  14. 104
      src/db/models/sso_config.rs
  15. 71
      src/db/models/sso_nonce.rs
  16. 23
      src/db/schemas/mysql/schema.rs
  17. 23
      src/db/schemas/postgresql/schema.rs
  18. 23
      src/db/schemas/sqlite/schema.rs

1
Cargo.lock

@ -2675,6 +2675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
dependencies = [ dependencies = [
"itoa", "itoa",
"js-sys",
"libc", "libc",
"num_threads", "num_threads",
"time-macros", "time-macros",

4
Cargo.toml

@ -138,6 +138,10 @@ pico-args = "0.5.0"
paste = "1.0.9" paste = "1.0.9"
governor = "0.5.0" governor = "0.5.0"
# OIDC SSo
openidconnect = "2.3.2"
# Capture CTRL+C # Capture CTRL+C
ctrlc = { version = "3.2.3", features = ["termination"] } ctrlc = { version = "3.2.3", features = ["termination"] }

2
migrations/mysql/2021-09-16-133000_add_sso/down.sql

@ -0,0 +1,2 @@
DROP TABLE sso_nonce;
DROP TABLE sso_config;

18
migrations/mysql/2021-09-16-133000_add_sso/up.sql

@ -0,0 +1,18 @@
ALTER TABLE organizations ADD COLUMN identifier TEXT;
CREATE TABLE sso_nonce (
uuid CHAR(36) NOT NULL PRIMARY KEY,
org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid),
nonce CHAR(36) NOT NULL
);
CREATE TABLE sso_config (
uuid CHAR(36) NOT NULL PRIMARY KEY,
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
use_sso BOOLEAN NOT NULL,
callback_path TEXT NOT NULL,
signed_out_callback_path TEXT NOT NULL,
authority TEXT,
client_id TEXT,
client_secret TEXT
);

2
migrations/postgresql/2021-09-16-133000_add_sso/down.sql

@ -0,0 +1,2 @@
DROP TABLE sso_nonce;
DROP TABLE sso_config;

18
migrations/postgresql/2021-09-16-133000_add_sso/up.sql

@ -0,0 +1,18 @@
ALTER TABLE organizations ADD COLUMN identifier TEXT;
CREATE TABLE sso_nonce (
uuid CHAR(36) NOT NULL PRIMARY KEY,
org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid),
nonce CHAR(36) NOT NULL
);
CREATE TABLE sso_config (
uuid CHAR(36) NOT NULL PRIMARY KEY,
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
use_sso BOOLEAN NOT NULL,
callback_path TEXT NOT NULL,
signed_out_callback_path TEXT NOT NULL,
authority TEXT,
client_id TEXT,
client_secret TEXT
);

2
migrations/sqlite/2021-09-16-133000_add_sso/down.sql

@ -0,0 +1,2 @@
DROP TABLE sso_nonce;
DROP TABLE sso_config;

18
migrations/sqlite/2021-09-16-133000_add_sso/up.sql

@ -0,0 +1,18 @@
ALTER TABLE organizations ADD COLUMN identifier TEXT;
CREATE TABLE sso_nonce (
uuid CHAR(36) NOT NULL PRIMARY KEY,
org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid),
nonce CHAR(36) NOT NULL
);
CREATE TABLE sso_config (
uuid CHAR(36) NOT NULL PRIMARY KEY,
org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid),
use_sso BOOLEAN NOT NULL,
callback_path TEXT NOT NULL,
signed_out_callback_path TEXT NOT NULL,
authority TEXT,
client_id TEXT,
client_secret TEXT
);

94
src/api/core/organizations.rs

@ -31,6 +31,8 @@ pub fn routes() -> Vec<Route> {
put_collection_users, put_collection_users,
put_organization, put_organization,
post_organization, post_organization,
get_organization_sso,
put_organization_sso,
post_organization_collections, post_organization_collections,
delete_organization_collection_user, delete_organization_collection_user,
post_organization_collection_delete_user, post_organization_collection_delete_user,
@ -92,6 +94,14 @@ struct OrgData {
struct OrganizationUpdateData { struct OrganizationUpdateData {
BillingEmail: String, BillingEmail: String,
Name: String, Name: String,
Identifier: Option<String>,
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct OrganizationSsoUpdateData {
Enabled: Option<bool>,
Data: Option<SsoOrganizationData>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -100,6 +110,45 @@ struct NewCollectionData {
Name: String, Name: String,
} }
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct SsoOrganizationData {
// authority: Option<String>,
// clientId: Option<String>,
// clientSecret: Option<String>,
AcrValues: Option<String>,
AdditionalEmailClaimTypes: Option<String>,
AdditionalNameClaimTypes: Option<String>,
AdditionalScopes: Option<String>,
AdditionalUserIdClaimTypes: Option<String>,
Authority: Option<String>,
ClientId: Option<String>,
ClientSecret: Option<String>,
ConfigType: Option<String>,
ExpectedReturnAcrValue: Option<String>,
GetClaimsFromUserInfoEndpoint: Option<bool>,
IdpAllowUnsolicitedAuthnResponse: Option<bool>,
IdpArtifactResolutionServiceUrl: Option<String>,
IdpBindingType: Option<u8>,
IdpDisableOutboundLogoutRequests: Option<bool>,
IdpEntityId: Option<String>,
IdpOutboundSigningAlgorithm: Option<String>,
IdpSingleLogoutServiceUrl: Option<String>,
IdpSingleSignOnServiceUrl: Option<String>,
IdpWantAuthnRequestsSigned: Option<bool>,
IdpX509PublicCert: Option<String>,
KeyConnectorUrlY: Option<String>,
KeyConnectorEnabled: Option<bool>,
MetadataAddress: Option<String>,
RedirectBehavior: Option<String>,
SpMinIncomingSigningAlgorithm: Option<String>,
SpNameIdFormat: Option<u8>,
SpOutboundSigningAlgorithm: Option<String>,
SpSigningBehavior: Option<u8>,
SpValidateCertificates: Option<bool>,
SpWantAssertionsSigned: Option<bool>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct OrgKeyData { struct OrgKeyData {
@ -134,6 +183,7 @@ async fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn:
let org = Organization::new(data.Name, data.BillingEmail, private_key, public_key); let org = Organization::new(data.Name, data.BillingEmail, private_key, public_key);
let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone()); let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone());
let sso_config = SsoConfig::new(org.uuid.clone());
let collection = Collection::new(org.uuid.clone(), data.CollectionName); let collection = Collection::new(org.uuid.clone(), data.CollectionName);
user_org.akey = data.Key; user_org.akey = data.Key;
@ -143,6 +193,7 @@ async fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn:
org.save(&conn).await?; org.save(&conn).await?;
user_org.save(&conn).await?; user_org.save(&conn).await?;
sso_config.save(&conn).await?;
collection.save(&conn).await?; collection.save(&conn).await?;
Ok(Json(org.to_json())) Ok(Json(org.to_json()))
@ -228,11 +279,54 @@ async fn post_organization(
org.name = data.Name; org.name = data.Name;
org.billing_email = data.BillingEmail; org.billing_email = data.BillingEmail;
org.identifier = data.Identifier;
org.save(&conn).await?; org.save(&conn).await?;
Ok(Json(org.to_json())) Ok(Json(org.to_json()))
} }
#[get("/organizations/<org_id>/sso")]
async fn get_organization_sso(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult {
match SsoConfig::find_by_org(&org_id, &conn).await {
Some(sso_config) => {
let config_json = Json(sso_config.to_json());
Ok(config_json)
}
None => err!("Can't find organization sso config"),
}
}
#[post("/organizations/<org_id>/sso", data = "<data>")]
async fn put_organization_sso(
org_id: String,
_headers: OwnerHeaders,
data: JsonUpcase<OrganizationSsoUpdateData>,
conn: DbConn,
) -> JsonResult {
let p: OrganizationSsoUpdateData = data.into_inner().data;
let d: SsoOrganizationData = p.Data.unwrap();
let mut sso_config = match SsoConfig::find_by_org(&org_id, &conn).await {
Some(sso_config) => sso_config,
None => SsoConfig::new(org_id),
};
sso_config.use_sso = p.Enabled.unwrap_or_default();
// let sso_config_data = data.Data.unwrap();
// TODO use real values
sso_config.callback_path = "http://localhost:8000/#/sso".to_string(); //data.CallbackPath;
sso_config.signed_out_callback_path = "http://localhost:8000/#/sso".to_string(); //data2.Data.unwrap().call
sso_config.authority = d.Authority;
sso_config.client_id = d.ClientId;
sso_config.client_secret = d.ClientSecret;
sso_config.save(&conn).await?;
Ok(Json(sso_config.to_json()))
}
// GET /api/collections?writeOnly=false // GET /api/collections?writeOnly=false
#[get("/collections")] #[get("/collections")]
async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> { async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {

246
src/api/identity.rs

@ -1,11 +1,15 @@
use chrono::Utc; use chrono::Utc;
use jsonwebtoken::DecodingKey;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::{ use rocket::{
form::{Form, FromForm}, form::{Form, FromForm},
http::Status,
response::Redirect,
Route, Route,
}; };
use serde_json::Value; use serde_json::Value;
use std::iter::FromIterator;
use crate::{ use crate::{
api::{ api::{
@ -20,7 +24,7 @@ use crate::{
}; };
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![login, prelogin] routes![login, prelogin, prevalidate, authorize]
} }
#[post("/connect/token", data = "<data>")] #[post("/connect/token", data = "<data>")]
@ -51,6 +55,13 @@ async fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResul
_api_key_login(data, conn, &ip).await _api_key_login(data, conn, &ip).await
} }
"authorization_code" => {
_check_is_some(&data.code, "code cannot be blank")?;
_check_is_some(&data.org_identifier, "org_identifier cannot be blank")?;
_check_is_some(&data.device_identifier, "device identifier cannot be blank")?;
_authorization_login(data, conn, &ip).await
}
t => err!("Invalid type", t), t => err!("Invalid type", t),
} }
} }
@ -87,6 +98,104 @@ async fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
}))) })))
} }
#[derive(Debug, Serialize, Deserialize)]
struct TokenPayload {
exp: i64,
email: String,
nonce: String,
}
async fn _authorization_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
let org_identifier = data.org_identifier.as_ref().unwrap();
let code = data.code.as_ref().unwrap();
let organization = Organization::find_by_identifier(org_identifier, &conn).await.unwrap();
let sso_config = SsoConfig::find_by_org(&organization.uuid, &conn).await.unwrap();
let (_access_token, refresh_token, id_token) = match get_auth_code_access_token(code, &sso_config).await {
Ok((_access_token, refresh_token, id_token)) => (_access_token, refresh_token, id_token),
Err(err) => err!(err),
};
// https://github.com/Keats/jsonwebtoken/issues/236#issuecomment-1093039195
// let token = jsonwebtoken::decode::<TokenPayload>(access_token.as_str()).unwrap().claims;
let mut validation = jsonwebtoken::Validation::default();
validation.insecure_disable_signature_validation();
let token = jsonwebtoken::decode::<TokenPayload>(id_token.as_str(), &DecodingKey::from_secret(&[]), &validation)
.unwrap()
.claims;
// let expiry = token.exp;
let nonce = token.nonce;
match SsoNonce::find_by_org_and_nonce(&organization.uuid, &nonce, &conn).await {
Some(sso_nonce) => {
match sso_nonce.delete(&conn).await {
Ok(_) => {
// let expiry = token.exp;
let user_email = token.email;
let now = Utc::now().naive_utc();
// COMMON
// TODO handle missing users, currently this will panic if the user does not exist!
let user = User::find_by_mail(&user_email, &conn).await.unwrap();
let (mut device, new_device) = get_device(&data, &conn, &user).await;
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, &conn).await?;
if CONFIG.mail_enabled() && new_device {
if let Err(e) =
mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await
{
error!("Error sending new device email: {:#?}", e);
if CONFIG.require_device_email() {
err!("Could not send login notification email. Please contact your administrator.")
}
}
}
device.refresh_token = refresh_token.clone();
device.save(&conn).await?;
let scope_vec = vec!["api".into(), "offline_access".into()];
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn).await;
let (access_token_new, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
device.save(&conn).await?;
let mut result = json!({
"access_token": access_token_new,
"expires_in": expires_in,
"token_type": "Bearer",
"refresh_token": refresh_token,
"Key": user.akey,
"PrivateKey": user.private_key,
"Kdf": user.client_kdf_type,
"KdfIterations": user.client_kdf_iter,
"ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing
"scope": "api offline_access",
"unofficialServer": true,
});
if let Some(token) = twofactor_token {
result["TwoFactorToken"] = Value::String(token);
}
info!("User {} logged in successfully. IP: {}", user.email, ip.ip);
Ok(Json(result))
}
Err(_) => err!("Failed to delete nonce"),
}
}
None => {
err!("Invalid nonce")
}
}
}
async fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult { async fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult {
// Validate scope // Validate scope
let scope = data.scope.as_ref().unwrap(); let scope = data.scope.as_ref().unwrap();
@ -116,6 +225,15 @@ async fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> Json
err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username)) err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username))
} }
// Check if org policy prevents password login
let user_orgs = UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::RequireSso, &conn).await;
if !user_orgs.is_empty() && user_orgs[0].atype != UserOrgType::Owner && user_orgs[0].atype != UserOrgType::Admin {
// if requires SSO is active, user is in exactly one org by policy rules
// policy only applies to "non-owner/non-admin" members
err!("Organization policy requires SSO sign in");
}
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() {
@ -486,11 +604,137 @@ struct ConnectData {
#[field(name = uncased("two_factor_remember"))] #[field(name = uncased("two_factor_remember"))]
#[field(name = uncased("twofactorremember"))] #[field(name = uncased("twofactorremember"))]
two_factor_remember: Option<i32>, two_factor_remember: Option<i32>,
// Needed for authorization code
code: Option<String>,
org_identifier: Option<String>,
} }
// TODO Might need to migrate this: https://github.com/SergioBenitez/Rocket/pull/1489#issuecomment-1114750006
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult { fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
if value.is_none() { if value.is_none() {
err!(msg) err!(msg)
} }
Ok(()) Ok(())
} }
#[get("/account/prevalidate?<domainHint>")]
#[allow(non_snake_case)]
// The compiler warns about unreachable code here. But I've tested it, and it seems to work
// as expected. All errors appear to be reachable, as is the Ok response.
#[allow(unreachable_code)]
async fn prevalidate(domainHint: String, conn: DbConn) -> JsonResult {
let empty_result = json!({});
// TODO: fix panic on failig to retrive (no unwrap on null)
let organization = Organization::find_by_identifier(&domainHint, &conn).await.unwrap();
let sso_config = SsoConfig::find_by_org(&organization.uuid, &conn);
match sso_config.await {
Some(sso_config) => {
if !sso_config.use_sso {
return err_code!("SSO Not allowed for organization", Status::BadRequest.code);
}
if sso_config.authority.is_none() || sso_config.client_id.is_none() || sso_config.client_secret.is_none() {
return err_code!("Organization is incorrectly configured for SSO", Status::BadRequest.code);
}
}
None => {
return err_code!("Unable to find sso config", Status::BadRequest.code);
}
}
if domainHint.is_empty() {
return err_code!("No Organization Identifier Provided", Status::BadRequest.code);
}
Ok(Json(empty_result))
}
use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreResponseType};
use openidconnect::reqwest::async_http_client;
use openidconnect::{
AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse,
RedirectUrl, Scope,
};
async fn get_client_from_sso_config(sso_config: &SsoConfig) -> Result<CoreClient, &'static str> {
let redirect = sso_config.callback_path.clone();
let client_id = ClientId::new(sso_config.client_id.as_ref().unwrap().to_string());
let client_secret = ClientSecret::new(sso_config.client_secret.as_ref().unwrap().to_string());
let issuer_url =
IssuerUrl::new(sso_config.authority.as_ref().unwrap().to_string()).or(Err("invalid issuer URL"))?;
// TODO: This comparison will fail if one URI has a trailing slash and the other one does not.
// Should we remove trailing slashes when saving? Or when checking?
let provider_metadata = match CoreProviderMetadata::discover_async(issuer_url, async_http_client).await {
Ok(metadata) => metadata,
Err(_err) => {
return Err("Failed to discover OpenID provider");
}
};
let client = CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret))
.set_redirect_uri(RedirectUrl::new(redirect).or(Err("Invalid redirect URL"))?);
Ok(client)
}
#[get("/connect/authorize?<domain_hint>&<state>")]
async fn authorize(domain_hint: String, state: String, conn: DbConn) -> ApiResult<Redirect> {
let organization = Organization::find_by_identifier(&domain_hint, &conn).await.unwrap();
let sso_config = SsoConfig::find_by_org(&organization.uuid, &conn).await.unwrap();
match get_client_from_sso_config(&sso_config).await {
Ok(client) => {
let (mut authorize_url, _csrf_state, nonce) = client
.authorize_url(
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.add_scope(Scope::new("email".to_string()))
.add_scope(Scope::new("profile".to_string()))
.url();
let sso_nonce = SsoNonce::new(organization.uuid, nonce.secret().to_string());
sso_nonce.save(&conn).await?;
// it seems impossible to set the state going in dynamically (requires static lifetime string)
// so I change it after the fact
let old_pairs = authorize_url.query_pairs();
let new_pairs = old_pairs.map(|pair| {
let (key, value) = pair;
if key == "state" {
return format!("{}={}", key, state);
}
format!("{}={}", key, value)
});
let full_query = Vec::from_iter(new_pairs).join("&");
authorize_url.set_query(Some(full_query.as_str()));
Ok(Redirect::to(authorize_url.to_string()))
}
Err(err) => err!("Unable to find client from identifier {}", err),
}
}
async fn get_auth_code_access_token(
code: &str,
sso_config: &SsoConfig,
) -> Result<(String, String, String), &'static str> {
let oidc_code = AuthorizationCode::new(String::from(code));
match get_client_from_sso_config(sso_config).await {
Ok(client) => match client.exchange_code(oidc_code).request_async(async_http_client).await {
Ok(token_response) => {
let access_token = token_response.access_token().secret().to_string();
let refresh_token = token_response.refresh_token().unwrap().secret().to_string();
let id_token = token_response.extra_fields().id_token().unwrap().to_string();
Ok((access_token, refresh_token, id_token))
}
Err(_err) => Err("Failed to contact token endpoint"),
},
Err(_err) => Err("unable to find client"),
}
}

4
src/db/models/mod.rs

@ -8,6 +8,8 @@ mod folder;
mod org_policy; mod org_policy;
mod organization; mod organization;
mod send; mod send;
mod sso_config;
mod sso_nonce;
mod two_factor; mod two_factor;
mod two_factor_incomplete; mod two_factor_incomplete;
mod user; mod user;
@ -22,6 +24,8 @@ pub use self::folder::{Folder, FolderCipher};
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::send::{Send, SendType}; pub use self::send::{Send, SendType};
pub use self::sso_config::SsoConfig;
pub use self::sso_nonce::SsoNonce;
pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_incomplete::TwoFactorIncomplete; pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, User, UserStampException}; pub use self::user::{Invitation, User, UserStampException};

2
src/db/models/org_policy.rs

@ -28,7 +28,7 @@ pub enum OrgPolicyType {
MasterPassword = 1, MasterPassword = 1,
PasswordGenerator = 2, PasswordGenerator = 2,
SingleOrg = 3, SingleOrg = 3,
// RequireSso = 4, // Not supported RequireSso = 4,
PersonalOwnership = 5, PersonalOwnership = 5,
DisableSend = 6, DisableSend = 6,
SendOptions = 7, SendOptions = 7,

2417
src/db/models/organization.rs

File diff suppressed because it is too large

104
src/db/models/sso_config.rs

@ -0,0 +1,104 @@
use crate::api::EmptyResult;
use crate::db::DbConn;
use crate::error::MapResult;
use serde_json::Value;
use super::Organization;
db_object! {
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "sso_config"]
#[belongs_to(Organization, foreign_key = "org_uuid")]
#[primary_key(uuid)]
pub struct SsoConfig {
pub uuid: String,
pub org_uuid: String,
pub use_sso: bool,
pub callback_path: String,
pub signed_out_callback_path: String,
pub authority: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
}
}
/// Local methods
impl SsoConfig {
pub fn new(org_uuid: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
org_uuid,
use_sso: false,
callback_path: String::from("http://localhost/#/sso/"),
signed_out_callback_path: String::from("http://localhost/#/sso/"),
authority: None,
client_id: None,
client_secret: None,
}
}
pub fn to_json(&self) -> Value {
json!({
"Id": self.uuid,
"UseSso": self.use_sso,
"CallbackPath": self.callback_path,
"SignedOutCallbackPath": self.signed_out_callback_path,
"Authority": self.authority,
"ClientId": self.client_id,
"ClientSecret": self.client_secret,
})
}
}
/// Database methods
impl SsoConfig {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(sso_config::table)
.values(SsoConfigDb::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(sso_config::table)
.filter(sso_config::uuid.eq(&self.uuid))
.set(SsoConfigDb::to_db(self))
.execute(conn)
.map_res("Error adding sso config to organization")
}
Err(e) => Err(e.into()),
}.map_res("Error adding sso config to organization")
}
postgresql {
let value = SsoConfigDb::to_db(self);
diesel::insert_into(sso_config::table)
.values(&value)
.on_conflict(sso_config::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error adding sso config to organization")
}
}
}
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(sso_config::table.filter(sso_config::uuid.eq(self.uuid)))
.execute(conn)
.map_res("Error deleting SSO Config")
}}
}
pub async fn find_by_org(org_uuid: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
sso_config::table
.filter(sso_config::org_uuid.eq(org_uuid))
.first::<SsoConfigDb>(conn)
.ok()
.from_db()
}}
}
}

71
src/db/models/sso_nonce.rs

@ -0,0 +1,71 @@
use crate::api::EmptyResult;
use crate::db::DbConn;
use crate::error::MapResult;
use super::Organization;
db_object! {
#[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "sso_nonce"]
#[belongs_to(Organization, foreign_key = "org_uuid")]
#[primary_key(uuid)]
pub struct SsoNonce {
pub uuid: String,
pub org_uuid: String,
pub nonce: String,
}
}
/// Local methods
impl SsoNonce {
pub fn new(org_uuid: String, nonce: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
org_uuid,
nonce,
}
}
}
/// Database methods
impl SsoNonce {
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
diesel::replace_into(sso_nonce::table)
.values(SsoNonceDb::to_db(self))
.execute(conn)
.map_res("Error saving device")
}
postgresql {
let value = SsoNonceDb::to_db(self);
diesel::insert_into(sso_nonce::table)
.values(&value)
.on_conflict(sso_nonce::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving SSO nonce")
}
}
}
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(sso_nonce::table.filter(sso_nonce::uuid.eq(self.uuid)))
.execute(conn)
.map_res("Error deleting SSO nonce")
}}
}
pub async fn find_by_org_and_nonce(org_uuid: &str, nonce: &str, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
sso_nonce::table
.filter(sso_nonce::org_uuid.eq(org_uuid))
.filter(sso_nonce::nonce.eq(nonce))
.first::<SsoNonceDb>(conn)
.ok()
.from_db()
}}
}
}

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

@ -100,11 +100,25 @@ table! {
uuid -> Text, uuid -> Text,
name -> Text, name -> Text,
billing_email -> Text, billing_email -> Text,
identifier -> Nullable<Text>,
private_key -> Nullable<Text>, private_key -> Nullable<Text>,
public_key -> Nullable<Text>, public_key -> Nullable<Text>,
} }
} }
table! {
sso_config (uuid) {
uuid -> Text,
org_uuid -> Text,
use_sso -> Bool,
callback_path -> Text,
signed_out_callback_path -> Text,
authority -> Nullable<Text>,
client_id -> Nullable<Text>,
client_secret -> Nullable<Text>,
}
}
table! { table! {
sends (uuid) { sends (uuid) {
uuid -> Text, uuid -> Text,
@ -203,6 +217,14 @@ table! {
} }
} }
table! {
sso_nonce (uuid) {
uuid -> Text,
org_uuid -> Text,
nonce -> Text,
}
}
table! { table! {
emergency_access (uuid) { emergency_access (uuid) {
uuid -> Text, uuid -> Text,
@ -239,6 +261,7 @@ joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid)); joinable!(users_organizations -> users (user_uuid));
joinable!(emergency_access -> users (grantor_uuid)); joinable!(emergency_access -> users (grantor_uuid));
joinable!(sso_nonce -> organizations (org_uuid));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
attachments, attachments,

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

@ -100,11 +100,25 @@ table! {
uuid -> Text, uuid -> Text,
name -> Text, name -> Text,
billing_email -> Text, billing_email -> Text,
identifier -> Nullable<Text>,
private_key -> Nullable<Text>, private_key -> Nullable<Text>,
public_key -> Nullable<Text>, public_key -> Nullable<Text>,
} }
} }
table! {
sso_config (uuid) {
uuid -> Text,
org_uuid -> Text,
use_sso -> Bool,
callback_path -> Text,
signed_out_callback_path -> Text,
authority -> Nullable<Text>,
client_id -> Nullable<Text>,
client_secret -> Nullable<Text>,
}
}
table! { table! {
sends (uuid) { sends (uuid) {
uuid -> Text, uuid -> Text,
@ -203,6 +217,14 @@ table! {
} }
} }
table! {
sso_nonce (uuid) {
uuid -> Text,
org_uuid -> Text,
nonce -> Text,
}
}
table! { table! {
emergency_access (uuid) { emergency_access (uuid) {
uuid -> Text, uuid -> Text,
@ -239,6 +261,7 @@ joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid)); joinable!(users_organizations -> users (user_uuid));
joinable!(emergency_access -> users (grantor_uuid)); joinable!(emergency_access -> users (grantor_uuid));
joinable!(sso_nonce -> organizations (org_uuid));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
attachments, attachments,

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

@ -100,11 +100,25 @@ table! {
uuid -> Text, uuid -> Text,
name -> Text, name -> Text,
billing_email -> Text, billing_email -> Text,
identifier -> Nullable<Text>,
private_key -> Nullable<Text>, private_key -> Nullable<Text>,
public_key -> Nullable<Text>, public_key -> Nullable<Text>,
} }
} }
table! {
sso_config (uuid) {
uuid -> Text,
org_uuid -> Text,
use_sso -> Bool,
callback_path -> Text,
signed_out_callback_path -> Text,
authority -> Nullable<Text>,
client_id -> Nullable<Text>,
client_secret -> Nullable<Text>,
}
}
table! { table! {
sends (uuid) { sends (uuid) {
uuid -> Text, uuid -> Text,
@ -203,6 +217,14 @@ table! {
} }
} }
table! {
sso_nonce (uuid) {
uuid -> Text,
org_uuid -> Text,
nonce -> Text,
}
}
table! { table! {
emergency_access (uuid) { emergency_access (uuid) {
uuid -> Text, uuid -> Text,
@ -239,6 +261,7 @@ joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid)); joinable!(users_organizations -> users (user_uuid));
joinable!(emergency_access -> users (grantor_uuid)); joinable!(emergency_access -> users (grantor_uuid));
joinable!(sso_nonce -> organizations (org_uuid));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
attachments, attachments,

Loading…
Cancel
Save