From 1c029d25d61fafd630ee26618632dea87f2d0be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Tue, 27 Oct 2026 04:13:24 +0900 Subject: [PATCH] test(agent-identity): rename validUntil to validTo DVLS names the bounds of a range `From` and `To`, so the validity window of a certificate and of a root is `validFrom` and `validTo`. The mock authority and the conformance suite replicate that API and follow the rename on the wire, in the mock's README, and in the Rust field that the camelCase derive maps to the wire name. DVLS renames the same field in Devolutions/RDM#20016, and the two changes should merge together. Issue: DGW-478 Co-Authored-By: Claude Sonnet 5.5 --- crates/agent-identity-conformance/src/dvls.rs | 8 ++++---- crates/agent-identity-mock/README.md | 6 +++--- crates/agent-identity-mock/src/api_admin/mod.rs | 6 +++--- .../agent-identity-mock/src/api_admin/tests.rs | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/agent-identity-conformance/src/dvls.rs b/crates/agent-identity-conformance/src/dvls.rs index 790cd11d0..445b86e2a 100644 --- a/crates/agent-identity-conformance/src/dvls.rs +++ b/crates/agent-identity-conformance/src/dvls.rs @@ -436,7 +436,7 @@ pub(crate) struct CertificateRecord { serial_number: Option, #[serde(rename = "validFrom", deserialize_with = "timestamp")] pub(crate) not_before: OffsetDateTime, - #[serde(rename = "validUntil", deserialize_with = "timestamp")] + #[serde(rename = "validTo", deserialize_with = "timestamp")] pub(crate) not_after: OffsetDateTime, /// The issuing root's thumbprint. #[serde(rename = "issuerThumbprint")] @@ -447,7 +447,7 @@ pub(crate) struct CertificateRecord { /// The current certificate, as an identity's summary shows it. #[derive(Clone, Debug, PartialEq, Deserialize)] pub(crate) struct CurrentCertificate { - #[serde(rename = "validUntil", deserialize_with = "timestamp")] + #[serde(rename = "validTo", deserialize_with = "timestamp")] pub(crate) not_after: OffsetDateTime, /// The issuing root's thumbprint. #[serde(rename = "issuerThumbprint")] @@ -670,7 +670,7 @@ pub(crate) struct RootSummary { #[serde(default, deserialize_with = "optional_timestamp")] valid_from: Option, #[serde(default, deserialize_with = "optional_timestamp")] - valid_until: Option, + valid_to: Option, } #[derive(Clone, Debug, PartialEq, Deserialize)] @@ -1209,7 +1209,7 @@ mod tests { "enrollmentTokenName": "test", "createdOn": "2026-09-25T12:00:00Z", "isRenewalRequested": false, - "certificate": { "issuerThumbprint": "root-id", "validUntil": "2026-12-24T12:00:00Z" } + "certificate": { "issuerThumbprint": "root-id", "validTo": "2026-12-24T12:00:00Z" } }) } diff --git a/crates/agent-identity-mock/README.md b/crates/agent-identity-mock/README.md index 898e787d3..4a029de41 100644 --- a/crates/agent-identity-mock/README.md +++ b/crates/agent-identity-mock/README.md @@ -121,7 +121,7 @@ Each operation requires the permission the table lists, and a session without it Enums are PascalCase. An identity's `status` is `Active` (an unexpired current or pending certificate), `Grace` (no unexpired certificate, but one within renewal grace), `Expired` (beyond renewal grace) or `Revoked`, as the DVLS intent's "Terminology" defines. A certificate's `status` is `Pending`, `Current` or `Retired`, a token's `status` is `Active`, `Exhausted`, `Expired` or `Revoked` (each winning over those before it), and the rotation's `phase` is `Idle` or `Rotating`. -Timestamps are RFC 3339 UTC with `Z` and named `*On`; certificate and root validity is `validFrom` and `validUntil`. +Timestamps are RFC 3339 UTC with `Z` and named `*On`; certificate and root validity is `validFrom` and `validTo`. A date-time parameter is UTC unless it carries an offset, and keeps its fraction of a second: token expiries, rotation deadlines and last-seen filters compare and return it exactly. List operations take `pageNumber` (default 1), `pageSize` (default 25, at most 100 identities or 500 tokens), `sortOrder` (`1` ascending by default, `0` descending) and `sortField`, and return `{ currentPage, data, pageSize, totalCount, totalPage }`. A page past the last is 400, and page 1 of an empty list is its last page. @@ -134,14 +134,14 @@ An ID that is not a GUID is 404, like an unknown one. | GET | `/api/v1/agent-identity/enrollment-token` | View | Page of every token, revoked ones included; `sortField` is `createdOn` (default), `name` or `expiresOn`, with ties in creation order. | | GET | `/api/v1/agent-identity/enrollment-token/{id}` | View | `AgentIdentityEnrollmentToken`, without the full token; 404 when unknown. | | POST | `/api/v1/agent-identity/enrollment-token/{id}/revoke` | Manage | 204, idempotent, or 404 when unknown. The token stays listed and readable, with `status` `Revoked`. It can still replay its own existing current key without side effects, while a new key gets `token_invalid`. | -| GET | `/api/v1/agent-identity` | View | Page of identities in creation order (`sortField` is `createdOn` only). Query: `view=Summary\|Full`, repeated `metadataKey`, repeated `status`, repeated `enrollmentTokenId`, `issuerThumbprint` (the current certificate's issuer), `lastSeenFrom` and `lastSeenTo` (inclusive), and `search` (case-insensitive friendly-name substring); filters combine with AND. The Summary view carries `id`, `friendlyName`, `status`, `isConnected`, `lastSeenOn`, `certificate { issuerThumbprint, validUntil }` while the identity has a current certificate, and the requested `metadata` keys, omitted when none match. | +| GET | `/api/v1/agent-identity` | View | Page of identities in creation order (`sortField` is `createdOn` only). Query: `view=Summary\|Full`, repeated `metadataKey`, repeated `status`, repeated `enrollmentTokenId`, `issuerThumbprint` (the current certificate's issuer), `lastSeenFrom` and `lastSeenTo` (inclusive), and `search` (case-insensitive friendly-name substring); filters combine with AND. The Summary view carries `id`, `friendlyName`, `status`, `isConnected`, `lastSeenOn`, `certificate { issuerThumbprint, validTo }` while the identity has a current certificate, and the requested `metadata` keys, omitted when none match. | | GET | `/api/v1/agent-identity/{id}` | View | Full view: the Summary members, all `metadata`, `certificates` (newest first), `createdOn`, `enrollmentTokenId`, `enrollmentTokenName`, `isRenewalRequested`, and `revokedOn` once revoked. | | PATCH | `/api/v1/agent-identity/{id}` | Manage | `{ friendlyName? }` (1..255 scalar values without leading or trailing white space); absent or `null` changes nothing; 200 → Full view. | | POST | `/api/v1/agent-identity/{id}/revoke` | Revoke | 204, idempotent; closes live streams PERMISSION_DENIED + `identity_revoked`. | | DELETE | `/api/v1/agent-identity/{id}` | Manage | 204, or 409 unless the identity is revoked. | | POST | `/api/v1/agent-identity/{id}/request-renewal` | Manage | 202; sets the flag and pushes `RenewRequested{reason:"admin"}` to live streams; 409 when the identity is revoked. | | POST | `/api/v1/agent-identity/rotation` | Rotate root | Body `{ deadlineOn? }`, `{}` for the rotation maximum: the latest `notAfter` among the old root's unexpired current or pending certificates of non-revoked identities, or now when there are none. A deadline at or before now is an emergency rotation. 202 → `AgentIdentityRotation`; 409 while rotating, without a root, or with `root_key_unavailable`; 422 for a deadline after the rotation maximum. | -| GET | `/api/v1/agent-identity/rotation` | View | `{ phase, startedOn?, deadlineOn?, oldRoot?, newRoot?, oldRootActiveIdentityCount }`, each root `{ thumbprint, validFrom, validUntil }`; the count includes each non-revoked identity with an unexpired current or pending old-root certificate, and is 0 while `Idle`. | +| GET | `/api/v1/agent-identity/rotation` | View | `{ phase, startedOn?, deadlineOn?, oldRoot?, newRoot?, oldRootActiveIdentityCount }`, each root `{ thumbprint, validFrom, validTo }`; the count includes each non-revoked identity with an unexpired current or pending old-root certificate, and is 0 while `Idle`. | | GET | `/api/v1/agent-identity/settings` | View | `{ authorityId, isAgentChannelEnabled, isAgentChannelAvailable, certificateLifetimeDays, rotationNotificationsPerSecond }`, by default `true`, `true`, `90` and `10`. | | PATCH | `/api/v1/agent-identity/settings` | Manage | `{ isAgentChannelEnabled?, certificateLifetimeDays? (1..365), rotationNotificationsPerSecond? (1..1000) }` → settings. An `isAgentChannelEnabled` that would grow a config past one channel envelope is 400 on that member, and nothing changes. | diff --git a/crates/agent-identity-mock/src/api_admin/mod.rs b/crates/agent-identity-mock/src/api_admin/mod.rs index ded51c55d..0e6eabd1c 100644 --- a/crates/agent-identity-mock/src/api_admin/mod.rs +++ b/crates/agent-identity-mock/src/api_admin/mod.rs @@ -660,7 +660,7 @@ fn identity_view(state: &State, identity: &Identity, now: i64) -> Value { if let Some(current) = identity.current_cert() { view["certificate"] = json!({ "issuerThumbprint": current.issuer, - "validUntil": rfc3339(current.not_after), + "validTo": rfc3339(current.not_after), }); } view @@ -696,7 +696,7 @@ fn certificate_view(cert: &Cert) -> Value { }, "thumbprint": cert.thumbprint, "validFrom": rfc3339(cert.not_before), - "validUntil": rfc3339(cert.not_after), + "validTo": rfc3339(cert.not_after), }) } @@ -875,7 +875,7 @@ fn rotation_view(state: &State, now: i64) -> Value { json!({ "thumbprint": root.thumbprint, "validFrom": rfc3339(root.not_before), - "validUntil": rfc3339(root.not_after), + "validTo": rfc3339(root.not_after), }) }) }; diff --git a/crates/agent-identity-mock/src/api_admin/tests.rs b/crates/agent-identity-mock/src/api_admin/tests.rs index da9812741..ac2324b6a 100644 --- a/crates/agent-identity-mock/src/api_admin/tests.rs +++ b/crates/agent-identity-mock/src/api_admin/tests.rs @@ -974,7 +974,7 @@ async fn token_has_no_delete_operation() -> anyhow::Result<()> { // ==== Identities ==== -/// The `(thumbprint, serialNumber, validFrom, validUntil)` of a DER certificate, as `AgentIdentityCertificate` defines +/// The `(thumbprint, serialNumber, validFrom, validTo)` of a DER certificate, as `AgentIdentityCertificate` defines /// them: the thumbprint is the base64url of the SHA-256 of the DER encoding, and the serial number is hexadecimal. fn certificate_fields(der: &[u8]) -> anyhow::Result<(String, String, String, String)> { use der::Decode as _; @@ -1006,10 +1006,10 @@ fn certificate_fields(der: &[u8]) -> anyhow::Result<(String, String, String, Str async fn get_identity_returns_the_full_view() -> anyhow::Result<()> { let (app, now) = frozen_app()?; let (id, chain) = enroll_with_chain(&app, json!({ "hostname": "host-a" })).await?; - let (thumbprint, serial, valid_from, valid_until) = certificate_fields(&chain[0])?; + let (thumbprint, serial, valid_from, valid_to) = certificate_fields(&chain[0])?; let (root, ..) = certificate_fields(&chain[1])?; // The certificate follows the default 90-day lifetime. - assert_eq!((&valid_from, &valid_until), (&rfc3339(now), &rfc3339(now + 90 * DAY))); + assert_eq!((&valid_from, &valid_to), (&rfc3339(now), &rfc3339(now + 90 * DAY))); let token_id = app.state.lock().await.identities[&id].token_id; let (status, view) = call(&app, Method::GET, &format!("/{id}"), None).await?; @@ -1021,7 +1021,7 @@ async fn get_identity_returns_the_full_view() -> anyhow::Result<()> { "status": "Active", "isConnected": false, "lastSeenOn": rfc3339(now), - "certificate": { "issuerThumbprint": root, "validUntil": valid_until }, + "certificate": { "issuerThumbprint": root, "validTo": valid_to }, "metadata": { "hostname": "host-a" }, "certificates": [{ "issuerThumbprint": root, @@ -1029,7 +1029,7 @@ async fn get_identity_returns_the_full_view() -> anyhow::Result<()> { "status": "Current", "thumbprint": thumbprint, "validFrom": valid_from, - "validUntil": valid_until, + "validTo": valid_to, }], "createdOn": rfc3339(now), "enrollmentTokenId": token_id, @@ -1435,7 +1435,7 @@ async fn rotation_defaults_to_the_rotation_maximum() -> anyhow::Result<()> { json!({ "thumbprint": root.thumbprint, "validFrom": rfc3339(root.not_before), - "validUntil": rfc3339(root.not_after), + "validTo": rfc3339(root.not_after), }) }; let new_root = state.issuing_root().thumbprint.clone(); @@ -1654,7 +1654,7 @@ async fn certificate_lifetime_applies_to_new_certificates() -> anyhow::Result<() assert_eq!((status, &settings["certificateLifetimeDays"]), (200, &json!(7))); let id = enroll(&app, json!({})).await?; let (_, view) = call(&app, Method::GET, &format!("/{id}"), None).await?; - assert_eq!(view["certificate"]["validUntil"], rfc3339(now + 7 * DAY)); + assert_eq!(view["certificate"]["validTo"], rfc3339(now + 7 * DAY)); Ok(()) } @@ -1666,7 +1666,7 @@ async fn leaf_lifetime_fault_overrides_the_setting() -> anyhow::Result<()> { app.state.lock().await.faults.leaf_lifetime_secs = Some(60); let id = enroll(&app, json!({})).await?; let (_, view) = call(&app, Method::GET, &format!("/{id}"), None).await?; - assert_eq!(view["certificate"]["validUntil"], rfc3339(now + 60)); + assert_eq!(view["certificate"]["validTo"], rfc3339(now + 60)); Ok(()) }