通过深度数据安全性实施最终用户内存隔离

本指南介绍了如何使用 OCI IAM 和 Oracle Deep Data Security 为 Oracle AI Agent Memory 应用强制实施最终用户隔离。

警告:仅学习环境。请勿将此示例部署到生产环境。

该应用程序使用 Flask 的开发服务器,内存中会话存储,环境文件密钥和简化的令牌生命周期处理。数据库设置脚本修改数据库范围的身份提供者设置和权限。仅使用一次性用户和隔离的非生产 Oracle Autonomous AI Database。

本指南构建了一个故意小的 Web 应用程序:

这条路不比爱丽丝和鲍勃本身。它将最终用户令牌传递到 Oracle AI Database,其中 Oracle Deep Data Security 强制执行代理内存自己的行策略。

本指南演示了一个部署。有关完整的安全功能,包括支持的每个策略、主用户、管理功能、运行时上下文 API、审计和撤销计时,请参见 Deep Data Security API and security reference 。

在本指南中,您将:

了解信任链

浏览器和应用程序使用两种不同的 OAuth 访问令牌:

  1. end-user token(终端用户令牌)标识 Alice 或 Bob,包括用户的 IAM 组。
  2. 机密应用程序通过客户机凭证获取的 database-access token(数据库访问令牌)可证明应用程序可以访问数据库资源。

在附加最终用户安全上下文之前,Oracle AI Database 会验证这两个令牌。

Oracle 在了解验证流和先决条件以及最终用户安全上下文中的完整生命周期中记录了此双令牌模型。

先决条件

您需要:

Oracle Deep Data Security 是数据库增强的细粒度授权。请参阅什么是 Oracle 深度数据安全性。

配置 OCI IAM 身份域

所有 OCI IAM 都在一个身份域中工作。记录其域 URL ,例如 https://idcs-<id>.identity.oraclecloud.com:443。

Oracle 权威演示是配置 OCI IAM 以实现应用介入访问。以下示例中的值与随本指南提供的示例环境文件匹配。

注册数据库资源

在 Identity domain > Integrated applications(身份域 > 集成应用程序)中:

  1. 添加名为 OracleDB 的机密应用程序。
  2. 将其配置为资源服务器:
    • 主要受众:OracleDB
    • 范围:DB_ACCESS_SCOPE
  3. 将其配置为客户机并允许使用客户机身份证明。
  4. 激活它。
  5. 记录其应用程序 ID 、客户端 ID 和客户端密钥。

全限定数据库范围是与范围名称连接在一起的受众:

OracleDBDB_ACCESS_SCOPE

请按照在 OCI IAM 中注册数据库了解当前的控制台标签和字段说明。

注册 Web 应用程序

添加另一个名为 OracleAgentMemoryWeb 的机密应用程序:

  1. 启用以授权方式强制授予。
  2. 将其配置为资源服务器:
    • 主要受众:OracleAgentMemoryWeb
    • 范围:APP_ACCESS_SCOPE
    • 访问令牌生命周期:本练习的 3600 秒
  3. 使用以下授权将其配置为客户端:
    • 授权码
    • 客户端凭据
  4. 添加此确切的重定向 URL:
    http://127.0.0.1:8000/auth/callback
  5. 在客户机资源下,向两者授予访问权限:
    • OracleDBDB_ACCESS_SCOPE
    • OracleAgentMemoryWebAPP_ACCESS_SCOPE
  6. 激活应用程序并记录其客户端 ID 和客户端密钥。

应用程序将授权码与 state 和 PKCE S256 一起使用。OCI IAM 在 Register the Application in OCI IAM 中记录了所需的客户端配置,并提供 PKCE 授权代码示例。

添加组声明

Oracle AI Database 可根据最终用户令牌的 group 声明激活外部映射的数据角色。OCI IAM 定制声明通过身份域 REST API(而非控制台)进行配置。

作为身份域管理员,下载可以调用身份域 API 的短期个人访问令牌。然后运行:

