Skip to content

Commit 0bc5c88

Browse files
committed
feat(core): refine combo security scheme implementation #1416
This commit refines the implementation of combo security schemes by introducing explicit AllOfSecurityScheme and OneOfSecurityScheme types. The getSecuritySchemes method in ConsumedThing has been updated to recursively resolve these schemes, and the tests have been expanded to cover nested allOf and oneOf scenarios.
1 parent ab42852 commit 0bc5c88

3 files changed

Lines changed: 178 additions & 25 deletions

File tree

packages/core/src/consumed-thing.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
ThingAction,
2525
ThingEvent,
2626
SecurityScheme,
27+
AllOfSecurityScheme,
28+
OneOfSecurityScheme,
2729
} from "./thing-description";
2830

2931
import { ThingModel } from "wot-thing-model-types";
@@ -445,30 +447,50 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
445447
}
446448

447449
getSecuritySchemes(security: Array<string>): Array<SecurityScheme> {
448-
const visited = new Set<string>();
450+
const alreadyProcessed = new Map<string, SecurityScheme | null>();
449451

450-
const scs: Array<SecurityScheme> = [];
451452
const visitSchemes = (security: Array<string>) => {
453+
const resolveComboScheme = (
454+
combo: ComboSecurityScheme
455+
): AllOfSecurityScheme | OneOfSecurityScheme | undefined => {
456+
if (combo.allOf instanceof Array) {
457+
const allOf = visitSchemes(combo.allOf as string[]);
458+
return <AllOfSecurityScheme>{
459+
scheme: "combo",
460+
allOf,
461+
};
462+
}
463+
if (combo.oneOf instanceof Array) {
464+
const oneOf = visitSchemes(combo.oneOf as string[]);
465+
return <OneOfSecurityScheme>{
466+
scheme: "combo",
467+
oneOf,
468+
};
469+
}
470+
return undefined; // not supported , but handled gracefully
471+
};
472+
const scs: SecurityScheme[] = [];
452473
for (const s of security) {
453-
if (visited.has(s)) {
474+
if (alreadyProcessed.has(s)) {
475+
scs.push(alreadyProcessed.get(s)!);
454476
continue;
455477
}
456-
visited.add(s);
478+
alreadyProcessed.set(s, null);
457479

458-
const ws = this.securityDefinitions[s + ""]; // String vs. string (fix wot-typescript-definitions?)
480+
let ws: SecurityScheme | undefined = this.securityDefinitions[s];
459481
// also push nosec in case of proxy
482+
if (ws?.scheme === "combo") {
483+
ws = resolveComboScheme(ws as ComboSecurityScheme);
484+
}
460485
if (ws != null) {
461-
if (ws.scheme === "combo") {
462-
const combo = ws as ComboSecurityScheme;
463-
visitSchemes(combo.allOf as string[]);
464-
} else {
465-
scs.push(ws);
466-
}
486+
scs.push(ws);
487+
// remember in case we came accross the same again
488+
alreadyProcessed.set(s, ws);
467489
}
468490
}
491+
return scs;
469492
};
470-
visitSchemes(security);
471-
return scs;
493+
return visitSchemes(security);
472494
}
473495

474496
ensureClientSecurity(client: ProtocolClient, form: Form | undefined): void {

packages/core/src/thing-description.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,16 @@ export interface NullSchema extends BaseSchema {
135135
}
136136

137137
// TODO AutoSecurityScheme
138-
// TODO ComboSecurityScheme
139138
export type SecurityType =
140139
| NoSecurityScheme
141140
| BasicSecurityScheme
142141
| DigestSecurityScheme
143142
| BearerSecurityScheme
144143
| APIKeySecurityScheme
145144
| OAuth2SecurityScheme
146-
| PSKSecurityScheme;
145+
| PSKSecurityScheme
146+
| AllOfSecurityScheme
147+
| OneOfSecurityScheme;
147148

148149
export interface SecurityScheme {
149150
scheme: string;
@@ -180,6 +181,17 @@ export interface PSKSecurityScheme extends SecurityScheme, TDT.PskSecurityScheme
180181
export interface OAuth2SecurityScheme extends SecurityScheme, TDT.OAuth2SecurityScheme {
181182
scheme: "oauth2";
182183
}
184+
export interface OneOfSecurityScheme extends SecurityScheme {
185+
scheme: "combo";
186+
oneOf: SecurityScheme[];
187+
allOf: never;
188+
}
189+
export interface AllOfSecurityScheme extends SecurityScheme {
190+
scheme: "combo";
191+
allOf: SecurityScheme[];
192+
oneOf: never;
193+
}
194+
export type ComboSecurityScheme = AllOfSecurityScheme | OneOfSecurityScheme;
183195

184196
/** Implements the Thing Property description */
185197
export abstract class ThingProperty extends BaseSchema {

packages/core/test/ClientTest.ts

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { Subscription } from "rxjs/Subscription";
2727

2828
import Servient from "../src/servient";
2929
import ConsumedThing from "../src/consumed-thing";
30-
import { Form, SecurityScheme } from "../src/thing-description";
30+
import { AllOfSecurityScheme, Form, OneOfSecurityScheme, SecurityScheme } from "../src/thing-description";
3131
import { ProtocolClient, ProtocolClientFactory } from "../src/protocol-interfaces";
3232
import { Content } from "../src/content";
3333
import { ContentSerdes } from "../src/content-serdes";
@@ -806,7 +806,7 @@ class WoTClientTest {
806806
expect(WoTClientTest.servient.hasClientFor(tcf2.scheme)).to.be.not.true;
807807
}
808808

809-
@test "ensure combo security"() {
809+
@test "ensure combo security - allOf"() {
810810
const ct = new ConsumedThing(WoTClientTest.servient);
811811
ct.securityDefinitions = {
812812
basic_sc: {
@@ -829,12 +829,69 @@ class WoTClientTest {
829829
href: "https://example.com/",
830830
};
831831
ct.ensureClientSecurity(pc, form);
832-
expect(pc.securitySchemes.length).equals(2);
833-
expect(pc.securitySchemes[0].scheme).equals("opcua-channel-security");
834-
expect(pc.securitySchemes[1].scheme).equals("opcua-authentication");
832+
expect(pc.securitySchemes.length).equals(1);
833+
expect(pc.securitySchemes[0].scheme).equals("combo");
834+
835+
const comboScheme = pc.securitySchemes[0] as AllOfSecurityScheme;
836+
expect(comboScheme.allOf).instanceOf(Array);
837+
expect(comboScheme.allOf.length).equal(2);
838+
expect(comboScheme.allOf[0].scheme).equals("opcua-channel-security");
839+
expect(comboScheme.allOf[1].scheme).equals("opcua-authentication");
840+
}
841+
842+
@test "ensure combo security - oneOf"() {
843+
const ct = new ConsumedThing(WoTClientTest.servient);
844+
ct.securityDefinitions = {
845+
basic_sc: {
846+
scheme: "basic",
847+
},
848+
opcua_secure_channel_encrypt_sc: {
849+
scheme: "opcua-channel-security",
850+
mode: "encrypt",
851+
},
852+
opcua_secure_channel_sign_sc: {
853+
scheme: "opcua-channel-security",
854+
mode: "sign",
855+
},
856+
opcua_authetication_sc: {
857+
scheme: "opcua-authentication",
858+
},
859+
comob_opcua_secure_channel: {
860+
scheme: "combo",
861+
oneOf: ["opcua_secure_channel_encrypt_sc", "opcua_secure_channel_sign_sc"],
862+
},
863+
combo_sc: {
864+
scheme: "combo",
865+
allOf: ["comob_opcua_secure_channel", "opcua_authetication_sc"],
866+
},
867+
};
868+
ct.security = ["combo_sc"];
869+
const pc = new TestProtocolClient();
870+
const form: Form = {
871+
href: "https://example.com/",
872+
};
873+
ct.ensureClientSecurity(pc, form);
874+
expect(pc.securitySchemes.length).equals(1);
875+
expect(pc.securitySchemes[0].scheme).equals("combo");
876+
877+
const comboScheme = pc.securitySchemes[0] as AllOfSecurityScheme;
878+
879+
expect(comboScheme.allOf).instanceOf(Array);
880+
expect(comboScheme.allOf.length).equal(2);
881+
expect(comboScheme.allOf[0].scheme).equals("combo");
882+
expect(comboScheme.allOf[1].scheme).equals("opcua-authentication");
883+
884+
//
885+
const firstScheme = comboScheme.allOf[0] as OneOfSecurityScheme;
886+
expect(firstScheme.scheme).equal("combo");
887+
expect(firstScheme.oneOf).instanceOf(Array);
888+
889+
expect(firstScheme.oneOf.length).equal(2);
890+
expect(firstScheme.oneOf[0].scheme).equal("opcua-channel-security");
891+
expect(firstScheme.oneOf[0].scheme).equal("opcua-channel-security");
835892
}
836893

837-
@test "ensure combo security in form"() {
894+
@test "ensure combo security in form - allOf"() {
838895
const ct = new ConsumedThing(WoTClientTest.servient);
839896
ct.securityDefinitions = {
840897
basic_sc: {
@@ -858,9 +915,11 @@ class WoTClientTest {
858915
security: ["combo_sc"],
859916
};
860917
ct.ensureClientSecurity(pc, form);
861-
expect(pc.securitySchemes.length).equals(2);
862-
expect(pc.securitySchemes[0].scheme).equals("opcua-channel-security");
863-
expect(pc.securitySchemes[1].scheme).equals("opcua-authentication");
918+
expect(pc.securitySchemes.length).equals(1);
919+
const comboScheme = pc.securitySchemes[0] as AllOfSecurityScheme;
920+
921+
expect(comboScheme.allOf[0].scheme).equals("opcua-channel-security");
922+
expect(comboScheme.allOf[1].scheme).equals("opcua-authentication");
864923
}
865924

866925
@test "ensure no infinite loop with recursive combo security"() {
@@ -879,6 +938,66 @@ class WoTClientTest {
879938
security: ["combo_sc"],
880939
};
881940
ct.ensureClientSecurity(pc, form);
882-
expect(pc.securitySchemes.length).equals(0);
941+
expect(pc.securitySchemes.length).equals(1);
942+
}
943+
944+
@test "complex combo security with repeated elements"() {
945+
const ct = new ConsumedThing(WoTClientTest.servient);
946+
ct.securityDefinitions = {
947+
// a badly designed combo that goes into infinite loop
948+
a: {
949+
scheme: "a",
950+
},
951+
b: {
952+
scheme: "b",
953+
},
954+
c: {
955+
scheme: "c",
956+
},
957+
combo_a_and_b: {
958+
scheme: "combo",
959+
allOf: ["a", "b"],
960+
},
961+
combo_a_and_c: {
962+
scheme: "combo",
963+
allOf: ["a", "c"],
964+
},
965+
combo_a_or_b: {
966+
scheme: "combo",
967+
oneOf: ["a", "b"],
968+
},
969+
combo_of_combo: {
970+
scheme: "combo",
971+
oneOf: ["combo_a_and_b", "combo_a_and_c"],
972+
},
973+
};
974+
ct.security = ["combo_of_combo"];
975+
const pc = new TestProtocolClient();
976+
const form: Form = {
977+
href: "https://example.com/",
978+
};
979+
ct.ensureClientSecurity(pc, form);
980+
expect(pc.securitySchemes.length).equals(1);
981+
expect(pc.securitySchemes[0].scheme).equal("combo");
982+
const comboOfCombo = pc.securitySchemes[0] as OneOfSecurityScheme;
983+
expect(comboOfCombo.oneOf).instanceOf(Array);
984+
expect(comboOfCombo.oneOf.length).equal(2);
985+
expect(comboOfCombo.oneOf[0].scheme).equal("combo");
986+
expect(comboOfCombo.oneOf[1].scheme).equal("combo");
987+
988+
const first = comboOfCombo.oneOf[0] as AllOfSecurityScheme;
989+
expect(first.allOf).instanceOf(Array);
990+
expect(first.allOf[0].scheme).equal("a");
991+
expect(first.allOf[1].scheme).equal("b");
992+
993+
const second = comboOfCombo.oneOf[1] as AllOfSecurityScheme;
994+
expect(second.allOf).instanceOf(Array);
995+
expect(second.allOf[0].scheme).equal("a");
996+
expect(second.allOf[1].scheme).equal("c");
997+
998+
// Verfy that a has been processed once - with strict equality
999+
const a1 = first.allOf[0];
1000+
const a2 = second.allOf[0];
1001+
expect(a1).equals(a2);
8831002
}
8841003
}

0 commit comments

Comments
 (0)