Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 24 additions & 1 deletion credentialsd-common/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub struct SetDevicePinOptions {}
pub struct Credential {
pub id: String,
pub name: String,
pub username: Option<String>,
pub display_name: Option<String>,
}

/// Emitted when an an authenticator presents multiple matching credentials, and
Expand Down Expand Up @@ -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("[email protected]"),
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("[email protected]"));
assert_eq!(get("display_name").as_deref(), Some("Alice"));
assert_eq!(get("username"), None);
}
}
43 changes: 36 additions & 7 deletions credentialsd-ui/src/gui/view_model/gtk/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mod imp {
pub name: RefCell<String>,

#[property(get, set)]
pub username: RefCell<Option<String>>,
pub display_name: RefCell<Option<String>>,
}

// The central trait for subclassing a GObject
Expand All @@ -38,24 +38,24 @@ glib::wrapper! {
}

impl CredentialObject {
pub fn new(id: &str, name: &str, username: &Option<String>) -> Self {
pub fn new(id: &str, name: &str, display_name: &Option<String>) -> 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()
}
}

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)
}
}

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)
}
}

Expand All @@ -64,7 +64,36 @@ impl From<CredentialObject> 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("[email protected]"),
display_name: Some(String::from("Alice")),
};
let obj = CredentialObject::from(&cred);
assert_eq!(obj.name(), "[email protected]");
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("[email protected]"),
display_name: None,
};
assert_eq!(Credential::from(CredentialObject::from(&cred)), cred);
}
}
4 changes: 2 additions & 2 deletions credentialsd-ui/src/gui/view_model/gtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion credentialsd/src/credential_service/nfc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ impl From<NfcStateInternal> for NfcState {
.as_ref()
.and_then(|u| u.name.clone())
.unwrap_or_else(|| String::from("<unknown>")),
username: x
display_name: x
.user
.as_ref()
.map(|u| u.display_name.clone())
Expand Down
2 changes: 1 addition & 1 deletion credentialsd/src/credential_service/usb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ impl From<UsbStateInternal> for UsbState {
.as_ref()
.and_then(|u| u.name.clone())
.unwrap_or_else(|| String::from("<unknown>")),
username: x
display_name: x
.user
.as_ref()
.map(|u| u.display_name.clone())
Expand Down
10 changes: 5 additions & 5 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`
Expand All @@ -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, "[email protected]".
display_name: string. A human-readable name for the account, intended for
display, from the WebAuthn `user.displayName` field. May be empty.
}
```

Expand Down