export OCI_IAM_DOMAIN_URL='https://<identity-domain-host>:443'
export OCI_IAM_ADMIN_TOKEN='<short-lived-personal-access-token>'

curl --fail-with-body \
  -X POST "$OCI_IAM_DOMAIN_URL/admin/v1/CustomClaims" \
  -H "Authorization: Bearer $OCI_IAM_ADMIN_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "schemas": [
      "urn:ietf:params:scim:schemas:oracle:idcs:CustomClaim"
    ],
    "name": "group",
    "value": "$user.groups.*.display",
    "expression": true,
    "mode": "always",
    "tokenType": "AT",
    "allScopes": true
  }'

成功请求将返回 HTTP 201。如果域已具有此声明,请勿创建重复项。请参阅在 OCI IAM 中配置组信息的定制声明。

创建 Alice、Bob 和应用程序组

  1. 创建一个名为 ORACLEAGENTMEMORY_USERS 的组。
  2. 创建测试用户 alice 和 bob。
  3. 将两个用户分配给 ORACLEAGENTMEMORY_USERS。
  4. 将组分配到 OracleAgentMemoryWeb。

数据库稍后会将此组名映射到数据角色。组名不区分大小写,但在整个设置过程中使用相同的拼写。请参阅在 OCI IAM 中创建用户和分配组。

准备的环境

下载完整的 Web 应用程序 deepsec_oci_iam_webapp.zip 。归档文件还包含此示例拥有的独立数据库设置和检查脚本。它们不会从 SDK 测试套件导入或打包脚本。

提取归档文件,输入其项目目录,并创建本地设置和运行时环境文件:

unzip deepsec_oci_iam_webapp.zip
cd deepsec_oci_iam_webapp
cp deepsec.env.example .deepsec.env
cp deepsec.runtime.env.example .deepsec.runtime.env
python -c "import secrets; print(secrets.token_hex(32))"

将生成的值放在 OAM_WEB_SECRET_KEY 中,并在两个文件中填充所需的每个占位符。Git 会忽略这两个文件名。

database_scripts 目录包含:

文件 责任
_common.sh 用于加载 .deepsec.env、验证所需值、检查 SQL*Plus 以及配置时导出 TNS_ADMIN 的专用助手。请勿直接运行它。
db_ociiam_setup.sh 为目标自治 AI 数据库启用 OCI IAM 外部验证,并替换 OCI_IAM_DOMAIN_DB_CRED$。
db_deepsec_user_setup.sh 创建方案所有者和应用程序池用户并授予其记录的权限允许列表。
db_list_all_data_roles.sh 对 Deep Data Security 角色、IAM 映射、数据授权、谓词、受保护对象、最终用户和应用程序身份执行只读检查。

.deepsec.env 仅由数据库设置使用。它包括安全管理员和模式所有者密码。Flask 进程将加载 .deepsec.runtime.env,并有意排除这两个特权密码。请勿将设置文件导出到启动 Web 应用程序的 shell 中。要使用不同的路径,请将 OAM_DEEPSEC_ENV_FILE 设置为设置,将 OAM_WEB_ENV_FILE 设置为 Web 进程。

重要的 IAM 值映射如下:

环境变量的 OCI IAM 值
OAM_DEEPSEC_OCI_DB_APP_ID 数据库应用程序的应用程序 ID
OAM_DEEPSEC_OCI_DB_CLIENT_ID 数据库应用程序的客户端 ID
OAM_DEEPSEC_OCI_DB_CLIENT_SECRET 数据库应用程序的客户端密钥
OAM_WEB_OCI_CLIENT_ID Web 应用程序的客户端 ID
OAM_WEB_OCI_CLIENT_SECRET Web 应用程序的客户端密钥
OAM_WEB_OCI_END_USER_SCOPE OracleAgentMemoryWebAPP_ACCESS_SCOPE
OAM_WEB_OCI_DATABASE_ACCESS_SCOPE OracleDBDB_ACCESS_SCOPE

