node-opcua-address-space 2.175.6 → 2.176.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.
@@ -1,6 +1,9 @@
1
- import type { ContinuationData, IContinuationPointInfo, IContinuationPointManager, ISessionBase } from "node-opcua-address-space-base";
1
+ import type { ContinuationData, IChannelBase, IContinuationPointInfo, IContinuationPointManager, ISessionBase, UAObject, UAObjectType } from "node-opcua-address-space-base";
2
+ import type { Certificate } from "node-opcua-crypto/web";
2
3
  import type { DataValue } from "node-opcua-data-value";
3
4
  import type { ReferenceDescription } from "node-opcua-service-browse";
5
+ import { MessageSecurityMode } from "node-opcua-types";
6
+ import { type AnyUserIdentityToken, type IServerBase, SessionContext } from "..";
4
7
  export declare class MockContinuationPointManager implements IContinuationPointManager {
5
8
  registerHistoryReadRaw(_maxElements: number, _dataValues: DataValue[], _cnt: ContinuationData): IContinuationPointInfo<DataValue>;
6
9
  getNextHistoryReadRaw(_numValues: number, _cnt: ContinuationData): IContinuationPointInfo<DataValue>;
@@ -9,3 +12,67 @@ export declare class MockContinuationPointManager implements IContinuationPointM
9
12
  dispose(): void;
10
13
  }
11
14
  export declare const mockSession: ISessionBase;
15
+ export interface MockSessionContextOptions {
16
+ /**
17
+ * user name carried by the Session's UserIdentityToken.
18
+ * `"anonymous"` produces an AnonymousIdentityToken; omitting it produces a Session
19
+ * with no token at all, i.e. one that was never activated. Either way the Session
20
+ * exists, which is what distinguishes this from SessionContext.defaultContext.
21
+ */
22
+ userName?: string;
23
+ /**
24
+ * the UserIdentityToken itself, when `userName` cannot express it — an X509IdentityToken,
25
+ * or a UserNameIdentityToken carrying a policyId. Takes precedence over `userName`.
26
+ */
27
+ userIdentityToken?: AnyUserIdentityToken;
28
+ /**
29
+ * SecureChannel security mode seen by isAccessRestricted().
30
+ *
31
+ * Leaving this undefined is the trap this helper exists to avoid: a node carrying
32
+ * AccessRestrictions SigningRequired then fails with BadSecurityModeInsufficient,
33
+ * because a hand-written mock that omits `channel` reads back securityMode ===
34
+ * undefined rather than "no restriction".
35
+ * @default MessageSecurityMode.None
36
+ */
37
+ securityMode?: MessageSecurityMode;
38
+ /** @default "" */
39
+ securityPolicy?: string;
40
+ /** client application-instance certificate, for the X509 / applicationUri paths */
41
+ clientCertificate?: Certificate | null;
42
+ /**
43
+ * pass `null` to attach no SecureChannel at all — a Session created but not yet bound
44
+ * to a channel. Otherwise a channel is synthesized from securityMode / securityPolicy /
45
+ * clientCertificate, or you may supply a complete one.
46
+ */
47
+ channel?: IChannelBase | null;
48
+ /**
49
+ * the Session's continuation point manager. The default throws on use, which is what
50
+ * you want unless the code under test really browses or reads history through it.
51
+ */
52
+ continuationPointManager?: IContinuationPointManager;
53
+ /** the server whose userManager / roleResolvers resolve this user's Roles */
54
+ server?: IServerBase;
55
+ /** the object a Method is being called on */
56
+ object?: UAObject | UAObjectType;
57
+ /** endpoint URL the Session was created on */
58
+ endpointUrl?: string;
59
+ }
60
+ /**
61
+ * Build a SessionContext that simulates a remote Session on a SecureChannel.
62
+ *
63
+ * Writing this by hand means assembling an ISessionBase and an IChannelBase and casting
64
+ * through `any`; every field omitted silently reads back as undefined, which is not the
65
+ * same as a permissive default. Use this instead:
66
+ *
67
+ * ```ts
68
+ * const context = makeMockSessionContext({
69
+ * userName: "admin",
70
+ * securityMode: MessageSecurityMode.SignAndEncrypt,
71
+ * server
72
+ * });
73
+ * ```
74
+ *
75
+ * For an in-process caller with no Session at all, use SessionContext.defaultContext —
76
+ * that one is granted every permission by design.
77
+ */
78
+ export declare function makeMockSessionContext(options?: MockSessionContextOptions): SessionContext;
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mockSession = exports.MockContinuationPointManager = void 0;
4
+ exports.makeMockSessionContext = makeMockSessionContext;
4
5
  const node_opcua_nodeid_1 = require("node-opcua-nodeid");
