From 20103a84cdc68c2aa8130aeecb7e17f1ff955d6e Mon Sep 17 00:00:00 2001 From: K Panuragh Date: Sun, 8 Nov 2026 20:24:50 +0530 Subject: [PATCH] common: rename Credential.username to display_name The field was filled from WebAuthn's user.displayName while the neighbouring name field holds user.name, so "username" pointed at the wrong value. Use the WebAuthn name instead and document both fields. This changes the dict key sent with SelectingCredential, so it is listed as a breaking change of the UI Controller API. Refs #79 --- CHANGELOG.md | 6 +++ credentialsd-common/src/model.rs | 25 ++++++++++- .../src/gui/view_model/gtk/credential.rs | 43 ++++++++++++++++--- credentialsd-ui/src/gui/view_model/gtk/mod.rs | 4 +- credentialsd/src/credential_service/nfc.rs | 2 +- credentialsd/src/credential_service/usb.rs | 2 +- doc/api.md | 10 ++--- 7 files changed, 75 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08bd8ac1..8f285130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # [unreleased] +## Breaking Changes + +### UI Controller API + +- Renamed the `username` field of `Credential` to `display_name`, matching WebAuthn's `user.displayName`. + # 0.4.0 [2026-11-05] ## Improvements diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 1c741842..38e245c4 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -82,7 +82,7 @@ pub struct SetDevicePinOptions {} pub struct Credential { pub id: String, pub name: String, - pub username: Option, + pub display_name: Option, } /// Emitted when an an authenticator presents multiple matching credentials, and @@ -373,3 +373,26 @@ impl Display for WindowHandle { } } } + +#[cfg(test)] +mod tests { + use zvariant::Value; + + use super::Credential; + + #[test] + fn credential_dict_uses_webauthn_field_names() { + let cred = Credential { + id: String::from("abc"), + name: String::from("alice@example.com"), + display_name: Some(String::from("Alice")), + }; + let Value::Dict(dict) = Value::from(cred) else { + panic!("Credential should serialize as a dict"); + }; + let get = |key: &str| dict.get::<&str, String>(&key).unwrap(); + assert_eq!(get("name").as_deref(), Some("alice@example.com")); + assert_eq!(get("display_name").as_deref(), Some("Alice")); + assert_eq!(get("username"), None); + } +} diff --git a/credentialsd-ui/src/gui/view_model/gtk/credential.rs b/credentialsd-ui/src/gui/view_model/gtk/credential.rs index ca8e54ff..a1c61866 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/credential.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/credential.rs @@ -18,7 +18,7 @@ mod imp { pub name: RefCell, #[property(get, set)] - pub username: RefCell>, + pub display_name: RefCell>, } // The central trait for subclassing a GObject @@ -38,10 +38,10 @@ glib::wrapper! { } impl CredentialObject { - pub fn new(id: &str, name: &str, username: &Option) -> Self { + pub fn new(id: &str, name: &str, display_name: &Option) -> Self { let mut builder = Object::builder().property("id", id).property("name", name); - if let Some(username) = username { - builder = builder.property("username", username); + if let Some(display_name) = display_name { + builder = builder.property("display-name", display_name); } builder.build() } @@ -49,13 +49,13 @@ impl CredentialObject { impl From for CredentialObject { fn from(value: crate::gui::view_model::Credential) -> Self { - Self::new(&value.id, &value.name, &value.username) + Self::new(&value.id, &value.name, &value.display_name) } } impl From<&crate::gui::view_model::Credential> for CredentialObject { fn from(value: &crate::gui::view_model::Credential) -> Self { - Self::new(&value.id, &value.name, &value.username) + Self::new(&value.id, &value.name, &value.display_name) } } @@ -64,7 +64,36 @@ impl From for crate::gui::view_model::Credential { Self { id: value.id(), name: value.name(), - username: value.username(), + display_name: value.display_name(), } } } + +#[cfg(test)] +mod tests { + use super::CredentialObject; + use crate::gui::view_model::Credential; + + #[test] + fn credential_object_round_trips_display_name() { + let cred = Credential { + id: String::from("abc"), + name: String::from("alice@example.com"), + display_name: Some(String::from("Alice")), + }; + let obj = CredentialObject::from(&cred); + assert_eq!(obj.name(), "alice@example.com"); + assert_eq!(obj.display_name().as_deref(), Some("Alice")); + assert_eq!(Credential::from(obj), cred); + } + + #[test] + fn credential_object_allows_missing_display_name() { + let cred = Credential { + id: String::from("abc"), + name: String::from("alice@example.com"), + display_name: None, + }; + assert_eq!(Credential::from(CredentialObject::from(&cred)), cred); + } +} diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index 76afc9e1..6e8356d8 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -309,8 +309,8 @@ impl ViewModel { .build(); let icon = gtk::Image::builder().icon_name(icon_name).build(); let mut display_label = credential.name().to_string(); - if let Some(username) = credential.username() { - display_label += &format!(" ({username})"); + if let Some(display_name) = credential.display_name() { + display_label += &format!(" ({display_name})"); } let label = gtk::Label::builder().label(display_label).build(); b.append(&icon); diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index 438a632e..5b7a7e7a 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -554,7 +554,7 @@ impl From for NfcState { .as_ref() .and_then(|u| u.name.clone()) .unwrap_or_else(|| String::from("")), - username: x + display_name: x .user .as_ref() .map(|u| u.display_name.clone()) diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index 12078337..526ac4af 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -674,7 +674,7 @@ impl From for UsbState { .as_ref() .and_then(|u| u.name.clone()) .unwrap_or_else(|| String::from("")), - username: x + display_name: x .user .as_ref() .map(|u| u.display_name.clone()) diff --git a/doc/api.md b/doc/api.md index d445b615..b1812c93 100644 --- a/doc/api.md +++ b/doc/api.md @@ -54,6 +54,7 @@ sequenceDiagram - (Gateway): Make `origin` and `type` a required method parameter. - (Gateway): Flatten nested D-Bus struct with `request_json` on CreateCredential and GetCredential - (Gateway): Remove Client Capabilities method from Gateway API until further notice. +- (UI Controller): Renamed the `username` field of `Credential` to `display_name`, matching WebAuthn's `user.displayName`. ### Improvements @@ -509,9 +510,6 @@ The device needs evidence of user presence (e.g. touch) to release the credentia ### BackgroundEvent::SelectingCredential -> TODO: field names of Credential type are confusing: "name" is an ID, and -> "username" is a name. We should flip them. - Multiple credentials have been found and the user has to select which to use `tag`: `0x13` @@ -521,8 +519,10 @@ Multiple credentials have been found and the user has to select which to use ``` Credential [a{sv}] { id: string. An opaque ID referring to the credential on the device. - name: string. A human-readable identifier for the account. - username: string. A human-readable name for the account, intended for display. May be empty. + name: string. A human-readable identifier for the account, from the WebAuthn + `user.name` field, for example, "alice@example.com". + display_name: string. A human-readable name for the account, intended for + display, from the WebAuthn `user.displayName` field. May be empty. } ```