仅当 DSN 是 tnsnames.ora 无法以其他方式搜索到的 TNS 别名时,才需要 OAM_DEEPSEC_CONFIG_DIR。当 sqlnet.ora 和 DSN 已经提供 python-oracledb 所需的一切时,钱包位置和密码是可选的。有关 Autonomous AI Database 连接选项,请参见 Connect Python Applications with a Wallet 。

同时配置嵌入提供程序。应用程序仅列出并添加内存,但代理内存在存储内存内容时仍会创建嵌入。该模板使用与 OpenAI 兼容的模型作为示例;将模型、API 基础、密钥和维替换为您的提供程序的值。

配置三个数据库帐户

警告:本节中的命令更改数据库范围的外部验证设置和帐户权限。首先检查脚本。不要针对共享或生产数据库运行它们。

使用三个不同的数据库用户:

责任 示例账户 允许的工作
安全管理员 ADMIN 启用 OCI IAM 集成,并创建、授予、撤销、列表和删除数据角色和数据授权。它从不用于 Web 请求。
托管模式责任人 OAM_SCHEMA_OWNER 创建代理内存表、索引、过程和调度程序作业。它仅用于方案生命周期操作。
应用程序数据库用户 OAM_APP_DB_USER 打开会话并附加最终用户安全上下文。它不接收普通的 SELECT、INSERT、UPDATE 或 DELETE 权限。

此分隔会关闭没有有效最终用户上下文的请求。Oracle 数据库配置指南建议使用 CREATE SESSION 和 CREATE END USER SECURITY CONTEXT 作为连接池帐户;请参阅为 IAM 集成配置数据库。

此示例使用自治 AI 数据库 ADMIN 账户进行设置。定制安全管理员帐户必须能够创建和更改两个数据库用户并授予其列出的系统权限。由于代理内存表属于单独的方案,因此策略管理需要管理员自己的方案中的 CREATE ANY DATA GRANT、DROP ANY DATA GRANT 和 ADMINISTER ANY DATA GRANT,而不仅仅是 CREATE DATA GRANT。还必须有权创建和删除策略使用的数据角色。

启用 OCI IAM 令牌验证

复查并运行以下命令:

bash database_scripts/db_ociiam_setup.sh

该脚本以安全管理员身份连接,并:

只能有一个外部身份提供者处于活动状态。示例脚本使用 force => TRUE 更新 OCI IAM 参数,这可能会中断其他外部验证配置。Oracle 在 Configure the Database for IAM Integration 中记录了确切的 Autonomous Database 调用。

创建所有者和池用户

复查并运行以下命令:

bash database_scripts/db_deepsec_user_setup.sh

该脚本不会更改 ADMIN。它创建所有者和应用程序池用户和授权:

-- Managed-schema owner
CREATE SESSION, CREATE TABLE, CREATE SEQUENCE,
CREATE VIEW, CREATE PROCEDURE, CREATE JOB

-- Runtime application pool
CREATE SESSION, CREATE END USER SECURITY CONTEXT

所有者接收自治 AI 数据库 DATA 表空间上的限额。池用户不会收到对所有者的表的直接权限。为了使示例专注于所需的语句,脚本不会检查现有用户或检查其当前权限。使用新帐户名;如果用户已存在或者其他 SQL 语句失败,该脚本将停止。

创建存储和自己的行策略

安装独立的示例环境:

uv sync

然后运行设置程序:

uv run python scripts/setup_memory.py

此命令仅对 OAM_WEB_MEMORY_STORE_ID 有意具有破坏性。其作用:

  1. 作为方案所有者连接以重新创建代理内存方案;
  2. 以安全管理员身份连接以添加自己的行策略并将其授予 ORACLEAGENTMEMORY_USERS。

添加和授权管理调用是幂等的。重新运行它们将替换相同的受管策略定义和分配,并重试修复由中断尝试保留的部分受管 DDL。