6
+ const node_opcua_types_1 = require("node-opcua-types");
7
+ const __1 = require("..");
5
8
  class MockContinuationPointManager {
6
9
  registerHistoryReadRaw(_maxElements, _dataValues, _cnt) {
7
10
  throw new Error("Method not implemented.");
@@ -26,4 +29,59 @@ exports.mockSession = {
26
29
  },
27
30
  continuationPointManager: new MockContinuationPointManager()
28
31
  };
32
+ /**
33
+ * Build a SessionContext that simulates a remote Session on a SecureChannel.
34
+ *
35
+ * Writing this by hand means assembling an ISessionBase and an IChannelBase and casting
36
+ * through `any`; every field omitted silently reads back as undefined, which is not the
37
+ * same as a permissive default. Use this instead:
38
+ *
39
+ * ```ts
40
+ * const context = makeMockSessionContext({
41
+ * userName: "admin",
42
+ * securityMode: MessageSecurityMode.SignAndEncrypt,
43
+ * server
44
+ * });
45
+ * ```
46
+ *
47
+ * For an in-process caller with no Session at all, use SessionContext.defaultContext —
48
+ * that one is granted every permission by design.
49
+ */
50
+ function makeMockSessionContext(options) {
51
+ const { userName, userIdentityToken, securityMode = node_opcua_types_1.MessageSecurityMode.None, securityPolicy = "", clientCertificate = null, channel, continuationPointManager = new MockContinuationPointManager(), server, object, endpointUrl } = options || {};
52
+ const makeChannel = () => {
53
+ if (channel !== undefined) {
54
+ return channel ?? undefined; // null means: no SecureChannel at all
55
+ }
56
+ return {
57
+ clientCertificate,
58
+ securityMode,
59
+ securityPolicy,
60
+ getTransportSettings() {
61
+ return { maxMessageSize: 0 };
62
+ }
63
+ };
64
+ };
65
+ const makeIdentityToken = () => {
66
+ if (userIdentityToken !== undefined) {
67
+ return userIdentityToken;
68
+ }
69
+ if (userName === undefined) {
70
+ return undefined; // a Session that was never activated
71
+ }
72
+ return userName === "anonymous" ? new node_opcua_types_1.AnonymousIdentityToken() : new node_opcua_types_1.UserNameIdentityToken({ userName });
73
+ };
74
+ const session = {
75
+ getSessionId() {
76
+ return node_opcua_nodeid_1.NodeId.nullNodeId;
77
+ },
78
+ continuationPointManager,
79
+ userIdentityToken: makeIdentityToken(),
80
+ channel: makeChannel(),
81
+ getEndpointUrl() {
82
+ return endpointUrl;
83
+ }
84
+ };
85
+ return new __1.SessionContext({ session, server, object });
86
+ }
29
87
  //# sourceMappingURL=mock_session.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mock_session.js","sourceRoot":"","sources":["../test_helpers/mock_session.ts"],"names":[],"mappings":";;;AAOA,yDAA2C;AAG3C,MAAa,4BAA4B;IAC9B,sBAAsB,CACzB,YAAoB,EACpB,WAAwB,EACxB,IAAsB;QAEtB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,qBAAqB,CAAC,UAAkB,EAAE,IAAsB;QACnE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,kBAAkB,CACrB,YAAoB,EACpB,OAA+B,EAC/B,IAAsB;QAEtB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,iBAAiB,CAAC,SAAiB,EAAE,IAAsB;QAC9D,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,OAAO;QACV,aAAa;IACjB,CAAC;CACJ;AAxBD,oEAwBC;AAEY,QAAA,WAAW,GAAiB;IACrC,YAAY;QACR,OAAO,IAAI,0BAAM,EAAE,CAAC;IACxB,CAAC;IACD,wBAAwB,EAAE,IAAI,4BAA4B,EAAE;CAC/D,CAAC"}
1
+ {"version":3,"file":"mock_session.js","sourceRoot":"","sources":["../test_helpers/mock_session.ts"],"names":[],"mappings":";;;AAiHA,wDAmDC;AAzJD,yDAA2C;AAE3C,uDAAsG;AACtG,0BAAiF;AAEjF,MAAa,4BAA4B;IAC9B,sBAAsB,CACzB,YAAoB,EACpB,WAAwB,EACxB,IAAsB;QAEtB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,qBAAqB,CAAC,UAAkB,EAAE,IAAsB;QACnE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,kBAAkB,CACrB,YAAoB,EACpB,OAA+B,EAC/B,IAAsB;QAEtB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,iBAAiB,CAAC,SAAiB,EAAE,IAAsB;QAC9D,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;IACM,OAAO;QACV,aAAa;IACjB,CAAC;CACJ;AAxBD,oEAwBC;AAEY,QAAA,WAAW,GAAiB;IACrC,YAAY;QACR,OAAO,IAAI,0BAAM,EAAE,CAAC;IACxB,CAAC;IACD,wBAAwB,EAAE,IAAI,4BAA4B,EAAE;CAC/D,CAAC;AAgDF;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,sBAAsB,CAAC,OAAmC;IACtE,MAAM,EACF,QAAQ,EACR,iBAAiB,EACjB,YAAY,GAAG,sCAAmB,CAAC,IAAI,EACvC,cAAc,GAAG,EAAE,EACnB,iBAAiB,GAAG,IAAI,EACxB,OAAO,EACP,wBAAwB,GAAG,IAAI,4BAA4B,EAAE,EAC7D,MAAM,EACN,MAAM,EACN,WAAW,EACd,GAAG,OAAO,IAAI,EAAE,CAAC;IAElB,MAAM,WAAW,GAAG,GAA6B,EAAE;QAC/C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,OAAO,IAAI,SAAS,CAAC,CAAC,sCAAsC;QACvE,CAAC;QACD,OAAO;YACH,iBAAiB;YACjB,YAAY;YACZ,cAAc;YACd,oBAAoB;gBAChB,OAAO,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC;YACjC,CAAC;SACJ,CAAC;IACN,CAAC,CAAC;IAEF,MAAM,iBAAiB,GAAG,GAAqC,EAAE;QAC7D,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;YAClC,OAAO,iBAAiB,CAAC;QAC7B,CAAC;QACD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,SAAS,CAAC,CAAC,qCAAqC;QAC3D,CAAC;QACD,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,yCAAsB,EAAE,CAAC,CAAC,CAAC,IAAI,wCAAqB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7G,CAAC,CAAC;IAEF,MAAM,OAAO,GAAiB;QAC1B,YAAY;YACR,OAAO,0BAAM,CAAC,UAAU,CAAC;QAC7B,CAAC;QACD,wBAAwB;QACxB,iBAAiB,EAAE,iBAAiB,EAAE;QACtC,OAAO,EAAE,WAAW,EAAE;QACtB,cAAc;YACV,OAAO,WAAW,CAAC;QACvB,CAAC;KACJ,CAAC;IAEF,OAAO,IAAI,kBAAc,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAC3D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-opcua-address-space",
3
- "version": "2.175.6",
3
+ "version": "2.176.0",
4
4
  "description": "pure nodejs OPCUA SDK - module address-space",
5
5
  "main": "./dist/src/index_current.js",
6
6
  "types": "./dist/source/index.d.ts",
@@ -21,48 +21,48 @@
21
21
  "@types/semver": "7.7.1",
22
22
  "chalk": "4.1.2",
23
23
  "dequeue": "^1.0.5",
24
- "node-opcua-address-space-base": "2.175.6",
24
+ "node-opcua-address-space-base": "2.176.0",
25
25
  "node-opcua-assert": "2.175.6",
26
- "node-opcua-basic-types": "2.175.6",
27
- "node-opcua-binary-stream": "2.175.6",
28
- "node-opcua-client-dynamic-extension-object": "2.175.6",
29
- "node-opcua-constants": "2.175.1",
26
+ "node-opcua-basic-types": "2.176.0",
27
+ "node-opcua-binary-stream": "2.176.0",
28
+ "node-opcua-client-dynamic-extension-object": "2.176.0",
29
+ "node-opcua-constants": "2.176.0",
30
30
  "node-opcua-crypto": "5.4.0",
31
- "node-opcua-data-access": "2.175.6",
32
- "node-opcua-data-model": "2.175.6",
33
- "node-opcua-data-value": "2.175.6",
34
- "node-opcua-date-time": "2.175.6",
31
+ "node-opcua-data-access": "2.176.0",
32
+ "node-opcua-data-model": "2.176.0",
33
+ "node-opcua-data-value": "2.176.0",
34
+ "node-opcua-date-time": "2.176.0",
35
35
  "node-opcua-debug": "2.175.6",
36
- "node-opcua-enum": "2.175.6",
37
- "node-opcua-extension-object": "2.175.6",
38
- "node-opcua-factory": "2.175.6",
39
- "node-opcua-nodeid": "2.175.6",
40
- "node-opcua-nodeset-ua": "2.175.6",
41
- "node-opcua-numeric-range": "2.175.6",
36
+ "node-opcua-enum": "2.176.0",
37
+ "node-opcua-extension-object": "2.176.0",
38
+ "node-opcua-factory": "2.176.0",
39
+ "node-opcua-nodeid": "2.176.0",
40
+ "node-opcua-nodeset-ua": "2.176.0",
41
+ "node-opcua-numeric-range": "2.176.0",
42
42
  "node-opcua-object-registry": "2.175.6",
43
- "node-opcua-pseudo-session": "2.175.6",
44
- "node-opcua-service-browse": "2.175.6",
45
- "node-opcua-service-call": "2.175.6",
46
- "node-opcua-service-history": "2.175.6",
47
- "node-opcua-service-translate-browse-path": "2.175.6",
48
- "node-opcua-service-write": "2.175.6",
49
- "node-opcua-status-code": "2.175.6",
50
- "node-opcua-types": "2.175.6",
51
- "node-opcua-utils": "2.175.6",
52
- "node-opcua-variant": "2.175.6",
53
- "node-opcua-xml2json": "2.175.6",
43
+ "node-opcua-pseudo-session": "2.176.0",
44
+ "node-opcua-service-browse": "2.176.0",
45
+ "node-opcua-service-call": "2.176.0",
46
+ "node-opcua-service-history": "2.176.0",
47
+ "node-opcua-service-translate-browse-path": "2.176.0",
48
+ "node-opcua-service-write": "2.176.0",
49
+ "node-opcua-status-code": "2.176.0",
50
+ "node-opcua-types": "2.176.0",
51
+ "node-opcua-utils": "2.176.0",
52
+ "node-opcua-variant": "2.176.0",
53
+ "node-opcua-xml2json": "2.176.0",
54
54
  "semver": "^7.8.5",
55
55
  "thenify-ex": "4.4.0",
56
56
  "xml-writer": "^1.7.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "humanize": "0.0.9",
60
- "node-opcua-benchmarker": "2.175.6",
60
+ "node-opcua-benchmarker": "2.176.0",
61
61
  "node-opcua-leak-detector": "2.175.6",
62
62
  "node-opcua-nodesets": "2.175.3",
63
- "node-opcua-packet-analyzer": "2.175.6",
64
- "node-opcua-schemas": "2.175.6",
65
- "node-opcua-service-filter": "2.175.6",
63
+ "node-opcua-packet-analyzer": "2.176.0",
64
+ "node-opcua-schemas": "2.176.0",
65
+ "node-opcua-service-filter": "2.176.0",
66
66
  "node-opcua-test-fixtures": "2.175.1"
67
67
  },
68
68
  "author": "Etienne Rossignon",
@@ -80,7 +80,7 @@
80
80
  "internet of things"
81
81
  ],
