Browse Source

split sso_config into own table

pull/1955/head
Stuart Heap 4 years ago
parent
commit
5c7161d1a6
No known key found for this signature in database GPG Key ID: C753450AB379AA25
  1. 1
      migrations/mysql/2021-09-16-133000_add-sso/down.sql
  2. 13
      migrations/mysql/2021-09-16-133000_add-sso/up.sql
  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. 1
      migrations/postgresql/2021-09-16-133000_add_sso/down.sql
  6. 23
      migrations/postgresql/2021-09-16-133000_add_sso/up.sql
  7. 1
      migrations/sqlite/2021-09-16-133000_add_sso/down.sql
  8. 23
      migrations/sqlite/2021-09-16-133000_add_sso/up.sql
  9. 40
      src/api/core/organizations.rs
  10. 42
      src/api/identity.rs
  11. 2
      src/db/models/mod.rs
  12. 18
      src/db/models/organization.rs
  13. 104
      src/db/models/sso_config.rs
  14. 7
      src/db/schemas/mysql/schema.rs
  15. 7
      src/db/schemas/postgresql/schema.rs
  16. 7
      src/db/schemas/sqlite/schema.rs

1
migrations/mysql/2021-09-16-133000_add-sso/down.sql

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

13
migrations/mysql/2021-09-16-133000_add-sso/up.sql

@ -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
);

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
);

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

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

23
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 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 ( 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), 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
); );

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

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

23
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 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 ( 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), 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
); );

40
src/api/core/organizations.rs

