返回
RSS Snowflake Engineering (Medium) AI 逐段翻译 发布 2026-09-05 03:01

在Snowflake上实现真正的数据层治理

DataHot 速览

本文介绍应用通过服务账号查询Snowflake时,容易形成与应用层并行的第二套权限模型。作者提出使用继承的调用者授权(inherited caller grants),在Streamlit和React/Next.js应用(基于SPCS)中启用调用者权限,让用户现有的Snowflake行级访问策略和脱敏策略自动生效。数据团队只需在数据层定义一次治理模型,即可覆盖所有外部应用,避免额外创建角色和重复授权。

为什么值得关注:展示了如何将应用层权限收敛回数据层统一治理,对使用Snowflake构建数据应用或BI前端的团队有直接借鉴价值。

本文目录 8 节
  1. 使用继承的调用者授权在Streamlit和React中解锁数据层治理
  2. 所有者权限与调用者权限
  3. 什么是继承的调用者授权?
  4. 账户设置
  5. 何时坚持使用所有者权限
  6. 在Streamlit(SPCS)上的调用者权限
  7. Snowflake应用运行时上的调用者权限
  8. 迈向统一的数据治理层

译文

AI 逐段翻译

使用继承的调用者授权在Streamlit和React中解锁数据层治理

每个通过服务账户查询Snowflake的应用程序都以相同的对话开始:谁能看到什么,我们是否需要新角色,如何为该用户过滤数据。答案随时间累积(新角色、新授权、在代码中重建的过滤逻辑),直到应用层运行着与你在数据层已经维护的权限模型并行的第二个权限模型。继承的调用者授权将治理折叠回数据层:用户现有的Snowflake权限自动管理他们在每个表面上可以看到的内容。

本文介绍了如何设置账户级调用者角色,并在SPCS上的Streamlit和React/Next.js应用中实现调用者权限。在Snowflake内部,我们的数据团队采用了这种模式,使我们能够:

  • 仅定义一次治理
  • 将该治理模型应用于每个面向客户的界面
  • 避免额外的角色创建和授权

所有者权限与调用者权限

Snowflake存储过程和应用程序运行时默认使用所有者权限:查询以应用程序所有者角色的身份运行,无论谁实际使用该应用。这与传统BI工具通过共享服务账户连接的模式相同。这种模式适用于应向所有人显示相同数据的仪表板,但当需要按用户进行行过滤或列掩码时则成为负担。

调用者权限则相反:查询使用调用应用程序的用户的权限运行。行访问策略看到实际用户。掩码策略生效。对象授权根据用户的角色层次结构进行检查。应用程序本身不需要实现这些——它从数据层自动获得。

什么是继承的调用者授权?

调用者授权不是数据权限。对表授予CALLER SELECT并不赋予角色读取该表的能力:调用用户仍然需要自己拥有SELECT权限。调用者授权授权应用程序在运行时使用调用者现有的SELECT权限。没有它,即使在调用者权限模式下,该权限也被隔离。

常规的GRANT CALLER将调用者权限范围限定到单个命名对象:

GRANT CALLER SELECT ON TABLE hr.compensation.salary_bands TO ROLE my_app_role;

继承授权(通过GRANT INHERITED CALLER)将此提升到模式、数据库甚至账户中给定类型的所有对象。关键是,它适用于将来的对象,无需执行显式的FUTURE GRANTS。

GRANT INHERITED CALLER SELECT ON ALL TABLES IN ACCOUNT TO ROLE snowflake_caller_rl;

这意味着你只需创建一次调用者角色,随着新模式、表和服务配置,它仍然有效。无需为每个新对象重新授权。

账户设置

运行之前需要注意一点:继承授权目前处于公共预览阶段。你需要先在账户级别选择启用——有关启用步骤,请参阅文档

在Snowflake,我们创建了一个单一的调用者角色,并为其加载了涵盖账户中所有对象的继承选择和使用授权。请注意,这涵盖了我们的应用程序的只读操作——写操作可以单独处理,可以通过相同的调用者框架,或通过所有者权限,具体取决于用例。示例语法如下:

-- Create the caller role
CREATE ROLE IF NOT EXISTS snowflake_caller_rl;

-- Inherited caller grants for all object types in the account
GRANT INHERITED CALLER USAGE ON ALL AGENTS IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER USAGE ON ALL MCP SERVERS IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER USAGE ON ALL CORTEX SEARCH SERVICES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER USAGE ON ALL DATABASES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER SELECT ON ALL DYNAMIC TABLES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER SELECT ON ALL EVENT TABLES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER SELECT ON ALL EXTERNAL TABLES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER USAGE ON ALL FILE FORMATS IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER USAGE ON ALL FUNCTIONS IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER SELECT ON ALL MATERIALIZED VIEWS IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER USAGE ON ALL PROCEDURES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER USAGE ON ALL SCHEMAS IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER SELECT ON ALL SEMANTIC VIEWS IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER READ ON ALL STAGES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER SELECT ON ALL TABLES IN ACCOUNT
  TO ROLE snowflake_caller_rl;
GRANT INHERITED CALLER SELECT ON ALL VIEWS IN ACCOUNT
  TO ROLE snowflake_caller_rl;

-- Make it universally available
GRANT ROLE snowflake_caller_rl TO ROLE PUBLIC;

授予PUBLIC意味着账户中的每个用户和角色自动拥有该角色。因为它只包含调用者授权,不包含直接对象权限,所以不会暴露任何数据。用户只能通过自己的角色层次结构访问他们已经有权访问的对象(包括通过SHOW CALLER GRANTS进行查看)。

何时坚持使用所有者权限

