openxiangda-devkit-core 2.0.0-alpha.70 → 2.0.0-alpha.71

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.
@@ -6,6 +6,7 @@ import { supportsFilter, supportsSearch, supportsSort, } from './field-query-pla
6
6
  import { physicalPlanForField } from './field-physical-plan.js';
7
7
  import { SEMANTIC_FIELD_PATH_PATTERN, semanticFieldPathRoot, validateAuthzSemanticBindings, } from './field-policy-path.js';
8
8
  import { compatibleWidgets, isCompatibleFieldWidget, resolveDataFieldSurfaceWidget, } from './field-surface.js';
9
+ import { DATA_POLICY_EXPRESSION_MAX_DEPTH, DATA_POLICY_EXPRESSION_MAX_GROUP_ITEMS, DATA_POLICY_EXPRESSION_MAX_LEAVES, dataPolicyExpressionToCnf, } from './data-policy-expression.js';
9
10
  // The compiler owns the application source contract. Devkit and every adapter
10
11
  // re-export this module instead of maintaining separate configuration models.
11
12
  import { validateWorkflowBinding, validateWorkflowDefinition, } from '../internal/workflow.js';
@@ -412,6 +413,55 @@ function object(value) {
412
413
  function string(value) {
413
414
  return typeof value === 'string' ? value.trim() : '';
414
415
  }
416
+ function policyRuleNodes(policy, policyPath, diagnostics) {
417
+ const baseRules = (Array.isArray(policy.rules) ? policy.rules : []).map((rule, index) => ({
418
+ rule: object(rule),
419
+ path: `${policyPath}.rules[${index}]`,
420
+ }));
421
+ if (policy.readExpression === undefined)
422
+ return baseRules;
423
+ const leaves = [];
424
+ let structuralError = false;
425
+ const visit = (value, path, depth) => {
426
+ if (depth > DATA_POLICY_EXPRESSION_MAX_DEPTH) {
427
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_DEPTH_EXCEEDED', `策略表达式深度不能超过 ${DATA_POLICY_EXPRESSION_MAX_DEPTH}`, path));
428
+ structuralError = true;
429
+ return;
430
+ }
431
+ const node = object(value);
432
+ const groupKeys = ['allOf', 'anyOf'].filter(key => key in node);
433
+ if (groupKeys.length === 0) {
434
+ leaves.push({ rule: node, path });
435
+ return;
436
+ }
437
+ const key = groupKeys[0];
438
+ const children = Array.isArray(node[key]) ? node[key] : [];
439
+ if (groupKeys.length !== 1 ||
440
+ Object.keys(node).length !== 1 ||
441
+ !Array.isArray(node[key]) ||
442
+ children.length < 1 ||
443
+ children.length > DATA_POLICY_EXPRESSION_MAX_GROUP_ITEMS) {
444
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_GROUP_INVALID', `表达式节点只能声明一个 allOf/anyOf,且包含 1 到 ${DATA_POLICY_EXPRESSION_MAX_GROUP_ITEMS} 个子项`, path));
445
+ structuralError = true;
446
+ return;
447
+ }
448
+ children.forEach((child, index) => visit(child, `${path}.${key}[${index}]`, depth + 1));
449
+ };
450
+ visit(policy.readExpression, `${policyPath}.readExpression`, 1);
451
+ if (leaves.length > DATA_POLICY_EXPRESSION_MAX_LEAVES) {
452
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_LEAVES_EXCEEDED', `策略表达式最多包含 ${DATA_POLICY_EXPRESSION_MAX_LEAVES} 个规则叶子`, `${policyPath}.readExpression`));
453
+ structuralError = true;
454
+ }
455
+ if (!structuralError && leaves.length > 0) {
456
+ try {
457
+ dataPolicyExpressionToCnf(policy.readExpression);
458
+ }
459
+ catch {
460
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_EXPANSION_EXCEEDED', '策略表达式规范化后超过 50 个 CNF 子句或 100 个规则实例', `${policyPath}.readExpression`));
461
+ }
462
+ }
463
+ return [...baseRules, ...leaves];
464
+ }
415
465
  function eventSchemaDeclaresPath(schema, path) {
416
466
  let current = schema;
417
467
  for (const segment of path.split('.')) {
@@ -1034,14 +1084,49 @@ export function validateAppConfig(value) {
1034
1084
  const policyCodes = new Set();
1035
1085
  policies.forEach((raw, index) => {
1036
1086
  const policy = object(raw);
1087
+ const policyPath = `authz.dataPolicies[${index}]`;
1088
+ const unknownPolicyKeys = Object.keys(policy).filter(key => ![
1089
+ 'code',
1090
+ 'name',
1091
+ 'resourceCode',
1092
+ 'unrestrictedRoleCodes',
1093
+ 'operations',
1094
+ 'matchMode',
1095
+ 'rules',
1096
+ 'readExpression',
1097
+ 'writeBoundary',
1098
+ ].includes(key));
1099
+ if (unknownPolicyKeys.length > 0) {
1100
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_KEYS_INVALID', 'data policy 包含未声明字段', policyPath));
1101
+ }
1037
1102
  const code = string(policy.code);
1038
1103
  const rules = Array.isArray(policy.rules) ? policy.rules : [];
1104
+ const hasReadExpression = policy.readExpression !== undefined;
1105
+ const operations = Array.isArray(policy.operations)
1106
+ ? policy.operations.map(string)
1107
+ : [];
1108
+ const operationsValid = policy.operations === undefined ||
1109
+ (Array.isArray(policy.operations) &&
1110
+ operations.length > 0 &&
1111
+ operations.length <= 4 &&
1112
+ new Set(operations).size === operations.length &&
1113
+ operations.every(operation => ['read', 'create', 'update', 'delete'].includes(operation)));
1114
+ const capabilityOnlyBoundary = policy.writeBoundary === 'capability_only' &&
1115
+ policy.matchMode === 'AND' &&
1116
+ rules.length === 0 &&
1117
+ hasReadExpression;
1118
+ const scopedBoundary = rules.length > 0 && policy.writeBoundary === undefined;
1119
+ const validPolicyForm = ['AND', 'OR'].includes(string(policy.matchMode)) &&
1120
+ (scopedBoundary || capabilityOnlyBoundary) &&
1121
+ (!hasReadExpression || policy.operations === undefined);
1039
1122
  if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
1040
1123
  policyCodes.has(code) ||
1041
1124
  !string(policy.name) ||
1042
- !['AND', 'OR'].includes(string(policy.matchMode)) ||
1043
- rules.length === 0) {
1044
- diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_INVALID', 'data policy 必须声明唯一 code、name、matchMode 和至少一条规则', `authz.dataPolicies[${index}]`));
1125
+ !validPolicyForm) {
1126
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_INVALID', 'data policy 必须声明基础 matchMode/rules;readExpression 只叠加到读取,空基础规则必须显式声明 writeBoundary=capability_only', policyPath));
1127
+ }
1128
+ if (!operationsValid) {
1129
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_OPERATIONS_INVALID', 'operations 必须是不重复的 read/create/update/delete 非空数组', `${policyPath}.operations`));
1045
1130
  }
1046
1131
  policyCodes.add(code);
1047
1132
  if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(string(policy.resourceCode))) {
@@ -1064,20 +1149,71 @@ export function validateAppConfig(value) {
1064
1149
  seenUnrestrictedRoleCodes.add(roleCode);
1065
1150
  });
1066
1151
  }
1067
- rules.forEach((rawRule, ruleIndex) => {
1068
- const rule = object(rawRule);
1069
- const rulePath = `authz.dataPolicies[${index}].rules[${ruleIndex}]`;
1152
+ const policyFields = resourceFieldTypes.get(string(policy.resourceCode));
1153
+ policyRuleNodes(policy, policyPath, diagnostics).forEach(({ rule, path: rulePath }) => {
1154
+ const unknownRuleKeys = Object.keys(rule).filter(key => ![
1155
+ 'subject',
1156
+ 'dimensionCode',
1157
+ 'relationCode',
1158
+ 'resourceCode',
1159
+ 'field',
1160
+ 'roleCodes',
1161
+ 'operation',
1162
+ 'valuePath',
1163
+ 'emptyMatchesAll',
1164
+ 'operator',
1165
+ 'value',
1166
+ 'operand',
1167
+ ].includes(key));
1168
+ if (unknownRuleKeys.length > 0) {
1169
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RULE_KEYS_INVALID', 'data policy 规则包含未声明字段', rulePath));
1170
+ }
1070
1171
  const subject = string(rule.subject);
1071
1172
  const dimensionCode = string(rule.dimensionCode);
1072
1173
  const relationCode = string(rule.relationCode);
1073
- const modes = [subject, dimensionCode, relationCode].filter(Boolean);
1174
+ const operator = string(rule.operator);
1175
+ const dbNow = rule.operand === 'db_now';
1176
+ const nullPredicate = ['is_null', 'is_not_null'].includes(operator);
1177
+ const constantPredicate = ['eq', 'not_eq', 'in', 'not_in'].includes(operator);
1178
+ const modes = [
1179
+ subject,
1180
+ dimensionCode,
1181
+ relationCode,
1182
+ dbNow ? 'db_now' : '',
1183
+ constantPredicate || nullPredicate ? 'constant' : '',
1184
+ ].filter(Boolean);
1074
1185
  const validCurrentUser = subject === 'current_user';
1075
1186
  const validDimension = Boolean(dimensionCode) && dimensionCodes.has(dimensionCode);
1076
1187
  const validRelationship = Boolean(relationCode) && Boolean(string(rule.resourceCode));
1188
+ const validConstant = constantPredicate &&
1189
+ (['in', 'not_in'].includes(operator)
1190
+ ? Array.isArray(rule.value) &&
1191
+ rule.value.length > 0 &&
1192
+ rule.value.length <= 100 &&
1193
+ rule.value.every(value => typeof value === 'string' && value.length <= 2048) &&
1194
+ new Set(rule.value).size === rule.value.length
1195
+ : typeof rule.value === 'string' &&
1196
+ rule.value.length > 0 &&
1197
+ rule.value.length <= 2048);
1198
+ const validNull = nullPredicate &&
1199
+ rule.value === undefined &&
1200
+ rule.operand === undefined;
1201
+ const validDbNow = dbNow &&
1202
+ ['lt', 'lte', 'gt', 'gte'].includes(operator) &&
1203
+ rule.value === undefined &&
1204
+ policyFields?.get(string(rule.field)) === 'datetime';
1077
1205
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(string(rule.field)) ||
1078
1206
  modes.length !== 1 ||
1079
- (!validCurrentUser && !validDimension && !validRelationship)) {
1080
- diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RULE_INVALID', '策略规则必须且只能声明 current_user、已声明 dimension 或 relationship 中的一种', rulePath));
1207
+ (!validCurrentUser &&
1208
+ !validDimension &&
1209
+ !validRelationship &&
1210
+ !validConstant &&
1211
+ !validNull &&
1212
+ !validDbNow)) {
1213
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RULE_INVALID', '策略规则必须且只能声明 current_user、dimension、relationship、constant/null 或 datetime db_now 中的一种', rulePath));
1214
+ }
1215
+ if (dbNow && !validDbNow) {
1216
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_DB_NOW_FIELD_INVALID', 'db_now 只支持 datetime 字段及 lt/lte/gt/gte 操作符', `${rulePath}.field`));
1081
1217
  }
1082
1218
  if (rule.roleCodes !== undefined) {
1083
1219
  const scopedRoleCodes = Array.isArray(rule.roleCodes)
@@ -1105,8 +1241,7 @@ export function validateAppConfig(value) {
1105
1241
  return;
1106
1242
  const sourceDimensions = new Set((Array.isArray(source.grants) ? source.grants : []).map(rawGrant => string(object(rawGrant).dimensionCode)));
1107
1243
  const matchingRules = policies.flatMap(rawPolicy => {
1108
- const rules = object(rawPolicy).rules;
1109
- return (Array.isArray(rules) ? rules : []).filter(rawRule => sourceDimensions.has(string(object(rawRule).dimensionCode)));
1244
+ return policyRuleNodes(object(rawPolicy), 'authz.dataPolicies', []).map(entry => entry.rule).filter(rawRule => sourceDimensions.has(string(rawRule.dimensionCode)));
1110
1245
  });
1111
1246
  if (matchingRules.length === 0 ||
1112
1247
  matchingRules.some(rawRule => string(object(rawRule).operation) !== 'read')) {
@@ -1990,6 +2125,40 @@ export function currentUserDataPolicy(input) {
1990
2125
  ],
1991
2126
  };
1992
2127
  }
2128
+ export const dataPolicyExpression = {
2129
+ allOf: (...values) => ({ allOf: values }),
2130
+ anyOf: (...values) => ({ anyOf: values }),
2131
+ constant: (input) => ({ ...input }),
2132
+ null: (input) => ({ ...input }),
2133
+ databaseNow: (input) => ({ ...input, operand: 'db_now' }),
2134
+ currentUser: (input) => ({ ...input, subject: 'current_user' }),
2135
+ dimension: (input) => ({ ...input }),
2136
+ relation: (input) => ({ ...input }),
2137
+ };
2138
+ export function resourceReadPolicy(input) {
2139
+ const common = {
2140
+ code: input.code,
2141
+ name: input.name,
2142
+ resourceCode: input.resourceCode,
2143
+ ...(input.unrestrictedRoleCodes
2144
+ ? { unrestrictedRoleCodes: [...input.unrestrictedRoleCodes] }
2145
+ : {}),
2146
+ readExpression: input.expression,
2147
+ };
2148
+ if (input.writeBoundary === 'capability_only') {
2149
+ return {
2150
+ ...common,
2151
+ matchMode: 'AND',
2152
+ rules: [],
2153
+ writeBoundary: 'capability_only',
2154
+ };
2155
+ }
2156
+ return {
2157
+ ...common,
2158
+ matchMode: input.matchMode,
2159
+ rules: input.rules,
2160
+ };
2161
+ }
1993
2162
  export function materializeDataResource(appCode, declaration) {
1994
2163
  const capabilities = resourceCapabilityCodes(appCode, declaration.code);
1995
2164
  const fields = declaration.fields.map(field => {