將第三方引擎與 OneLake 安全性整合(預覽版)

本文說明第三方引擎開發者如何整合 OneLake 安全性,以查詢 OneLake 的資料,同時執行列級安全(RLS)與欄位級安全(CLS)。 此整合採用 授權引擎模型,引擎直接從 OneLake 讀取資料,並在自身計算層強制執行安全政策。

備註

此功能為預覽版的一部分,僅供評估與開發用途。 它可能會根據意見反應而變更,不建議用於生產環境。

概觀

OneLake 安全定義了細緻的存取控制政策,包括資料表層級、列層級和欄位層級的安全,只需在 OneLake 中設定一次。 像 Spark 和 SQL 分析端點這類 Fabric 引擎會在查詢時強制執行這些政策。 然而,OneLake 安全保證無論資料如何存取,都能執行細緻的存取控制政策。 因此,未經授權的外部讀取 OneLake 文件的請求會被阻擋,以確保資料不會外洩。

授權的引擎型號解決了這個問題。 你註冊一個專用身份(服務主體或管理身份),擁有資料的完整讀取權限,也能讀取安全元資料。 你的引擎會利用這個身份來:

  1. 閱讀 OneLake 的原始資料檔。
  2. 透過呼叫 獲取授權訪問主體 API來取得特定使用者的有效安全策略。
  3. 在自己的查詢執行層中套用回傳的列與欄篩選。
  4. 只將允許的資料回傳給最終使用者。

這種方式讓引擎能完全掌控查詢規劃與快取,同時保持安全執行與 Fabric 引擎所提供的一致性,並將 授權 控制權交由使用者掌握。

先決條件

在開始整合之前,請確保你具備以下條件:

  • Microsoft Entra 服務主體或管理身份,你的引擎用來存取 OneLake。 僅支援 Microsoft Entra 身分。
  • Workspace 成員(或更高)角色,代表目標工作空間中引擎身份。 這賦予該身份必要的權限,可以讀取 OneLake 的資料檔案和安全元資料。
  • 啟用了 OneLake 安全性的 Fabric 項目(lakehouse、鏡像資料庫或鏡像目錄)。
  • OneLake 安全角色會在物品 上設定 ,並搭配你想強制執行的任何 RLS 或 CLS 政策。
  • 引擎身份必須對它所讀取的資料表擁有 不受限制的讀取權限 。 若 RLS 或 CLS 政策套用於引擎身份本身,API 呼叫會回傳錯誤。

建築

下圖顯示授權引擎整合的高階授權流程圖。

┌──────────────┐       ┌──────────────────┐       ┌───────────┐
│  End user    │──1──▶│  3rd-party engine │──2──▶│  OneLake  │
│  (query)     │       │(service principal)│◀──3──│  (data +  │
│              │◀──6──│                   │──4──▶│  security)│
└──────────────┘       └──────────────────┘       └───────────┘
  1. 最終使用者向第三方引擎提交查詢。
  2. 引擎識別碼會向 OneLake 進行認證,並使用 OneLake API 讀取原始資料檔案(Delta parquet)。
  3. OneLake 會回傳所請求的資料。
  4. 引擎會呼叫 principalAccess API,傳遞最終使用者的 Microsoft Entra 物件 ID,以取得使用者的有效存取權限。
  5. 引擎會將回傳的存取過濾器(資料表存取、RLS 謂詞、CLS 欄位清單)套用到其自身的計算層資料上。
  6. 引擎僅將經過過濾且允許的結果回傳給最終使用者。

步驟 1:設定引擎識別碼

你的引擎需要一個 OneLake 能辨識並信任的 Microsoft Entra 身份。 這個身份會代表你的引擎讀取資料、檔案和安全元資料。

  1. 請在 Microsoft Entra ID 中建立或識別您引擎的服務主體或受管理身份。 如需詳細資訊,請參閱 Microsoft Entra ID 中的應用程式和服務主體物件。

  2. 將身份加入工作區的成員角色。 請在 Fabric 入口網站中,進入工作區選項,將服務主體加入成員角色。 這授予身份:

    • 讀取 OneLake 中該工作區所有資料檔的讀取權限。
    • 透過授權引擎 API 存取 OneLake 安全角色的元資料。

    欲了解更多工作區角色資訊,請參閱 工作空間中的角色。

  3. 確保該身份擁有不受限制的存取權限。 引擎身份必須對每個查詢的資料表擁有完整的讀取權限。 若任何 OneLake 安全角色對引擎身份套用 RLS 或 CLS 限制,資料讀取與 API 呼叫皆失敗。 最佳做法是不將引擎標識加入任何含有 RLS 或 CLS 限制的 OneLake 安全角色中。

這很重要

你可以隨時透過將引擎從工作區角色中移除來撤銷其存取權限。 撤銷存取權約在兩分鐘內生效。

