Del via


Integrer en tredjepartsmotor med OneLake-sikkerhed (forhåndsvisning)

Denne artikel beskriver, hvordan tredjeparts motorudviklere kan integrere med OneLake-sikkerhed for at forespørge data fra OneLake, samtidig med at de håndhæver række-niveau sikkerhed (RLS) og kolonne-niveau sikkerhed (CLS). Integrationen bruger den autoriserede motormodel, hvor din motor læser data direkte fra OneLake og håndhæver sikkerhedspolitikker i sit eget beregningslag.

Bemærkning

Denne funktion er en del af en forhåndsvisning og leveres kun til evaluering og udvikling. Det kan ændre sig baseret på feedback og anbefales ikke til produktionsbrug.

Oversigt

OneLake-sikkerhed definerer fintgåede adgangskontrolpolitikker – herunder tabel-, række- og kolonne-niveau sikkerhed – når de er installeret i OneLake. Microsoft Fabric-motorer som Spark og SQL-analyse-endpointet håndhæver disse politikker ved forespørgselstidspunktet. Dog garanterer OneLake-sikkerhed håndhævelse af detaljerede adgangskontrolpolitikker uanset hvordan dataene tilgås. Som følge heraf blokeres uautoriserede eksterne anmodninger om at læse filer fra OneLake for at sikre, at data ikke lækkes.

Den autoriserede motormodel løser dette problem. Du registrerer en dedikeret identitet (service principal eller managed identity), som har fuld læseadgang til dataene og også kan læse sikkerhedsmetadata. Din engine bruger denne identitet til at:

  1. Læs rådatafilerne fra OneLake.
  2. Hent de effektive sikkerhedspolitikker for en given bruger ved at kalde Get autoriseret adgang til en hoved-API .
  3. Anvend de returnerede række- og kolonnefiltre i dets eget forespørgselsudførelseslag.
  4. Returnér kun de tilladte data til slutbrugeren.

Denne tilgang giver din engine fuld kontrol over forespørgselsplanlægning og caching, samtidig med at sikkerhedshåndhævelsen holdes i overensstemmelse med, hvad Fabric-motorer leverer, og kontrollen over autorisationen overlades til brugeren.

Forudsætninger

Før du begynder at integrere, skal du sikre dig, at du har følgende:

  • En Microsoft Entra-tjenesteprincipal eller administreret identitet , som din engine bruger til at få adgang til OneLake. Kun Microsoft Entra-identiteter understøttes.
  • Workspace Member (eller højere) rolle for engine-identiteten i målarbejdsområdet. Dette giver identiteten de nødvendige privilegier til at læse datafiler og sikkerhedsmetadata fra OneLake.
  • Et Fabric-element (lakehouse, spejlet database eller spejlet katalog) med OneLake-sikkerhed aktiveret.
  • OneLake sikkerhedsroller konfigureret på varen med eventuelle RLS- eller CLS-politikker, du ønsker at håndhæve.
  • Motorens identitet skal have ubegrænset læseadgang til de tabeller, den læser. Hvis RLS- eller CLS-politikker gælder for selve motoridentiteten, returnerer API-kald fejl.

Arkitektur

Følgende diagram viser det overordnede autorisationsflow for en autoriseret motorintegration.

┌──────────────┐       ┌──────────────────┐       ┌───────────┐
│  End user    │──1──▶│  3rd-party engine │──2──▶│  OneLake  │
│  (query)     │       │(service principal)│◀──3──│  (data +  │
│              │◀──6──│                   │──4──▶│  security)│
└──────────────┘       └──────────────────┘       └───────────┘
  1. Slutbrugeren indsender en forespørgsel til tredjepartsmotoren.
  2. Motorens identitet autentificeres til OneLake og læser de rå datafiler (Delta parquet) ved hjælp af OneLake API'er.
  3. OneLake returnerer de ønskede data.
  4. Motoren kalder API'et principalAccess og sender slutbrugerens Microsoft Entra-objekt-ID for at få brugerens effektive adgang.
  5. Motoren anvender de returnerede adgangsfiltre (tabeladgang, RLS-prædikater, CLS-kolonnelister) på dataene i sit eget beregningslag.
  6. Motoren returnerer kun de filtrerede, tilladte resultater til slutbrugeren.

