Werkplaats / De werkplaats
In jouw eigen tempo
← AI-security & red teaming
MODULE 04 · LES 12

Begrens de tenant vóór het rangschikken

Vergelijk laat filteren met zoeken binnen een afzonderlijk vertrouwde tenantscope en behoud de gewone taak.

90 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je laat met kleine inproductscores zien hoe een vreemde kaart een gewone top-k-taak verdringt.
  • Je begrenst de zoekruimte vooraf en onderscheidt lokale vergelijkingssporen van echte autorisatie.

Twee bedrijven gebruiken dezelfde zoektoepassing. Jij werkt voor bedrijf a en zoekt een eigen kaart, maar een kaart van bedrijf b lijkt nog beter op je vraag. Die vreemde kaart mag niet bepalen welke eigen informatie je te zien krijgt.

Een tenant is hier één afgeschermde klantomgeving. De toepassing kent jouw tenant al vanuit een afzonderlijk vertrouwde context. Een tenantnaam in je zoektekst of in een opgehaald document verleent geen toegang.

1. Kies eerst in welke kast je zoekt

Stel je twee archiefkasten voor. Eerst in beide kasten de beste kaart pakken en daarna de vreemde kaart weggooien, is iets anders dan meteen in de juiste kast zoeken. In de eerste route kan je eindigen met lege handen, terwijl je eigen kast een bruikbaar antwoord bevat.

We gebruiken kleine tweedimensionale vectoren. De score s is het inproduct van vraagvector q en documentvector v. q₁ en q₂ zijn de twee vraagcoördinaten; v₁ en v₂ horen bij de kaart.

s=q1v1+q2v2

Voor vraag [1,0] krijgt je eigen kaart [2,0] score 2; de vreemde kaart [8,0] krijgt score 8. Dit zijn zelfgekozen oefencoördinaten, geen echte tekstembeddings. Een hogere score betekent hier alleen hoger in onze rangschikking.

def item(name, tenant, vector):
    return dict(id=name, tenant=tenant,
        source="manual", revision="r1",
        text="color=blue",
        vector=vector)

records = [
    item("eigen", "a", [2, 0]),
    item("vreemd", "b", [8, 0]),
]
query = [1, 0]
print(2 * 1 + 0 * 0)
print(8 * 1 + 0 * 0)
# 2
# 8

2. Achteraf verwijderen vult geen plaats aan

De naïeve route rangschikt alle kaarten en bewaart de beste k. Pas daarna verwijdert zij kaarten van andere tenants. Met k=1 wint de vreemde kaart; na verwijderen blijft geen resultaat over.

late = postfiltered_search(
    records, query, "a", 1)
print(late["hits"])
print(late["compared_ids"])
# []
# ['eigen', 'vreemd']

Het vergelijkingsspoor laat zien welke kaarten een numerieke score kregen. Dit spoor is uitsluitend instrumentatie van onze lokale oefening. Het is geen advies om vreemde document-id’s of scores aan bezoekers te tonen.

01
CHECKPOINTToon het gevolg van een te late tenantfilter

3. Autorisatie bepaalt de zoekruimte

Noem de volledige oefentabel D en de vertrouwde tenant t. Dₜ bevat uitsluitend de records uit die tenant. R is de resultatenlijst; top-k rangschikt haar invoer en neemt hoogstens k kaarten.

R=topk(Dt)

De verdedigde route kiest Dₜ vóór het berekenen van scores. Zij vergelijkt de vreemde kaart dus niet. De eigen kaart blijft beschikbaar, ook wanneer een andere tenant een nog hoger scorende kaart toevoegt.

early = scoped_search(
    records, query, "a", 1)
print(early["hits"])
print(early["compared_ids"])
# [{'id': 'eigen', 'score': 2}]
# ['eigen']

Beide functies leveren nieuwe resultaten en laten de invoer intact. De gegeven helpers controleren eerst de volledige oefentabel. Dat onderscheid is belangrijk: het numerieke spoor bewijst onze zoekvolgorde, geen afwezigheid van timing- of validatielekken in een echte gedeelde index.

02
CHECKPOINTBegrens de tenant vóór het scoren
VERKEN HET ZELFZoek eerst in de juiste verzameling

Vergelijk dezelfde vraag en dezelfde vertrouwde tenant. Links wordt pas na top-k gefilterd; rechts mogen alleen toegestane kaarten meedoen.

Vraag [1, 0] · vertrouwde tenant a · k=1.

Kaarten en de late scoreberekening

KaartTenantVectorScore
a-2a[0, 2]0
b-1b[8, 8]8
a-1a[2, 0]2

De score is ons eigen inproduct. Bij k=0 wordt geen kaart gescoord. Het spoor toont alleen de numerieke zoekvolgorde in deze lokale oefening.

1. Alle kaarten → top-k → tenantfilter

Gescoorde ids

a-2, b-1, a-1

Resultaten op volgorde

Geen hits

2. Tenantfilter → scoren → top-k

Gescoorde ids

a-2, a-1

Resultaten op volgorde

  1. a-1score 2
Een gewone taak wordt verdrongen

Na laat filteren blijft geen hit over, terwijl vooraf begrenzen wel een eigen kaart behoudt.

Een tenantnaam in een document of zoektekst verleent geen rechten. De gekozen tenant is hier apart vertrouwde oefencontext; deze tabel bouwt geen echte authenticatie of gedeelde index.

