尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Onyx 云托管外部应用凭据:OAuth 凭证集中化、租户预置与管理 API 锁定的实现机制

Onyx 云托管外部应用凭据:OAuth 凭证集中化、租户预置与管理 API 锁定的实现机制 Onyx 云托管外部应用凭据OAuth 凭证集中化、租户预置与管理 API 锁定的实现机制【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer本文以 Onyxdanswer 项目Craft 模块的设计文档 cloud-managed-app-credentials.md 为核心系统讲解云托管外部应用凭据Cloud-Managed External-App Credentials方案在 Onyx Cloud 上Gmail、Google Calendar、Slack、Linear 等内置外部应用的 OAuth 客户端凭据由 Onyx 统一持有并按租户预置租户管理员只能启停应用与配置动作策略用户侧沿用标准 OAuth 流程。读完后你可以掌握该方案的OnyxManagedExtApp接口设计、EXT_APP_*环境变量配置规范、租户预置流程以及管理 API 的云端锁定lockdown实现与测试验证方式。1. 核心模型Onyx 托管与自托管的归属边界该方案要解决的问题是在多云租户的 SaaS 场景下如果每个租户管理员都自行到上游服务商Google、Slack、Linear 等注册 OAuth 应用会产生大量配置成本、凭据泄露面与合规风险。Onyx Cloud 的做法是反转凭据所有权Onyx 持有 OAuth 客户端凭据内置外部应用Gmail、Google Calendar、Slack、Linear的client_id/client_secret由 Onyx 平台统一注册、统一维护每个租户创建时这些应用已按 Onyx 凭据预置完成租户管理员无需注册自己的 OAuth 应用。管理员权限收窄在 Cloud 上租户管理员只能对托管内置应用做两件事——启停enable/disable与设置动作策略action policies不能创建、不能编辑凭据/网关配置、不能删除。凭据与网关配置auth_template、upstream_url_patterns永远不会下发给客户端。用户侧零变化应用一旦启用每个用户照常走既有 OAuth 授权流程针对 Onyx 的 OAuth 应用换取属于自己的 per-user token。自托管不受影响Self-hosted 部署维持原状——管理员自己创建内置应用并提供自己的凭据应用保持可编辑。文档 cloud-managed-app-credentials.md 的 Behavior 一节进一步给出三条约束每个租户每种类型至多一个内置应用One built-in per type per tenant同一app_type在一个租户内最多存在一个内置应用由内置 skill 的唯一 slug 强制CUSTOM应用不受限、可重复创建。Cloud 上种子化且默认禁用Seeded, disabled, on Cloud租户创建时所有 Onyx 托管内置应用都以禁用状态预置Onyx 凭据已填入。管理员只能开关与设策略Admins toggle set policies onlyCloud 上对内置应用的凭据/网关配置永远不返回给客户端。这套托管与否的判断在代码层有唯一事实来源single source of truth即下一节的OnyxManagedExtApp接口。2. 提供者接口OnyxManagedExtApp 的声明与校验设计文档指出凡凭据由 Onyx 持有的内置提供者都必须继承OnyxManagedExtApp定义于 base.py。该接口的三个关键机制如下2.1 managed_org_credentials声明 Onyx 持有的凭据映射class OnyxManagedExtApp(ExternalAppProvider, abstractTrue): # Onyx-owned credential values, sourced from the EXT_APP_APP_TYPE_FIELD # constants in app_configs. Keys must match the specs required fields. managed_org_credentials: ClassVar[dict[str, str]] {}具体托管提供者只需把类变量managed_org_credentials的 key 映射到自身 spec 中required_org_credential_fields声明的字段即可。以 Gmail 为例gmail.py 中class GmailProvider(GoogleOAuthProvider, OnyxManagedExtApp): spec GoogleOAuthProvider.build_spec( app_typeExternalAppType.GMAIL, app_nameGmail, scope_CLOUD_SCOPE if MULTI_TENANT else _SELF_HOSTED_SCOPE, upstream_url_patterns[https://gmail\\.googleapis\\.com/gmail/.*], google_api_nameGmail API, endpoint_catalog_ENDPOINTS, ) managed_org_credentials { client_id: EXT_APP_GMAIL_CLIENT_ID, client_secret: EXT_APP_GMAIL_CLIENT_SECRET, }一个值得注意的细节是 scope 的双轨制gmail.py自托管模式使用gmail.modify全量 scopeCloud 模式MULTI_TENANT受限于 Google 对 Onyx 自有 OAuth 客户端的受限 scope 策略只申请gmail.send加gmail.drafts.create所有标记requires_self_hosted_scopeTrue的动作如读取消息在云端目录中自动剔除。2.2 类定义期的键集合校验OnyxManagedExtApp.__init_subclass__在子类定义时立即校验managed_org_credentials的 key 集合是否与spec.descriptor.required_org_credential_fields的 key 集合完全相等不一致直接抛出TypeErrorrequired {f.key for f in cls.spec.descriptor.required_org_credential_fields} configured set(cls.managed_org_credentials) if configured ! required: raise TypeError( f{cls.__name__} is an OnyxManagedExtApp but its fmanaged_org_credentials keys {sorted(configured)} do not fmatch its required credential fields {sorted(required)}. )这保证了预置provisioning时写入数据库的凭据 key 一定与上游授权模板auth_template所需字段精确对齐错误在 import 阶段就暴露而不是运行时。2.3 configured_managed_credentials()三态解析语义def configured_managed_credentials(self) - dict[str, str] | None: This providers Onyx-owned credentials if fully configured, else None. creds {k: v.strip() for k, v in self.managed_org_credentials.items()} if not any(creds.values()): return None # nothing configured if all(creds.values()): return creds # partially set — almost always a config mistake worth surfacing ... return None三态语义全部为空 →None未配置全部非空 → 返回清洗后的凭据字典部分配置 → 视为未配置并记录 warning日志中会指出具体缺失的EXT_APP_TYPE_FIELD变量名通常意味着运维漏配。空白字符串如 按未设置处理。test_managed_credentials.py 对这三态逐一做了单测覆盖test_unset_is_none、test_full_set_resolves、test_partial_set_is_none、test_blank_value_counts_as_unset并额外固定了key 必须与 required 字段相等这一不变式。2.4 注册表与托管判定的唯一入口registry.py 中的get_onyx_managed_provider是判定入口def get_onyx_managed_provider(app_type: ExternalAppType) - OnyxManagedExtApp | None: provider PROVIDERS.get(app_type) return provider if isinstance(provider, OnyxManagedExtApp) else None它的 docstring 明确说明该函数不以MULTI_TENANT为门槛——is not None就是该应用是否 Onyx 托管的检查云端锁定由调用方叠加MULTI_TENANT条件组成。当前注册表中共 8 个内置提供者Slack、Google Calendar、Google Drive、Gmail、Linear、GitHub、HubSpot、Notion从源码结构看它们全部继承了OnyxManagedExtApptest_managed_external_apps.py 中的test_all_built_ins_are_onyx_managed也把这一点固定为显式不变式若未来某个内置应用改由管理员自配凭据需刻意更新该测试。uses_cloud_scope()registry.py则是MULTI_TENANT and get_onyx_managed_provider(app_type) is not None的组合判断供动作目录过滤复用withheld_on_cloud()返回该应用在云端被剔除的动作列表。3. 凭据配置EXT_APP_* 环境变量运维侧Onyx 云团队通过按字段拆分的环境变量注入凭据这些常量定义在 app_configs.pyEXT_APP_APP_TYPE_FIELD e.g. EXT_APP_GMAIL_CLIENT_ID, EXT_APP_SLACK_CLIENT_SECRET仓库中实际定义的常量覆盖 8 个托管应用每个应用各一对*_CLIENT_ID/*_CLIENT_SECRET应用环境变量SlackEXT_APP_SLACK_CLIENT_ID、EXT_APP_SLACK_CLIENT_SECRETGmailEXT_APP_GMAIL_CLIENT_ID、EXT_APP_GMAIL_CLIENT_SECRETGoogle CalendarEXT_APP_GOOGLE_CALENDAR_CLIENT_ID、EXT_APP_GOOGLE_CALENDAR_CLIENT_SECRETGoogle DriveEXT_APP_GOOGLE_DRIVE_CLIENT_ID、EXT_APP_GOOGLE_DRIVE_CLIENT_SECRETLinearEXT_APP_LINEAR_CLIENT_ID、EXT_APP_LINEAR_CLIENT_SECRETGitHubEXT_APP_GITHUB_CLIENT_ID、EXT_APP_GITHUB_CLIENT_SECRETHubSpotEXT_APP_HUBSPOT_CLIENT_ID、EXT_APP_HUBSPOT_CLIENT_SECRETNotionEXT_APP_NOTION_CLIENT_ID、EXT_APP_NOTION_CLIENT_SECRET同一段配置中还定义了总开关AUTO_PROVISION_DEFAULT_EXTERNAL_APPS ( os.environ.get(AUTO_PROVISION_DEFAULT_EXTERNAL_APPS, false).lower() true )设计要点对应文档 Credential configuration 一节命名防冲突EXT_APP_前缀 具体应用类型如GMAIL而非GOOGLE使其与 auth-flow 的GOOGLE_OAUTH_*变量清晰区分。每个常量映射到对应提供者managed_org_credentials的字段上。落库加密存储值写入organization_credentials列该列是EncryptedJson类型静态加密encrypt at rest。允许不配置某个提供者的变量全部留空是合法状态——该应用仍会被预置只是暂时没有凭据配置完成前无法被有意义地启用。4. 租户预置种子化、幂等与部分失败回滚文档 Provisioning 一节描述的设计是provision_built_in_external_apps(db_session)位于 provisioning.py随setup_tenant执行与configure_default_api_keys并列当前仓库的setup_tenant确实按序调用configure_default_api_keys与setup_onyx见 provisioning.py。该流程由AUTO_PROVISION_DEFAULT_EXTERNAL_APPS门控默认false云上设为true。对每个托管内置应用首次创建以禁用状态创建应用填入运维侧凭据未配置则为空重复执行如setup_tenant重试就地刷新凭据但不动启用状态与策略且只有当该类型的凭据当前有配置时才覆写因此重跑绝不会把配置中不再提及的凭据清空credential refresh without wiping。单应用失败会被回滚并记录日志保证一个坏应用不阻塞其余应用的预置。从测试代码结构看当前实现里对既有租户的种子化改由 Alembic 迁移承担test_managed_external_apps.py 的模块注释明确写道 Built-in apps are seeded into existing tenants by an Alembic migration rather than at tenant setup, so these tests seed directly viacreate_external_app测试通过create_external_app直接种子化来模拟托管应用。与之配套的是 external_app.py 中的set_external_app_organization_credentials——docstring 说明它供 Onyx 托管的预置/轮换路径使用刻意不触碰其他任何状态skill 偏好、策略、网关配置均保持不变这正是刷新凭据不清空其他状态的落点。5. 管理 API 的云端锁定管理端点实现于 api.py。文档 Admin API 一节列出的四条路由行为均可在源码中逐一对应5.1 POST /admin/apps/built-in —— 自建托管内置应用被拒绝if MULTI_TENANT and get_onyx_managed_provider(request.app_type) is not None: raise OnyxError( OnyxErrorCode.INVALID_INPUT, Built-in apps are provided by Onyx; use PATCH /admin/apps/{id} to set action policies., )api.py即自托管模式下该端点照常可用管理员自建并填自己的凭据Cloud 模式下托管内置应用在此被拒绝错误信息直接指引使用 PATCH 端点。CUSTOM类型则被要求走POST /admin/apps/custom。5.2 PATCH /admin/apps/{id} —— 云端托管应用的唯一变更通道PATCH 端点以 id 为唯一键对托管应用只生效enabled与action_policies两个字段其余请求字段一律按UNSET处理app update_external_app( ... enablednone_as_unset(request.enabled), namenone_as_unset(request.name), # Gateway config is Onyx-owned for managed built-ins; leave it untouched. upstream_url_patterns( UNSET if managed else none_as_unset(request.upstream_url_patterns) ), auth_templateUNSET if managed else none_as_unset(request.auth_template), organization_credentials( UNSET if managed else none_as_unset(request.organization_credentials) ), action_policiesaction_policies, )api.py策略写入前还经过resolve_action_overrides校验与裁剪只接受当前目录中存在的action_id等于目录默认值或已不在目录中的条目会被剪掉——目录默认值从不物化成行新发布的端点在读取时直接解析为default_policy因此新增动作对全租户无迁移地生效见 registry.py 的effective_policy/resolve_action_overrides。变更后的提交顺序也值得注意端点先db_session.commit()落库再执行 sandbox 推送push_skills_for_users并二次提交使数据库成为唯一事实来源、sandbox 文件成为派生投影api.py避免推送失败让数据库领先于运行时。5.3 DELETE /admin/apps/{id} —— 云端拒绝删除if MULTI_TENANT and get_onyx_managed_provider(app.app_type) is not None: raise OnyxError( OnyxErrorCode.INVALID_INPUT, Built-in apps are provided by Onyx and cannot be deleted., )api.py5.4 响应层脱敏managed 置空 vs 自托管掩码_to_admin_responseapi.py是客户端可见性的关键分界managed MULTI_TENANT and get_onyx_managed_provider(app.app_type) is not None return ExternalAppAdminResponse( ... upstream_url_patterns[] if managed else list(app.upstream_url_patterns), auth_template{} if managed else app.auth_template, organization_credentials( {} if managed else app.organization_credentials.get_value(apply_maskTrue) ), ... is_onyx_managedmanaged, )托管应用organization_credentials、auth_template、upstream_url_patterns全部置空blanked并置is_onyx_managedTrue只暴露身份、启用状态与策略视图自托管内置应用凭据仍返回但是**掩码masked**形态且写入路径会把回显的掩码值还原为原值resolve_masked_credentials防止未改动的 secret 被覆写成它的掩码。响应模型中的is_onyx_managed: bool False字段models.py供前端使用——文档指出前端web/src/app/craft/v1/apps/据此隐藏凭据表单、添加内置应用入口与删除控件对托管应用只保留启用开关和策略编辑器。6. 用户侧 OAuth 流程零改动复用文档 OAuth 一节确认用户端点GET /apps、POST /apps/{id}/credentials与 OAuth 启动/回调路径在 Cloud 与自托管之间完全不变。云端仅共享一个 Onyx 自有的 OAuth 客户端、使用对所有租户固定的回调地址{WEB_DOMAIN}/craft/v1/apps/oauth/callback凭据注入与 token 刷新直接读取种子化的organization_credentials无需任何代码分叉。源码侧的用户端点可印证这一无感知设计api.pyGET /apps只列出enabled_onlyTrue的应用并附该用户的凭据状态credential_keys、掩码后的credential_values、authenticated标志不暴露组织凭据与原始 auth 模板POST /apps/{id}/credentials先检查app.enabled被管理员禁用时直接报 This app is currently disabled by an admin.——这正是管理员开关能即时作用于用户 OAuth 流程的落点。令牌刷新的健壮性由基类 base.py 的OAuthExternalAppProvider模板方法保障refresh_credentials以TokenRefreshTerminalErrorgrant 已死需重新授权与TokenRefreshTransientError网络/5xx 等瞬时错误保留现有 token 稍后重试区分失败类别并把新凭据合并到已存凭据上而非整体替换使 Slack 的team_id等仅在连接期返回的字段得以保留。云端与自托管共用这套刷新逻辑读取的都是各自租户种子化的组织凭据。7. 测试可独立验证的不变式清单文档 Tests 一节指向两个测试文件覆盖了方案的关键承诺test_managed_external_apps.py外部依赖单测注册表不变式provider 注册表与内置 skill id 集合一致且当前全部为 Onyx 托管test_all_built_ins_are_onyx_managed云端锁定Cloud guardstest_cloud_blocks_built_in_create验证创建被拒且数据库无落库test_cloud_patch_updates_policies_and_protects_creds_and_config验证 PATCH 携带的攻击性配置字段伪造 URL 模式、伪造 client_id/secret被忽略、响应中凭据/配置置空、数据库中的种子凭据原样保留test_cloud_blocks_built_in_delete验证删除被拒自托管对照test_self_hosted_built_in_response_shows_config_and_masked_creds验证非MULTI_TENANT下配置可见、凭据为掩码而非置空固定了 managed 与非 managed 的响应分界。test_managed_credentials.py纯单元测试凭据解析的三态语义未设置/全量/部分/空白以及 key 与 required 字段相等的显式不变式。8. 小结设计决策与适用边界维度Onyx CloudSelf-hostedOAuth 客户端凭据Onyx 持有EXT_APP_*环境变量注入管理员自建并填写租户内应用数量每app_type至多一个内置种子化且默认禁用管理员自由创建管理员可操作项仅enabledaction_policiesPATCH创建/编辑凭据与网关配置/删除响应中凭据/网关配置置空 is_onyx_managedTrue掩码凭据用户 OAuth 流程单客户端、固定回调完全复用既有流程不变该方案的工程要点可以归纳为三点接口即事实来源是否托管只由是否继承OnyxManagedExtApp决定isinstance检查贯穿注册表与 API类定义期校验__init_subclass__保证凭据映射与授权模板字段精确对齐配置错误在 import 时即失败锁定在写路径与读路径同时实施PATCH 写路径把网关字段按UNSET丢弃、_to_admin_response读路径置空双保险。适用边界方面云端锁定判断始终形如MULTI_TENANT and get_onyx_managed_provider(...) is not None因此同一套代码在自托管单租户部署下退化为管理员全权配置模式无需任何额外开关。【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表