Trin 1: Opsæt motoridentiteten

Din engine har brug for en Microsoft Entra-identitet, som OneLake genkender og stoler på. Denne identitet læser datafiler og sikkerhedsmetadata på vegne af din motor.

  1. Opret eller identificer en serviceprincipal eller administreret identitet i Microsoft Entra ID til din engine. Du kan få flere oplysninger under Objekter for program- og tjenesteprincipal i Microsoft Entra ID.

  2. Tilføj identiteten til rollen som arbejdsområdemedlem. I Fabric-portalen skal du gå til arbejdsområdets indstillinger og tilføje serviceprincipalen til medlemrollen . Dette giver identiteten:

    • Læs adgang til alle datafiler i OneLake for elementer i det arbejdsområde.
    • Adgang til at læse OneLake sikkerhedsrollemetadata via de autoriserede engine API'er.

    For mere information om arbejdsområder, se Roller i arbejdsområder.

  3. Sørg for, at identiteten har ubegrænset adgang. Motoridentiteten skal have fuld læseadgang til hver tabel, den forespørger i. Hvis en hvilken som helst OneLake-sikkerhedsrolle anvender RLS- eller CLS-begrænsninger på motorens identitet, fejler datalæsninger og API-kald. Den bedste praksis er ikke at tilføje motoridentiteten til nogen OneLake-sikkerhedsroller, der indeholder RLS- eller CLS-begrænsninger.

Vigtigt!

Du kan til enhver tid tilbagekalde motorens adgang ved at fjerne den fra arbejdsområdet. Tilbagekaldelse af adgang træder i kraft inden for cirka 2 minutter.

Trin 2: Læs data fra OneLake

Med motoridentiteten konfigureret kan din motor læse datafiler direkte fra OneLake ved hjælp af de standard Azure Data Lake Storage (ADLS) Gen2-kompatible API'er.

OneLake-data er tilgængelige på:

https://onelake.dfs.fabric.microsoft.com/{workspaceId}/{itemId}/Tables/{schema}/{tableName}/

Din motor autentificerer ved hjælp af et bærertoken opnået gennem Microsoft Entra OAuth 2.0 klientens loginoplysninger. Brug OneLake-ressourceomfanget https://storage.azure.com/.default , når du anmoder om tokenet.

Eksempel: Autentificere og læse data (Python)

from azure.identity import ClientSecretCredential
from azure.storage.filedatalake import DataLakeServiceClient

tenant_id = "<your-tenant-id>"
client_id = "<your-service-principal-client-id>"
client_secret = "<your-service-principal-secret>"

credential = ClientSecretCredential(tenant_id, client_id, client_secret)

service_client = DataLakeServiceClient(
    account_url="https://onelake.dfs.fabric.microsoft.com",
    credential=credential
)

# Access a specific item in a workspace
file_system_client = service_client.get_file_system_client("<workspace-id>")
directory_client = file_system_client.get_directory_client("<item-id>/Tables/dbo/Customers")

# List and read Delta parquet files
for path in directory_client.get_paths():
    if path.name.endswith(".parquet"):
        file_client = file_system_client.get_file_client(path.name)
        downloaded = file_client.download_file()
        data = downloaded.readall()
        # Process the parquet data with your engine

For mere information om OneLake API'er, se OneLake adgang med API'er.

Trin 3: Hent brugerens effektive adgang

Efter at have læst rådataene skal din motor afgøre, hvad den forespørgende bruger må se. Kald Get autoriseret adgang til en principal API for at få brugerens effektive adgang til varen.

API-slutpunkt

GET https://onelake.dfs.fabric.microsoft.com/v1.0/workspaces/{workspaceId}/artifacts/{artifactId}/securityPolicy/principalAccess

Brødtekst i anmodning

{
  "aadObjectId": "<end-user-entra-object-id>",
  "inputPath": "Tables",
  "maxResults": 500 //optional, default is 500
}
Parameter Type Påkrævet Beskrivelse
aadObjectId streng Ja Microsoft Entra-objekt-ID'et på slutbrugeren, hvis adgang du vil tjekke.
inputPath streng Ja Enten Tables eller Files. Returnerer brugerens adgang til den angivne sektion af genstanden. For de fleste forespørgselsmotorer vil inputPath være Tables.
fortsættelseToken streng Nej Bruges til at hente fortsatte resultater, når resultatsættet overstiger maxResults.
maxResults heltal Nej Maksimalt antal elementer pr. side. Standard er 500.

