Skip to content

Commit 9277256

Browse files
authored
Improved ID obfuscation (#1775)
* Make running kiota tests faster by turning off default handlers (such as retries) * Remove redundant TranslateAsync calls in tests * Fix inconsistencies in test assertions * Assert on obfuscated links, mention sqids * Switch from int to long, use invariant culture, use singleton codec * Add API to remove a resource from the graph (after assembly scanning) * Add test support for obfuscation in operations * Add annotation to hide underlying resource ID type in OpenAPI document * Add OpenAPI tests for ID obfuscation
1 parent 23be010 commit 9277256

File tree

133 files changed

+14500
-297
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

133 files changed

+14500
-297
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace JsonApiDotNetCore.OpenApi.Swashbuckle.Annotations;
2+
3+
/// <summary>
4+
/// Hides the underlying resource ID type in OpenAPI documents.
5+
/// </summary>
6+
/// <remarks>
7+
/// For example, when used on a resource type that implements <c><![CDATA[IIdentifiable<long>]]></c>, excludes the <c>format</c> property on the ID
8+
/// schema. As a result, the ID type is displayed as <c>string</c> instead of
9+
/// <c>
10+
/// string($int64)
11+
/// </c>
12+
/// in SwaggerUI.
13+
/// </remarks>
14+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct)]
15+
public sealed class HideResourceIdTypeInOpenApiAttribute : Attribute;

src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Components/ResourceIdSchemaGenerator.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System.Reflection;
12
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Middleware;
4+
using JsonApiDotNetCore.OpenApi.Swashbuckle.Annotations;
25
using Microsoft.OpenApi.Models;
36
using Swashbuckle.AspNetCore.SwaggerGen;
47

@@ -7,32 +10,48 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators.Components;
710
internal sealed class ResourceIdSchemaGenerator
811
{
912
private readonly SchemaGenerator _defaultSchemaGenerator;
13+
private readonly IControllerResourceMapping _controllerResourceMapping;
1014

11-
public ResourceIdSchemaGenerator(SchemaGenerator defaultSchemaGenerator)
15+
public ResourceIdSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IControllerResourceMapping controllerResourceMapping)
1216
{
1317
ArgumentNullException.ThrowIfNull(defaultSchemaGenerator);
18+
ArgumentNullException.ThrowIfNull(controllerResourceMapping);
1419

1520
_defaultSchemaGenerator = defaultSchemaGenerator;
21+
_controllerResourceMapping = controllerResourceMapping;
1622
}
1723

18-
public OpenApiSchema GenerateSchema(ResourceType resourceType, SchemaRepository schemaRepository)
24+
public OpenApiSchema GenerateSchema(ParameterInfo parameter, SchemaRepository schemaRepository)
1925
{
20-
ArgumentNullException.ThrowIfNull(resourceType);
26+
ArgumentNullException.ThrowIfNull(parameter);
27+
ArgumentNullException.ThrowIfNull(schemaRepository);
28+
29+
Type? controllerType = parameter.Member.ReflectedType;
30+
ConsistencyGuard.ThrowIf(controllerType == null);
31+
32+
ResourceType? resourceType = _controllerResourceMapping.GetResourceTypeForController(controllerType);
33+
ConsistencyGuard.ThrowIf(resourceType == null);
2134

22-
return GenerateSchema(resourceType.IdentityClrType, schemaRepository);
35+
return GenerateSchema(resourceType, schemaRepository);
2336
}
2437

