From 5c7161d1a6e4e4f10289d8f20385fc8a245a7654 Mon Sep 17 00:00:00 2001 From: Stuart Heap Date: Wed, 22 Sep 2021 11:44:23 +0200 Subject: [PATCH] split sso_config into own table --- .../mysql/2021-09-16-133000_add-sso/down.sql | 1 - .../mysql/2021-09-16-133000_add-sso/up.sql | 13 --- .../mysql/2021-09-16-133000_add_sso/down.sql | 2 + .../mysql/2021-09-16-133000_add_sso/up.sql | 18 +++ .../2021-09-16-133000_add_sso/down.sql | 1 + .../2021-09-16-133000_add_sso/up.sql | 23 ++-- .../sqlite/2021-09-16-133000_add_sso/down.sql | 1 + .../sqlite/2021-09-16-133000_add_sso/up.sql | 23 ++-- src/api/core/organizations.rs | 40 +++++-- src/api/identity.rs | 42 +++---- src/db/models/mod.rs | 2 + src/db/models/organization.rs | 18 --- src/db/models/sso_config.rs | 104 ++++++++++++++++++ src/db/schemas/mysql/schema.rs | 7 ++ src/db/schemas/postgresql/schema.rs | 7 ++ src/db/schemas/sqlite/schema.rs | 7 ++ 16 files changed, 228 insertions(+), 81 deletions(-) delete mode 100644 migrations/mysql/2021-09-16-133000_add-sso/down.sql delete mode 100644 migrations/mysql/2021-09-16-133000_add-sso/up.sql create mode 100644 migrations/mysql/2021-09-16-133000_add_sso/down.sql create mode 100644 migrations/mysql/2021-09-16-133000_add_sso/up.sql create mode 100644 src/db/models/sso_config.rs diff --git a/migrations/mysql/2021-09-16-133000_add-sso/down.sql b/migrations/mysql/2021-09-16-133000_add-sso/down.sql deleted file mode 100644 index 2c946dc5..00000000 --- a/migrations/mysql/2021-09-16-133000_add-sso/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE sso_nonce; diff --git a/migrations/mysql/2021-09-16-133000_add-sso/up.sql b/migrations/mysql/2021-09-16-133000_add-sso/up.sql deleted file mode 100644 index 38a8ecbb..00000000 --- a/migrations/mysql/2021-09-16-133000_add-sso/up.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE organizations ADD COLUMN identifier TEXT; -ALTER TABLE organizations ADD COLUMN use_sso BOOLEAN NOT NULL; -ALTER TABLE organizations ADD COLUMN callback_path TEXT NOT NULL; -ALTER TABLE organizations ADD COLUMN signed_out_callback_path TEXT NOT NULL; -ALTER TABLE organizations ADD COLUMN authority TEXT; -ALTER TABLE organizations ADD COLUMN client_id TEXT; -ALTER TABLE organizations ADD COLUMN client_secret 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 -); diff --git a/migrations/mysql/2021-09-16-133000_add_sso/down.sql b/migrations/mysql/2021-09-16-133000_add_sso/down.sql new file mode 100644 index 00000000..ade3aeed --- /dev/null +++ b/migrations/mysql/2021-09-16-133000_add_sso/down.sql @@ -0,0 +1,2 @@ +DROP TABLE sso_nonce; +DROP TABLE sso_config; diff --git a/migrations/mysql/2021-09-16-133000_add_sso/up.sql b/migrations/mysql/2021-09-16-133000_add_sso/up.sql new file mode 100644 index 00000000..e4210214 --- /dev/null +++ b/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 +); diff --git a/migrations/postgresql/2021-09-16-133000_add_sso/down.sql b/migrations/postgresql/2021-09-16-133000_add_sso/down.sql index 2c946dc5..ade3aeed 100644 --- a/migrations/postgresql/2021-09-16-133000_add_sso/down.sql +++ b/migrations/postgresql/2021-09-16-133000_add_sso/down.sql @@ -1 +1,2 @@ DROP TABLE sso_nonce; +DROP TABLE sso_config; diff --git a/migrations/postgresql/2021-09-16-133000_add_sso/up.sql b/migrations/postgresql/2021-09-16-133000_add_sso/up.sql index 38a8ecbb..e4210214 100644 --- a/migrations/postgresql/2021-09-16-133000_add_sso/up.sql +++ b/migrations/postgresql/2021-09-16-133000_add_sso/up.sql @@ -1,13 +1,18 @@ -ALTER TABLE organizations ADD COLUMN identifier TEXT; -ALTER TABLE organizations ADD COLUMN use_sso BOOLEAN NOT NULL; -ALTER TABLE organizations ADD COLUMN callback_path TEXT NOT NULL; -ALTER TABLE organizations ADD COLUMN signed_out_callback_path TEXT NOT NULL; -ALTER TABLE organizations ADD COLUMN authority TEXT; -ALTER TABLE organizations ADD COLUMN client_id TEXT; -ALTER TABLE organizations ADD COLUMN client_secret TEXT; +ALTER TABLE organizations ADD COLUMN identifier TEXT; CREATE TABLE sso_nonce ( - uuid CHAR(36) NOT NULL PRIMARY KEY, + uuid CHAR(36) NOT NULL PRIMARY KEY, org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid), - nonce CHAR(36) NOT NULL + 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 ); diff --git a/migrations/sqlite/2021-09-16-133000_add_sso/down.sql b/migrations/sqlite/2021-09-16-133000_add_sso/down.sql index 2c946dc5..ade3aeed 100644 --- a/migrations/sqlite/2021-09-16-133000_add_sso/down.sql +++ b/migrations/sqlite/2021-09-16-133000_add_sso/down.sql @@ -1 +1,2 @@ DROP TABLE sso_nonce; +DROP TABLE sso_config; diff --git a/migrations/sqlite/2021-09-16-133000_add_sso/up.sql b/migrations/sqlite/2021-09-16-133000_add_sso/up.sql index 38a8ecbb..e4210214 100644 --- a/migrations/sqlite/2021-09-16-133000_add_sso/up.sql +++ b/migrations/sqlite/2021-09-16-133000_add_sso/up.sql @@ -1,13 +1,18 @@ -ALTER TABLE organizations ADD COLUMN identifier TEXT; -ALTER TABLE organizations ADD COLUMN use_sso BOOLEAN NOT NULL; -ALTER TABLE organizations ADD COLUMN callback_path TEXT NOT NULL; -ALTER TABLE organizations ADD COLUMN signed_out_callback_path TEXT NOT NULL; -ALTER TABLE organizations ADD COLUMN authority TEXT; -ALTER TABLE organizations ADD COLUMN client_id TEXT; -ALTER TABLE organizations ADD COLUMN client_secret TEXT; +ALTER TABLE organizations ADD COLUMN identifier TEXT; CREATE TABLE sso_nonce ( - uuid CHAR(36) NOT NULL PRIMARY KEY, + uuid CHAR(36) NOT NULL PRIMARY KEY, org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid), - nonce CHAR(36) NOT NULL + 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 ); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index cc76bedd..18b1f240 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -24,6 +24,7 @@ pub fn routes() -> Vec { put_collection_users, put_organization, post_organization, + get_organization_sso, put_organization_sso, post_organization_collections, delete_organization_collection_user, @@ -116,6 +117,7 @@ fn create_organization(headers: Headers, data: JsonUpcase, conn: DbConn 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 sso_config = SsoConfig::new(org.uuid.clone()); let collection = Collection::new(org.uuid.clone(), data.CollectionName); user_org.akey = data.Key; @@ -125,6 +127,7 @@ fn create_organization(headers: Headers, data: JsonUpcase, conn: DbConn org.save(&conn)?; user_org.save(&conn)?; + sso_config.save(&conn)?; collection.save(&conn)?; Ok(Json(org.to_json())) @@ -182,7 +185,9 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe #[get("/organizations/")] fn get_organization(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult { match Organization::find_by_uuid(&org_id, &conn) { - Some(organization) => Ok(Json(organization.to_json())), + Some(organization) => { + Ok(Json(organization.to_json())) + }, None => err!("Can't find organization details"), } } @@ -219,6 +224,14 @@ fn post_organization( Ok(Json(org.to_json())) } +#[get("/organizations//sso")] +fn get_organization_sso(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult { + match SsoConfig::find_by_org(&org_id, &conn) { + Some(sso_config) => Ok(Json(sso_config.to_json())), + None => err!("Can't find organization sso config"), + } +} + #[put("/organizations//sso", data = "")] fn put_organization_sso( org_id: String, @@ -228,20 +241,23 @@ fn put_organization_sso( ) -> JsonResult { let data: OrganizationSsoUpdateData = data.into_inner().data; - let mut org = match Organization::find_by_uuid(&org_id, &conn) { - Some(organization) => organization, - None => err!("Can't find organization details"), + let mut sso_config = match SsoConfig::find_by_org(&org_id, &conn) { + Some(sso_config) => sso_config, + None => { + let sso_config = SsoConfig::new(org_id); + sso_config + }, }; - org.use_sso = data.UseSso; - org.callback_path = data.CallbackPath; - org.signed_out_callback_path = data.SignedOutCallbackPath; - org.authority = data.Authority; - org.client_id = data.ClientId; - org.client_secret = data.ClientSecret; + sso_config.use_sso = data.UseSso; + sso_config.callback_path = data.CallbackPath; + sso_config.signed_out_callback_path = data.SignedOutCallbackPath; + sso_config.authority = data.Authority; + sso_config.client_id = data.ClientId; + sso_config.client_secret = data.ClientSecret; - org.save(&conn)?; - Ok(Json(org.to_json())) + sso_config.save(&conn)?; + Ok(Json(sso_config.to_json())) } // GET /api/collections?writeOnly=false diff --git a/src/api/identity.rs b/src/api/identity.rs index 3df4defe..0f8aba3b 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -98,8 +98,9 @@ fn _authorization_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonR 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).unwrap(); + let sso_config = SsoConfig::find_by_org(&organization.uuid, &conn).unwrap(); - let (access_token, refresh_token) = match get_auth_code_access_token(&code, &organization) { + let (access_token, refresh_token) = match get_auth_code_access_token(&code, &sso_config) { Ok((access_token, refresh_token)) => (access_token, refresh_token), Err(err) => err!(err), }; @@ -537,24 +538,26 @@ fn _check_is_some(value: &Option, msg: &str) -> EmptyResult { #[get("/account/prevalidate?")] #[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)] fn prevalidate(domainHint: String, conn: DbConn) -> JsonResult { let empty_result = json!({}); - let organization = Organization::find_by_identifier(&domainHint, &conn); - // 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. - match organization { - Some(organization) => { - if !organization.use_sso { + let organization = Organization::find_by_identifier(&domainHint, &conn).unwrap(); + let sso_config = SsoConfig::find_by_org(&organization.uuid, &conn); + match sso_config { + Some(sso_config) => { + if !sso_config.use_sso { return err_code!("SSO Not allowed for organization", Status::BadRequest.code); } - if organization.authority.is_none() - || organization.client_id.is_none() - || organization.client_secret.is_none() { + 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!("Organization not found by identifier", Status::BadRequest.code); + return err_code!("Unable to find sso config", Status::BadRequest.code); }, } @@ -576,11 +579,11 @@ use openidconnect::{ Scope, OAuth2TokenResponse, }; -fn get_client_from_org (organization: &Organization) -> Result { - let redirect = organization.callback_path.to_string(); - let client_id = ClientId::new(organization.client_id.as_ref().unwrap().to_string()); - let client_secret = ClientSecret::new(organization.client_secret.as_ref().unwrap().to_string()); - let issuer_url = IssuerUrl::new(organization.authority.as_ref().unwrap().to_string()).expect("invalid issuer URL"); +fn get_client_from_sso_config (sso_config: &SsoConfig) -> Result { + let redirect = sso_config.callback_path.to_string(); + 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()).expect("invalid issuer URL"); let provider_metadata = match CoreProviderMetadata::discover(&issuer_url, http_client) { Ok(metadata) => metadata, Err(_err) => { @@ -603,7 +606,8 @@ fn authorize( conn: DbConn, ) -> ApiResult { let organization = Organization::find_by_identifier(&domain_hint, &conn).unwrap(); - match get_client_from_org(&organization) { + let sso_config = SsoConfig::find_by_org(&organization.uuid, &conn).unwrap(); + match get_client_from_sso_config(&sso_config) { Ok(client) => { let (mut authorize_url, _csrf_state, nonce) = client .authorize_url( @@ -639,10 +643,10 @@ fn authorize( fn get_auth_code_access_token ( code: &str, - organization: &Organization, + sso_config: &SsoConfig, ) -> Result<(String, String), &'static str> { let oidc_code = AuthorizationCode::new(String::from(code)); - match get_client_from_org(organization) { + match get_client_from_sso_config(sso_config) { Ok(client) => { match client.exchange_code(oidc_code).request(http_client) { Ok(token_response) => { diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 601365cb..81a8c6c3 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -10,6 +10,7 @@ mod send; mod two_factor; mod user; mod sso_nonce; +mod sso_config; pub use self::attachment::Attachment; pub use self::cipher::Cipher; @@ -23,3 +24,4 @@ pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::user::{Invitation, User, UserStampException}; pub use self::sso_nonce::SsoNonce; +pub use self::sso_config::SsoConfig; diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 7996a9ab..63b88556 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -15,12 +15,6 @@ db_object! { pub identifier: Option, pub private_key: Option, pub public_key: Option, - pub use_sso: bool, - pub callback_path: String, - pub signed_out_callback_path: String, - pub authority: Option, - pub client_id: Option, - pub client_secret: Option, } #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -139,12 +133,6 @@ impl Organization { private_key, public_key, identifier: None, - 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, } } @@ -162,7 +150,6 @@ impl Organization { "UseGroups": false, // not supported by us "UseTotp": true, "UsePolicies": true, - "UseSso": self.use_sso, "SelfHost": true, "UseApi": false, // not supported by us "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), @@ -180,11 +167,6 @@ impl Organization { "PlanType": 5, // TeamsAnnually plan "UsersGetPremium": true, "Object": "organization", - "CallbackPath": self.callback_path, - "SignedOutCallbackPath": self.signed_out_callback_path, - "Authority": self.authority, - "ClientId": self.client_id, - "ClientSecret": self.client_secret, }) } } diff --git a/src/db/models/sso_config.rs b/src/db/models/sso_config.rs new file mode 100644 index 00000000..61e63113 --- /dev/null +++ b/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, + pub client_id: Option, + pub client_secret: Option, + } +} + +/// 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 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 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 fn find_by_org(org_uuid: &str, conn: &DbConn) -> Option { + db_run! { conn: { + sso_config::table + .filter(sso_config::org_uuid.eq(org_uuid)) + .first::(conn) + .ok() + .from_db() + }} + } +} diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 6e0c42ad..34b5d737 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -103,6 +103,13 @@ table! { identifier -> Nullable, private_key -> Nullable, public_key -> Nullable, + } +} + +table! { + sso_config (uuid) { + uuid -> Text, + org_uuid -> Text, use_sso -> Bool, callback_path -> Text, signed_out_callback_path -> Text, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 71a95ecd..88591b55 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -103,6 +103,13 @@ table! { identifier -> Nullable, private_key -> Nullable, public_key -> Nullable, + } +} + +table! { + sso_config (uuid) { + uuid -> Text, + org_uuid -> Text, use_sso -> Bool, callback_path -> Text, signed_out_callback_path -> Text, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 71a95ecd..88591b55 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -103,6 +103,13 @@ table! { identifier -> Nullable, private_key -> Nullable, public_key -> Nullable, + } +} + +table! { + sso_config (uuid) { + uuid -> Text, + org_uuid -> Text, use_sso -> Bool, callback_path -> Text, signed_out_callback_path -> Text,