Eksempelrespons (kun RLS)

{
  "identityETag": "3fc4dc476ded773e4cf43936190bf20fa9480a077b25edc0b4bbe247112542f6",
  "metadataETag": "\"eyJhciI6IlwiMHg4R...\"",
  "value": [
    {
      "path": "Tables/dbo/Customers",
      "access": ["Read"],
      "rows": "SELECT * FROM [dbo].[Customers] WHERE [customerId] = '123'",
      "effect": "Permit"
    },
    {
      "path": "Tables/dbo/Employees",
      "access": ["Read"],
      "rows": "SELECT * FROM [dbo].[Employees] WHERE [address] = '123'",
      "effect": "Permit"
    },
    {
      "path": "Tables/dbo/EmployeeTerritories",
      "access": ["Read"],
      "effect": "Permit"
    }
  ]
}

Prøverespons (RLS og CLS)

Når sikkerhed på kolonneniveau konfigureres på en tabel, indeholder svaret et columns array, der kun viser de kolonner, brugeren har adgang til. Kolonner, der ikke findes i dette array, er skjult for brugeren.

{
  "identityETag": "79372bc169b00882d9abec3d404032131e96bc406e15c6766514723021e153eb",
  "metadataETag": "\"eyJhciI6IlwiMHg4R...\"",
  "value": [
    {
      "path": "Tables/dbo/Customers",
      "access": ["Read"],
      "columns": [
        {
          "name": "address",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        },
        {
          "name": "city",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        },
        {
          "name": "contactTitle",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        },
        {
          "name": "country",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        },
        {
          "name": "fax",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        },
        {
          "name": "phone",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        },
        {
          "name": "postalCode",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        },
        {
          "name": "region",
          "columnEffect": "Permit",
          "columnAction": ["Read"]
        }
      ],
      "rows": "SELECT * FROM [dbo].[Customers] WHERE [customerID] = 'ALFKI'",
      "effect": "Permit"
    },
    {
      "path": "Tables/dbo/Employees",
      "access": ["Read"],
      "rows": "SELECT * FROM [dbo].[Employees] WHERE [address] = '123'",
      "effect": "Permit"
    }
  ]
}

Forståelse af responsen

Svaret indeholder et array af PrincipalAccessEntry objekter, hvor hver repræsenterer en tabel, som brugeren har adgang til. Tabeller, der ikke er til stede i svaret, er ikke tilgængelige for brugeren.

Felt Type Beskrivelse
path streng Stien til tabellen, som brugeren kan tilgå, for eksempel Tables/dbo/Customers.
access streng[] Udvalget af adgangstyper givet. Er det kun Read understøttet i øjeblikket.
columns Objekt[] Et array af kolonneobjekter, som brugeren har adgang til. Hvert objekt indeholder name (kolonnenavn), columnEffect (Permit), og columnAction (["Read"]). Hvis dette felt mangler, gælder ingen CLS, og alle kolonner er tilladt. Hvis de er til stede, bør kun de angivne kolonner returneres.
rows streng En T-SQL-sætning SELECT , der repræsenterer sikkerhedsfilteret på rækkeniveau. Kun rækker, der matcher dette prædikat, bør returneres til brugeren. Hvis dette felt mangler, gælder der ingen RLS, og alle rækker er tilladt.
effect streng Effekttypen. Lige nu altid Permit.

Vigtigt!

Feltet rows indeholder et T-SQL-udtryk, som din engine skal parse og anvende som et filterprædikat. Udtrykket bruger et SELECT * FROM [schema].[table] WHERE ... format. Din engine skal udtrække klausulen WHERE og anvende den på de data, der returneres.

ETags til caching

Svaret indeholder to ETag-værdier, der muliggør effektiv caching:

  • identityETag: Repræsenterer brugerens nuværende identitet og gruppemedlemskab. Cache brugerens adgangsresultat og genbrug det, indtil denne ETag ændres.
  • metadataETag: Repræsenterer den aktuelle tilstand af genstandens sikkerhedskonfiguration. Cache rollemetadata og genbrug dem, indtil denne ETag ændres.

