Skip to content

Commit 7cf2b9a

Browse files
author
Bart Koelman
committed
Fixes for usage of obfuscated IDs
1 parent 7880f67 commit 7cf2b9a

File tree

11 files changed

+142
-25
lines changed

11 files changed

+142
-25
lines changed
Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
1-
using JsonApiDotNetCore.Configuration;
1+
using System.Threading.Tasks;
2+
using JsonApiDotNetCore.Configuration;
23
using JsonApiDotNetCore.Controllers;
34
using JsonApiDotNetCore.Services;
45
using JsonApiDotNetCoreExample.Models;
6+
using Microsoft.AspNetCore.Mvc;
57
using Microsoft.Extensions.Logging;
68

79
namespace JsonApiDotNetCoreExample.Controllers
810
{
9-
public sealed class PassportsController : JsonApiController<Passport>
11+
public sealed class PassportsController : BaseJsonApiController<Passport>
1012
{
1113
public PassportsController(
1214
IJsonApiOptions jsonApiOptions,
1315
ILoggerFactory loggerFactory,
1416
IResourceService<Passport, int> resourceService)
1517
: base(jsonApiOptions, loggerFactory, resourceService)
1618
{ }
19+
20+
[HttpGet]
21+
public override async Task<IActionResult> GetAsync() => await base.GetAsync();
22+
23+
[HttpGet("{id}")]
24+
public async Task<IActionResult> GetAsync(string id)
25+
{
26+
int idValue = HexadecimalObfuscationCodec.Decode(id);
27+
return await base.GetAsync(idValue);
28+
}
29+
30+
[HttpPatch("{id}")]
31+
public async Task<IActionResult> PatchAsync(string id, [FromBody] Passport entity)
32+
{
33+
int idValue = HexadecimalObfuscationCodec.Decode(id);
34+
return await base.PatchAsync(idValue, entity);
35+
}
36+
37+
[HttpPost]
38+
public override async Task<IActionResult> PostAsync([FromBody] Passport entity)
39+
{
40+
return await base.PostAsync(entity);
41+
}
42+
43+
[HttpDelete("{id}")]
44+
public async Task<IActionResult> DeleteAsync(string id)
45+
{
46+
int idValue = HexadecimalObfuscationCodec.Decode(id);
47+
return await base.DeleteAsync(idValue);
48+
}
1749
}
1850
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Text;
5+
6+
namespace JsonApiDotNetCoreExample
7+
{
8+
public static class HexadecimalObfuscationCodec
9+
{
10+
public static int Decode(string value)
11+
{
12+
if (string.IsNullOrEmpty(value))
13+
{
14+
return 0;
15+
}
16+
17+
if (!value.StartsWith("x"))
18+
{
19+
throw new InvalidOperationException("Invalid obfuscated id.");
20+
}
21+
22+
string stringValue = FromHexString(value.Substring(1));
23+
return int.Parse(stringValue);
24+
}
25+
26+
private static string FromHexString(string hexString)
27+
{
28+
List<byte> bytes = new List<byte>(hexString.Length / 2);
29+
for (int index = 0; index < hexString.Length; index += 2)
30+
{
31+
var hexChar = hexString.Substring(index, 2);
32+
byte bt = byte.Parse(hexChar, NumberStyles.HexNumber);
33+
bytes.Add(bt);
34+
}
35+
36+
var chars = Encoding.Unicode.GetChars(bytes.ToArray());
37+
return new string(chars);
38+
}
39+
40+
public static string Encode(object value)
41+
{
42+
if (value is int intValue && intValue == 0)
43+
{
44+
return string.Empty;
45+
}
46+
47+
string stringValue = value.ToString();
48+
return 'x' + ToHexString(stringValue);
49+
}
50+
51+
private static string ToHexString(string value)
52+
{
53+
var builder = new StringBuilder();
54+
55+
foreach (byte bt in Encoding.Unicode.GetBytes(value))
56+
{
57+
builder.Append(bt.ToString("X2"));
58+
}
59+
60+
return builder.ToString();
61+
}
62+
}
63+
}

src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ public class Passport : Identifiable
1313
private readonly ISystemClock _systemClock;
1414
private int? _socialSecurityNumber;
1515

16+
protected override string GetStringId(object value)
17+
{
18+
return HexadecimalObfuscationCodec.Encode(value);
19+
}
20+
21+
protected override int GetTypedId(string value)
22+
{
23+
return HexadecimalObfuscationCodec.Decode(value);
24+
}
25+
1626
[Attr]
1727
public int? SocialSecurityNumber
1828
{

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Collections.Generic;
55
using System.Linq;
66
using System.Reflection;
7-
using JsonApiDotNetCore.Models;
87

98
namespace JsonApiDotNetCore.Extensions
109
{
@@ -52,13 +51,6 @@ public static IEnumerable CopyToTypedCollection(this IEnumerable source, Type co
5251
return concreteCollectionInstance;
5352
}
5453

55-
public static string GetResourceStringId<TResource, TId>(TId id) where TResource : class, IIdentifiable<TId>
56-
{
57-
var tempResource = TypeHelper.CreateInstance<TResource>();
58-
tempResource.Id = id;
59-
return tempResource.StringId;
60-
}
61-
6254
/// <summary>
6355
/// Whether the specified source type implements or equals the specified interface.
6456
/// </summary>

src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public IEnumerable LoadDbValues(LeftType entityTypeForRepository, IEnumerable en
8181
.GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance)
8282
.MakeGenericMethod(entityTypeForRepository, idType);
8383
var cast = ((IEnumerable<object>)entities).Cast<IIdentifiable>();
84-
var ids = cast.Select(e => TypeHelper.ConvertType(e.StringId, idType)).CopyToList(idType);
84+
var ids = cast.Select(TypeHelper.GetResourceTypedId).CopyToList(idType);
8585
var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, relationshipsToNextLayer });
8686
if (values == null) return null;
8787
return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(entityTypeForRepository), values.CopyToList(entityTypeForRepository));

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Linq.Expressions;
77
using JsonApiDotNetCore.Extensions;
88
using JsonApiDotNetCore.Models;
9-
using Microsoft.Extensions.DependencyInjection;
109

