-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #297 from meilisearch/generate_token
Adding generateTenantToken method to the client
- Loading branch information
Showing
8 changed files
with
264 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
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,87 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace MeiliSearch\Endpoints; | ||
|
||
use DateTime; | ||
use MeiliSearch\Contracts\Endpoint; | ||
use MeiliSearch\Exceptions\InvalidArgumentException; | ||
use MeiliSearch\Http\Serialize\Json; | ||
|
||
class TenantToken extends Endpoint | ||
{ | ||
private function base64url_encode($data) | ||
{ | ||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); | ||
} | ||
|
||
private function validateTenantTokenArguments($searchRules, ?array $options = []): void | ||
{ | ||
if (!\array_key_exists('apiKey', $options) || '' == $options['apiKey'] || \strlen($options['apiKey']) <= 8) { | ||
throw InvalidArgumentException::emptyArgument('api key'); | ||
} | ||
if ((!\is_array($searchRules) && !\is_object($searchRules)) || null == $searchRules) { | ||
throw InvalidArgumentException::emptyArgument('search rules'); | ||
} | ||
if (\array_key_exists('expiresAt', $options) && new DateTime() > $options['expiresAt']) { | ||
throw InvalidArgumentException::dateIsExpired($options['expiresAt']); | ||
} | ||
} | ||
|
||
/** | ||
* Generate a new tenant token. | ||
* | ||
* The $options parameter is an array, and the following keys are accepted: | ||
* - apiKey: The API key parent of the token. If you leave it empty the client API Key will be used. | ||
* - expiresAt: A DateTime when the key will expire. Note that if an expiresAt value is included it should be in UTC time. | ||
*/ | ||
public function generateTenantToken($searchRules, ?array $options = []): string | ||
{ | ||
if (!\array_key_exists('apiKey', $options) || '' == $options['apiKey']) { | ||
$options['apiKey'] = $this->apiKey; | ||
} | ||
|
||
// Validate every field | ||
$this->validateTenantTokenArguments($searchRules, $options); | ||
|
||
$json = new Json(); | ||
|
||
// Standard JWT header for encryption with SHA256/HS256 algorithm | ||
$header = [ | ||
'typ' => 'JWT', | ||
'alg' => 'HS256', | ||
]; | ||
|
||
// Add the required fields to the payload | ||
$payload = []; | ||
$payload['apiKeyPrefix'] = substr($options['apiKey'], 0, 8); | ||
$payload['searchRules'] = $searchRules; | ||
if (\array_key_exists('expiresAt', $options)) { | ||
$payload['exp'] = $options['expiresAt']->getTimestamp(); | ||
} | ||
|
||
// Serialize the Header | ||
$jsonHeader = $json->serialize($header); | ||
|
||
// Serialize the Payload | ||
$jsonPayload = $json->serialize($payload); | ||
|
||
// Encode Header to Base64Url String | ||
$encodedHeader = $this->base64url_encode($jsonHeader); | ||
|
||
// Encode Payload to Base64Url String | ||
$encodedPayload = $this->base64url_encode($jsonPayload); | ||
|
||
// Create Signature Hash | ||
$signature = hash_hmac('sha256', $encodedHeader.'.'.$encodedPayload, $options['apiKey'], true); | ||
|
||
// Encode Signature to Base64Url String | ||
$encodedSignature = $this->base64url_encode($signature); | ||
|
||
// Create JWT | ||
$jwtToken = $encodedHeader.'.'.$encodedPayload.'.'.$encodedSignature; | ||
|
||
return $jwtToken; | ||
} | ||
} |
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,154 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Tests\Endpoints; | ||
|
||
/* @phpstan-ignore-next-line */ | ||
use Datetime; | ||
use MeiliSearch\Client; | ||
use MeiliSearch\Exceptions\ApiException; | ||
use MeiliSearch\Exceptions\InvalidArgumentException; | ||
use Tests\TestCase; | ||
|
||
final class TenantTokenTest extends TestCase | ||
{ | ||
private string $privateKey; | ||
private Client $privateClient; | ||
|
||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
$this->createEmptyIndex('tenantToken'); | ||
|
||
$response = $this->client->getKeys(); | ||
$this->privateKey = array_reduce($response, function ($carry, $item) { | ||
if ($item->getDescription() && str_contains($item->getDescription(), 'Default Admin API')) { | ||
return $item->getKey(); | ||
} | ||
}); | ||
$this->privateClient = new Client($this->host, $this->privateKey); | ||
} | ||
|
||
public function testGenerateTenantTokenWithSearchRulesOnly(): void | ||
{ | ||
$promise = $this->client->index('tenantToken')->addDocuments(self::DOCUMENTS); | ||
$this->client->waitForTask($promise['uid']); | ||
|
||
$token = $this->privateClient->generateTenantToken(['*']); | ||
$tokenClient = new Client($this->host, $token); | ||
$response = $tokenClient->index('tenantToken')->search(''); | ||
|
||
$this->assertArrayHasKey('hits', $response->toArray()); | ||
$this->assertCount(7, $response->getHits()); | ||
} | ||
|
||
public function testGenerateTenantTokenWithSearchRulesAsObject(): void | ||
{ | ||
$promise = $this->client->index('tenantToken')->addDocuments(self::DOCUMENTS); | ||
$this->client->waitForTask($promise['uid']); | ||
|
||
$token = $this->privateClient->generateTenantToken((object) ['*' => (object) []]); | ||
$tokenClient = new Client($this->host, $token); | ||
$response = $tokenClient->index('tenantToken')->search(''); | ||
|
||
$this->assertArrayHasKey('hits', $response->toArray()); | ||
$this->assertCount(7, $response->getHits()); | ||
} | ||
|
||
public function testGenerateTenantTokenWithFilter(): void | ||
{ | ||
$promise = $this->client->index('tenantToken')->addDocuments(self::DOCUMENTS); | ||
$this->client->waitForTask($promise['uid']); | ||
$promiseFromFilter = $this->client->index('tenantToken')->updateFilterableAttributes([ | ||
'id', | ||
]); | ||
$this->client->waitForTask($promiseFromFilter['uid']); | ||
|
||
$token = $this->privateClient->generateTenantToken((object) ['tenantToken' => (object) ['filter' => 'id > 10']]); | ||
$tokenClient = new Client($this->host, $token); | ||
$response = $tokenClient->index('tenantToken')->search(''); | ||
|
||
$this->assertArrayHasKey('hits', $response->toArray()); | ||
$this->assertCount(4, $response->getHits()); | ||
} | ||
|
||
public function testGenerateTenantTokenWithSearchRulesOnOneIndex(): void | ||
{ | ||
$this->createEmptyIndex('tenantTokenDuplicate'); | ||
|
||
$token = $this->privateClient->generateTenantToken(['tenantToken']); | ||
$tokenClient = new Client($this->host, $token); | ||
$response = $tokenClient->index('tenantToken')->search(''); | ||
|
||
$this->assertArrayHasKey('hits', $response->toArray()); | ||
$this->assertArrayHasKey('query', $response->toArray()); | ||
$this->expectException(ApiException::class); | ||
$tokenClient->index('tenantTokenDuplicate')->search(''); | ||
} | ||
|
||
public function testGenerateTenantTokenWithApiKey(): void | ||
{ | ||
$options = [ | ||
'apiKey' => $this->privateKey, | ||
]; | ||
|
||
$token = $this->client->generateTenantToken(['*'], $options); | ||
$tokenClient = new Client($this->host, $token); | ||
$response = $tokenClient->index('tenantToken')->search(''); | ||
|
||
$this->assertArrayHasKey('hits', $response->toArray()); | ||
} | ||
|
||
public function testGenerateTenantTokenWithExpiresAt(): void | ||
{ | ||
/* @phpstan-ignore-next-line */ | ||
$date = new DateTime(); | ||
$tomorrow = $date->modify('+1 day'); | ||
$options = [ | ||
'expiresAt' => $tomorrow, | ||
]; | ||
|
||
$token = $this->privateClient->generateTenantToken(['*'], $options); | ||
$tokenClient = new Client($this->host, $token); | ||
$response = $tokenClient->index('tenantToken')->search(''); | ||
|
||
$this->assertArrayHasKey('hits', $response->toArray()); | ||
} | ||
|
||
public function testGenerateTenantTokenWithSearchRulesEmptyArray(): void | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->privateClient->generateTenantToken([]); | ||
} | ||
|
||
public function testGenerateTenantTokenWithBadExpiresAt(): void | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
|
||
/* @phpstan-ignore-next-line */ | ||
$date = new DateTime(); | ||
$yesterday = $date->modify('-1 day'); | ||
$options = [ | ||
'expiresAt' => $yesterday, | ||
]; | ||
|
||
$this->privateClient->generateTenantToken(['*'], $options); | ||
} | ||
|
||
public function testGenerateTenantTokenWithNoApiKey(): void | ||
{ | ||
$client = new Client($this->host); | ||
|
||
$this->expectException(InvalidArgumentException::class); | ||
$client->generateTenantToken(['*']); | ||
} | ||
|
||
public function testGenerateTenantTokenWithEmptyApiKey(): void | ||
{ | ||
$client = new Client($this->host); | ||
|
||
$this->expectException(InvalidArgumentException::class); | ||
$client->generateTenantToken(['*'], ['apiKey' => '']); | ||
} | ||
} |
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