该策略为 OCI IAM 组和数据授权创建外部映射的数据角色,其行谓词将存储的 user_id 值与 ORA_END_USER_CONTEXT.username 进行比较。Oracle 在 CREATE DATA ROLE 中记录了映射语法,在 CREATE DATA Grants 中记录了行授权。

检查结果:

bash database_scripts/db_list_all_data_roles.sh

您应看到 IAM 映射的角色、代理内存数据授权及其受保护的所有者方案表。此检验脚本仅查询目录视图;它不创建、更改或删除深度数据安全对象。

将数据库内嵌入模型与 Deep Sec 结合使用

当应用程序使用 OracleDBEmbedder 和默认 provider="database" 以及存储在 Oracle AI Database 中的嵌入模型(通常为导入的 ONNX 模型,例如 DMUSER.DOC_MODEL)时,本节适用。在此配置中,直接嵌入使用 Oracle 的 VECTOR_EMBEDDING SQL 运算符,因此模型需要以下描述的 SELECT ON MINING MODEL 权限。

OracleDBEmbedder 还通过 DBMS_VECTOR_CHAIN.UTL_TO_EMBEDDING(例如,使用 provider="openai")支持远程提供程序。这些配置仍会从 Oracle AI Database 调用嵌入请求,但不会访问驻留在数据库中的挖掘模型;本节中的 SELECT ON MINING MODEL 指令不适用。代理内存操作仍必须在 OracleMemoryEndUserSecurityContext 内运行。

代理内存策略授予对代理内存表的访问权限。它不授予对 Oracle AI Database 嵌入模型的访问权限。模型需要自己的数据库权限:SELECT ON MINING MODEL。

请勿将该权限授予应用程序池用户。在请求期间,Deep Sec 会通过从 OCI IAM 组映射的数据角色(而不是池用户的正常权限)向用户授权。因此,当最终用户安全上下文处于活动状态时,仅授予池用户的权限不可用。

为需要使用该模型的每个 IAM 组创建一个映射的数据角色。将该数据角色指定为可以访问该模型的普通数据库角色。以数据库安全管理员或其他有权创建角色和授予模型访问权限的账户身份运行以下 SQL。将示例名称替换为应用程序的名称:

-- This must exactly match the OCI IAM group passed to OciGroupPrincipal.
CREATE DATA ROLE APP_MEMORY_USERS_DATA_ROLE
  MAPPED TO 'IAM_OAUTH_GROUP=YOUR_IAM_GROUP';

-- This ordinary database role carries access to one embedding model.
CREATE ROLE APP_DOC_MODEL_ROLE;

GRANT SELECT ON MINING MODEL YOUR_USER.DOC_MODEL
  TO APP_DOC_MODEL_ROLE;

-- Give model access to the IAM group's Deep Sec data role.
GRANT APP_DOC_MODEL_ROLE TO APP_MEMORY_USERS_DATA_ROLE;

然后,应用程序在授予代理内存策略时使用相同的组名称:

grant_agent_memory_policies(
    admin_connection,
    owner_schema="OAM_SCHEMA_OWNER",
    memory_store_id="MEMORY",
    principals=[OciGroupPrincipal("YOUR_IAM_GROUP")],
    policies=[UserOwnRowsDeepDataSecurityPolicy()],
)

grant_agent_memory_policies() 查找映射到 YOUR_IAM_GROUP 的现有角色,并将代理内存表数据授权添加到该同一角色。它不会创建第二个角色。在运行时,OCI IAM 令牌包含 YOUR_IAM_GROUP 的用户可以访问允许的代理内存行和 DMUSER.DOC_MODEL。

如果应用程序在此模型设置之前已调用 grant_agent_memory_policies(),则代理内存已创建映射的数据角色。请勿为同一组创建其他映射的角色。找到现有角色并向其授予普通模型角色:

SELECT data_role
FROM sys.dba_data_roles
WHERE UPPER(mapped_to) = UPPER('IAM_OAUTH_GROUP=YOUR_IAM_GROUP');

