From 57632e7a8c9a045f0e55b598bfc5bcf27cf5c3c4 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Wed, 3 Nov 2021 17:45:57 +0100 Subject: [PATCH 01/40] init specification --- text/XXXX-scoped-api-keys | 128 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 text/XXXX-scoped-api-keys diff --git a/text/XXXX-scoped-api-keys b/text/XXXX-scoped-api-keys new file mode 100644 index 00000000..6f5918cc --- /dev/null +++ b/text/XXXX-scoped-api-keys @@ -0,0 +1,128 @@ +- Title: Scoped API Keys +- Start Date: 2021-10-15 +- Specification PR: [#]() +- Discovery Issue: [#51](https://github.com/meilisearch/product/issues/51) + +# Scoped API Keys + +## 1. Functional Specification + +### I. Summary + +The SDKs can generate `Scoped API Keys` inheriting from a MeiliSearch's `API key` generated to force filters during a search for an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. + +### II. Motivation + +`Scoped API Keys` are introduced to solve multi-tenant use-case. By managing this, we reduce one of the last major deal-breakers that makes users not choose MeiliSearch as a solution despite all our advantages. + +Users regularly request Multi-Tenant API keys over time. Users today need to set up workarounds to meet this need. Some of them implement reverse-proxy or managed authentication systems like Hasura or Kong to filter what can and cannot be read. Others decide to use server code as a facade to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, less efficient, and requires necessary skills that not everyone has. + +### III. Glossary + +| Term | Definition | +|--------------------|------------| +| Master Key | This is the master key that allows to the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | +| API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the client code. | +| Scoped API Key | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | +| Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | + +### IV. Personas + +| Persona | Role | +|---------|------| +| Mark | Mark is a developer for a SaaS company. He will implement the code to communicate with MeiliSearch to solve technical/product needs. | +| UserX | UserX represents any end-user searching from the frontend interfaces provided by Anna and Mark's company. | + +### V. `Scoped API Key` Explanations + +#### Summary Key Points + +- `Scoped API keys` are generated from a MeiliSearch parent `API key` on the client's server code to resolve multi-tenancy use-case by restricting access to data within an index according to the criteria chosen by the team managing a MeiliSearch instance. +- These `Scoped API keys` can't be less restrictive than a parent `API key` and can only be used for the search action with a predefined forced filter field. +- These `Scoped API keys` are not stored and thus retrievable on MeiliSearch. This is why we highly advise setting an expiration time on that type of API key for security reasons. + +#### Solving Multi-Tenancy with `Scoped API Keys` + +![](https://i.imgur.com/J4jVe1n.png) + +Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that an end-user can only access his documents at search time. **His database contains many users and he hopes to have many more in the future.** + +When a user registers, the backend-side client code generates a `Scoped API Key` specifically for that end-user so he can only access his documents. + +A `filter` parameter is set to restrict the search for documents having a `user_id` attribute. This `filter` parameter is contained in the `Scoped API key` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` + +This `Scoped API Key` is generated from a parent `API key` used to cipher the `Scoped API Key`. On the MeiliSearch side, this permits checking permissions and authorizing the `UserX` search request using this `Scoped API Key`. + +--- + +#### Generating a `Scoped API Key` + +```javascript +const scopedApiKeyRestrictions = { + "indexesPolicy": { + "products": { + "filter": "user_id = 1" + }, + "reviews": { + "filter": "user_id = 1 AND published = true" + } + }, + "expiresIn": 3600 +} + +export const generateScopedApiKey = () => { + return (parentApiKey: string, restrictions: scopedApiKeyRestrictions): string => { + //hash the parentApiKey and keep a prefix of 4 chars of the hashed parentApiKey + const prefixKey = crypto + .createHmac('sha256', parentApiKey) + .digest('hex') + .substr(0, 4); + + //serialize restrictions (indexesPolicies object and expiresIn) + const queryParameters = serializeQueryParameters(restrictions); + + //create the secured part (parentApiKey + queryParameters) + const securedKey = crypto + .createHmac('sha256', parentApiKey) + .update(queryParameters) + .digest('hex'); + + //return the generated `Scoped Api Key` + return Buffer.from(prefixKey + securedKey + queryParameters).toString('base64'); + }; +}; +``` + +##### scopedApiKeyRestrictions + +The format allows defining specific enforced search filters for accessible indexes (these indexes must be defined in the parent `Api Key` used to generate the `Scoped API key` and have the search action). + +If the user does not want to define specific filters for each index accessible to the end-user, he can use the `*` index wildcard rule. + +A policy per index allows overriding the `"*"` behavior. + +The `scoped API Keys` also accept a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. + + +```javascript +const scopedApiKeyRestrictions = { + "indexesPolicy": { + "*": { //all search on indexes different than reviews will have the enforced filter `user_id` + "filter": "user_id = 1" + }, + "reviews": { + "filter": "user_id = 1 AND published = true" + } + }, + "expiresIn": null //No expiration time ⚠️ Is not recommended for security and quality of life reasons because the only way to revoke it is to delete the parent key +} +``` + +#### Validity + +`Scoped API Keys` expire or are revoked when the parent `API Key` is deleted or expires. + +## 3. Future Possibilities + +- Handle more search parameters restrictions. +- Extends `Scoped API Keys` to more than `search` action. \ No newline at end of file From dbfa3358ca974b1e141ed95e0491e3dfaab517dd Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Wed, 3 Nov 2021 17:48:39 +0100 Subject: [PATCH 02/40] update filename --- text/{XXXX-scoped-api-keys => 0089-scoped-api-keys.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{XXXX-scoped-api-keys => 0089-scoped-api-keys.md} (98%) diff --git a/text/XXXX-scoped-api-keys b/text/0089-scoped-api-keys.md similarity index 98% rename from text/XXXX-scoped-api-keys rename to text/0089-scoped-api-keys.md index 6f5918cc..54e31f30 100644 --- a/text/XXXX-scoped-api-keys +++ b/text/0089-scoped-api-keys.md @@ -1,6 +1,6 @@ - Title: Scoped API Keys - Start Date: 2021-10-15 -- Specification PR: [#]() +- Specification PR: [#89](https://github.com/meilisearch/specifications/pull/89) - Discovery Issue: [#51](https://github.com/meilisearch/product/issues/51) # Scoped API Keys From 53844f450e74587f22b55d6d45bcf05d4f1836d1 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Wed, 3 Nov 2021 17:51:49 +0100 Subject: [PATCH 03/40] update typo --- text/0089-scoped-api-keys.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index 54e31f30..f5eb5a57 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -9,7 +9,7 @@ ### I. Summary -The SDKs can generate `Scoped API Keys` inheriting from a MeiliSearch's `API key` generated to force filters during a search for an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. +The SDKs can generate `Scoped API Keys` inheriting from a MeiliSearch's `API key` generated to force filters during a search for an end-user. A `Scoped API Key` is generated on the user server-side code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. ### II. Motivation @@ -21,9 +21,9 @@ Users regularly request Multi-Tenant API keys over time. Users today need to set | Term | Definition | |--------------------|------------| -| Master Key | This is the master key that allows to the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | +| Master Key | This is the master key that allows the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | | API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the client code. | -| Scoped API Key | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | +| Scoped API Key | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | | Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | ### IV. Personas From cf70826603b2ebcdaa2cdab7d27dfd08ca6dbaba Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Wed, 3 Nov 2021 17:56:20 +0100 Subject: [PATCH 04/40] rephrase motivation --- text/0089-scoped-api-keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index f5eb5a57..60e0eec3 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -15,7 +15,7 @@ The SDKs can generate `Scoped API Keys` inheriting from a MeiliSearch's `API key `Scoped API Keys` are introduced to solve multi-tenant use-case. By managing this, we reduce one of the last major deal-breakers that makes users not choose MeiliSearch as a solution despite all our advantages. -Users regularly request Multi-Tenant API keys over time. Users today need to set up workarounds to meet this need. Some of them implement reverse-proxy or managed authentication systems like Hasura or Kong to filter what can and cannot be read. Others decide to use server code as a facade to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, less efficient, and requires necessary skills that not everyone has. +Users regularly request Multi-Tenant indexes. Users today need to set up workarounds to meet this need. Some of them implement reverse-proxy or managed authentication systems like Hasura or Kong to filter what can and cannot be read. Others decide to use server code as a facade to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, less efficient, and requires necessary skills that not everyone has. ### III. Glossary From c229ad5c95193c6f06cb35b752654f434cb6ea64 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Tue, 9 Nov 2021 10:53:59 +0100 Subject: [PATCH 05/40] rename master occurences by main --- text/0089-scoped-api-keys.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index 60e0eec3..2c203e42 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -21,8 +21,8 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro | Term | Definition | |--------------------|------------| -| Master Key | This is the master key that allows the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | -| API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the client code. | +| Main Key | This is the main key that allows the creation of other API keys. The main key is defined by the user when launching MeiliSearch. | +| API Key | API keys are stored and managed from the endpoint `/keys` by the main key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the client code. | | Scoped API Key | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | | Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | From f8efda54faeeff39eef70f6913a92dca169967f9 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Wed, 24 Nov 2021 13:17:56 +0100 Subject: [PATCH 06/40] replace mention of main by master --- text/0089-scoped-api-keys.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index 2c203e42..60e0eec3 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -21,8 +21,8 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro | Term | Definition | |--------------------|------------| -| Main Key | This is the main key that allows the creation of other API keys. The main key is defined by the user when launching MeiliSearch. | -| API Key | API keys are stored and managed from the endpoint `/keys` by the main key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the client code. | +| Master Key | This is the master key that allows the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | +| API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the client code. | | Scoped API Key | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | | Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | From 356ceeb1102089de6741acaa82d49a3dda5c3144 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Thu, 25 Nov 2021 17:09:17 +0100 Subject: [PATCH 07/40] Update text/0089-scoped-api-keys.md Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-scoped-api-keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index 60e0eec3..224647ab 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -49,7 +49,7 @@ Let's say that `Mark` is a developer for a SaaS platform. He would like to ensur When a user registers, the backend-side client code generates a `Scoped API Key` specifically for that end-user so he can only access his documents. -A `filter` parameter is set to restrict the search for documents having a `user_id` attribute. This `filter` parameter is contained in the `Scoped API key` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` +The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is contained in the `Scoped API key` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` This `Scoped API Key` is generated from a parent `API key` used to cipher the `Scoped API Key`. On the MeiliSearch side, this permits checking permissions and authorizing the `UserX` search request using this `Scoped API Key`. From 188ed2e4dce41e0811176cc676a2384b7a583c8b Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Thu, 25 Nov 2021 17:12:56 +0100 Subject: [PATCH 08/40] replace client code by frontend or backend --- text/0089-scoped-api-keys.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index 224647ab..baccd47a 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -22,7 +22,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro | Term | Definition | |--------------------|------------| | Master Key | This is the master key that allows the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | -| API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the client code. | +| API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the frontend/backend code. | | Scoped API Key | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | | Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | @@ -37,7 +37,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro #### Summary Key Points -- `Scoped API keys` are generated from a MeiliSearch parent `API key` on the client's server code to resolve multi-tenancy use-case by restricting access to data within an index according to the criteria chosen by the team managing a MeiliSearch instance. +- `Scoped API keys` are generated from a MeiliSearch parent `API key` on the user's backend code to resolve multi-tenancy use-case by restricting access to data within an index according to the criteria chosen by the team managing a MeiliSearch instance. - These `Scoped API keys` can't be less restrictive than a parent `API key` and can only be used for the search action with a predefined forced filter field. - These `Scoped API keys` are not stored and thus retrievable on MeiliSearch. This is why we highly advise setting an expiration time on that type of API key for security reasons. @@ -47,7 +47,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that an end-user can only access his documents at search time. **His database contains many users and he hopes to have many more in the future.** -When a user registers, the backend-side client code generates a `Scoped API Key` specifically for that end-user so he can only access his documents. +When a user registers, the backend code generates a `Scoped API Key` specifically for that end-user so he can only access his documents. The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is contained in the `Scoped API key` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` From dcc555aa07d2f19e284b38bdb22432277912f991 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 26 Nov 2021 10:43:51 +0100 Subject: [PATCH 09/40] Update text/0089-scoped-api-keys.md Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-scoped-api-keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index baccd47a..bc8048a2 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -13,7 +13,7 @@ The SDKs can generate `Scoped API Keys` inheriting from a MeiliSearch's `API key ### II. Motivation -`Scoped API Keys` are introduced to solve multi-tenant use-case. By managing this, we reduce one of the last major deal-breakers that makes users not choose MeiliSearch as a solution despite all our advantages. +`Scoped API Keys` are introduced to solve multi-tenant use-cases. By providing the management of API Keys, we remove one of the last major deal-breakers that makes users not choose MeiliSearch as a solution despite all our advantages. Users regularly request Multi-Tenant indexes. Users today need to set up workarounds to meet this need. Some of them implement reverse-proxy or managed authentication systems like Hasura or Kong to filter what can and cannot be read. Others decide to use server code as a facade to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, less efficient, and requires necessary skills that not everyone has. From 43ab0096d9486b835d786364a7a541e7a601fb2c Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 26 Nov 2021 10:44:02 +0100 Subject: [PATCH 10/40] Update text/0089-scoped-api-keys.md Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-scoped-api-keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index bc8048a2..f0522e85 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -51,7 +51,7 @@ When a user registers, the backend code generates a `Scoped API Key` specificall The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is contained in the `Scoped API key` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` -This `Scoped API Key` is generated from a parent `API key` used to cipher the `Scoped API Key`. On the MeiliSearch side, this permits checking permissions and authorizing the `UserX` search request using this `Scoped API Key`. +This `Scoped API Key` is generated from a parent `API key` used to cipher the `Scoped API Key`. On the MeiliSearch side, the payload of the `scoped API key` defines what data the user is allowed to retrieve, in order words, MeiliSearch grants authorization based on the permission defined in the Scoped API key payload. --- From 0ddfa010b998be372d43ca16cedc80861b88abce Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 26 Nov 2021 10:44:14 +0100 Subject: [PATCH 11/40] Update text/0089-scoped-api-keys.md Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-scoped-api-keys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index f0522e85..f30c7144 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -45,7 +45,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro ![](https://i.imgur.com/J4jVe1n.png) -Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that an end-user can only access his documents at search time. **His database contains many users and he hopes to have many more in the future.** +Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that an end-user can only access their documents at search time. **His database contains many users and he hopes to have many more in the future.** When a user registers, the backend code generates a `Scoped API Key` specifically for that end-user so he can only access his documents. From d5a9e2565f10cfbba5caab87f6db9cb366b4505d Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Thu, 25 Nov 2021 17:16:25 +0100 Subject: [PATCH 12/40] update javascript code sample for generateScopedApiKey method --- text/0089-scoped-api-keys.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/text/0089-scoped-api-keys.md b/text/0089-scoped-api-keys.md index f30c7144..03bacdbf 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-scoped-api-keys.md @@ -72,11 +72,8 @@ const scopedApiKeyRestrictions = { export const generateScopedApiKey = () => { return (parentApiKey: string, restrictions: scopedApiKeyRestrictions): string => { - //hash the parentApiKey and keep a prefix of 4 chars of the hashed parentApiKey - const prefixKey = crypto - .createHmac('sha256', parentApiKey) - .digest('hex') - .substr(0, 4); + //extract the 8 first characters of the parentApiKey + const prefix = parentApiKey.substring(0,8); //serialize restrictions (indexesPolicies object and expiresIn) const queryParameters = serializeQueryParameters(restrictions); @@ -88,7 +85,7 @@ export const generateScopedApiKey = () => { .digest('hex'); //return the generated `Scoped Api Key` - return Buffer.from(prefixKey + securedKey + queryParameters).toString('base64'); + return Buffer.from(prefix + securedKey + queryParameters).toString('base64'); }; }; ``` @@ -101,7 +98,7 @@ If the user does not want to define specific filters for each index accessible t A policy per index allows overriding the `"*"` behavior. -The `scoped API Keys` also accept a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. +`Scoped API Keys` also accept a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. ```javascript From ed36888be26d28a7773a8e6a07aa988e8876f9af Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 13 Dec 2021 10:46:13 +0100 Subject: [PATCH 13/40] Rename Scoped API Key to Tenant Token --- ...coped-api-keys.md => 0089-tenant-token.md} | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) rename text/{0089-scoped-api-keys.md => 0089-tenant-token.md} (55%) diff --git a/text/0089-scoped-api-keys.md b/text/0089-tenant-token.md similarity index 55% rename from text/0089-scoped-api-keys.md rename to text/0089-tenant-token.md index 03bacdbf..82eaa3f2 100644 --- a/text/0089-scoped-api-keys.md +++ b/text/0089-tenant-token.md @@ -1,19 +1,19 @@ -- Title: Scoped API Keys +- Title: Tenant Tokens - Start Date: 2021-10-15 - Specification PR: [#89](https://github.com/meilisearch/specifications/pull/89) - Discovery Issue: [#51](https://github.com/meilisearch/product/issues/51) -# Scoped API Keys +# Tenant Token ## 1. Functional Specification ### I. Summary -The SDKs can generate `Scoped API Keys` inheriting from a MeiliSearch's `API key` generated to force filters during a search for an end-user. A `Scoped API Key` is generated on the user server-side code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. +The SDKs can generate `Tenant tokens` inheriting from a MeiliSearch's `API key` generated to force filters during a search for an end-user. A `Tenant token` is generated on the user server-side code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. ### II. Motivation -`Scoped API Keys` are introduced to solve multi-tenant use-cases. By providing the management of API Keys, we remove one of the last major deal-breakers that makes users not choose MeiliSearch as a solution despite all our advantages. +`Tenant tokens` are introduced to solve multi-tenant use-cases. By providing the management of API Keys, we remove one of the last major deal-breakers that makes users not choose MeiliSearch as a solution despite all our advantages. Users regularly request Multi-Tenant indexes. Users today need to set up workarounds to meet this need. Some of them implement reverse-proxy or managed authentication systems like Hasura or Kong to filter what can and cannot be read. Others decide to use server code as a facade to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, less efficient, and requires necessary skills that not everyone has. @@ -23,7 +23,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro |--------------------|------------| | Master Key | This is the master key that allows the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | | API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the frontend/backend code. | -| Scoped API Key | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | +| Tenant Token | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | | Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | ### IV. Personas @@ -33,32 +33,32 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro | Mark | Mark is a developer for a SaaS company. He will implement the code to communicate with MeiliSearch to solve technical/product needs. | | UserX | UserX represents any end-user searching from the frontend interfaces provided by Anna and Mark's company. | -### V. `Scoped API Key` Explanations +### V. `Tenant Token` Explanations #### Summary Key Points -- `Scoped API keys` are generated from a MeiliSearch parent `API key` on the user's backend code to resolve multi-tenancy use-case by restricting access to data within an index according to the criteria chosen by the team managing a MeiliSearch instance. -- These `Scoped API keys` can't be less restrictive than a parent `API key` and can only be used for the search action with a predefined forced filter field. -- These `Scoped API keys` are not stored and thus retrievable on MeiliSearch. This is why we highly advise setting an expiration time on that type of API key for security reasons. +- `Tenant tokens` are generated from a MeiliSearch parent `API key` on the user's backend code to resolve multi-tenancy use-case by restricting access to data within an index according to the criteria chosen by the team managing a MeiliSearch instance. +- These `Tenant tokens` can't be less restrictive than a parent `API key` and can only be used for the search action with a predefined forced filter field. +- These `Tenant tokens` are not stored and thus retrievable on MeiliSearch. This is why we highly advise setting an expiration time on that type of API key for security reasons. -#### Solving Multi-Tenancy with `Scoped API Keys` +#### Solving Multi-Tenancy with `Tenant tokens` ![](https://i.imgur.com/J4jVe1n.png) Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that an end-user can only access their documents at search time. **His database contains many users and he hopes to have many more in the future.** -When a user registers, the backend code generates a `Scoped API Key` specifically for that end-user so he can only access his documents. +When a user registers, the backend code generates a `Tenant token` specifically for that end-user so he can only access his documents. -The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is contained in the `Scoped API key` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` +The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is contained in the `Tenant token` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` -This `Scoped API Key` is generated from a parent `API key` used to cipher the `Scoped API Key`. On the MeiliSearch side, the payload of the `scoped API key` defines what data the user is allowed to retrieve, in order words, MeiliSearch grants authorization based on the permission defined in the Scoped API key payload. +This `Tenant token` is generated from a parent `API key` used to cipher the `Tenant token`. On the MeiliSearch side, the payload of the `tenant token` defines what data the user is allowed to retrieve, in order words, MeiliSearch grants authorization based on the permission defined in the Tenant token payload. --- -#### Generating a `Scoped API Key` +#### Generating a `Tenant token` ```javascript -const scopedApiKeyRestrictions = { +const tenantTokenRestrictions = { "indexesPolicy": { "products": { "filter": "user_id = 1" @@ -70,8 +70,8 @@ const scopedApiKeyRestrictions = { "expiresIn": 3600 } -export const generateScopedApiKey = () => { - return (parentApiKey: string, restrictions: scopedApiKeyRestrictions): string => { +export const generateTenantToken = () => { + return (parentApiKey: string, restrictions: tenantTokenRestrictions): string => { //extract the 8 first characters of the parentApiKey const prefix = parentApiKey.substring(0,8); @@ -84,25 +84,25 @@ export const generateScopedApiKey = () => { .update(queryParameters) .digest('hex'); - //return the generated `Scoped Api Key` + //return the generated `Tenant Token` return Buffer.from(prefix + securedKey + queryParameters).toString('base64'); }; }; ``` -##### scopedApiKeyRestrictions +##### tenantTokenRestrictions -The format allows defining specific enforced search filters for accessible indexes (these indexes must be defined in the parent `Api Key` used to generate the `Scoped API key` and have the search action). +The format allows defining specific enforced search filters for accessible indexes (these indexes must be defined in the parent `Api Key` used to generate the `Tenant Token` and have the search action). If the user does not want to define specific filters for each index accessible to the end-user, he can use the `*` index wildcard rule. A policy per index allows overriding the `"*"` behavior. -`Scoped API Keys` also accept a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. +A `Tenant token` also accept a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. ```javascript -const scopedApiKeyRestrictions = { +const tenantTokenRestrictions = { "indexesPolicy": { "*": { //all search on indexes different than reviews will have the enforced filter `user_id` "filter": "user_id = 1" @@ -117,9 +117,9 @@ const scopedApiKeyRestrictions = { #### Validity -`Scoped API Keys` expire or are revoked when the parent `API Key` is deleted or expires. +`Tenant tokens` expire or are revoked when the parent `API Key` is deleted or expires. ## 3. Future Possibilities - Handle more search parameters restrictions. -- Extends `Scoped API Keys` to more than `search` action. \ No newline at end of file +- Extends `Tenant Token` to more than `search` action. \ No newline at end of file From ef094e661a17072ae2eceb44f69daa86c755cfe6 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 13 Dec 2021 13:23:56 +0100 Subject: [PATCH 14/40] Apply suggestions from code review Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-tenant-token.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/text/0089-tenant-token.md b/text/0089-tenant-token.md index 82eaa3f2..11a9d302 100644 --- a/text/0089-tenant-token.md +++ b/text/0089-tenant-token.md @@ -23,7 +23,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro |--------------------|------------| | Master Key | This is the master key that allows the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | | API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the frontend/backend code. | -| Tenant Token | These keys are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | +| Tenant Token | These tokens are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | | Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | ### IV. Personas @@ -37,9 +37,9 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro #### Summary Key Points -- `Tenant tokens` are generated from a MeiliSearch parent `API key` on the user's backend code to resolve multi-tenancy use-case by restricting access to data within an index according to the criteria chosen by the team managing a MeiliSearch instance. -- These `Tenant tokens` can't be less restrictive than a parent `API key` and can only be used for the search action with a predefined forced filter field. -- These `Tenant tokens` are not stored and thus retrievable on MeiliSearch. This is why we highly advise setting an expiration time on that type of API key for security reasons. +- `Tenant tokens` are generated from a MeiliSearch parent `API key` on the user's backend code. They are meant to resolve multi-tenancy by restricting access to data within an index according to the criteria chosen by the team managing the MeiliSearch instance. +- `Tenant tokens` cannot be less restrictive than their respective parent `API key` and can only be used for searching with a predefined forced filter field. +- `Tenant tokens` are not stored and thus not retrievable on MeiliSearch. This is why we highly advise setting an expiration time on them for security reasons. #### Solving Multi-Tenancy with `Tenant tokens` @@ -47,7 +47,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that an end-user can only access their documents at search time. **His database contains many users and he hopes to have many more in the future.** -When a user registers, the backend code generates a `Tenant token` specifically for that end-user so he can only access his documents. +When a user registers, the backend code generates a `Tenant token` specifically for that end-user so they can only access their documents. The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is contained in the `Tenant token` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` From ace7dce5b59e8f119101c4f7e42771716ba59ea0 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 3 Jan 2022 14:51:33 +0100 Subject: [PATCH 15/40] precise message from reviews --- text/0089-tenant-token.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0089-tenant-token.md b/text/0089-tenant-token.md index 11a9d302..a0ada28a 100644 --- a/text/0089-tenant-token.md +++ b/text/0089-tenant-token.md @@ -45,7 +45,7 @@ Users regularly request Multi-Tenant indexes. Users today need to set up workaro ![](https://i.imgur.com/J4jVe1n.png) -Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that an end-user can only access their documents at search time. **His database contains many users and he hopes to have many more in the future.** +Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that end-user can only access their documents at search time. **His database contains many users and he hopes to have many more in the future.** When a user registers, the backend code generates a `Tenant token` specifically for that end-user so they can only access their documents. @@ -94,11 +94,11 @@ export const generateTenantToken = () => { The format allows defining specific enforced search filters for accessible indexes (these indexes must be defined in the parent `Api Key` used to generate the `Tenant Token` and have the search action). -If the user does not want to define specific filters for each index accessible to the end-user, he can use the `*` index wildcard rule. +If the user does not want to define specific filters for each index accessible to the parent API Key, he can use the `*` index wildcard rule. A policy per index allows overriding the `"*"` behavior. -A `Tenant token` also accept a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. +A `Tenant token` also accepts a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. ```javascript From 1db05d86a287c1fb93f72b5f23a7d554b224fcdf Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Thu, 13 Jan 2022 18:45:44 +0100 Subject: [PATCH 16/40] Add JWT part --- text/0089-tenant-token.md | 170 +++++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 77 deletions(-) diff --git a/text/0089-tenant-token.md b/text/0089-tenant-token.md index a0ada28a..5fadc204 100644 --- a/text/0089-tenant-token.md +++ b/text/0089-tenant-token.md @@ -7,119 +7,135 @@ ## 1. Functional Specification -### I. Summary +### 1.1 Summary -The SDKs can generate `Tenant tokens` inheriting from a MeiliSearch's `API key` generated to force filters during a search for an end-user. A `Tenant token` is generated on the user server-side code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. +A `Tenant token` is generated on the user server-side code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. -### II. Motivation +A Tenant Token is a JWT containing the information necessary for MeiliSearch to verify it and extract permission/rules to apply it to the end user's search. -`Tenant tokens` are introduced to solve multi-tenant use-cases. By providing the management of API Keys, we remove one of the last major deal-breakers that makes users not choose MeiliSearch as a solution despite all our advantages. +#### 1.1.1 Summary Key Points -Users regularly request Multi-Tenant indexes. Users today need to set up workarounds to meet this need. Some of them implement reverse-proxy or managed authentication systems like Hasura or Kong to filter what can and cannot be read. Others decide to use server code as a facade to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, less efficient, and requires necessary skills that not everyone has. +- `Tenant tokens` are meant to solve multi-tenancy in an elegant way. +- `Tenant tokens` contains rules that ensure that a `Tenant token` holder (e.g an end-user), only access to documents matching rules chosen at the `tenant token` creation. +- `Tenant tokens` are JWTs. +- `Tenant tokens` are signed from a MeiliSearch `API key` on the user's backend code. +- `Tenant tokens` cannot be less restrictive than the signing `API key` and can only be used for searching. It mean that a Tenant Token cannot search within more indexes than the API Key that signed that token. +- `Tenant tokens` can have different filters for each index accessible by the signing API key. These filters rule per index are described in an `indexesPolicy` json object +- `Tenant tokens` are not stored and thus not retrievable on MeiliSearch. -### III. Glossary +### 1.2 Motivation -| Term | Definition | -|--------------------|------------| -| Master Key | This is the master key that allows the creation of other API keys. The master key is defined by the user when launching MeiliSearch. | -| API Key | API keys are stored and managed from the endpoint `/keys` by the master key holder. These are the keys used by the technical teams to interact with MeiliSearch at the level of the frontend/backend code. | -| Tenant Token | These tokens are not stored and managed by a MeiliSearch instance. They are generated for each end-user by the backend code from a MeiliSearch API Key. They are used by the end-users to only search the documents belonging to them. | -| Multi-Tenancy | By multi-tenancy, we mean that an end-user only accesses data belonging to him within an index shared with other end-users. | +`Tenant tokens` are introduced to solve multi-tenant use-cases. -### IV. Personas +Users today need to set up workarounds to have multi-tenant indexes. Most of the time, they have to use server code as a frontend to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, to implement, and the performance is degraded because the frontend code does not communicate directly with MeiliSearch. -| Persona | Role | -|---------|------| -| Mark | Mark is a developer for a SaaS company. He will implement the code to communicate with MeiliSearch to solve technical/product needs. | -| UserX | UserX represents any end-user searching from the frontend interfaces provided by Anna and Mark's company. | +### 1.3 `Tenant Token` Explanations -### V. `Tenant Token` Explanations +#### 1.3.1 Example: Solving Multi-Tenancy with `Tenant tokens` -#### Summary Key Points +![](https://user-images.githubusercontent.com/3692335/149373631-de6f3c5f-a514-4c8d-b018-ee09ccaeaf4d.png) -- `Tenant tokens` are generated from a MeiliSearch parent `API key` on the user's backend code. They are meant to resolve multi-tenancy by restricting access to data within an index according to the criteria chosen by the team managing the MeiliSearch instance. -- `Tenant tokens` cannot be less restrictive than their respective parent `API key` and can only be used for searching with a predefined forced filter field. -- `Tenant tokens` are not stored and thus not retrievable on MeiliSearch. This is why we highly advise setting an expiration time on them for security reasons. +`Mark` is a developer for a SaaS platform. He would like to ensure that every end-user can only access their documents at search time. -#### Solving Multi-Tenancy with `Tenant tokens` +When an end-user registers, the Mark's backend code generates a `Tenant token` for that end-user so they can only access their documents. -![](https://i.imgur.com/J4jVe1n.png) +This tenant-token is signed with a MeiliSearch API Key so that MeiliSearch can ensure that the token is valid when search requests have to be authorized. -Let's say that `Mark` is a developer for a SaaS platform. He would like to ensure that end-user can only access their documents at search time. **His database contains many users and he hopes to have many more in the future.** +The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is part of the Tenant Token payload and cannot be modified. -When a user registers, the backend code generates a `Tenant token` specifically for that end-user so they can only access their documents. +`filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` -The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is contained in the `Tenant token` payload and cannot be modified during the search by the end-user making the request. `filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` +On the MeiliSearch side, the payload of the `tenant token` defines what data the user is allowed to retrieve, in order words, MeiliSearch grants authorization based on the permission defined in the Tenant token payload and force the `filter` field on top of any other filters that might have been set on the front-end; -This `Tenant token` is generated from a parent `API key` used to cipher the `Tenant token`. On the MeiliSearch side, the payload of the `tenant token` defines what data the user is allowed to retrieve, in order words, MeiliSearch grants authorization based on the permission defined in the Tenant token payload. +## 2. Technical Details ---- +### 2.1 `Tenant Token` details -#### Generating a `Tenant token` +A Tenant Token generated for MeiliSearch must respect several conditions. A Tenant Token is a JWT. -```javascript -const tenantTokenRestrictions = { - "indexesPolicy": { - "products": { - "filter": "user_id = 1" - }, - "reviews": { - "filter": "user_id = 1 AND published = true" - } - }, - "expiresIn": 3600 +#### 2.1.1 Header: Algorithm and token type + +Tenant Token MUST be signed with the combination of `HMAC + SHA256`. + +e.g. + +```json +{ + "alg": "HS256", + "typ": "JWT" } +``` -export const generateTenantToken = () => { - return (parentApiKey: string, restrictions: tenantTokenRestrictions): string => { - //extract the 8 first characters of the parentApiKey - const prefix = parentApiKey.substring(0,8); +#### 2.1.2 Payload: Data - //serialize restrictions (indexesPolicies object and expiresIn) - const queryParameters = serializeQueryParameters(restrictions); +MeiliSearch needs information within the token to check its validity and use it to perform end-user requests. - //create the secured part (parentApiKey + queryParameters) - const securedKey = crypto - .createHmac('sha256', parentApiKey) - .update(queryParameters) - .digest('hex'); +This information can be separated into two parts, on one hand, the information allows to check the validity of the Tenant Token, on the other hand, the business logic information allows to have a token with pre-defined search parameters / rules for the end user's search. - //return the generated `Tenant Token` - return Buffer.from(prefix + securedKey + queryParameters).toString('base64'); - }; -}; -``` +##### 2.1.2.1 Validity related -##### tenantTokenRestrictions +| Fields | Description | Comments | +| -------- | -------- | -------- | +| `iss` (Issuer claim) | Must contain the first 8 characters of the signing `MeiliSearch API key` used to generate the JWT | | | +| `exp` (Expiration Time claim) | A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. | This value is optional. | -The format allows defining specific enforced search filters for accessible indexes (these indexes must be defined in the parent `Api Key` used to generate the `Tenant Token` and have the search action). +##### 2.1.2.2 Business logic related -If the user does not want to define specific filters for each index accessible to the parent API Key, he can use the `*` index wildcard rule. +| Fields | Description | Comments | +| -------- | -------- | -------- | +| `indexesPolicy` | This field contains descriptions of the rules applied for search queries performed with the JWT for all and specific indexes accessible by the signing API key used to generate the JWT. | Let's say an index uses a different field to separate documents belonging to one user from another one, but another index that needs to be accessible uses a different field in its schema. Being able to define specific rules per accessible index avoids having to generate several tenant tokens for an end-user.| -A policy per index allows overriding the `"*"` behavior. +##### 2.1.2.3 Payload example -A `Tenant token` also accepts a number of seconds in the `expiresIn` field until it expires. This field should be mandatory and explicitly set to `null` if no expiration time is needed. +Given a MeiliSearch API Key used to sign the JWT from the user backend code. Here is a valid payload data for a JWT. +e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d1631b54f3426fa8b2595` -```javascript -const tenantTokenRestrictions = { - "indexesPolicy": { - "*": { //all search on indexes different than reviews will have the enforced filter `user_id` - "filter": "user_id = 1" - }, - "reviews": { - "filter": "user_id = 1 AND published = true" - } - }, - "expiresIn": null //No expiration time ⚠️ Is not recommended for security and quality of life reasons because the only way to revoke it is to delete the parent key +```json +{ + "iss": "rkDxFUHd", <- The first 8 characters of the signing API Key + "exp": 1641835850, <- An expiration date in seconds from 1970-01-01T00:00:00Z UTC + "indexesPolicy": { <- The indexesPolicy Json Object. + "*": { + "filter": "user_id = 1" + } + } } ``` -#### Validity +> In this example, `indexesPolicy` allows to specify, that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), this filter will be applied on all search requests. + +### 2.3 `Tenant Token` Javascript Code Sample + +```javascript -`Tenant tokens` expire or are revoked when the parent `API Key` is deleted or expires. +meiliSearchApikey = 'rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d1631b54f3426fa8b2595'; + +header = { + "alg": "HS256", + "typ": "JWT" +} + +base64Header = base64Encode(header) + +payload = { + "iss": meiliSearchApiKey.substr(8), + "exp": 1641835850, + "indexesPolicy": { + "*": { + "filter": "user_id = 1" + } + } +} + +base64Payload = base64Encode(payload) + +signature = HS256(base64Header + '.' + base64Payload, meiliSearchApiKey) + +TenantToken = base64Header + '.' + base64Payload + '.' + signature +``` ## 3. Future Possibilities -- Handle more search parameters restrictions. -- Extends `Tenant Token` to more than `search` action. \ No newline at end of file +- Handle more signing method on MeiliSearch side. +- Handle more search parameters restictions in `indexesPolicy`. \ No newline at end of file From 2a932364e2889ec6c37beb72259d0b1436ca2421 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 14 Jan 2022 12:17:44 +0100 Subject: [PATCH 17/40] Rename specification file --- text/{0089-tenant-token.md => 0089-tenant-tokens.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename text/{0089-tenant-token.md => 0089-tenant-tokens.md} (100%) diff --git a/text/0089-tenant-token.md b/text/0089-tenant-tokens.md similarity index 100% rename from text/0089-tenant-token.md rename to text/0089-tenant-tokens.md From d8da26d705b49cb5805b7b7d573f6d549a70b8ff Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 14 Jan 2022 12:47:54 +0100 Subject: [PATCH 18/40] Update specification texts --- text/0089-tenant-tokens.md | 44 ++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 5fadc204..ca075486 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -9,25 +9,25 @@ ### 1.1 Summary -A `Tenant token` is generated on the user server-side code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. +A `Tenant token` is generated on the user code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request given enforced rules specified from the user business logic. A Tenant Token is a JWT containing the information necessary for MeiliSearch to verify it and extract permission/rules to apply it to the end user's search. #### 1.1.1 Summary Key Points -- `Tenant tokens` are meant to solve multi-tenancy in an elegant way. -- `Tenant tokens` contains rules that ensure that a `Tenant token` holder (e.g an end-user), only access to documents matching rules chosen at the `tenant token` creation. -- `Tenant tokens` are JWTs. -- `Tenant tokens` are signed from a MeiliSearch `API key` on the user's backend code. -- `Tenant tokens` cannot be less restrictive than the signing `API key` and can only be used for searching. It mean that a Tenant Token cannot search within more indexes than the API Key that signed that token. -- `Tenant tokens` can have different filters for each index accessible by the signing API key. These filters rule per index are described in an `indexesPolicy` json object -- `Tenant tokens` are not stored and thus not retrievable on MeiliSearch. +- `Tenant tokens` are JWTs generated on the user side. Thus not stored or retrievable on MeiliSearch side. +- `Tenant tokens` contain rules that ensure that a `Tenant token` holder (e.g an end-user) only has access documents matching rules chosen at the `tenant token` creation. +- `Tenant tokens` are signed from a MeiliSearch `API key` on the user's code. +- `Tenant tokens` cannot be less restrictive than the signing `API key` and can only be used for searching. A Tenant Token cannot search within more indexes than the API Key that signed that Tenant Token. +- `Tenant tokens` can have different rules for each index accessible by the signing API key. These filters rule per index are described in an `indexesPolicy` json object. +- The only rule at the moment is the search parameter `filter`. Other rules may be added in the future. +- When a request is made with the Tenant Token, MeiliSearch checks if this Tenant Token is authorized to make the request and in this case injects the rules at search time. ### 1.2 Motivation `Tenant tokens` are introduced to solve multi-tenant use-cases. -Users today need to set up workarounds to have multi-tenant indexes. Most of the time, they have to use server code as a frontend to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, to implement, and the performance is degraded because the frontend code does not communicate directly with MeiliSearch. +Users today need to set up workarounds to have multi-tenant indexes. Most of the time, they have to use server code to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, to implement, and the performance is degraded because the frontend code does not communicate directly with MeiliSearch. ### 1.3 `Tenant Token` Explanations @@ -39,19 +39,17 @@ Users today need to set up workarounds to have multi-tenant indexes. Most of the When an end-user registers, the Mark's backend code generates a `Tenant token` for that end-user so they can only access their documents. -This tenant-token is signed with a MeiliSearch API Key so that MeiliSearch can ensure that the token is valid when search requests have to be authorized. +This tenant-token is signed with a MeiliSearch API Key so that MeiliSearch can ensure that the tenant-token has been generated from a known entity. -The `filter` parameter is set to restrict the search for documents having an `user_id` attribute. This `filter` parameter is part of the Tenant Token payload and cannot be modified. +MeiliSearch check if the Tenant Token is authorized to make the search request. -`filter` can be made of any valid filters. e.g. `user_id = 10 and category = Romantic` - -On the MeiliSearch side, the payload of the `tenant token` defines what data the user is allowed to retrieve, in order words, MeiliSearch grants authorization based on the permission defined in the Tenant token payload and force the `filter` field on top of any other filters that might have been set on the front-end; +Then MeiliSearch extract the Tenant Token's rules to apply for the search request. ## 2. Technical Details ### 2.1 `Tenant Token` details -A Tenant Token generated for MeiliSearch must respect several conditions. A Tenant Token is a JWT. +A Tenant Token generated for MeiliSearch must respect several conditions. #### 2.1.1 Header: Algorithm and token type @@ -68,9 +66,9 @@ e.g. #### 2.1.2 Payload: Data -MeiliSearch needs information within the token to check its validity and use it to perform end-user requests. +MeiliSearch needs information within the tenant token to check its validity and use it to authorize and perform end-user requests. -This information can be separated into two parts, on one hand, the information allows to check the validity of the Tenant Token, on the other hand, the business logic information allows to have a token with pre-defined search parameters / rules for the end user's search. +This information can be separated into two parts, on one hand, the information allows to check the validity of the Tenant Token, on the other hand, the business logic information allows to apply search parameters / rules for the end user's search request. ##### 2.1.2.1 Validity related @@ -83,11 +81,11 @@ This information can be separated into two parts, on one hand, the information a | Fields | Description | Comments | | -------- | -------- | -------- | -| `indexesPolicy` | This field contains descriptions of the rules applied for search queries performed with the JWT for all and specific indexes accessible by the signing API key used to generate the JWT. | Let's say an index uses a different field to separate documents belonging to one user from another one, but another index that needs to be accessible uses a different field in its schema. Being able to define specific rules per accessible index avoids having to generate several tenant tokens for an end-user.| +| `indexesPolicy` | This JSON object contains rules description to apply for search queries performed with the JWT depending the searched index. | Let's say an index uses a field to separate documents belonging to one user from another one, but another index needs to separate belonging using a different field in its schema. Defining specific rules per accessible index avoids having to generate several tenant tokens for an end-user. | ##### 2.1.2.3 Payload example -Given a MeiliSearch API Key used to sign the JWT from the user backend code. Here is a valid payload data for a JWT. +Given a MeiliSearch API Key used to sign the JWT from the user code. Here is an example of a valid payload for a tenant token. e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d1631b54f3426fa8b2595` @@ -105,7 +103,7 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 > In this example, `indexesPolicy` allows to specify, that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), this filter will be applied on all search requests. -### 2.3 `Tenant Token` Javascript Code Sample +### 2.2 `Tenant Token` Javascript Code Sample ```javascript @@ -119,7 +117,7 @@ header = { base64Header = base64Encode(header) payload = { - "iss": meiliSearchApiKey.substr(8), + "iss": meiliSearchApiKey.slice(0,8), "exp": 1641835850, "indexesPolicy": { "*": { @@ -137,5 +135,5 @@ TenantToken = base64Header + '.' + base64Payload + '.' + signature ## 3. Future Possibilities -- Handle more signing method on MeiliSearch side. -- Handle more search parameters restictions in `indexesPolicy`. \ No newline at end of file +- Handle more signing method for the Tenant Token. +- Handle more search parameters restrictions in `indexesPolicy`. \ No newline at end of file From aa77a671a4097292ebb560f308d842e63dddeb53 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 14 Jan 2022 13:23:06 +0100 Subject: [PATCH 19/40] Add examples for indexesPolicy --- text/0089-tenant-tokens.md | 106 +++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index ca075486..56f4600a 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -103,6 +103,112 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 > In this example, `indexesPolicy` allows to specify, that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), this filter will be applied on all search requests. +##### 2.1.2.4 `iss` field + +##### 2.1.2.5 `exp` field + +##### 2.1.2.6 `indexesPolicy` object + +`indexesPolicy` is a description of the possible rules for each index. + +Here are some valid examples in an attempt to cover all possible use cases. + +--- + +> In this case, all indexes searchable from the signing API Key will be searchable by the tenant token without specific rules. + +```json +{ + "indexesPolicy": {} +} +``` + +is equivalent to + +```json +{ + "indexesPolicy": { + "*": {} + } +} +``` + +--- + +> In this case, all indexes searchable from the signing API Key will be searchable by the tenant token and MeiliSearch will apply the filter definition before applying the search parameters added by the end user. + +```json +{ + "indexesPolicy": { + "*": { + "filter": "user_id = 1" + } + } +} +``` + +--- + +> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index without applying specific rules. + +```json +{ + "indexesPolicy": { + "medical_records": {} + } +} +``` + +--- + +> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index and specific rules will be applied at search time. + +```json +{ + "indexesPolicy": { + "medical_records": { + "filter": "user_id = 1" + } + } +} +``` + +--- + +> In this case, if the medical_records and medical_appointments indexes are searchable from the signing API Key, the tenant token can only search in those indexes and will apply specific rules given the searched index. + +```json +{ + "indexesPolicy": { + "medical_records": { + "filter": "user_id = 1" + }, + "medical_appointments": { + "filter": "user_id = 1 AND accepted = true" + } + } +} +``` + +--- + +> In this case, if all indexes searchable from the signing API Key will be searchable and the rules under `*` will be applied at search time. The medical_appointments index policy will replace the `*` rules specifically when this index is searched. + +```json +{ + "indexesPolicy": { + "*": { + "filter": "user_id = 1" + }, + "medical_appointments": { + "filter": "user_id = 1 AND accepted = true" + } + } +} +``` + +--- + ### 2.2 `Tenant Token` Javascript Code Sample ```javascript From 950286788a5c01438e0918710e9254960ccc6972 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 14 Jan 2022 13:25:52 +0100 Subject: [PATCH 20/40] Update indexesPolicy examples texts --- text/0089-tenant-tokens.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 56f4600a..34bc46e6 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -149,7 +149,7 @@ is equivalent to --- -> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index without applying specific rules. +> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index without applying specific rules before applying the search parameters added by the end user. ```json { @@ -161,7 +161,7 @@ is equivalent to --- -> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index and specific rules will be applied at search time. +> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index and specific rules will be applied at search time before applying the search parameters added by the end user. ```json { @@ -175,7 +175,7 @@ is equivalent to --- -> In this case, if the medical_records and medical_appointments indexes are searchable from the signing API Key, the tenant token can only search in those indexes and will apply specific rules given the searched index. +> In this case, if the medical_records and medical_appointments indexes are searchable from the signing API Key, the tenant token can only search in those indexes and will apply specific rules given the searched index before applying the search parameters added by the end user. ```json { From 1d99c31bfb7730e51cd81f0c68f67e078c776609 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 14 Jan 2022 13:27:04 +0100 Subject: [PATCH 21/40] Update indexesPolicy examples texts --- text/0089-tenant-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 34bc46e6..8fb3b88e 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -192,7 +192,7 @@ is equivalent to --- -> In this case, if all indexes searchable from the signing API Key will be searchable and the rules under `*` will be applied at search time. The medical_appointments index policy will replace the `*` rules specifically when this index is searched. +> In this case, all searchable indexes from the signing API Key will be searchable and the rules under `*` will be applied at search time. The medical_appointments index policy will replace the `*` rules specifically when this index is searched by those specified for that index. ```json { From dfe2882948e0c1d11b6aa225df0731cdff993c27 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 14 Jan 2022 13:28:31 +0100 Subject: [PATCH 22/40] Update indexesPolicy examples texts --- text/0089-tenant-tokens.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 8fb3b88e..d34373e0 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -107,7 +107,7 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 ##### 2.1.2.5 `exp` field -##### 2.1.2.6 `indexesPolicy` object +##### 2.1.2.6 `indexesPolicy` JSON object `indexesPolicy` is a description of the possible rules for each index. @@ -115,7 +115,7 @@ Here are some valid examples in an attempt to cover all possible use cases. --- -> In this case, all indexes searchable from the signing API Key will be searchable by the tenant token without specific rules. +> In this case, all searchable indexes from the signing API Key will be searchable by the tenant token without specific rules. ```json { @@ -135,7 +135,7 @@ is equivalent to --- -> In this case, all indexes searchable from the signing API Key will be searchable by the tenant token and MeiliSearch will apply the filter definition before applying the search parameters added by the end user. +> In this case, all searchable indexes from the signing API Key will be searchable by the tenant token and MeiliSearch will apply the filter definition before applying the search parameters added by the end user. ```json { From 4a88c3a1c26bd593ab31464410c640ef571ee5a4 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Fri, 14 Jan 2022 13:55:31 +0100 Subject: [PATCH 23/40] Add a multi-tenant definition and tenant examples for MeiliSearch --- text/0089-tenant-tokens.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index d34373e0..f3d9e2f7 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -25,7 +25,9 @@ A Tenant Token is a JWT containing the information necessary for MeiliSearch to ### 1.2 Motivation -`Tenant tokens` are introduced to solve multi-tenant use-cases. +`Tenant tokens` are introduced to solve multi-tenant indexes use-case. + +> Multi-Tenant Indexes Definition: It is an index that stores documents that may belong to different tenants. In our case, a tenant within an index can be a user or a company, etc.. In general, the data of one tenant should not be accessible by other tenants. Users today need to set up workarounds to have multi-tenant indexes. Most of the time, they have to use server code to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, to implement, and the performance is degraded because the frontend code does not communicate directly with MeiliSearch. From d955b54fdc175fee5a827658362e641a2d8adfa3 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Tue, 18 Jan 2022 12:09:25 +0100 Subject: [PATCH 24/40] Update text/0089-tenant-tokens.md Co-authored-by: Tommy <68053732+dichotommy@users.noreply.github.com> --- text/0089-tenant-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index f3d9e2f7..9c65d140 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -39,7 +39,7 @@ Users today need to set up workarounds to have multi-tenant indexes. Most of the `Mark` is a developer for a SaaS platform. He would like to ensure that every end-user can only access their documents at search time. -When an end-user registers, the Mark's backend code generates a `Tenant token` for that end-user so they can only access their documents. +When an end-user registers, Mark's backend code generates a `Tenant token` for that end-user so they can only access their documents. This tenant-token is signed with a MeiliSearch API Key so that MeiliSearch can ensure that the tenant-token has been generated from a known entity. From 2669521368b505e4a01cdd71f318db9ef53e7621 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Tue, 18 Jan 2022 12:09:33 +0100 Subject: [PATCH 25/40] Update text/0089-tenant-tokens.md Co-authored-by: Tommy <68053732+dichotommy@users.noreply.github.com> --- text/0089-tenant-tokens.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 9c65d140..5e500d41 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -43,9 +43,9 @@ When an end-user registers, Mark's backend code generates a `Tenant token` for t This tenant-token is signed with a MeiliSearch API Key so that MeiliSearch can ensure that the tenant-token has been generated from a known entity. -MeiliSearch check if the Tenant Token is authorized to make the search request. +MeiliSearch checks if the Tenant Token is authorized to make the search request. -Then MeiliSearch extract the Tenant Token's rules to apply for the search request. +Then MeiliSearch extracts the Tenant Token's rules to apply for the search request. ## 2. Technical Details From bc52bdd7b888e33a87177db18faaeb8b51a97ecd Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 12:59:27 +0100 Subject: [PATCH 26/40] Add array format for indexesPolicy and rename iss to apiKeyPrefix --- text/0089-tenant-tokens.md | 41 ++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 5e500d41..8db1352d 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -74,16 +74,16 @@ This information can be separated into two parts, on one hand, the information a ##### 2.1.2.1 Validity related -| Fields | Description | Comments | -| -------- | -------- | -------- | -| `iss` (Issuer claim) | Must contain the first 8 characters of the signing `MeiliSearch API key` used to generate the JWT | | | -| `exp` (Expiration Time claim) | A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. | This value is optional. | +| Fields | Required? | Description | Comments | +| -------- |----------- | ----------- | -------- | +| `apiKeyPrefix` (Custom claim) | Required | Must contain the first 8 characters of the signing `MeiliSearch API key` used to generate the JWT | | +| `exp` (Expiration Time claim) | Optional | A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time. | If the signing API key expires, the Tenant Token also expires. Thus said, the `exp` can't be greater than the expiration date of the signing API key. | ##### 2.1.2.2 Business logic related -| Fields | Description | Comments | -| -------- | -------- | -------- | -| `indexesPolicy` | This JSON object contains rules description to apply for search queries performed with the JWT depending the searched index. | Let's say an index uses a field to separate documents belonging to one user from another one, but another index needs to separate belonging using a different field in its schema. Defining specific rules per accessible index avoids having to generate several tenant tokens for an end-user. | +| Fields | Required? | Description | Comments | +| -------- | --------- |------------ | -------- | +| `indexesPolicy` | Required | This JSON object contains rules description to apply for search queries performed with the JWT depending the searched index. A Tenant Token cannot access more indexes at search time than those defined as accessible by the signing API key. | Let's say an index uses a field to separate documents belonging to one user from another one, but another index needs to separate belonging using a different field in its schema. Defining specific rules per accessible index avoids having to generate several tenant tokens for an end-user. | ##### 2.1.2.3 Payload example @@ -93,7 +93,7 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 ```json { - "iss": "rkDxFUHd", <- The first 8 characters of the signing API Key + "apiKeyPrefix": "rkDxFUHd", <- The first 8 characters of the signing MeiliSearch API Key "exp": 1641835850, <- An expiration date in seconds from 1970-01-01T00:00:00Z UTC "indexesPolicy": { <- The indexesPolicy Json Object. "*": { @@ -105,10 +105,14 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 > In this example, `indexesPolicy` allows to specify, that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), this filter will be applied on all search requests. -##### 2.1.2.4 `iss` field +##### 2.1.2.4 `apiKeyPrefix` field + +`apiKeyPrefix` permits to verify that the signing API key of the Token is known and valid within MeiliSearch. It must contain the first 8 characters of the MeiliSearch API key that generates and signs the Tenant Token. ##### 2.1.2.5 `exp` field +`exp` permits to specify the expiration date of the Tenant Token if needed. The format is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. + ##### 2.1.2.6 `indexesPolicy` JSON object `indexesPolicy` is a description of the possible rules for each index. @@ -135,6 +139,15 @@ is equivalent to } ``` +is equivalent to + +```json +{ + "indexesPolicy": ["*"] //In array notation +} + +``` + --- > In this case, all searchable indexes from the signing API Key will be searchable by the tenant token and MeiliSearch will apply the filter definition before applying the search parameters added by the end user. @@ -161,6 +174,14 @@ is equivalent to } ``` +is equivalent to + +```json +{ + "indexesPolicy": ["medical_records"] +} +``` + --- > In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index and specific rules will be applied at search time before applying the search parameters added by the end user. @@ -225,7 +246,7 @@ header = { base64Header = base64Encode(header) payload = { - "iss": meiliSearchApiKey.slice(0,8), + "apiKeyPrefix": meiliSearchApiKey.slice(0,8), "exp": 1641835850, "indexesPolicy": { "*": { From e2c1523b4dd093bcc580077eae146b29000428be Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 13:45:52 +0100 Subject: [PATCH 27/40] update indexesPolicy formats example --- text/0089-tenant-tokens.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 8db1352d..3f30f290 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -123,9 +123,12 @@ Here are some valid examples in an attempt to cover all possible use cases. > In this case, all searchable indexes from the signing API Key will be searchable by the tenant token without specific rules. + ```json { - "indexesPolicy": {} + "indexesPolicy": { + "*": {} + } } ``` @@ -134,7 +137,7 @@ is equivalent to ```json { "indexesPolicy": { - "*": {} + "*": null } } ``` @@ -143,9 +146,8 @@ is equivalent to ```json { - "indexesPolicy": ["*"] //In array notation + "indexesPolicy": ["*"] //This notation does not allow the addition of specific rules. The search will just be accessible on all accessibles indexes from the signing API Key for the Tenant Token without specific rules. } - ``` --- @@ -173,6 +175,15 @@ is equivalent to } } ``` +is equivalent to + +```json +{ + "indexesPolicy": { + "medical_records": null + } +} +``` is equivalent to From 247996f7bddda42011a09a5ec800a02c8c75e802 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 18:37:45 +0100 Subject: [PATCH 28/40] rename indexesPolicy to searchRules and add supported JWT signatures --- text/0089-tenant-tokens.md | 55 ++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 3f30f290..72476e41 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -19,7 +19,7 @@ A Tenant Token is a JWT containing the information necessary for MeiliSearch to - `Tenant tokens` contain rules that ensure that a `Tenant token` holder (e.g an end-user) only has access documents matching rules chosen at the `tenant token` creation. - `Tenant tokens` are signed from a MeiliSearch `API key` on the user's code. - `Tenant tokens` cannot be less restrictive than the signing `API key` and can only be used for searching. A Tenant Token cannot search within more indexes than the API Key that signed that Tenant Token. -- `Tenant tokens` can have different rules for each index accessible by the signing API key. These filters rule per index are described in an `indexesPolicy` json object. +- `Tenant tokens` can have different rules for each index accessible by the signing API key. These filters rule per index are described in an `searchRules` json object. - The only rule at the moment is the search parameter `filter`. Other rules may be added in the future. - When a request is made with the Tenant Token, MeiliSearch checks if this Tenant Token is authorized to make the request and in this case injects the rules at search time. @@ -55,9 +55,21 @@ A Tenant Token generated for MeiliSearch must respect several conditions. #### 2.1.1 Header: Algorithm and token type -Tenant Token MUST be signed with the combination of `HMAC + SHA256`. +The Tenant Token MUST be signed with one of the following algorithms: -e.g. +- `HS256` +- `HS384` +- `HS512` +- `RS256` +- `RS384` +- `RS512` +- `PS256` +- `PS384` +- `PS512` +- `ES256` +- `ES384` + +e.g. With `HS256` ```json { @@ -83,7 +95,7 @@ This information can be separated into two parts, on one hand, the information a | Fields | Required? | Description | Comments | | -------- | --------- |------------ | -------- | -| `indexesPolicy` | Required | This JSON object contains rules description to apply for search queries performed with the JWT depending the searched index. A Tenant Token cannot access more indexes at search time than those defined as accessible by the signing API key. | Let's say an index uses a field to separate documents belonging to one user from another one, but another index needs to separate belonging using a different field in its schema. Defining specific rules per accessible index avoids having to generate several tenant tokens for an end-user. | +| `searchRules` | Required | This JSON object contains rules description to apply for search queries performed with the JWT depending the searched index. A Tenant Token cannot access more indexes at search time than those defined as accessible by the signing API key. | Let's say an index uses a field to separate documents belonging to one user from another one, but another index needs to separate belonging using a different field in its schema. Defining specific rules per accessible index avoids having to generate several tenant tokens for an end-user. | ##### 2.1.2.3 Payload example @@ -95,7 +107,7 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 { "apiKeyPrefix": "rkDxFUHd", <- The first 8 characters of the signing MeiliSearch API Key "exp": 1641835850, <- An expiration date in seconds from 1970-01-01T00:00:00Z UTC - "indexesPolicy": { <- The indexesPolicy Json Object. + "searchRules": { <- The searchRules Json Object. "*": { "filter": "user_id = 1" } @@ -103,7 +115,7 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 } ``` -> In this example, `indexesPolicy` allows to specify, that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), this filter will be applied on all search requests. +> In this example, `searchRules` allows to specify, that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), this filter will be applied on all search requests. ##### 2.1.2.4 `apiKeyPrefix` field @@ -113,9 +125,9 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 `exp` permits to specify the expiration date of the Tenant Token if needed. The format is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. -##### 2.1.2.6 `indexesPolicy` JSON object +##### 2.1.2.6 `searchRules` JSON object -`indexesPolicy` is a description of the possible rules for each index. +`searchRules` is a description of the possible rules for each index. Here are some valid examples in an attempt to cover all possible use cases. @@ -126,7 +138,7 @@ Here are some valid examples in an attempt to cover all possible use cases. ```json { - "indexesPolicy": { + "searchRules": { "*": {} } } @@ -136,7 +148,7 @@ is equivalent to ```json { - "indexesPolicy": { + "searchRules": { "*": null } } @@ -146,7 +158,7 @@ is equivalent to ```json { - "indexesPolicy": ["*"] //This notation does not allow the addition of specific rules. The search will just be accessible on all accessibles indexes from the signing API Key for the Tenant Token without specific rules. + "searchRules": ["*"] //This notation does not allow the addition of specific rules. The search will just be accessible on all accessibles indexes from the signing API Key for the Tenant Token without specific rules. } ``` @@ -156,7 +168,7 @@ is equivalent to ```json { - "indexesPolicy": { + "searchRules": { "*": { "filter": "user_id = 1" } @@ -170,7 +182,7 @@ is equivalent to ```json { - "indexesPolicy": { + "searchRules": { "medical_records": {} } } @@ -179,7 +191,7 @@ is equivalent to ```json { - "indexesPolicy": { + "searchRules": { "medical_records": null } } @@ -189,7 +201,7 @@ is equivalent to ```json { - "indexesPolicy": ["medical_records"] + "searchRules": ["medical_records"] } ``` @@ -199,7 +211,7 @@ is equivalent to ```json { - "indexesPolicy": { + "searchRules": { "medical_records": { "filter": "user_id = 1" } @@ -213,7 +225,7 @@ is equivalent to ```json { - "indexesPolicy": { + "searchRules": { "medical_records": { "filter": "user_id = 1" }, @@ -230,7 +242,7 @@ is equivalent to ```json { - "indexesPolicy": { + "searchRules": { "*": { "filter": "user_id = 1" }, @@ -240,6 +252,9 @@ is equivalent to } } ``` +--- + +Note: The `filter` field accepts array, string and the mixed syntax as described in the [filter and facet specification](0027-filter-and-facet-behavior.md). --- @@ -259,7 +274,7 @@ base64Header = base64Encode(header) payload = { "apiKeyPrefix": meiliSearchApiKey.slice(0,8), "exp": 1641835850, - "indexesPolicy": { + "searchRules": { "*": { "filter": "user_id = 1" } @@ -276,4 +291,4 @@ TenantToken = base64Header + '.' + base64Payload + '.' + signature ## 3. Future Possibilities - Handle more signing method for the Tenant Token. -- Handle more search parameters restrictions in `indexesPolicy`. \ No newline at end of file +- Handle more search parameters restrictions in `searchRules`. \ No newline at end of file From 87c3799f33f0c63f57508e01bfb9ec8fb57d21c2 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 18:39:37 +0100 Subject: [PATCH 29/40] Rephrase searchRules explanations --- text/0089-tenant-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 72476e41..b18e7198 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -127,7 +127,7 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 ##### 2.1.2.6 `searchRules` JSON object -`searchRules` is a description of the possible rules for each index. +`searchRules` contains the rules to be enforced at search time for all or specific accessible indexes for the signing API Key. Here are some valid examples in an attempt to cover all possible use cases. From 94a754fb7c71c370e906db30b1f66ac92be301e8 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 18:40:05 +0100 Subject: [PATCH 30/40] Update text/0089-tenant-tokens.md Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-tenant-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index b18e7198..7bcce52c 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -123,7 +123,7 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 ##### 2.1.2.5 `exp` field -`exp` permits to specify the expiration date of the Tenant Token if needed. The format is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. +`exp` is used to specify the expiration date of the Tenant Token if needed. The format is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. ##### 2.1.2.6 `searchRules` JSON object From 624bf68ab76d307511bc0eb651cb65b324cb5767 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 18:42:37 +0100 Subject: [PATCH 31/40] Update text/0089-tenant-tokens.md Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-tenant-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 7bcce52c..cdc3fe17 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -207,7 +207,7 @@ is equivalent to --- -> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index and specific rules will be applied at search time before applying the search parameters added by the end user. +> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index, plus specific rules limits their search results. These rules are always applied and cannot be overwritten by the users search parameters. ```json { From 2bcd81fecda199668a51a1946dae10a257fc2202 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 18:42:50 +0100 Subject: [PATCH 32/40] Update text/0089-tenant-tokens.md Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-tenant-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index cdc3fe17..d5d73f69 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -221,7 +221,7 @@ is equivalent to --- -> In this case, if the medical_records and medical_appointments indexes are searchable from the signing API Key, the tenant token can only search in those indexes and will apply specific rules given the searched index before applying the search parameters added by the end user. +> In this case, if the medical_records and medical_appointments indexes are searchable from the signing API Key, the tenant token can only search in those indexes and specific rules limits their search results. These rules are always applied and cannot be overwritten by the users search parameters. ```json { From f40e687d435322a39bba6c45cb5c5be62a892622 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 24 Jan 2022 19:27:04 +0100 Subject: [PATCH 33/40] Rephrase explanations from suggestions --- text/0089-tenant-tokens.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index d5d73f69..a94dc310 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -133,8 +133,7 @@ Here are some valid examples in an attempt to cover all possible use cases. --- -> In this case, all searchable indexes from the signing API Key will be searchable by the tenant token without specific rules. - +> In this case, all indexes on which the signing API Key has permissions are searchable by the tenant token without any restrictions. ```json { @@ -164,7 +163,7 @@ is equivalent to --- -> In this case, all searchable indexes from the signing API Key will be searchable by the tenant token and MeiliSearch will apply the filter definition before applying the search parameters added by the end user. +> In this case, all searchable indexes from the signing API Key are searchable by the tenant token and MeiliSearch will apply the filter rule before applying the search parameters added by the end user. ```json { @@ -178,7 +177,7 @@ is equivalent to --- -> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index without applying specific rules before applying the search parameters added by the end user. +> In this case, if the `medical_records` index is searchable from the signing API Key, the tenant token can only search in the `medical_records` index. No further rules impact search results on `medical_records`. ```json { @@ -207,7 +206,7 @@ is equivalent to --- -> In this case, if the medical_records index is searchable from the signing API Key, the tenant token can only search in the medical_records index, plus specific rules limits their search results. These rules are always applied and cannot be overwritten by the users search parameters. +> In this case, if the `medical_records` index is searchable from the signing API Key, the tenant token can only search in the `medical_records` index, MeiliSearch will apply the filter rule before applying the search parameters added by the end user. ```json { @@ -221,7 +220,7 @@ is equivalent to --- -> In this case, if the medical_records and medical_appointments indexes are searchable from the signing API Key, the tenant token can only search in those indexes and specific rules limits their search results. These rules are always applied and cannot be overwritten by the users search parameters. +> In this case, if the `medical_records` and `medical_appointments` indexes are searchable from the signing API Key, the tenant token can only search in those indexes. MeiliSearch will apply the filter rule before applying the search parameters added by the end user. ```json { @@ -238,7 +237,7 @@ is equivalent to --- -> In this case, all searchable indexes from the signing API Key will be searchable and the rules under `*` will be applied at search time. The medical_appointments index policy will replace the `*` rules specifically when this index is searched by those specified for that index. +> In this case, all searchable indexes from the signing API Key are searchable and search requests will apply specific rules defined in the wildcard field: `*`. The `medical_appointments` index rules, defined in the field of the same name, overwrites the rules defined in the wildcard field `*` for this specific index. ```json { From d7ff25a58c83c6cb1b5915805b50460a5bf74f1f Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Tue, 25 Jan 2022 17:06:24 +0100 Subject: [PATCH 34/40] Update scheme --- text/0089-tenant-tokens.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index a94dc310..76dcc278 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -35,7 +35,7 @@ Users today need to set up workarounds to have multi-tenant indexes. Most of the #### 1.3.1 Example: Solving Multi-Tenancy with `Tenant tokens` -![](https://user-images.githubusercontent.com/3692335/149373631-de6f3c5f-a514-4c8d-b018-ee09ccaeaf4d.png) +![](https://user-images.githubusercontent.com/3692335/151013496-d33ab507-f972-465d-b942-899fc2bd0a22.png) `Mark` is a developer for a SaaS platform. He would like to ensure that every end-user can only access their documents at search time. From 68f77674e14ea9a1ba9a472e94ba37da36e82189 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Tue, 25 Jan 2022 17:48:12 +0100 Subject: [PATCH 35/40] Update text/0089-tenant-tokens.md Co-authored-by: Many --- text/0089-tenant-tokens.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 76dcc278..474d133e 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -60,14 +60,6 @@ The Tenant Token MUST be signed with one of the following algorithms: - `HS256` - `HS384` - `HS512` -- `RS256` -- `RS384` -- `RS512` -- `PS256` -- `PS384` -- `PS512` -- `ES256` -- `ES384` e.g. With `HS256` From 8e9edae26362a2f0985566594b38ca1ac3d7ba0f Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Wed, 26 Jan 2022 16:03:18 +0100 Subject: [PATCH 36/40] Mention tenant token revoking --- text/0089-tenant-tokens.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 474d133e..f779a1e6 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -279,7 +279,20 @@ signature = HS256(base64Header + '.' + base64Payload, meiliSearchApiKey) TenantToken = base64Header + '.' + base64Payload + '.' + signature ``` +### 2.2 Revoking a Tenant Token + +This is not possible at the moment to revoke a Tenant Token on the MeiliSearch side but may be added in the future. + +For the moment the only way is to **delete the API key that signed it** using the `DELETE - /keys/:apiKey` endpoints of MeiliSearch. + +🚨 **Doing this will revoke all tenant tokens signed by this API Key.** + +Another much more drastic method is to modify the `master key` of the MeiliSearch instance. + +🚨🚨 **Doing this will regenerate all the API Keys and thus revoke all the tenant tokens generated regardless of the signing API Key.** + ## 3. Future Possibilities - Handle more signing method for the Tenant Token. -- Handle more search parameters restrictions in `searchRules`. \ No newline at end of file +- Handle more search parameters restrictions in `searchRules`. +- Find a solution to invalidate a specific Tenant Token. \ No newline at end of file From 5f0ff4ed42c9e5a593a38b991b550710f8f3a658 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Wed, 16 Feb 2022 09:47:26 +0100 Subject: [PATCH 37/40] Add precision on SDKs and Meilisearch role for Tenant Token --- text/0089-tenant-tokens.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index f779a1e6..a0d14d7b 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -15,13 +15,14 @@ A Tenant Token is a JWT containing the information necessary for MeiliSearch to #### 1.1.1 Summary Key Points -- `Tenant tokens` are JWTs generated on the user side. Thus not stored or retrievable on MeiliSearch side. +- `Tenant tokens` are JWTs generated on the user side by using our SDKs or their own custom code. Thus not stored or retrievable on MeiliSearch side. - `Tenant tokens` contain rules that ensure that a `Tenant token` holder (e.g an end-user) only has access documents matching rules chosen at the `tenant token` creation. - `Tenant tokens` are signed from a MeiliSearch `API key` on the user's code. - `Tenant tokens` cannot be less restrictive than the signing `API key` and can only be used for searching. A Tenant Token cannot search within more indexes than the API Key that signed that Tenant Token. - `Tenant tokens` can have different rules for each index accessible by the signing API key. These filters rule per index are described in an `searchRules` json object. - The only rule at the moment is the search parameter `filter`. Other rules may be added in the future. -- When a request is made with the Tenant Token, MeiliSearch checks if this Tenant Token is authorized to make the request and in this case injects the rules at search time. +- `Tenant tokens` are sent to Meilisearch via the `Authorization` header. +- When a search request is made with a Tenant Token, MeiliSearch decodes it to checks if the tenant token is authorized to make the request and then extract and inject the search rules for the search request being made. ### 1.2 Motivation From 56d3aaa48d82e407919739f66ebce1e1ba0f053b Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Sun, 20 Feb 2022 12:25:51 +0100 Subject: [PATCH 38/40] Apply suggestions from code review Co-authored-by: Bruno Casali Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com> --- text/0089-tenant-tokens.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index a0d14d7b..5f6fa8f0 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -96,11 +96,11 @@ Given a MeiliSearch API Key used to sign the JWT from the user code. Here is an e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d1631b54f3426fa8b2595` -```json +```jsonc { - "apiKeyPrefix": "rkDxFUHd", <- The first 8 characters of the signing MeiliSearch API Key - "exp": 1641835850, <- An expiration date in seconds from 1970-01-01T00:00:00Z UTC - "searchRules": { <- The searchRules Json Object. + "apiKeyPrefix": "rkDxFUHd", // The first 8 characters of the signing MeiliSearch API Key + "exp": 1641835850, // An expiration date in seconds from 1970-01-01T00:00:00Z UTC + "searchRules": { // The searchRules Json Object. "*": { "filter": "user_id = 1" } @@ -148,7 +148,7 @@ is equivalent to is equivalent to -```json +```jsonc { "searchRules": ["*"] //This notation does not allow the addition of specific rules. The search will just be accessible on all accessibles indexes from the signing API Key for the Tenant Token without specific rules. } @@ -275,7 +275,9 @@ payload = { base64Payload = base64Encode(payload) -signature = HS256(base64Header + '.' + base64Payload, meiliSearchApiKey) +hashedSignature = HS256(base64Header + '.' + base64Payload, meiliSearchApiKey) +signature = base64Encode(hashedSignature) + TenantToken = base64Header + '.' + base64Payload + '.' + signature ``` From 1f3a26a22695906c02bc5fa40b9c4050fab297f5 Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Tue, 22 Feb 2022 14:20:35 +0100 Subject: [PATCH 39/40] Add Future Possibilities on tenant token formatting error --- text/0089-tenant-tokens.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index 5f6fa8f0..c4297be5 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -17,7 +17,7 @@ A Tenant Token is a JWT containing the information necessary for MeiliSearch to - `Tenant tokens` are JWTs generated on the user side by using our SDKs or their own custom code. Thus not stored or retrievable on MeiliSearch side. - `Tenant tokens` contain rules that ensure that a `Tenant token` holder (e.g an end-user) only has access documents matching rules chosen at the `tenant token` creation. -- `Tenant tokens` are signed from a MeiliSearch `API key` on the user's code. +- `Tenant tokens` are signed from a MeiliSearch `API key` resource on the user's code. - `Tenant tokens` cannot be less restrictive than the signing `API key` and can only be used for searching. A Tenant Token cannot search within more indexes than the API Key that signed that Tenant Token. - `Tenant tokens` can have different rules for each index accessible by the signing API key. These filters rule per index are described in an `searchRules` json object. - The only rule at the moment is the search parameter `filter`. Other rules may be added in the future. @@ -114,6 +114,8 @@ e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d16 `apiKeyPrefix` permits to verify that the signing API key of the Token is known and valid within MeiliSearch. It must contain the first 8 characters of the MeiliSearch API key that generates and signs the Tenant Token. +The `apiKeyPrefix` can't be generated from a master-key. + ##### 2.1.2.5 `exp` field `exp` is used to specify the expiration date of the Tenant Token if needed. The format is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. @@ -298,4 +300,5 @@ Another much more drastic method is to modify the `master key` of the MeiliSearc - Handle more signing method for the Tenant Token. - Handle more search parameters restrictions in `searchRules`. -- Find a solution to invalidate a specific Tenant Token. \ No newline at end of file +- Find a solution to invalidate a specific Tenant Token. +- Find a solution to help error resolution / error making for a tenant token payload when a search request occurs. E.g. Help to locate that the error is coming from the `searchRules` `filter` field from a tenant token and not from the search request `filter` parameter. \ No newline at end of file From 4bc03969f801dbe2e41d0f7ee9da5c9b01c9f8be Mon Sep 17 00:00:00 2001 From: Guillaume Mourier Date: Mon, 7 Mar 2022 12:58:30 +0100 Subject: [PATCH 40/40] Replace MeiliSearch by Meilisearch, fix typos, rephrase sentences and reorganize sections --- text/0089-tenant-tokens.md | 186 +++++++++++++++---------------------- 1 file changed, 75 insertions(+), 111 deletions(-) diff --git a/text/0089-tenant-tokens.md b/text/0089-tenant-tokens.md index c4297be5..d053674b 100644 --- a/text/0089-tenant-tokens.md +++ b/text/0089-tenant-tokens.md @@ -1,62 +1,59 @@ - Title: Tenant Tokens -- Start Date: 2021-10-15 -- Specification PR: [#89](https://github.com/meilisearch/specifications/pull/89) -- Discovery Issue: [#51](https://github.com/meilisearch/product/issues/51) -# Tenant Token +# Tenant Tokens -## 1. Functional Specification +## 1. Summary -### 1.1 Summary +A `Tenant token` is generated by the user code to be used by an end-user when making search queries. -A `Tenant token` is generated on the user code to be used by an end-user. It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request given enforced rules specified from the user business logic. +It allows users to have multi-tenant indexes and thus restricts access to documents depending on the end-user making the search request. -A Tenant Token is a JWT containing the information necessary for MeiliSearch to verify it and extract permission/rules to apply it to the end user's search. +A Tenant Token is a JWT containing the information necessary for Meilisearch to verify it and extract permission/rules to apply it to the end user's search. -#### 1.1.1 Summary Key Points +### 1.1. Key Points -- `Tenant tokens` are JWTs generated on the user side by using our SDKs or their own custom code. Thus not stored or retrievable on MeiliSearch side. -- `Tenant tokens` contain rules that ensure that a `Tenant token` holder (e.g an end-user) only has access documents matching rules chosen at the `tenant token` creation. -- `Tenant tokens` are signed from a MeiliSearch `API key` resource on the user's code. -- `Tenant tokens` cannot be less restrictive than the signing `API key` and can only be used for searching. A Tenant Token cannot search within more indexes than the API Key that signed that Tenant Token. -- `Tenant tokens` can have different rules for each index accessible by the signing API key. These filters rule per index are described in an `searchRules` json object. -- The only rule at the moment is the search parameter `filter`. Other rules may be added in the future. -- `Tenant tokens` are sent to Meilisearch via the `Authorization` header. -- When a search request is made with a Tenant Token, MeiliSearch decodes it to checks if the tenant token is authorized to make the request and then extract and inject the search rules for the search request being made. +- `Tenant tokens` are JWTs generated on the user side by using Meilisearch SDKs or their custom code. `Tenant tokens` are not stored nor retrievable on the Meilisearch side. +- `Tenant tokens` contain rules that ensure that a `Tenant token` holder (e.g. an end-user) only has access to documents matching rules chosen at the `tenant token` creation. +- `Tenant tokens` are signed from a Meilisearch `API key` resource on the user's code. +- `Tenant tokens` must not be signed by the master key. +- `Tenant tokens` cannot be more permissive than the signing `API key`. +- `Tenant tokens` must be signed by an `API Key` having the `search` action defined. +- `Tenant tokens` can have different rules for each index accessible by the signing API key. These rules are described in the `searchRules` JSON object. +- The only rule available in the `searchRules` object is the search parameter `filter`. +- `Tenant tokens` are sent to Meilisearch via the `Authorization` header like any `API Keys` or the master key. +- When Meilisearch receives a search query emitted with a `Tenant token`, the `tenant token` is decoded, then the `searchRules` are applied for the search request before the search parameters. -### 1.2 Motivation +## 2. Motivation `Tenant tokens` are introduced to solve multi-tenant indexes use-case. -> Multi-Tenant Indexes Definition: It is an index that stores documents that may belong to different tenants. In our case, a tenant within an index can be a user or a company, etc.. In general, the data of one tenant should not be accessible by other tenants. +> Multi-Tenant Indexes Definition: It is an index that stores documents that may belong to different tenants. In our case, a tenant within an index can be a user or a company, etc. In general, the data of one tenant should not be accessible by other tenants. -Users today need to set up workarounds to have multi-tenant indexes. Most of the time, they have to use server code to implement the access restriction logic before requesting MeiliSearch. It is difficult to maintain, to implement, and the performance is degraded because the frontend code does not communicate directly with MeiliSearch. +Users today need to set up workarounds to have multi-tenant indexes. They have to use server code to implement the access restriction logic before requesting Meilisearch. It isn't easy to maintain, to implement, and the performance is not optimal because the frontend code does not communicate directly with Meilisearch. -### 1.3 `Tenant Token` Explanations +## 3. Functional Specification -#### 1.3.1 Example: Solving Multi-Tenancy with `Tenant tokens` +### 3.1. Example: Solving Multi-Tenancy with `Tenant tokens` ![](https://user-images.githubusercontent.com/3692335/151013496-d33ab507-f972-465d-b942-899fc2bd0a22.png) `Mark` is a developer for a SaaS platform. He would like to ensure that every end-user can only access their documents at search time. -When an end-user registers, Mark's backend code generates a `Tenant token` for that end-user so they can only access their documents. +When an end-user registers, Mark's backend code generates a `Tenant token` for that end-user so they can only access their documents at search time. -This tenant-token is signed with a MeiliSearch API Key so that MeiliSearch can ensure that the tenant-token has been generated from a known entity. +This tenant-token is signed with a Meilisearch API Key so that Meilisearch can ensure that the tenant-token has been generated from a known entity. -MeiliSearch checks if the Tenant Token is authorized to make the search request. +Meilisearch checks if the Tenant Token is authorized to make the search request. -Then MeiliSearch extracts the Tenant Token's rules to apply for the search request. +Then Meilisearch extracts the Tenant Token's rules to apply for the search request. -## 2. Technical Details +### 3.2. `Tenant Token` Details -### 2.1 `Tenant Token` details +Tenant Tokens are JWTs and must respect several conditions to be understandable by a Meilisearch instance. -A Tenant Token generated for MeiliSearch must respect several conditions. +#### 3.2.1. Header: Algorithm and token type -#### 2.1.1 Header: Algorithm and token type - -The Tenant Token MUST be signed with one of the following algorithms: +The Tenant Token must be signed with one of the following algorithms: - `HS256` - `HS384` @@ -71,60 +68,38 @@ e.g. With `HS256` } ``` -#### 2.1.2 Payload: Data - -MeiliSearch needs information within the tenant token to check its validity and use it to authorize and perform end-user requests. +#### 3.2.2. Payload: Data -This information can be separated into two parts, on one hand, the information allows to check the validity of the Tenant Token, on the other hand, the business logic information allows to apply search parameters / rules for the end user's search request. +Meilisearch needs information within the tenant token to check its validity and use it to authorize and perform end-user search requests. -##### 2.1.2.1 Validity related +##### 3.2.2.1. Validity Information -| Fields | Required? | Description | Comments | +| Fields | Required | Description | Comments | | -------- |----------- | ----------- | -------- | -| `apiKeyPrefix` (Custom claim) | Required | Must contain the first 8 characters of the signing `MeiliSearch API key` used to generate the JWT | | +| `apiKeyPrefix` (Custom claim) | Required | Must contain the first 8 characters of the signing `Meilisearch API key` used to generate the JWT | | | `exp` (Expiration Time claim) | Optional | A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time. | If the signing API key expires, the Tenant Token also expires. Thus said, the `exp` can't be greater than the expiration date of the signing API key. | -##### 2.1.2.2 Business logic related +###### 3.2.2.1.1. `apiKeyPrefix` field -| Fields | Required? | Description | Comments | -| -------- | --------- |------------ | -------- | -| `searchRules` | Required | This JSON object contains rules description to apply for search queries performed with the JWT depending the searched index. A Tenant Token cannot access more indexes at search time than those defined as accessible by the signing API key. | Let's say an index uses a field to separate documents belonging to one user from another one, but another index needs to separate belonging using a different field in its schema. Defining specific rules per accessible index avoids having to generate several tenant tokens for an end-user. | +`apiKeyPrefix` permits to verify that the signing API key of the Token is known and valid within Meilisearch. It must contain the first 8 characters of the Meilisearch API key that generates and signs the Tenant Token. -##### 2.1.2.3 Payload example +The `apiKeyPrefix` can't be generated from a master key, and the `API Key` must have the `search` action defined. -Given a MeiliSearch API Key used to sign the JWT from the user code. Here is an example of a valid payload for a tenant token. +###### 3.2.2.1.2. `exp` field -e.g `MeiliSearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d1631b54f3426fa8b2595` - -```jsonc -{ - "apiKeyPrefix": "rkDxFUHd", // The first 8 characters of the signing MeiliSearch API Key - "exp": 1641835850, // An expiration date in seconds from 1970-01-01T00:00:00Z UTC - "searchRules": { // The searchRules Json Object. - "*": { - "filter": "user_id = 1" - } - } -} -``` - -> In this example, `searchRules` allows to specify, that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), this filter will be applied on all search requests. - -##### 2.1.2.4 `apiKeyPrefix` field - -`apiKeyPrefix` permits to verify that the signing API key of the Token is known and valid within MeiliSearch. It must contain the first 8 characters of the MeiliSearch API key that generates and signs the Tenant Token. - -The `apiKeyPrefix` can't be generated from a master-key. +`exp` is used to specify the expiration date of the Tenant Token if needed. The format is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. -##### 2.1.2.5 `exp` field +##### 3.2.2.2. Business Logic Information -`exp` is used to specify the expiration date of the Tenant Token if needed. The format is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds. +| Fields | Required | Description | Comments | +| -------- | --------- |------------ | -------- | +| `searchRules` | Required | This JSON object contains rules to apply for search queries performed with the JWT depending on the searched index. A Tenant Token cannot access more indexes at search time than those defined as accessible by the signing API key. | Let's say an index uses a field to separate documents belonging to one end-user from another one, but another index needs to separate belonging using a different field in its schema. Defining specific search rules per accessible index avoids generating several tenant tokens for an end-user. | -##### 2.1.2.6 `searchRules` JSON object +###### 3.2.2.2.1. `searchRules` JSON object `searchRules` contains the rules to be enforced at search time for all or specific accessible indexes for the signing API Key. -Here are some valid examples in an attempt to cover all possible use cases. +Here are the accepted formats for the `searchRules` property. --- @@ -150,15 +125,17 @@ is equivalent to is equivalent to -```jsonc +```json { - "searchRules": ["*"] //This notation does not allow the addition of specific rules. The search will just be accessible on all accessibles indexes from the signing API Key for the Tenant Token without specific rules. + "searchRules": ["*"] } ``` +The search is authorized on all accessible indexes from the signing API Key for the Tenant Token without specific rules. + --- -> In this case, all searchable indexes from the signing API Key are searchable by the tenant token and MeiliSearch will apply the filter rule before applying the search parameters added by the end user. +> In this case, all searchable indexes from the signing API Key are searchable by the tenant token, and Meilisearch applies the `filter` search rule before applying the request search parameters. ```json { @@ -172,7 +149,7 @@ is equivalent to --- -> In this case, if the `medical_records` index is searchable from the signing API Key, the tenant token can only search in the `medical_records` index. No further rules impact search results on `medical_records`. +> In this case, if the `medical_records` index is searchable from the signing API Key, the tenant token is only authorized to search in the `medical_records` index. ```json { @@ -201,7 +178,7 @@ is equivalent to --- -> In this case, if the `medical_records` index is searchable from the signing API Key, the tenant token can only search in the `medical_records` index, MeiliSearch will apply the filter rule before applying the search parameters added by the end user. +> In this case, if the `medical_records` index is searchable from the signing API Key, the tenant token is only authorized to search in the `medical_records` index, and Meilisearch applies the `filter` search rule before applying the request search parameters. ```json { @@ -215,7 +192,7 @@ is equivalent to --- -> In this case, if the `medical_records` and `medical_appointments` indexes are searchable from the signing API Key, the tenant token can only search in those indexes. MeiliSearch will apply the filter rule before applying the search parameters added by the end user. +> In this case, if the `medical_records` and `medical_appointments` indexes are searchable from the signing API Key, the tenant token is only authorized to search in those indexes, and Meilisearch applies the `filter` search rule before applying the request search parameters. ```json { @@ -232,7 +209,7 @@ is equivalent to --- -> In this case, all searchable indexes from the signing API Key are searchable and search requests will apply specific rules defined in the wildcard field: `*`. The `medical_appointments` index rules, defined in the field of the same name, overwrites the rules defined in the wildcard field `*` for this specific index. +> In this case, all searchable indexes from the signing API Key are searchable, and Meilisearch applies the `filter` search rule before applying the request search parameters for all indexes except for the `medical_appointments` index. A dedicated `filter` search rule is applied when making a search query on this index. ```json { @@ -248,57 +225,44 @@ is equivalent to ``` --- -Note: The `filter` field accepts array, string and the mixed syntax as described in the [filter and facet specification](0027-filter-and-facet-behavior.md). - ---- - -### 2.2 `Tenant Token` Javascript Code Sample +> The `filter` field accepts an array, a string, and the mixed syntax as described in the [filter and facet specification](0027-filter-and-facet-behavior.md). -```javascript +##### 3.2.2.3. Payload example -meiliSearchApikey = 'rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d1631b54f3426fa8b2595'; +Given a Meilisearch API Key used to sign the JWT from the user code. Here is an example of a valid payload for a tenant token. -header = { - "alg": "HS256", - "typ": "JWT" -} +e.g. `Meilisearch API key: rkDxFUHd02193e120218f72cc51a9db62729fdb4003e271f960d1631b54f3426fa8b2595` -base64Header = base64Encode(header) - -payload = { - "apiKeyPrefix": meiliSearchApiKey.slice(0,8), - "exp": 1641835850, - "searchRules": { +```jsonc +{ + "apiKeyPrefix": "rkDxFUHd", // The first 8 characters of the signing Meilisearch API Key + "exp": 1641835850, // An expiration date in seconds from 1970-01-01T00:00:00Z UTC + "searchRules": { // The searchRules Json Object definition "*": { "filter": "user_id = 1" } } } +``` -base64Payload = base64Encode(payload) - -hashedSignature = HS256(base64Header + '.' + base64Payload, meiliSearchApiKey) -signature = base64Encode(hashedSignature) - +> In this example, `"*"` allows to specify that no matter which index is searched (among all those accessible by the signing API key that generated the tenant token), the `filter` search rule is applied on all search requests. -TenantToken = base64Header + '.' + base64Payload + '.' + signature -``` -### 2.2 Revoking a Tenant Token +### 3.3. Tenant Token Revokation -This is not possible at the moment to revoke a Tenant Token on the MeiliSearch side but may be added in the future. +It is not possible to revoke a specific tenant token. -For the moment the only way is to **delete the API key that signed it** using the `DELETE - /keys/:apiKey` endpoints of MeiliSearch. +The only way to do so is to **delete the API key that signed it** using the `DELETE - /keys/:apiKey` endpoints of Meilisearch. -🚨 **Doing this will revoke all tenant tokens signed by this API Key.** +🚨 **Doing this revoke all tenant tokens signed by this API Key.** -Another much more drastic method is to modify the `master key` of the MeiliSearch instance. +Another much more drastic method is to modify the `master key` of the Meilisearch instance. -🚨🚨 **Doing this will regenerate all the API Keys and thus revoke all the tenant tokens generated regardless of the signing API Key.** +🚨🚨 **Doing this regenerate all the API Keys and thus revoke all the tenant tokens generated regardless of the signing API Key.** -## 3. Future Possibilities +## 4. Future Possibilities -- Handle more signing method for the Tenant Token. +- Handle more signing methods for the Tenant Token. - Handle more search parameters restrictions in `searchRules`. -- Find a solution to invalidate a specific Tenant Token. -- Find a solution to help error resolution / error making for a tenant token payload when a search request occurs. E.g. Help to locate that the error is coming from the `searchRules` `filter` field from a tenant token and not from the search request `filter` parameter. \ No newline at end of file +- Add a possibility to revoke a specific Tenant Token. +- Introduce an endpoint to generate tenant tokens on the Meilisearch side. \ No newline at end of file