Browse Source

Merge 6b4d8048e0 into 660faee68e

pull/7422/merge
Tom 22 hours ago
committed by GitHub
parent
commit
4ab9377ef0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .env.template
  2. 6
      src/config.rs
  3. 61
      src/sso.rs

4
.env.template

@ -501,6 +501,10 @@
## Allow unknown email verification status. Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover.
# SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION=false
## Automatically add users on their first SSO sign-in as accepted members of this organization.
## An administrator must confirm them and assign collections or groups. No invitation email is sent.
# SSO_DEFAULT_ORGANIZATION_UUID=00000000-0000-0000-0000-000000000000
## Base URL of the OIDC server (auto-discovery is used)
## - Should not include the `/.well-known/openid-configuration` part and no trailing `/`
## - ${SSO_AUTHORITY}/.well-known/openid-configuration should return a json document: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse

6
src/config.rs

@ -805,6 +805,8 @@ make_config! {
sso_signups_match_email: bool, true, def, true;
/// Allow unknown email verification status |> Allowing this with `SSO_SIGNUPS_MATCH_EMAIL=true` open potential account takeover.
sso_allow_unknown_email_verification: bool, true, def, false;
/// Default organization UUID |> Automatically add users on their first SSO sign-in as accepted members of this organization. An administrator must confirm them and assign collections or groups. No invitation email is sent.
sso_default_organization_uuid: String, true, option;
/// Client ID
sso_client_id: String, true, def, String::new();
/// Client Key
@ -1086,6 +1088,10 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?;
}
if let Some(org_uuid) = &cfg.sso_default_organization_uuid {
crate::sso::normalize_organization_uuid(org_uuid)?;
}
if cfg._enable_yubico {
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support")

61
src/sso.rs

@ -12,7 +12,10 @@ use crate::{
auth::{AuthMethod, AuthTokens, BW_EXPIRATION, DEFAULT_REFRESH_VALIDITY, TokenWrapper},
db::{
DbConn,
models::{Device, OIDCAuthenticatedUser, SsoAuth, SsoUser, User},
models::{
Device, Membership, MembershipStatus, MembershipType, OIDCAuthenticatedUser, Organization, OrganizationId,
SsoAuth, SsoUser, User,
},
},
sso_client::Client,
};
@ -328,6 +331,8 @@ pub async fn redeem(
sso_auth.delete(conn).await?;
if sso_user.is_none() {
enroll_user_in_default_organization(user, conn).await?;
let user_sso = SsoUser {
user_uuid: user.uuid.clone(),
identifier: auth_user.identifier.clone(),
@ -354,6 +359,38 @@ pub async fn redeem(
}
}
async fn enroll_user_in_default_organization(user: &User, conn: &DbConn) -> ApiResult<()> {
let Some(org_uuid) = CONFIG.sso_default_organization_uuid() else {
return Ok(());
};
let org_id = normalize_organization_uuid(&org_uuid)?;
if Membership::find_by_user_and_org(&user.uuid, &org_id, conn).await.is_some() {
return Ok(());
}
if Organization::find_by_uuid(&org_id, conn).await.is_none() {
err!("The organization configured in `SSO_DEFAULT_ORGANIZATION_UUID` does not exist")
}
let mut membership = Membership::new(user.uuid.clone(), org_id.clone(), None);
membership.status = MembershipStatus::Accepted as i32;
membership.atype = MembershipType::User as i32;
membership.save(conn).await?;
info!("Added SSO user {} to default organization {} pending confirmation", user.uuid, org_id);
Ok(())
}
// `Uuid::parse_str` also accepts non-canonical forms (uppercase, braced, without hyphens),
// while stored organization uuids are always lowercase hyphenated and compared as strings.
pub(crate) fn normalize_organization_uuid(org_uuid: &str) -> ApiResult<OrganizationId> {
let Ok(parsed) = uuid::Uuid::parse_str(org_uuid) else {
err!("`SSO_DEFAULT_ORGANIZATION_UUID` must be a valid UUID")
};
Ok(OrganizationId::from(parsed.to_string()))
}
// We always return a refresh_token (with no refresh_token some secrets are not displayed in the web front).
// If there is no SSO refresh_token, we keep the access_token to be able to call user_info to check for validity
pub fn create_auth_tokens(
@ -471,3 +508,25 @@ pub async fn exchange_refresh_token(
None => err!("No token present while in SSO"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_organization_uuid_to_canonical_form() {
for input in [
"1B2C3D4E-5F60-7182-93A4-B5C6D7E8F901",
"{1b2c3d4e-5f60-7182-93a4-b5c6d7e8f901}",
"1b2c3d4e5f60718293a4b5c6d7e8f901",
] {
let org_id = normalize_organization_uuid(input).expect("valid UUID form should be accepted");
assert_eq!(org_id.to_string(), "1b2c3d4e-5f60-7182-93a4-b5c6d7e8f901");
}
}
#[test]
fn rejects_invalid_organization_uuid() {
assert!(normalize_organization_uuid("not-a-uuid").is_err());
}
}

Loading…
Cancel
Save