82
82
  "homepage": "http://node-opcua.github.io/",
83
- "gitHead": "e233d906138995583f42359831d1908e3cb005e7",
83
+ "gitHead": "a4569830a5a564a7ff3e8cf37c44fd35b3f72ac1",
84
84
  "files": [
85
85
  "dist",
86
86
  "distHelpers",
@@ -1,4 +1,38 @@
1
+ /**
2
+ * how a part of the access policy declared in a NodeSet2 file is handled at load time.
3
+ *
4
+ * - `"apply"` : the declaration is installed on the node, as mandated by OPC UA Part 6 F.3.
5
+ * - `"ignore"` : the declaration is discarded. This is what node-opcua did before these options
6
+ * existed: a nodeset declaring a restrictive policy would load fail-open.
7
+ */
8
+ export type NodeSetPermissionsPolicy = "apply" | "ignore";
9
+
1
10
  export interface NodeSetLoaderOptions {
2
11
  loadDraftNodes?: boolean;
3
12
  loadDeprecatedNodes?: boolean;
13
+ /**
14
+ * the `<RolePermissions>` element and the `HasNoPermissions` attribute: *who* may do *what*.
15
+ *
16
+ * This is a property of the information model, so it applies by default. `HasNoPermissions`
17
+ * yields an empty permission list, which is distinct from an absent `<RolePermissions>`:
18
+ * the former grants nothing, the latter inherits the namespace default.
19
+ * @default "apply"
20
+ */
21
+ permissions?: NodeSetPermissionsPolicy;
22
+ /**
23
+ * the `AccessRestrictions` attribute: how the SecureChannel must be secured — signed,
24
+ * encrypted, session-bound — before the node may be reached at all.
25
+ *
26
+ * This one is **opt-in**, because it is a property of the deployment rather than of the
27
+ * model, and node-opcua cannot assume the deployment matches what the nodeset author had
28
+ * in mind. `Opc.Ua.NodeSet2.xml` carries 359 of these, and enforcing them denies 199
29
+ * variables under the Server Object alone — the RoleSet internals, `ServerConfiguration`,
30
+ * method arguments — to any Session on an unsecured channel. That is the correct reading
31
+ * of the attribute, and exactly what you want on a hardened server; it is also a large
32
+ * behaviour change for the many deployments that run with `MessageSecurityMode.None`.
33
+ *
34
+ * Turn it on once your endpoints require signing or encryption.
35
+ * @default "ignore"
36
+ */
37
+ accessRestrictions?: NodeSetPermissionsPolicy;
4
38
  }
@@ -21,9 +21,11 @@ import { coerceBoolean, coerceByte, coerceInt32, StatusCodes } from "node-opcua-
21
21
  import { DataTypeIds } from "node-opcua-constants";
22
22
  import {
23
23
  type AccessLevelFlag,
24
+ type AccessRestrictionsFlag,
24
25
  coerceLocalizedText,
25
26
  type LocalizedText,
26
27
  makeAccessLevelFlag,
28
+ makeAccessRestrictionsFlag,
27
29
  NodeClass,
28
30
  type QualifiedName,
29
31
  stringToQualifiedName
@@ -32,7 +34,7 @@ import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
32
34
  import type { ExtensionObject } from "node-opcua-extension-object";
33
35
  import { getBuiltInType } from "node-opcua-factory";
34
36
  import { NodeId, resolveNodeId } from "node-opcua-nodeid";
35
- import type { EnumFieldOptions } from "node-opcua-types";
37
+ import type { EnumFieldOptions, RolePermissionTypeOptions } from "node-opcua-types";
36
38
  import { DataType, Variant, VariantArrayType, type VariantOptions } from "node-opcua-variant";
37
39
  import { _definitionParser, ReaderState, type ReaderStateParserLike, Xml2Json, type XmlAttributes } from "node-opcua-xml2json";
38
40
  import semver from "semver";
@@ -75,6 +77,32 @@ function convertAccessLevel(accessLevel?: string | null): AccessLevelFlag {
75
77
  return makeAccessLevelFlag(accessLevelN);
76
78
  }
77
79
 
80
+ /**
81
+ * the XSD gives UserAccessLevel a default of 1, but a nodeset that raises AccessLevel and stays
82
+ * silent on UserAccessLevel means "the user may do whatever the node allows", not "read only".
83
+ * Falling back on accessLevel is what every other stack does and what node-opcua has always done;
84
+ * what changes here is that an *explicit* UserAccessLevel is no longer dropped.
85
+ */
86
+ function convertUserAccessLevel(userAccessLevel: string | null | undefined, accessLevel: AccessLevelFlag): AccessLevelFlag {
87
+ if (userAccessLevel === undefined || userAccessLevel === null || userAccessLevel === "") {
88
+ return accessLevel;
89
+ }
90
+ return convertAccessLevel(userAccessLevel);
91
+ }
92
+
93
+ function convertAccessRestrictions(accessRestrictions?: string | null): AccessRestrictionsFlag | undefined {
94
+ if (accessRestrictions === undefined || accessRestrictions === null || accessRestrictions === "") {
95
+ // undefined is not None: it means "inherit the namespace default"
96
+ return undefined;
97
+ }
98
+ const value = parseInt(accessRestrictions, 10);
99
+ if (Number.isNaN(value)) {
100
+ errorLog("load_nodeset2: ignoring invalid AccessRestrictions attribute", accessRestrictions);
101
+ return undefined;
102
+ }
103
+ return makeAccessRestrictionsFlag(value);
104
+ }
105
+
78
106
  type Task = (addressSpace: IAddressSpace) => Promise<void>;
79
107
 
80
108
  function makeDefaultVariant(
@@ -404,6 +432,53 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
404
432
  }
405
433
  }
406
434
  };
