清除过滤器
公告
Claire Zheng · 七月 29
InterSystems IRIS 2025.2 引入了 IRISSECURITY 数据库,用于存储安全数据。 与之前用于存储安全数据的数据库 IRISSYS 不同,IRISSECURITY 支持加密,可以保护静态敏感数据。 在今后的版本中,IRISSECURITY 将可实现镜像。
此版本还引入了可以执行常规安全管理任务的 %SecurityAdministrator 角色。
本文中介绍的更改将影响持续交付 (CD) 和扩展维护 (EM) 版本通道。 也就是说,从版本 2025.2(CD,于 2025 年 7 月 23 日发布)和 2026.1 (EM) 开始,InterSystems IRIS 将包含 IRISSECURITY 数据库,并且在升级时,所有安全数据会自动从 IRISSYS 迁移到 IRISSECURITY。
虽然 InterSystems IRIS 2025.2 预计于 2025 年 7 月 23 日发布,但我们暂缓了 InterSystems IRIS for Health 和 HealthShare Health Connect 2025.2 的公开发布,原因是我们正在着手完成针对已知镜像问题的修复计划,该问题会影响 OAuth 配置数据。
升级须知
IRISSECURITY 对用户与安全数据的交互方式做出了多处可能导致功能中断的更改:
用户无法再直接访问安全global,而必须使用各种安全类提供的 API。
OAuth2 Global无法再映射到其他数据库。
用户无法再随意查询安全表,即使在 SQL 安全已禁用的情况下也是如此。
系统数据库现在使用的预定义资源无法更改。 在 Unix 上,如果为之前版本的系统数据库创建并指定了新资源,在升级时,该新资源将被预定义资源替代(但如果有任何角色引用了非默认资源,则必须手动将其更改为使用默认资源,以保持数据库访问权限)。 在 Windows 上,必须将资源更改回默认资源。 如果您尝试在 Windows 上升级,而数据库具有非默认资源,升级将停止(实例不会修改),并会显示错误消息“Database must have a resource label of…”
以下各部分将详细介绍这些更改,以及在您依赖原始行为的情况下应采取的替代措施,但总体而言,在您进行升级之前,应当验证并测试您的应用程序和宏:
使用提供的安全 API 管理安全功能(而非直接访问global)。
拥有使用这些 API 所必需的权限(%DB_IRISSYS:R 和 Admin_Secure:U)。
Global 访问
之前,当安全global存储在 IRISSYS 数据库中时,用户可以通过以下权限访问安全数据:
%DB_IRISSYS:R:直接读取和通过安全 API 读取安全global。
%DB_IRISSYS:RW:读取和写入安全global。
%DB_IRISSYS:RW 和 Admin_Secure:U:通过安全 API 管理安全功能。
在 InterSystems IRIS 2025.2 中:
用户无法再直接访问安全global。
%DB_IRISSYS:R 和 %Admin_Secure:U 这两个权限是访问安全数据(通过提供的安全 API)以及通过各种安全类管理安全功能所需的最低权限。
对于常规安全管理,您可以使用新的 %SecurityAdministrator 角色。
已移除对安全数据的只读访问权限(之前可以通过 %DB_IRISSYS:R 实现)。
Global 存储位置
在 InterSystems IRIS 2025.2 中,以下安全global已从 IRISSYS 迁移到 IRISSECURITY 中的 ^SECURITY:
^SYS("SECURITY")
^OAuth2.*
^PKI.*
^SYS.TokenAuthD
下表列出了已迁移的最关键的global及其安全类、原存储位置和新存储位置:
安全类
原存储位置 (IRISSYS)
新存储位置 (IRISSECURITY)
不适用
^SYS("Security","Version")
^SECURITY("Version")
Security.Applications
^SYS("Security","ApplicationsD")
^SECURITY("ApplicationsD")
Security.DocDBs
^SYS("Security","DocDBsD")
^SECURITY("DocDBsD")
Security.Events
^SYS("Security","EventsD")
^SECURITY("EventsD")
Security.LDAPConfigs
^SYS("Security","LDAPConfigsD")
^SECURITY("LDAPConfigsD")
Security.KMIPServers
^SYS("Security","KMIPServerD")
^SECURITY("KMIPServerD")
Security.Resources
^SYS("Security","ResourcesD")
^SECURITY("ResourcesD")
Security.Roles
^SYS("Security","RolesD")
^SECURITY("RolesD")
Security.Services
^SYS("Security","ServicesD")
^SECURITY("ServicesD")
Security.SSLConfigs
^SYS("Security","SSLConfigsD")
^SECURITY("SSLConfigsD")
Security.System
^SYS("Security","SystemD")
^SECURITY("SystemD")
Security.Users
^SYS("Security","UsersD")
^SECURITY("UsersD")
%SYS.PhoneProviders
^SYS("Security","PhoneProvidersD")
^SECURITY("PhoneProvidersD ")
%SYS.X509Credentials
^SYS("Security","X509CredentialsD")
^SECURITY("X509CredentialsD ")
%SYS.OpenAIM.IdentityServices
^SYS("Security","OpenAIMIdentityServersD")
^SECURITY("OpenAIMIdentityServersD")
OAuth2.AccessToken
^OAuth2. AccessTokenD
^SECURITY("OAuth2.AccessToken ")
OAuth2.Client
^OAuth2.ClientD
^SECURITY("OAuth2.Client")
OAuth2.ServerDefinition
^OAuth2.ServerDefinitionD
^SECURITY("OAuth2.ServerDefinitionD")
OAuth2.Client.MetaData
^OAuth2.Client.MetaDataD
^SECURITY("OAuth2.Client.MetaDataD")
OAuth2.Server.AccessToken
^OAuth2.Server.AccessTokenD
^SECURITY("OAuth2.Server.AccessTokenD")
OAuth2.Server.Client
^OAuth2.Server.ClientD
^SECURITY("OAuth2.Server.ClientD")
OAuth2.Server.Configuration
^OAuth2.Server.ConfigurationD
^SECURITY("OAuth2.Server.ConfigurationD")
OAuth2.Server.JWTid
^OAuth2.Server.JWTidD
^SECURITY("OAuth2.Server.JWTidD")
OAuth2.Server.Metadata
^OAuth2.Server.MetadataD
^SECURITY("OAuth2.Server.MetadataD")
PKI.CAClient
^PKI.CAClientD
^SECURITY("PKI.CAClient")
PKI.CAServer
^PKI.CAServerD
^SECURITY("PKI.CAServer")
PKI.Certificate
^PKI.CertificateD
^SECURITY("PKI.Certificate")
%SYS.TokenAuth
^SYS.TokenAuthD
^SECURITY("TokenAuthD")
OAuth2 Global 映射
之前,可以将 OAuth2 Global映射到其他数据库,从而可以镜像 OAuth2 配置。
在 InterSystems IRIS 2025.2 中,无法再映射 OAuth2 global,且不能镜像 IRISSECURITY。 如果您过去依赖此行为进行镜像,可以使用以下任何替代方法:
手动对主节点和故障转移节点进行更改。
从主节点导出设置,然后将其导入到故障转移节点(需要 % ALL 权限)。
导出 OAuth2 配置数据:
set items = $name(^|"^^:ds:IRISSECURITY"|SECURITY("OAuth2"))_".gbl"
set filename = "/home/oauth2data.gbl"
do $SYSTEM.OBJ.Export(items,filename)
导入 OAuth2 配置数据:
do $SYSTEM.OBJ.Import(filename)
SQL 安全
之前,SQL 安全由 CPF 参数 DBMSSecurity 控制。 当 DBMSSecurity 禁用时,拥有 SQL 权限的用户可以随意查询数据库中的所有表。
在 InterSystems IRIS 2025.2 中:
DBMSSecurity CPF 参数已被替换为系统范围的 SQL 安全属性。 可以通过多种方式对此进行设置:
管理门户:System Administration > Security > System Security > System-wide Security Parameters > Enable SQL security(系统管理 > 安全 > 系统安全 > 系统范围的安全参数 > 启用 SQL 安全)
SetOption: ##class(%SYSTEM.SQL.Util).SetOption("SQLSecurity", "1")
Security.System.Modify: ##Class(Security.System).Modify(,.properties),其中,properties 为 properties("SQLSecurity")=1
安全表(security table)现只能通过 Detail 和 List API 进行查询,即使在 SQL 安全处于禁用状态的情况下,也需要同时具有 %DB_IRISSYS:R 和 %Admin_Secure:U 权限才能进行查询。
例如,要获取角色列表,无法再直接查询 Security.Roles 表, 而应使用 Security.Roles_List() 查询:
SELECT Name, Description FROM Security.Roles_List()
加密 IRISSECURITY
要加密 IRISSECURITY,请按以下步骤操作:
创建新的加密密钥。 转到 System Administration > Encryption > Create New Encryption Key File(系统管理 > 加密 > 创建新的加密密钥文件),并指定以下设置:
Key File(密钥文件)– 加密密钥的名称。
Administrator Name(管理员名称)– 管理员的名称。
Password(密码)– 密钥文件的密码。
激活加密密钥。 转到 System Administration > Encryption > Database Encryption(系统管理 > 加密 > 数据库加密),并选择 Activate Key(激活密钥),指定第 1 步中的 Key File(密钥文件)、Administrator Name(管理员名称)和 Password(密码)。
转到 System Administration > Encryption > Database Encryption(系统管理 > 加密 > 数据库加密),并选择 Configure Startup Settings(配置启动设置)。
从 Key Activation at Startup(启动时的密钥激活)下拉菜单中选择一种密钥激活方法。 InterSystems 强烈建议选择 Interactive(交互式)密钥激活。
在 Encrypt IRISSECURITY Database(加密 IRISSECURITY 数据库)下拉列表中,选择 Yes(是)。
重新启动系统,以加密 IRISSECURITY。
百分比类(那些类名以%开头的类,可以在任何命名空间访问)访问规则
在之前版本的 InterSystems IRIS 中,管理 Web 应用程序对附加百分比类的访问权限的过程涉及到对安全global进行写入操作。 在 InterSystems IRIS 2025.2 中,可以通过管理门户或 ^SECURITY 例程完成此过程。
管理门户(Management Portal)
通过管理门户创建百分比类访问规则:
转到 System Administration > Security > Web Applications(系统管理 > 安全 > Web 应用程序)。
选择您的 Web 应用程序。
在 Percent Class Access(百分比类访问)选项卡中设置以下选项:
Type(类型):控制该规则是仅适用于应用程序对指定百分比类的访问 (AllowClass),还是适用于包含指定前缀的所有类 (AllowPrefix)。
Class name(类名称):允许应用程序访问的百分比类或前缀。
Allow access(允许访问):是否允许应用程序访问指定的百分比类或软件包。
Add this same access to ALL applications(为所有应用程序添加相同的访问权限):是否为所有应用程序应用此规则。
^SECURITY
通过 ^SECURITY 例程创建类访问规则:
在 %SYS 命名空间中,运行 ^SECURITY 例程:
DO ^SECURITY
选择选项 5, 1, 8, 和 1,以输入类访问规则提示。
按照提示指定以下内容:
Application?(应用程序?)– 应用程序名称。
Allow type?(允许类型?)– 该规则是适用于应用程序访问特定类 (AllowClass) 还是访问包含指定前缀的所有类 (AllowPrefix)。
Class or package name?(类或软件包名称?)– 允许应用程序访问的类或前缀。
Allow access?(允许访问?)– 是否允许应用程序访问指定类或软件包。
文章
Michael Lei · 九月 13, 2022
Globals是InterSystems IRIS的数据持久性的核心。它很灵活,允许存储JSON文档、关系数据、面向对象的数据、OLAP立方体和自定义数据模型,例如思维导图。要了解如何使用globals来存储、删除和获取思维导图数据,请遵循以下步骤:
1. 把repo Clone/git到任意本地目录
$ git clone https://github.com/yurimarx/global-mindmap.git
2. 在该目录下打开Docker 终端并执行:
$ docker-compose build
3. 启动 IRIS 容器:
$ docker-compose up -d
4. 访问 http://localhost:3000 来使用思维导图的前端并创建类似以上的思维导图
本例子的源代码
存储数据 (更多请访问: https://www.npmjs.com/package/mind-elixir):
{
topic: 'node topic',
id: 'bd1c24420cd2c2f5',
style: { fontSize: '32', color: '#3298db', background: '#ecf0f1' },
parent: null,
tags: ['Tag'],
icons: ['😀'],
hyperLink: 'https://github.com/ssshooter/mind-elixir-core',
}
注意parent属性,它被用来在mindmap节点之间建立父/子关系。
使用Globals 来存储思维导图的源代码
ClassMethod StoreMindmapNode
/// Store mindmap node
ClassMethod StoreMindmapNode() As %Status
{
Try {
Set data = {}.%FromJSON(%request.Content)
Set ^mindmap(data.id) = data.id /// set mindmap key
Set ^mindmap(data.id, "topic") = data.topic /// set topic subscript
Set ^mindmap(data.id, "style", "fontSize") = data.style.fontSize /// set style properties subscripts
Set ^mindmap(data.id, "style", "color") = data.style.color
Set ^mindmap(data.id, "style", "background") = data.style.background
Set ^mindmap(data.id, "parent") = data.parent /// store parent id subscript
Set ^mindmap(data.id, "tags") = data.tags.%ToJSON() /// store tags subscript
Set ^mindmap(data.id, "icons") = data.icons.%ToJSON() /// store icons subscript
Set ^mindmap(data.id, "hyperLink") = data.hyperLink /// store hyperLink subscript
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write "Saved"
Return $$$OK
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return $$$NOTOK
}
}
我们创建了一个名为^mindmap的Global。对于每个思维导图的属性,它被存储在一个Globals下标中。下标的键是mindmap的id属性。
删除思维导图节点的源代码 - kill the global
ClassMethod DeleteMindmapNode
/// Delete mindmap node
ClassMethod DeleteMindmapNode(id As %String) As %Status
{
Try {
Kill ^mindmap(id) /// delete selected mindmap node using the id (global key)
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write "Deleted"
Return $$$OK
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return $$$NOTOK
}
}
这个例子使用mindmap.id作为mindmap的Global Key,所以删除很容易: call Kill ^mindmap(<mindmap id>)
获得所有存储内容的源代码- 用 $ORDER循环globals
ClassMethod GetMindmap - return all mindmap global nodes
/// Get mindmap content
ClassMethod GetMindmap() As %Status
{
Try {
Set Nodes = []
Set Key = $Order(^mindmap("")) /// get the first mindmap node stored - the root
Set Row = 0
While (Key '= "") { /// while get child mindmap nodes
Do Nodes.%Push({}) /// create a item into result
Set Nodes.%Get(Row).style = {}
Set Nodes.%Get(Row).id = Key /// return the id property
Set Nodes.%Get(Row).hyperLink = ^mindmap(Key,"hyperLink") /// return the hyperlink property
Set Nodes.%Get(Row).icons = ^mindmap(Key,"icons") /// return icons property
Set Nodes.%Get(Row).parent = ^mindmap(Key,"parent") /// return parent id property
Set Nodes.%Get(Row).style.background = ^mindmap(Key,"style", "background") /// return the style properties
Set Nodes.%Get(Row).style.color = ^mindmap(Key,"style", "color")
Set Nodes.%Get(Row).style.fontSize = ^mindmap(Key,"style", "fontSize")
Set Nodes.%Get(Row).tags = ^mindmap(Key,"tags") /// return tags property
Set Nodes.%Get(Row).topic = ^mindmap(Key,"topic") /// return topic property (title mindmap node)
Set Row = Row + 1
Set Key = $Order(^mindmap(Key)) /// get the key to the next mindmap global node
}
Set %response.Status = 200
Set %response.Headers("Access-Control-Allow-Origin")="*"
Write Nodes.%ToJSON()
Return $$$OK
} Catch err {
write !, "Error name: ", ?20, err.Name,
!, "Error code: ", ?20, err.Code,
!, "Error location: ", ?20, err.Location,
!, "Additional data: ", ?20, err.Data, !
Return $$$NOTOK
}
}
用$Order(^mindmap("")) - empty "" - 得到第一个mindmap Global (根节点)。对于每个属性值,我们使用^mindmap(Key,<property name>)。最后,调用$Order(^mindmap(Key))来获得下一个事件。
前端
Mind-elixir和React被用来渲染和编辑mindmap,消耗使用IRIS构建的API后端。见mindmap的反应组件:
Mindmap React component - consuming IRIS REST API
import React from "react";
import MindElixir, { E } from "mind-elixir";
import axios from 'axios';
class Mindmap extends React.Component {
componentDidMount() {
this.dynamicWidth = window.innerWidth;
this.dynamicHeight = window.innerHeight;
axios.get(`http://localhost:52773/global-mindmap/hasContent`)
.then(res => {
if (res.data == "1") {
axios.get(`http://localhost:52773/global-mindmap/get`)
.then(res2 => {
this.ME = new MindElixir({
el: "#map",
direction: MindElixir.LEFT,
data: this.renderExistentMindmap(res2.data),
draggable: true, // default true
contextMenu: true, // default true
toolBar: true, // default true
nodeMenu: true, // default true
keypress: true // default true
});
this.ME.bus.addListener('operation', operation => {
console.log(operation)
if (operation.name == 'finishEdit' || operation.name == 'editStyle') {
this.saveMindmapNode(operation.obj)
} else if (operation.name == 'removeNode') {
this.deleteMindmapNode(operation.obj.id)
}
})
this.ME.init();
})
} else {
this.ME = new MindElixir({
el: "#map",
direction: MindElixir.LEFT,
data: MindElixir.new("New Mindmap"),
draggable: true, // default true
contextMenu: true, // default true
toolBar: true, // default true
nodeMenu: true, // default true
keypress: true // default true
});
this.ME.bus.addListener('operation', operation => {
console.log(operation)
if (operation.name == 'finishEdit' || operation.name == 'editStyle') {
this.saveMindmapNode(operation.obj)
} else if (operation.name == 'removeNode') {
this.deleteMindmapNode(operation.obj.id)
}
})
this.saveMindmapNode(this.ME.nodeData)
this.ME.init();
}
})
}
render() {
return (
<div id="map" style={{ height: window.innerHeight + 'px', width: '100%' }} />
);
}
deleteMindmapNode(mindmapNodeId) {
axios.delete(`http://localhost:52773/global-mindmap/delete/${mindmapNodeId}`)
.then(res => {
console.log(res);
console.log(res.data);
})
}
saveMindmapNode(node) {
axios.post(`http://localhost:52773/global-mindmap/save`, {
topic: (node.topic == undefined ? "" : node.topic),
id: node.id,
style: (node.style == undefined ? "" : node.style),
parent: (node.parent == undefined ? "" : node.parent.id),
tags: (node.tags == undefined ? [] : node.tags),
icons: (node.icons == undefined ? [] : node.icons),
hyperLink: (node.hyperLink == undefined ? "" : node.hyperLink)
})
.then(res => {
console.log(res);
console.log(res.data);
})
}
renderExistentMindmap(data) {
let root = data[0]
let nodeData = {
id: root.id,
topic: root.topic,
root: true,
style: {
background: root.style.background,
color: root.style.color,
fontSize: root.style.fontSize,
},
hyperLink: root.hyperLink,
children: []
}
this.createTree(nodeData, data)
return { nodeData }
}
createTree(nodeData, data) {
for(let i = 1; i < data.length; i++) {
if(data[i].parent == nodeData.id) {
let newNode = {
id: data[i].id,
topic: data[i].topic,
root: false,
style: {
background: data[i].style.background,
color: data[i].style.color,
fontSize: data[i].style.fontSize,
},
hyperLink: data[i].hyperLink,
children: []
}
nodeData.children.push(newNode)
this.createTree(newNode, data)
}
}
}
}
export default Mindmap;