关于 domain-separation
domain-separation 是面向 ServiceNow 多租户配置的开发指南,涵盖域层级、用户成员关系、域感知查询和跨域访问。它提供 ES5 示例,并列出可通过 MCP 使用的 ServiceNow 工具。
主要功能
- 01规划域层级
- 02配置域感知查询
- 03管理用户域成员关系
- 04设置域可见性与切换
- 05提供 MCP 工具工作流
适用场景
- 01为租户规划域结构
- 02将用户分配到指定域
- 03检查当前域中的记录可见性
- 04通过 MCP 查询域配置
MCP 相关
文档列出可用于域分离任务的 MCP 工具,包括 snow_query_table、snow_execute_script 和 snow_artifact_manage。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ serac-labs/ serac --skill domain-separation安装后在 Claude Code 中输入/domain-separation确认已加载。
详细介绍
ServiceNow 的域分离
域分离通过在不同域之间划分数据和流程来实现多租户。
域架构
TOP (Global)
├── Domain A (Customer 1)
│ ├── Sub-domain A1
│ └── Sub-domain A2
└── Domain B (Customer 2)
└── Sub-domain B1
主要表
| 表 | 用途 |
|---|---|
domain | 域定义 |
sys_user_has_domain | 用户域成员关系 |
domain_path | 域层级路径 |
sys_db_object | 表的域设置 |
域配置(ES5)
创建域
// Create domain (ES5 ONLY!)
var domain = new GlideRecord("domain")
domain.initialize()
domain.setValue("name", "Acme Corp")
domain.setValue("description", "Domain for Acme Corporation")
// Parent domain (empty for top-level)
domain.setValue("parent", parentDomainSysId)
// Domain visibility
domain.setValue("active", true)
domain.insert()
域感知查询
// Query respecting domain separation (ES5 ONLY!)
function getDomainAwareRecords(tableName, query) {
var gr = new GlideRecord(tableName)
// Domain separation is automatic when enabled
// Records are filtered to user's visible domains
if (query) {
gr.addEncodedQuery(query)
}
gr.query()
var records = []
while (gr.next()) {
records.push({
sys_id: gr.getUniqueValue(),
sys_domain: gr.getValue("sys_domain"),
sys_domain_path: gr.getValue("sys_domain_path"),
})
}
return records
}
跨域访问
// Access records across domains (requires elevated privileges) (ES5 ONLY!)
function getCrossdomainRecords(tableName) {
var gr = new GlideRecord(tableName)
// Disable domain separation for this query
gr.setQueryReferences(false)
// Query all domains
gr.queryNoDomain()
var records = []
while (gr.next()) {
records.push({
sys_id: gr.getUniqueValue(),
domain: gr.sys_domain.getDisplayValue(),
})
}
return records
}
用户域成员关系(ES5)
将用户分配到域
// Add user to domain (ES5 ONLY!)
function addUserToDomain(userSysId, domainSysId, isPrimary) {
// Check if already assigned
var existing = new GlideRecord("sys_user_has_domain")
existing.addQuery("user", userSysId)
existing.addQuery("domain", domainSysId)
existing.query()
if (existing.next()) {
return existing.getUniqueValue()
}
// Create assignment
var assignment = new GlideRecord("sys_user_has_domain")
assignment.initialize()
assignment.setValue("user", userSysId)
assignment.setValue("domain", domainSysId)
assignment.setValue("primary", isPrimary)
return assignment.insert()
}
获取用户所属的域
// Get domains accessible to user (ES5 ONLY!)
function getUserDomains(userSysId) {
var domains = []
var membership = new GlideRecord("sys_user_has_domain")
membership.addQuery("user", userSysId)
membership.query()
while (membership.next()) {
var domain = membership.domain.getRefRecord()
domains.push({
sys_id: domain.getUniqueValue(),
name: domain.getValue("name"),
is_primary: membership.getValue("primary") === "true",
})
}
return domains
}
域分离表(ES5)
配置表以启用域分离
// Enable domain separation on table (ES5 ONLY!)
// Note: This is typically done via UI, shown for reference
var tableConfig = new GlideRecord("sys_db_object")
if (tableConfig.get("name", "u_custom_table")) {
// Enable domain separation
tableConfig.setValue("domain_separated", true)
// Domain separation type
// 'simple' = records belong to one domain
// 'containment' = records visible to parent domains
tableConfig.setValue("domain_id_type", "simple")
tableConfig.update()
}
在指定域中创建记录
// Create record in specific domain (ES5 ONLY!)
function createInDomain(tableName, data, domainSysId) {
var gr = new GlideRecord(tableName)
gr.initialize()
// Set field values
for (var field in data) {
if (data.hasOwnProperty(field)) {
gr.setValue(field, data[field])
}
}
// Set domain
gr.setValue("sys_domain", domainSysId)
return gr.insert()
}
域选择器(ES5)
获取选择器可用的域
// Get domains for domain picker widget (ES5 ONLY!)
function getDomainsForPicker() {
var domains = []
var userId = gs.getUserID()
// Get user's accessible domains
var membership = new GlideRecord("sys_user_has_domain")
membership.addQuery("user", userId)
membership.query()
while (membership.next()) {
var domain = membership.domain.getRefRecord()
if (domain.getValue("active") === "true") {
domains.push({
sys_id: domain.getUniqueValue(),
name: domain.getValue("name"),
is_primary: membership.getValue("primary") === "true",
is_current: domain.getUniqueValue() === gs.getSession().getCurrentDomainID(),
})
}
}
// Sort: primary first, then alphabetically
domains.sort(function (a, b) {
if (a.is_primary && !b.is_primary) return -1
if (!a.is_primary && b.is_primary) return 1
return a.name.localeCompare(b.name)
})
return domains
}
切换当前域
// Switch user's current domain (ES5 ONLY!)
function switchDomain(domainSysId) {
var session = gs.getSession()
// Verify user has access
var membership = new GlideRecord("sys_user_has_domain")
membership.addQuery("user", gs.getUserID())
membership.addQuery("domain", domainSysId)
membership.query()
if (!membership.next()) {
gs.addErrorMessage("You do not have access to this domain")
return false
}
// Switch domain
session.setDomainID(domainSysId)
gs.addInfoMessage("Switched to domain: " + membership.domain.getDisplayValue())
return true
}
域可见性规则(ES5)
检查域可见性
// Check if record is visible in current domain (ES5 ONLY!)
function isRecordVisibleInDomain(tableName, recordSysId) {
var gr = new GlideRecord(tableName)
gr.addQuery("sys_id", recordSysId)
gr.query()
// If record is found, it's visible in current domain context
return gr.hasNext()
}
获取域路径
// Get full domain hierarchy path (ES5 ONLY!)
function getDomainPath(domainSysId) {
var path = []
var domain = new GlideRecord("domain")
if (!domain.get(domainSysId)) {
return path
}
// Build path from current to root
while (domain.isValidRecord()) {
path.unshift({
sys_id: domain.getUniqueValue(),
name: domain.getValue("name"),
})
if (!domain.parent) break
domain = domain.parent.getRefRecord()
}
return path
}
MSP/托管服务模式(ES5)
新租户入驻
// Create new tenant domain with initial setup (ES5 ONLY!)
function onboardTenant(tenantData) {
// Create domain
var domain = new GlideRecord("domain")
domain.initialize()
domain.setValue("name", tenantData.name)
domain.setValue("parent", tenantData.parentDomain || "")
var domainSysId = domain.insert()
// Create tenant admin user
var adminUser = new GlideRecord("sys_user")
adminUser.initialize()
adminUser.setValue("user_name", tenantData.adminEmail)
adminUser.setValue("email", tenantData.adminEmail)
adminUser.setValue("first_name", tenantData.adminFirstName)
adminUser.setValue("last_name", tenantData.adminLastName)
var adminSysId = adminUser.insert()
// Assign user to domain
addUserToDomain(adminSysId, domainSysId, true)
// Assign tenant admin role
var role = new GlideRecord("sys_user_has_role")
role.initialize()
role.setValue("user", adminSysId)
role.setValue("role", getTenantAdminRoleSysId())
role.insert()
return {
domain_sys_id: domainSysId,
admin_sys_id: adminSysId,
}
}
MCP 工具集成
可用工具
| 工具 | 用途 |
|---|---|
snow_query_table | 查询域感知数据 |
snow_execute_script | 测试域脚本 |
snow_artifact_manage | 查找域配置(action='find') |
示例工作流
// 1. Query domains
await snow_query_table({
table: "domain",
query: "active=true",
fields: "name,parent,sys_id",
})
// 2. Get user domain memberships
await snow_query_table({
table: "sys_user_has_domain",
query: "user=user_sys_id",
fields: "domain,primary",
})
// 3. Check domain-separated tables
await snow_query_table({
table: "sys_db_object",
query: "domain_separated=true",
fields: "name,label,domain_id_type",
})
最佳实践
- 规划层级 - 在实施前设计域结构
- 精简域 - 仅创建必要的隔离
- 用户访问权限 - 分配最低限度的必需域
- 测试 - 使用域选择器进行测试
- 全局数据 - 将共享数据保留在 TOP 域中
- 性能 - 域查询会增加开销
- 文档 - 记录域的用途
- 仅使用 ES5 - 不使用现代 JavaScript 语法
评论
支持 Markdown