435
+ // #region access policy (AccessRestrictions / HasNoPermissions / RolePermissions)
436
+ // two independent switches: who may do what is a property of the model and applies by
437
+ // default, whereas how the channel must be secured is a property of the deployment and
438
+ // is opt-in. See NodeSetLoaderOptions.
439
+ const applyRolePermissions = (options.permissions ?? "apply") === "apply";
440
+ const applyAccessRestrictions = (options.accessRestrictions ?? "ignore") === "apply";
441
+
442
+ interface INodePermissions {
443
+ accessRestrictions?: AccessRestrictionsFlag;
444
+ rolePermissions?: RolePermissionTypeOptions[];
445
+ }
446
+
447
+ /**
448
+ * read the access policy carried by the attributes of any UANode.
449
+ * The `<RolePermissions>` element is handled separately by `role_permissions_parser`.
450
+ */
451
+ function convertNodePermissions(attrs: XmlAttributes): INodePermissions {
452
+ return {
453
+ accessRestrictions: applyAccessRestrictions ? convertAccessRestrictions(attrs.AccessRestrictions) : undefined,
454
+ // HasNoPermissions="true" is not the same as an absent <RolePermissions>: the former
455
+ // grants nothing at all, the latter inherits the namespace default. An empty array
456
+ // is how BaseNode distinguishes the two.
457
+ rolePermissions: applyRolePermissions && coerceBoolean(attrs.HasNoPermissions) ? [] : undefined
458
+ };
459
+ }
460
+
461
+ const role_permissions_parser = {
462
+ // biome-ignore lint/suspicious/noExplicitAny: xml2json parser callback with dynamic this binding
463
+ init(this: any) {
464
+ this.array = [] as RolePermissionTypeOptions[];
465
+ if (applyRolePermissions) {
466
+ this.parent.obj.rolePermissions = this.array;
467
+ }
468
+ },
469
+ parser: {
470
+ RolePermission: {
471
+ // biome-ignore lint/suspicious/noExplicitAny: xml2json parser callback with dynamic this binding
472
+ finish(this: any) {
473
+ this.parent.array.push({
474
+ roleId: _translateNodeId(this.text.trim()),
475
+ permissions: parseInt(this.attrs.Permissions || "0", 10)
476
+ });
477
+ }
478
+ }
479
+ }
480
+ };
481
+ // #endregion
407
482
  // #region UAObject