步驟 2:讀取 OneLake 的資料

在引擎身份設定完成後,你的引擎可以直接使用標準的 Azure Data Lake Storage(ADLS)Gen2 相容 API 讀取 OneLake 的資料檔案。

OneLake 資料可於以下網址取得:

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

你的引擎是透過 Microsoft Entra OAuth 2.0 用戶端憑證流程取得的持有憑證來認證的。 請求代幣時請使用 OneLake 資源範圍 https://storage.azure.com/.default 。

範例:驗證與讀取資料(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

欲了解更多 OneLake API 資訊,請參閱 OneLake 與 API 存取。

步驟 3:取得使用者的有效存取權限

讀取原始資料後,你的引擎必須判斷查詢使用者被允許看到什麼。 呼叫 取得主體的授權存取 API 以獲取使用者對該項目的有效存取權。

API 端點

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

請求主體

{
  "aadObjectId": "<end-user-entra-object-id>",
  "inputPath": "Tables",
  "maxResults": 500 //optional, default is 500
}
參數 類型 Required 說明
aadObjectId 字串 是的 你想檢查存取權限的最終使用者的 Microsoft Entra 物件 ID。
輸入路徑 字串 是的 Tables 或 Files。 回傳使用者對該項目指定區段的存取權限。 對大多數查詢引擎來說,inputPath 會是 Tables。
continuationToken 字串 No 當結果集超過 maxResults時,用於取得持續結果。
maxResults 整數 No 每頁最多項目數。 預設值為 500。

取樣響應(僅限 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"
    }
  ]
}

樣本響應(RLS 與 CLS)

當資料表上設定欄位層級安全性時,回應會包含 columns 一個陣列,僅列出使用者被允許存取的欄位。 未在此陣列中出現的欄位會對使用者隱藏。

{
  "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"
    }
  ]
}

理解回應

回應包含一個物件陣列 PrincipalAccessEntry ,每個物件代表使用者可存取的一個資料表。 回應中不存在的資料表則無法被使用者存取。

領域 類型 說明
path 字串 使用者可存取的資料表路徑,例如 Tables/dbo/Customers。
access 字串[] 已授予的存取類型陣列。 目前僅支援 Read。
columns 物件[] 使用者被允許存取的欄位物件陣列。 每個物件包含 name (欄位名稱)、 columnEffect (Permit)、( 和 columnAction )。["Read"] 若缺少此欄位,則不適用 CLS,且允許所有欄位。 如果存在,只回傳列出的欄位。
rows 字串 一個代表列層安全過濾器的 T-SQL SELECT 陳述句。 只有符合此謂詞的列才應回傳給使用者。 若缺少此欄位,則不適用 RLS,允許所有列。
effect 字串 效果類型。 目前一律為 Permit。

這很重要

欄位包含 rows 一個 T-SQL 表達式,你的引擎必須解析並套用它作為過濾謂詞。 這個表達式使用一種 SELECT * FROM [schema].[table] WHERE ... 格式。 你的引擎必須擷取該 WHERE 子句並套用到回傳的資料上。

快取用的 ETag

回應包含兩個ETag值,以促進高效的快取:

  • identityETag: 代表使用者身份及群組成員狀態的當前狀態。 快取使用者的存取結果並重複使用,直到這個 ETag 改變。
  • metadataETag: 代表該物品安全配置的當前狀態。 快取角色元資料,並在這個 ETag 改變前重複使用。

使用這些 ETag 搭配 If-None-Match 請求標頭,以避免重新取得未更改的資料。 這提升了多使用者快取的效能。

範例:擷取有效存取(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']}")

步驟四:套用安全過濾器

取得使用者有效存取權後,引擎必須對資料套用安全政策,才能回傳結果。 這一步至關重要——你的引擎負責正確執行政策。

資料表層級過濾

只回傳回應中 principalAccess 出現的資料表資料。 如果資料表未被列出,使用者無法存取該資料,也不會回傳資料。

# 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

列層級安全性篩選

當存取項目中出現欄位 rows 時,引擎必須解析 T-SQL 謂詞,並將其作為資料表資料的過濾器套用。 這個 rows 值是一個 SELECT 帶有 WHERE 子句的陳述,定義使用者可以看到哪些列。

這很重要

如果你的引擎無法解析 SQL 語句,那麼對具有非 null rows 屬性的資料表的查詢應該會錯誤失敗,且不會回傳任何資料。 這確保用戶只能存取他們被允許看到的內容。

例如,以下 RLS 濾波器:

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

你的引擎應該會擷取這些謂詞並套用它們來過濾資料:

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)

這很重要

當存取項目缺少該 rows 欄位時,該資料表不會適用 RLS,所有列都應回傳。 當欄位存在時,您的引擎必須過濾資料。 用 RLS 回傳未過濾的資料表是安全違規。

