openxiangda 1.0.137 → 1.0.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -6963,6 +6963,21 @@ function validateResourceItem(kind, item, errors, warnings) {
6963
6963
  } else {
6964
6964
  errors.push(`${label}: 不支持的 viewType: ${definition.viewType}`);
6965
6965
  }
6966
+ for (const [index, group] of (item.permissionGroups || []).entries()) {
6967
+ const actions = group.actions || group.operations;
6968
+ if (actions !== undefined) {
6969
+ if (!Array.isArray(actions)) {
6970
+ errors.push(`${label}.permissionGroups[${index}]: actions/operations 必须是数组`);
6971
+ } else {
6972
+ for (const action of actions) {
6973
+ const normalizedAction = normalizePermissionActionValue(action);
6974
+ if (!DATA_VIEW_PERMISSION_ACTIONS.has(normalizedAction)) {
6975
+ errors.push(`${label}.permissionGroups[${index}]: 不支持的数据视图 action "${action}"`);
6976
+ }
6977
+ }
6978
+ }
6979
+ }
6980
+ }
6966
6981
  validateDataViewPerformance(label, definition, errors, warnings, storageMode);
6967
6982
  }
6968
6983
  if (kind === 'storageConfigs') {
@@ -7104,6 +7119,19 @@ function validateResourceItem(kind, item, errors, warnings) {
7104
7119
  if (kind === 'formPermissionGroups') {
7105
7120
  if (!item.name) errors.push(`${label}: 缺少 name`);
7106
7121
  if (!item.formCode && !item.formUuid) errors.push(`${label}: 缺少 formCode 或 formUuid`);
7122
+ const actions = item.actions || item.operations;
7123
+ if (actions !== undefined) {
7124
+ if (!Array.isArray(actions)) {
7125
+ errors.push(`${label}: actions/operations 必须是数组`);
7126
+ } else {
7127
+ for (const action of actions) {
7128
+ const normalizedAction = normalizePermissionActionValue(action);
7129
+ if (!FORM_PERMISSION_ACTIONS.has(normalizedAction)) {
7130
+ errors.push(`${label}: 不支持的表单 action "${action}"`);
7131
+ }
7132
+ }
7133
+ }
7134
+ }
7107
7135
  if (item.dataPermission?.type === 'scope_policy' && !getScopePolicyCode(item.dataPermission)) {
7108
7136
  errors.push(`${label}: dataPermission.type=scope_policy 时必须声明 policyCode`);
7109
7137
  }
@@ -7136,9 +7164,33 @@ function validateScopePermissionResources(manifest, errors, warnings) {
7136
7164
  ? (Array.isArray(item.permissionGroups) ? item.permissionGroups : [])
7137
7165
  : [item];
7138
7166
  for (const group of groups) {
7167
+ const actions = kind === 'dataViews'
7168
+ ? normalizeDataViewPermissionGroupActions(group)
7169
+ : normalizeFormPermissionGroupActions(group);
7139
7170
  const dataPermission = group?.dataPermission;
7171
+ if (
7172
+ kind === 'formPermissionGroups' &&
7173
+ actions.includes('create') &&
7174
+ (!dataPermission || dataPermission.type !== 'scope_policy')
7175
+ ) {
7176
+ warnings.push(`${resourceLabel(kind, item)}: 声明了 create,但未声明 create 对应的 scope_policy;若新增数据必须落在用户业务范围内,请补充 dataPermission.type=scope_policy`);
7177
+ }
7140
7178
  if (!dataPermission) continue;
7141
7179
  if (dataPermission.type === 'scope_policy') {
7180
+ if (kind === 'formPermissionGroups') {
7181
+ const missing = ['view', 'edit', 'delete', 'export']
7182
+ .filter(action => !actions.includes(action));
7183
+ if (missing.length) {
7184
+ warnings.push(`${resourceLabel(kind, item)}: 使用 scope_policy 但 action 覆盖不完整,缺少 ${missing.join(', ')};请确认每个角色的 action matrix`);
7185
+ }
7186
+ }
7187
+ if (kind === 'dataViews') {
7188
+ const missing = ['query', 'stats', 'export']
7189
+ .filter(action => !actions.includes(action));
7190
+ if (missing.length) {
7191
+ warnings.push(`${resourceLabel(kind, item)}: Data View 使用 scope_policy 但 action 覆盖不完整,缺少 ${missing.join(', ')}`);
7192
+ }
7193
+ }
7142
7194
  const policyCode = getScopePolicyCode(dataPermission);
7143
7195
  if (!policyCode) {
7144
7196
  errors.push(`${resourceLabel(kind, item)}: scope_policy 缺少 policyCode`);
@@ -10472,11 +10524,20 @@ function normalizeDataViewManifest(bound, dataView) {
10472
10524
  definition: normalizedDefinition,
10473
10525
  refreshConfig: dataView.refreshConfig || dataView.refresh || normalizedDefinition.refresh,
10474
10526
  permissionGroups: Array.isArray(dataView.permissionGroups)
10475
- ? dataView.permissionGroups.map(group => stripUndefinedValues(withoutResourceMeta(group)))
10527
+ ? dataView.permissionGroups.map(group => normalizeDataViewPermissionGroupManifest(group))
10476
10528
  : undefined,
10477
10529
  });
10478
10530
  }
10479
10531
 
10532
+ function normalizeDataViewPermissionGroupManifest(group = {}) {
10533
+ const body = stripUndefinedValues({
10534
+ ...withoutResourceMeta(group),
10535
+ operations: normalizeDataViewPermissionGroupActions(group),
10536
+ });
10537
+ delete body.actions;
10538
+ return body;
10539
+ }
10540
+
10480
10541
  function normalizeRouteManifest(route) {
10481
10542
  const code = route.code || route.resourceCode;
10482
10543
  return stripUndefinedValues({
@@ -11161,10 +11222,12 @@ function normalizeFormPermissionGroupManifest(bound, group) {
11161
11222
  name: group.name || group.code,
11162
11223
  type: group.type || 'view',
11163
11224
  roles: group.roles || [],
11225
+ operations: normalizeFormPermissionGroupActions(group),
11164
11226
  };
11165
11227
  delete body.code;
11166
11228
  delete body.formCode;
11167
11229
  delete body.form;
11230
+ delete body.actions;
11168
11231
  return stripUndefinedValues(body);
11169
11232
  }
11170
11233
 
@@ -11186,6 +11249,52 @@ function sortStringValues(value = []) {
11186
11249
  return [...(value || [])].map(item => String(item)).sort();
11187
11250
  }
11188
11251
 
11252
+ const FORM_PERMISSION_ACTIONS = new Set([
11253
+ 'view',
11254
+ 'create',
11255
+ 'edit',
11256
+ 'delete',
11257
+ 'export',
11258
+ 'import',
11259
+ 'change_records',
11260
+ 'workflow',
11261
+ ]);
11262
+
11263
+ const DATA_VIEW_PERMISSION_ACTIONS = new Set([
11264
+ 'query',
11265
+ 'stats',
11266
+ 'export',
11267
+ 'refresh',
11268
+ ]);
11269
+
11270
+ function normalizePermissionActionValue(action) {
11271
+ const value = String(action || '').trim();
11272
+ if (!value) return '';
11273
+ if (value === 'submit') return 'create';
11274
+ return value;
11275
+ }
11276
+
11277
+ function normalizePermissionActionArray(value, allowed, fallback = []) {
11278
+ const source = Array.isArray(value) ? value : fallback;
11279
+ return unique(source.map(normalizePermissionActionValue).filter(action => allowed.has(action)));
11280
+ }
11281
+
11282
+ function normalizeFormPermissionGroupActions(group = {}) {
11283
+ if (group.actions !== undefined || group.operations !== undefined) {
11284
+ return normalizePermissionActionArray(group.actions || group.operations, FORM_PERMISSION_ACTIONS);
11285
+ }
11286
+ return group.type === 'submit'
11287
+ ? ['create']
11288
+ : ['view', 'edit', 'delete', 'change_records', 'workflow'];
11289
+ }
11290
+
11291
+ function normalizeDataViewPermissionGroupActions(group = {}) {
11292
+ if (group.actions !== undefined || group.operations !== undefined) {
11293
+ return normalizePermissionActionArray(group.actions || group.operations, DATA_VIEW_PERMISSION_ACTIONS);
11294
+ }
11295
+ return ['query'];
11296
+ }
11297
+
11189
11298
  function pickObjectKeys(source = {}, keys = []) {
11190
11299
  return keys.reduce((acc, key) => {
11191
11300
  acc[key] = source[key];
@@ -11407,7 +11516,7 @@ function normalizeDataViewPermissionGroupsForPlan(groups = []) {
11407
11516
  code,
11408
11517
  name: group.name || code || '默认权限组',
11409
11518
  roles: Array.isArray(group.roles) ? sortStringValues(group.roles) : [],
11410
- operations: Array.isArray(group.operations) ? sortStringValues(group.operations) : ['query'],
11519
+ operations: sortStringValues(normalizeDataViewPermissionGroupActions(group)),
11411
11520
  fieldPermissions: normalizeFieldPermissionsForPlan(group.fieldPermissions),
11412
11521
  dataPermission: group.dataPermission || undefined,
11413
11522
  });
@@ -124,15 +124,19 @@ const DESIGN_GATE_TOPICS = [
124
124
  '哪些角色可以设置应用角色、分配角色成员、给角色分配接口权限;这些角色是否需要 `app:role:manage`',
125
125
  '哪些角色可以维护页面权限组、表单权限组、组织账号;是否需要 `app:page-permission-group:manage`、`app:form-permission-group:manage`、`app:organization:manage`',
126
126
  '业务范围字段是什么,哪些可见字段需要派生隐藏 scalar key 供表单权限条件匹配',
127
- '页面/菜单/路由、表单 submit/view/manage/fieldAccessPolicy/dataScope 的权限矩阵如何落到 resources',
127
+ '每个角色的 action matrix 是什么:view/create/edit/delete/export/import/change_records/workflow 是否分别授权',
128
+ '导出/导入是否独立授权;导出范围是否与 view 范围不同,导入是否还需要逐行 create 范围校验',
129
+ 'create 是否需要 scope_policy 校验提交数据,防止新增到未授权业务范围',
130
+ '页面/菜单/路由、表单 actions/fieldAccessPolicy/dataPermission/scope_policy 的权限矩阵如何落到 resources',
128
131
  '查询参数是否参与授权;如果参与,只能做上下文、筛选或 ticket 输入,敏感数据必须由 public-access grant、角色、表单权限组或 App Function 校验',
129
132
  '验收时需要模拟哪些角色、账号状态、业务范围、查询参数篡改和拒绝场景',
130
133
  ],
131
134
  recommendedDefaults: [
132
135
  '正式后台优先选择 managed-platform-account;如果平台账号已存在,选择 existing-platform-user-assignment;固定内部门户选择 static-role-permission',
133
136
  'query-param-context 仅用于低风险页面上下文、筛选条件或公开 ticket 输入,不作为敏感数据授权依据',
134
- '角色写 src/resources/roles,页面组写 permissions/page-groups,表单组写 permissions/form-groups,显式声明 dataScope/operations/fieldAccessPolicy',
137
+ '角色写 src/resources/roles,页面组写 permissions/page-groups,表单组写 permissions/form-groups,显式声明 actions/dataPermission/fieldAccessPolicy;平台内部仍兼容存储为 operations',
135
138
  '复杂业务范围优先写 permissions/scope-dimensions、permissions/scope-grant-sources、permissions/data-scope-policies,并在表单权限组或 data-view 权限组使用 dataPermission.type=scope_policy',
139
+ 'scope_policy 只表达数据范围,actions 表达操作能力;按钮隐藏只是 UX,后端 action check 才是权威',
136
140
  'scope_policy 默认语义是个人授权 + 当前应用角色授权;多维 rules 显式 AND;空授权集合拒绝;管理员绕过但可审计',
137
141
  '能管理角色或给别人分配角色的应用角色必须在 roles manifest 声明 apiPermissionCodes;至少包含 app:role:manage,按需加入 app:page-permission-group:manage、app:form-permission-group:manage、app:organization:manage',
138
142
  '由校区管理员等委托管理员创建的新角色,如果还具备继续管理账号/角色/权限的能力,也必须同步声明并发布对应 apiPermissionCodes',
@@ -144,6 +148,7 @@ const DESIGN_GATE_TOPICS = [
144
148
  '不能只做前端权限隐藏,必须有后端角色、表单权限组、public-access grant 或 App Function 校验',
145
149
  '查询参数不能作为敏感授权依据,只能作为上下文、筛选或 ticket 输入',
146
150
  '只配菜单可见,不配后端数据权限',
151
+ '只隐藏按钮但没有表单权限组 actions 或 App Function 服务端校验',
147
152
  '为了省事授予全部数据再在前端过滤',
148
153
  '为每个客户/项目/区域/学院/班级/门店创建一个角色或一个权限组',
149
154
  '把应用表单里的业务范围强行同步成平台部门、平台角色或大量权限组',
@@ -64,7 +64,7 @@ AI 在实现成熟交互前必须先判断是否已有可靠组件或库:
64
64
  | 富文本编辑 | `EditorField` | 含图片上传集成 | 完整工具栏、平台图床、粘贴清洗 |
65
65
  | 数字签名 | `DigitalSignatureField` | 平台级签名 | 签名采集、验证、归档 |
66
66
  | 地理位置 | `LocationField` | 接入地图服务 | 定位、地图选点、地址解析 |
67
- | 数据管理列表 | `DataManagementList` | 接入数据查询 API | 分页、筛选、导出、批量操作、字段权限 |
67
+ | 数据管理列表 | `DataManagementList` | 接入数据查询 API | 分页、筛选、导出、批量操作、字段权限、后端 action summary 按钮控制 |
68
68
 
69
69
  ### 示例
70
70
 
@@ -89,6 +89,12 @@ OSS 浏览器直传所需的 CORS 规则在 `src/resources/storage/<code>.json`
89
89
 
90
90
  业务应用只能从 `openxiangda`、`openxiangda/runtime`、`openxiangda/runtime/react` 等公开入口导入组件和 SDK。不要为了减小包体积去引用 `openxiangda/packages/sdk/dist/...` 内部文件;React SPA 模板已提供 vendor chunk 分组,SDK 会把 `antd-mobile`、`dayjs` 等大型 UI 依赖交给应用构建器按路由拆分。
91
91
 
92
+ `DataManagementList` 默认 `permissionMode="auto"`,进入页面会读取表单 action
93
+ summary,并用 `can.create/export/import/delete/workflow/change_records` 隐藏新增、
94
+ 导入、导出、删除、流程等按钮。`readonly=true` 仍会强制关闭新增、编辑、删除和
95
+ 导入。`actionOverrides` 只能关闭按钮,不能扩大后端权限;敏感授权必须依赖后端
96
+ form actions、`scope_policy` 或 App Function 服务端校验。
97
+
92
98
  > ✅ 一句话原则:**这些场景看到了,直接抄上表,不要自己写。**
93
99
 
94
100
  ---
@@ -27,8 +27,8 @@ Required design:
27
27
  scalar scope keys, platform role member sync state.
28
28
  - `src/resources/roles/*.json`: stable app role codes.
29
29
  - `src/resources/permissions/page-groups/*.json`: menu, route, and page access.
30
- - `src/resources/permissions/form-groups/*.json`: submit/view/manage, data
31
- scope, operations, and `fieldAccessPolicy`.
30
+ - `src/resources/permissions/form-groups/*.json`: `actions`, data scope or
31
+ `dataPermission.type=scope_policy`, and `fieldAccessPolicy`.
32
32
  - `apiPermissionCodes` on roles that are allowed to manage roles, role members,
33
33
  permission groups, or organization accounts.
34
34
  - Sync App Functions or trusted JS_CODE: sync organization units, accounts, and
@@ -142,8 +142,13 @@ The platform keeps RBAC and data range separate:
142
142
 
143
143
  - Roles decide who can enter pages, submit/view forms, use operations, and
144
144
  maintain permissions.
145
+ - Form `actions` decide what a role can do: `view`, `create`, `edit`, `delete`,
146
+ `export`, `import`, `change_records`, and `workflow`.
147
+ - Data View `actions` decide query capabilities: `query`, `stats`, `export`,
148
+ and `refresh`.
145
149
  - Field access remains `fieldAccessPolicy`.
146
- - Business scope policies decide which rows a matched role/user can read.
150
+ - Business scope policies decide which rows or submitted scope values a matched
151
+ role/user can access for the current action.
147
152
  - App Functions must still check role/scope server-side before sensitive
148
153
  writes.
149
154
 
@@ -166,12 +171,36 @@ Default semantics:
166
171
  a separate authorization row.
167
172
  - App Function form read helpers enforce view permission for real callers. Do
168
173
  not grant broad form read permission and then filter in function/page code.
174
+ - `DataManagementList` can hide buttons from the backend action summary, but it
175
+ is not the authority. Sensitive actions must be represented by form actions or
176
+ App Function server checks.
169
177
 
170
178
  Do not model a dynamic business object set by generating dozens or hundreds of
171
179
  roles or permission groups. Use a business authorization form and `scope_policy`
172
180
  when the object set is maintained by users, imported from another system, or
173
181
  expected to grow.
174
182
 
183
+ Recommended action pattern:
184
+
185
+ ```json
186
+ {
187
+ "code": "campus_profile_manage",
188
+ "formCode": "student_profile",
189
+ "name": "校区管理员档案权限",
190
+ "type": "view",
191
+ "roles": ["campus_admin"],
192
+ "actions": ["view", "edit", "export"],
193
+ "dataPermission": {
194
+ "type": "scope_policy",
195
+ "policyCode": "campus_row_scope"
196
+ },
197
+ "fieldAccessPolicy": {
198
+ "defaultAccess": "readonly",
199
+ "fields": [{ "fieldId": "remark", "access": "edit" }]
200
+ }
201
+ }
202
+ ```
203
+
175
204
  ## Role Setting Delegation
176
205
 
177
206
  Setting application roles is itself permission controlled. A user can be a
@@ -289,7 +289,43 @@ Reference the policy from a form permission group:
289
289
  "name": "学院查看学生",
290
290
  "type": "view",
291
291
  "roles": ["college_admin"],
292
- "operations": ["view"],
292
+ "actions": ["view", "edit", "export"],
293
+ "dataPermission": {
294
+ "type": "scope_policy",
295
+ "policyCode": "college_data"
296
+ }
297
+ }
298
+ ```
299
+
300
+ Unified permission model:
301
+
302
+ - `scope_policy` means data range: which rows this user/current app role can
303
+ access.
304
+ - `actions` means operation ability: what the role can do to rows in that
305
+ range. Form actions are `view`, `create`, `edit`, `delete`, `export`,
306
+ `import`, `change_records`, and `workflow`.
307
+ - The platform still stores actions in the legacy `operations` field. Manifests
308
+ may use either `actions` or `operations`; prefer `actions` in new resources.
309
+ - `type: "submit"` is the legacy create group and maps to `actions:["create"]`.
310
+ - `export` is independent from `view` for new apps. Old apps may temporarily
311
+ rely on view-to-export compatibility, but `permission audit` should be used to
312
+ migrate to explicit `export`.
313
+ - `create` with `scope_policy` validates submitted scope fields; a campus admin
314
+ cannot create a row for another campus just because the create button is
315
+ visible.
316
+ - Frontend button hiding is UX only. The backend action check and action-specific
317
+ data range are authoritative.
318
+
319
+ Create permission with business scope:
320
+
321
+ ```json
322
+ {
323
+ "code": "college_student_create",
324
+ "formCode": "student",
325
+ "name": "学院管理员新增本学院学生",
326
+ "type": "submit",
327
+ "roles": ["college_admin"],
328
+ "actions": ["create"],
293
329
  "dataPermission": {
294
330
  "type": "scope_policy",
295
331
  "policyCode": "college_data"
@@ -298,7 +334,8 @@ Reference the policy from a form permission group:
298
334
  ```
299
335
 
300
336
  For Data View permission groups, use the same `dataPermission` shape inside the
301
- view's `permissionGroups`.
337
+ view's `permissionGroups`. Data View actions are `query`, `stats`, `export`,
338
+ and `refresh`.
302
339
 
303
340
  Default runtime semantics:
304
341
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.137",
3
+ "version": "1.0.138",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -1021,6 +1021,12 @@ interface ProcessDefinition {
1021
1021
  interface ViewPermissionSummary {
1022
1022
  fieldPermissions: Record<string, 'FORM_FILED_HIDDEN' | 'FORM_FILED_VIEW' | 'FORM_FILED_EDIT'>;
1023
1023
  operations: string[];
1024
+ actions?: string[];
1025
+ can?: Record<string, boolean>;
1026
+ fieldAccessPolicy?: any;
1027
+ hasFullAccess?: boolean;
1028
+ resourceType?: string;
1029
+ matchedGroupCodes?: string[];
1024
1030
  }
1025
1031
  /** 表单实例数据 */
1026
1032
  interface FormInstanceData {
@@ -1021,6 +1021,12 @@ interface ProcessDefinition {
1021
1021
  interface ViewPermissionSummary {
1022
1022
  fieldPermissions: Record<string, 'FORM_FILED_HIDDEN' | 'FORM_FILED_VIEW' | 'FORM_FILED_EDIT'>;
1023
1023
  operations: string[];
1024
+ actions?: string[];
1025
+ can?: Record<string, boolean>;
1026
+ fieldAccessPolicy?: any;
1027
+ hasFullAccess?: boolean;
1028
+ resourceType?: string;
1029
+ matchedGroupCodes?: string[];
1024
1030
  }
1025
1031
  /** 表单实例数据 */
1026
1032
  interface FormInstanceData {