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

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.
@@ -18,6 +18,112 @@ const DATE_TRIGGER_RESERVED_DATA_FIELDS = new Set([
18
18
  'dueAt',
19
19
  ]);
20
20
  const APP_DATA_FIELD_TYPES = new Set(DATA_FIELD_TYPES);
21
+ const authorizationProjectionFieldPathSchema = {
22
+ type: 'string',
23
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}(?:\\.(?:value|snapshot\\.[A-Za-z_][A-Za-z0-9_]{0,127}))?$',
24
+ };
25
+ const authorizationProjectionControlProperties = {
26
+ enabledField: {
27
+ type: 'string',
28
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}$',
29
+ },
30
+ effectiveFromField: {
31
+ type: 'string',
32
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}$',
33
+ },
34
+ effectiveToField: {
35
+ type: 'string',
36
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}$',
37
+ },
38
+ failureMode: { const: 'strict' },
39
+ };
40
+ const roleMembershipSourceInputSchema = {
41
+ type: 'object',
42
+ additionalProperties: false,
43
+ required: [
44
+ 'code',
45
+ 'name',
46
+ 'resourceCode',
47
+ 'userIdField',
48
+ 'roleCode',
49
+ 'failureMode',
50
+ ],
51
+ properties: {
52
+ code: {
53
+ type: 'string',
54
+ pattern: '^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$',
55
+ maxLength: 100,
56
+ },
57
+ name: { type: 'string', minLength: 1, maxLength: 255 },
58
+ resourceCode: { type: 'string' },
59
+ userIdField: authorizationProjectionFieldPathSchema,
60
+ roleCode: { type: 'string' },
61
+ ...authorizationProjectionControlProperties,
62
+ },
63
+ };
64
+ const relationshipGrantSourceInputSchema = {
65
+ type: 'object',
66
+ additionalProperties: false,
67
+ required: [
68
+ 'code',
69
+ 'name',
70
+ 'resourceCode',
71
+ 'subject',
72
+ 'relationCode',
73
+ 'targetResourceCode',
74
+ 'resourceIdField',
75
+ 'operations',
76
+ 'failureMode',
77
+ ],
78
+ properties: {
79
+ code: {
80
+ type: 'string',
81
+ pattern: '^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$',
82
+ maxLength: 100,
83
+ },
84
+ name: { type: 'string', minLength: 1, maxLength: 255 },
85
+ resourceCode: { type: 'string' },
86
+ subject: {
87
+ oneOf: [
88
+ {
89
+ type: 'object',
90
+ additionalProperties: false,
91
+ required: ['type', 'userIdField'],
92
+ properties: {
93
+ type: { const: 'user' },
94
+ userIdField: authorizationProjectionFieldPathSchema,
95
+ },
96
+ },
97
+ {
98
+ type: 'object',
99
+ additionalProperties: false,
100
+ required: ['type', 'userIdField', 'roleCode'],
101
+ properties: {
102
+ type: { const: 'role_membership' },
103
+ userIdField: authorizationProjectionFieldPathSchema,
104
+ roleCode: { type: 'string' },
105
+ },
106
+ },
107
+ ],
108
+ },
109
+ relationCode: { type: 'string', minLength: 1, maxLength: 128 },
110
+ targetResourceCode: { type: 'string' },
111
+ resourceIdField: authorizationProjectionFieldPathSchema,
112
+ operations: {
113
+ type: 'array',
114
+ minItems: 1,
115
+ maxItems: 20,
116
+ uniqueItems: true,
117
+ items: {
118
+ type: 'string',
119
+ minLength: 1,
120
+ maxLength: 64,
121
+ pattern: '^[A-Za-z0-9][A-Za-z0-9:._*-]{0,63}$',
122
+ },
123
+ },
124
+ ...authorizationProjectionControlProperties,
125
+ },
126
+ };
21
127
  export const openXiangdaAppConfigSchema = {
22
128
  $id: 'openxiangda.app-config/v3',
23
129
  type: 'object',
@@ -172,6 +278,16 @@ export const openXiangdaAppConfigSchema = {
172
278
  maxItems: 100,
173
279
  items: { type: 'object' },
174
280
  },
281
+ roleMembershipSources: {
282
+ type: 'array',
283
+ maxItems: 100,
284
+ items: roleMembershipSourceInputSchema,
285
+ },
286
+ relationshipGrantSources: {
287
+ type: 'array',
288
+ maxItems: 100,
289
+ items: relationshipGrantSourceInputSchema,
290
+ },
175
291
  dataPolicies: {
176
292
  type: 'array',
177
293
  maxItems: 100,
@@ -637,6 +753,8 @@ export function validateAppConfig(value) {
637
753
  for (const [name, values] of [
638
754
  ['scopeDimensions', authz.scopeDimensions],
639
755
  ['scopeSources', authz.scopeSources],
756
+ ['roleMembershipSources', authz.roleMembershipSources],
757
+ ['relationshipGrantSources', authz.relationshipGrantSources],
640
758
  ['dataPolicies', authz.dataPolicies],
641
759
  ]) {
642
760
  if (values !== undefined && !Array.isArray(values)) {
@@ -660,12 +778,25 @@ export function validateAppConfig(value) {
660
778
  resourceCode,
661
779
  new Set(fields.keys()),
662
780
  ]));
781
+ const resourceFieldDeclarations = new Map(dataResources.map(rawResource => {
782
+ const resource = object(rawResource);
783
+ const fields = Array.isArray(object(resource.schema).fields)
784
+ ? object(resource.schema).fields
785
+ : [];
786
+ return [
787
+ string(resource.code),
788
+ new Map(fields.map(rawField => {
789
+ const field = object(rawField);
790
+ return [string(field.code), field];
791
+ })),
792
+ ];
793
+ }));
663
794
  const dimensionCodes = new Set();
664
795
  dimensions.forEach((raw, index) => {
665
796
  const dimension = object(raw);
666
797
  const code = string(dimension.code);
667
798
  const dimensionPath = `authz.scopeDimensions[${index}]`;
668
- if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
799
+ if (!/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(code) ||
669
800
  dimensionCodes.has(code) ||
670
801
  !string(dimension.name) ||
671
802
  (dimension.valueType !== undefined &&
@@ -724,10 +855,10 @@ export function validateAppConfig(value) {
724
855
  const roleCode = string(subject.roleCode);
725
856
  const resourceCode = string(source.resourceCode);
726
857
  const declaredFields = resourceFields.get(resourceCode);
727
- if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
858
+ if (!/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(code) ||
728
859
  sourceCodes.has(code) ||
729
860
  !string(source.name) ||
730
- !/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(resourceCode) ||
861
+ !/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(resourceCode) ||
731
862
  !['user', 'role_membership'].includes(subjectType) ||
732
863
  !SEMANTIC_FIELD_PATH_PATTERN.test(string(subject.userIdField)) ||
733
864
  (subjectType === 'role_membership' && !roleCodes.has(roleCode)) ||
@@ -786,6 +917,120 @@ export function validateAppConfig(value) {
786
917
  }
787
918
  sourceCodes.add(code);
788
919
  });
920
+ const projectionSourceCodes = new Set();
921
+ const roleMembershipSources = Array.isArray(authz.roleMembershipSources)
922
+ ? authz.roleMembershipSources
923
+ : [];
924
+ const relationshipGrantSources = Array.isArray(authz.relationshipGrantSources)
925
+ ? authz.relationshipGrantSources
926
+ : [];
927
+ const projectionUserPathValid = (path, fields) => {
928
+ const root = semanticFieldPathRoot(path);
929
+ return (SEMANTIC_FIELD_PATH_PATTERN.test(path) &&
930
+ path === `${root}.value` &&
931
+ string(fields?.get(root)?.type) === 'user.single');
932
+ };
933
+ const projectionControlFieldsValid = (source, fields) => source.failureMode === 'strict' &&
934
+ (source.enabledField === undefined ||
935
+ string(fields?.get(string(source.enabledField))?.type) === 'boolean') &&
936
+ (source.effectiveFromField === undefined ||
937
+ ['date', 'datetime'].includes(string(fields?.get(string(source.effectiveFromField))?.type))) &&
938
+ (source.effectiveToField === undefined ||
939
+ ['date', 'datetime'].includes(string(fields?.get(string(source.effectiveToField))?.type)));
940
+ roleMembershipSources.forEach((raw, index) => {
941
+ const source = object(raw);
942
+ const sourcePath = `authz.roleMembershipSources[${index}]`;
943
+ const unknownKeys = Object.keys(source).filter(key => ![
944
+ 'code',
945
+ 'name',
946
+ 'resourceCode',
947
+ 'userIdField',
948
+ 'roleCode',
949
+ 'enabledField',
950
+ 'effectiveFromField',
951
+ 'effectiveToField',
952
+ 'failureMode',
953
+ ].includes(key));
954
+ if (unknownKeys.length > 0) {
955
+ diagnostics.push(diagnostic('APP_CONFIG_SCHEMA_INVALID', `role membership source 包含未知属性 ${unknownKeys.join(', ')}`, sourcePath));
956
+ }
957
+ const code = string(source.code);
958
+ const resourceCode = string(source.resourceCode);
959
+ const fields = resourceFieldDeclarations.get(resourceCode);
960
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
961
+ code.length > 100 ||
962
+ projectionSourceCodes.has(code) ||
963
+ !string(source.name) ||
964
+ !fields ||
965
+ !projectionUserPathValid(string(source.userIdField), fields) ||
966
+ !roleCodes.has(string(source.roleCode)) ||
967
+ !projectionControlFieldsValid(source, fields)) {
968
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_MEMBERSHIP_SOURCE_INVALID', 'role membership source 必须声明唯一来源、user.single.value、已声明 package role 与严格控制字段', sourcePath));
969
+ }
970
+ projectionSourceCodes.add(code);
971
+ });
972
+ relationshipGrantSources.forEach((raw, index) => {
973
+ const source = object(raw);
974
+ const sourcePath = `authz.relationshipGrantSources[${index}]`;
975
+ const unknownKeys = Object.keys(source).filter(key => ![
976
+ 'code',
977
+ 'name',
978
+ 'resourceCode',
979
+ 'subject',
980
+ 'relationCode',
981
+ 'targetResourceCode',
982
+ 'resourceIdField',
983
+ 'operations',
984
+ 'enabledField',
985
+ 'effectiveFromField',
986
+ 'effectiveToField',
987
+ 'failureMode',
988
+ ].includes(key));
989
+ if (unknownKeys.length > 0) {
990
+ diagnostics.push(diagnostic('APP_CONFIG_SCHEMA_INVALID', `relationship grant source 包含未知属性 ${unknownKeys.join(', ')}`, sourcePath));
991
+ }
992
+ const code = string(source.code);
993
+ const resourceCode = string(source.resourceCode);
994
+ const targetResourceCode = string(source.targetResourceCode);
995
+ const fields = resourceFieldDeclarations.get(resourceCode);
996
+ const subject = object(source.subject);
997
+ const subjectType = string(subject.type);
998
+ const subjectUnknownKeys = Object.keys(subject).filter(key => !['type', 'userIdField', 'roleCode'].includes(key));
999
+ if (subjectUnknownKeys.length > 0) {
1000
+ diagnostics.push(diagnostic('APP_CONFIG_SCHEMA_INVALID', `relationship grant subject 包含未知属性 ${subjectUnknownKeys.join(', ')}`, `${sourcePath}.subject`));
1001
+ }
1002
+ const resourceIdPath = string(source.resourceIdField);
1003
+ const resourceIdRoot = semanticFieldPathRoot(resourceIdPath);
1004
+ const resourceIdField = fields?.get(resourceIdRoot);
1005
+ const resourceReference = object(resourceIdField?.source);
1006
+ const resourceIdValid = (resourceIdPath === 'id' && targetResourceCode === resourceCode) ||
1007
+ (resourceIdPath === `${resourceIdRoot}.value` &&
1008
+ string(resourceIdField?.type) === 'resource-ref.single' &&
1009
+ string(resourceReference.resourceCode) === targetResourceCode);
1010
+ const operations = Array.isArray(source.operations)
1011
+ ? source.operations.map(string)
1012
+ : [];
1013
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
1014
+ code.length > 100 ||
1015
+ projectionSourceCodes.has(code) ||
1016
+ !string(source.name) ||
1017
+ !fields ||
1018
+ !resourceFieldDeclarations.has(targetResourceCode) ||
1019
+ !['user', 'role_membership'].includes(subjectType) ||
1020
+ !projectionUserPathValid(string(subject.userIdField), fields) ||
1021
+ (subjectType === 'role_membership' &&
1022
+ !roleCodes.has(string(subject.roleCode))) ||
1023
+ !/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(string(source.relationCode)) ||
1024
+ !resourceIdValid ||
1025
+ operations.length === 0 ||
1026
+ operations.length > 20 ||
1027
+ new Set(operations).size !== operations.length ||
1028
+ operations.some(operation => !/^[A-Za-z0-9][A-Za-z0-9:._*-]{0,63}$/.test(operation)) ||
1029
+ !projectionControlFieldsValid(source, fields)) {
1030
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_RELATIONSHIP_GRANT_SOURCE_INVALID', 'relationship grant source 必须声明唯一来源、有效主体/目标关系、常量操作与严格控制字段', sourcePath));
1031
+ }
1032
+ projectionSourceCodes.add(code);
1033
+ });
789
1034
  const policyCodes = new Set();
790
1035
  policies.forEach((raw, index) => {
791
1036
  const policy = object(raw);