node-opcua-role-set-server 2.174.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.
Files changed (55) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +188 -0
  3. package/dist/audit.d.ts +33 -0
  4. package/dist/audit.js +28 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/bind_restriction_methods.d.ts +31 -0
  7. package/dist/bind_restriction_methods.js +94 -0
  8. package/dist/bind_restriction_methods.js.map +1 -0
  9. package/dist/bind_role_methods.d.ts +48 -0
  10. package/dist/bind_role_methods.js +143 -0
  11. package/dist/bind_role_methods.js.map +1 -0
  12. package/dist/bind_user_management.d.ts +58 -0
  13. package/dist/bind_user_management.js +153 -0
  14. package/dist/bind_user_management.js.map +1 -0
  15. package/dist/harden.d.ts +35 -0
  16. package/dist/harden.js +28 -0
  17. package/dist/harden.js.map +1 -0
  18. package/dist/index.d.ts +13 -0
  19. package/dist/index.js +35 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/install_role_based_security.d.ts +78 -0
  22. package/dist/install_role_based_security.js +41 -0
  23. package/dist/install_role_based_security.js.map +1 -0
  24. package/dist/install_role_set.d.ts +73 -0
  25. package/dist/install_role_set.js +308 -0
  26. package/dist/install_role_set.js.map +1 -0
  27. package/dist/install_user_management.d.ts +68 -0
  28. package/dist/install_user_management.js +158 -0
  29. package/dist/install_user_management.js.map +1 -0
  30. package/dist/role_set_resolver.d.ts +23 -0
  31. package/dist/role_set_resolver.js +30 -0
  32. package/dist/role_set_resolver.js.map +1 -0
  33. package/dist/security_checks.d.ts +25 -0
  34. package/dist/security_checks.js +41 -0
  35. package/dist/security_checks.js.map +1 -0
  36. package/dist/user_management_user_manager.d.ts +37 -0
  37. package/dist/user_management_user_manager.js +72 -0
  38. package/dist/user_management_user_manager.js.map +1 -0
  39. package/dist/variant_args.d.ts +13 -0
  40. package/dist/variant_args.js +18 -0
  41. package/dist/variant_args.js.map +1 -0
  42. package/package.json +54 -0
  43. package/source/audit.ts +53 -0
  44. package/source/bind_restriction_methods.ts +117 -0
  45. package/source/bind_role_methods.ts +196 -0
  46. package/source/bind_user_management.ts +215 -0
  47. package/source/harden.ts +46 -0
  48. package/source/index.ts +48 -0
  49. package/source/install_role_based_security.ts +130 -0
  50. package/source/install_role_set.ts +426 -0
  51. package/source/install_user_management.ts +250 -0
  52. package/source/role_set_resolver.ts +38 -0
  53. package/source/security_checks.ts +47 -0
  54. package/source/user_management_user_manager.ts +104 -0
  55. package/source/variant_args.ts +22 -0
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkSecurityAdminAccess = checkSecurityAdminAccess;
4
+ exports.checkEncryptedChannel = checkEncryptedChannel;
5
+ const node_opcua_nodeid_1 = require("node-opcua-nodeid");
6
+ const node_opcua_role_set_common_1 = require("node-opcua-role-set-common");
7
+ const node_opcua_status_code_1 = require("node-opcua-status-code");
8
+ const node_opcua_types_1 = require("node-opcua-types");
9
+ /**
10
+ * Verify the calling session holds the SecurityAdmin role.
11
+ * @returns a `Bad_UserAccessDenied` denial, or `null` if authorized.
12
+ */
13
+ function checkSecurityAdminAccess(context) {
14
+ const roles = context.getCurrentUserRoles();
15
+ const hasSecurityAdmin = roles.some((r) => (0, node_opcua_nodeid_1.sameNodeId)(r, node_opcua_role_set_common_1.WellKnownRoleIds.SecurityAdmin));
16
+ if (!hasSecurityAdmin) {
17
+ return { statusCode: node_opcua_status_code_1.StatusCodes.BadUserAccessDenied };
18
+ }
19
+ return null;
20
+ }
21
+ /**
22
+ * Verify the call arrives over an encrypted (SignAndEncrypt) SecureChannel
23
+ * (OPC 10000-18 — `Bad_SecurityModeInsufficient`).
24
+ *
25
+ * When the channel security mode cannot be determined (e.g. an in-process
26
+ * `PseudoSession` with no channel), the check is skipped — such sessions are
27
+ * inherently local/trusted. Remote sessions always expose a channel mode.
28
+ *
29
+ * @returns a denial result, or `null` if the channel is acceptable.
30
+ */
31
+ function checkEncryptedChannel(context) {
32
+ const securityMode = context.session?.channel?.securityMode;
33
+ if (securityMode === undefined) {
34
+ return null; // mode unknown (in-process) → cannot enforce
35
+ }
36
+ if (securityMode !== node_opcua_types_1.MessageSecurityMode.SignAndEncrypt) {
37
+ return { statusCode: node_opcua_status_code_1.StatusCodes.BadSecurityModeInsufficient };
38
+ }
39
+ return null;
40
+ }
41
+ //# sourceMappingURL=security_checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security_checks.js","sourceRoot":"","sources":["../source/security_checks.ts"],"names":[],"mappings":";;AAkBA,4DAOC;AAYD,sDASC;AAtCD,yDAA+C;AAC/C,2EAA8D;AAE9D,mEAAqD;AACrD,uDAAuD;AAEvD;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,OAAwB;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAC5C,MAAM,gBAAgB,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,8BAAU,EAAC,CAAC,EAAE,6CAAgB,CAAC,aAAa,CAAC,CAAC,CAAC;IAC1F,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACpB,OAAO,EAAE,UAAU,EAAE,oCAAW,CAAC,mBAAmB,EAAE,CAAC;IAC3D,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,qBAAqB,CAAC,OAAwB;IAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC;IAC5D,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC,CAAC,6CAA6C;IAC9D,CAAC;IACD,IAAI,YAAY,KAAK,sCAAmB,CAAC,cAAc,EAAE,CAAC;QACtD,OAAO,EAAE,UAAU,EAAE,oCAAW,CAAC,2BAA2B,EAAE,CAAC;IACnE,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { type NodeId, type NodeIdLike } from "node-opcua-nodeid";
2
+ import type { IIdentityMappingStore, IUserManagementStore } from "node-opcua-role-set-common";
3
+ import { type StatusCode } from "node-opcua-status-code";
4
+ import { type IdentityMappingRuleType } from "node-opcua-types";
5
+ /** The subset of the ServerSession that `isValidUser` is bound to (`this`). */
6
+ interface SessionThis {
7
+ getSessionId?(): NodeId;
8
+ /** Set so ActivateSession returns Good_PasswordChangeRequired (node-opcua-server). */
9
+ passwordChangeRequired?: boolean;
10
+ }
11
+ export interface IManagedUserManager {
12
+ /** Validate credentials (sync). Returns true for Good and Good_PasswordChangeRequired. */
13
+ isValidUser(this: SessionThis, userName: string, password: string): boolean;
14
+ /** Resolve the Roles granted to a user (only Anonymous while a change is required). */
15
+ getUserRoles(userName: string): NodeId[];
16
+ /**
17
+ * The IdentityMappingRules configured for a Role. The server core binds each
18
+ * Role's `Identities` Property to this, so it stays in sync with the same
19
+ * identity store that drives {@link getUserRoles} — one source of truth.
20
+ */
21
+ getIdentitiesForRole(role: NodeId): IdentityMappingRuleType[];
22
+ /**
23
+ * StatusCode recorded at activation for the given session — `Good`,
24
+ * `Good_PasswordChangeRequired`, or a `Bad_*` code. Pass the SessionId the
25
+ * client holds (`clientSession.sessionId`). `undefined` if never activated.
26
+ */
27
+ getSessionAuthStatus(sessionId: NodeIdLike): StatusCode | undefined;
28
+ /** StatusCode of the last authentication for each user (convenience). */
29
+ readonly lastAuthStatus: ReadonlyMap<string, StatusCode>;
30
+ }
31
+ /**
32
+ * Create a server `userManager` backed by the given user store. Roles are
33
+ * resolved from `identityStore` (a UserName identity rule per user); while a
34
+ * user must change the password, only the Anonymous Role is granted.
35
+ */
36
+ export declare function createUserManager(userStore: IUserManagementStore, identityStore: IIdentityMappingStore): IManagedUserManager;
37
+ export {};
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createUserManager = createUserManager;
4
+ /**
5
+ * @module node-opcua-role-set-server
6
+ *
7
+ * Bridges an {@link IUserManagementStore} (and an optional role
8
+ * {@link IIdentityMappingStore}) to the OPC UA server `userManager` interface so
9
+ * that CreateSession / ActivateSession authenticate against the managed users.
10
+ *
11
+ * It records the StatusCode of the **last authentication** — in particular
12
+ * `Good_PasswordChangeRequired` for a user that must change the password
13
+ * (OPC 10000-18 §5.2.8). Because the OPC UA stack does not propagate that code
14
+ * into the ActivateSession response, the status is recorded here:
15
+ * - per **session** ({@link IManagedUserManager.getSessionAuthStatus}),
16
+ * keyed by the SessionId the client also holds, so it can be observed in the
17
+ * context of the very session that activated;
18
+ * - per **user** ({@link IManagedUserManager.lastAuthStatus}) for convenience.
19
+ *
20
+ * The server `userManager` invokes `isValidUser` with `this` bound to the
21
+ * ServerSession, which is how the SessionId is captured (non-breaking).
22
+ */
23
+ const node_opcua_constants_1 = require("node-opcua-constants");
24
+ const node_opcua_nodeid_1 = require("node-opcua-nodeid");
25
+ const node_opcua_status_code_1 = require("node-opcua-status-code");
26
+ const node_opcua_types_1 = require("node-opcua-types");
27
+ const has = (mask, bit) => (mask & bit) === bit;
28
+ /**
29
+ * Create a server `userManager` backed by the given user store. Roles are
30
+ * resolved from `identityStore` (a UserName identity rule per user); while a
31
+ * user must change the password, only the Anonymous Role is granted.
32
+ */
33
+ function createUserManager(userStore, identityStore) {
34
+ const lastAuthStatus = new Map();
35
+ const statusBySession = new Map();
36
+ const anonymousOnly = () => [(0, node_opcua_nodeid_1.makeNodeId)(node_opcua_constants_1.WellKnownRoles.Anonymous)];
37
+ return {
38
+ lastAuthStatus,
39
+ getSessionAuthStatus(sessionId) {
40
+ return statusBySession.get(sessionId.toString());
41
+ },
42
+ isValidUser(userName, password) {
43
+ const result = userStore.authenticate(userName, password);
44
+ lastAuthStatus.set(userName, result.statusCode);
45
+ // `this` is the ServerSession; key the status by the SessionId the
46
+ // client also holds, so the outcome can be observed per session.
47
+ const sessionId = this?.getSessionId?.();
48
+ if (sessionId) {
49
+ statusBySession.set(sessionId.toString(), result.statusCode);
50
+ }
51
+ // flag the session so ActivateSession returns Good_PasswordChangeRequired
52
+ if (this) {
53
+ this.passwordChangeRequired = result.statusCode === node_opcua_status_code_1.StatusCodes.GoodPasswordChangeRequired;
54
+ }
55
+ return result.statusCode === node_opcua_status_code_1.StatusCodes.Good || result.statusCode === node_opcua_status_code_1.StatusCodes.GoodPasswordChangeRequired;
56
+ },
57
+ getUserRoles(userName) {
58
+ const user = userStore.getUsers().find((u) => u.userName === userName);
59
+ if (user && has(user.userConfiguration, node_opcua_types_1.UserConfigurationMask.MustChangePassword)) {
60
+ return anonymousOnly();
61
+ }
62
+ const token = new node_opcua_types_1.UserNameIdentityToken({ userName });
63
+ return identityStore.resolveRoles(token);
64
+ },
65
+ getIdentitiesForRole(role) {
66
+ // Backs each Role's `Identities` Property via the server core, from the
67
+ // same store as getUserRoles — so the two can never drift apart.
68
+ return identityStore.getIdentitiesForRole(role);
69
+ }
70
+ };
71
+ }
72
+ //# sourceMappingURL=user_management_user_manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user_management_user_manager.js","sourceRoot":"","sources":["../source/user_management_user_manager.ts"],"names":[],"mappings":";;AA4DA,8CA2CC;AAvGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,+DAAsD;AACtD,yDAA6E;AAE7E,mEAAsE;AACtE,uDAA8G;AA8B9G,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,GAAW,EAAW,EAAE,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,KAAK,GAAG,CAAC;AAEzE;;;;GAIG;AACH,SAAgB,iBAAiB,CAAC,SAA+B,EAAE,aAAoC;IACnG,MAAM,cAAc,GAAG,IAAI,GAAG,EAAsB,CAAC;IACrD,MAAM,eAAe,GAAG,IAAI,GAAG,EAAsB,CAAC;IACtD,MAAM,aAAa,GAAG,GAAa,EAAE,CAAC,CAAC,IAAA,8BAAU,EAAC,qCAAc,CAAC,SAAS,CAAC,CAAC,CAAC;IAE7E,OAAO;QACH,cAAc;QAEd,oBAAoB,CAAC,SAAqB;YACtC,OAAO,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,WAAW,CAAoB,QAAgB,EAAE,QAAgB;YAC7D,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAC1D,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;YAChD,mEAAmE;YACnE,iEAAiE;YACjE,MAAM,SAAS,GAAG,IAAI,EAAE,YAAY,EAAE,EAAE,CAAC;YACzC,IAAI,SAAS,EAAE,CAAC;gBACZ,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;YACjE,CAAC;YACD,0EAA0E;YAC1E,IAAI,IAAI,EAAE,CAAC;gBACP,IAAI,CAAC,sBAAsB,GAAG,MAAM,CAAC,UAAU,KAAK,oCAAW,CAAC,0BAA0B,CAAC;YAC/F,CAAC;YACD,OAAO,MAAM,CAAC,UAAU,KAAK,oCAAW,CAAC,IAAI,IAAI,MAAM,CAAC,UAAU,KAAK,oCAAW,CAAC,0BAA0B,CAAC;QAClH,CAAC;QAED,YAAY,CAAC,QAAgB;YACzB,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;YACvE,IAAI,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,iBAAiB,EAAE,wCAAqB,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAChF,OAAO,aAAa,EAAE,CAAC;YAC3B,CAAC;YACD,MAAM,KAAK,GAAyB,IAAI,wCAAqB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC5E,OAAO,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC7C,CAAC;QAED,oBAAoB,CAAC,IAAY;YAC7B,wEAAwE;YACxE,iEAAiE;YACjE,OAAO,aAAa,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACpD,CAAC;KACJ,CAAC;AACN,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @module node-opcua-role-set-server
3
+ *
4
+ * Small helpers to read Method input arguments out of a `Variant[]` safely.
5
+ */
6
+ import type { UserConfigurationMask } from "node-opcua-types";
7
+ import type { Variant } from "node-opcua-variant";
8
+ /** The string value of an argument, or `null` if absent / not a string. */
9
+ export declare function asString(v: Variant | undefined): string | null;
10
+ /** The boolean value of an argument (missing / non-boolean → `false`). */
11
+ export declare function asBoolean(v: Variant | undefined): boolean;
12
+ /** The UserConfigurationMask value of an argument (missing / non-numeric → `0`). */
13
+ export declare function asMask(v: Variant | undefined): UserConfigurationMask;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asString = asString;
4
+ exports.asBoolean = asBoolean;
5
+ exports.asMask = asMask;
6
+ /** The string value of an argument, or `null` if absent / not a string. */
7
+ function asString(v) {
8
+ return v && typeof v.value === "string" ? v.value : null;
9
+ }
10
+ /** The boolean value of an argument (missing / non-boolean → `false`). */
11
+ function asBoolean(v) {
12
+ return !!v && v.value === true;
13
+ }
14
+ /** The UserConfigurationMask value of an argument (missing / non-numeric → `0`). */
15
+ function asMask(v) {
16
+ return (v && typeof v.value === "number" ? v.value : 0);
17
+ }
18
+ //# sourceMappingURL=variant_args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variant_args.js","sourceRoot":"","sources":["../source/variant_args.ts"],"names":[],"mappings":";;AASA,4BAEC;AAGD,8BAEC;AAGD,wBAEC;AAbD,2EAA2E;AAC3E,SAAgB,QAAQ,CAAC,CAAsB;IAC3C,OAAO,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC;AAED,0EAA0E;AAC1E,SAAgB,SAAS,CAAC,CAAsB;IAC5C,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC;AACnC,CAAC;AAED,oFAAoF;AACpF,SAAgB,MAAM,CAAC,CAAsB;IACzC,OAAO,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAA0B,CAAC;AACrF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "node-opcua-role-set-server",
3
+ "version": "2.174.0",
4
+ "description": "pure nodejs OPCUA SDK - server-side role-set management (OPC 10000-18)",
5
+ "scripts": {
6
+ "build": "tsc -b",
7
+ "lint": "eslint source/**/*.ts",
8
+ "clean": "npx rimraf -g node_modules dist *.tsbuildinfo",
9
+ "test": "mocha",
10
+ "test:check": "tsc --noEmit -p test/tsconfig.json"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "devEngines": {
15
+ "node": ">=18.x",
16
+ "npm": ">=10.x"
17
+ },
18
+ "dependencies": {
19
+ "node-opcua-address-space": "2.174.0",
20
+ "node-opcua-address-space-base": "2.174.0",
21
+ "node-opcua-constants": "2.174.0",
22
+ "node-opcua-data-model": "2.174.0",
23
+ "node-opcua-nodeid": "2.174.0",
24
+ "node-opcua-role-set-common": "2.174.0",
25
+ "node-opcua-service-call": "2.174.0",
26
+ "node-opcua-status-code": "2.173.0",
27
+ "node-opcua-types": "2.174.0",
28
+ "node-opcua-variant": "2.174.0"
29
+ },
30
+ "devDependencies": {
31
+ "node-opcua-leak-detector": "2.172.0",
32
+ "node-opcua-nodesets": "2.174.0"
33
+ },
34
+ "author": "Etienne Rossignon",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git://github.com/node-opcua/node-opcua.git"
39
+ },
40
+ "keywords": [
41
+ "OPCUA",
42
+ "opcua",
43
+ "m2m",
44
+ "iot",
45
+ "opc ua",
46
+ "internet of things"
47
+ ],
48
+ "homepage": "http://node-opcua.github.io/",
49
+ "gitHead": "064029878247adf2fa5d14a8124e1930431e2428",
50
+ "files": [
51
+ "dist",
52
+ "source"
53
+ ]
54
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @module node-opcua-role-set-server
3
+ *
4
+ * Shared helper to raise an audit method event (a subtype of
5
+ * AuditUpdateMethodEventType) on the Server Object.
6
+ */
7
+ import type { UAObject } from "node-opcua-address-space";
8
+ import { NodeId } from "node-opcua-nodeid";
9
+ import type { Variant } from "node-opcua-variant";
10
+ import { VariantArrayType } from "node-opcua-variant";
11
+
12
+ export interface AuditMethodEventFields {
13
+ /** The Node the audited operation acted on (Role / UserManagement Object). */
14
+ sourceNode: NodeId;
15
+ /** A short label, e.g. "Method/AddIdentity". */
16
+ sourceName: string;
17
+ /** The audited Method NodeId (omit for events not raised from a single Method call). */
18
+ methodId?: NodeId;
19
+ /** The Session user who invoked the Method. */
20
+ clientUserId: string;
21
+ /** TRUE when the operation succeeded. */
22
+ status: boolean;
23
+ /** Human-readable message (must NOT contain secrets such as passwords). */
24
+ message: string;
25
+ /**
26
+ * The Method input arguments — include ONLY when they carry no secret
27
+ * (e.g. an IdentityMappingRule). Omit for password-bearing Methods.
28
+ */
29
+ inputArguments?: Variant[];
30
+ }
31
+
32
+ /**
33
+ * Raise `eventType` on the given Server Object with the standard
34
+ * AuditUpdateMethodEventType fields. No-op if there is no Server Object.
35
+ */
36
+ export function raiseAuditMethodEvent(serverObject: UAObject | undefined, eventType: string, fields: AuditMethodEventFields): void {
37
+ if (!serverObject) {
38
+ return;
39
+ }
40
+ serverObject.raiseEvent(eventType, {
41
+ actionTimeStamp: { dataType: "DateTime", value: new Date() },
42
+ status: { dataType: "Boolean", value: fields.status },
43
+ serverId: { dataType: "String", value: "" },
44
+ clientAuditEntryId: { dataType: "String", value: "" },
45
+ clientUserId: { dataType: "String", value: fields.clientUserId },
46
+ sourceNode: { dataType: "NodeId", value: fields.sourceNode },
47
+ sourceName: { dataType: "String", value: fields.sourceName },
48
+ methodId: { dataType: "NodeId", value: fields.methodId ?? NodeId.nullNodeId },
49
+ severity: { dataType: "UInt16", value: 10 },
50
+ message: { dataType: "LocalizedText", value: fields.message },
51
+ inputArguments: { dataType: "Variant", arrayType: VariantArrayType.Array, value: fields.inputArguments ?? [] }
52
+ });
53
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @module node-opcua-role-set-server
3
+ *
4
+ * Method handlers for the RoleType application/endpoint restriction Methods
5
+ * (OPC 10000-18 §4.4.7-10): AddApplication / RemoveApplication / AddEndpoint /
6
+ * RemoveEndpoint, backed by an {@link IRoleRestrictionStore}.
7
+ *
8
+ * All require the SecurityAdmin Role and an encrypted channel, and raise a
9
+ * RoleMappingRuleChangedAuditEventType (§4.5, reusing the same audit hook as the
10
+ * identity Methods).
11
+ */
12
+ import type { ISessionContext, UAMethod } from "node-opcua-address-space-base";
13
+ import type { NodeId } from "node-opcua-nodeid";
14
+ import type { EndpointCriteria, IRoleRestrictionStore } from "node-opcua-role-set-common";
15
+ import type { CallMethodResultOptions } from "node-opcua-service-call";
16
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
17
+ import { EndpointType } from "node-opcua-types";
18
+ import type { Variant } from "node-opcua-variant";
19
+ import type { RoleMappingRuleChangedAudit } from "./bind_role_methods.js";
20
+ import { checkEncryptedChannel, checkSecurityAdminAccess } from "./security_checks.js";
21
+ import { asString } from "./variant_args.js";
22
+
23
+ export interface BindRestrictionMethodsOptions {
24
+ restrictionStore: IRoleRestrictionStore;
25
+ /** Called after every successful mutation so the caller can persist / refresh. */
26
+ onMutation?: () => Promise<void>;
27
+ /** Called after an authorized attempt to raise an audit event. */
28
+ onAudit?: (audit: RoleMappingRuleChangedAudit) => void;
29
+ }
30
+
31
+ function endpointFromArg(v: Variant | undefined): EndpointCriteria | null {
32
+ if (!v || !(v.value instanceof EndpointType)) {
33
+ return null;
34
+ }
35
+ const e = v.value;
36
+ return {
37
+ endpointUrl: e.endpointUrl ?? undefined,
38
+ securityMode: e.securityMode,
39
+ securityPolicyUri: e.securityPolicyUri ?? undefined,
40
+ transportProfileUri: e.transportProfileUri ?? undefined
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Build a handler that, after the SecurityAdmin + encrypted-channel checks,
46
+ * applies `mutate` to the Role's restrictions, persists, and audits.
47
+ */
48
+ function makeRestrictionHandler(
49
+ options: BindRestrictionMethodsOptions,
50
+ method: RoleMappingRuleChangedAudit["method"],
51
+ mutate: (roleNodeId: NodeId, inputArguments: Variant[]) => StatusCode | null
52
+ ) {
53
+ const { onMutation, onAudit } = options;
54
+ return async function (this: UAMethod, inputArguments: Variant[], context: ISessionContext): Promise<CallMethodResultOptions> {
55
+ const insecure = checkEncryptedChannel(context);
56
+ if (insecure) return insecure;
57
+ const denied = checkSecurityAdminAccess(context);
58
+ if (denied) return denied;
59
+
60
+ const roleNode = this.parent;
61
+ if (!roleNode) {
62
+ return { statusCode: StatusCodes.BadInternalError };
63
+ }
64
+ const statusCode = mutate(roleNode.nodeId, inputArguments);
65
+ if (statusCode === null) {
66
+ return { statusCode: StatusCodes.BadInvalidArgument };
67
+ }
68
+ if (statusCode === StatusCodes.Good && onMutation) {
69
+ await onMutation();
70
+ }
71
+ onAudit?.({
72
+ method,
73
+ roleNodeId: roleNode.nodeId,
74
+ methodNodeId: this.nodeId,
75
+ userName: context.getUserName(),
76
+ inputArguments,
77
+ statusCode
78
+ });
79
+ return { statusCode };
80
+ };
81
+ }
82
+
83
+ /** AddApplication (§4.4.7). */
84
+ export function makeAddApplicationHandler(options: BindRestrictionMethodsOptions) {
85
+ return makeRestrictionHandler(options, "AddApplication", (roleNodeId, args) => {
86
+ const uri = asString(args[0]);
87
+ if (uri === null) return null;
88
+ return options.restrictionStore.addApplication(roleNodeId, uri) ? StatusCodes.Good : StatusCodes.BadAlreadyExists;
89
+ });
90
+ }
91
+
92
+ /** RemoveApplication (§4.4.8). */
93
+ export function makeRemoveApplicationHandler(options: BindRestrictionMethodsOptions) {
94
+ return makeRestrictionHandler(options, "RemoveApplication", (roleNodeId, args) => {
95
+ const uri = asString(args[0]);
96
+ if (uri === null) return null;
97
+ return options.restrictionStore.removeApplication(roleNodeId, uri) ? StatusCodes.Good : StatusCodes.BadNotFound;
98
+ });
99
+ }
100
+
101
+ /** AddEndpoint (§4.4.9). */
102
+ export function makeAddEndpointHandler(options: BindRestrictionMethodsOptions) {
103
+ return makeRestrictionHandler(options, "AddEndpoint", (roleNodeId, args) => {
104
+ const endpoint = endpointFromArg(args[0]);
105
+ if (endpoint === null) return null;
106
+ return options.restrictionStore.addEndpoint(roleNodeId, endpoint) ? StatusCodes.Good : StatusCodes.BadAlreadyExists;
107
+ });
108
+ }
109
+
110
+ /** RemoveEndpoint (§4.4.10). */
111
+ export function makeRemoveEndpointHandler(options: BindRestrictionMethodsOptions) {
112
+ return makeRestrictionHandler(options, "RemoveEndpoint", (roleNodeId, args) => {
113
+ const endpoint = endpointFromArg(args[0]);
114
+ if (endpoint === null) return null;
115
+ return options.restrictionStore.removeEndpoint(roleNodeId, endpoint) ? StatusCodes.Good : StatusCodes.BadNotFound;
116
+ });
117
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @module node-opcua-role-set-server
3
+ *
4
+ * Method handlers for RoleType AddIdentity / RemoveIdentity
5
+ * and RoleSetType AddRole / RemoveRole.
6
+ *
7
+ * Each handler follows the OPC UA method binding pattern:
8
+ * - `this` is the UAMethod node
9
+ * - `inputArguments` is Variant[]
10
+ * - `context` is ISessionContext
11
+ * - returns Promise<CallMethodResultOptions>
12
+ */
13
+ import type { ISessionContext, UAMethod } from "node-opcua-address-space-base";
14
+ import type { NodeId } from "node-opcua-nodeid";
15
+ import { sameNodeId } from "node-opcua-nodeid";
16
+ import { type IIdentityMappingStore, WellKnownRoleIds } from "node-opcua-role-set-common";
17
+ import type { CallMethodResultOptions } from "node-opcua-service-call";
18
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
19
+ import { IdentityCriteriaType, IdentityMappingRuleType } from "node-opcua-types";
20
+ import type { Variant } from "node-opcua-variant";
21
+ import { checkEncryptedChannel, checkSecurityAdminAccess } from "./security_checks.js";
22
+
23
+ /**
24
+ * Well-known Roles whose mapping rules a Server shall not allow to be
25
+ * changed (OPC 10000-18 §4.3): Anonymous, AuthenticatedUser and
26
+ * TrustedApplication. (TrustedApplication has no constant in this build and
27
+ * is matched by NodeId when present.)
28
+ */
29
+ function isImmutableRole(roleId: NodeId): boolean {
30
+ return sameNodeId(roleId, WellKnownRoleIds.Anonymous) || sameNodeId(roleId, WellKnownRoleIds.AuthenticatedUser);
31
+ }
32
+
33
+ /**
34
+ * Administrative Roles to which weak (Anonymous / AuthenticatedUser) identity
35
+ * rules must not be added (OPC 10000-18 §4.4.5: a Server should refuse to add
36
+ * an ANONYMOUS_5 rule to Roles with administrator privileges).
37
+ */
38
+ function isPrivilegedRole(roleId: NodeId): boolean {
39
+ return sameNodeId(roleId, WellKnownRoleIds.SecurityAdmin) || sameNodeId(roleId, WellKnownRoleIds.ConfigureAdmin);
40
+ }
41
+
42
+ /** True for weak criteria that must not be granted to administrative Roles. */
43
+ function isWeakCriteria(criteriaType: IdentityCriteriaType): boolean {
44
+ return criteriaType === IdentityCriteriaType.Anonymous || criteriaType === IdentityCriteriaType.AuthenticatedUser;
45
+ }
46
+
47
+ /**
48
+ * Extract the IdentityMappingRuleType from the first input argument.
49
+ */
50
+ function extractIdentityRule(inputArguments: Variant[]): IdentityMappingRuleType | null {
51
+ if (!inputArguments || inputArguments.length < 1) {
52
+ return null;
53
+ }
54
+ const value = inputArguments[0].value;
55
+ if (value instanceof IdentityMappingRuleType) {
56
+ return value;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Details of an identity-mapping change, passed to {@link BindRoleMethodsOptions.onAudit}
63
+ * so the caller can raise a `RoleMappingRuleChangedAuditEventType` (OPC 10000-18 §4.5).
64
+ * Raised for every authorized attempt — both successful updates and refusals
65
+ * (e.g. immutable Role, duplicate) — with the resulting `statusCode`.
66
+ */
67
+ export interface RoleMappingRuleChangedAudit {
68
+ method: "AddIdentity" | "RemoveIdentity" | "AddApplication" | "RemoveApplication" | "AddEndpoint" | "RemoveEndpoint";
69
+ roleNodeId: NodeId;
70
+ methodNodeId: NodeId;
71
+ userName: string;
72
+ inputArguments: Variant[];
73
+ statusCode: StatusCode;
74
+ }
75
+
76
+ export interface BindRoleMethodsOptions {
77
+ store: IIdentityMappingStore;
78
+ /** Called after every mutation (add/remove) so the caller can persist. */
79
+ onMutation?: () => Promise<void>;
80
+ /** Called after an authorized AddIdentity/RemoveIdentity attempt to raise an audit event. */
81
+ onAudit?: (audit: RoleMappingRuleChangedAudit) => void;
82
+ }
83
+
84
+ /**
85
+ * Create an AddIdentity method handler bound to the given store.
86
+ * The handler extracts the role NodeId from its parent Role object.
87
+ */
88
+ export function makeAddIdentityHandler(options: BindRoleMethodsOptions) {
89
+ const { store, onMutation, onAudit } = options;
90
+
91
+ return async function _addIdentity(
92
+ this: UAMethod,
93
+ inputArguments: Variant[],
94
+ context: ISessionContext
95
+ ): Promise<CallMethodResultOptions> {
96
+ // 1. The SecureChannel must be encrypted
97
+ const insecure = checkEncryptedChannel(context);
98
+ if (insecure) return insecure;
99
+
100
+ // 2. Security check
101
+ const denied = checkSecurityAdminAccess(context);
102
+ if (denied) return denied;
103
+
104
+ // 3. Extract the rule from input
105
+ const rule = extractIdentityRule(inputArguments);
106
+ if (!rule) {
107
+ return { statusCode: StatusCodes.BadInvalidArgument };
108
+ }
109
+
110
+ // 4. The parent of this method is the Role object
111
+ const roleNode = this.parent;
112
+ if (!roleNode) {
113
+ return { statusCode: StatusCodes.BadInternalError };
114
+ }
115
+
116
+ // 5-7. Compute the outcome of the authorized rule-change attempt
117
+ let statusCode: StatusCode;
118
+ if (isImmutableRole(roleNode.nodeId)) {
119
+ // well-known immutable Roles cannot be changed (§4.3)
120
+ statusCode = StatusCodes.BadRequestNotAllowed;
121
+ } else if (isWeakCriteria(rule.criteriaType) && isPrivilegedRole(roleNode.nodeId)) {
122
+ // refuse weak (Anonymous/AuthenticatedUser) rules on administrative Roles (§4.4.5)
123
+ statusCode = StatusCodes.BadRequestNotAllowed;
124
+ } else {
125
+ // duplicate rule is reported as Bad_AlreadyExists
126
+ statusCode = store.addIdentity(roleNode.nodeId, rule) ? StatusCodes.Good : StatusCodes.BadAlreadyExists;
127
+ }
128
+
129
+ if (statusCode === StatusCodes.Good && onMutation) {
130
+ await onMutation();
131
+ }
132
+ onAudit?.({
133
+ method: "AddIdentity",
134
+ roleNodeId: roleNode.nodeId,
135
+ methodNodeId: this.nodeId,
136
+ userName: context.getUserName(),
137
+ inputArguments,
138
+ statusCode
139
+ });
140
+ return { statusCode };
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Create a RemoveIdentity method handler bound to the given store.
146
+ */
147
+ export function makeRemoveIdentityHandler(options: BindRoleMethodsOptions) {
148
+ const { store, onMutation, onAudit } = options;
149
+
150
+ return async function _removeIdentity(
151
+ this: UAMethod,
152
+ inputArguments: Variant[],
153
+ context: ISessionContext
154
+ ): Promise<CallMethodResultOptions> {
155
+ // 1. The SecureChannel must be encrypted
156
+ const insecure = checkEncryptedChannel(context);
157
+ if (insecure) return insecure;
158
+
159
+ // 2. Security check
160
+ const denied = checkSecurityAdminAccess(context);
161
+ if (denied) return denied;
162
+
163
+ // 3. Extract the rule from input
164
+ const rule = extractIdentityRule(inputArguments);
165
+ if (!rule) {
166
+ return { statusCode: StatusCodes.BadInvalidArgument };
167
+ }
168
+
169
+ // 4. The parent of this method is the Role object
170
+ const roleNode = this.parent;
171
+ if (!roleNode) {
172
+ return { statusCode: StatusCodes.BadInternalError };
173
+ }
174
+
175
+ // 5-6. Compute the outcome of the authorized rule-change attempt
176
+ let statusCode: StatusCode;
177
+ if (isImmutableRole(roleNode.nodeId)) {
178
+ statusCode = StatusCodes.BadRequestNotAllowed;
179
+ } else {
180
+ statusCode = store.removeIdentity(roleNode.nodeId, rule) ? StatusCodes.Good : StatusCodes.BadNoMatch;
181
+ }
182
+
183
+ if (statusCode === StatusCodes.Good && onMutation) {
184
+ await onMutation();
185
+ }
186
+ onAudit?.({
187
+ method: "RemoveIdentity",
188
+ roleNodeId: roleNode.nodeId,
189
+ methodNodeId: this.nodeId,
190
+ userName: context.getUserName(),
191
+ inputArguments,
192
+ statusCode
193
+ });
194
+ return { statusCode };
195
+ };
196
+ }