Skip to content

Commit 53ab333

Browse files
committed
feat(binding-opcua): add method to reflect on current connected user authenticated and security in demo server
1 parent 8ae1c34 commit 53ab333

2 files changed

Lines changed: 178 additions & 1 deletion

File tree

packages/binding-opcua/test/fixture/basic-opcua-server.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ import {
3434
CallbackT,
3535
CallMethodResultOptions,
3636
WellKnownRoles,
37+
SecurityPolicy,
3738
} from "node-opcua";
38-
import { KeyValuePair, PermissionType } from "node-opcua-types";
39+
import { KeyValuePair, MessageSecurityMode, PermissionType } from "node-opcua-types";
3940
import { createLoggers } from "@node-wot/core";
4041

4142
const { info } = createLoggers("binding-opcua", "basic-opcua-server");
@@ -240,6 +241,52 @@ export async function startServer(): Promise<OPCUAServer> {
240241
});
241242
onlyForAuthenticated.setValueFromSource({ dataType: "String", value: "Secret" });
242243

244+
const whoAmIMethod = namespace.addMethod(addressSpace.rootFolder.objects.server, {
245+
browseName: "WhoAmI",
246+
nodeId: "s=WhoAmI",
247+
inputArguments: [],
248+
outputArguments: [
249+
{
250+
name: "UserName",
251+
description: { text: "The name of the user that called the method" },
252+
dataType: DataType.String,
253+
},
254+
{
255+
name: "UserIdentityTokenType",
256+
description: { text: "The token type of the user that called the method" },
257+
dataType: DataType.String,
258+
},
259+
{
260+
name: "ChannelSecurityMode",
261+
description: { text: "The security mode used on the channel that called the method" },
262+
dataType: DataType.String,
263+
},
264+
{
265+
name: "ChannelSecurityPolicyUri",
266+
description: { text: "The security policy used on the channel that called the method" },
267+
dataType: DataType.String,
268+
},
269+
],
270+
});
271+
whoAmIMethod.bindMethod(
272+
(inputArguments: Variant[], context: ISessionContext, callback: CallbackT<CallMethodResultOptions>) => {
273+
const userIdentityToken = context.session?.userIdentityToken?.constructor.name ?? "Anonymous";
274+
const userName = context.session?.userIdentityToken && (context.session.userIdentityToken as any).userName;
275+
const securityMode =
276+
MessageSecurityMode[context.session?.channel?.securityMode || MessageSecurityMode.None];
277+
const securityPolicy = context.session?.channel?.securityPolicy ?? SecurityPolicy.None;
278+
const callMethodResult = {
279+
statusCode: StatusCodes.Good,
280+
outputArguments: [
281+
{ dataType: DataType.String, value: userName },
282+
{ dataType: DataType.String, value: userIdentityToken },
283+
{ dataType: DataType.String, value: securityMode },
284+
{ dataType: DataType.String, value: securityPolicy },
285+
],
286+
};
287+
callback(null, callMethodResult);
288+
}
289+
);
243290
await server.start();
244291
info(`Server started: ${server.getEndpointUrl()}`);
245292
return server;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/********************************************************************************
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
* Document License (2015-05-13) which is available at
11+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
14+
********************************************************************************/
15+
import { expect } from "chai";
16+
import debug from "debug";
17+
import { OPCUAServer } from "node-opcua";
18+
19+
import {
20+
IBasicSessionCallAsync,
21+
MessageSecurityMode,
22+
ObjectIds,
23+
OPCUAClient,
24+
SecurityPolicy,
25+
UserIdentityInfo,
26+
UserTokenType,
27+
} from "node-opcua-client";
28+
import { startServer } from "./fixture/basic-opcua-server";
29+
30+
interface WhoAmIResult {
31+
userName?: string;
32+
userIdentityTokenType?: string;
33+
securityMode?: string;
34+
securityPolicy?: string;
35+
err?: Error;
36+
}
37+
async function _whoAmI(session: IBasicSessionCallAsync): Promise<WhoAmIResult> {
38+
const result = await session.call({
39+
objectId: ObjectIds.Server,
40+
methodId: "ns=1;s=WhoAmI",
41+
});
42+
debug(result.toString());
43+
const userName = result?.outputArguments?.[0].value as string;
44+
const userIdentityTokenType = result?.outputArguments?.[1].value as string;
45+
const securityMode = result?.outputArguments?.[2].value as string;
46+
const securityPolicy = result?.outputArguments?.[3].value as string;
47+
const retVal = { userName, userIdentityTokenType, securityMode, securityPolicy };
48+
return retVal;
49+
}
50+
51+
async function whoAmI(
52+
endpoint: string,
53+
securityMode: MessageSecurityMode,
54+
securityPolicy: SecurityPolicy,
55+
userIdentity: UserIdentityInfo
56+
): Promise<WhoAmIResult> {
57+
const client = OPCUAClient.create({
58+
endpointMustExist: false,
59+
securityMode,
60+
securityPolicy,
61+
});
62+
try {
63+
await client.connect(endpoint);
64+
const session = await client.createSession(userIdentity);
65+
try {
66+
const result = await _whoAmI(session);
67+
return result;
68+
} finally {
69+
await session.close();
70+
}
71+
} catch (err) {
72+
return { err: err as Error };
73+
} finally {
74+
await client.disconnect();
75+
}
76+
}
77+
78+
describe("WhoAmI Method", function () {
79+
this.timeout(20000);
80+
81+
let opcuaServer: OPCUAServer;
82+
let endpoint: string;
83+
before(async () => {
84+
opcuaServer = await startServer();
85+
endpoint = opcuaServer.getEndpointUrl();
86+
debug(`endpoint = ${endpoint}`);
87+
});
88+
after(async () => {
89+
await opcuaServer.shutdown();
90+
});
91+
92+
it("should return correct info for anonymous session", async () => {
93+
const result = await whoAmI(endpoint, MessageSecurityMode.None, SecurityPolicy.None, {
94+
type: UserTokenType.Anonymous,
95+
});
96+
97+
expect(result.err).eql(undefined);
98+
expect(result.userName).to.eql(null);
99+
expect(result.userIdentityTokenType).to.eql("AnonymousIdentityToken");
100+
expect(result.securityMode).to.eql("None");
101+
expect(result.securityPolicy).to.eql("http://opcfoundation.org/UA/SecurityPolicy#None");
102+
});
103+
104+
it("should return correct info for user/password session", async () => {
105+
const result = await whoAmI(endpoint, MessageSecurityMode.None, SecurityPolicy.None, {
106+
type: UserTokenType.UserName,
107+
userName: "joe",
108+
password: "password_for_joe",
109+
});
110+
111+
expect(result.err).to.be.undefined;
112+
expect(result.userName).to.eql("joe");
113+
expect(result.userIdentityTokenType).to.eql("UserNameIdentityToken");
114+
expect(result.securityMode).to.eql("None");
115+
expect(result.securityPolicy).to.eql("http://opcfoundation.org/UA/SecurityPolicy#None");
116+
});
117+
it("should return correct info for user/password session with Sign security", async () => {
118+
const result = await whoAmI(endpoint, MessageSecurityMode.Sign, SecurityPolicy.Basic256Sha256, {
119+
type: UserTokenType.UserName,
120+
userName: "admin",
121+
password: "password_for_admin",
122+
});
123+
124+
expect(result.err).to.be.undefined;
125+
expect(result.userName).to.eql("admin");
126+
expect(result.userIdentityTokenType).to.eql("UserNameIdentityToken");
127+
expect(result.securityMode).to.eql("Sign");
128+
expect(result.securityPolicy).to.eql("http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");
129+
});
130+
});

0 commit comments

Comments
 (0)