node-opcua-address-space 2.175.3 → 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.
- package/dist/source/interfaces/nodeset_loader_options.d.ts +33 -0
- package/dist/source/loader/load_nodeset2.js +91 -12
- package/dist/source/loader/load_nodeset2.js.map +1 -1
- package/dist/source/loader/make_xml_extension_object_parser.js +62 -0
- package/dist/source/loader/make_xml_extension_object_parser.js.map +1 -1
- package/dist/source/session_context.d.ts +90 -4
- package/dist/source/session_context.js +181 -9
- package/dist/source/session_context.js.map +1 -1
- package/dist/src/base_node_impl.js +49 -32
- package/dist/src/base_node_impl.js.map +1 -1
- package/dist/src/base_node_private.js +6 -1
- package/dist/src/base_node_private.js.map +1 -1
- package/dist/src/nodeset_tools/nodeset_to_xml.js +47 -4
- package/dist/src/nodeset_tools/nodeset_to_xml.js.map +1 -1
- package/dist/src/ua_variable_impl_ext_obj.js +6 -1
- package/dist/src/ua_variable_impl_ext_obj.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/distHelpers/mock_session.d.ts +68 -1
- package/distHelpers/mock_session.js +58 -0
- package/distHelpers/mock_session.js.map +1 -1
- package/package.json +37 -37
- package/source/interfaces/nodeset_loader_options.ts +34 -0
- package/source/loader/load_nodeset2.ts +105 -13
- package/source/loader/make_xml_extension_object_parser.ts +67 -0
- package/source/session_context.ts +262 -12
- package/src/base_node_impl.ts +54 -37
- package/src/base_node_private.ts +4 -1
- package/src/nodeset_tools/nodeset_to_xml.ts +50 -4
- package/src/ua_variable_impl_ext_obj.ts +6 -1
|
@@ -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":";;;
|
|
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.
|
|
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.
|
|
25
|
-
"node-opcua-assert": "2.175.
|
|
26
|
-
"node-opcua-basic-types": "2.
|
|
27
|
-
"node-opcua-binary-stream": "2.
|
|
28
|
-
"node-opcua-client-dynamic-extension-object": "2.
|
|
29
|
-
"node-opcua-constants": "2.
|
|
30
|
-
"node-opcua-crypto": "5.
|
|
31
|
-
"node-opcua-data-access": "2.
|
|
32
|
-
"node-opcua-data-model": "2.
|
|
33
|
-
"node-opcua-data-value": "2.
|
|
34
|
-
"node-opcua-date-time": "2.
|
|
35
|
-
"node-opcua-debug": "2.175.
|
|
36
|
-
"node-opcua-enum": "2.
|
|
37
|
-
"node-opcua-extension-object": "2.
|
|
38
|
-
"node-opcua-factory": "2.
|
|
39
|
-
"node-opcua-nodeid": "2.
|
|
40
|
-
"node-opcua-nodeset-ua": "2.
|
|
41
|
-
"node-opcua-numeric-range": "2.
|
|
42
|
-
"node-opcua-object-registry": "2.175.
|
|
43
|
-
"node-opcua-pseudo-session": "2.
|
|
44
|
-
"node-opcua-service-browse": "2.
|
|
45
|
-
"node-opcua-service-call": "2.
|
|
46
|
-
"node-opcua-service-history": "2.
|
|
47
|
-
"node-opcua-service-translate-browse-path": "2.
|
|
48
|
-
"node-opcua-service-write": "2.
|
|
49
|
-
"node-opcua-status-code": "2.
|
|
50
|
-
"node-opcua-types": "2.
|
|
51
|
-
"node-opcua-utils": "2.
|
|
52
|
-
"node-opcua-variant": "2.
|
|
53
|
-
"node-opcua-xml2json": "2.
|
|
24
|
+
"node-opcua-address-space-base": "2.176.0",
|
|
25
|
+
"node-opcua-assert": "2.175.6",
|
|
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
|
+
"node-opcua-crypto": "5.4.0",
|
|
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
|
+
"node-opcua-debug": "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
|
+
"node-opcua-object-registry": "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.
|
|
61
|
-
"node-opcua-leak-detector": "2.175.
|
|
60
|
+
"node-opcua-benchmarker": "2.176.0",
|
|
61
|
+
"node-opcua-leak-detector": "2.175.6",
|
|
62
62
|
"node-opcua-nodesets": "2.175.3",
|
|
63
|
-
"node-opcua-packet-analyzer": "2.
|
|
64
|
-
"node-opcua-schemas": "2.
|
|
65
|
-
"node-opcua-service-filter": "2.
|
|
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": "
|
|
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 };
|
|
@@ -699,10 +784,11 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
|
|
|
699
784
|
valueRank,
|
|
700
785
|
arrayDimensions: valueRank <= 0 ? null : stringToUInt32Array(attrs.ArrayDimensions),
|
|
701
786
|
minimumSamplingInterval: attrs.MinimumSamplingInterval ? parseInt(attrs.MinimumSamplingInterval, 10) : 0,
|
|
702
|
-
historizing:
|
|
787
|
+
historizing: coerceBoolean(attrs.Historizing),
|
|
703
788
|
nodeId,
|
|
704
789
|
accessLevel: accessLevel,
|
|
705
|
-
userAccessLevel:
|
|
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) => {
|
|
@@ -822,8 +909,10 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
|
|
|
822
909
|
valueRank,
|
|
823
910
|
arrayDimensions: valueRank <= 0 ? null : stringToUInt32Array(attrs.ArrayDimensions),
|
|
824
911
|
minimumSamplingInterval: attrs.MinimumSamplingInterval ? parseInt(attrs.MinimumSamplingInterval, 10) : 0,
|
|
912
|
+
// UAVariableType has no Historizing attribute in the XSD: keep it false
|
|
825
913
|
historizing: false,
|
|
826
|
-
nodeId: convertToNodeId(attrs.NodeId) || null
|
|
914
|
+
nodeId: convertToNodeId(attrs.NodeId) || null,
|
|
915
|
+
...convertNodePermissions(attrs)
|
|
827
916
|
};
|
|
828
917
|
this.isDraft = attrs.ReleaseStatus === "Draft";
|
|
829
918
|
this.isDeprecated = attrs.ReleaseStatus === "Deprecated";
|
|
@@ -847,6 +936,7 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
|
|
|
847
936
|
}
|
|
848
937
|
},
|
|
849
938
|
References: references_parser,
|
|
939
|
+
RolePermissions: role_permissions_parser,
|
|
850
940
|
Value: makeVariantReader<ReaderUAVariableTypeL2>(
|
|
851
941
|
(self: ReaderUAVariableTypeL2, data: VariantOptions) => {
|
|
852
942
|
self.parent.obj.value = data;
|
|
@@ -877,7 +967,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
|
|
|
877
967
|
browseName: convertQualifiedName(attrs.BrowseName),
|
|
878
968
|
parentNodeId: attrs.ParentNodeId || null,
|
|
879
969
|
nodeId: convertToNodeId(attrs.NodeId) || null,
|
|
880
|
-
methodDeclarationId: attrs.MethodDeclarationId ? _translateNodeId(attrs.MethodDeclarationId) : null
|
|
970
|
+
methodDeclarationId: attrs.MethodDeclarationId ? _translateNodeId(attrs.MethodDeclarationId) : null,
|
|
971
|
+
...convertNodePermissions(attrs)
|
|
881
972
|
};
|
|
882
973
|
this.isDraft = attrs.ReleaseStatus === "Draft";
|
|
883
974
|
this.isDeprecated = attrs.ReleaseStatus === "Deprecated";
|
|
@@ -896,7 +987,8 @@ function makeNodeSetParserEngine(addressSpace: IAddressSpace, options: NodeSetLo
|
|
|
896
987
|
this.parent.obj.displayName = this.text;
|
|
897
988
|
}
|
|
898
989
|
},
|
|
899
|
-
References: references_parser
|
|
990
|
+
References: references_parser,
|
|
991
|
+
RolePermissions: role_permissions_parser
|
|
900
992
|
}
|
|
901
993
|
};
|
|
902
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 || "");
|