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..8a56791e 100644 --- a/tests/Meilisearch.Tests/TenantTokenTests.cs +++ b/tests/Meilisearch.Tests/TenantTokenTests.cs @@ -7,7 +7,8 @@ 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 +71,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 +96,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 +134,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 +160,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()); } } }