GRANT APP_DOC_MODEL_ROLE TO <DATA_ROLE_RETURNED_BY_THE_QUERY>;

将每个运行时 OracleDBEmbedder 操作保留在 OracleMemoryEndUserSecurityContext 中。请勿使用方案所有者、单独的管理员连接或没有最终用户安全上下文的连接来为用户请求运行嵌入。这将绕过深层安全授权边界。

了解应用程序

可下载项目 deepsec_oci_iam_webapp.zip 包含完整的 Web 应用程序和数据库脚本。关键的应用部分是故意小的。

启动和完成 OAuth

登录路由将创建随机 state 和 PKCE 值。只有不透明的会话 ID 放置在 HttpOnly 和 SameSite=Lax Cookie 中;令牌保留在进程本地演示会话存储中。

def start_authorization(config: RuntimeConfig) -> AuthorizationRequest:
    """Build a state-bound Authorization Code request with PKCE."""
    state = secrets.token_urlsafe(32)
    code_verifier = secrets.token_urlsafe(64)
    challenge = (
        base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
        .rstrip(b"=")
        .decode("ascii")
    )
    query = urllib.parse.urlencode(
        {
            "client_id": config.oauth_client_id,
            "response_type": "code",
            "redirect_uri": config.redirect_uri,
            "scope": config.end_user_scope,
            "state": state,
            "code_challenge": challenge,
            "code_challenge_method": "S256",
        }
    )
    return AuthorizationRequest(
        url=f"{config.domain_url}/oauth2/v1/authorize?{query}",
        state=state,
        code_verifier=code_verifier,
    )


def exchange_authorization_code(
    config: RuntimeConfig,
    code: str,
    code_verifier: str,
) -> EndUserToken:
    """Exchange one browser authorization code for an end-user token."""
    payload = _token_request(
        config,
        {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": config.redirect_uri,
            "code_verifier": code_verifier,
        },
    )
    access_token, expires_at = _required_access_token(payload)
    return EndUserToken(
        access_token=access_token,
        username=_display_username(access_token),
        expires_at=expires_at,
    )

回调将 state 与常量时间进行比较,并使用相同的重定向 URI 和 PKCE 验证器交换代码。应用程序分别获取数据库访问令牌:

    def get(self, config: RuntimeConfig) -> str:
        """Return a current database-access token."""
        with self._lock:
            if self._token is not None and time.time() < self._expires_at - 60:
                return self._token
            payload = _token_request(
                config,
                {
                    "grant_type": "client_credentials",
                    "scope": config.database_access_scope,
                },
            )
            self._token, self._expires_at = _required_access_token(payload)
            return self._token

OCI IAM 在验证 OCI IAM 配置中记录了两个令牌请求。

每个代理内存操作的范围

该应用程序将两个令牌组合成一个 python-oracledb 最终用户安全上下文。然后,使用 OracleMemoryEndUserSecurityContext 对每个存储初始化和操作进行限定:

def list_visible_memories(
    config: RuntimeConfig,
    pool: Any,
    user_context: Any,
) -> list[Any]:
    """List rows visible to the effective OCI IAM end user."""
    with OracleMemoryEndUserSecurityContext(user_context):
        store = create_runtime_store(config, pool)
        return store.list("memory", limit=100)


def add_memory(
    config: RuntimeConfig,
    pool: Any,
    user_context: Any,
    content: str,
    target_username: str,
) -> None:
    """Attempt to insert a row for the username supplied by the browser."""
    with OracleMemoryEndUserSecurityContext(user_context):
        store = create_runtime_store(config, pool)
        store.add(
            contents=[content],
            record_type="memory",
            user_ids=[target_username],
        )

运行时存储使用 SchemaPolicy.NO_CHECK,因为模式创建、验证、升级和重新创建属于模式所有者设置路径。当最终用户上下文处于活动状态时,SDK 会拒绝另一个方案策略,并在没有上下文处于活动状态时拒绝此受保护的运行时存储上的后续操作。安全管理功能同样拒绝带有最终用户上下文的连接。