@ -24,6 +24,7 @@ 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, put_organization_sso,
post_organization_collections, post_organization_collections,
delete_organization_collection_user, delete_organization_collection_user,
@ -116,6 +117,7 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
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;
@ -125,6 +127,7 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
org.save(&conn)?; org.save(&conn)?;
user_org.save(&conn)?; user_org.save(&conn)?;
sso_config.save(&conn)?;
collection.save(&conn)?; collection.save(&conn)?;
Ok(Json(org.to_json())) Ok(Json(org.to_json()))
@ -182,7 +185,9 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe
#[get("/organizations/<org_id>")] #[get("/organizations/<org_id>")]
fn get_organization(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult { fn get_organization(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult {
match Organization::find_by_uuid(&org_id, &conn) { 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"), None => err!("Can't find organization details"),
} }
} }
@ -219,6 +224,14 @@ fn post_organization(
Ok(Json(org.to_json())) Ok(Json(org.to_json()))
} }
#[get("/organizations/<org_id>/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/<org_id>/sso", data = "<data>")] #[put("/organizations/<org_id>/sso", data = "<data>")]
fn put_organization_sso( fn put_organization_sso(
org_id: String, org_id: String,
@ -228,20 +241,23 @@ fn put_organization_sso(
) -> JsonResult { ) -> JsonResult {
let data: OrganizationSsoUpdateData = data.into_inner().data; let data: OrganizationSsoUpdateData = data.into_inner().data;
let mut org = match Organization::find_by_uuid(&org_id, &conn) { let mut sso_config = match SsoConfig::find_by_org(&org_id, &conn) {
Some(organization) => organization, Some(sso_config) => sso_config,
None => err!("Can't find organization details"), None => {
let sso_config = SsoConfig::new(org_id);
sso_config
},
}; };
org.use_sso = data.UseSso; sso_config.use_sso = data.UseSso;
org.callback_path = data.CallbackPath; sso_config.callback_path = data.CallbackPath;
org.signed_out_callback_path = data.SignedOutCallbackPath; sso_config.signed_out_callback_path = data.SignedOutCallbackPath;
org.authority = data.Authority; sso_config.authority = data.Authority;
org.client_id = data.ClientId; sso_config.client_id = data.ClientId;
org.client_secret = data.ClientSecret; sso_config.client_secret = data.ClientSecret;
org.save(&conn)?; sso_config.save(&conn)?;
Ok(Json(org.to_json())) Ok(Json(sso_config.to_json()))
} }
// GET /api/collections?writeOnly=false // GET /api/collections?writeOnly=false

42
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 org_identifier = data.org_identifier.as_ref().unwrap();
let code = data.code.as_ref().unwrap(); let code = data.code.as_ref().unwrap();
let organization = Organization::find_by_identifier(org_identifier, &conn).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), Ok((access_token, refresh_token)) => (access_token, refresh_token),
Err(err) => err!(err), Err(err) => err!(err),
}; };
@ -537,24 +538,26 @@ fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
#[get("/account/prevalidate?<domainHint>")] #[get("/account/prevalidate?<domainHint>")]
#[allow(non_snake_case)] #[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 { fn prevalidate(domainHint: String, conn: DbConn) -> JsonResult {
let empty_result = json!({}); let empty_result = json!({});
let organization = Organization::find_by_identifier(&domainHint, &conn); let organization = Organization::find_by_identifier(&domainHint, &conn).unwrap();
// The compiler warns about unreachable code here. But I've tested it, and it seems to work let sso_config = SsoConfig::find_by_org(&organization.uuid, &conn);
// as expected. All errors appear to be reachable, as is the Ok response. match sso_config {
match organization { Some(sso_config) => {
Some(organization) => { if !sso_config.use_sso {
if !organization.use_sso {
return err_code!("SSO Not allowed for organization", Status::BadRequest.code); return err_code!("SSO Not allowed for organization", Status::BadRequest.code);
} }
if organization.authority.is_none() if sso_config.authority.is_none()
|| organization.client_id.is_none() || sso_config.client_id.is_none()
|| organization.client_secret.is_none() { || sso_config.client_secret.is_none() {
return err_code!("Organization is incorrectly configured for SSO", Status::BadRequest.code); return err_code!("Organization is incorrectly configured for SSO", Status::BadRequest.code);
} }
}, },
None => { 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, Scope, OAuth2TokenResponse,
}; };
fn get_client_from_org (organization: &Organization) -> Result<CoreClient, &'static str> { fn get_client_from_sso_config (sso_config: &SsoConfig) -> Result<CoreClient, &'static str> {
let redirect = organization.callback_path.to_string(); let redirect = sso_config.callback_path.to_string();
let client_id = ClientId::new(organization.client_id.as_ref().unwrap().to_string()); let client_id = ClientId::new(sso_config.client_id.as_ref().unwrap().to_string());
let client_secret = ClientSecret::new(organization.client_secret.as_ref().unwrap().to_string()); let client_secret = ClientSecret::new(sso_config.client_secret.as_ref().unwrap().to_string());
let issuer_url = IssuerUrl::new(organization.authority.as_ref().unwrap().to_string()).expect("invalid issuer URL"); 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) { let provider_metadata = match CoreProviderMetadata::discover(&issuer_url, http_client) {
Ok(metadata) => metadata, Ok(metadata) => metadata,
Err(_err) => { Err(_err) => {
@ -603,7 +606,8 @@ fn authorize(
conn: DbConn, conn: DbConn,
) -> ApiResult<Redirect> { ) -> ApiResult<Redirect> {
let organization = Organization::find_by_identifier(&domain_hint, &conn).unwrap(); 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) => { Ok(client) => {
let (mut authorize_url, _csrf_state, nonce) = client let (mut authorize_url, _csrf_state, nonce) = client
.authorize_url( .authorize_url(
@ -639,10 +643,10 @@ fn authorize(
fn get_auth_code_access_token ( fn get_auth_code_access_token (
code: &str, code: &str,
organization: &Organization, sso_config: &SsoConfig,
) -> Result<(String, String), &'static str> { ) -> Result<(String, String), &'static str> {
let oidc_code = AuthorizationCode::new(String::from(code)); let oidc_code = AuthorizationCode::new(String::from(code));
match get_client_from_org(organization) { match get_client_from_sso_config(sso_config) {
Ok(client) => { Ok(client) => {
match client.exchange_code(oidc_code).request(http_client) { match client.exchange_code(oidc_code).request(http_client) {
Ok(token_response) => { Ok(token_response) => {

2
src/db/models/mod.rs

@ -10,6 +10,7 @@ mod send;
mod two_factor; mod two_factor;
mod user; mod user;
mod sso_nonce; mod sso_nonce;
mod sso_config;
pub use self::attachment::Attachment; pub use self::attachment::Attachment;
pub use self::cipher::Cipher; 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::two_factor::{TwoFactor, TwoFactorType};
pub use self::user::{Invitation, User, UserStampException}; pub use self::user::{Invitation, User, UserStampException};
pub use self::sso_nonce::SsoNonce; pub use self::sso_nonce::SsoNonce;
pub use self::sso_config::SsoConfig;

18
src/db/models/organization.rs

@ -15,12 +15,6 @@ db_object! {
pub identifier: Option<String>, pub identifier: Option<String>,
pub private_key: Option<String>, pub private_key: Option<String>,
pub public_key: Option<String>, pub public_key: Option<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>,
} }
#[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -139,12 +133,6 @@ impl Organization {
private_key, private_key,
public_key, public_key,
identifier: None, 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 "UseGroups": false, // not supported by us
"UseTotp": true, "UseTotp": true,
"UsePolicies": true, "UsePolicies": true,
"UseSso": self.use_sso,
"SelfHost": true, "SelfHost": true,
"UseApi": false, // not supported by us "UseApi": false, // not supported by us
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
@ -180,11 +167,6 @@ impl Organization {
"PlanType": 5, // TeamsAnnually plan "PlanType": 5, // TeamsAnnually plan
"UsersGetPremium": true, "UsersGetPremium": true,
"Object": "organization", "Object": "organization",
"CallbackPath": self.callback_path,
"SignedOutCallbackPath": self.signed_out_callback_path,
"Authority": self.authority,
"ClientId": self.client_id,
"ClientSecret": self.client_secret,
}) })
} }
} }

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 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<Self> {
db_run! { conn: {
sso_config::table
.filter(sso_config::org_uuid.eq(org_uuid))
.first::<SsoConfigDb>(conn)
.ok()
.from_db()
}}
}
}

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

@ -103,6 +103,13 @@ table! {
identifier -> Nullable<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, use_sso -> Bool,
callback_path -> Text, callback_path -> Text,
signed_out_callback_path -> Text, signed_out_callback_path -> Text,

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

@ -103,6 +103,13 @@ table! {
identifier -> Nullable<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, use_sso -> Bool,
callback_path -> Text, callback_path -> Text,
signed_out_callback_path -> Text, signed_out_callback_path -> Text,

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

@ -103,6 +103,13 @@ table! {
identifier -> Nullable<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, use_sso -> Bool,
callback_path -> Text, callback_path -> Text,
signed_out_callback_path -> Text, signed_out_callback_path -> Text,

Loading…
Cancel
Save