1110
namespace JsonApiDotNetCore.Internal
1211
{
@@ -238,5 +237,25 @@ public static object CreateInstance(Type type)
238237
throw new InvalidOperationException($"Failed to create an instance of '{type.FullName}' using its default constructor.", exception);
239238
}
240239
}
240+
241+
public static object ConvertStringIdToTypedId(Type resourceType, string stringId, IResourceFactory resourceFactory)
242+
{
243+
var tempResource = resourceFactory.CreateInstance(resourceType);
244+
tempResource.StringId = stringId;
245+
return GetResourceTypedId(tempResource);
246+
}
247+
248+
public static object GetResourceTypedId(IIdentifiable resource)
249+
{
250+
PropertyInfo property = resource.GetType().GetProperty(nameof(Identifiable.Id));
251+
return property.GetValue(resource);
252+
}
253+
254+
public static string GetResourceStringId<TResource, TId>(TId id) where TResource : class, IIdentifiable<TId>
255+
{
256+
TResource tempResource = CreateInstance<TResource>();
257+
tempResource.Id = id;
258+
return tempResource.StringId;
259+
}
241260
}
242261
}

src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,9 @@ private void SetForeignKey(IIdentifiable entity, PropertyInfo foreignKey, HasOne
198198
// For a server deserializer, it should be mapped to a BadRequest HTTP error code.
199199
throw new FormatException($"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type.");
200200
}
201-
var convertedId = TypeHelper.ConvertType(id, foreignKey.PropertyType);
202-
foreignKey.SetValue(entity, convertedId);
201+
202+
var typedId = TypeHelper.ConvertStringIdToTypedId(attr.PropertyInfo.PropertyType, id, _resourceFactory);
203+
foreignKey.SetValue(entity, typedId);
203204
}
204205

205206
/// <summary>

src/JsonApiDotNetCore/Services/DefaultResourceService.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public virtual async Task DeleteAsync(TId id)
9595
var succeeded = await _repository.DeleteAsync(id);
9696
if (!succeeded)
9797
{
98-
string resourceId = TypeExtensions.GetResourceStringId<TResource, TId>(id);
98+
string resourceId = TypeHelper.GetResourceStringId<TResource, TId>(id);
9999
throw new ResourceNotFoundException(resourceId, _currentRequestResource.ResourceName);
100100
}
101101

