From dd37745736eec42c7e8a110576835d8bee90dff2 Mon Sep 17 00:00:00 2001 From: Justin Baur Date: Tue, 8 Mar 2022 13:22:47 -0500 Subject: [PATCH] Fix OneLogin Import (#1899) * Add PermissiveStringConverter * Formatting * Add value check * Fix PR feedback * Run formatter --- .../Request/OrganizationImportRequestModel.cs | 5 + src/Core/Utilities/JsonHelpers.cs | 71 ++++++++ .../PermissiveStringConverterTests.cs | 159 ++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 test/Core.Test/Utilities/PermissiveStringConverterTests.cs diff --git a/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs b/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs index 285d4475d5..ffdb3ebc25 100644 --- a/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs +++ b/src/Api/Models/Public/Request/OrganizationImportRequestModel.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using Bit.Core.Entities; using Bit.Core.Models.Business; +using Bit.Core.Utilities; namespace Bit.Api.Models.Public.Request { @@ -41,10 +43,12 @@ namespace Bit.Api.Models.Public.Request /// external_id_123456 [Required] [StringLength(300)] + [JsonConverter(typeof(PermissiveStringConverter))] public string ExternalId { get; set; } /// /// The associated external ids for members in this group. /// + [JsonConverter(typeof(PermissiveStringEnumerableConverter))] public IEnumerable MemberExternalIds { get; set; } public ImportedGroup ToImportedGroup(Guid organizationId) @@ -79,6 +83,7 @@ namespace Bit.Api.Models.Public.Request /// external_id_123456 [Required] [StringLength(300)] + [JsonConverter(typeof(PermissiveStringConverter))] public string ExternalId { get; set; } /// /// Determines if this member should be removed from the organization during import. diff --git a/src/Core/Utilities/JsonHelpers.cs b/src/Core/Utilities/JsonHelpers.cs index 6cb54676cd..954a9a14d5 100644 --- a/src/Core/Utilities/JsonHelpers.cs +++ b/src/Core/Utilities/JsonHelpers.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using NS = Newtonsoft.Json; @@ -129,4 +132,72 @@ namespace Bit.Core.Utilities writer.WriteStringValue(CoreHelpers.ToEpocMilliseconds(value.Value).ToString()); } } + + /// + /// Allows reading a string from a JSON number or string, should only be used on properties + /// + public class PermissiveStringConverter : JsonConverter + { + internal static PermissiveStringConverter Instance = new(); + + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetDecimal().ToString(), + JsonTokenType.True => bool.TrueString, + JsonTokenType.False => bool.FalseString, + _ => throw new JsonException($"Unsupported TokenType: {reader.TokenType}"), + }; + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } + + /// + /// Allows reading a JSON array of number or string, should only be used on whose generic type is + /// + public class PermissiveStringEnumerableConverter : JsonConverter> + { + public override IEnumerable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var stringList = new List(); + + // Handle special cases or throw + if (reader.TokenType != JsonTokenType.StartArray) + { + // An array was expected but to be extra permissive allow reading from anything other than an object + if (reader.TokenType == JsonTokenType.StartObject) + { + throw new JsonException("Cannot read JSON Object to an IEnumerable."); + } + + stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options)); + return stringList; + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options)); + } + + return stringList; + } + + public override void Write(Utf8JsonWriter writer, IEnumerable value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (var str in value) + { + PermissiveStringConverter.Instance.Write(writer, str, options); + } + + writer.WriteEndArray(); + } + } } diff --git a/test/Core.Test/Utilities/PermissiveStringConverterTests.cs b/test/Core.Test/Utilities/PermissiveStringConverterTests.cs new file mode 100644 index 0000000000..36945bbd19 --- /dev/null +++ b/test/Core.Test/Utilities/PermissiveStringConverterTests.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Bit.Core.Utilities; +using Bit.Test.Common.Helpers; +using Xunit; + +namespace Bit.Core.Test.Utilities +{ + public class PermissiveStringConverterTests + { + private const string numberJson = "{ \"StringProp\": 1, \"EnumerableStringProp\": [ 2, 3 ]}"; + private const string stringJson = "{ \"StringProp\": \"1\", \"EnumerableStringProp\": [ \"2\", \"3\" ]}"; + private const string nullAndEmptyJson = "{ \"StringProp\": null, \"EnumerableStringProp\": [] }"; + private const string singleValueJson = "{ \"StringProp\": 1, \"EnumerableStringProp\": \"Hello!\" }"; + private const string nullJson = "{ \"StringProp\": null, \"EnumerableStringProp\": null }"; + private const string boolJson = "{ \"StringProp\": true, \"EnumerableStringProp\": [ false, 1.2]}"; + private const string objectJsonOne = "{ \"StringProp\": { \"Message\": \"Hi\"}, \"EnumerableStringProp\": []}"; + private const string objectJsonTwo = "{ \"StringProp\": \"Hi\", \"EnumerableStringProp\": {}}"; + private readonly string bigNumbersJson = + "{ \"StringProp\":" + decimal.MinValue + ", \"EnumerableStringProp\": [" + ulong.MaxValue + ", " + long.MinValue + "]}"; + + [Theory] + [InlineData(numberJson)] + [InlineData(stringJson)] + public void Read_Success(string json) + { + var obj = JsonSerializer.Deserialize(json); + Assert.Equal("1", obj.StringProp); + Assert.Equal(2, obj.EnumerableStringProp.Count()); + Assert.Equal("2", obj.EnumerableStringProp.ElementAt(0)); + Assert.Equal("3", obj.EnumerableStringProp.ElementAt(1)); + } + + [Fact] + public void Read_Boolean_Success() + { + var obj = JsonSerializer.Deserialize(boolJson); + Assert.Equal("True", obj.StringProp); + Assert.Equal(2, obj.EnumerableStringProp.Count()); + Assert.Equal("False", obj.EnumerableStringProp.ElementAt(0)); + Assert.Equal("1.2", obj.EnumerableStringProp.ElementAt(1)); + } + + [Fact] + public void Read_BigNumbers_Success() + { + var obj = JsonSerializer.Deserialize(bigNumbersJson); + Assert.Equal(decimal.MinValue.ToString(), obj.StringProp); + Assert.Equal(2, obj.EnumerableStringProp.Count()); + Assert.Equal(ulong.MaxValue.ToString(), obj.EnumerableStringProp.ElementAt(0)); + Assert.Equal(long.MinValue.ToString(), obj.EnumerableStringProp.ElementAt(1)); + } + + [Fact] + public void Read_SingleValue_Success() + { + var obj = JsonSerializer.Deserialize(singleValueJson); + Assert.Equal("1", obj.StringProp); + Assert.Single(obj.EnumerableStringProp); + Assert.Equal("Hello!", obj.EnumerableStringProp.ElementAt(0)); + } + + [Fact] + public void Read_NullAndEmptyJson_Success() + { + var obj = JsonSerializer.Deserialize(nullAndEmptyJson); + Assert.Null(obj.StringProp); + Assert.Empty(obj.EnumerableStringProp); + } + + [Fact] + public void Read_Null_Success() + { + var obj = JsonSerializer.Deserialize(nullJson); + Assert.Null(obj.StringProp); + Assert.Null(obj.EnumerableStringProp); + } + + [Theory] + [InlineData(objectJsonOne)] + [InlineData(objectJsonTwo)] + public void Read_Object_Throws(string json) + { + var exception = Assert.Throws(() => JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Write_Success() + { + var json = JsonSerializer.Serialize(new TestObject + { + StringProp = "1", + EnumerableStringProp = new List + { + "2", + "3", + }, + }); + + var jsonElement = JsonDocument.Parse(json).RootElement; + + var stringProp = AssertHelper.AssertJsonProperty(jsonElement, "StringProp", JsonValueKind.String); + Assert.Equal("1", stringProp.GetString()); + var list = AssertHelper.AssertJsonProperty(jsonElement, "EnumerableStringProp", JsonValueKind.Array); + Assert.Equal(2, list.GetArrayLength()); + var firstElement = list[0]; + Assert.Equal(JsonValueKind.String, firstElement.ValueKind); + Assert.Equal("2", firstElement.GetString()); + var secondElement = list[1]; + Assert.Equal(JsonValueKind.String, secondElement.ValueKind); + Assert.Equal("3", secondElement.GetString()); + } + + [Fact] + public void Write_Null() + { + // When the values are null the converters aren't actually ran and it automatically serializes null + var json = JsonSerializer.Serialize(new TestObject + { + StringProp = null, + EnumerableStringProp = null, + }); + + var jsonElement = JsonDocument.Parse(json).RootElement; + + AssertHelper.AssertJsonProperty(jsonElement, "StringProp", JsonValueKind.Null); + AssertHelper.AssertJsonProperty(jsonElement, "EnumerableStringProp", JsonValueKind.Null); + } + + [Fact] + public void Write_Empty() + { + // When the values are null the converters aren't actually ran and it automatically serializes null + var json = JsonSerializer.Serialize(new TestObject + { + StringProp = "", + EnumerableStringProp = Enumerable.Empty(), + }); + + var jsonElement = JsonDocument.Parse(json).RootElement; + + var stringVal = AssertHelper.AssertJsonProperty(jsonElement, "StringProp", JsonValueKind.String).GetString(); + Assert.Equal("", stringVal); + var array = AssertHelper.AssertJsonProperty(jsonElement, "EnumerableStringProp", JsonValueKind.Array); + Assert.Equal(0, array.GetArrayLength()); + } + } + + public class TestObject + { + [JsonConverter(typeof(PermissiveStringConverter))] + public string StringProp { get; set; } + + [JsonConverter(typeof(PermissiveStringEnumerableConverter))] + public IEnumerable EnumerableStringProp { get; set; } + } +}