对于每个代理内存数据库操作,上下文管理器:

池的数据库帐户没有回退表权限。End-User Security Context Payload Creation 中介绍了 python-oracledb 有效负载 API。深度数据安全 API 和安全参考介绍了上下文管理器的完整合同。

让数据库自行决定

路由将转发提交的用户名保持不变。它捕获并清理数据库错误,但不包含 username == signed_in_user 授权分支:

@web.route("/", methods=["GET", "POST"])
def index() -> Response | tuple[str, int]:
    """Show visible memories and let the user attempt one insert."""
    config, pool, _ = _extensions()
    _, session = _session()
    if session is None or session.end_user_token is None:
        return render_template("index.html", session=None, memories=[])

    message = None
    status = 200
    try:
        user_context = _security_context(config, session)
        if request.method == "POST":
            if not hmac.compare_digest(
                request.form.get("csrf_token", ""),
                session.csrf_token,
            ):
                return render_template("error.html", message="The form expired."), 400
            content = request.form.get("content", "").strip()
            target_username = request.form.get("username", "").strip()
            if not content or not target_username:
                message = "Content and username are required."
                status = 400
            elif len(content) > 4000 or len(target_username) > 255:
                message = "The submitted memory is too large."
                status = 400
            else:
                try:
                    add_memory(
                        config,
                        pool,
                        user_context,
                        content,
                        target_username,
                    )
                    message = "Memory added."
                except oracledb.DatabaseError:
                    #Keep listing permitted rows after the deliberately denied write.
                    message = "Oracle AI Database denied this operation for the effective end user."
                    status = 403

        memories = list_visible_memories(config, pool, user_context)
    except oracledb.DatabaseError:
        #Never expose raw database errors, token contents, or submitted values.
        message = "Oracle AI Database denied this operation for the effective end user."
        memories = []
        status = 403
    except OAuthError:
        message = "The login session expired. Sign in again."
        memories = []
        status = 401

    return (
        render_template(
            "index.html",
            session=session,
            memories=memories,
            message=message,
        ),
        status,
    )

Jinja 转义显示内存内容。从不呈现原始数据库例外、OAuth 响应、令牌和提交的值。

运行并验证应用程序

启动 Flask 开发服务器:

uv run python run.py

打开 http://127.0.0.1:8000。

验证授权边界:

  1. 以 Alice 身份登录。
  2. 添加 Alice's first memory 并预填充用户名 alice。
  3. 确认内存显示在可见内存下。
  4. 添加 Alice tries to write for Bob,但将用户名更改为 bob。
  5. 确认 Oracle AI Database 拒绝了此操作。
  6. 注销,以 Bob 身份登录,并确认 Alice 的内存不可见。
  7. 以 Bob 身份添加内存,以 Alice 身份重新登录,并确认 Bob 的内存不可见。

这显示了两个独立的控件:

对于生产运营,请配置 Oracle Unified Auditing for the Deep Sec 管理和您的组织必须保留的最终用户安全上下文操作。此外,还说明了记录的撤销时间合同:SDK 策略撤销在管理调用返回之前提交,而从 OCI IAM 组中删除用户不会重写已发布的访问令牌。有关审计指导、令牌失效行为以及指向相应 Oracle Deep Sec 和 OCI IAM 文档的链接,请参见 Deep Data Security 。

如果登录成功,但数据库拒绝执行每项操作,则检查最终用户令牌并验证 group 是否包含 ORACLEAGENTMEMORY_USERS。Oracle Deep Data Security access and privilegebleshooting 还介绍了管理员如何检查活动数据角色。

生产需要什么

警告:完成本指南不会使示例可用于生产。

在调整设计之前,请至少替换或添加:

保持核心不变:运行时池帐户没有直接表 DML 权限,并且每个代理内存请求都在经过验证的 OracleMemoryEndUserSecurityContext 内运行。