Brug disse ETags sammen med If-None-Match anmodningsheaderen for at undgå at hente uændrede data igen. Dette forbedrer ydeevnen for multi-bruger caches.

Eksempel: Hent effektiv adgang (Python)

import requests

# Get a token for the OneLake DFS endpoint
token = credential.get_token("https://storage.azure.com/.default").token

workspace_id = "<workspace-id>"
artifact_id = "<artifact-id>"
user_object_id = "<end-user-entra-object-id>"

url = (
    f"https://onelake.dfs.fabric.microsoft.com/v1.0/"
    f"workspaces/{workspace_id}/artifacts/{artifact_id}/"
    f"securityPolicy/principalAccess"
)

headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "application/json"
}

body = {
    "aadObjectId": user_object_id,
    "inputPath": "Tables"
}

response = requests.get(url, headers=headers, json=body)
access_data = response.json()

# The response contains the user's effective access
for entry in access_data["value"]:
    print(f"Table: {entry['path']}, Access: {entry['access']}")
    if "columns" in entry:
        col_names = [col["name"] for col in entry["columns"]]
        print(f"  CLS permitted columns: {col_names}")
    if "rows" in entry:
        print(f"  RLS filter: {entry['rows']}")

Trin 4: Anvend sikkerhedsfiltre

Efter at have hentet brugerens effektive adgang, skal din motor anvende sikkerhedspolitikkerne på dataene, før resultaterne returneres. Dette trin er afgørende – din engine er ansvarlig for korrekt håndhævelse af politikkerne.

Tabelniveau-filtrering

Returner kun data fra tabeller, der vises i svaret principalAccess . Hvis en tabel ikke er opført, har brugeren ingen adgang til den, og ingen data bør returneres.

# Build a set of accessible tables for the user
accessible_tables = {entry["path"] for entry in access_data["value"]}

# Before returning query results, verify the table is accessible
def is_table_accessible(table_path: str) -> bool:
    return table_path in accessible_tables

Sikkerhedsfiltrering på rækkeniveau

Når et rows felt er til stede i en adgangspost, skal din engine parse T-SQL-prædikatet og anvende det som et filter på tabellens data. Værdien rows er en SELECT sætning med en WHERE klausul, der definerer, hvilke rækker brugeren kan se.

Vigtigt!

Hvis din engine ikke kan parse SQL-sætninger, burde forespørgsler mod tabeller med en ikke-null rows egenskab fejle med en fejl og ikke returnere nogen data. Dette sikrer, at brugerne kun får adgang til det, de har lov til at se.

For eksempel følgende RLS-filter:

SELECT * FROM [dbo].[Customers] WHERE [customerId] = '123' UNION SELECT * FROM [dbo].[Customers] WHERE [customerID] = 'ALFKI'

Din engine bør udtrække prædikaterne og anvende dem til at filtrere dataene:

import sqlparse

def extract_rls_predicates(rls_expression: str) -> list:
    """
    Parse the RLS T-SQL expression and extract WHERE clause predicates.
    The expression may contain UNION of multiple SELECT statements.
    """
    predicates = []
    statements = rls_expression.split(" UNION ")
    for stmt in statements:
        parsed = sqlparse.parse(stmt)[0]
        where_seen = False
        where_clause = []
        for token in parsed.tokens:
            if where_seen:
                where_clause.append(str(token).strip())
            if token.ttype is sqlparse.tokens.Keyword and token.value.upper() == "WHERE":
                where_seen = True
        if where_clause:
            predicates.append(" ".join(where_clause))
    return predicates


def apply_rls_filter(dataframe, access_entry: dict):
    """Apply RLS filtering to a dataframe based on the access entry."""
    if "rows" not in access_entry:
        return dataframe  # No RLS, return all rows

    predicates = extract_rls_predicates(access_entry["rows"])
    # Combine predicates with OR (UNION semantic)
    combined_filter = " OR ".join(f"({p})" for p in predicates)
    return dataframe.filter(combined_filter)

Vigtigt!

