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.
- package/LICENSE +22 -0
- package/README.md +188 -0
- package/dist/audit.d.ts +33 -0
- package/dist/audit.js +28 -0
- package/dist/audit.js.map +1 -0
- package/dist/bind_restriction_methods.d.ts +31 -0
- package/dist/bind_restriction_methods.js +94 -0
- package/dist/bind_restriction_methods.js.map +1 -0
- package/dist/bind_role_methods.d.ts +48 -0
- package/dist/bind_role_methods.js +143 -0
- package/dist/bind_role_methods.js.map +1 -0
- package/dist/bind_user_management.d.ts +58 -0
- package/dist/bind_user_management.js +153 -0
- package/dist/bind_user_management.js.map +1 -0
- package/dist/harden.d.ts +35 -0
- package/dist/harden.js +28 -0
- package/dist/harden.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/install_role_based_security.d.ts +78 -0
- package/dist/install_role_based_security.js +41 -0
- package/dist/install_role_based_security.js.map +1 -0
- package/dist/install_role_set.d.ts +73 -0
- package/dist/install_role_set.js +308 -0
- package/dist/install_role_set.js.map +1 -0
- package/dist/install_user_management.d.ts +68 -0
- package/dist/install_user_management.js +158 -0
- package/dist/install_user_management.js.map +1 -0
- package/dist/role_set_resolver.d.ts +23 -0
- package/dist/role_set_resolver.js +30 -0
- package/dist/role_set_resolver.js.map +1 -0
- package/dist/security_checks.d.ts +25 -0
- package/dist/security_checks.js +41 -0
- package/dist/security_checks.js.map +1 -0
- package/dist/user_management_user_manager.d.ts +37 -0
- package/dist/user_management_user_manager.js +72 -0
- package/dist/user_management_user_manager.js.map +1 -0
- package/dist/variant_args.d.ts +13 -0
- package/dist/variant_args.js +18 -0
- package/dist/variant_args.js.map +1 -0
- package/package.json +54 -0
- package/source/audit.ts +53 -0
- package/source/bind_restriction_methods.ts +117 -0
- package/source/bind_role_methods.ts +196 -0
- package/source/bind_user_management.ts +215 -0
- package/source/harden.ts +46 -0
- package/source/index.ts +48 -0
- package/source/install_role_based_security.ts +130 -0
- package/source/install_role_set.ts +426 -0
- package/source/install_user_management.ts +250 -0
- package/source/role_set_resolver.ts +38 -0
- package/source/security_checks.ts +47 -0
- package/source/user_management_user_manager.ts +104 -0
- package/source/variant_args.ts +22 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-role-set-server
|
|
3
|
+
*
|
|
4
|
+
* Method handlers for the UserManagementType Methods (OPC 10000-18 §5.2):
|
|
5
|
+
* AddUser, ModifyUser, RemoveUser and ChangePassword, backed by an
|
|
6
|
+
* {@link IUserManagementStore}.
|
|
7
|
+
*
|
|
8
|
+
* AddUser / ModifyUser / RemoveUser require the SecurityAdmin Role and an
|
|
9
|
+
* encrypted channel. ChangePassword is callable by the Session user itself but
|
|
10
|
+
* still requires an encrypted channel and a USERNAME user token (§5.2.8).
|
|
11
|
+
*/
|
|
12
|
+
import type { ISessionContext, UAMethod } from "node-opcua-address-space-base";
|
|
13
|
+
import type { NodeId } from "node-opcua-nodeid";
|
|
14
|
+
import type { IUserManagementStore } 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 { UserConfigurationMask, UserNameIdentityToken } from "node-opcua-types";
|
|
18
|
+
import type { Variant } from "node-opcua-variant";
|
|
19
|
+
import { checkEncryptedChannel, checkSecurityAdminAccess } from "./security_checks.js";
|
|
20
|
+
import { asBoolean, asMask, asString } from "./variant_args.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Details of a user-management change for {@link BindUserManagementOptions.onAudit}.
|
|
24
|
+
* Deliberately carries **no password** — only who did what to whom and the result —
|
|
25
|
+
* so an AuditUpdateMethodEventType can be raised without leaking secrets.
|
|
26
|
+
*/
|
|
27
|
+
export interface UserManagementAudit {
|
|
28
|
+
method: "AddUser" | "ModifyUser" | "RemoveUser" | "ChangePassword";
|
|
29
|
+
/** The user the operation targets (the Session user for ChangePassword). */
|
|
30
|
+
targetUserName: string;
|
|
31
|
+
/** The Session user who invoked the Method. */
|
|
32
|
+
callerUserName: string;
|
|
33
|
+
methodNodeId: NodeId;
|
|
34
|
+
statusCode: StatusCode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BindUserManagementOptions {
|
|
38
|
+
store: IUserManagementStore;
|
|
39
|
+
/** Called after every successful mutation so the caller can persist / refresh. */
|
|
40
|
+
onMutation?: () => Promise<void>;
|
|
41
|
+
/** Called after an authorized Method attempt to raise an audit event (no secrets). */
|
|
42
|
+
onAudit?: (audit: UserManagementAudit) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Called after a user is effectively deactivated — disabled via ModifyUser
|
|
45
|
+
* (§5.2.6) or deleted via RemoveUser (§5.2.7) — so the caller can terminate
|
|
46
|
+
* that user's still-active sessions (a disabled user must lose access, not
|
|
47
|
+
* merely be barred from re-authenticating).
|
|
48
|
+
*/
|
|
49
|
+
onUserDeactivated?: (userName: string) => void | Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const isDisabled = (mask: UserConfigurationMask): boolean =>
|
|
53
|
+
(mask & UserConfigurationMask.Disabled) === UserConfigurationMask.Disabled;
|
|
54
|
+
|
|
55
|
+
/** Create an AddUser Method handler (§5.2.5). */
|
|
56
|
+
export function makeAddUserHandler(options: BindUserManagementOptions) {
|
|
57
|
+
const { store, onMutation, onAudit } = options;
|
|
58
|
+
return async function _addUser(
|
|
59
|
+
this: UAMethod,
|
|
60
|
+
inputArguments: Variant[],
|
|
61
|
+
context: ISessionContext
|
|
62
|
+
): Promise<CallMethodResultOptions> {
|
|
63
|
+
const insecure = checkEncryptedChannel(context);
|
|
64
|
+
if (insecure) return insecure;
|
|
65
|
+
const denied = checkSecurityAdminAccess(context);
|
|
66
|
+
if (denied) return denied;
|
|
67
|
+
|
|
68
|
+
const userName = asString(inputArguments[0]);
|
|
69
|
+
const password = asString(inputArguments[1]);
|
|
70
|
+
if (userName === null || password === null) {
|
|
71
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
72
|
+
}
|
|
73
|
+
const userConfiguration = asMask(inputArguments[2]);
|
|
74
|
+
const description = asString(inputArguments[3]) ?? "";
|
|
75
|
+
|
|
76
|
+
const statusCode = store.addUser(userName, password, userConfiguration, description);
|
|
77
|
+
if (statusCode === StatusCodes.Good && onMutation) {
|
|
78
|
+
await onMutation();
|
|
79
|
+
}
|
|
80
|
+
onAudit?.({
|
|
81
|
+
method: "AddUser",
|
|
82
|
+
targetUserName: userName,
|
|
83
|
+
callerUserName: context.getUserName(),
|
|
84
|
+
methodNodeId: this.nodeId,
|
|
85
|
+
statusCode
|
|
86
|
+
});
|
|
87
|
+
return { statusCode };
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Create a ModifyUser Method handler (§5.2.6). */
|
|
92
|
+
export function makeModifyUserHandler(options: BindUserManagementOptions) {
|
|
93
|
+
const { store, onMutation, onAudit, onUserDeactivated } = options;
|
|
94
|
+
return async function _modifyUser(
|
|
95
|
+
this: UAMethod,
|
|
96
|
+
inputArguments: Variant[],
|
|
97
|
+
context: ISessionContext
|
|
98
|
+
): Promise<CallMethodResultOptions> {
|
|
99
|
+
const insecure = checkEncryptedChannel(context);
|
|
100
|
+
if (insecure) return insecure;
|
|
101
|
+
const denied = checkSecurityAdminAccess(context);
|
|
102
|
+
if (denied) return denied;
|
|
103
|
+
|
|
104
|
+
const userName = asString(inputArguments[0]);
|
|
105
|
+
if (userName === null) {
|
|
106
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
107
|
+
}
|
|
108
|
+
const modifyUserConfiguration = asBoolean(inputArguments[3]);
|
|
109
|
+
const userConfiguration = asMask(inputArguments[4]);
|
|
110
|
+
const statusCode = store.modifyUser(
|
|
111
|
+
userName,
|
|
112
|
+
{
|
|
113
|
+
modifyPassword: asBoolean(inputArguments[1]),
|
|
114
|
+
password: asString(inputArguments[2]) ?? "",
|
|
115
|
+
modifyUserConfiguration,
|
|
116
|
+
userConfiguration,
|
|
117
|
+
modifyDescription: asBoolean(inputArguments[5]),
|
|
118
|
+
description: asString(inputArguments[6]) ?? ""
|
|
119
|
+
},
|
|
120
|
+
context.getUserName()
|
|
121
|
+
);
|
|
122
|
+
if (statusCode === StatusCodes.Good) {
|
|
123
|
+
if (onMutation) await onMutation();
|
|
124
|
+
// a freshly disabled user must lose any live session, not just be barred from re-login
|
|
125
|
+
if (modifyUserConfiguration && isDisabled(userConfiguration) && onUserDeactivated) {
|
|
126
|
+
await onUserDeactivated(userName);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
onAudit?.({
|
|
130
|
+
method: "ModifyUser",
|
|
131
|
+
targetUserName: userName,
|
|
132
|
+
callerUserName: context.getUserName(),
|
|
133
|
+
methodNodeId: this.nodeId,
|
|
134
|
+
statusCode
|
|
135
|
+
});
|
|
136
|
+
return { statusCode };
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Create a RemoveUser Method handler (§5.2.7). */
|
|
141
|
+
export function makeRemoveUserHandler(options: BindUserManagementOptions) {
|
|
142
|
+
const { store, onMutation, onAudit, onUserDeactivated } = options;
|
|
143
|
+
return async function _removeUser(
|
|
144
|
+
this: UAMethod,
|
|
145
|
+
inputArguments: Variant[],
|
|
146
|
+
context: ISessionContext
|
|
147
|
+
): Promise<CallMethodResultOptions> {
|
|
148
|
+
const insecure = checkEncryptedChannel(context);
|
|
149
|
+
if (insecure) return insecure;
|
|
150
|
+
const denied = checkSecurityAdminAccess(context);
|
|
151
|
+
if (denied) return denied;
|
|
152
|
+
|
|
153
|
+
const userName = asString(inputArguments[0]);
|
|
154
|
+
if (userName === null) {
|
|
155
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
156
|
+
}
|
|
157
|
+
const statusCode = store.removeUser(userName, context.getUserName());
|
|
158
|
+
if (statusCode === StatusCodes.Good) {
|
|
159
|
+
if (onMutation) await onMutation();
|
|
160
|
+
// a removed user must lose any live session as well (§5.2.7)
|
|
161
|
+
if (onUserDeactivated) await onUserDeactivated(userName);
|
|
162
|
+
}
|
|
163
|
+
onAudit?.({
|
|
164
|
+
method: "RemoveUser",
|
|
165
|
+
targetUserName: userName,
|
|
166
|
+
callerUserName: context.getUserName(),
|
|
167
|
+
methodNodeId: this.nodeId,
|
|
168
|
+
statusCode
|
|
169
|
+
});
|
|
170
|
+
return { statusCode };
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create a ChangePassword Method handler (§5.2.8).
|
|
176
|
+
*
|
|
177
|
+
* Operates on the Session user, requires an encrypted channel and a USERNAME
|
|
178
|
+
* user token (`Bad_InvalidState` otherwise). Does **not** require SecurityAdmin.
|
|
179
|
+
*/
|
|
180
|
+
export function makeChangePasswordHandler(options: BindUserManagementOptions) {
|
|
181
|
+
const { store, onMutation, onAudit } = options;
|
|
182
|
+
return async function _changePassword(
|
|
183
|
+
this: UAMethod,
|
|
184
|
+
inputArguments: Variant[],
|
|
185
|
+
context: ISessionContext
|
|
186
|
+
): Promise<CallMethodResultOptions> {
|
|
187
|
+
const insecure = checkEncryptedChannel(context);
|
|
188
|
+
if (insecure) return insecure;
|
|
189
|
+
|
|
190
|
+
const token = context.session?.userIdentityToken;
|
|
191
|
+
if (!(token instanceof UserNameIdentityToken)) {
|
|
192
|
+
return { statusCode: StatusCodes.BadInvalidState };
|
|
193
|
+
}
|
|
194
|
+
const userName = token.userName ?? "";
|
|
195
|
+
|
|
196
|
+
const oldPassword = asString(inputArguments[0]);
|
|
197
|
+
const newPassword = asString(inputArguments[1]);
|
|
198
|
+
if (oldPassword === null || newPassword === null) {
|
|
199
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const statusCode = store.changePassword(userName, oldPassword, newPassword);
|
|
203
|
+
if (statusCode === StatusCodes.Good && onMutation) {
|
|
204
|
+
await onMutation();
|
|
205
|
+
}
|
|
206
|
+
onAudit?.({
|
|
207
|
+
method: "ChangePassword",
|
|
208
|
+
targetUserName: userName,
|
|
209
|
+
callerUserName: userName,
|
|
210
|
+
methodNodeId: this.nodeId,
|
|
211
|
+
statusCode
|
|
212
|
+
});
|
|
213
|
+
return { statusCode };
|
|
214
|
+
};
|
|
215
|
+
}
|
package/source/harden.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-role-set-server
|
|
3
|
+
*
|
|
4
|
+
* Browse / channel hardening for the security-sensitive RoleSet & User
|
|
5
|
+
* Management nodes (OPC 10000-18 §4.4.1 / §5), built entirely on the two
|
|
6
|
+
* mechanisms the address space already enforces — no custom Browse filter or
|
|
7
|
+
* read hook:
|
|
8
|
+
*
|
|
9
|
+
* - **RolePermissions** — a node that carries RolePermissions grants *nothing*
|
|
10
|
+
* to a Role that is not listed. Restricting a node to `SecurityAdmin` both
|
|
11
|
+
* hides it from a non-admin's Browse (`isBrowseAccessRestricted`) and denies
|
|
12
|
+
* Read/Call (`Bad_UserAccessDenied`).
|
|
13
|
+
* - **AccessRestrictions(EncryptionRequired)** — the node may only be
|
|
14
|
+
* read/called over a `SignAndEncrypt` channel, otherwise the core returns
|
|
15
|
+
* `Bad_SecurityModeInsufficient`, so identity/user data never leaves the
|
|
16
|
+
* server over an unencrypted channel.
|
|
17
|
+
*
|
|
18
|
+
* Both checks run in the core (`ua_variable_impl` / `ua_method_impl`) *before*
|
|
19
|
+
* the bound Method handler, so the per-Method `checkSecurityAdminAccess` /
|
|
20
|
+
* `checkEncryptedChannel` guards become defense-in-depth rather than the only
|
|
21
|
+
* line of defense.
|
|
22
|
+
*/
|
|
23
|
+
import type { BaseNode } from "node-opcua-address-space";
|
|
24
|
+
import { AccessRestrictionsFlag, allPermissions } from "node-opcua-data-model";
|
|
25
|
+
import { WellKnownRoleIds } from "node-opcua-role-set-common";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Restrict a node to the `SecurityAdmin` Role, reachable only over an encrypted
|
|
29
|
+
* channel — used for the admin-only configuration Methods and the sensitive
|
|
30
|
+
* identity/restriction/user Properties.
|
|
31
|
+
*/
|
|
32
|
+
export function hardenAdminOnly(node: BaseNode | null | undefined): void {
|
|
33
|
+
if (!node) return;
|
|
34
|
+
node.setRolePermissions([{ roleId: WellKnownRoleIds.SecurityAdmin, permissions: allPermissions }]);
|
|
35
|
+
node.setAccessRestrictions(AccessRestrictionsFlag.EncryptionRequired);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Require an encrypted channel for a node that must stay visible/callable to
|
|
40
|
+
* ordinary authenticated users (e.g. `ChangePassword`, §5.2.8) — no Role
|
|
41
|
+
* restriction, just `EncryptionRequired`.
|
|
42
|
+
*/
|
|
43
|
+
export function hardenEncryptedOnly(node: BaseNode | null | undefined): void {
|
|
44
|
+
if (!node) return;
|
|
45
|
+
node.setAccessRestrictions(AccessRestrictionsFlag.EncryptionRequired);
|
|
46
|
+
}
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-role-set-server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { type AuditMethodEventFields, raiseAuditMethodEvent } from "./audit.js";
|
|
6
|
+
export {
|
|
7
|
+
type BindRestrictionMethodsOptions,
|
|
8
|
+
makeAddApplicationHandler,
|
|
9
|
+
makeAddEndpointHandler,
|
|
10
|
+
makeRemoveApplicationHandler,
|
|
11
|
+
makeRemoveEndpointHandler
|
|
12
|
+
} from "./bind_restriction_methods.js";
|
|
13
|
+
export {
|
|
14
|
+
type BindRoleMethodsOptions,
|
|
15
|
+
makeAddIdentityHandler,
|
|
16
|
+
makeRemoveIdentityHandler,
|
|
17
|
+
type RoleMappingRuleChangedAudit
|
|
18
|
+
} from "./bind_role_methods.js";
|
|
19
|
+
export {
|
|
20
|
+
type BindUserManagementOptions,
|
|
21
|
+
makeAddUserHandler,
|
|
22
|
+
makeChangePasswordHandler,
|
|
23
|
+
makeModifyUserHandler,
|
|
24
|
+
makeRemoveUserHandler,
|
|
25
|
+
type UserManagementAudit
|
|
26
|
+
} from "./bind_user_management.js";
|
|
27
|
+
export {
|
|
28
|
+
type CreateRoleBasedSecurityOptions,
|
|
29
|
+
createRoleBasedSecurity,
|
|
30
|
+
type InstallRoleBasedSecurityOptions,
|
|
31
|
+
type RoleBasedSecurity,
|
|
32
|
+
type RoleBasedUser
|
|
33
|
+
} from "./install_role_based_security.js";
|
|
34
|
+
export {
|
|
35
|
+
type InstallRoleSetOptions,
|
|
36
|
+
type InstallRoleSetResult,
|
|
37
|
+
type IServerForRoleSet,
|
|
38
|
+
installRoleSet
|
|
39
|
+
} from "./install_role_set.js";
|
|
40
|
+
export {
|
|
41
|
+
type InstallUserManagementOptions,
|
|
42
|
+
type InstallUserManagementResult,
|
|
43
|
+
type IServerForUserManagement,
|
|
44
|
+
installUserManagement
|
|
45
|
+
} from "./install_user_management.js";
|
|
46
|
+
export { RoleSetResolver } from "./role_set_resolver.js";
|
|
47
|
+
export { checkEncryptedChannel, checkSecurityAdminAccess } from "./security_checks.js";
|
|
48
|
+
export { createUserManager, type IManagedUserManager } from "./user_management_user_manager.js";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-role-set-server
|
|
3
|
+
*
|
|
4
|
+
* One-call wiring for role-based security (OPC 10000-18) with a **single source
|
|
5
|
+
* of truth**. It owns one user store and one identity store and exposes them
|
|
6
|
+
* through the server `userManager` bridge — the only integration point the
|
|
7
|
+
* server core uses to resolve a session's Roles (`getUserRoles`) and to back
|
|
8
|
+
* each Role's `Identities` Property (`getIdentitiesForRole`).
|
|
9
|
+
*
|
|
10
|
+
* Because the `userManager` is created **before** the server (it is a
|
|
11
|
+
* constructor option) but the RoleSet / User Management Methods are installed
|
|
12
|
+
* **after** `server.start()`, this comes in two phases:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const security = createRoleBasedSecurity({
|
|
16
|
+
* users: [{ userName: "admin", password: "pw", roles: [WellKnownRoleIds.SecurityAdmin] }]
|
|
17
|
+
* });
|
|
18
|
+
* const server = new OPCUAServer({ ..., userManager: security.userManager });
|
|
19
|
+
* await server.start();
|
|
20
|
+
* await security.install(server, { persistencePath: "./role-set.json" });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Everything (resolution, the `Identities` Property, the AddIdentity/AddUser
|
|
24
|
+
* Methods, persistence) is backed by the same two stores, so the "legacy"
|
|
25
|
+
* (userManager) and "modern" (RoleSet Methods) views can never drift apart.
|
|
26
|
+
*/
|
|
27
|
+
import type { NodeId } from "node-opcua-nodeid";
|
|
28
|
+
import {
|
|
29
|
+
ArchiveStore,
|
|
30
|
+
InMemoryIdentityMappingStore,
|
|
31
|
+
InMemoryUserManagementStore,
|
|
32
|
+
type PasswordPolicy
|
|
33
|
+
} from "node-opcua-role-set-common";
|
|
34
|
+
import { IdentityCriteriaType, IdentityMappingRuleType, UserConfigurationMask } from "node-opcua-types";
|
|
35
|
+
import { type InstallRoleSetResult, type IServerForRoleSet, installRoleSet } from "./install_role_set.js";
|
|
36
|
+
import {
|
|
37
|
+
type InstallUserManagementResult,
|
|
38
|
+
type IServerForUserManagement,
|
|
39
|
+
installUserManagement
|
|
40
|
+
} from "./install_user_management.js";
|
|
41
|
+
import { createUserManager, type IManagedUserManager } from "./user_management_user_manager.js";
|
|
42
|
+
|
|
43
|
+
/** A user to seed, with the well-known/custom Roles it holds. */
|
|
44
|
+
export interface RoleBasedUser {
|
|
45
|
+
userName: string;
|
|
46
|
+
password: string;
|
|
47
|
+
/** Role NodeIds granted to this user (e.g. `WellKnownRoleIds.Operator`). */
|
|
48
|
+
roles?: NodeId[];
|
|
49
|
+
userConfiguration?: UserConfigurationMask;
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CreateRoleBasedSecurityOptions {
|
|
54
|
+
/** Password policy enforced by the user store. */
|
|
55
|
+
policy?: PasswordPolicy;
|
|
56
|
+
/** Users to seed up front (more can be added later via the Methods). */
|
|
57
|
+
users?: RoleBasedUser[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InstallRoleBasedSecurityOptions {
|
|
61
|
+
/** A shared archive coordinator. When omitted, one is built from `persistencePath`. */
|
|
62
|
+
persistence?: ArchiveStore;
|
|
63
|
+
/** Path to a single consolidated archive for roles + users. */
|
|
64
|
+
persistencePath?: string;
|
|
65
|
+
/** Encrypt that archive at rest (AES-256-GCM, key derived from this secret). */
|
|
66
|
+
persistenceSecret?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The shared stores + bridge produced by {@link createRoleBasedSecurity}. */
|
|
70
|
+
export interface RoleBasedSecurity {
|
|
71
|
+
/** The single user store (passwords / user lifecycle). */
|
|
72
|
+
userStore: InMemoryUserManagementStore;
|
|
73
|
+
/** The single identity store (UserName -> Role mappings). */
|
|
74
|
+
identityStore: InMemoryIdentityMappingStore;
|
|
75
|
+
/** The server `userManager` to pass to the `OPCUAServer` constructor. */
|
|
76
|
+
userManager: IManagedUserManager;
|
|
77
|
+
/**
|
|
78
|
+
* Install the RoleSet + User Management Methods on the running server,
|
|
79
|
+
* bound to the same two stores. Call **after** `server.start()`.
|
|
80
|
+
*/
|
|
81
|
+
install(
|
|
82
|
+
server: IServerForRoleSet & IServerForUserManagement,
|
|
83
|
+
options?: InstallRoleBasedSecurityOptions
|
|
84
|
+
): Promise<{ roleSet: InstallRoleSetResult; userManagement: InstallUserManagementResult }>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const userNameRule = (userName: string): IdentityMappingRuleType =>
|
|
88
|
+
new IdentityMappingRuleType({ criteriaType: IdentityCriteriaType.UserName, criteria: userName });
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create the shared stores and the `userManager` bridge, seeding any initial
|
|
92
|
+
* users and their Roles. Pass `userManager` to the `OPCUAServer` constructor,
|
|
93
|
+
* then call {@link RoleBasedSecurity.install} after `server.start()`.
|
|
94
|
+
*/
|
|
95
|
+
export function createRoleBasedSecurity(options?: CreateRoleBasedSecurityOptions): RoleBasedSecurity {
|
|
96
|
+
const userStore = new InMemoryUserManagementStore(options?.policy);
|
|
97
|
+
const identityStore = new InMemoryIdentityMappingStore();
|
|
98
|
+
|
|
99
|
+
for (const user of options?.users ?? []) {
|
|
100
|
+
userStore.addUser(
|
|
101
|
+
user.userName,
|
|
102
|
+
user.password,
|
|
103
|
+
user.userConfiguration ?? UserConfigurationMask.None,
|
|
104
|
+
user.description ?? ""
|
|
105
|
+
);
|
|
106
|
+
for (const role of user.roles ?? []) {
|
|
107
|
+
identityStore.addIdentity(role, userNameRule(user.userName));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const userManager = createUserManager(userStore, identityStore);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
userStore,
|
|
115
|
+
identityStore,
|
|
116
|
+
userManager,
|
|
117
|
+
async install(server, installOptions) {
|
|
118
|
+
// one coordinator shared by both installers (so users + roles land in one file)
|
|
119
|
+
const persistence =
|
|
120
|
+
installOptions?.persistence ??
|
|
121
|
+
(installOptions?.persistencePath
|
|
122
|
+
? new ArchiveStore(installOptions.persistencePath, { secret: installOptions.persistenceSecret })
|
|
123
|
+
: undefined);
|
|
124
|
+
|
|
125
|
+
const roleSet = await installRoleSet(server, { store: identityStore, persistence });
|
|
126
|
+
const userManagement = await installUserManagement(server, { store: userStore, persistence });
|
|
127
|
+
return { roleSet, userManagement };
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|