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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-role-set-server
|
|
3
|
+
*
|
|
4
|
+
* IRoleResolver adapter backed by an IIdentityMappingStore, optionally enforcing
|
|
5
|
+
* per-Role application/endpoint restrictions (OPC 10000-18 §4.4.1).
|
|
6
|
+
*/
|
|
7
|
+
import type { NodeId } from "node-opcua-nodeid";
|
|
8
|
+
import type {
|
|
9
|
+
AnyUserIdentityToken,
|
|
10
|
+
IIdentityMappingStore,
|
|
11
|
+
IRoleRestrictionStore,
|
|
12
|
+
ResolutionContext
|
|
13
|
+
} from "node-opcua-role-set-common";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Adapts an {@link IIdentityMappingStore} (and an optional
|
|
17
|
+
* {@link IRoleRestrictionStore}) to the `IRoleResolver` contract so it can be
|
|
18
|
+
* registered on `OPCUAServer.roleResolvers`.
|
|
19
|
+
*
|
|
20
|
+
* A Role is granted when the UserIdentityToken matches **and** — if a
|
|
21
|
+
* restriction store is provided and a resolution context is available — the
|
|
22
|
+
* Session's application/endpoint comply with the Role's restrictions.
|
|
23
|
+
*/
|
|
24
|
+
export class RoleSetResolver {
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly store: IIdentityMappingStore,
|
|
27
|
+
private readonly restrictionStore?: IRoleRestrictionStore
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
public resolveRoles(userIdentityToken: AnyUserIdentityToken, context?: ResolutionContext): NodeId[] {
|
|
31
|
+
const roles = this.store.resolveRoles(userIdentityToken);
|
|
32
|
+
if (!this.restrictionStore || !context) {
|
|
33
|
+
return roles;
|
|
34
|
+
}
|
|
35
|
+
const restrictionStore = this.restrictionStore;
|
|
36
|
+
return roles.filter((roleId) => restrictionStore.complies(roleId, context));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-role-set-server
|
|
3
|
+
*
|
|
4
|
+
* Shared authorization checks for the RoleType and UserManagement Methods
|
|
5
|
+
* (OPC 10000-18 §4.4 / §5.2): all require the SecurityAdmin Role and an
|
|
6
|
+
* encrypted SecureChannel.
|
|
7
|
+
*/
|
|
8
|
+
import type { ISessionContext } from "node-opcua-address-space-base";
|
|
9
|
+
import { sameNodeId } from "node-opcua-nodeid";
|
|
10
|
+
import { WellKnownRoleIds } from "node-opcua-role-set-common";
|
|
11
|
+
import type { CallMethodResultOptions } from "node-opcua-service-call";
|
|
12
|
+
import { StatusCodes } from "node-opcua-status-code";
|
|
13
|
+
import { MessageSecurityMode } from "node-opcua-types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Verify the calling session holds the SecurityAdmin role.
|
|
17
|
+
* @returns a `Bad_UserAccessDenied` denial, or `null` if authorized.
|
|
18
|
+
*/
|
|
19
|
+
export function checkSecurityAdminAccess(context: ISessionContext): CallMethodResultOptions | null {
|
|
20
|
+
const roles = context.getCurrentUserRoles();
|
|
21
|
+
const hasSecurityAdmin = roles.some((r) => sameNodeId(r, WellKnownRoleIds.SecurityAdmin));
|
|
22
|
+
if (!hasSecurityAdmin) {
|
|
23
|
+
return { statusCode: StatusCodes.BadUserAccessDenied };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Verify the call arrives over an encrypted (SignAndEncrypt) SecureChannel
|
|
30
|
+
* (OPC 10000-18 — `Bad_SecurityModeInsufficient`).
|
|
31
|
+
*
|
|
32
|
+
* When the channel security mode cannot be determined (e.g. an in-process
|
|
33
|
+
* `PseudoSession` with no channel), the check is skipped — such sessions are
|
|
34
|
+
* inherently local/trusted. Remote sessions always expose a channel mode.
|
|
35
|
+
*
|
|
36
|
+
* @returns a denial result, or `null` if the channel is acceptable.
|
|
37
|
+
*/
|
|
38
|
+
export function checkEncryptedChannel(context: ISessionContext): CallMethodResultOptions | null {
|
|
39
|
+
const securityMode = context.session?.channel?.securityMode;
|
|
40
|
+
if (securityMode === undefined) {
|
|
41
|
+
return null; // mode unknown (in-process) → cannot enforce
|
|
42
|
+
}
|
|
43
|
+
if (securityMode !== MessageSecurityMode.SignAndEncrypt) {
|
|
44
|
+
return { statusCode: StatusCodes.BadSecurityModeInsufficient };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-role-set-server
|
|
3
|
+
*
|
|
4
|
+
* Bridges an {@link IUserManagementStore} (and an optional role
|
|
5
|
+
* {@link IIdentityMappingStore}) to the OPC UA server `userManager` interface so
|
|
6
|
+
* that CreateSession / ActivateSession authenticate against the managed users.
|
|
7
|
+
*
|
|
8
|
+
* It records the StatusCode of the **last authentication** — in particular
|
|
9
|
+
* `Good_PasswordChangeRequired` for a user that must change the password
|
|
10
|
+
* (OPC 10000-18 §5.2.8). Because the OPC UA stack does not propagate that code
|
|
11
|
+
* into the ActivateSession response, the status is recorded here:
|
|
12
|
+
* - per **session** ({@link IManagedUserManager.getSessionAuthStatus}),
|
|
13
|
+
* keyed by the SessionId the client also holds, so it can be observed in the
|
|
14
|
+
* context of the very session that activated;
|
|
15
|
+
* - per **user** ({@link IManagedUserManager.lastAuthStatus}) for convenience.
|
|
16
|
+
*
|
|
17
|
+
* The server `userManager` invokes `isValidUser` with `this` bound to the
|
|
18
|
+
* ServerSession, which is how the SessionId is captured (non-breaking).
|
|
19
|
+
*/
|
|
20
|
+
import { WellKnownRoles } from "node-opcua-constants";
|
|
21
|
+
import { makeNodeId, type NodeId, type NodeIdLike } from "node-opcua-nodeid";
|
|
22
|
+
import type { AnyUserIdentityToken, IIdentityMappingStore, IUserManagementStore } from "node-opcua-role-set-common";
|
|
23
|
+
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
24
|
+
import { type IdentityMappingRuleType, UserConfigurationMask, UserNameIdentityToken } from "node-opcua-types";
|
|
25
|
+
|
|
26
|
+
/** The subset of the ServerSession that `isValidUser` is bound to (`this`). */
|
|
27
|
+
interface SessionThis {
|
|
28
|
+
getSessionId?(): NodeId;
|
|
29
|
+
/** Set so ActivateSession returns Good_PasswordChangeRequired (node-opcua-server). */
|
|
30
|
+
passwordChangeRequired?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IManagedUserManager {
|
|
34
|
+
/** Validate credentials (sync). Returns true for Good and Good_PasswordChangeRequired. */
|
|
35
|
+
isValidUser(this: SessionThis, userName: string, password: string): boolean;
|
|
36
|
+
/** Resolve the Roles granted to a user (only Anonymous while a change is required). */
|
|
37
|
+
getUserRoles(userName: string): NodeId[];
|
|
38
|
+
/**
|
|
39
|
+
* The IdentityMappingRules configured for a Role. The server core binds each
|
|
40
|
+
* Role's `Identities` Property to this, so it stays in sync with the same
|
|
41
|
+
* identity store that drives {@link getUserRoles} — one source of truth.
|
|
42
|
+
*/
|
|
43
|
+
getIdentitiesForRole(role: NodeId): IdentityMappingRuleType[];
|
|
44
|
+
/**
|
|
45
|
+
* StatusCode recorded at activation for the given session — `Good`,
|
|
46
|
+
* `Good_PasswordChangeRequired`, or a `Bad_*` code. Pass the SessionId the
|
|
47
|
+
* client holds (`clientSession.sessionId`). `undefined` if never activated.
|
|
48
|
+
*/
|
|
49
|
+
getSessionAuthStatus(sessionId: NodeIdLike): StatusCode | undefined;
|
|
50
|
+
/** StatusCode of the last authentication for each user (convenience). */
|
|
51
|
+
readonly lastAuthStatus: ReadonlyMap<string, StatusCode>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const has = (mask: number, bit: number): boolean => (mask & bit) === bit;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a server `userManager` backed by the given user store. Roles are
|
|
58
|
+
* resolved from `identityStore` (a UserName identity rule per user); while a
|
|
59
|
+
* user must change the password, only the Anonymous Role is granted.
|
|
60
|
+
*/
|
|
61
|
+
export function createUserManager(userStore: IUserManagementStore, identityStore: IIdentityMappingStore): IManagedUserManager {
|
|
62
|
+
const lastAuthStatus = new Map<string, StatusCode>();
|
|
63
|
+
const statusBySession = new Map<string, StatusCode>();
|
|
64
|
+
const anonymousOnly = (): NodeId[] => [makeNodeId(WellKnownRoles.Anonymous)];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
lastAuthStatus,
|
|
68
|
+
|
|
69
|
+
getSessionAuthStatus(sessionId: NodeIdLike): StatusCode | undefined {
|
|
70
|
+
return statusBySession.get(sessionId.toString());
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
isValidUser(this: SessionThis, userName: string, password: string): boolean {
|
|
74
|
+
const result = userStore.authenticate(userName, password);
|
|
75
|
+
lastAuthStatus.set(userName, result.statusCode);
|
|
76
|
+
// `this` is the ServerSession; key the status by the SessionId the
|
|
77
|
+
// client also holds, so the outcome can be observed per session.
|
|
78
|
+
const sessionId = this?.getSessionId?.();
|
|
79
|
+
if (sessionId) {
|
|
80
|
+
statusBySession.set(sessionId.toString(), result.statusCode);
|
|
81
|
+
}
|
|
82
|
+
// flag the session so ActivateSession returns Good_PasswordChangeRequired
|
|
83
|
+
if (this) {
|
|
84
|
+
this.passwordChangeRequired = result.statusCode === StatusCodes.GoodPasswordChangeRequired;
|
|
85
|
+
}
|
|
86
|
+
return result.statusCode === StatusCodes.Good || result.statusCode === StatusCodes.GoodPasswordChangeRequired;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
getUserRoles(userName: string): NodeId[] {
|
|
90
|
+
const user = userStore.getUsers().find((u) => u.userName === userName);
|
|
91
|
+
if (user && has(user.userConfiguration, UserConfigurationMask.MustChangePassword)) {
|
|
92
|
+
return anonymousOnly();
|
|
93
|
+
}
|
|
94
|
+
const token: AnyUserIdentityToken = new UserNameIdentityToken({ userName });
|
|
95
|
+
return identityStore.resolveRoles(token);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
getIdentitiesForRole(role: NodeId): IdentityMappingRuleType[] {
|
|
99
|
+
// Backs each Role's `Identities` Property via the server core, from the
|
|
100
|
+
// same store as getUserRoles — so the two can never drift apart.
|
|
101
|
+
return identityStore.getIdentitiesForRole(role);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
|
|
9
|
+
/** The string value of an argument, or `null` if absent / not a string. */
|
|
10
|
+
export function asString(v: Variant | undefined): string | null {
|
|
11
|
+
return v && typeof v.value === "string" ? v.value : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The boolean value of an argument (missing / non-boolean → `false`). */
|
|
15
|
+
export function asBoolean(v: Variant | undefined): boolean {
|
|
16
|
+
return !!v && v.value === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The UserConfigurationMask value of an argument (missing / non-numeric → `0`). */
|
|
20
|
+
export function asMask(v: Variant | undefined): UserConfigurationMask {
|
|
21
|
+
return (v && typeof v.value === "number" ? v.value : 0) as UserConfigurationMask;
|
|
22
|
+
}
|