408
483
  const state_UAObject = {
409
484
  // biome-ignore lint/suspicious/noExplicitAny: xml2json parser callback with dynamic this binding
@@ -416,7 +491,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
416
491
  nodeId: convertToNodeId(attrs.NodeId) || null,
417
492
  browseName: convertQualifiedName(attrs.BrowseName),
418
493
  eventNotifier: coerceByte(attrs.EventNotifier) || 0,
419
- symbolicName: attrs.SymbolicName || null
494
+ symbolicName: attrs.SymbolicName || null,
495
+ ...convertNodePermissions(attrs)
420
496
  };
421
497
 
422
498
  this.isDraft = attrs.ReleaseStatus === "Draft";
@@ -444,7 +520,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
444
520
  }
445
521
  },
446
522
 
447
- References: references_parser
523
+ References: references_parser,
524
+ RolePermissions: role_permissions_parser
448
525
  }
449
526
  };
450
527
  // #endregion
@@ -459,7 +536,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
459
536
  isAbstract: coerceBoolean(attrs.IsAbstract),
460
537
  nodeId: convertToNodeId(attrs.NodeId) || null,
461
538
  browseName: convertQualifiedName(attrs.BrowseName),
462
- eventNotifier: coerceByte(attrs.EventNotifier) || 0
539
+ eventNotifier: coerceByte(attrs.EventNotifier) || 0,
540
+ ...convertNodePermissions(attrs)
463
541
  };