Når rows feltet mangler i en adgangspost, gælder der ingen RLS for den tabel, og alle rækker bør returneres. Når feltet er til stede, skal din motor filtrere dataene. At returnere ufiltrerede data for en tabel med RLS er en sikkerhedsovertrædelse.

Sikkerhedsfiltrering på kolonneniveau

Når CLS konfigureres på en tabel, indeholder svaret principalAccess et columns array, der eksplicit lister de kolonner, brugeren har tilladelse til at få adgang til. Hvert kolonneobjekt indeholder:

Property Type Beskrivelse
name streng Kolonnenavnet (kasusfølsomt).
columnEffect streng Effekten gjaldt for kolonnen. Lige nu altid Permit.
columnAction streng[] De tilladte handlinger på kolonnen. Er det kun Read understøttet i øjeblikket.

Hvis columnsfeltet mangler i en adgangspost, gælder der ingen CLS, og alle kolonner i tabellen er tilladt. Hvis columns feltet er til stede, skal din motor kun returnere de oplyste kolonner.

def get_permitted_columns(access_entry: dict) -> list | None:
    """
    Return the list of permitted column names for a table.
    Returns None if no CLS applies (all columns are permitted).
    """
    if "columns" not in access_entry:
        return None  # No CLS, all columns are permitted

    return [
        col["name"]
        for col in access_entry["columns"]
        if col.get("columnEffect") == "Permit"
        and "Read" in col.get("columnAction", [])
    ]


def apply_cls_filter(dataframe, access_entry: dict):
    """Apply CLS filtering to a dataframe based on the access entry."""
    permitted_columns = get_permitted_columns(access_entry)
    if permitted_columns is None:
        return dataframe  # No CLS, return all columns

    # Only keep columns that are in the permitted list
    return dataframe.select(permitted_columns)

Vigtigt!

Når columns feltet mangler i en adgangspost, gælder der ingen CLS, og alle kolonner bør returneres. Når feltet er til stede, skal din motor kun returnere de angivne kolonner. At returnere skjulte søjler er en sikkerhedsovertrædelse.

Håndtering af tabeller uden adgang

Hvis en bruger forespørger en tabel, der ikke optræder i svaret principalAccess , skal din engine nægte adgang. Fald ikke tilbage på at returnere ufiltrerede data.

def query_table(table_path: str, user_access: dict):
    """Query a table with OneLake security enforcement."""
    # Find the user's access entry for this table
    entry = next(
        (e for e in user_access["value"] if e["path"] == table_path),
        None
    )

    if entry is None:
        raise PermissionError(
            f"Access denied: user doesn't have permission to access {table_path}"
        )

    # Read the data from OneLake
    data = read_table_from_onelake(table_path)

    # Apply column-level security
    data = apply_cls_filter(data, entry)

    # Apply row-level security
    data = apply_rls_filter(data, entry)

    return data

Trin 5: Håndter caching og ændringsdetektion

For produktionsintegrationsniveau, især motorer med multi-bruger datacaches, skal du håndtere ændringer i sikkerhedspolitikker og medlemskaber af brugergrupper.

Cache-sikkerhedsmetadata

Brug og-værdierne identityETagmetadataETag fra svaret principalAccess til at afgøre, hvornår cachet sikkerhedsinformation er forældet:

  • identityETag: Ændres, når brugerens gruppemedlemskaber eller identitetsegenskaber opdateres. Cache brugerens effektive adgang, der er tastet på (userId, identityETag).
  • metadataETag: Ændres, når OneLake-sikkerhedsrollerne eller -politikkerne på genstanden opdateres. Cache-rolledefinitioner baseret på (artifactId, metadataETag).

Afstemning om ændringer

Poll API'en principalAccess periodisk for at opdage ændringer. API'et bør polles før forespørgselskørsel for at sikre, at intet er ændret, i stedet for at levere resultater direkte fra cachen. Brug If-None-Match headeren med det tidligere modtagne ETag for at minimere båndbredden:

headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "application/json",
    "If-None-Match": f'"{cached_etag}"'
}

response = requests.get(url, headers=headers, json=body)

if response.status_code == 304:
    # Security hasn't changed, use cached data
    pass
