Skip to content

Commit 504b100

Browse files
amandli-kssk-keeper
authored andcommitted
Added fallback method for out-of-sync response
1 parent f7dbfa4 commit 504b100

3 files changed

Lines changed: 269 additions & 32 deletions

File tree

KeeperSdk/vault/KeeperNSFFunctions.cs

Lines changed: 112 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,18 @@ public static async Task UpdateKeeperNSFRecordInternal(this VaultOnline vault, s
470470
if (!vault.TryGetKeeperNSFRecord(recordUid, out var record))
471471
throw new VaultException($"Keeper NSF record '{recordUid}' not found");
472472

473+
await vault.UpdateKeeperNSFRecordInternal(record, title, recordType, notes, fields).ConfigureAwait(false);
474+
}
475+
476+
public static async Task UpdateKeeperNSFRecordInternal(this VaultOnline vault, KeeperNSFRecord record, string title, string recordType, string notes, IDictionary<string, object> fields)
477+
{
478+
if (record == null)
479+
throw new ArgumentNullException(nameof(record));
480+
481+
var recordUid = record.RecordUid;
482+
if (string.IsNullOrEmpty(recordUid))
483+
throw new VaultException("Record UID cannot be empty");
484+
473485
if (record.RecordKey == null)
474486
throw new VaultException($"Record key not available for record '{recordUid}'");
475487

@@ -529,7 +541,13 @@ public static async Task UpdateKeeperNSFRecordInternal(this VaultOnline vault, s
529541
var result = rs.Records[0];
530542
if (result.Status != RecordModifyResult.RsSuccess)
531543
{
532-
throw new VaultException($"Failed to update record: {result.Message}");
544+
var status = Enum.GetName(typeof(RecordModifyResult), result.Status) ?? "";
545+
if (status.StartsWith("Rs", StringComparison.Ordinal))
546+
{
547+
status = status.Substring(2);
548+
}
549+
550+
throw new KeeperApiException(status.ToSnakeCase(), result.Message ?? "Failed to update record");
533551
}
534552
}
535553
}
@@ -1062,35 +1080,7 @@ private static string GetFolderName(VaultOnline vault, string folderUid)
10621080
public static async Task<KeeperNSFRecordDetailsResult> GetKeeperNSFRecordDetailsInternal(
10631081
this VaultOnline vault, IReadOnlyList<string> recordUids)
10641082
{
1065-
if (recordUids == null || recordUids.Count == 0)
1066-
{
1067-
throw new KeeperInvalidParameter("GetKeeperNSFRecordDetails", nameof(recordUids), "", "at least one record UID required");
1068-
}
1069-
1070-
var request = new RecordDetailsProto.RecordDataRequest
1071-
{
1072-
ClientTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
1073-
};
1074-
1075-
foreach (var uid in recordUids.Where(u => !string.IsNullOrWhiteSpace(u)))
1076-
{
1077-
var uidBytes = uid.Trim().Base64UrlDecode();
1078-
if (uidBytes == null || uidBytes.Length == 0)
1079-
{
1080-
Trace.TraceWarning($"KeeperNSF: Skipping record details request with malformed UID '{uid}'");
1081-
continue;
1082-
}
1083-
1084-
request.RecordUids.Add(ByteString.CopyFrom(uidBytes));
1085-
}
1086-
1087-
if (request.RecordUids.Count == 0)
1088-
{
1089-
throw new KeeperInvalidParameter("GetKeeperNSFRecordDetails", nameof(recordUids), "", "no valid record UIDs");
1090-
}
1091-
1092-
var response = await vault.Auth.ExecuteAuthRest<RecordDetailsProto.RecordDataRequest, RecordDetailsProto.RecordDataResponse>(
1093-
"vault/records/v3/details/data", request).ConfigureAwait(false);
1083+
var response = await vault.FetchKeeperNSFRecordDetailsDataAsync(recordUids).ConfigureAwait(false);
10941084

10951085
var result = new KeeperNSFRecordDetailsResult();
10961086
foreach (var forbiddenUid in response.ForbiddenRecords)
@@ -1129,6 +1119,98 @@ public static async Task<KeeperNSFRecordDetailsResult> GetKeeperNSFRecordDetails
11291119
return result;
11301120
}
11311121

