La clause WHERE

La clause WHERE dont vous devez vous souvenir à chaque requête

Dans la plupart des analyses intégrées, la seule chose qui se trouve entre les données de deux clients est un WHERE workspace_id = :me dont votre code d’application doit se souvenir à chaque requête.

Cela vaut pour le premier tableau de bord et le dixième. Ensuite, une requête oublie le filtre et le client A examine les revenus du client B dans une vignette qui s’affiche sans se plaindre.

Ce n’est pas un bug d’analyse. Il s’agit d’un bug de sécurité lié aux fuites de données portant un costume d’analyse. La sortie ressemble à un graphique, elle est donc examinée comme un graphique, alors qu’elle devrait être examinée comme une limite d’authentification.

L’histoire

Voici un scénario illustratif. L’entreprise s’invente ; le mécanisme ne l’est pas, et j’ai observé la forme de cela se produire plus d’une fois.

Un SaaS d’analyse de vente au détail vend un tableau de bord aux chaînes de magasins. Chaque chaîne est un espace de travail. Dans un espace de travail, un responsable régional explore un seul magasin : les revenus du jour, les commandes de cette semaine, une ligne de tendance par jour.

L’isolement réside là où il réside habituellement, dans la couche API. Chaque gestionnaire extrait l’espace de travail de la session authentifiée et ajoute le filtre avant l’exécution de la requête.

SELECT sum(revenue), sum(orders)
FROM store_daily_sales
WHERE workspace_id = $1   -- from the session, never the client
  AND store_id = $2
  AND day >= $3;

Pour un an, c’est bien. Il y a peut-être quinze points finaux. La révision du code détecte la seule fois où quelqu’un colle une requête sans le workspace_id doubler. L’équipe se sent bien et elle n’a pas tort.

Le produit propose ensuite la fonctionnalité que tout le monde demande : « demandez vos données ». Une boîte de discussion sur le tableau de bord. Un responsable régional tape « lequel de mes magasins a connu la croissance la plus rapide le mois dernier » et obtient une réponse dans un langage simple.

Sous le capot, un modèle écrit désormais le SQL.

Relisez-le, car c’est l’intégralité de l’essai. Le filtre qui vivait auparavant dans quinze points de terminaison révisés manuellement est désormais la responsabilité d’un modèle de langage générant du nouveau SQL sur chaque question, pour chaque utilisateur, en production. Vous faites confiance au modèle pour vous en souvenir WHERE workspace_id = :me sur une requête, aucun humain ne la lira avant son exécution.

Les vignettes du tableau de bord et le copilote sont tous deux consommateurs de logiciels. Aucun d’eux ne peut regarder un résultat et penser « attendez, ces chiffres sont trop gros, cela doit être la chaîne de quelqu’un d’autre ». Un analyste humain pourrait le comprendre. Une tuile restitue simplement ce qu’on lui donne, et un modèle répond couramment avec la voix de la confiance.

La perspicacité

L’isolement réimplémenté par requête est une violation en attente de la requête qui oublie. Cela était déjà vrai lorsque les requêtes étaient écrites à la main, lorsque le même risque était caché derrière un petit ensemble dénombrable de points de terminaison qu’une personne pouvait examiner. Le copilote vient de retirer ce couvercle.

Lorsque le consommateur est un logiciel, « Je me souviendrai du filtre » cesse d’être un plan. La vignette du tableau de bord ne peut pas vérifier sa propre portée. Le modèle ne le peut pas non plus, et il échoue dans la direction la plus dangereuse, en produisant une réponse plausible plutôt qu’une erreur. Un disparu WHERE ne génère pas d’erreur. Il renvoie silencieusement les mauvaises lignes.

Le problème le plus profond est celui de l’endroit où se situe la limite des locataires. Mettez-le dans le SQL et tout ce qui écrit du SQL, y compris un modèle, doit s’en souvenir à chaque requête.

Comment les équipes tentent généralement de tenir le coup

Il y a ici une progression honnête, et chaque étape est une véritable amélioration qui manque de route.

Créez votre propre filtre dans le code de l’application. Le WHERE workspace_id = :me modèle. La règle est correcte, mais elle réside dans la logique de l’application et est retapée à chaque requête. Sa sécurité dépend de la discipline et du nombre de surfaces dont vous disposez. Ajoutez une surface qui écrit ses propres requêtes, comme un copilote, et la discipline ne suffit plus, car il n’y a pas d’ensemble fixe de requêtes sur lesquelles discipliner.

Sécurité au niveau des lignes des variables de session de base de données. Plus fort, car désormais la base de données applique le prédicat au lieu de vos gestionnaires. C’est une véritable avancée. Mais cela présente des arêtes vives : dans le cadre d’un pooler de connexions en mode transaction, une variable de session définie pour un locataire peut se répercuter sur le prochain emprunteur de cette connexion, à moins que vous ne soyez méticuleux lors de sa réinitialisation. Et cela ne couvre que la couche SQL. Il ne fait rien pour le contrat d’API d’analyse situé au-dessus, ni pour l’interface basée sur un modèle qui appelle cette API. Votre isolement et les surfaces de vos produits sont renforcés à différents endroits.

Outils de tableau de bord embarqués génériques avec paramètres signés ou verrouillés. Ceux-ci s’étendent à un tableau de bord humain : vous connectez le locataire à un jeton d’intégration, l’outil refuse de laisser le navigateur l’élargir. C’est la bonne idée, et pour un tableau de bord rendu, cela fonctionne. Mais il sécurise un tableau de bord, pas une API gouvernée ni un copilote IA. Dès que votre consommateur est un logiciel qui compose ses propres requêtes, un paramètre de tableau de bord verrouillé n’est plus la limite.