@@ -149,7 +149,7 @@ public virtual async Task<TResource> GetAsync(TId id)
149149

150150
if (entity == null)
151151
{
152-
string resourceId = TypeExtensions.GetResourceStringId<TResource, TId>(id);
152+
string resourceId = TypeHelper.GetResourceStringId<TResource, TId>(id);
153153
throw new ResourceNotFoundException(resourceId, _currentRequestResource.ResourceName);
154154
}
155155

@@ -177,7 +177,7 @@ public virtual async Task<TResource> GetRelationshipsAsync(TId id, string relati
177177

178178
if (entity == null)
179179
{
180-
string resourceId = TypeExtensions.GetResourceStringId<TResource, TId>(id);
180+
string resourceId = TypeHelper.GetResourceStringId<TResource, TId>(id);
181181
throw new ResourceNotFoundException(resourceId, _currentRequestResource.ResourceName);
182182
}
183183

@@ -207,7 +207,7 @@ public virtual async Task<TResource> UpdateAsync(TId id, TResource requestEntity
207207
TResource databaseEntity = await _repository.Get(id).FirstOrDefaultAsync();
208208
if (databaseEntity == null)
209209
{
210-
string resourceId = TypeExtensions.GetResourceStringId<TResource, TId>(id);
210+
string resourceId = TypeHelper.GetResourceStringId<TResource, TId>(id);
211211
throw new ResourceNotFoundException(resourceId, _currentRequestResource.ResourceName);
212212
}
213213

@@ -243,7 +243,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa
243243

244244
if (entity == null)
245245
{
246-
string resourceId = TypeExtensions.GetResourceStringId<TResource, TId>(id);
246+
string resourceId = TypeHelper.GetResourceStringId<TResource, TId>(id);
247247
throw new ResourceNotFoundException(resourceId, _currentRequestResource.ResourceName);
248248
}
249249

test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ public async Task Cascade_Permission_Error_Create_ToOne_Relationship()
301301
{
302302
{ "passport", new
303303
{
304-
data = new { type = "passports", id = $"{lockedPerson.Passport.Id}" }
304+
data = new { type = "passports", id = $"{lockedPerson.Passport.StringId}" }
305305
}
306306
}
307307
}
@@ -353,7 +353,7 @@ public async Task Cascade_Permission_Error_Updating_ToOne_Relationship()
353353
{
354354
{ "passport", new
355355
{
356-
data = new { type = "passports", id = $"{newPassport.Id}" }
356+
data = new { type = "passports", id = $"{newPassport.StringId}" }
357357
}
358358
}
359359
}
@@ -447,7 +447,7 @@ public async Task Cascade_Permission_Error_Delete_ToOne_Relationship()
447447
await context.SaveChangesAsync();
448448

449449
var httpMethod = new HttpMethod("DELETE");
450-
var route = $"/api/v1/passports/{lockedPerson.PassportId}";
450+
var route = $"/api/v1/passports/{lockedPerson.Passport.StringId}";
451451
var request = new HttpRequestMessage(httpMethod, route);
452452

453453
// Act

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public async Task GetSingleResource_TopLevel_AppliesEagerLoad()
6060
_dbContext.SaveChanges();
6161

6262
// Act
63-
var (body, response) = await Get($"/api/v1/passports/{passport.Id}");
63+
var (body, response) = await Get($"/api/v1/passports/{passport.StringId}");
6464

6565
// Assert
6666
AssertEqualStatusCode(HttpStatusCode.OK, response);
@@ -182,7 +182,7 @@ public async Task PatchResource_TopLevel_AppliesEagerLoad()
182182
var content = serializer.Serialize(passport);
183183

184184
// Act
185-
var (body, response) = await Patch($"/api/v1/passports/{passport.Id}", content);
185+
var (body, response) = await Patch($"/api/v1/passports/{passport.StringId}", content);
186186

187187
// Assert
188188
AssertEqualStatusCode(HttpStatusCode.OK, response);

0 commit comments

Comments
 (0)