调用者权限并非总是正确选择。所有者权限适用于以下情况:

  • 该应用是公共仪表板,所有用户应看到相同的聚合数据
  • 你希望所有用户拥有统一访问权限,且不需要按用户过滤
  • 你正在构建服务账户式集成,应用作为独立主体

但是,对于任何按用户可见性、掩码或审计跟踪重要的情况,调用者权限是正确默认。

在Streamlit(SPCS)上的调用者权限

对于来自Looker或Tableau的开发者,将此调用者权限实现视为用户委托的连接:每次查询以单独用户的身份执行,而不是共享服务账户。

一旦调用者角色存在,Streamlit开发者可以构建完全将应用所有者角色与其查询的数据解耦的应用。如果账户中的任何角色对其具有行访问或掩码策略的表拥有SELECT权限,则该应用将为每个单独用户尊重这些策略,而无需一行过滤代码。

连接设置需要两件事:使用调用者权限连接而非默认连接,并使用会话作用域缓存以防止一个用户会话的数据泄漏到另一个会话。

import streamlit as st
import pandas as pd

DATA_CACHE_TTL = 300

# Module-level — the caller token is valid for only 2 minutes from session start.
# Do not create this connection conditionally or on a non-landing page.
conn = st.connection("snowflake-callers-rights")

# Use session scoped cache to prevent leakage across user sessions
@st.cache_data(ttl=DATA_CACHE_TTL, scope="session")
def run_query(_conn, sql: str) -> pd.DataFrame:
    df = _conn.query(sql)
    df.columns = df.columns.str.lower()
    return df

在本地开发中,snowflake-callers-rights在容器运行时之外会抛出错误。对于本地测试,切换到st.connection("snowflake")(所有者权限)并将该连接指向connections.toml中的账户。

此实现有两个重要警告:

  • 仅限SPCS:调用者权限连接仅适用于SPCS驱动的Streamlit应用。传统仓库计算应用不支持此模式。
  • 版本要求受限调用者权限需要Streamlit v1.53.1或更高版本。旧版本中的错误消息不清晰:如果调用者授权未按预期工作,版本是第一要检查的。

Snowflake应用运行时上的调用者权限

Snowflake应用运行时(预览版)上的React/Next.js应用遵循相同原则,但需要自行组装令牌组合。

SPCS提供两个令牌:位于/snowflake/session/token的服务令牌(标识应用本身),以及通过sf-context-current-user-token标头的每请求用户令牌(标识调用者)。Snowflake接受将两者拼接成一个OAuth凭据。你可以使用lib/snowflake.ts中的以下代码块示例:

// lib/snowflake.ts
import fs from "fs";
import { headers } from "next/headers";
import Snowflake from "snowflake-sdk";

const _pool = new Map<string, Snowflake.Connection>();

function getServiceToken(): string {
  return fs.readFileSync("/snowflake/session/token", "utf8").trim();
}

async function getConnection(serviceToken: string, callerToken: string): Promise<Snowflake.Connection> {
  if (!_pool.has(callerToken)) {
    const conn = Snowflake.createConnection({
      host: process.env.SNOWFLAKE_HOST,
      account: process.env.SNOWFLAKE_ACCOUNT!,
      authenticator: "OAUTH",
      // Combined token: service identity proves which app is calling;
      // caller identity proves who the signed-in user is.
      // Snowflake runs the query as the intersection of what each allows.
      token: `${serviceToken}.${callerToken}`,
    });
    await new Promise<void>((resolve, reject) =>
      conn.connect((err) => (err ? reject(err) : resolve()))
    );
    _pool.set(callerToken, conn);
  }
  return _pool.get(callerToken)!;
}

export async function query<T = Record<string, unknown>>(sql: string, binds?: unknown[]): Promise<T[]> {
  const serviceToken = getServiceToken();
  const callerToken = (await headers()).get("sf-context-current-user-token");
  if (!callerToken) throw new Error("No sf-context-current-user-token header — is the app running in SPCS with caller's rights?");
  const conn = await getConnection(serviceToken, callerToken);

  return new Promise((resolve, reject) =>
    conn.execute({
      sqlText: sql,
      binds,
      complete: (err, _stmt, rows) => (err ? reject(err) : resolve((rows ?? []) as T[])),
    })
  );
}

然后在API路由的下游:

// app/api/compensation/route.ts
import { query } from "@/lib/snowflake";

// Caller's rights responses are per-user — never cache or prerender.
export const dynamic = "force-dynamic";

export async function GET() {
  const rows = await query(
    `SELECT employee_id, band, salary FROM hr.compensation.salary_bands WHERE snowhouse_username = CURRENT_USER()`
  );
  return Response.json(rows);
}

对于原始 SPCS 服务(非 App Runtime),请在你的服务规范中的 capabilities.securityContext 下添加 executeAsCaller: true。否则,Snowflake 不会注入用户令牌头。

关于缓存的注意:如果你缓存查询结果,请将缓存键的范围设置为包含调用者令牌。没有按用户划分范围的缓存会将一个用户的过滤数据提供给另一个用户:这与 Streamlit 中 scope="session" 所防范的风险相同。

迈向统一的数据治理层

在查询时,Snowflake 应用调用用户的角色,而不是服务账户的角色。行访问策略会过滤到该用户可以看到的内容。列掩码会应用。对象授权会根据其完整的角色层次进行检查。如果用户在工作表中看不到某些内容,他们在你的应用中也看不到。

在 Snowflake 上实现真正的数据层治理最初发表于Snowflake Builders Blog: Data Engineers, App Developers, AI, & Data Science在 Medium 上,人们在那里通过突出显示和回应这个故事来继续对话。

这篇内容对你有用吗?

反馈只用于改善内容筛选,不等同于收藏

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存