Chacun de ces éléments aide. Chacun d’eux applique l’isolement à une couche différente de celle où votre prochain consommateur apparaîtra. C’est le modèle qui mérite d’être cité : l’isolement ne cesse d’être réimplémenté pour chaque surface, et chaque nouvelle surface est un nouvel endroit à oublier.

Le principe

L’isolation appartient à l’interface, déclarée une fois et appliquée par le runtime, et non rajoutée par requête ou par surface.

Si la limite est une propriété de l’interface que chaque consommateur appelle, alors le tableau de bord, l’API de reporting et le copilote héritent tous de la même règle sans qu’aucun d’entre eux n’écrive une règle. WHERE. Un appelant qui n’écrit jamais le filtre ne peut pas oublier le filtre.

Comment Gaur fait ça

Dans Gaur, les consommateurs n’envoient pas de SQL. Ils appellent un contrat, et la sécurité au niveau des lignes fait partie du contrat, elle est donc appliquée à chaque requête générée par le runtime, sur chaque protocole, que l’appelant soit une vignette de tableau de bord ou un modèle.

Deux choses font que cela tient dans un environnement multi-locataire, et la distinction entre elles est la partie importante.

Contexte d’authentification est lié à la clé API lors de la création de la clé. Il est immuable et au niveau de l’espace de travail, par exemple workspace_id. L’appelant ne peut pas le modifier, car il a été corrigé au moment où la clé a été émise.

Contexte de la demande est envoyé par appel par votre backend, dérivé de la session authentifiée, et varie selon l’utilisateur final, par exemple store_id. C’est l’élément que le navigateur ne doit jamais définir. Si un utilisateur final pouvait mettre le sien store_id sur demande, ils pouvaient lire d’autres magasins. Votre backend le définit à partir de la session à laquelle il fait déjà confiance ; le client n’y touche jamais.

La règle elle-même est un booléen sql état avec {{ placeholder }} jetons, plus un parameters carte qui lie chaque jeton à l’un ou l’autre auth. ou request.. Voici un contrat illustratif. Le modèle de données de vente au détail est inventé ; le rls la structure et son application constituent le véritable mécanisme documenté.

{
  "name": "store_sales",
  "sources": [{ "source_type": "model", "name": "store_daily_sales", "alias": "s" }],
  "dimensions": {
    "day": { "sql": "s.day", "type": "date", "semantic_type": "time", "time_grains": ["day","week","month"] }
  },
  "measures": {
    "revenue": { "sql": "sum(s.revenue)", "type": "number", "additivity": "additive" },
    "orders":  { "sql": "sum(s.orders)",  "type": "number", "additivity": "additive" }
  },
  "rls": {
    "sql": "s.workspace_id = {{ workspace_id }} AND s.store_id = {{ store_id }}",
    "parameters": {
      "workspace_id": "auth.workspace_id",
      "store_id": "request.store_id"
    }
  }
}

Les deux jetons sont injectés en tant que paramètres SQL liés, jamais interpolés sous forme de chaîne dans le texte de la requête. Ainsi, le prédicat d’isolation n’est pas un endroit où réside l’injection, et les valeurs arrivent sous forme de paramètres que l’appelant n’a pas pu écrire.

La même règle est appliquée de manière identique dans l’API de requête REST, le point de terminaison de discussion compatible OpenAI et MCP. C’est la ligne qui compte pour l’histoire ci-dessus. Lorsque l’équipe ajoute le copilote, celui-ci parle au point de terminaison de chat, qui appelle le même contrat, qui comporte le même rls bloc. La fonctionnalité IA hérite de l’isolation exacte dont disposait déjà le tableau de bord. Rien n’a besoin d’être synchronisé, car aucun appelant, humain ou modèle, n’écrit jamais le filtre.

Maintenant la partie honnête. Un contrat ne vous empêche pas d’écrire une mauvaise règle. Si vous liez store_id à auth quand ça devrait être requestou vous oubliez une clause, le runtime appliquera fidèlement votre erreur. Ce que le contrat change, c’est là où réside le risque. Cela vous fait passer de « chaque point de terminaison et chaque requête générée doit mémoriser le filtre » à « une règle, écrite une fois, testée une fois ». Vous examinez la limite en un seul endroit au lieu de faire confiance à chaque surface pour la rajouter. Cela ne garantit pas l’exactitude, et une mauvaise règle est toujours appliquée fidèlement. Ce qu’il fait, c’est faire oublier WHERE structurellement difficile à expédier, une promesse plus étroite et plus honnête.

Prise sans vergogne

Si vous envoyez des analyses orientées client ou un copilote d’IA sur des données par locataire, l’échec de cet article est votre échec par défaut, et non un cas limite. Partout où le consommateur est un logiciel (un tableau de bord intégré, une API de reporting, un agent appelant un outil, une boîte de discussion sur vos données d’entreprise), la limite du locataire ne peut pas dépendre du fait que chaque requête mémorise un WHERE.

Gaur vous permet de déclarer la règle d’isolement une fois sur le contrat et de demander au runtime de l’appliquer de la même manière dans REST, chat et MCP, de sorte qu’une nouvelle surface hérite de la limite au lieu de la réimplémenter. Cela contribue à rendre la fuite entre locataires structurellement difficile à expédier, mais pas impossible à écrire.

Si telle est la limite que vous essayez de respecter, réservez une démo sur gaur.run et apportez votre cas multi-tenant le plus compliqué.

Articles similaires