1122+
private static async Task<RecordDetailsProto.RecordDataResponse> FetchKeeperNSFRecordDetailsDataAsync(
1123+
this VaultOnline vault, IReadOnlyList<string> recordUids)
1124+
{
1125+
if (recordUids == null || recordUids.Count == 0)
1126+
{
1127+
throw new KeeperInvalidParameter("GetKeeperNSFRecordDetails", nameof(recordUids), "", "at least one record UID required");
1128+
}
1129+
1130+
var request = new RecordDetailsProto.RecordDataRequest
1131+
{
1132+
ClientTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
1133+
};
1134+
1135+
foreach (var uid in recordUids.Where(u => !string.IsNullOrWhiteSpace(u)))
1136+
{
1137+
var uidBytes = uid.Trim().Base64UrlDecode();
1138+
if (uidBytes == null || uidBytes.Length == 0)
1139+
{
1140+
Trace.TraceWarning($"KeeperNSF: Skipping record details request with malformed UID '{uid}'");
1141+
continue;
1142+
}
1143+
1144+
request.RecordUids.Add(ByteString.CopyFrom(uidBytes));
1145+
}
1146+
1147+
if (request.RecordUids.Count == 0)
1148+
{
1149+
throw new KeeperInvalidParameter("GetKeeperNSFRecordDetails", nameof(recordUids), "", "no valid record UIDs");
1150+
}
1151+
1152+
return await vault.Auth.ExecuteAuthRest<RecordDetailsProto.RecordDataRequest, RecordDetailsProto.RecordDataResponse>(
1153+
"vault/records/v3/details/data", request).ConfigureAwait(false);
1154+
}
1155+
1156+
internal static async Task<KeeperNSFRecord> GetRefreshedKeeperNSFRecordAsync(this VaultOnline vault, string recordUid)
1157+
{
1158+
if (string.IsNullOrWhiteSpace(recordUid))
1159+
return null;
1160+
1161+
var trimmedUid = recordUid.Trim();
1162+
var response = await vault.FetchKeeperNSFRecordDetailsDataAsync(new[] { trimmedUid }).ConfigureAwait(false);
1163+
1164+
var recordData = (response.Data ?? Enumerable.Empty<Records.RecordData>())
1165+
.FirstOrDefault(x =>
1166+
{
1167+
if (x.RecordUid == null || x.RecordUid.IsEmpty)
1168+
return false;
1169+
var uid = CryptoUtils.Base64UrlEncode(x.RecordUid.ToByteArray());
1170+
return string.Equals(uid, trimmedUid, StringComparison.OrdinalIgnoreCase);
1171+
});
1172+
1173+
return recordData != null && TryBuildKeeperNSFRecordFromDetailsData(vault, recordData, trimmedUid, out var record)
1174+
? record
1175+
: null;
1176+
}
1177+
1178+
private static bool TryBuildKeeperNSFRecordFromDetailsData(
1179+
VaultOnline vault, Records.RecordData recordData, string recordUid, out KeeperNSFRecord record)
1180+
{
1181+
record = null;
1182+
if (recordData == null || string.IsNullOrWhiteSpace(recordUid))
1183+
return false;
1184+
1185+
if (!TryDecryptKeeperNSFRecordDetailsData(vault, recordData, recordUid, out var data))
1186+
return false;
1187+
1188+
var recordKey = TryDecryptKeeperNSFRecordKeyFromDetails(vault, recordData, recordUid);
1189+
if (recordKey == null || recordKey.Length == 0)
1190+
return false;
1191+
1192+
vault.TryGetKeeperNSFRecord(recordUid, out var cached);
1193+
1194+
record = new KeeperNSFRecord
1195+
{
1196+
RecordUid = recordUid,
1197+
Title = !string.IsNullOrEmpty(data?.Title) ? data.Title : data?.Name,
1198+
Type = data?.Type,
1199+
Notes = data?.Notes,
1200+
Revision = recordData.Revision,
1201+
Version = recordData.Version,
1202+
Shared = cached?.Shared ?? false,
1203+
ClientModifiedTime = cached?.ClientModifiedTime ?? 0,
1204+
FileSize = cached?.FileSize ?? 0,
1205+
ThumbnailSize = cached?.ThumbnailSize ?? 0,
1206+
FolderUid = cached?.FolderUid,
1207+
FolderName = cached?.FolderName,
1208+
RecordKey = recordKey,
1209+
Data = data,
1210+
};
1211+
return true;
1212+
}
1213+
11321214
private static void ResolveKeeperNSFRecordTitleAndType(KeeperNSFRecord record, out string title, out string type)
11331215
{
11341216
if (!string.IsNullOrEmpty(record?.Type))

KeeperSdk/vault/VaultOnline.cs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,63 @@ public Task<KeeperRecord> CreateRecord(KeeperRecord record, string folderUid = n
186186
return this.AddRecordToFolder(record, folderUid);
187187
}
188188

189+
/// <inheritdoc/>
190+
public async Task<TryUpdateRecordResult> TryUpdateRecord(KeeperRecord record, bool skipExtra = true)
191+
{
192+
try
193+
{
194+
var recordUpdated = await this.PutRecord(record, skipExtra).ConfigureAwait(false);
195+
return new TryUpdateRecordResult(true, recordUpdated, null);
196+
}
197+
catch (Exception e)
198+
{
199+
return new TryUpdateRecordResult(false, null, FormatUpdateErrorReason(e));
200+
}
201+
}
202+
189203
/// <inheritdoc/>
190204
public Task<KeeperRecord> UpdateRecord(KeeperRecord record, bool skipExtra = true)
191205
{
192-
return this.PutRecord(record, skipExtra);
206+
return UpdateRecordAsync(record, skipExtra);
207+
}
208+
209+
private async Task<KeeperRecord> UpdateRecordAsync(KeeperRecord record, bool skipExtra)
210+
{
211+
try
212+
{
213+
return await this.PutRecord(record, skipExtra).ConfigureAwait(false);
214+
}
215+
catch (KeeperApiException e) when (IsRecordOutOfSync(e))
216+
{
217+
if (string.IsNullOrEmpty(record?.Uid))
218+
{
219+
throw;
220+
}
221+
222+
var refreshed = await RecordSkipSyncDown
223+
.GetOwnedRecordsAsync(Auth, new[] { record.Uid })
224+
.ConfigureAwait(false);
225+
if (refreshed.Records.Count == 0)
226+
{
227+
throw;
228+
}
229+
230+
var refreshedRecord = refreshed.Records[0];
231+
return await this.PutRecord(refreshedRecord, skipExtra).ConfigureAwait(false);
232+
}
233+
}
234+
235+
private static bool IsRecordOutOfSync(KeeperApiException e)
236+
{
237+
return string.Equals(e.Code, "out_of_sync", StringComparison.OrdinalIgnoreCase)
238+
|| string.Equals(e.Code, "RS_OUT_OF_SYNC", StringComparison.OrdinalIgnoreCase);
239+
}
240+
241+
private static string FormatUpdateErrorReason(Exception e)
242+
{
243+
return e is KeeperApiException kae && !string.IsNullOrEmpty(kae.Code)
244+
? $"{kae.Code}: {kae.Message}"
245+
: e.Message;
193246
}
194247

195248
/// <inheritdoc/>
@@ -363,13 +416,50 @@ public async Task<string> CreateKeeperNSFRecord(string title, string recordType
363416
return recordUid;
364417
}
365418

419+
/// <inheritdoc/>
420+
public async Task<TryUpdateKeeperNSFRecordResult> TryUpdateKeeperNSFRecord(string recordUid, string title = null, string recordType = null, string notes = null, IDictionary<string, object> fields = null)
421+
{
422+
try
423+
{
424+
await this.UpdateKeeperNSFRecordInternal(recordUid, title, recordType, notes, fields).ConfigureAwait(false);
425+
return new TryUpdateKeeperNSFRecordResult(true, recordUid, null);
426+
}
427+
catch (Exception e)
428+
{
429+
return new TryUpdateKeeperNSFRecordResult(false, null, FormatUpdateErrorReason(e));
430+
}
431+
}
432+
366433
/// <inheritdoc/>
367434
public async Task UpdateKeeperNSFRecord(string recordUid, string title = null, string recordType = null, string notes = null, IDictionary<string, object> fields = null)
368435
{
369-
await this.UpdateKeeperNSFRecordInternal(recordUid, title, recordType, notes, fields);
436+
await UpdateKeeperNSFRecordAsync(recordUid, title, recordType, notes, fields).ConfigureAwait(false);
370437
await ScheduleSyncDown(TimeSpan.FromMilliseconds(100));
371438
}
372439

440+
private async Task UpdateKeeperNSFRecordAsync(string recordUid, string title, string recordType, string notes, IDictionary<string, object> fields)
441+
{
442+
try
443+
{
444+
await this.UpdateKeeperNSFRecordInternal(recordUid, title, recordType, notes, fields).ConfigureAwait(false);
445+
}
446+
catch (KeeperApiException e) when (IsRecordOutOfSync(e))
447+
{
448+
if (string.IsNullOrEmpty(recordUid))
449+
{
450+
throw;
451+
}
452+
453+
var refreshedRecord = await this.GetRefreshedKeeperNSFRecordAsync(recordUid).ConfigureAwait(false);
454+
if (refreshedRecord == null)
455+
{
456+
throw;
457+
}
458+
459+
await this.UpdateKeeperNSFRecordInternal(refreshedRecord, title, recordType, notes, fields).ConfigureAwait(false);
460+
}
461+
}
462+
373463
/// <inheritdoc/>
374464
public async Task ShareKeeperNSFRecord(string recordUid, string userEmail, string role = "viewer")
375465
{

KeeperSdk/vault/VaultTypes.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,14 @@ public interface IVault : IVaultData
384384
/// <exception cref="Authentication.KeeperApiException"></exception>
385385
Task<KeeperRecord> UpdateRecord(KeeperRecord record, bool skipExtra = true);
386386

387+
/// <summary>
388+
/// Attempts to modify a password record without throwing.
389+
/// </summary>
390+
/// <param name="record">Keeper Record.</param>
391+
/// <param name="skipExtra">Do not update file attachment information on the record.</param>
392+
/// <returns>A task returning success with the updated record, or failure with an error reason.</returns>
393+
Task<TryUpdateRecordResult> TryUpdateRecord(KeeperRecord record, bool skipExtra = true);
394+
387395
/// <summary>
388396
/// Modifies multiple password records.
389397
/// </summary>
@@ -519,6 +527,17 @@ Task<FolderNode> CreateFolder(string name, string parentFolderUid = null,
519527
/// <param name="fields">Fields to add or update. Values may be strings (plain scalars) or complex objects (e.g. host = { hostName, port } on a databaseCredentials record).</param>
520528
Task UpdateKeeperNSFRecord(string recordUid, string title = null, string recordType = null, string notes = null, IDictionary<string, object> fields = null);
521529

530+
/// <summary>
531+
/// Attempts to update a Keeper NSF record without throwing.
532+
/// </summary>
533+
/// <param name="recordUid">Record UID to update.</param>
534+
/// <param name="title">New title (null to keep existing).</param>
535+
/// <param name="recordType">New record type (null to keep existing).</param>
536+
/// <param name="notes">New notes (null to keep existing).</param>
537+
/// <param name="fields">Fields to add or update.</param>
538+
/// <returns>A task returning success with the record UID, or failure with an error reason.</returns>
539+
Task<TryUpdateKeeperNSFRecordResult> TryUpdateKeeperNSFRecord(string recordUid, string title = null, string recordType = null, string notes = null, IDictionary<string, object> fields = null);
540+
522541
/// <summary>
523542
/// Grants a user access to a Keeper NSF record by sharing the record key.
524543
/// </summary>
@@ -1148,6 +1167,52 @@ public VaultException(string message) : base(message)
11481167
}
11491168
}
11501169

1170+
/// <summary>
1171+
/// Result of <see cref="IVault.TryUpdateRecord"/>.
1172+
/// </summary>
1173+
public sealed class TryUpdateRecordResult
1174+
{
1175+
/// <exclude/>
1176+
public TryUpdateRecordResult(bool success, KeeperRecord record, string reason)
1177+
{
1178+
Success = success;
1179+
Record = record;
1180+
Reason = reason;
1181+
}
1182+
1183+
/// <summary><c>true</c> when the record was updated successfully.</summary>
1184+
public bool Success { get; }
1185+
1186+
/// <summary>Updated record when <see cref="Success"/> is <c>true</c>.</summary>
1187+
public KeeperRecord Record { get; }
1188+
1189+
/// <summary>Error reason when <see cref="Success"/> is <c>false</c>.</summary>
1190+
public string Reason { get; }
1191+
}
1192+
1193+
/// <summary>
1194+
/// Result of <see cref="IVault.TryUpdateKeeperNSFRecord"/>.
1195+
/// </summary>
1196+
public sealed class TryUpdateKeeperNSFRecordResult
1197+
{
1198+
/// <exclude/>
1199+
public TryUpdateKeeperNSFRecordResult(bool success, string recordUid, string reason)
1200+
{
1201+
Success = success;
1202+
RecordUid = recordUid;
1203+
Reason = reason;
1204+
}
1205+
1206+
/// <summary><c>true</c> when the record was updated successfully.</summary>
1207+
public bool Success { get; }
1208+
1209+
/// <summary>Updated record UID when <see cref="Success"/> is <c>true</c>.</summary>
1210+
public string RecordUid { get; }
1211+
1212+
/// <summary>Error reason when <see cref="Success"/> is <c>false</c>.</summary>
1213+
public string Reason { get; }
1214+
}
1215+
11511216
/// <summary>
11521217
/// Result of <see cref="RecordSkipSyncDown.GetOwnedRecordsAsync"/> or <see cref="RecordSkipSyncDown.GetSharedFolderRecordsAsync"/> (both call <c>vault/get_records_details</c>).
11531218
/// </summary>

0 commit comments

Comments
 (0)