Alle kaarten, tenants, receipts, meetrijen en claims zijn eigen oefengegevens. Deze vergelijking doet geen echte zoek- of netwerkopdracht.

4. Bewaar ook de gewone taak

Alles weigeren voorkomt de verkeerde kaart, maar helpt de leerling niet zoeken. Toegestane kaarten moeten op hun eigen score blijven verschijnen. Bij gelijke scores kiezen we alfabetisch op id, zodat een andere invoervolgorde geen willekeurige winnaar maakt.

records.append(item(
    "ander", "a", [2, 0]))
reply = scoped_search(
    records, query, "a", 2)
print([r["id"] for r in reply["hits"]])
# ['ander', 'eigen']

Een ontbrekende tenant heeft terecht geen hits. Met k=0 wordt nog steeds de invoervorm gecontroleerd, maar er worden geen scores berekend. Onze bovengrenzen zijn twaalf records, twee gehele coördinaten tussen -8 en 8 en k tussen 0 en 6.

Samenvatting

  • Een vertrouwde tenantscope bepaalt welke kaarten mogen meedoen vóór de rangschikking.
  • Achteraf filteren kan een gewone taak verdringen, ook zonder vreemde tekst terug te geven.
  • Controleer zowel de begrenzing als het behouden van toegestane resultaten.

In de volgende les onderzoek je welke inhoud überhaupt in de beoordeelde verzameling thuishoort.

DE VOLGENDE STAPVertrouw herkomst buiten het document
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 2
01
Toon het gevolg van een te late tenantfilter

Maak postfiltered_search(records, query, tenant, k): rangschik alle records en filter pas na de top-k op tenant. Geef hits met id/score en compared_ids in invoervolgorde terug. Sorteer op aflopende score, daarna oplopend id. Gebruik de gegeven validators; k is een geheel getal van 0 tot 6 en geeft bij nul twee lege lijsten.

Een hint

Bereken eerst de globale rangschikking en pas daarna het tenantfilter; houd het vergelijkingsspoor apart.

02
Begrens de tenant vóór het scoren

Maak scoped_search met dezelfde uitvoervorm en sortering. Filter op de vertrouwde tenant vóór het vergelijken en rangschikken; compared_ids bevat uitsluitend die records, in invoervolgorde. Gebruik de gegeven validators en dezelfde k-regels. De publieke taak moet blijven werken zonder buitenlandse records te vergelijken.

Een hint

De vertrouwde scope bepaalt welke records mogen deelnemen aan de numerieke vergelijking.

Vastgelopen? Bekijk de voorbeeldoplossing
import re
_ID = re.compile('[a-z][a-z0-9-]{0,31}', re.ASCII)

# Jouw functies

def postfiltered_search(records, query, tenant, k):
    data, vector, scope = (_records(records), _vector(query), _id(tenant))
    if type(k) is not int or not 0 <= k <= 6:
        raise ValueError('k')
    if k == 0:
        return {'hits': [], 'compared_ids': []}
    ranked = [(row, sum((a * b for a, b in zip(row['vector'], vector)))) for row in data]
    ranked.sort(key=lambda pair: (-pair[1], pair[0]['id']))
    hits = [{'id': row['id'], 'score': score} for row, score in ranked[:k] if row['tenant'] == scope]
    return {'hits': hits, 'compared_ids': [row['id'] for row in data]}

def scoped_search(records, query, tenant, k):
    data, vector, scope = (_records(records), _vector(query), _id(tenant))
    if type(k) is not int or not 0 <= k <= 6:
        raise ValueError('k')
    if k == 0:
        return {'hits': [], 'compared_ids': []}
    permitted = [row for row in data if row['tenant'] == scope]
    ranked = [(row, sum((a * b for a, b in zip(row['vector'], vector)))) for row in permitted]
    ranked.sort(key=lambda pair: (-pair[1], pair[0]['id']))
    return {'hits': [{'id': row['id'], 'score': score} for row, score in ranked[:k]], 'compared_ids': [row['id'] for row in permitted]}

# Gegeven validatiehelpers

def _record(value, keys):
    if type(value) is not dict or any((type(key) is not str for key in value)) or set(value) != set(keys):
        raise ValueError('record')
    return value

def _id(value):
    if type(value) is not str or _ID.fullmatch(value) is None:
        raise ValueError('id')
    return value

def _text(value):
    if type(value) is not str or len(value) > 256:
        raise ValueError('text')
    try:
        value.encode('utf-8')
    except UnicodeEncodeError:
        raise ValueError('utf8') from None
    return value

def _vector(value):
    if type(value) not in (list, tuple) or len(value) != 2:
        raise ValueError('vector')
    if any((type(item) is not int or not -8 <= item <= 8 for item in value)):
        raise ValueError('coordinate')
    return list(value)

def _document(value):
    _record(value, ('id', 'tenant', 'source', 'revision', 'text', 'vector'))
    return {'id': _id(value['id']), 'tenant': _id(value['tenant']), 'source': _id(value['source']), 'revision': _id(value['revision']), 'text': _text(value['text']), 'vector': _vector(value['vector'])}

def _records(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('records')
    result = [_document(row) for row in value]
    if len({row['id'] for row in result}) != len(result):
        raise ValueError('duplicate record')
    return result

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.