From 2723a89833a603475eef5116c1da0b000a1c9b37 Mon Sep 17 00:00:00 2001 From: David Bond Date: Mon, 27 Jul 2026 12:59:17 +0100 Subject: [PATCH 1/2] Add Resource/ResourceGroup rename tests and EntityPropertyWrite for safe hidden-field writes PutAsync already supports Resource and ResourceGroup (both IdentifiedItem + IHasEndpoint), so rename works via a full-object PUT. Add integration tests proving it, including that a uptimepingcheck rename must set BOTH Name and DisplayName (a DisplayName-only change silently no-ops on those devices) while the ping target Host is preserved. Because LogicMonitor masks secret custom properties (snmp.community, *.pass, *.key) as ******** on read, round-tripping a whole object would write the mask back and clobber the real value. Add EntityPropertyWrite (the admin-gated, config-as-code shape { type, id, name, value }) plus SetCustomPropertyAsync / SetCustomPropertiesAsync, which write one field at a time and never send the mask. Portal-free unit tests cover the config deserialisation and endpoint mapping; an integration test writes snmp.community via EntityPropertyWrite. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Resources/EntityPropertyWriteTests.cs | 80 +++++++ .../Resources/ResourceRenameTests.cs | 200 ++++++++++++++++++ .../LogicMonitorClient_Resources.cs | 48 +++++ .../Resources/EntityPropertyWrite.cs | 65 ++++++ .../EntityPropertyWriteTargetType.cs | 27 +++ 5 files changed, 420 insertions(+) create mode 100644 LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs create mode 100644 LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs create mode 100644 LogicMonitor.Api/Resources/EntityPropertyWrite.cs create mode 100644 LogicMonitor.Api/Resources/EntityPropertyWriteTargetType.cs diff --git a/LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs b/LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs new file mode 100644 index 00000000..05c11b86 --- /dev/null +++ b/LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json; + +namespace LogicMonitor.Api.Test.Resources; + +/// +/// Portal-free tests for - the config-as-code shape used to write +/// hidden (masked) property fields such as snmp.community / *.pass / *.key onto resources and groups. +/// No network or credentials required. +/// +public class EntityPropertyWriteTests +{ + [Fact] + public void HiddenFieldWriteConfig_Deserializes() + { + // The exact config shape a caller supplies to write hidden fields. + const string json = """ + [ + { "type": "resourceGroup", "id": 1234, "name": "snmp.community", "value": "public" } + ] + """; + + var writes = JsonConvert.DeserializeObject>(json); + + writes.Should().NotBeNull(); + writes!.Should().HaveCount(1); + + var write = writes[0]; + write.Type.Should().Be(EntityPropertyWriteTargetType.ResourceGroup); + write.Id.Should().Be(1234); + write.Name.Should().Be("snmp.community"); + write.Value.Should().Be("public"); + + // The write targets the group's own properties collection (one field), never a full object PUT, + // so a masked ******** value can never be sent back and clobber the real secret. + write.PropertiesSubUrl().Should().Be("device/groups/1234/properties"); + } + + [Fact] + public void ResourceTarget_MapsToDevicePropertiesEndpoint() + { + var write = new EntityPropertyWrite + { + Type = EntityPropertyWriteTargetType.Resource, + Id = 42, + Name = "esx.pass", + Value = "s3cret" + }; + + write.PropertiesSubUrl().Should().Be("device/devices/42/properties"); + } + + [Fact] + public void RoundTrips_WithCamelCaseTargetType() + { + var write = new EntityPropertyWrite + { + Type = EntityPropertyWriteTargetType.ResourceGroup, + Id = 1234, + Name = "snmp.community", + Value = "public" + }; + + var json = JObject.Parse(JsonConvert.SerializeObject(write)); + + json["type"]!.Value().Should().Be("resourceGroup"); + json["id"]!.Value().Should().Be(1234); + json["name"]!.Value().Should().Be("snmp.community"); + json["value"]!.Value().Should().Be("public"); + } + + [Fact] + public void UnknownTarget_Throws() + { + var write = new EntityPropertyWrite { Type = EntityPropertyWriteTargetType.Unknown, Id = 1, Name = "x" }; + + var act = write.PropertiesSubUrl; + + act.Should().Throw(); + } +} diff --git a/LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs b/LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs new file mode 100644 index 00000000..d97b489e --- /dev/null +++ b/LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs @@ -0,0 +1,200 @@ +namespace LogicMonitor.Api.Test.Resources; + +/// +/// Integration tests (live portal) proving: +/// 1. A can be renamed via a full-object . +/// 2. An Uptime ping-check can be renamed via PutAsync - and that BOTH Name and +/// DisplayName must be set (a DisplayName-only change silently no-ops on uptimepingcheck devices), while +/// the ping target (Host) is preserved. +/// 3. A hidden/secret custom property (snmp.community) can be written safely via +/// +/// (one field at a time) - the admin-gated, clobber-free alternative to round-tripping a whole object +/// whose secret fields come back masked as ********. +/// +public class ResourceRenameTests(ITestOutputHelper iTestOutputHelper, Fixture fixture) + : TestWithOutput(iTestOutputHelper, fixture), IClassFixture +{ + private const string TargetHost = "8.8.8.8"; + + [Fact] + public async Task PutAsync_RenamesResourceGroup() + { + const string originalName = "IntegrationTest-Rename-Group-Before"; + const string renamedName = "IntegrationTest-Rename-Group-After"; + + await DeleteResourceGroupIfExistsAsync(originalName); + await DeleteResourceGroupIfExistsAsync(renamedName); + + var group = await LogicMonitorClient.CreateAsync( + new ResourceGroupCreationDto { ParentId = "1", Name = originalName }, + CancellationToken); + + try + { + // Fetch the full object, change the Name, and PUT it back. + var fetched = await LogicMonitorClient.GetAsync(group.Id, CancellationToken); + fetched.Name = renamedName; + await LogicMonitorClient.PutAsync(fetched, CancellationToken); + + var reloaded = await LogicMonitorClient.GetAsync(group.Id, CancellationToken); + reloaded.Name.Should().Be(renamedName); + } + finally + { + await LogicMonitorClient.DeleteAsync(group, true, CancellationToken); + } + } + + [Fact] + public async Task PutAsync_RenamesUptimePingCheckResource_SettingNameAndDisplayName() + { + const string originalName = "IntegrationTest-Rename-Ping-Before"; + const string renamedName = "IntegrationTest-Rename-Ping-After"; + + await DeleteResourceIfExistsAsync(originalName); + await DeleteResourceIfExistsAsync(renamedName); + + var collectorId = await GetLiveCollectorIdAsync(); + + PingCheckResource resource; + try + { + resource = await LogicMonitorClient.CreateAsync( + new PingCheckResourceCreationDto + { + Name = originalName, + DisplayName = originalName, + Description = "Integration test rename ping check", + ResourceGroupIds = "1", + PreferredCollectorId = collectorId, + DisableAlerting = true, + IsInternal = true, + HostName = TargetHost, + PollingIntervalMinutes = 5, + PacketCount = 5, + TimeoutMs = 500, + PercentPacketsNotReceivedInTime = 80, + SyntheticsCollectorIds = [collectorId], + TestLocation = new UptimeTestLocation { All = true, CollectorIds = [collectorId], SmgIds = [] }, + Alerting = new UptimeAlertSettings + { + OverallAlertLevel = Level.Critical, + IndividualAlertLevel = Level.Warning, + IndividualCheckpointAlertsEnabled = true, + FailedCheckCountBeforeAlerting = 1, + AlertCondition = SiteMonitorAlertCondition.AllLocations + } + }, + CancellationToken); + } + catch (LogicMonitorApiException ex) when (IsUptimeUnavailable(ex)) + { + Assert.Skip($"Portal does not have LM Uptime enabled: {ex.Message}"); + return; + } + + try + { + var fetched = await LogicMonitorClient.GetAsync(resource.Id, CancellationToken); + var originalHost = fetched.HostName; + + // A rename on a uptimepingcheck device must set BOTH Name and DisplayName; a DisplayName-only + // change silently no-ops on these devices. For these devices Name is a label, not the ping + // target (that is HostName), so changing Name is safe. + fetched.Name = renamedName; + fetched.DisplayName = renamedName; + await LogicMonitorClient.PutAsync(fetched, CancellationToken); + + var reloaded = await LogicMonitorClient.GetAsync(resource.Id, CancellationToken); + reloaded.Name.Should().Be(renamedName); + reloaded.DisplayName.Should().Be(renamedName); + reloaded.HostName.Should().Be(originalHost, "the ping target must be preserved across a rename"); + reloaded.ResourceType.Should().Be(ResourceType.Ping); + } + finally + { + if (resource.Id > 0) + { + await LogicMonitorClient.DeleteAsync(resource, cancellationToken: CancellationToken); + } + } + } + + [Fact] + public async Task SetCustomPropertyAsync_WritesHiddenField_ViaEntityPropertyWrite() + { + const string groupName = "IntegrationTest-HiddenField-Group"; + const string secretPropertyName = "snmp.community"; + + await DeleteResourceGroupIfExistsAsync(groupName); + + var group = await LogicMonitorClient.CreateAsync( + new ResourceGroupCreationDto { ParentId = "1", Name = groupName }, + CancellationToken); + + try + { + // Write the hidden field one property at a time - never a full-object PUT - so the masked + // ******** value is never sent and the real value cannot be clobbered. This is the admin-gated + // config-as-code shape: [ { "type": "resourceGroup", "id": , "name": "snmp.community", "value": "public" } ] + var write = new EntityPropertyWrite + { + Type = EntityPropertyWriteTargetType.ResourceGroup, + Id = group.Id, + Name = secretPropertyName, + Value = "public" + }; + await LogicMonitorClient.SetCustomPropertyAsync(write, SetPropertyMode.Create, CancellationToken); + + var properties = await LogicMonitorClient.GetResourceGroupPropertiesAsync(group.Id, CancellationToken); + properties.Should().ContainSingle(p => p.Name == secretPropertyName, + "the hidden field must be written (its value is masked as ******** by LogicMonitor on read)"); + + // The batch entry point applies the same write as a config list. + await LogicMonitorClient.SetCustomPropertiesAsync([write], SetPropertyMode.Update, CancellationToken); + + properties = await LogicMonitorClient.GetResourceGroupPropertiesAsync(group.Id, CancellationToken); + properties.Should().ContainSingle(p => p.Name == secretPropertyName); + } + finally + { + await LogicMonitorClient.DeleteAsync(group, true, CancellationToken); + } + } + + /// + /// True when a creation error indicates the portal cannot host LM Uptime checks (feature disabled or the + /// Ping_Check LogicModules are not imported) - in which case the test should be skipped, not failed. + /// + private static bool IsUptimeUnavailable(LogicMonitorApiException ex) + => ex.Message.Contains("Uptime feature is not enabled", StringComparison.OrdinalIgnoreCase) + || ex.Message.Contains("datasources not found", StringComparison.OrdinalIgnoreCase); + + private async Task GetLiveCollectorIdAsync() + { + var collectors = await LogicMonitorClient.GetAllAsync(CancellationToken); + var collector = collectors.FirstOrDefault(c => c.Id == CollectorId) + ?? collectors.FirstOrDefault(c => !c.IsDown) + ?? collectors.FirstOrDefault(); + collector.Should().NotBeNull("the portal must have at least one Collector for an internal Uptime check"); + return collector!.Id; + } + + private async Task DeleteResourceGroupIfExistsAsync(string fullPath) + { + var existing = await LogicMonitorClient.GetResourceGroupByFullPathAsync(fullPath, CancellationToken); + if (existing is not null) + { + await LogicMonitorClient.DeleteAsync(existing, true, CancellationToken); + } + } + + private async Task DeleteResourceIfExistsAsync(string displayName) + { + var existing = await LogicMonitorClient.GetResourceByDisplayNameAsync(displayName, CancellationToken); + if (existing is not null) + { + await LogicMonitorClient.DeleteAsync(existing, cancellationToken: CancellationToken); + } + } +} diff --git a/LogicMonitor.Api/LogicMonitorClient_Resources.cs b/LogicMonitor.Api/LogicMonitorClient_Resources.cs index 21d2aa87..734804e4 100644 --- a/LogicMonitor.Api/LogicMonitorClient_Resources.cs +++ b/LogicMonitor.Api/LogicMonitorClient_Resources.cs @@ -205,6 +205,54 @@ public Task SetResourceGroupCustomPropertyAsync( "device/groups", cancellationToken); + /// + /// Applies a single , writing one custom property onto the + /// target or . + /// + /// + /// This writes only the named property (never a full object), so masked ******** secret + /// values are never sent back and cannot be clobbered. It is the safe, admin-gated way to set + /// hidden fields such as snmp.community, *.pass and *.key. + /// + /// The property write directive. + /// How to set the property (Create, Update, Delete or Automatic). + /// The cancellation token. + /// Thrown when the write targets an unsupported entity type. + public Task SetCustomPropertyAsync( + EntityPropertyWrite write, + SetPropertyMode mode, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(write); + return write.Type switch + { + EntityPropertyWriteTargetType.Resource + => SetResourceCustomPropertyAsync(write.Id, write.Name, write.Value, mode, cancellationToken), + EntityPropertyWriteTargetType.ResourceGroup + => SetResourceGroupCustomPropertyAsync(write.Id, write.Name, write.Value, mode, cancellationToken), + _ => throw new NotSupportedException($"Unsupported {nameof(EntityPropertyWriteTargetType)}: {write.Type}") + }; + } + + /// + /// Applies a batch of directives in order. This is the + /// config-as-code entry point for setting hidden property fields across resources and groups. + /// + /// The property write directives to apply, in order. + /// How to set each property (Create, Update, Delete or Automatic). + /// The cancellation token. + public async Task SetCustomPropertiesAsync( + IEnumerable writes, + SetPropertyMode mode, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(writes); + foreach (var write in writes) + { + await SetCustomPropertyAsync(write, mode, cancellationToken).ConfigureAwait(false); + } + } + /// /// Gets Resources /// diff --git a/LogicMonitor.Api/Resources/EntityPropertyWrite.cs b/LogicMonitor.Api/Resources/EntityPropertyWrite.cs new file mode 100644 index 00000000..d37c4b2a --- /dev/null +++ b/LogicMonitor.Api/Resources/EntityPropertyWrite.cs @@ -0,0 +1,65 @@ +namespace LogicMonitor.Api.Resources; + +/// +/// A single directive to write one custom property onto a or +/// , identified by type and id. +/// +/// +/// +/// This is the config-as-code shape for setting hidden property fields (e.g. +/// snmp.community, *.pass, *.key). LogicMonitor masks such values as +/// ******** on GET, so a full object round-trip (GET then +/// ) would +/// write the mask back and clobber the real value. Writing the property directly, one field at a +/// time, avoids that: only the named property is changed and the mask is never sent. +/// +/// +/// Applying these writes requires LogicMonitor administrator rights, so the operation is safe by +/// virtue of that gate. A list of these objects deserialises directly from JSON such as: +/// +/// [ { "type": "resourceGroup", "id": 1234, "name": "snmp.community", "value": "public" } ] +/// +/// +/// +[DataContract] +public class EntityPropertyWrite +{ + /// + /// The type of entity to write the property onto. + /// + [DataMember(Name = "type")] + public EntityPropertyWriteTargetType Type { get; set; } + + /// + /// The id of the target or . + /// + [DataMember(Name = "id")] + public int Id { get; set; } + + /// + /// The custom property name to write (e.g. snmp.community). + /// + [DataMember(Name = "name")] + public string Name { get; set; } = string.Empty; + + /// + /// The custom property value to write. If null and the property exists, it is removed. + /// + [DataMember(Name = "value")] + public string? Value { get; set; } + + /// + /// The properties sub-URL that this write targets, e.g. + /// device/groups/1234/properties or device/devices/1234/properties. + /// + /// Thrown when is not a supported target. + public string PropertiesSubUrl() => Type switch + { + EntityPropertyWriteTargetType.Resource => $"device/devices/{Id}/properties", + EntityPropertyWriteTargetType.ResourceGroup => $"device/groups/{Id}/properties", + _ => throw new NotSupportedException($"Unsupported {nameof(EntityPropertyWriteTargetType)}: {Type}") + }; + + /// + public override string ToString() => $"{Type} {Id}: {Name}={Value}"; +} diff --git a/LogicMonitor.Api/Resources/EntityPropertyWriteTargetType.cs b/LogicMonitor.Api/Resources/EntityPropertyWriteTargetType.cs new file mode 100644 index 00000000..dfbd0cf7 --- /dev/null +++ b/LogicMonitor.Api/Resources/EntityPropertyWriteTargetType.cs @@ -0,0 +1,27 @@ +namespace LogicMonitor.Api.Resources; + +/// +/// The type of entity that an targets. +/// +[DataContract] +[JsonConverter(typeof(StringEnumConverter))] +public enum EntityPropertyWriteTargetType +{ + /// + /// Unknown / unset. + /// + [EnumMember(Value = "unknown")] + Unknown = 0, + + /// + /// A (device). + /// + [EnumMember(Value = "resource")] + Resource, + + /// + /// A (device group). + /// + [EnumMember(Value = "resourceGroup")] + ResourceGroup +} From 7800d74dabeb05f24a6eac1c62bdda5f284df15e Mon Sep 17 00:00:00 2001 From: David Bond Date: Mon, 27 Jul 2026 14:00:08 +0100 Subject: [PATCH 2/2] Correct masked-secret framing: empirically, full PUT does NOT clobber Resource/ResourceGroup secrets Ran a live experiment on the panoramicdata portal: set a hidden custom property (test.pass), did a full-object GET->PUT round-trip (sending back the masked ********), then read the true stored value via a reveal PropertySource (with a fresh-run marker to rule out staleness). The value was preserved - LogicMonitor treats the ******** mask as "unchanged" on write. So PutAsync-based rename is secret-safe; the earlier "PUT clobbers" rationale was wrong for device/group properties (it originated from a NetScan credentials incident, a different path). EntityPropertyWrite remains the admin-gated, config-as-code way to set/rotate hidden fields (you supply the real value). Docs/comments corrected accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Resources/EntityPropertyWriteTests.cs | 4 ++-- .../Resources/ResourceRenameTests.cs | 18 ++++++++++----- .../LogicMonitorClient_Resources.cs | 8 ++++--- .../Resources/EntityPropertyWrite.cs | 22 ++++++++++++------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs b/LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs index 05c11b86..7a6fc44e 100644 --- a/LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs +++ b/LogicMonitor.Api.Test/Resources/EntityPropertyWriteTests.cs @@ -30,8 +30,8 @@ public void HiddenFieldWriteConfig_Deserializes() write.Name.Should().Be("snmp.community"); write.Value.Should().Be("public"); - // The write targets the group's own properties collection (one field), never a full object PUT, - // so a masked ******** value can never be sent back and clobber the real secret. + // The write targets the group's own properties collection (one field), so the caller always + // supplies the real value rather than round-tripping a masked ********. write.PropertiesSubUrl().Should().Be("device/groups/1234/properties"); } diff --git a/LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs b/LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs index d97b489e..c4a42b8a 100644 --- a/LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs +++ b/LogicMonitor.Api.Test/Resources/ResourceRenameTests.cs @@ -6,10 +6,16 @@ namespace LogicMonitor.Api.Test.Resources; /// 2. An Uptime ping-check can be renamed via PutAsync - and that BOTH Name and /// DisplayName must be set (a DisplayName-only change silently no-ops on uptimepingcheck devices), while /// the ping target (Host) is preserved. -/// 3. A hidden/secret custom property (snmp.community) can be written safely via +/// 3. A hidden/secret custom property (snmp.community) can be set/rotated via /// -/// (one field at a time) - the admin-gated, clobber-free alternative to round-tripping a whole object -/// whose secret fields come back masked as ********. +/// (one field at a time, supplying the real value) - the admin-gated, config-as-code way to write +/// hidden fields. +/// +/// Note on the original masked-secret concern: a full-object PUT round-trip does NOT clobber a +/// Resource/ResourceGroup secret - LogicMonitor treats the ******** mask as "unchanged" on write. This +/// was verified empirically on a live portal (set test.pass, GET->PUT the whole object, then read the +/// real stored value back via a reveal PropertySource that echoes nominated hidden props, with a +/// fresh-run marker to defeat staleness; the value survived). So PutAsync-based rename is secret-safe. /// public class ResourceRenameTests(ITestOutputHelper iTestOutputHelper, Fixture fixture) : TestWithOutput(iTestOutputHelper, fixture), IClassFixture @@ -134,9 +140,9 @@ public async Task SetCustomPropertyAsync_WritesHiddenField_ViaEntityPropertyWrit try { - // Write the hidden field one property at a time - never a full-object PUT - so the masked - // ******** value is never sent and the real value cannot be clobbered. This is the admin-gated - // config-as-code shape: [ { "type": "resourceGroup", "id": , "name": "snmp.community", "value": "public" } ] + // Write the hidden field one property at a time, supplying the real value (never round-tripping + // a masked ********). This is the admin-gated config-as-code shape used to set/rotate hidden + // fields: [ { "type": "resourceGroup", "id": , "name": "snmp.community", "value": "public" } ] var write = new EntityPropertyWrite { Type = EntityPropertyWriteTargetType.ResourceGroup, diff --git a/LogicMonitor.Api/LogicMonitorClient_Resources.cs b/LogicMonitor.Api/LogicMonitorClient_Resources.cs index 734804e4..f9c9c715 100644 --- a/LogicMonitor.Api/LogicMonitorClient_Resources.cs +++ b/LogicMonitor.Api/LogicMonitorClient_Resources.cs @@ -210,9 +210,11 @@ public Task SetResourceGroupCustomPropertyAsync( /// target or . /// /// - /// This writes only the named property (never a full object), so masked ******** secret - /// values are never sent back and cannot be clobbered. It is the safe, admin-gated way to set - /// hidden fields such as snmp.community, *.pass and *.key. + /// This writes only the named property (never a full object), so you always supply the real value + /// rather than round-tripping a masked ********. It is the admin-gated way to set or rotate + /// hidden fields such as snmp.community, *.pass and *.key. (For reference: a + /// full-object PUT does not clobber a Resource/ResourceGroup secret either - the server treats the + /// ******** mask as "unchanged" - but this method is how you change the stored value.) /// /// The property write directive. /// How to set the property (Create, Update, Delete or Automatic). diff --git a/LogicMonitor.Api/Resources/EntityPropertyWrite.cs b/LogicMonitor.Api/Resources/EntityPropertyWrite.cs index d37c4b2a..1a4754ad 100644 --- a/LogicMonitor.Api/Resources/EntityPropertyWrite.cs +++ b/LogicMonitor.Api/Resources/EntityPropertyWrite.cs @@ -6,16 +6,22 @@ namespace LogicMonitor.Api.Resources; /// /// /// -/// This is the config-as-code shape for setting hidden property fields (e.g. -/// snmp.community, *.pass, *.key). LogicMonitor masks such values as -/// ******** on GET, so a full object round-trip (GET then -/// ) would -/// write the mask back and clobber the real value. Writing the property directly, one field at a -/// time, avoids that: only the named property is changed and the mask is never sent. +/// This is the config-as-code shape for setting or rotating hidden property fields (e.g. +/// snmp.community, *.pass, *.key) to a new value. It writes one named property +/// at a time, so a masked ******** value is never sent - you always supply the real value. /// /// -/// Applying these writes requires LogicMonitor administrator rights, so the operation is safe by -/// virtue of that gate. A list of these objects deserialises directly from JSON such as: +/// Note on the masked-secret concern: LogicMonitor masks these values as ******** on GET, but +/// for a / the server treats that mask as +/// "leave unchanged" on write, so a full-object round-trip (GET then +/// , e.g. for a +/// rename) does not clobber the real secret. This was verified empirically. Use +/// when you actually want to change the stored value, not merely +/// preserve it across an unrelated update. +/// +/// +/// Applying these writes requires LogicMonitor administrator rights. A list of these objects +/// deserialises directly from JSON such as: /// /// [ { "type": "resourceGroup", "id": 1234, "name": "snmp.community", "value": "public" } ] ///