@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.
@@ -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
- users: this.parseUsersConfig(config.users, credentials),
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
- parseUsersConfig(rawUsers, credentials) {
49
+ parseAuthConfig(config, credentials) {
50
+ const safeConfig = config || {};
51
+ let groups = [];
52
+ let users = [];
53
+
48
54
  try {
49
- const users = this.normalizeUsersConfig(rawUsers);
50
- const credentialUser = this.normalizeCredentialUser(credentials);
51
- if (credentialUser) {
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 users configuration in editor, using only credential user: " + error.message);
57
- const credentialUser = this.normalizeCredentialUser(credentials);
58
- return credentialUser ? [credentialUser] : [];
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) {
@@ -179,6 +179,7 @@ module.exports = function (RED) {
179
179
  function childControl(node) {
180
180
 
181
181
  const child = registry.resolveChild(node.serverRef)
182
+ child.setMaxListeners(0);
182
183
 
183
184
  child.on("message", (msg) => {
184
185