-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
239: Changes related to the next Meilisearch release (v0.26.0) r=brunoocasali a=meili-bot Related to this issue: meilisearch/integration-guides#181 This PR: - gathers the changes related to the next Meilisearch release (v0.26.0) so that this package is ready when the official release is out. - should pass the tests against the [latest pre-release of Meilisearch](https://github.com/meilisearch/meilisearch/releases). - might eventually contain test failures until the Meilisearch v0.26.0 is out.⚠️ This PR should NOT be merged until the next release of Meilisearch (v0.26.0) is out. _This PR is auto-generated for the [pre-release week](https://github.com/meilisearch/integration-guides/blob/master/guides/pre-release-week.md) purpose._ Co-authored-by: meili-bot <[email protected]> Co-authored-by: Bruno Casali <[email protected]>
- Loading branch information
Showing
12 changed files
with
323 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
src/Meilisearch/Errors/MeilisearchTenantTokenApiKeyInvalid.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System; | ||
|
||
namespace Meilisearch | ||
{ | ||
/// <summary> | ||
/// Represents an exception thrown when `apiKey` is not present | ||
/// to sign correctly the Tenant Token generation. | ||
/// </summary> | ||
public class MeilisearchTenantTokenApiKeyInvalid : Exception | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="MeilisearchTenantTokenApiKeyInvalid"/> class. | ||
/// </summary> | ||
public MeilisearchTenantTokenApiKeyInvalid() | ||
: base("Cannot generate a signed token without a valid apiKey. Provide one in the MeilisearchClient instance or in the method params.") | ||
{ | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using System; | ||
|
||
namespace Meilisearch | ||
{ | ||
/// <summary> | ||
/// Represents an exception thrown when the provided expiration date is invalid or in the past. | ||
/// </summary> | ||
public class MeilisearchTenantTokenExpired : Exception | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="MeilisearchTenantTokenExpired"/> class. | ||
/// </summary> | ||
public MeilisearchTenantTokenExpired() | ||
: base("Provide a valid UTC DateTime in the future.") | ||
{ | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using System; | ||
|
||
using JWT.Algorithms; | ||
using JWT.Builder; | ||
|
||
namespace Meilisearch | ||
{ | ||
public class TenantToken | ||
{ | ||
/// <summary> | ||
/// Generates a Tenant Token in a JWT string format. | ||
/// </summary> | ||
/// <returns>JWT string</returns> | ||
public static string GenerateToken(TenantTokenRules searchRules, string apiKey, DateTime? expiresAt) | ||
{ | ||
if (String.IsNullOrEmpty(apiKey) || apiKey.Length < 8) | ||
{ | ||
throw new MeilisearchTenantTokenApiKeyInvalid(); | ||
} | ||
|
||
var builder = JwtBuilder | ||
.Create() | ||
.WithAlgorithm(new HMACSHA256Algorithm()) | ||
.AddClaim("apiKeyPrefix", apiKey.Substring(0, 8)) | ||
.AddClaim("searchRules", searchRules.ToClaim()) | ||
.WithSecret(apiKey); | ||
|
||
if (expiresAt.HasValue) | ||
{ | ||
if (DateTime.Compare(DateTime.UtcNow, (DateTime)expiresAt) > 0) | ||
{ | ||
throw new MeilisearchTenantTokenExpired(); | ||
} | ||
|
||
builder.AddClaim("exp", ((DateTimeOffset)expiresAt).ToUnixTimeSeconds()); | ||
} | ||
|
||
return builder.Encode(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace Meilisearch | ||
{ | ||
/// <summary> | ||
/// Wrapper class used to map all the supported types to be used in | ||
/// the `searchRules` claim in the Tenant Tokens. | ||
/// </summary> | ||
public class TenantTokenRules | ||
{ | ||
private object _rules; | ||
|
||
public TenantTokenRules(Dictionary<string, object> rules) | ||
{ | ||
_rules = rules; | ||
} | ||
|
||
public TenantTokenRules(string[] rules) | ||
{ | ||
_rules = rules; | ||
} | ||
|
||
/// <summary> | ||
/// Accessor method used to retrieve the searchRules claim. | ||
/// </summary> | ||
/// <returns>A object with the supported type representing the `searchRules`.</returns> | ||
public object ToClaim() | ||
{ | ||
return _rules; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
|
||
using JWT.Algorithms; | ||
using JWT.Builder; | ||
using JWT.Exceptions; | ||
|
||
using Xunit; | ||
|
||
namespace Meilisearch.Tests | ||
{ | ||
public abstract class TenantTokenTests<TFixture> : IAsyncLifetime where TFixture : IndexFixture | ||
{ | ||
private TenantTokenRules _searchRules = new TenantTokenRules(new string[] { "*" }); | ||
|
||
private readonly TFixture _fixture; | ||
private JwtBuilder _builder; | ||
private Index _basicIndex; | ||
private readonly MeilisearchClient _client; | ||
private readonly string _indexName = "books"; | ||
private string _key; | ||
|
||
public TenantTokenTests(TFixture fixture) | ||
{ | ||
_fixture = fixture; | ||
_client = fixture.DefaultClient; | ||
_key = Guid.NewGuid().ToString(); | ||
} | ||
|
||
public async Task InitializeAsync() | ||
{ | ||
await _fixture.DeleteAllIndexes(); | ||
_basicIndex = await _fixture.SetUpBasicIndex(_indexName); | ||
_builder = JwtBuilder | ||
.Create() | ||
.WithAlgorithm(new HMACSHA256Algorithm()) | ||
.MustVerifySignature(); | ||
} | ||
|
||
public Task DisposeAsync() => Task.CompletedTask; | ||
|
||
[Fact] | ||
public void DoesNotGenerateASignedTokenWithoutAKey() | ||
{ | ||
Assert.Throws<MeilisearchTenantTokenApiKeyInvalid>( | ||
() => TenantToken.GenerateToken(_searchRules, null, null) | ||
); | ||
} | ||
|
||
[Fact] | ||
public void SignsTokenWithGivenKey() | ||
{ | ||
var token = TenantToken.GenerateToken(_searchRules, _key, null); | ||
|
||
Assert.Throws<SignatureVerificationException>( | ||
() => _builder.WithSecret("other-key").Decode(token) | ||
); | ||
|
||
_builder.WithSecret(_key).Decode(token); | ||
} | ||
|
||
[Fact] | ||
public void GeneratesTokenWithExpiresAt() | ||
{ | ||
var expiration = DateTimeOffset.UtcNow.AddDays(1).DateTime; | ||
var token = TenantToken.GenerateToken(_searchRules, _key, expiration); | ||
|
||
_builder.WithSecret(_key).Decode(token); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsExceptionWhenExpiresAtIsInThePast() | ||
{ | ||
var expiresAt = new DateTime(1995, 12, 20); | ||
|
||
Assert.Throws<MeilisearchTenantTokenExpired>( | ||
() => TenantToken.GenerateToken(_searchRules, _key, expiresAt) | ||
); | ||
} | ||
|
||
[Fact] | ||
public void ContainsValidClaims() | ||
{ | ||
var token = TenantToken.GenerateToken(_searchRules, _key, null); | ||
|
||
var claims = _builder.WithSecret(_key).Decode<IDictionary<string, object>>(token); | ||
|
||
Assert.Equal(claims["apiKeyPrefix"], _key.Substring(0, 8)); | ||
Assert.Equal(claims["searchRules"], _searchRules.ToClaim()); | ||
} | ||
|
||
[Fact] | ||
public void ClientDecodesSuccessfullyUsingApiKeyFromInstance() | ||
{ | ||
var token = _client.GenerateTenantToken(_searchRules); | ||
|
||
_builder.WithSecret(_client.ApiKey).Decode(token); | ||
} | ||
|
||
[Fact] | ||
public void ClientDecodesSuccessfullyUsingApiKeyFromArgument() | ||
{ | ||
var token = _client.GenerateTenantToken(_searchRules, apiKey: _key); | ||
|
||
_builder.WithSecret(_key).Decode(token); | ||
} | ||
|
||
[Fact] | ||
public void ClientThrowsIfNoKeyIsAvailable() | ||
{ | ||
var customClient = new MeilisearchClient(_fixture.MeilisearchAddress); | ||
|
||
Assert.Throws<MeilisearchTenantTokenApiKeyInvalid>( | ||
() => customClient.GenerateTenantToken(_searchRules) | ||
); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(PossibleSearchRules))] | ||
public async void SearchesSuccessfullyWithTheNewToken(dynamic data) | ||
{ | ||
var keyOptions = new Key | ||
{ | ||
Description = "Key generate a tenant token", | ||
Actions = new string[] { "*" }, | ||
Indexes = new string[] { "*" }, | ||
ExpiresAt = null, | ||
}; | ||
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" }); | ||
await admClient.Index(_indexName).WaitForTaskAsync(task.Uid); | ||
|
||
var token = admClient.GenerateTenantToken(new TenantTokenRules(data)); | ||
var customClient = new MeilisearchClient(_fixture.MeilisearchAddress, token); | ||
|
||
await customClient.Index(_indexName).SearchAsync<Movie>(string.Empty); | ||
} | ||
|
||
public static IEnumerable<object[]> PossibleSearchRules() | ||
{ | ||
// {'*': {}} | ||
yield return new object[] { new Dictionary<string, object> { { "*", new Dictionary<string, object> { } } } }; | ||
// {'books': {}} | ||
yield return new object[] { new Dictionary<string, object> { { "books", new Dictionary<string, object> { } } } }; | ||
// {'*': null} | ||
yield return new object[] { new Dictionary<string, object> { { "*", null } } }; | ||
// {'books': null} | ||
yield return new object[] { new Dictionary<string, object> { { "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<string, object> { { "*", new Dictionary<string, object> { { "filter", "tag = Tale" } } } } }; | ||
// {'books': {"filter": 'tag = Tale'}} | ||
yield return new object[] { new Dictionary<string, object> { { "books", new Dictionary<string, object> { { "filter", "tag = Tale" } } } } }; | ||
} | ||
} | ||
} |