elif response.status_code == 200:
    # Security has changed, update cache
    new_access_data = response.json()
    update_cache(user_id, new_access_data)

Ventetidsovervejelser

  • Ændringer i OneLakes definitioner af sikkerhedsroller tager cirka 5 minutter at udbrede.
  • Ændringer i brugergruppemedlemskaber i Microsoft Entra ID tager cirka 1 time at blive vist i OneLake.
  • Nogle stofmotorer har deres eget caching-lag, så det kan kræve ekstra tid.

Design dit pollinginterval og cache TTL derefter. En anbefalet tilgang er at spørge hvert 5. minut for ændringer i sikkerhedsmetadata og opdatere brugerspecifik adgang ved hver forespørgsel eller med kortere intervaller.

Trin 6: Håndter paginering

API'et principalAccess understøtter paginering for elementer med mange tabeller. Når svaret indeholder flere elementer end maxResults, indeholder svaret en continuationToken.

all_entries = []
continuation_token = None

while True:
    body = {
        "aadObjectId": user_object_id,
        "inputPath": "Tables",
        "maxResults": 500
    }
    if continuation_token:
        body["continuationToken"] = continuation_token

    response = requests.get(url, headers=headers, json=body)
    data = response.json()
    all_entries.extend(data["value"])

    # Check for continuation token in response
    continuation_token = data.get("continuationToken")
    if not continuation_token:
        break

Fejlhåndtering

Håndter følgende fejlscenarier i din integration:

HTTP-status Fejlkode Beskrivelse Anbefalet handling
200 - Success. Behandl svaret.
404 ItemNotFound Arbejdsområdet eller objektet eksisterer ikke, eller motoridentiteten har ikke adgang. Verificér arbejdsområde-ID og artefakt-ID. Bekræft at engine-identiteten har workspace-medlem-adgang.
412 ForudsætningMislykkedes Den leverede ETag stemmer If-Match ikke overens med den nuværende ressource ETag. Hent ressourcen igen uden If-Match headeren for at få den nyeste ETag.
429 - Hastighedsgrænsen er overskredet. Vent på den varighed, der er angivet i Retry-After headeren, før du prøver igen.

Bedste praksis for sikkerhed

Følg disse bedste praksisser for at sikre en sikker integration:

  • Beskyt motorens identitetsoplysninger. Servicelederen har øget adgangen til data i OneLake. Gem legitimationsoplysninger sikkert ved hjælp af tjenester som Azure Key Vault.
  • Eksponér ikke rådata for slutbrugerne. Anvend altid de sikkerhedsfiltre, API'et returnerer principalAccess , før du returnerer nogen data. At springe håndhævelsen over er et sikkerhedsbrud.
  • Valider RLS-prædikater omhyggeligt. Parse og anvend T-SQL-klausulens WHERE prædikater nøjagtigt. Forkert parsing kan føre til datalækage. Hvis parsingsfejl eller usikker syntaksmapping opstår, fejl forespørgslen med en RLS-parsingfejl i stedet for at vise delvise eller usikre resultater for brugeren.
  • Håndter manglende tabeller, da adgang nægtes. Hvis en tabel ikke er til stede i API-svaret, har brugeren ikke adgang. Fald aldrig tilbage på ufiltrerede data, OneLake-sikkerhed bruger altid afvisning som standard.
  • Auditadgang. Log hvilke brugere der får adgang til hvilke tabeller, og hvilke sikkerhedspolitikker der blev anvendt til overholdelse og fejlfinding.
  • Afstemning om sikkerhedsændringer. Brug ETags til at opdage ændringer og opdatere cachede politikker hurtigt.

Begrænsninger

  • API'en principalAccess er i forhåndsvisning og kan ændre sig baseret på feedback.
  • Kun adgangstypen Read og Permit effekten understøttes i dag.
  • Motoridentiteten skal have ubegrænset root-niveau adgang. Hvis RLS eller CLS gælder for motorens identitet, fejler API-kald.
  • RLS-prædikater bruger T-SQL-syntaks. Din engine er ansvarlig for at parse og anvende prædikaterne korrekt.
  • Ændringer i sikkerhedspolitikken tager cirka 5 minutter at udbrede. Ændringer af medlemskab i brugergrupper tager cirka 1 time.