464
542
  },
465
543
  // biome-ignore lint/suspicious/noExplicitAny: xml2json parser callback with dynamic this binding
@@ -481,7 +559,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
481
559
  }
482
560
  },
483
561
 
484
- References: references_parser
562
+ References: references_parser,
563
+ RolePermissions: role_permissions_parser
485
564
  }
486
565
  };
487
566
  // #endregion
@@ -495,7 +574,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
495
574
  nodeClass: NodeClass.ReferenceType,
496
575
  isAbstract: coerceBoolean(attrs.IsAbstract),
497
576
  nodeId: convertToNodeId(attrs.NodeId) || null,
498
- browseName: convertQualifiedName(attrs.BrowseName)
577
+ browseName: convertQualifiedName(attrs.BrowseName),
578
+ ...convertNodePermissions(attrs)
499
579
  };
500
580
  },
501
581
  // biome-ignore lint/suspicious/noExplicitAny: xml2json parser callback with dynamic this binding
@@ -523,7 +603,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
523
603
  this.parent.obj.inverseName = this.text;
524
604
  }
525
605
  },
526
- References: references_parser
606
+ References: references_parser,
607
+ RolePermissions: role_permissions_parser
527
608
  }
528
609
  };
529
610
 
@@ -555,7 +636,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
555
636
  displayName: "",
556
637
  description: "",
