@vitormnm/node-red-simple-opcua 1.4.2 → 1.5.0
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/README.md +5 -0
- package/client/lib/opcua-client-browser.js +40 -2
- package/client/opcua-client-utils.js +12 -11
- package/client/view/opcua-client.js +10 -6
- package/package.json +42 -42
- package/resources/bmc-button.svg +22 -0
- package/server/lib/opcua-address-space-builder.js +122 -4
- package/server/lib/opcua-config.js +145 -14
- package/server/lib/opcua-constants.js +10 -0
- package/server/lib/opcua-server-runtime-child.js +56 -2
- package/server/lib/opcua-server-runtime.js +71 -6
- package/server/opcua-server-io.js +1 -0
- package/server/opcua-server.html +35 -1523
- package/server/opcua-server.js +24 -13
- package/server/view/opcua-server.css +492 -0
- package/server/view/opcua-server.js +1435 -0
|
@@ -19,6 +19,7 @@ class OpcUaServerConfigParser {
|
|
|
19
19
|
|
|
20
20
|
parseNodeConfig(config, credentials) {
|
|
21
21
|
const security = this.applySecuritySettings(config.securityPolicy, config.securityMode);
|
|
22
|
+
const auth = this.parseAuthConfig(config, credentials);
|
|
22
23
|
return {
|
|
23
24
|
id: this.node.id,
|
|
24
25
|
name: config.name,
|
|
@@ -29,7 +30,8 @@ class OpcUaServerConfigParser {
|
|
|
29
30
|
resourcePath: config.resourcePath || DEFAULT_RESOURCE_PATH,
|
|
30
31
|
treeConfig: this.parseTreeConfig(config.tree),
|
|
31
32
|
allowAnonymous: this.normalizeAllowAnonymous(config.allowAnonymous),
|
|
32
|
-
|
|
33
|
+
groups: auth.groups,
|
|
34
|
+
users: auth.users,
|
|
33
35
|
securityPolicy: security.securityPolicy,
|
|
34
36
|
securityMode: security.securityMode
|
|
35
37
|
};
|
|
@@ -44,19 +46,55 @@ class OpcUaServerConfigParser {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
parseAuthConfig(config, credentials) {
|
|
50
|
+
const safeConfig = config || {};
|
|
51
|
+
let groups = [];
|
|
52
|
+
let users = [];
|
|
53
|
+
|
|
48
54
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
users.unshift(credentialUser);
|
|
53
|
-
}
|
|
54
|
-
return users;
|
|
55
|
+
groups = this.normalizeGroupsConfig(
|
|
56
|
+
credentials && credentials.groups !== undefined ? credentials.groups : safeConfig.groups
|
|
57
|
+
);
|
|
55
58
|
} catch (error) {
|
|
56
|
-
this.node.warn("Invalid
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
this.node.warn("Invalid groups configuration in editor, using derived groups only: " + error.message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
users = this.normalizeUsersConfig(
|
|
64
|
+
credentials && credentials.users !== undefined ? credentials.users : safeConfig.users
|
|
65
|
+
);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this.node.warn("Invalid users configuration in editor, using only legacy credential user: " + error.message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const credentialUser = this.normalizeCredentialUser(credentials);
|
|
71
|
+
if (credentialUser) {
|
|
72
|
+
users.unshift(credentialUser);
|
|
59
73
|
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
groups: this.buildResolvedGroups(groups, users),
|
|
77
|
+
users
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
buildResolvedGroups(groups, users) {
|
|
82
|
+
const resolved = [];
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
|
|
85
|
+
const addGroup = (groupName) => {
|
|
86
|
+
const normalized = typeof groupName === "string" ? groupName.trim() : "";
|
|
87
|
+
if (!normalized || seen.has(normalized)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
seen.add(normalized);
|
|
91
|
+
resolved.push(normalized);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
(Array.isArray(groups) ? groups : []).forEach(addGroup);
|
|
95
|
+
(Array.isArray(users) ? users : []).forEach((user) => addGroup(user && user.group));
|
|
96
|
+
|
|
97
|
+
return resolved;
|
|
60
98
|
}
|
|
61
99
|
|
|
62
100
|
normalizeTreeConfig(rawTree) {
|
|
@@ -125,6 +163,37 @@ class OpcUaServerConfigParser {
|
|
|
125
163
|
};
|
|
126
164
|
}
|
|
127
165
|
|
|
166
|
+
normalizeAccessPermissions(rawPermissions) {
|
|
167
|
+
let values = rawPermissions;
|
|
168
|
+
|
|
169
|
+
if (values === undefined || values === null || values === "") {
|
|
170
|
+
values = ["public"];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (typeof values === "string") {
|
|
174
|
+
values = values.indexOf(",") >= 0
|
|
175
|
+
? values.split(",")
|
|
176
|
+
: [values];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!Array.isArray(values)) {
|
|
180
|
+
throw new Error("'accessPermission' must be an array or string");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
const normalized = values.reduce((result, value) => {
|
|
185
|
+
const permission = String(value || "").trim().toLowerCase();
|
|
186
|
+
if (!permission || seen.has(permission)) {
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
seen.add(permission);
|
|
190
|
+
result.push(permission);
|
|
191
|
+
return result;
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
return normalized.length ? normalized : ["public"];
|
|
195
|
+
}
|
|
196
|
+
|
|
128
197
|
normalizeObjectTypes(objectTypes) {
|
|
129
198
|
if (!Array.isArray(objectTypes)) {
|
|
130
199
|
throw new Error("'objectsTypes' must be an array");
|
|
@@ -133,6 +202,53 @@ class OpcUaServerConfigParser {
|
|
|
133
202
|
return objectTypes.map((objectTypeConfig) => this.normalizeBranch(objectTypeConfig, "object type"));
|
|
134
203
|
}
|
|
135
204
|
|
|
205
|
+
normalizeGroupsConfig(rawGroups) {
|
|
206
|
+
let parsed = rawGroups;
|
|
207
|
+
|
|
208
|
+
if (parsed === undefined || parsed === null || parsed === "") {
|
|
209
|
+
parsed = [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (typeof parsed === "string") {
|
|
213
|
+
parsed = JSON.parse(parsed);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!Array.isArray(parsed)) {
|
|
217
|
+
throw new Error("Groups configuration must be an array");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
return parsed.reduce((groups, groupConfig) => {
|
|
222
|
+
const groupName = this.normalizeGroup(groupConfig);
|
|
223
|
+
if (!seen.has(groupName)) {
|
|
224
|
+
seen.add(groupName);
|
|
225
|
+
groups.push(groupName);
|
|
226
|
+
}
|
|
227
|
+
return groups;
|
|
228
|
+
}, []);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
normalizeGroup(groupConfig) {
|
|
232
|
+
if (typeof groupConfig === "string") {
|
|
233
|
+
const groupName = groupConfig.trim();
|
|
234
|
+
if (!groupName) {
|
|
235
|
+
throw new Error("Each group requires a non-empty name");
|
|
236
|
+
}
|
|
237
|
+
return groupName;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!groupConfig || typeof groupConfig !== "object" || Array.isArray(groupConfig)) {
|
|
241
|
+
throw new Error("Each group must be a string or an object");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const groupName = typeof groupConfig.name === "string" ? groupConfig.name.trim() : "";
|
|
245
|
+
if (!groupName) {
|
|
246
|
+
throw new Error("Each group requires a non-empty name");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return groupName;
|
|
250
|
+
}
|
|
251
|
+
|
|
136
252
|
normalizeUsersConfig(rawUsers) {
|
|
137
253
|
let parsed = rawUsers;
|
|
138
254
|
|
|
@@ -163,7 +279,8 @@ class OpcUaServerConfigParser {
|
|
|
163
279
|
return {
|
|
164
280
|
username,
|
|
165
281
|
password,
|
|
166
|
-
passwordHash: ""
|
|
282
|
+
passwordHash: "",
|
|
283
|
+
group: "default"
|
|
167
284
|
};
|
|
168
285
|
}
|
|
169
286
|
|
|
@@ -196,6 +313,7 @@ class OpcUaServerConfigParser {
|
|
|
196
313
|
description: branchConfig.description || "",
|
|
197
314
|
nodeId: this.normalizeOptionalNodeId(branchConfig.nodeId),
|
|
198
315
|
namespaceId: this.normalizeNamespaceId(branchConfig.namespaceId),
|
|
316
|
+
accessPermission: this.normalizeAccessPermissions(branchConfig.accessPermission || branchConfig.accessPermissions),
|
|
199
317
|
folders: Array.isArray(branchConfig.folders)
|
|
200
318
|
? branchConfig.folders.map((folderConfig) => this.normalizeBranch(folderConfig, "folder"))
|
|
201
319
|
: [],
|
|
@@ -271,7 +389,8 @@ class OpcUaServerConfigParser {
|
|
|
271
389
|
description: variableConfig.description || "",
|
|
272
390
|
displayName: variableConfig.displayName || name,
|
|
273
391
|
nodeId: this.normalizeOptionalNodeId(variableConfig.nodeId),
|
|
274
|
-
namespaceId: this.normalizeNamespaceId(variableConfig.namespaceId)
|
|
392
|
+
namespaceId: this.normalizeNamespaceId(variableConfig.namespaceId),
|
|
393
|
+
accessPermission: this.normalizeAccessPermissions(variableConfig.accessPermission || variableConfig.accessPermissions)
|
|
275
394
|
};
|
|
276
395
|
}
|
|
277
396
|
|
|
@@ -288,6 +407,7 @@ class OpcUaServerConfigParser {
|
|
|
288
407
|
description: methodConfig.description || "",
|
|
289
408
|
nodeId: this.normalizeOptionalNodeId(methodConfig.nodeId),
|
|
290
409
|
namespaceId: this.normalizeNamespaceId(methodConfig.namespaceId),
|
|
410
|
+
accessPermission: this.normalizeAccessPermissions(methodConfig.accessPermission || methodConfig.accessPermissions),
|
|
291
411
|
inputs: Array.isArray(methodConfig.inputs)
|
|
292
412
|
? methodConfig.inputs.map((arg) => this.normalizeMethodArg(arg))
|
|
293
413
|
: Array.isArray(methodConfig.inputArguments)
|
|
@@ -336,6 +456,7 @@ class OpcUaServerConfigParser {
|
|
|
336
456
|
description: typeof alarmConfig.description === "string" ? alarmConfig.description : "",
|
|
337
457
|
nodeId: this.normalizeOptionalNodeId(alarmConfig.nodeId),
|
|
338
458
|
namespaceId: this.normalizeNamespaceId(alarmConfig.namespaceId),
|
|
459
|
+
accessPermission: this.normalizeAccessPermissions(alarmConfig.accessPermission || alarmConfig.accessPermissions),
|
|
339
460
|
enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true
|
|
340
461
|
};
|
|
341
462
|
|
|
@@ -364,6 +485,11 @@ class OpcUaServerConfigParser {
|
|
|
364
485
|
const username = typeof userConfig.username === "string" ? userConfig.username.trim() : "";
|
|
365
486
|
const passwordHash = typeof userConfig.passwordHash === "string" ? userConfig.passwordHash : "";
|
|
366
487
|
const password = typeof userConfig.password === "string" ? userConfig.password : "";
|
|
488
|
+
const group = typeof userConfig.group === "string"
|
|
489
|
+
? userConfig.group.trim()
|
|
490
|
+
: typeof userConfig.role === "string"
|
|
491
|
+
? userConfig.role.trim()
|
|
492
|
+
: "";
|
|
367
493
|
|
|
368
494
|
if (!username) {
|
|
369
495
|
throw new Error("Each user requires a non-empty username");
|
|
@@ -373,10 +499,15 @@ class OpcUaServerConfigParser {
|
|
|
373
499
|
throw new Error("Each user requires a password or password hash");
|
|
374
500
|
}
|
|
375
501
|
|
|
502
|
+
if (!group) {
|
|
503
|
+
throw new Error("Each user requires a non-empty group");
|
|
504
|
+
}
|
|
505
|
+
|
|
376
506
|
return {
|
|
377
507
|
username,
|
|
378
508
|
password,
|
|
379
|
-
passwordHash
|
|
509
|
+
passwordHash,
|
|
510
|
+
group
|
|
380
511
|
};
|
|
381
512
|
}
|
|
382
513
|
|
|
@@ -13,6 +13,11 @@ const {
|
|
|
13
13
|
MessageSecurityMode,
|
|
14
14
|
UserTokenType,
|
|
15
15
|
coerceNodeId ,
|
|
16
|
+
resolveNodeId,
|
|
17
|
+
PermissionType,
|
|
18
|
+
makeRoles,
|
|
19
|
+
WellKnownRoles,
|
|
20
|
+
OPCUACertificateManager,
|
|
16
21
|
} = opcua;
|
|
17
22
|
|
|
18
23
|
const DEFAULT_PORT = 4840;
|
|
@@ -90,12 +95,17 @@ module.exports = {
|
|
|
90
95
|
OPCUAServer,
|
|
91
96
|
Variant,
|
|
92
97
|
DataType,
|
|
98
|
+
OPCUACertificateManager,
|
|
93
99
|
StatusCodes,
|
|
94
100
|
VariantArrayType,
|
|
95
101
|
SecurityPolicy,
|
|
96
102
|
MessageSecurityMode,
|
|
97
103
|
UserTokenType,
|
|
98
104
|
coerceNodeId,
|
|
105
|
+
resolveNodeId,
|
|
106
|
+
PermissionType,
|
|
107
|
+
makeRoles,
|
|
108
|
+
WellKnownRoles,
|
|
99
109
|
DEFAULT_PORT,
|
|
100
110
|
DEFAULT_SERVER_NAME,
|
|
101
111
|
DEFAULT_NAMESPACE_URI,
|
|
@@ -100,6 +100,17 @@ class OpcUaServerProcess {
|
|
|
100
100
|
nodeId: nodeId
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
process.send({
|
|
104
|
+
type: "send",
|
|
105
|
+
data: {
|
|
106
|
+
payload: {
|
|
107
|
+
status: "running"
|
|
108
|
+
},
|
|
109
|
+
topic : settings.serverName
|
|
110
|
+
},
|
|
111
|
+
nodeId: nodeId
|
|
112
|
+
});
|
|
113
|
+
|
|
103
114
|
|
|
104
115
|
} catch (error) {
|
|
105
116
|
this.isRunning = false;
|
|
@@ -110,6 +121,17 @@ class OpcUaServerProcess {
|
|
|
110
121
|
data: "Failed to start OPC UA server: " + error.message
|
|
111
122
|
});
|
|
112
123
|
|
|
124
|
+
process.send({
|
|
125
|
+
type: "send",
|
|
126
|
+
data: {
|
|
127
|
+
payload: {
|
|
128
|
+
status: "error"
|
|
129
|
+
},
|
|
130
|
+
topic : settings.serverName
|
|
131
|
+
},
|
|
132
|
+
nodeId: nodeId
|
|
133
|
+
});
|
|
134
|
+
|
|
113
135
|
process.send({
|
|
114
136
|
type: "status",
|
|
115
137
|
data: {
|
|
@@ -214,7 +236,7 @@ class OpcUaServerProcess {
|
|
|
214
236
|
msg.payload = result.payload;
|
|
215
237
|
this.assignReadMetadata(msg, identifierType, result.identifiers);
|
|
216
238
|
|
|
217
|
-
|
|
239
|
+
|
|
218
240
|
|
|
219
241
|
if (result.identifiers.length === 1) {
|
|
220
242
|
msg.topic = result.identifiers[0];
|
|
@@ -438,7 +460,7 @@ class OpcUaServerProcess {
|
|
|
438
460
|
writtenPaths
|
|
439
461
|
);
|
|
440
462
|
|
|
441
|
-
|
|
463
|
+
|
|
442
464
|
if (writtenPaths.length === 1) {
|
|
443
465
|
msg.topic = writtenPaths[0];
|
|
444
466
|
}
|
|
@@ -592,6 +614,17 @@ class OpcUaServerProcess {
|
|
|
592
614
|
nodeId: nodeId
|
|
593
615
|
});
|
|
594
616
|
|
|
617
|
+
process.send({
|
|
618
|
+
type: "send",
|
|
619
|
+
data: {
|
|
620
|
+
payload: {
|
|
621
|
+
status: "updating"
|
|
622
|
+
},
|
|
623
|
+
topic : this.node.serverName
|
|
624
|
+
},
|
|
625
|
+
nodeId: nodeId
|
|
626
|
+
});
|
|
627
|
+
|
|
595
628
|
await this.ensureReady();
|
|
596
629
|
|
|
597
630
|
const nextTree = this.parser.normalizeTreeConfig(payload);
|
|
@@ -600,6 +633,17 @@ class OpcUaServerProcess {
|
|
|
600
633
|
|
|
601
634
|
const endpointUrl = await this.runtime.getEndpointUrl();
|
|
602
635
|
|
|
636
|
+
process.send({
|
|
637
|
+
type: "send",
|
|
638
|
+
data: {
|
|
639
|
+
payload: {
|
|
640
|
+
status: "running"
|
|
641
|
+
},
|
|
642
|
+
topic : this.node.serverName
|
|
643
|
+
},
|
|
644
|
+
nodeId: nodeId
|
|
645
|
+
});
|
|
646
|
+
|
|
603
647
|
|
|
604
648
|
process.send({
|
|
605
649
|
type: "status",
|
|
@@ -623,6 +667,16 @@ class OpcUaServerProcess {
|
|
|
623
667
|
nodeId: nodeId
|
|
624
668
|
});
|
|
625
669
|
|
|
670
|
+
process.send({
|
|
671
|
+
type: "send",
|
|
672
|
+
data: {
|
|
673
|
+
payload: {
|
|
674
|
+
status: "error"
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
nodeId: nodeId
|
|
678
|
+
});
|
|
679
|
+
|
|
626
680
|
process.send({
|
|
627
681
|
type: "status",
|
|
628
682
|
data: {
|
|
@@ -5,10 +5,20 @@
|
|
|
5
5
|
const {
|
|
6
6
|
OPCUAServer,
|
|
7
7
|
UserTokenType,
|
|
8
|
-
buildApplicationUri
|
|
8
|
+
buildApplicationUri,
|
|
9
|
+
makeRoles,
|
|
10
|
+
WellKnownRoles,
|
|
11
|
+
resolveNodeId
|
|
9
12
|
} = require("./opcua-constants");
|
|
10
13
|
const { OpcUaAddressSpaceBuilder } = require("./opcua-address-space-builder");
|
|
11
14
|
const { OpcUaServerMethods } = require("./opcua-server-methods");
|
|
15
|
+
let bcrypt = null;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
bcrypt = require("bcryptjs");
|
|
19
|
+
} catch (error) {
|
|
20
|
+
bcrypt = null;
|
|
21
|
+
}
|
|
12
22
|
|
|
13
23
|
class OpcUaServerRuntime {
|
|
14
24
|
constructor(options) {
|
|
@@ -22,6 +32,7 @@ class OpcUaServerRuntime {
|
|
|
22
32
|
this.namespaceUri = options.settings.namespaceUri;
|
|
23
33
|
this.resourcePath = options.settings.resourcePath;
|
|
24
34
|
this.allowAnonymous = options.settings.allowAnonymous;
|
|
35
|
+
this.groups = options.settings.groups;
|
|
25
36
|
this.users = options.settings.users;
|
|
26
37
|
this.securityPolicy = options.settings.securityPolicy;
|
|
27
38
|
this.securityMode = options.settings.securityMode;
|
|
@@ -55,7 +66,8 @@ class OpcUaServerRuntime {
|
|
|
55
66
|
registry: this.registry,
|
|
56
67
|
node: this.node,
|
|
57
68
|
serverName: this.serverName,
|
|
58
|
-
addressSpace: this.addressSpace
|
|
69
|
+
addressSpace: this.addressSpace,
|
|
70
|
+
allowAnonymous: this.allowAnonymous
|
|
59
71
|
});
|
|
60
72
|
|
|
61
73
|
|
|
@@ -195,9 +207,6 @@ class OpcUaServerRuntime {
|
|
|
195
207
|
buildNumber: "1",
|
|
196
208
|
buildDate: new Date()
|
|
197
209
|
},
|
|
198
|
-
// serverCertificateManager: {
|
|
199
|
-
// automaticallyAcceptUnknownCertificate: true
|
|
200
|
-
// },
|
|
201
210
|
serverCapabilities: {
|
|
202
211
|
maxSessions: this.maxConnections
|
|
203
212
|
},
|
|
@@ -210,12 +219,64 @@ class OpcUaServerRuntime {
|
|
|
210
219
|
securityModes: [this.securityMode],
|
|
211
220
|
allowAnonymous: this.allowAnonymous,
|
|
212
221
|
userManager: {
|
|
213
|
-
isValidUser: (username, password) => this.isValidUser(username, password)
|
|
222
|
+
isValidUser: (username, password) => this.isValidUser(username, password),
|
|
223
|
+
getUserRoles: (username) => this.getUserRoles(username)
|
|
214
224
|
},
|
|
215
225
|
userTokenPolicies
|
|
216
226
|
};
|
|
217
227
|
}
|
|
218
228
|
|
|
229
|
+
getUserRoles(username) {
|
|
230
|
+
const normalizedUserName = typeof username === "string" ? username.trim() : "";
|
|
231
|
+
if (!normalizedUserName || normalizedUserName.toLowerCase() === "anonymous") {
|
|
232
|
+
return makeRoles([WellKnownRoles.Anonymous]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const user = this.users.find((entry) => entry && entry.username === normalizedUserName);
|
|
236
|
+
if (!user) {
|
|
237
|
+
return makeRoles([WellKnownRoles.AuthenticatedUser]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const roles = [resolveNodeId("WellKnownRole_AuthenticatedUser")];
|
|
241
|
+
const customRole = this.resolveGroupRoleNodeId(user.group);
|
|
242
|
+
if (customRole) {
|
|
243
|
+
roles.push(customRole);
|
|
244
|
+
}
|
|
245
|
+
return roles;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
resolveGroupRoleNodeId(groupName) {
|
|
249
|
+
const normalized = String(groupName || "").trim().toLowerCase();
|
|
250
|
+
if (!normalized || normalized === "public") {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const wellKnownRoles = {
|
|
255
|
+
operator: "WellKnownRole_Operator",
|
|
256
|
+
supervisor: "WellKnownRole_Supervisor",
|
|
257
|
+
engineer: "WellKnownRole_Engineer",
|
|
258
|
+
engineering: "WellKnownRole_Engineer",
|
|
259
|
+
observer: "WellKnownRole_Observer",
|
|
260
|
+
admin: "WellKnownRole_ConfigureAdmin",
|
|
261
|
+
configureadmin: "WellKnownRole_ConfigureAdmin",
|
|
262
|
+
securityadmin: "WellKnownRole_SecurityAdmin"
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (wellKnownRoles[normalized]) {
|
|
266
|
+
return resolveNodeId(wellKnownRoles[normalized]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return resolveNodeId("ns=1;s=NodeRedRole/" + this.sanitizeRoleSegment(normalized));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
sanitizeRoleSegment(value) {
|
|
273
|
+
return String(value || "")
|
|
274
|
+
.trim()
|
|
275
|
+
.toLowerCase()
|
|
276
|
+
.replace(/\s+/g, "_")
|
|
277
|
+
.replace(/[^a-z0-9._-]/g, "_");
|
|
278
|
+
}
|
|
279
|
+
|
|
219
280
|
isValidUser(username, password) {
|
|
220
281
|
return this.users.some((user) => {
|
|
221
282
|
if (user.username !== username) {
|
|
@@ -227,6 +288,10 @@ class OpcUaServerRuntime {
|
|
|
227
288
|
}
|
|
228
289
|
|
|
229
290
|
if (user.passwordHash) {
|
|
291
|
+
if (!bcrypt) {
|
|
292
|
+
this.node.warn("bcryptjs is not installed, so hashed passwords cannot be validated.");
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
230
295
|
try {
|
|
231
296
|
return bcrypt.compareSync(password, user.passwordHash);
|
|
232
297
|
} catch (error) {
|