Skip to content

Commit e5bbddc

Browse files
authored
feat(core): add support for combo security #1416 (#1417)
* feat(core): add support for combo security ComboSecurityScheme #1416 * feat(core): ensure combo resolution doesn't go into infinite loop #1416 * 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. * feat(core): add validation for combo security schemes #1416 This commit introduces validation for combo security schemes to ensure that a combo scheme contains either 'allOf' or 'oneOf', but not both. - Throws an error if a combo scheme is invalid. - Adds unit tests to verify the new validation logic.
1 parent 7c1aac0 commit e5bbddc

File tree

3 files changed

+304
-12
lines changed

3 files changed

+304
-12
lines changed

packages/core/src/consumed-thing.ts

Lines changed: 48 additions & 8 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";
@@ -41,6 +43,7 @@ import UriTemplate = require("uritemplate");
4143
import { InteractionOutput, ActionInteractionOutput } from "./interaction-output";
4244
import {
4345
ActionElement,
46+
ComboSecurityScheme,
4447
EventElement,
4548
FormElementEvent,
4649
FormElementProperty,
@@ -444,15 +447,52 @@ export default class ConsumedThing extends Thing implements IConsumedThing {
444447
}
445448

446449
getSecuritySchemes(security: Array<string>): Array<SecurityScheme> {
447-
const scs: Array<SecurityScheme> = [];
448-
for (const s of security) {
449-
const ws = this.securityDefinitions[s + ""]; // String vs. string (fix wot-typescript-definitions?)
450-
// also push nosec in case of proxy
451-
if (ws != null) {
452-
scs.push(ws);
450+
const alreadyProcessed = new Map<string, SecurityScheme | null>();
451+
452+
const visitSchemes = (security: Array<string>) => {
453+
const resolveComboScheme = (
454+
combo: ComboSecurityScheme,
455+
name: string
456+
): AllOfSecurityScheme | OneOfSecurityScheme | undefined => {
457+
if (combo.allOf instanceof Array && combo.oneOf === undefined) {
458+
const allOf = visitSchemes(combo.allOf as string[]);
459+
return <AllOfSecurityScheme>{
460+
scheme: "combo",
461+
allOf,
462+
};
463+
} else if (combo.oneOf instanceof Array && combo.allOf === undefined) {
464+
const oneOf = visitSchemes(combo.oneOf as string[]);
465+
return <OneOfSecurityScheme>{
466+
scheme: "combo",
467+
oneOf,
468+
};
469+
} else {
470+
// invalid combination that should be spotted by the TD schema verificator
471+
throw new Error(`Combo SecurityScheme '${name}' is invalid`);
472+
}
473+
};
474+
const scs: SecurityScheme[] = [];
475+
for (const s of security) {
476+
if (alreadyProcessed.has(s)) {
477+
scs.push(alreadyProcessed.get(s)!);
478+
continue;
479+
}
480+
alreadyProcessed.set(s, null);
481+
482+
let ws: SecurityScheme | undefined = this.securityDefinitions[s];
483+
// also push nosec in case of proxy
484+
if (ws?.scheme === "combo") {
485+
ws = resolveComboScheme(ws as ComboSecurityScheme, s);
486+
}
487+
if (ws != null) {
488+
scs.push(ws);
489+
// remember in case we came accross the same again
490+
alreadyProcessed.set(s, ws);
491+
}
453492
}
454-
}
455-
return scs;
493+
return scs;
494+
};
495+
return visitSchemes(security);
456496
}
457497

458498
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: 242 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
*/
2222

2323
import { suite, test } from "@testdeck/mocha";
24-
import { expect, should, use as chaiUse } from "chai";
24+
import { expect, should, use as chaiUse, assert } from "chai";
2525

2626
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";
@@ -805,4 +805,244 @@ class WoTClientTest {
805805
// eslint-disable-next-line no-unused-expressions
806806
expect(WoTClientTest.servient.hasClientFor(tcf2.scheme)).to.be.not.true;
807807
}
808+
809+
@test "ensure combo security - allOf"() {
810+
const ct = new ConsumedThing(WoTClientTest.servient);
811+
ct.securityDefinitions = {
812+
basic_sc: {
813+
scheme: "basic",
814+
},
815+
opcua_secure_channel_sc: {
816+
scheme: "opcua-channel-security",
817+
},
818+
opcua_authetication_sc: {
819+
scheme: "opcua-authentication",
820+
},
821+
combo_sc: {
822+
scheme: "combo",
823+
allOf: ["opcua_secure_channel_sc", "opcua_authetication_sc"],
824+
},
825+
};
826+
ct.security = ["combo_sc"];
827+
const pc = new TestProtocolClient();
828+
const form: Form = {
829+
href: "https://example.com/",
830+
};
831+
ct.ensureClientSecurity(pc, form);
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");
892+
}
893+
894+
@test "ensure combo security in form - allOf"() {
895+
const ct = new ConsumedThing(WoTClientTest.servient);
896+
ct.securityDefinitions = {
897+
basic_sc: {
898+
scheme: "basic",
899+
},
900+
opcua_secure_channel_sc: {
901+
scheme: "opcua-channel-security",
902+
},
903+
opcua_authetication_sc: {
904+
scheme: "opcua-authentication",
905+
},
906+
combo_sc: {
907+
scheme: "combo",
908+
allOf: ["opcua_secure_channel_sc", "opcua_authetication_sc"],
909+
},
910+
};
911+
ct.security = "basic";
912+
const pc = new TestProtocolClient();
913+
const form: Form = {
914+
href: "https://example.com/",
915+
security: ["combo_sc"],
916+
};
917+
ct.ensureClientSecurity(pc, form);
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");
923+
}
924+
925+
@test "ensure no infinite loop with recursive combo security"() {
926+
const ct = new ConsumedThing(WoTClientTest.servient);
927+
ct.securityDefinitions = {
928+
// a badly designed combo that goes into infinite loop
929+
combo_sc: {
930+
scheme: "combo",
931+
allOf: ["combo_sc", "combo_sc"],
932+
},
933+
};
934+
ct.security = "basic";
935+
const pc = new TestProtocolClient();
936+
const form: Form = {
937+
href: "https://example.com/",
938+
security: ["combo_sc"],
939+
};
940+
ct.ensureClientSecurity(pc, form);
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);
1002+
}
1003+
1004+
@test "invalid combo with allOf AND onOf should be detected and throw"() {
1005+
const ct = new ConsumedThing(WoTClientTest.servient);
1006+
ct.securityDefinitions = {
1007+
// a badly designed combo has allOf and oneOf
1008+
a: {
1009+
scheme: "a",
1010+
},
1011+
b: {
1012+
scheme: "b",
1013+
},
1014+
combo_oneOf_and_allof: {
1015+
scheme: "combo",
1016+
allOf: ["a", "b"],
1017+
oneOf: ["a", "b"],
1018+
},
1019+
};
1020+
ct.security = ["combo_oneOf_and_allof"];
1021+
const pc = new TestProtocolClient();
1022+
const form: Form = {
1023+
href: "https://example.com/",
1024+
};
1025+
assert.throws(() => {
1026+
ct.ensureClientSecurity(pc, form);
1027+
}, /Combo SecurityScheme 'combo_oneOf_and_allof' is invalid/);
1028+
}
1029+
1030+
@test "invalid combo with missing allOf and oneOf should be detected and throw"() {
1031+
const ct = new ConsumedThing(WoTClientTest.servient);
1032+
ct.securityDefinitions = {
1033+
// a badly designed combo has NO allOf and NO oneOf
1034+
1035+
combo_without_oneOf_and_without_allof: {
1036+
scheme: "combo",
1037+
},
1038+
};
1039+
ct.security = ["combo_without_oneOf_and_without_allof"];
1040+
const pc = new TestProtocolClient();
1041+
const form: Form = {
1042+
href: "https://example.com/",
1043+
};
1044+
assert.throws(() => {
1045+
ct.ensureClientSecurity(pc, form);
1046+
}, /Combo SecurityScheme 'combo_without_oneOf_and_without_allof' is invalid/);
1047+
}
8081048
}

0 commit comments

Comments
 (0)