欄位層級安全過濾

當在資料表上設定 CLS 時,回應包含一個陣列,會明確列出使用者可存取的欄位。 每個欄位物件包含:

房產 類型 說明
name 字串 欄位名稱(大小寫區分)。
columnEffect 字串 這個效果也反映在柱子上。 目前一律為 Permit。
columnAction 字串[] 在欄位上允許的操作。 目前僅支援 Read。

若存取項目columns該欄位,則不適用 CLS,且允許資料表中的所有欄位。 如果columns欄位存在,你的引擎必須只回傳列出的欄位。

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)

這很重要

當存取項目中缺少該 columns 欄位時,則不適用 CLS,所有欄位都應回傳。 當欄位存在時,你的引擎只需回傳列出的欄位。 歸還隱藏欄位是安全違規。

處理無法存取的資料表

如果使用者查詢的表格不在回應中 principalAccess ,你的引擎必須拒絕存取。 不要退回未過濾的資料。

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

步驟 5:處理快取與變更偵測

對於生產級整合,尤其是具備多使用者資料快取的引擎,你需要處理安全政策和使用者群組成員的變更。

快取安全元資料

利用identityETag回應中的 metadataETag and principalAccess 值判斷快取安全資訊何時過期:

  • identityETag:當使用者的群組成員身份或身份屬性更新時,會有變化。 將使用者的有效存取權限快取,並以(userId, identityETag)作為索引。
  • metadataETag:OneLake 的安全角色或項目政策更新時所發生的變動。 快取角色定義以 (artifactId, metadataETag)為關鍵。

變動投票

定期輪詢 principalAccess API 以偵測變更。 API 應該在查詢執行前輪詢,以確保沒有變動,而不是直接從快取中提供結果。 使用先前接收的 If-None-Match 搭配 ETag 標頭來減少頻寬。

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)

延遲考量

  • OneLake 安全角色定義的變更約需 5 分鐘 即可完成。
  • Microsoft Entra ID 中使用者群組成員資格的變更,在 OneLake 中反映約需 1 小時 。
  • 有些 Fabric 引擎有自己的快取層,可能需要額外時間。

設計您的輪詢間隔並相應設定快取的存活時間 (TTL)。 建議做法是每 5 分鐘輪詢一次安全元資料變更,並在每次查詢或縮短間隔內刷新使用者專屬存取權限。

步驟六:處理分頁

API principalAccess 支援多資料表的項目分頁。 當回應包含的項目多於 maxResults時,回應包含 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

錯誤處理

在整合過程中處理以下錯誤情境:

HTTP 狀態 錯誤碼 說明 建議的動作
200 - 成功。 處理回應。
404 未找到的物品 工作區或項目不存在,或引擎身份沒有存取權限。 驗證工作區 ID 和工件 ID。 確認引擎識別具有工作空間成員的存取權限。
412 前置條件未滿足 提供的 ETag If-Match 與目前資源 ETag 不符。 重新取得沒有 If-Match 標頭的資源,取得最新的 ETag。
429 - 速率超過上限。 請等標頭上指定的 Retry-After 時間再重試。

安全性最佳做法

請遵循以下最佳實務以確保安全整合:

  • 保護引擎的身份憑證。 服務主體在 OneLake 中擁有提升的資料存取權限。 透過像 Azure Key Vault 這類服務安全儲存憑證。
  • 不要將原始資料暴露給終端使用者。 在回傳任何資料前,務必套用 API 回傳的安全 principalAccess 過濾器。 跳過執法程序是安全違規。
  • 仔細驗證 RLS 的謂詞。 準確解析並套用 T-SQL WHERE 子句的條件。 錯誤的解析可能導致資料外洩。 若發生解析錯誤或語法映射不確定,應以 RLS 解析錯誤失敗,而非向使用者顯示部分或不安全的結果。
  • 處理資料表遺失情況時,視為存取被拒。 如果 API 回應中沒有資料表,使用者就無法存取。 千萬不要回退到未經過濾的資料,OneLake 的安全性預設為拒絕存取。
  • 審計存取權。 記錄哪些使用者存取了哪些資料表,以及為了合規和故障排除而套用了哪些安全政策。
  • 安全變更意見徵集 使用 ETag 標籤來偵測變更並迅速刷新快取的策略。

局限性

  • API principalAccess 目前處於預覽階段,可能會根據回饋做出調整。
  • 目前僅支援 Read 存取類型與 Permit 效果。
  • 引擎身份必須擁有不受限制的根層存取權限。 若 RLS 或 CLS 套用於引擎身份,API 呼叫會失敗。
  • RLS 謂詞使用 T-SQL 語法。 你的引擎負責正確地解析和應用謂詞。
  • 安全政策變更約需 5 分鐘才能完成。 使用者群組成員變更約需一小時。