Browse Source

Apply review feedback

- Rename the test module to `tests` and move it to the bottom of the file
- Remove the redundant membership defaults test and inline the helper
- Align the SSO_DEFAULT_ORGANIZATION_UUID description between
  .env.template and config.rs
- Reuse normalize_organization_uuid in the config validation so
  validation and organization lookup accept the same input
pull/7422/head
tom27052006 1 week ago
parent
commit
9fc9b7c6fa
  1. 2
      .env.template
  2. 8
      src/config.rs
  3. 74
      src/sso.rs

2
.env.template

@ -502,7 +502,7 @@
# 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 users and assign collections or groups. No invitation email is sent.
## 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)

8
src/config.rs

@ -805,7 +805,7 @@ 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 before they can access assigned organization data.
/// 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();
@ -1088,10 +1088,8 @@ 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
&& uuid::Uuid::parse_str(org_uuid).is_err()
{
err!("`SSO_DEFAULT_ORGANIZATION_UUID` must be a valid UUID")
if let Some(org_uuid) = &cfg.sso_default_organization_uuid {
crate::sso::normalize_organization_uuid(org_uuid)?;
}
if cfg._enable_yubico {

74
src/sso.rs

@ -14,7 +14,7 @@ use crate::{
DbConn,
models::{
Device, Membership, MembershipStatus, MembershipType, OIDCAuthenticatedUser, Organization, OrganizationId,
SsoAuth, SsoUser, User, UserId,
SsoAuth, SsoUser, User,
},
},
sso_client::Client,
@ -373,7 +373,9 @@ async fn enroll_user_in_default_organization(user: &User, conn: &DbConn) -> ApiR
err!("The organization configured in `SSO_DEFAULT_ORGANIZATION_UUID` does not exist")
}
let membership = new_default_sso_membership(user.uuid.clone(), org_id.clone());
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);
@ -382,57 +384,13 @@ async fn enroll_user_in_default_organization(user: &User, conn: &DbConn) -> ApiR
// `Uuid::parse_str` also accepts non-canonical forms (uppercase, braced, without hyphens),
// while stored organization uuids are always lowercase hyphenated and compared as strings.
fn normalize_organization_uuid(org_uuid: &str) -> ApiResult<OrganizationId> {
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()))
}
fn new_default_sso_membership(user_uuid: UserId, org_id: OrganizationId) -> Membership {
let mut membership = Membership::new(user_uuid, org_id, None);
membership.status = MembershipStatus::Accepted as i32;
membership.atype = MembershipType::User as i32;
membership
}
#[cfg(test)]
mod default_organization_tests {
use crate::db::models::{MembershipStatus, MembershipType, OrganizationId, UserId};
use super::*;
#[test]
fn default_sso_membership_is_accepted_user_without_full_access() {
let membership = new_default_sso_membership(
UserId::from("00000000-0000-0000-0000-000000000001"),
OrganizationId::from("00000000-0000-0000-0000-000000000002"),
);
assert!(!membership.access_all);
assert_eq!(membership.status, MembershipStatus::Accepted as i32);
assert_eq!(membership.atype, MembershipType::User as i32);
assert!(membership.invited_by_email.is_none());
}
#[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());
}
}
// 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(
@ -550,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