25-
public OpenApiSchema GenerateSchema(Type resourceIdClrType, SchemaRepository schemaRepository)
38+
public OpenApiSchema GenerateSchema(ResourceType resourceType, SchemaRepository schemaRepository)
2639
{
27-
ArgumentNullException.ThrowIfNull(resourceIdClrType);
40+
ArgumentNullException.ThrowIfNull(resourceType);
2841
ArgumentNullException.ThrowIfNull(schemaRepository);
2942

30-
OpenApiSchema idSchema = _defaultSchemaGenerator.GenerateSchema(resourceIdClrType, schemaRepository);
43+
OpenApiSchema idSchema = _defaultSchemaGenerator.GenerateSchema(resourceType.IdentityClrType, schemaRepository);
3144
ConsistencyGuard.ThrowIf(idSchema.Reference != null);
3245

3346
idSchema.Type = "string";
3447

35-
if (resourceIdClrType != typeof(string))
48+
var hideIdTypeAttribute = resourceType.ClrType.GetCustomAttribute<HideResourceIdTypeInOpenApiAttribute>();
49+
50+
if (hideIdTypeAttribute != null)
51+
{
52+
idSchema.Format = null;
53+
}
54+
else if (resourceType.IdentityClrType != typeof(string))
3655
{
3756
// When using string IDs, it's discouraged (but possible) to use an empty string as primary key value, because
3857
// some things won't work: get-by-id, update and delete resource are impossible, and rendered links are unusable.

src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/JsonApiSchemaGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public OpenApiSchema GenerateSchema(Type schemaType, SchemaRepository schemaRepo
3434

3535
if (parameterInfo is { Name: "id" } && IsJsonApiParameter(parameterInfo))
3636
{
37-
return _resourceIdSchemaGenerator.GenerateSchema(schemaType, schemaRepository);
37+
return _resourceIdSchemaGenerator.GenerateSchema(parameterInfo, schemaRepository);
3838
}
3939

4040
DocumentSchemaGenerator? schemaGenerator = GetDocumentSchemaGenerator(schemaType);

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,32 @@ private static bool IsImplicitManyToManyJoinEntity(IEntityType entityType)
167167
return entityType is { IsPropertyBag: true, HasSharedClrType: true };
168168
}
169169

170+
/// <summary>
171+
/// Removes a JSON:API resource.
172+
/// </summary>
173+
/// <typeparam name="TResource">
174+
/// The resource CLR type.
175+
/// </typeparam>
176+
public ResourceGraphBuilder Remove<TResource>()
177+
where TResource : class, IIdentifiable
178+
{
179+
return Remove(typeof(TResource));
180+
}
181+
182+
/// <summary>
183+
/// Removes a JSON:API resource.
184+
/// </summary>
185+
/// <param name="resourceClrType">
186+
/// The resource CLR type.
187+
/// </param>
188+
public ResourceGraphBuilder Remove(Type resourceClrType)
189+
{
190+
ArgumentNullException.ThrowIfNull(resourceClrType);
191+
192+
_resourceTypesByClrType.Remove(resourceClrType);
193+
return this;
194+
}
195+
170196
/// <summary>
171197
/// Adds a JSON:API resource.
172198
/// </summary>

test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/BankAccountsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
namespace JsonApiDotNetCoreTests.IntegrationTests.IdObfuscation;
66

77
public sealed class BankAccountsController(
8-
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<BankAccount, int> resourceService)
8+
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<BankAccount, long> resourceService)
99
: ObfuscatedIdentifiableController<BankAccount>(options, resourceGraph, loggerFactory, resourceService);

test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/DebitCardsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
namespace JsonApiDotNetCoreTests.IntegrationTests.IdObfuscation;
66

77
public sealed class DebitCardsController(
8-
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<DebitCard, int> resourceService)
8+
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<DebitCard, long> resourceService)
99
: ObfuscatedIdentifiableController<DebitCard>(options, resourceGraph, loggerFactory, resourceService);

test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.IdObfuscation;
88

99
internal sealed class HexadecimalCodec
1010
{
11-
public int Decode(string? value)
11+
// This implementation is deliberately simple for demonstration purposes.
12+
// Consider using something more robust, such as https://github.com/sqids/sqids-dotnet.
13+
public static HexadecimalCodec Instance { get; } = new();
14+
15+
private HexadecimalCodec()
16+
{
17+
}
18+
19+
public long Decode(string? value)
1220
{
1321
if (value == null)
1422
{
@@ -25,7 +33,7 @@ public int Decode(string? value)
2533
}
2634

2735
string stringValue = FromHexString(value[1..]);
28-
return int.Parse(stringValue);
36+
return long.Parse(stringValue, CultureInfo.InvariantCulture);
2937
}
3038

3139
private static string FromHexString(string hexString)
@@ -35,22 +43,22 @@ private static string FromHexString(string hexString)
3543
for (int index = 0; index < hexString.Length; index += 2)
3644
{
3745
string hexChar = hexString.Substring(index, 2);
38-
byte bt = byte.Parse(hexChar, NumberStyles.HexNumber);
46+
byte bt = byte.Parse(hexChar, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
3947
bytes.Add(bt);
4048
}
4149

4250
char[] chars = Encoding.ASCII.GetChars([.. bytes]);
4351
return new string(chars);
4452
}
4553

46-
public string? Encode(int value)
54+
public string? Encode(long value)
4755
{
4856
if (value == 0)
4957
{
5058
return null;
5159
}
5260

53-
string stringValue = value.ToString();
61+
string stringValue = value.ToString(CultureInfo.InvariantCulture);
5462
return $"x{ToHexString(stringValue)}";
5563
}
5664

@@ -60,7 +68,7 @@ private static string ToHexString(string value)
6068

6169
foreach (byte bt in Encoding.ASCII.GetBytes(value))
6270
{
63-
builder.Append(bt.ToString("X2"));
71+
builder.Append(bt.ToString("X2", CultureInfo.InvariantCulture));
6472
}
6573

6674
return builder.ToString();

0 commit comments

Comments
 (0)