557
638
  symbolicName: attrs.SymbolicName,
558
- partialDefinition: []
639
+ partialDefinition: [],
640
+ ...convertNodePermissions(attrs)
559
641
  };
560
642
 
561
643
  this.isDraft = attrs.ReleaseStatus === "Draft";
@@ -618,6 +700,7 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
618
700
  }
619
701
  },
620
702
  References: references_parser,
703
+ RolePermissions: role_permissions_parser,
621
704
 
622
705
  Definition: _definitionParser
623
706
  }
@@ -677,6 +760,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
677
760
  historizing: boolean;
678
761
  accessLevel: number;
679
762
  userAccessLevel: number;
763
+ accessRestrictions?: AccessRestrictionsFlag;
764
+ rolePermissions?: RolePermissionTypeOptions[];
680
765
  }
681
766
  type ReaderUAVariableL1 = ReaderStateParserLike & { obj: IUAVariableProps; isDraft: boolean; isDeprecated: boolean };
682
767
  type ReaderUAVariableL2 = ReaderStateParserLike & { parent: ReaderUAVariableL1; text: string };
@@ -702,7 +787,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
702
787
  historizing: coerceBoolean(attrs.Historizing),
703
788
  nodeId,
704
789
  accessLevel: accessLevel,
705
- userAccessLevel: accessLevel // convertAccessLevel(attrs.UserAccessLevel || attrs.AccessLevel);
790
+ userAccessLevel: convertUserAccessLevel(attrs.UserAccessLevel, accessLevel),
791
+ ...convertNodePermissions(attrs)
706
792
  };
707
793
  this.isDraft = attrs.ReleaseStatus === "Draft" || false;
708
794
  this.isDeprecated = attrs.ReleaseStatus === "Deprecated" || false;
@@ -784,6 +870,7 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
784
870
  }
785
871
  },
786
872
  References: references_parser,
873
+ RolePermissions: role_permissions_parser,
787
874
 
788
875
  Value: makeVariantReader<ReaderUAVariableL2>(
789
876
  (self: ReaderUAVariableL2, data: VariantOptions) => {
@@ -824,7 +911,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
824
911
  minimumSamplingInterval: attrs.MinimumSamplingInterval ? parseInt(attrs.MinimumSamplingInterval, 10) : 0,
825
912
  // UAVariableType has no Historizing attribute in the XSD: keep it false
826
913
  historizing: false,
827
- nodeId: convertToNodeId(attrs.NodeId) || null
914
+ nodeId: convertToNodeId(attrs.NodeId) || null,
915
+ ...convertNodePermissions(attrs)
828
916
  };
829
917
  this.isDraft = attrs.ReleaseStatus === "Draft";
830
918
  this.isDeprecated = attrs.ReleaseStatus === "Deprecated";
@@ -848,6 +936,7 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
848
936
  }
849
937
  },
850
938
  References: references_parser,
939
+ RolePermissions: role_permissions_parser,
851
940
  Value: makeVariantReader<ReaderUAVariableTypeL2>(
852
941
  (self: ReaderUAVariableTypeL2, data: VariantOptions) => {
853
942
  self.parent.obj.value = data;
@@ -878,7 +967,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
878
967
  browseName: convertQualifiedName(attrs.BrowseName),
879
968
  parentNodeId: attrs.ParentNodeId || null,
880
969
  nodeId: convertToNodeId(attrs.NodeId) || null,
881
- methodDeclarationId: attrs.MethodDeclarationId ? _translateNodeId(attrs.MethodDeclarationId) : null
970
+ methodDeclarationId: attrs.MethodDeclarationId ? _translateNodeId(attrs.MethodDeclarationId) : null,
971
+ ...convertNodePermissions(attrs)
882
972
  };
883
973
  this.isDraft = attrs.ReleaseStatus === "Draft";
884
974
  this.isDeprecated = attrs.ReleaseStatus === "Deprecated";
@@ -897,7 +987,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
897
987
  this.parent.obj.displayName = this.text;
898
988
  }
899
989
  },
900
- References: references_parser
990
+ References: references_parser,
991
+ RolePermissions: role_permissions_parser
901
992
  }
902
993
  };
903
994
  // #endregion
@@ -13,6 +13,7 @@ import {
13
13
  } from "node-opcua-basic-types";
14
14
  import { make_debugLog, make_warningLog } from "node-opcua-debug";
15
15
  import { coerceNodeId, ExpandedNodeId, type INodeId, type NodeId, NodeIdType } from "node-opcua-nodeid";
