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.
- package/dist/compiler/bundle.d.ts.map +1 -1
- package/dist/compiler/bundle.js +73 -12
- package/dist/compiler/bundle.js.map +1 -1
- package/dist/compiler/config.d.ts +56 -1
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +180 -11
- package/dist/compiler/config.js.map +1 -1
- package/dist/compiler/data-policy-expression.d.ts +12 -0
- package/dist/compiler/data-policy-expression.d.ts.map +1 -0
- package/dist/compiler/data-policy-expression.js +43 -0
- package/dist/compiler/data-policy-expression.js.map +1 -0
- package/dist/compiler/field-policy-path.d.ts.map +1 -1
- package/dist/compiler/field-policy-path.js +23 -4
- package/dist/compiler/field-policy-path.js.map +1 -1
- package/dist/workspace-loader.js +1 -1
- package/dist/workspace-loader.js.map +1 -1
- package/package.json +2 -2
package/dist/compiler/config.js
CHANGED
|
@@ -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
|
-
!
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
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
|
|
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 &&
|
|
1080
|
-
|
|
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
|
-
|
|
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 => {
|