refactor(oidc): prep. support of Open Cloud Mesh

add federation kind (OCM, OIDC, MagicLink)
    rename oidc_provider into federation_issuer
    rename oidc_subject into federation_subject
This commit is contained in:
Edouard Vanbelle
2026-08-08 15:10:23 +02:00
parent 521040521d
commit 10a8dd7d8b
13 changed files with 1254 additions and 127 deletions
@@ -110,8 +110,9 @@ mod tests {
"alice@example.com".to_string(),
Some("alice".to_string()),
None,
None,
None,
None, // federation_kind
None, // federation_issuer
None, // federation_subject
UserRole::User,
0,
false,
@@ -152,8 +153,9 @@ mod tests {
"bob@example.com".to_string(),
None,
None,
None,
None,
None, // federation_kind
None, // federation_issuer
None, // federation_subject
UserRole::User,
0,
false,
+3 -3
View File
@@ -131,7 +131,7 @@ pub struct AdminUserSummaryDto {
pub is_external: bool,
/// TRUE when the user has a server-verifiable password on file
/// (`password_hash IS NOT NULL`). The admin table uses this
/// alongside `oidc_provider` and `opaque_registered` to render
/// alongside `federation_issuer` and `opaque_registered` to render
/// the user's full capability set: a `password` chip lights up
/// here, an OIDC provider name renders the SSO badge, an
/// envelope-on-file flips the OPAQUE chip. A user with none of
@@ -171,7 +171,7 @@ impl From<UserListEntry> for AdminUserSummaryDto {
storage_used_bytes: entry.storage_used_bytes,
last_login_at: entry.last_login_at,
active: entry.active,
auth_provider: entry.oidc_provider.unwrap_or_else(|| "local".to_string()),
auth_provider: entry.federation_issuer.unwrap_or_else(|| "local".to_string()),
is_external: entry.is_external,
has_password: entry.has_password,
opaque_registered: entry.opaque_registered,
@@ -208,7 +208,7 @@ impl From<User> for UserDto {
last_login_at: p.last_login_at,
active: p.active,
// Some(provider) moves the String; None still allocates "local".
auth_provider: p.oidc_provider.unwrap_or_else(|| "local".to_string()),
auth_provider: p.federation_issuer.unwrap_or_else(|| "local".to_string()),
image: p.image,
can_edit_image,
is_external: p.is_external,
+12 -8
View File
@@ -194,10 +194,14 @@ pub trait UserStoragePort: Send + Sync + 'static {
/// Changes a user's password
async fn change_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DomainError>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(
/// Finds a user by federation (issuer, subject) pair. Historically
/// called for OIDC lookups (the only federation kind in-tree at rename
/// time); after Phase B/C of the federation-identity rename the
/// caller passes the true `iss` URL rather than a display label. See
/// `docs/plan/ocm.md § Schema rename` for the transition.
async fn get_user_by_federation_subject(
&self,
provider: &str,
issuer: &str,
subject: &str,
) -> Result<User, DomainError>;
@@ -383,12 +387,12 @@ pub trait SessionStoragePort: Send + Sync + 'static {
/// OIDC Back-Channel Logout fallback when the IdP didn't supply a `sid`:
/// revoke every session belonging to the user identified by
/// `(oidc_provider, oidc_subject)`. Returns the affected user id, or
/// `None` if we don't know that user.
async fn revoke_user_sessions_by_oidc_subject(
/// `(federation_issuer, federation_subject)`. Returns the affected
/// user id, or `None` if we don't know that user.
async fn revoke_user_sessions_by_federation_subject(
&self,
oidc_provider: &str,
oidc_subject: &str,
issuer: &str,
subject: &str,
) -> Result<Option<Uuid>, DomainError>;
}
@@ -560,8 +560,9 @@ impl AuthApplicationService {
dto.email.clone(),
dto.username.clone(),
password_hash,
None,
None,
None, // federation_kind: local password registration
None, // federation_issuer
None, // federation_subject
role,
quota,
false,
@@ -662,8 +663,9 @@ impl AuthApplicationService {
email,
Some(username.clone()),
Some(password_hash),
None,
None,
None, // federation_kind: setup admin is local
None, // federation_issuer
None, // federation_subject
role,
quota,
false,
@@ -1523,7 +1525,7 @@ impl AuthApplicationService {
.await?
} else if let Some(sub) = claims.sub.as_ref() {
self.session_storage
.revoke_user_sessions_by_oidc_subject(&provider_name, sub)
.revoke_user_sessions_by_federation_subject(&provider_name, sub)
.await?
.into_iter()
.collect()
@@ -2810,8 +2812,9 @@ impl AuthApplicationService {
email,
Some(dto.username.clone()),
Some(password_hash),
None,
None,
None, // federation_kind: admin-created external, no federation link yet
None, // federation_issuer
None, // federation_subject
UserRole::User,
0,
true,
@@ -2821,8 +2824,9 @@ impl AuthApplicationService {
email,
Some(dto.username.clone()),
Some(password_hash),
None,
None,
None, // federation_kind: admin-created local user
None, // federation_issuer
None, // federation_subject
role,
quota,
false,
@@ -3395,7 +3399,7 @@ impl AuthApplicationService {
// 5. Look up existing user by OIDC subject
let user = match self
.user_storage
.get_user_by_oidc_subject(&provider_name, &claims.sub)
.get_user_by_federation_subject(&provider_name, &claims.sub)
.await
{
Ok(mut existing_user) => {
@@ -3511,6 +3515,14 @@ impl AuthApplicationService {
oidc_email,
Some(username.clone()),
None,
Some(crate::domain::entities::user::FederationKind::Oidc),
// TODO Phase B: `provider_name` still carries the
// OXICLOUD_OIDC_PROVIDER_NAME display label instead
// of the true `iss` URL. Lazy-rebind on subsequent
// logins converts the row (see docs/plan/ocm.md
// § Rename PR — Phase B). First-login value is the
// label for backwards compatibility with existing
// rows.
Some(provider_name.clone()),
Some(claims.sub.clone()),
role,
@@ -225,12 +225,18 @@ impl MagicLinkInviteService {
// External users are created without a username or password.
// `password_hash IS NULL` is the canonical no-password marker.
//
// federation_kind stays None in Phase A of the federation-identity
// rename — magic-link externals get their `federation_kind` stamp
// in a future PR when the invite handler is refactored to opt into
// the composable federation model. Behaviour is unchanged today.
let mut user = User::new(
normalised_email.to_string(),
None,
None,
None,
None,
None, // federation_kind
None, // federation_issuer
None, // federation_subject
UserRole::User,
0,
true,
@@ -1006,15 +1012,21 @@ mod tests {
use crate::domain::entities::user::{User, UserRole};
fn user(password: Option<&str>, oidc: Option<(&str, &str)>) -> User {
let (provider, subject) = match oidc {
Some((p, s)) => (Some(p.to_string()), Some(s.to_string())),
None => (None, None),
use crate::domain::entities::user::FederationKind;
let (kind, issuer, subject) = match oidc {
Some((p, s)) => (
Some(FederationKind::Oidc),
Some(p.to_string()),
Some(s.to_string()),
),
None => (None, None, None),
};
User::new(
"test@example.com".to_string(),
None,
password.map(str::to_string),
provider,
kind,
issuer,
subject,
UserRole::User,
0,