isc-transforms-mcp 1.0.7 → 1.0.9
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "sailpoint.isc.transforms.accountAttribute.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - accountAttribute",
|
|
5
|
-
"description": "Strict schema derived from the SailPoint official Account Attribute operation documentation.",
|
|
5
|
+
"description": "Strict schema derived from the SailPoint official Account Attribute operation documentation. Retrieves a specific attribute value from an account associated with a source on an identity.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -12,15 +12,18 @@
|
|
|
12
12
|
],
|
|
13
13
|
"properties": {
|
|
14
14
|
"type": {
|
|
15
|
-
"const": "accountAttribute"
|
|
15
|
+
"const": "accountAttribute",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'accountAttribute'."
|
|
16
17
|
},
|
|
17
18
|
"name": {
|
|
18
19
|
"type": "string",
|
|
19
|
-
"minLength": 1
|
|
20
|
+
"minLength": 1,
|
|
21
|
+
"description": "Display name for this transform, shown in UI dropdowns and identity profile mappings."
|
|
20
22
|
},
|
|
21
23
|
"requiresPeriodicRefresh": {
|
|
22
24
|
"type": "boolean",
|
|
23
|
-
"default": false
|
|
25
|
+
"default": false,
|
|
26
|
+
"description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Set to true when the source account data changes frequently. Default is false."
|
|
24
27
|
},
|
|
25
28
|
"attributes": {
|
|
26
29
|
"type": "object",
|
|
@@ -32,113 +35,95 @@
|
|
|
32
35
|
"sourceName": {
|
|
33
36
|
"type": "string",
|
|
34
37
|
"minLength": 1,
|
|
35
|
-
"description": "Source display name to search (e.g., 'Active Directory').
|
|
38
|
+
"description": "Source display name to search (e.g., 'Active Directory'). WARNING: references the mutable display name — if the source is renamed, this value must be updated. For stability, prefer applicationName. Mutually exclusive with applicationId and applicationName."
|
|
36
39
|
},
|
|
37
40
|
"applicationId": {
|
|
38
41
|
"type": "string",
|
|
39
42
|
"minLength": 1,
|
|
40
|
-
"description": "
|
|
43
|
+
"description": "External GUID of the source. Stable alternative to sourceName that does not break if the source is renamed. Mutually exclusive with sourceName and applicationName."
|
|
41
44
|
},
|
|
42
45
|
"applicationName": {
|
|
43
46
|
"type": "string",
|
|
44
47
|
"minLength": 1,
|
|
45
|
-
"description": "
|
|
48
|
+
"description": "Immutable internal source name. Most stable source reference — does not change when the source display name changes. Mutually exclusive with sourceName and applicationId."
|
|
46
49
|
},
|
|
47
50
|
"attributeName": {
|
|
48
51
|
"type": "string",
|
|
49
52
|
"minLength": 1,
|
|
50
|
-
"description": "
|
|
53
|
+
"description": "Name of the account attribute to retrieve. Must exactly match the attribute name visible in the source schema / account attribute list in the ISC UI."
|
|
51
54
|
},
|
|
52
55
|
"accountSortAttribute": {
|
|
53
56
|
"type": "string",
|
|
54
57
|
"minLength": 1,
|
|
55
58
|
"default": "created",
|
|
56
|
-
"description": "
|
|
59
|
+
"description": "Account schema attribute used to sort multiple accounts before selecting one. Default is 'created' (ascending — oldest account wins). Must be a valid account schema attribute name."
|
|
57
60
|
},
|
|
58
61
|
"accountSortDescending": {
|
|
59
62
|
"type": "boolean",
|
|
60
63
|
"default": false,
|
|
61
|
-
"description": "
|
|
64
|
+
"description": "Controls sort order when multiple accounts exist. false = ascending (oldest first, default). true = descending (newest first)."
|
|
62
65
|
},
|
|
63
66
|
"accountReturnFirstLink": {
|
|
64
67
|
"type": "boolean",
|
|
65
68
|
"default": false,
|
|
66
|
-
"description": "
|
|
69
|
+
"description": "Controls null handling when multiple accounts exist. false = skip accounts with null values and return the first non-null (default). true = return the first sorted account's value even if it is null."
|
|
67
70
|
},
|
|
68
71
|
"accountFilter": {
|
|
69
72
|
"type": "string",
|
|
70
73
|
"minLength": 1,
|
|
71
|
-
"description": "Database filter
|
|
74
|
+
"description": "Database-level filter using sailpoint.object.Filter syntax. Applied before account retrieval. IMPORTANT: only the following fields are searchable at database level — nativeIdentity, displayName, entitlements. For other attributes (e.g. custom fields, status) use accountPropertyFilter. Example: !(nativeIdentity.startsWith(\"*DELETED*\"))"
|
|
72
75
|
},
|
|
73
76
|
"accountPropertyFilter": {
|
|
74
77
|
"type": "string",
|
|
75
78
|
"minLength": 1,
|
|
76
|
-
"description": "In-memory filter
|
|
79
|
+
"description": "In-memory filter using sailpoint.object.Filter syntax. Applied after account retrieval. All account attributes are available for filtering (not limited to searchable fields). Example: (status != \"terminated\")"
|
|
77
80
|
},
|
|
78
81
|
"input": {
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
+
"description": "Explicit input data to override the implicit source+attribute mapping. Can be a nested transform object or a static string value.",
|
|
83
|
+
"oneOf": [
|
|
84
|
+
{
|
|
85
|
+
"type": "string"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "object",
|
|
89
|
+
"required": ["type"],
|
|
90
|
+
"properties": {
|
|
91
|
+
"type": { "type": "string", "minLength": 1 },
|
|
92
|
+
"attributes": { "type": "object" }
|
|
93
|
+
},
|
|
94
|
+
"additionalProperties": true
|
|
95
|
+
}
|
|
96
|
+
]
|
|
82
97
|
}
|
|
83
98
|
},
|
|
84
99
|
"allOf": [
|
|
85
100
|
{
|
|
86
|
-
"description": "Exactly one source reference must be provided: sourceName OR applicationId OR applicationName.",
|
|
101
|
+
"description": "Exactly one source reference must be provided: sourceName OR applicationId OR applicationName. Using more than one is not allowed.",
|
|
87
102
|
"oneOf": [
|
|
88
103
|
{
|
|
89
|
-
"required": [
|
|
90
|
-
"sourceName"
|
|
91
|
-
],
|
|
104
|
+
"required": ["sourceName"],
|
|
92
105
|
"not": {
|
|
93
106
|
"anyOf": [
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
"applicationId"
|
|
97
|
-
]
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"required": [
|
|
101
|
-
"applicationName"
|
|
102
|
-
]
|
|
103
|
-
}
|
|
107
|
+
{ "required": ["applicationId"] },
|
|
108
|
+
{ "required": ["applicationName"] }
|
|
104
109
|
]
|
|
105
110
|
}
|
|
106
111
|
},
|
|
107
112
|
{
|
|
108
|
-
"required": [
|
|
109
|
-
"applicationId"
|
|
110
|
-
],
|
|
113
|
+
"required": ["applicationId"],
|
|
111
114
|
"not": {
|
|
112
115
|
"anyOf": [
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
"sourceName"
|
|
116
|
-
]
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
"required": [
|
|
120
|
-
"applicationName"
|
|
121
|
-
]
|
|
122
|
-
}
|
|
116
|
+
{ "required": ["sourceName"] },
|
|
117
|
+
{ "required": ["applicationName"] }
|
|
123
118
|
]
|
|
124
119
|
}
|
|
125
120
|
},
|
|
126
121
|
{
|
|
127
|
-
"required": [
|
|
128
|
-
"applicationName"
|
|
129
|
-
],
|
|
122
|
+
"required": ["applicationName"],
|
|
130
123
|
"not": {
|
|
131
124
|
"anyOf": [
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
"sourceName"
|
|
135
|
-
]
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
"required": [
|
|
139
|
-
"applicationId"
|
|
140
|
-
]
|
|
141
|
-
}
|
|
125
|
+
{ "required": ["sourceName"] },
|
|
126
|
+
{ "required": ["applicationId"] }
|
|
142
127
|
]
|
|
143
128
|
}
|
|
144
129
|
}
|
|
@@ -149,16 +134,34 @@
|
|
|
149
134
|
},
|
|
150
135
|
"examples": [
|
|
151
136
|
{
|
|
137
|
+
"name": "Get hire date from HR source",
|
|
152
138
|
"type": "accountAttribute",
|
|
153
|
-
"name": "Account Attribute Transform",
|
|
154
139
|
"attributes": {
|
|
155
140
|
"sourceName": "Corporate HR",
|
|
156
|
-
"attributeName": "HIREDATE"
|
|
141
|
+
"attributeName": "HIREDATE"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "Get status from newest non-service account",
|
|
146
|
+
"type": "accountAttribute",
|
|
147
|
+
"requiresPeriodicRefresh": true,
|
|
148
|
+
"attributes": {
|
|
149
|
+
"applicationName": "corp-active-directory",
|
|
150
|
+
"attributeName": "employeeStatus",
|
|
157
151
|
"accountSortAttribute": "created",
|
|
158
152
|
"accountSortDescending": true,
|
|
159
|
-
"accountReturnFirstLink":
|
|
153
|
+
"accountReturnFirstLink": false,
|
|
154
|
+
"accountFilter": "!(nativeIdentity.startsWith(\"*DELETED*\"))",
|
|
160
155
|
"accountPropertyFilter": "(WORKER_STATUS__c == \"active\")"
|
|
161
156
|
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "Get department using immutable source ID",
|
|
160
|
+
"type": "accountAttribute",
|
|
161
|
+
"attributes": {
|
|
162
|
+
"applicationId": "2c91808b7cda982781e0a6b92e0f01a9",
|
|
163
|
+
"attributeName": "department"
|
|
164
|
+
}
|
|
162
165
|
}
|
|
163
166
|
]
|
|
164
167
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "sailpoint.isc.transforms.conditional.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - conditional",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC Conditional transform. Evaluates a 'ValueA eq ValueB' expression and returns positiveCondition (true) or negativeCondition (false). Only 'eq' is supported — other operators throw IllegalArgumentException at runtime. Comparisons are case-sensitive. Operands cannot be null at runtime.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -12,31 +12,22 @@
|
|
|
12
12
|
],
|
|
13
13
|
"properties": {
|
|
14
14
|
"type": {
|
|
15
|
-
"const": "conditional"
|
|
15
|
+
"const": "conditional",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'conditional'."
|
|
16
17
|
},
|
|
17
18
|
"name": {
|
|
18
19
|
"type": "string",
|
|
19
|
-
"minLength": 1
|
|
20
|
+
"minLength": 1,
|
|
21
|
+
"description": "Display name for this transform, shown in UI dropdowns and identity profile mappings."
|
|
20
22
|
},
|
|
21
23
|
"requiresPeriodicRefresh": {
|
|
22
24
|
"type": "boolean",
|
|
23
25
|
"default": false,
|
|
24
|
-
"description": "
|
|
26
|
+
"description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Default is false."
|
|
25
27
|
},
|
|
26
28
|
"attributes": {
|
|
27
29
|
"type": "object",
|
|
28
|
-
"description": "Conditional logic configuration,
|
|
29
|
-
"additionalProperties": {
|
|
30
|
-
"description": "Dynamic variables referenced via $variableName. Can be string literals or nested transforms.",
|
|
31
|
-
"anyOf": [
|
|
32
|
-
{
|
|
33
|
-
"type": "string"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"$ref": "#/$defs/NestedTransform"
|
|
37
|
-
}
|
|
38
|
-
]
|
|
39
|
-
},
|
|
30
|
+
"description": "Conditional logic configuration. Required: expression, positiveCondition, negativeCondition. Optional: dynamic variable keys (any additional key) whose values are static strings or nested transforms, referenced via $variableName in the expression and conditions.",
|
|
40
31
|
"required": [
|
|
41
32
|
"expression",
|
|
42
33
|
"positiveCondition",
|
|
@@ -46,26 +37,36 @@
|
|
|
46
37
|
"expression": {
|
|
47
38
|
"type": "string",
|
|
48
39
|
"minLength": 1,
|
|
49
|
-
"
|
|
50
|
-
"
|
|
40
|
+
"pattern": "^.+\\s+eq\\s+.+$",
|
|
41
|
+
"description": "Equality expression of the form '<ValueA> eq <ValueB>'. RULES: (1) Only 'eq' operator is supported — !=, ==, >, <, ne, gt, lt etc. throw IllegalArgumentException. (2) Comparisons are case-sensitive: 'Engineering' != 'engineering'. (3) Operands cannot be null at runtime. (4) Variables are referenced with $variableName syntax and must be declared as keys in attributes."
|
|
51
42
|
},
|
|
52
43
|
"positiveCondition": {
|
|
53
44
|
"type": "string",
|
|
54
|
-
"
|
|
55
|
-
"description": "Output when expression evaluates to true. May reference a dynamic variable via $var."
|
|
45
|
+
"description": "Value returned when expression evaluates to true. Can be a static string (e.g., 'Active') or a $variableName reference to a declared variable in attributes. Can be empty string to return a blank value."
|
|
56
46
|
},
|
|
57
47
|
"negativeCondition": {
|
|
58
48
|
"type": "string",
|
|
59
|
-
"
|
|
60
|
-
"description": "Output when expression evaluates to false. May reference a dynamic variable via $var."
|
|
49
|
+
"description": "Value returned when expression evaluates to false. Can be a static string (e.g., 'Inactive') or a $variableName reference to a declared variable in attributes. Can be empty string to return a blank value."
|
|
61
50
|
}
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": {
|
|
53
|
+
"description": "Dynamic variable declaration. Key becomes the variable name, referenced as $keyName in expression, positiveCondition, or negativeCondition. Value must be a static string or a nested transform object.",
|
|
54
|
+
"anyOf": [
|
|
55
|
+
{
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Static string value for this variable."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"$ref": "#/$defs/NestedTransform"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
},
|
|
65
66
|
"$defs": {
|
|
66
67
|
"NestedTransform": {
|
|
67
68
|
"type": "object",
|
|
68
|
-
"description": "Nested transform object used as a dynamic variable.
|
|
69
|
+
"description": "Nested transform object used as a dynamic variable. The 'name' field is optional in nested transforms per SailPoint docs examples.",
|
|
69
70
|
"additionalProperties": false,
|
|
70
71
|
"required": [
|
|
71
72
|
"type",
|
|
@@ -73,18 +74,22 @@
|
|
|
73
74
|
],
|
|
74
75
|
"properties": {
|
|
75
76
|
"id": {
|
|
76
|
-
"type": "string"
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "Optional transform ID when referencing an existing saved transform."
|
|
77
79
|
},
|
|
78
80
|
"name": {
|
|
79
81
|
"type": "string",
|
|
80
|
-
"minLength": 1
|
|
82
|
+
"minLength": 1,
|
|
83
|
+
"description": "Optional display name for the nested transform."
|
|
81
84
|
},
|
|
82
85
|
"type": {
|
|
83
86
|
"type": "string",
|
|
84
|
-
"minLength": 1
|
|
87
|
+
"minLength": 1,
|
|
88
|
+
"description": "The transform operation type (e.g., 'accountAttribute', 'static', 'dateFormat')."
|
|
85
89
|
},
|
|
86
90
|
"requiresPeriodicRefresh": {
|
|
87
|
-
"type": "boolean"
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
88
93
|
},
|
|
89
94
|
"attributes": {
|
|
90
95
|
"type": "object",
|
|
@@ -97,7 +102,7 @@
|
|
|
97
102
|
"examples": [
|
|
98
103
|
{
|
|
99
104
|
"type": "conditional",
|
|
100
|
-
"name": "
|
|
105
|
+
"name": "Department Science Check",
|
|
101
106
|
"attributes": {
|
|
102
107
|
"expression": "$department eq Science",
|
|
103
108
|
"positiveCondition": "true",
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"type": "conditional",
|
|
116
|
-
"name": "
|
|
121
|
+
"name": "Assign Building by Department",
|
|
117
122
|
"attributes": {
|
|
118
123
|
"expression": "$department eq Science",
|
|
119
124
|
"positiveCondition": "$scienceBuilding",
|
|
@@ -138,6 +143,25 @@
|
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"type": "conditional",
|
|
149
|
+
"name": "Active Employee Flag",
|
|
150
|
+
"requiresPeriodicRefresh": true,
|
|
151
|
+
"attributes": {
|
|
152
|
+
"expression": "$workerStatus eq active",
|
|
153
|
+
"positiveCondition": "true",
|
|
154
|
+
"negativeCondition": "false",
|
|
155
|
+
"workerStatus": {
|
|
156
|
+
"type": "accountAttribute",
|
|
157
|
+
"attributes": {
|
|
158
|
+
"applicationName": "corp-hr-system",
|
|
159
|
+
"attributeName": "WORKER_STATUS__c",
|
|
160
|
+
"accountSortAttribute": "created",
|
|
161
|
+
"accountSortDescending": true
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
141
165
|
}
|
|
142
166
|
]
|
|
143
167
|
}
|
package/dist/transforms/lint.js
CHANGED
|
@@ -176,29 +176,85 @@ function lintRuleBackedInvariants(requestedType, normalized, msgs) {
|
|
|
176
176
|
// ---------------------------------------------------------------------------
|
|
177
177
|
function lintAccountAttribute(attrs) {
|
|
178
178
|
const msgs = [];
|
|
179
|
+
// --- 1. Source reference: exactly one of sourceName / applicationId / applicationName ---
|
|
179
180
|
const sourceFields = ["sourceName", "applicationId", "applicationName"];
|
|
180
181
|
const presentSources = sourceFields.filter((f) => attrs?.[f] !== undefined && attrs?.[f] !== null && String(attrs[f]).trim() !== "");
|
|
181
182
|
if (presentSources.length === 0) {
|
|
182
|
-
push(msgs, "error", "accountAttribute requires exactly one source reference: sourceName
|
|
183
|
+
push(msgs, "error", "accountAttribute requires exactly one source reference: sourceName (display name), " +
|
|
184
|
+
"applicationId (external GUID), or applicationName (immutable internal name).", "attributes");
|
|
183
185
|
}
|
|
184
186
|
else if (presentSources.length > 1) {
|
|
185
|
-
push(msgs, "error", `accountAttribute must have exactly ONE source reference; found multiple: ${presentSources.join(", ")}.
|
|
187
|
+
push(msgs, "error", `accountAttribute must have exactly ONE source reference; found multiple: ${presentSources.join(", ")}. ` +
|
|
188
|
+
"Remove all but one. Prefer applicationName for stability (display names can change).", "attributes");
|
|
186
189
|
}
|
|
187
|
-
//
|
|
190
|
+
// --- 2. sourceName: non-empty, and warn about display-name fragility ---
|
|
191
|
+
if (attrs?.sourceName !== undefined) {
|
|
192
|
+
if (typeof attrs.sourceName !== "string" || attrs.sourceName.trim() === "") {
|
|
193
|
+
push(msgs, "error", "sourceName must be a non-empty string matching the source's display name.", "attributes.sourceName");
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
push(msgs, "warn", "sourceName references the source display name, which can change. " +
|
|
197
|
+
"If the source is renamed the transform will break. Consider using applicationName (immutable) for long-term stability.", "attributes.sourceName");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// --- 3. applicationId: non-empty string ---
|
|
201
|
+
if (attrs?.applicationId !== undefined) {
|
|
202
|
+
if (typeof attrs.applicationId !== "string" || attrs.applicationId.trim() === "") {
|
|
203
|
+
push(msgs, "error", "applicationId must be a non-empty string (external GUID of the source).", "attributes.applicationId");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// --- 4. applicationName: non-empty string ---
|
|
207
|
+
if (attrs?.applicationName !== undefined) {
|
|
208
|
+
if (typeof attrs.applicationName !== "string" || attrs.applicationName.trim() === "") {
|
|
209
|
+
push(msgs, "error", "applicationName must be a non-empty string (immutable internal source name).", "attributes.applicationName");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// --- 5. attributeName: required (caught by schema), but also check non-empty ---
|
|
213
|
+
if (attrs?.attributeName !== undefined) {
|
|
214
|
+
if (typeof attrs.attributeName !== "string" || attrs.attributeName.trim() === "") {
|
|
215
|
+
push(msgs, "error", "attributeName must be a non-empty string matching the account attribute name in the source schema.", "attributes.attributeName");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// --- 6. accountSortAttribute: non-empty string; default is 'created' ---
|
|
219
|
+
if (attrs?.accountSortAttribute !== undefined) {
|
|
220
|
+
if (typeof attrs.accountSortAttribute !== "string") {
|
|
221
|
+
push(msgs, "error", "accountSortAttribute must be a string (schema attribute name). Default is 'created'.", "attributes.accountSortAttribute");
|
|
222
|
+
}
|
|
223
|
+
else if (attrs.accountSortAttribute.trim() === "") {
|
|
224
|
+
push(msgs, "error", "accountSortAttribute must not be empty. Omit it to use the default ('created'), or provide a valid account schema attribute name.", "attributes.accountSortAttribute");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// --- 7. accountSortDescending: boolean ---
|
|
188
228
|
if (attrs?.accountSortDescending !== undefined && typeof attrs.accountSortDescending !== "boolean") {
|
|
189
|
-
push(msgs, "error", "accountSortDescending must be a boolean.", "attributes.accountSortDescending");
|
|
229
|
+
push(msgs, "error", "accountSortDescending must be a boolean (true = descending, false = ascending). Default is false.", "attributes.accountSortDescending");
|
|
190
230
|
}
|
|
231
|
+
// --- 8. accountReturnFirstLink: boolean ---
|
|
191
232
|
if (attrs?.accountReturnFirstLink !== undefined && typeof attrs.accountReturnFirstLink !== "boolean") {
|
|
192
|
-
push(msgs, "error", "accountReturnFirstLink must be a boolean."
|
|
193
|
-
|
|
194
|
-
if (attrs?.accountSortAttribute !== undefined && typeof attrs.accountSortAttribute !== "string") {
|
|
195
|
-
push(msgs, "error", "accountSortAttribute must be a string.", "attributes.accountSortAttribute");
|
|
233
|
+
push(msgs, "error", "accountReturnFirstLink must be a boolean. " +
|
|
234
|
+
"true = return the first sorted account's value even if null; false = skip nulls and return first non-null. Default is false.", "attributes.accountReturnFirstLink");
|
|
196
235
|
}
|
|
197
|
-
|
|
198
|
-
|
|
236
|
+
// --- 9. accountFilter: type, non-empty, and searchable-fields hint ---
|
|
237
|
+
if (attrs?.accountFilter !== undefined) {
|
|
238
|
+
if (typeof attrs.accountFilter !== "string") {
|
|
239
|
+
push(msgs, "error", "accountFilter must be a string (sailpoint.object.Filter expression).", "attributes.accountFilter");
|
|
240
|
+
}
|
|
241
|
+
else if (attrs.accountFilter.trim() === "") {
|
|
242
|
+
push(msgs, "error", "accountFilter must not be empty if provided. Omit it to disable database-level filtering.", "attributes.accountFilter");
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
push(msgs, "info", "accountFilter applies a database-level sailpoint.object.Filter. " +
|
|
246
|
+
"Only these fields are searchable at database level: nativeIdentity, displayName, entitlements. " +
|
|
247
|
+
"For other account attributes (e.g. custom fields, status), use accountPropertyFilter instead.", "attributes.accountFilter");
|
|
248
|
+
}
|
|
199
249
|
}
|
|
200
|
-
|
|
201
|
-
|
|
250
|
+
// --- 10. accountPropertyFilter: type and non-empty ---
|
|
251
|
+
if (attrs?.accountPropertyFilter !== undefined) {
|
|
252
|
+
if (typeof attrs.accountPropertyFilter !== "string") {
|
|
253
|
+
push(msgs, "error", "accountPropertyFilter must be a string (sailpoint.object.Filter expression).", "attributes.accountPropertyFilter");
|
|
254
|
+
}
|
|
255
|
+
else if (attrs.accountPropertyFilter.trim() === "") {
|
|
256
|
+
push(msgs, "error", "accountPropertyFilter must not be empty if provided. Omit it to disable in-memory filtering.", "attributes.accountPropertyFilter");
|
|
257
|
+
}
|
|
202
258
|
}
|
|
203
259
|
return msgs;
|
|
204
260
|
}
|
|
@@ -207,28 +263,88 @@ function lintAccountAttribute(attrs) {
|
|
|
207
263
|
// ---------------------------------------------------------------------------
|
|
208
264
|
function lintConditional(attrs) {
|
|
209
265
|
const msgs = [];
|
|
266
|
+
const RESERVED = new Set(["expression", "positiveCondition", "negativeCondition"]);
|
|
267
|
+
// Helper: extract all $varName references from a string
|
|
268
|
+
const extractVars = (s) => (s.match(/\$([A-Za-z_][A-Za-z0-9_]*)/g) ?? []).map((v) => v.slice(1));
|
|
269
|
+
// All non-reserved keys in attributes are declared dynamic variables
|
|
270
|
+
const declaredVars = new Set(Object.keys(attrs ?? {}).filter((k) => !RESERVED.has(k)));
|
|
271
|
+
// --- 1. expression: required, non-empty ---
|
|
210
272
|
const exprRaw = attrs?.expression;
|
|
211
273
|
const expr = String(exprRaw ?? "").trim();
|
|
212
274
|
if (!expr) {
|
|
213
275
|
push(msgs, "error", "Missing required attribute: expression.", "attributes.expression");
|
|
214
276
|
return msgs;
|
|
215
277
|
}
|
|
278
|
+
// --- 2. Forbidden operators (using any of these throws IllegalArgumentException at runtime) ---
|
|
216
279
|
const forbidden = /(!=|==|>=|<=|>|<|\bne\b|\bgt\b|\blt\b|\bge\b|\ble\b)/i;
|
|
217
280
|
if (forbidden.test(expr)) {
|
|
218
|
-
push(msgs, "error", `Unsupported operator in expression
|
|
281
|
+
push(msgs, "error", `Unsupported operator in expression '${expr}'. ` +
|
|
282
|
+
"Only 'eq' is supported — using !=, ==, >, <, ne, gt, lt, ge, le throws IllegalArgumentException at runtime.", "attributes.expression");
|
|
283
|
+
}
|
|
284
|
+
// --- 3. Must contain exactly one 'eq' ---
|
|
285
|
+
const eqMatches = expr.match(/\beq\b/gi) ?? [];
|
|
286
|
+
if (eqMatches.length === 0) {
|
|
287
|
+
push(msgs, "error", `Conditional expression must use the 'eq' comparator: '<ValueA> eq <ValueB>'. Got: '${expr}'.`, "attributes.expression");
|
|
219
288
|
}
|
|
220
|
-
if (
|
|
221
|
-
push(msgs, "error", `
|
|
289
|
+
else if (eqMatches.length > 1) {
|
|
290
|
+
push(msgs, "error", `Expression must contain exactly one 'eq'. Found ${eqMatches.length} occurrences in: '${expr}'. ` +
|
|
291
|
+
"Nest multiple conditions using separate conditional transforms if needed.", "attributes.expression");
|
|
222
292
|
}
|
|
293
|
+
// --- 4. Both sides of 'eq' must be non-empty ---
|
|
223
294
|
const parts = expr.split(/\beq\b/i);
|
|
224
|
-
|
|
225
|
-
|
|
295
|
+
const valueA = parts[0]?.trim() ?? "";
|
|
296
|
+
const valueB = parts[1]?.trim() ?? "";
|
|
297
|
+
if (parts.length !== 2 || valueA.length === 0 || valueB.length === 0) {
|
|
298
|
+
push(msgs, "error", `Expression must follow '<ValueA> eq <ValueB>' with non-empty values on both sides. Got: '${expr}'.`, "attributes.expression");
|
|
299
|
+
}
|
|
300
|
+
// --- 5. Case-sensitivity info for literal operands ---
|
|
301
|
+
if (valueA.length > 0 && valueB.length > 0) {
|
|
302
|
+
const aIsVar = valueA.startsWith("$");
|
|
303
|
+
const bIsVar = valueB.startsWith("$");
|
|
304
|
+
if (!aIsVar || !bIsVar) {
|
|
305
|
+
push(msgs, "info", "Conditional comparisons are case-sensitive. " +
|
|
306
|
+
`'${!aIsVar ? valueA : valueB}' must match the source value exactly — ` +
|
|
307
|
+
"'Engineering' and 'engineering' are treated as different values.", "attributes.expression");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// --- 6. Cross-check $variable references in expression ---
|
|
311
|
+
if (valueA.length > 0 && valueB.length > 0) {
|
|
312
|
+
for (const varName of extractVars(expr)) {
|
|
313
|
+
if (!declaredVars.has(varName)) {
|
|
314
|
+
push(msgs, "error", `Expression references '$${varName}' but no matching variable key '${varName}' is declared in attributes. ` +
|
|
315
|
+
`Add a '${varName}' key to attributes as a static string or nested transform.`, "attributes.expression");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
226
318
|
}
|
|
227
|
-
|
|
228
|
-
|
|
319
|
+
// --- 7. positiveCondition: type check + $var cross-check ---
|
|
320
|
+
const posRaw = attrs?.positiveCondition;
|
|
321
|
+
if (posRaw !== undefined) {
|
|
322
|
+
if (typeof posRaw !== "string") {
|
|
323
|
+
push(msgs, "error", "positiveCondition must be a string — either a static value or a $variableName reference.", "attributes.positiveCondition");
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
for (const varName of extractVars(posRaw)) {
|
|
327
|
+
if (!declaredVars.has(varName)) {
|
|
328
|
+
push(msgs, "error", `positiveCondition references '$${varName}' but no matching variable key '${varName}' is declared in attributes. ` +
|
|
329
|
+
`Add a '${varName}' key to attributes as a static string or nested transform.`, "attributes.positiveCondition");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
229
333
|
}
|
|
230
|
-
|
|
231
|
-
|
|
334
|
+
// --- 8. negativeCondition: type check + $var cross-check ---
|
|
335
|
+
const negRaw = attrs?.negativeCondition;
|
|
336
|
+
if (negRaw !== undefined) {
|
|
337
|
+
if (typeof negRaw !== "string") {
|
|
338
|
+
push(msgs, "error", "negativeCondition must be a string — either a static value or a $variableName reference.", "attributes.negativeCondition");
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
for (const varName of extractVars(negRaw)) {
|
|
342
|
+
if (!declaredVars.has(varName)) {
|
|
343
|
+
push(msgs, "error", `negativeCondition references '$${varName}' but no matching variable key '${varName}' is declared in attributes. ` +
|
|
344
|
+
`Add a '${varName}' key to attributes as a static string or nested transform.`, "attributes.negativeCondition");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
232
348
|
}
|
|
233
349
|
return msgs;
|
|
234
350
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isc-transforms-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for SailPoint Identity Security Cloud (ISC) Transform authoring — scaffold, strict lint, catalog, and safe upsert to live tenants.",
|
|
6
6
|
"author": {
|