16
+ import { coerceStatusCode, StatusCodes } from "node-opcua-status-code";
16
17
  import { EnumDefinition, StructureDefinition } from "node-opcua-types";
17
18
  import { lowerFirstLetter } from "node-opcua-utils";
18
19
  import { DataType, Variant, type VariantOptions } from "node-opcua-variant";
@@ -72,6 +73,61 @@ interface Parser<T> extends ReaderStateParserLike {
72
73
  parent: any;
73
74
  text: string;
74
75
  }
76
+
77
+ // <StatusCode><Code>2153644032</Code></StatusCode>
78
+ const statusCode_parser: ReaderStateParserLike = {
79
+ init(this: any) {
80
+ this.value = StatusCodes.Good;
81
+ },
82
+ parser: {
83
+ Code: {
84
+ finish(this: any) {
85
+ this.parent.value = coerceStatusCode(parseInt(this.text, 10));
86
+ }
87
+ }
88
+ }
89
+ };
90
+
91
+ // a DiagnosticInfo field is stored on the enclosing DiagnosticInfo, which is only
92
+ // created when at least one field is present ( <DiagnosticInfo/> stays empty ).
93
+ function _diagnosticInfoField(name: string, convert: (text: string) => unknown): ReaderStateParserLike {
94
+ return {
95
+ finish(this: any) {
96
+ this.parent.value = this.parent.value || {};
97
+ this.parent.value[name] = convert(this.text);
98
+ }
99
+ };
100
+ }
101
+
102
+ // <DiagnosticInfo><SymbolicId>1</SymbolicId>...<InnerDiagnosticInfo>...</InnerDiagnosticInfo></DiagnosticInfo>
103
+ const diagnosticInfo_parser: ReaderStateParserLike = {
104
+ init(this: any) {
105
+ this.value = undefined;
106
+ },
107
+ parser: {
108
+ SymbolicId: _diagnosticInfoField("symbolicId", (text) => parseInt(text, 10)),
109
+ NamespaceUri: _diagnosticInfoField("namespaceUri", (text) => parseInt(text, 10)),
110
+ Locale: _diagnosticInfoField("locale", (text) => parseInt(text, 10)),
111
+ LocalizedText: _diagnosticInfoField("localizedText", (text) => parseInt(text, 10)),
112
+ AdditionalInfo: _diagnosticInfoField("additionalInfo", (text) => text),
113
+ InnerStatusCode: {
114
+ ...statusCode_parser,
115
+ finish(this: any) {
116
+ this.parent.value = this.parent.value || {};
117
+ this.parent.value.innerStatusCode = this.value;
118
+ }
119
+ }
120
+ }
121
+ };
122
+ // InnerDiagnosticInfo is a DiagnosticInfo in turn: the parser is registered after the
123
+ // fact so that it can refer to the reader being built.
124
+ diagnosticInfo_parser.parser!.InnerDiagnosticInfo = {
125
+ ...diagnosticInfo_parser,
126
+ finish(this: any) {
127
+ this.parent.value = this.parent.value || {};
128
+ this.parent.value.innerDiagnosticInfo = this.value;
129
+ }
130
+ };
75
131
  const partials = {
76
132
  LocalizedText: localizedText_parser.LocalizedText,
77
133
  QualifiedName: makeQualifiedNameParser((nodeId: string) => coerceNodeId(nodeId)).QualifiedName,
@@ -195,6 +251,10 @@ const partials = {
195
251
  }
196
252
  },
197
253
 
254
+ StatusCode: statusCode_parser,
255
+
256
+ DiagnosticInfo: diagnosticInfo_parser,
257
+
198
258
  // <ExpandedNodeId><Identifier>svr=1;nsu=http://acme.com/UA/;i=42</Identifier></ExpandedNodeId>
199
259
  ExpandedNodeId: <Parser<ExpandedNodeId>>{
200
260
  finish(this: Parser<ExpandedNodeId>) {
@@ -305,6 +365,13 @@ function _makeTypeReader(
305
365
 
306
366
  reader.parser![field.name || ""] = {
307
367
  parser: fieldParser.parser,
368
+ // the field reader borrows the partial's sub-parsers: it must borrow its init
369
+ // too, or the state those sub-parsers write into is never set up.
370
+ init(this: any, elementName: string, attrs: XmlAttributes, parent: IReaderState, engine: Xml2Json) {
371
+ if (fieldParser.init) {
372
+ fieldParser.init.call(this, elementName, attrs, parent, engine);
373
+ }
374
+ },
308
375
  // endElement: fieldReader.endElement,
309
376
  finish(this: any) {
310
377
  const elName = lowerFirstLetter(field.name || "");