diff --git a/src/application/dtos/user_dto.rs b/src/application/dtos/user_dto.rs index e90ac9a4..9147fafa 100644 --- a/src/application/dtos/user_dto.rs +++ b/src/application/dtos/user_dto.rs @@ -406,6 +406,23 @@ pub struct UpdateProfileDto { pub ui_preferences: Option, } +impl UpdateProfileDto { + /// True when the patch touches at least one field whose source of + /// truth is an external identity provider — currently `given_name` + /// and `family_name`. For OIDC-managed users the auth service + /// refuses the whole patch when this returns true (IdP pushes those + /// fields on every login; editing them here would be silently + /// overwritten). Local-only fields — `ui_preferences`, + /// `notify_on_share`, `preferred_locale`, the claim-once `username` + /// (never re-synced from the IdP) — return `false` so an OIDC user + /// can still change their view mode, share-mail opt-in, locale, + /// etc. Add future IdP-authoritative fields (e.g. `email`, `image`) + /// here if they land in this DTO. + pub fn touches_idp_managed_fields(&self) -> bool { + self.given_name.is_some() || self.family_name.is_some() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct AuthResponseDto { pub user: UserDto, diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index 7bdc3dae..4dc606e5 100644 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -2446,7 +2446,13 @@ impl AuthApplicationService { ) -> Result { let mut user = self.user_storage.get_user_by_id(caller_id).await?; - if user.is_oidc_user() { + // For OIDC-managed users, refuse the patch ONLY when it touches + // a field the IdP owns (currently `given_name` / `family_name` + // — see `UpdateProfileDto::touches_idp_managed_fields`). Local- + // only fields (ui_preferences, notify_on_share, preferred_locale, + // claim-once username) stay editable — those are personal + // OxiCloud preferences, not identity data pushed by the IdP. + if user.is_oidc_user() && dto.touches_idp_managed_fields() { tracing::info!( target: "audit", event = "auth.profile_update_rejected", @@ -2457,7 +2463,7 @@ impl AuthApplicationService { return Err(DomainError::new( ErrorKind::AccessDenied, "User", - "Your profile is managed by the identity provider and \ + "Your name is managed by the identity provider and \ cannot be edited here. Update it at the IdP — changes \ will propagate on your next sign-in.", ));