Skip to content

Commit

Permalink
Create Tenant Token Endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
alallema committed Mar 1, 2022
1 parent 8a67f8c commit 55c0e20
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 76 deletions.
4 changes: 3 additions & 1 deletion src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use MeiliSearch\Endpoints\Keys;
use MeiliSearch\Endpoints\Stats;
use MeiliSearch\Endpoints\Tasks;
use MeiliSearch\Endpoints\TenantToken;
use MeiliSearch\Endpoints\Version;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
Expand All @@ -35,14 +36,14 @@ class Client
private Stats $stats;
private Tasks $tasks;
private Dumps $dumps;
private TenantToken $tenantToken;

public function __construct(
string $url,
string $apiKey = null,
ClientInterface $httpClient = null,
RequestFactoryInterface $requestFactory = null
) {
$this->apiKey = $apiKey;
$this->http = new Http\Client($url, $apiKey, $httpClient, $requestFactory);
$this->index = new Indexes($this->http);
$this->health = new Health($this->http);
Expand All @@ -51,5 +52,6 @@ public function __construct(
$this->tasks = new Tasks($this->http);
$this->keys = new Keys($this->http);
$this->dumps = new Dumps($this->http);
$this->tenantToken = new TenantToken($this->http, $apiKey);
}
}
4 changes: 3 additions & 1 deletion src/Contracts/Endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ abstract class Endpoint
{
protected const PATH = '';
protected Http $http;
protected ?string $apiKey;

public function __construct(Http $http)
public function __construct(Http $http, ?string $apiKey = null)
{
$this->http = $http;
$this->apiKey = $apiKey;
}

public function show(): ?array
Expand Down
69 changes: 1 addition & 68 deletions src/Delegates/HandlesSystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@

namespace MeiliSearch\Delegates;

use DateTime;
use MeiliSearch\Exceptions\InvalidArgumentException;
use MeiliSearch\Http\Serialize\Json;

trait HandlesSystem
{
public function health(): ?array
Expand Down Expand Up @@ -36,71 +32,8 @@ public function stats(): array
return $this->stats->show();
}

public function base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

/**
* 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
{
// Validate every fields
if (!\array_key_exists('apiKey', $options) && null == $this->apiKey) {
throw InvalidArgumentException::emptyArgument('api key');
}
if (null == $searchRules) {
throw InvalidArgumentException::emptyArgument('search rules');
}
if (\array_key_exists('expiresAt', $options) && new DateTime() > $options['expiresAt']) {
throw InvalidArgumentException::dateIsExpired($options['expiresAt']);
}

$json = new Json();

// Standard JWT header for encryption with SHA256/HS256 algorithm
$header = [
'typ' => 'JWT',
'alg' => 'HS256',
];

if (!\array_key_exists('apiKey', $options)) {
$options['apiKey'] = $this->apiKey;
}

// Add the required fields to the 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;
return $this->tenantToken->generateTenantToken($searchRules, $options);
}
}
82 changes: 82 additions & 0 deletions src/Endpoints/TenantToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?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), '+/', '-_'), '=');
}

/**
* 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(object|array $searchRules, ?array $options = []): string
{
// Validate every fields
if ((!\array_key_exists('apiKey', $options) || '' == $options['apiKey']) && \is_null($this->apiKey)) {
throw InvalidArgumentException::emptyArgument('api key');
}
if (null == $searchRules) {
throw InvalidArgumentException::emptyArgument('search rules');
}
if (\array_key_exists('expiresAt', $options) && new DateTime() > $options['expiresAt']) {
throw InvalidArgumentException::dateIsExpired($options['expiresAt']);
}

$json = new Json();

// Standard JWT header for encryption with SHA256/HS256 algorithm
$header = [
'typ' => 'JWT',
'alg' => 'HS256',
];

if (!\array_key_exists('apiKey', $options)) {
$options['apiKey'] = $this->apiKey;
}

// 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;
}
}
56 changes: 50 additions & 6 deletions tests/Endpoints/TenantTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Tests\Endpoints;

/* @phpstan-ignore-next-line */
use Datetime;
use MeiliSearch\Client;
use MeiliSearch\Exceptions\ApiException;
Expand Down Expand Up @@ -47,6 +48,46 @@ public function testGenerateTenantTokenWithSearchRulesOnly(): void
$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->assertArrayHasKey('offset', $response->toArray());
$this->assertArrayHasKey('limit', $response->toArray());
$this->assertArrayHasKey('processingTimeMs', $response->toArray());
$this->assertArrayHasKey('query', $response->toArray());
$this->assertSame(7, $response->getNbHits());
$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->assertArrayHasKey('offset', $response->toArray());
$this->assertArrayHasKey('limit', $response->toArray());
$this->assertArrayHasKey('processingTimeMs', $response->toArray());
$this->assertArrayHasKey('query', $response->toArray());
$this->assertSame(4, $response->getNbHits());
$this->assertCount(4, $response->getHits());
}

public function testGenerateTenantTokenWithSearchRulesOnOneIndex(): void
{
$this->createEmptyIndex('tenantTokenDuplicate');
Expand Down Expand Up @@ -78,6 +119,7 @@ public function testGenerateTenantTokenWithApiKey(): void

public function testGenerateTenantTokenWithExpiresAt(): void
{
/* @phpstan-ignore-next-line */
$date = new DateTime();
$tomorrow = $date->modify('+1 day');
$options = [
Expand All @@ -94,12 +136,6 @@ public function testGenerateTenantTokenWithExpiresAt(): void
$this->assertArrayHasKey('query', $response->toArray());
}

public function testGenerateTenantTokenWithEmptySearchRules(): void
{
$this->expectException(InvalidArgumentException::class);
$token = $this->privateClient->generateTenantToken('');
}

public function testGenerateTenantTokenWithSearchRulesEmptyArray(): void
{
$this->expectException(InvalidArgumentException::class);
Expand All @@ -108,6 +144,7 @@ public function testGenerateTenantTokenWithSearchRulesEmptyArray(): void

public function testGenerateTenantTokenWithBadExpiresAt(): void
{
/* @phpstan-ignore-next-line */
$date = new DateTime();
$yesterday = $date->modify('-1 day');
$options = [
Expand All @@ -124,4 +161,11 @@ public function testGenerateTenantTokenWithNoApiKey(): void
$this->expectException(InvalidArgumentException::class);
$token = $client->generateTenantToken(['*']);
}

public function testGenerateTenantTokenWithEmptyApiKey(): void
{
$client = new Client($this->host);
$this->expectException(InvalidArgumentException::class);
$token = $client->generateTenantToken(['*'], ['apiKey' => '']);
}
}

0 comments on commit 55c0e20

Please sign in to comment.