From 5e9eb439a050aedd5314c4efa92f52a425550a61 Mon Sep 17 00:00:00 2001 From: ahmednfwela Date: Wed, 10 Jul 2024 07:39:48 +0300 Subject: [PATCH 1/2] fix invalid API key handling --- meilisearch-dart | 1 + .../MeilisearchTenantTokenApiKeyInvalid.cs | 2 +- src/Meilisearch/TenantToken.cs | 4 +- src/Meilisearch/TenantTokenRules.cs | 22 +++- tests/Meilisearch.Tests/TenantTokenTests.cs | 117 ++++++++++++++---- 5 files changed, 116 insertions(+), 30 deletions(-) create mode 160000 meilisearch-dart diff --git a/meilisearch-dart b/meilisearch-dart new file mode 160000 index 00000000..1d22b746 --- /dev/null +++ b/meilisearch-dart @@ -0,0 +1 @@ +Subproject commit 1d22b746b5207fbdd149076f4e0f4756f803fe8d diff --git a/src/Meilisearch/Errors/MeilisearchTenantTokenApiKeyInvalid.cs b/src/Meilisearch/Errors/MeilisearchTenantTokenApiKeyInvalid.cs index 2e77ec32..b9a50d3a 100644 --- a/src/Meilisearch/Errors/MeilisearchTenantTokenApiKeyInvalid.cs +++ b/src/Meilisearch/Errors/MeilisearchTenantTokenApiKeyInvalid.cs @@ -12,7 +12,7 @@ public class MeilisearchTenantTokenApiKeyInvalid : Exception /// Initializes a new instance of the class. /// public MeilisearchTenantTokenApiKeyInvalid() - : base("Cannot generate a signed token without a valid apiKey. Provide one in the MeilisearchClient instance or in the method params.") + : base("Cannot generate a signed token without a valid apiKey. Provide one in the MeilisearchClient instance or in the method params. The key MUST be at least 16 characters, or 128 bits") { } } diff --git a/src/Meilisearch/TenantToken.cs b/src/Meilisearch/TenantToken.cs index 1fe20025..5651a1cd 100644 --- a/src/Meilisearch/TenantToken.cs +++ b/src/Meilisearch/TenantToken.cs @@ -16,12 +16,12 @@ public class TenantToken /// JWT string public static string GenerateToken(string apiKeyUid, TenantTokenRules searchRules, string apiKey, DateTime? expiresAt) { - if (String.IsNullOrEmpty(apiKeyUid)) + if (string.IsNullOrEmpty(apiKeyUid)) { throw new MeilisearchTenantTokenApiKeyUidInvalid(); } - if (String.IsNullOrEmpty(apiKey) || apiKey.Length < 8) + if (string.IsNullOrEmpty(apiKey) || apiKey.Length < 16) { throw new MeilisearchTenantTokenApiKeyInvalid(); } diff --git a/src/Meilisearch/TenantTokenRules.cs b/src/Meilisearch/TenantTokenRules.cs index 31e0aa83..fcff7d80 100644 --- a/src/Meilisearch/TenantTokenRules.cs +++ b/src/Meilisearch/TenantTokenRules.cs @@ -10,11 +10,31 @@ public class TenantTokenRules { private readonly object _rules; - public TenantTokenRules(Dictionary rules) + /// + /// Initializes a new instance of the class based on a rules json object. + /// + /// + /// + /// example: + /// + /// {'*': {"filter": 'tag = Tale'}} + /// + /// + public TenantTokenRules(IReadOnlyDictionary rules) { _rules = rules; } + /// + /// Initializes a new instance of the class based on a rules string array. + /// + /// + /// + /// example: + /// + /// ['books'] + /// + /// public TenantTokenRules(string[] rules) { _rules = rules; diff --git a/tests/Meilisearch.Tests/TenantTokenTests.cs b/tests/Meilisearch.Tests/TenantTokenTests.cs index ef875667..9a588a16 100644 --- a/tests/Meilisearch.Tests/TenantTokenTests.cs +++ b/tests/Meilisearch.Tests/TenantTokenTests.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; - using Xunit; namespace Meilisearch.Tests { - public abstract class TenantTokenTests : IAsyncLifetime where TFixture : IndexFixture + public abstract class TenantTokenTests : IAsyncLifetime + where TFixture : IndexFixture { private readonly TenantTokenRules _searchRules = new TenantTokenRules(new string[] { "*" }); @@ -70,9 +70,21 @@ public void ClientThrowsIfNoKeyIsAvailable() ); } + + [Fact] + public void ClientThrowsIfKeyIsLessThan128Bits() + { + var customClient = new MeilisearchClient(_fixture.MeilisearchAddress(), "masterKey"); + Assert.Throws( + () => customClient.GenerateTenantToken(_uid, _searchRules) + ); + } + + + [Theory] [MemberData(nameof(PossibleSearchRules))] - public async void SearchesSuccessfullyWithTheNewToken(dynamic data) + public async void SearchesSuccessfullyWithTheNewToken(object data) { var keyOptions = new Key { @@ -83,10 +95,26 @@ public async void SearchesSuccessfullyWithTheNewToken(dynamic data) }; var createdKey = await _client.CreateKeyAsync(keyOptions); var admClient = new MeilisearchClient(_fixture.MeilisearchAddress(), createdKey.KeyUid); - var task = await admClient.Index(_indexName).UpdateFilterableAttributesAsync(new string[] { "tag", "book_id" }); + var task = await admClient + .Index(_indexName) + .UpdateFilterableAttributesAsync(new string[] { "tag", "book_id" }); await admClient.Index(_indexName).WaitForTaskAsync(task.TaskUid); - var token = admClient.GenerateTenantToken(createdKey.Uid, new TenantTokenRules(data)); + TenantTokenRules tokenRules; + if (data is string[] dataStringArray) + { + tokenRules = new TenantTokenRules(dataStringArray); + } + else if (data is IReadOnlyDictionary dataDictionary) + { + tokenRules = new TenantTokenRules(dataDictionary); + } + else + { + throw new Exception("Invalid data type"); + } + + var token = admClient.GenerateTenantToken(createdKey.Uid, tokenRules); var customClient = new MeilisearchClient(_fixture.MeilisearchAddress(), token); await customClient.Index(_indexName).SearchAsync(string.Empty); @@ -105,12 +133,17 @@ public async Task SearchFailsWhenTokenIsExpired() var createdKey = await _client.CreateKeyAsync(keyOptions); var admClient = new MeilisearchClient(_fixture.MeilisearchAddress(), createdKey.KeyUid); - var token = admClient.GenerateTenantToken(createdKey.Uid, new TenantTokenRules(new[] { "*" }), expiresAt: DateTime.UtcNow.AddSeconds(1)); + var token = admClient.GenerateTenantToken( + createdKey.Uid, + new TenantTokenRules(new[] { "*" }), + expiresAt: DateTime.UtcNow.AddSeconds(1) + ); var customClient = new MeilisearchClient(_fixture.MeilisearchAddress(), token); Thread.Sleep(TimeSpan.FromSeconds(2)); - await Assert.ThrowsAsync(async () => - await customClient.Index(_indexName).SearchAsync(string.Empty)); + await Assert.ThrowsAsync( + async () => await customClient.Index(_indexName).SearchAsync(string.Empty) + ); } [Fact] @@ -126,29 +159,61 @@ public async void SearchSucceedsWhenTokenIsNotExpired() var createdKey = await _client.CreateKeyAsync(keyOptions); var admClient = new MeilisearchClient(_fixture.MeilisearchAddress(), createdKey.KeyUid); - var token = admClient.GenerateTenantToken(createdKey.Uid, new TenantTokenRules(new[] { "*" }), expiresAt: DateTime.UtcNow.AddMinutes(1)); + var token = admClient.GenerateTenantToken( + createdKey.Uid, + new TenantTokenRules(new[] { "*" }), + expiresAt: DateTime.UtcNow.AddMinutes(1) + ); var customClient = new MeilisearchClient(_fixture.MeilisearchAddress(), token); await customClient.Index(_indexName).SearchAsync(string.Empty); } - public static IEnumerable PossibleSearchRules() + public static TheoryData PossibleSearchRules() { - // {'*': {}} - yield return new object[] { new Dictionary { { "*", new Dictionary { } } } }; - // {'books': {}} - yield return new object[] { new Dictionary { { "books", new Dictionary { } } } }; - // {'*': null} - yield return new object[] { new Dictionary { { "*", null } } }; - // {'books': null} - yield return new object[] { new Dictionary { { "books", null } } }; - // ['*'] - yield return new object[] { new string[] { "*" } }; - // ['books'] - yield return new object[] { new string[] { "books" } }; - // {'*': {"filter": 'tag = Tale'}} - yield return new object[] { new Dictionary { { "*", new Dictionary { { "filter", "tag = Tale" } } } } }; - // {'books': {"filter": 'tag = Tale'}} - yield return new object[] { new Dictionary { { "books", new Dictionary { { "filter", "tag = Tale" } } } } }; + IEnumerable SubPossibleSearchRules() + { + // {'*': {}} + yield return new Dictionary + { + { + "*", + new Dictionary { } + } + }; + // {'books': {}} + yield return new Dictionary + { + { + "books", + new Dictionary { } + } + }; + // {'*': null} + yield return new Dictionary { { "*", null } }; + // {'books': null} + yield return new Dictionary { { "books", null } }; + // ['*'] + yield return new string[] { "*" }; + // ['books'] + yield return new string[] { "books" }; + // {'*': {"filter": 'tag = Tale'}} + yield return new Dictionary + { + { + "*", + new Dictionary { { "filter", "tag = Tale" } } + } + }; + // {'books': {"filter": 'tag = Tale'}} + yield return new Dictionary + { + { + "books", + new Dictionary { { "filter", "tag = Tale" } } + } + }; + } + return new TheoryData(SubPossibleSearchRules()); } } } From 39ca582cae3ed2aef095fa6d5e2f8a69e3905d20 Mon Sep 17 00:00:00 2001 From: ahmednfwela Date: Wed, 10 Jul 2024 07:49:17 +0300 Subject: [PATCH 2/2] format --- tests/Meilisearch.Tests/TenantTokenTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Meilisearch.Tests/TenantTokenTests.cs b/tests/Meilisearch.Tests/TenantTokenTests.cs index 9a588a16..8a56791e 100644 --- a/tests/Meilisearch.Tests/TenantTokenTests.cs +++ b/tests/Meilisearch.Tests/TenantTokenTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Xunit; namespace Meilisearch.Tests @@ -70,11 +71,11 @@ public void ClientThrowsIfNoKeyIsAvailable() ); } - + [Fact] public void ClientThrowsIfKeyIsLessThan128Bits() { - var customClient = new MeilisearchClient(_fixture.MeilisearchAddress(), "masterKey"); + var customClient = new MeilisearchClient(_fixture.MeilisearchAddress(), "masterKey"); Assert.Throws( () => customClient.GenerateTenantToken(_uid, _searchRules) );