diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index 67d10482b..ee095efda 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -24,6 +24,8 @@ import { ThingAction, ThingEvent, SecurityScheme, + AllOfSecurityScheme, + OneOfSecurityScheme, } from "./thing-description"; import { ThingModel } from "wot-thing-model-types"; @@ -41,6 +43,7 @@ import UriTemplate = require("uritemplate"); import { InteractionOutput, ActionInteractionOutput } from "./interaction-output"; import { ActionElement, + ComboSecurityScheme, EventElement, FormElementEvent, FormElementProperty, @@ -444,15 +447,52 @@ export default class ConsumedThing extends Thing implements IConsumedThing { } getSecuritySchemes(security: Array): Array { - const scs: Array = []; - for (const s of security) { - const ws = this.securityDefinitions[s + ""]; // String vs. string (fix wot-typescript-definitions?) - // also push nosec in case of proxy - if (ws != null) { - scs.push(ws); + const alreadyProcessed = new Map(); + + const visitSchemes = (security: Array) => { + const resolveComboScheme = ( + combo: ComboSecurityScheme, + name: string + ): AllOfSecurityScheme | OneOfSecurityScheme | undefined => { + if (combo.allOf instanceof Array && combo.oneOf === undefined) { + const allOf = visitSchemes(combo.allOf as string[]); + return { + scheme: "combo", + allOf, + }; + } else if (combo.oneOf instanceof Array && combo.allOf === undefined) { + const oneOf = visitSchemes(combo.oneOf as string[]); + return { + scheme: "combo", + oneOf, + }; + } else { + // invalid combination that should be spotted by the TD schema verificator + throw new Error(`Combo SecurityScheme '${name}' is invalid`); + } + }; + const scs: SecurityScheme[] = []; + for (const s of security) { + if (alreadyProcessed.has(s)) { + scs.push(alreadyProcessed.get(s)!); + continue; + } + alreadyProcessed.set(s, null); + + let ws: SecurityScheme | undefined = this.securityDefinitions[s]; + // also push nosec in case of proxy + if (ws?.scheme === "combo") { + ws = resolveComboScheme(ws as ComboSecurityScheme, s); + } + if (ws != null) { + scs.push(ws); + // remember in case we came accross the same again + alreadyProcessed.set(s, ws); + } } - } - return scs; + return scs; + }; + return visitSchemes(security); } ensureClientSecurity(client: ProtocolClient, form: Form | undefined): void { diff --git a/packages/core/src/thing-description.ts b/packages/core/src/thing-description.ts index 61fc2433e..4b69f6471 100644 --- a/packages/core/src/thing-description.ts +++ b/packages/core/src/thing-description.ts @@ -135,7 +135,6 @@ export interface NullSchema extends BaseSchema { } // TODO AutoSecurityScheme -// TODO ComboSecurityScheme export type SecurityType = | NoSecurityScheme | BasicSecurityScheme @@ -143,7 +142,9 @@ export type SecurityType = | BearerSecurityScheme | APIKeySecurityScheme | OAuth2SecurityScheme - | PSKSecurityScheme; + | PSKSecurityScheme + | AllOfSecurityScheme + | OneOfSecurityScheme; export interface SecurityScheme { scheme: string; @@ -180,6 +181,17 @@ export interface PSKSecurityScheme extends SecurityScheme, TDT.PskSecurityScheme export interface OAuth2SecurityScheme extends SecurityScheme, TDT.OAuth2SecurityScheme { scheme: "oauth2"; } +export interface OneOfSecurityScheme extends SecurityScheme { + scheme: "combo"; + oneOf: SecurityScheme[]; + allOf: never; +} +export interface AllOfSecurityScheme extends SecurityScheme { + scheme: "combo"; + allOf: SecurityScheme[]; + oneOf: never; +} +export type ComboSecurityScheme = AllOfSecurityScheme | OneOfSecurityScheme; /** Implements the Thing Property description */ export abstract class ThingProperty extends BaseSchema { diff --git a/packages/core/test/ClientTest.ts b/packages/core/test/ClientTest.ts index 47c6819ad..0ce53283d 100644 --- a/packages/core/test/ClientTest.ts +++ b/packages/core/test/ClientTest.ts @@ -21,13 +21,13 @@ */ import { suite, test } from "@testdeck/mocha"; -import { expect, should, use as chaiUse } from "chai"; +import { expect, should, use as chaiUse, assert } from "chai"; import { Subscription } from "rxjs/Subscription"; import Servient from "../src/servient"; import ConsumedThing from "../src/consumed-thing"; -import { Form, SecurityScheme } from "../src/thing-description"; +import { AllOfSecurityScheme, Form, OneOfSecurityScheme, SecurityScheme } from "../src/thing-description"; import { ProtocolClient, ProtocolClientFactory } from "../src/protocol-interfaces"; import { Content } from "../src/content"; import { ContentSerdes } from "../src/content-serdes"; @@ -805,4 +805,244 @@ class WoTClientTest { // eslint-disable-next-line no-unused-expressions expect(WoTClientTest.servient.hasClientFor(tcf2.scheme)).to.be.not.true; } + + @test "ensure combo security - allOf"() { + const ct = new ConsumedThing(WoTClientTest.servient); + ct.securityDefinitions = { + basic_sc: { + scheme: "basic", + }, + opcua_secure_channel_sc: { + scheme: "opcua-channel-security", + }, + opcua_authetication_sc: { + scheme: "opcua-authentication", + }, + combo_sc: { + scheme: "combo", + allOf: ["opcua_secure_channel_sc", "opcua_authetication_sc"], + }, + }; + ct.security = ["combo_sc"]; + const pc = new TestProtocolClient(); + const form: Form = { + href: "https://example.com/", + }; + ct.ensureClientSecurity(pc, form); + expect(pc.securitySchemes.length).equals(1); + expect(pc.securitySchemes[0].scheme).equals("combo"); + + const comboScheme = pc.securitySchemes[0] as AllOfSecurityScheme; + expect(comboScheme.allOf).instanceOf(Array); + expect(comboScheme.allOf.length).equal(2); + expect(comboScheme.allOf[0].scheme).equals("opcua-channel-security"); + expect(comboScheme.allOf[1].scheme).equals("opcua-authentication"); + } + + @test "ensure combo security - oneOf"() { + const ct = new ConsumedThing(WoTClientTest.servient); + ct.securityDefinitions = { + basic_sc: { + scheme: "basic", + }, + opcua_secure_channel_encrypt_sc: { + scheme: "opcua-channel-security", + mode: "encrypt", + }, + opcua_secure_channel_sign_sc: { + scheme: "opcua-channel-security", + mode: "sign", + }, + opcua_authetication_sc: { + scheme: "opcua-authentication", + }, + comob_opcua_secure_channel: { + scheme: "combo", + oneOf: ["opcua_secure_channel_encrypt_sc", "opcua_secure_channel_sign_sc"], + }, + combo_sc: { + scheme: "combo", + allOf: ["comob_opcua_secure_channel", "opcua_authetication_sc"], + }, + }; + ct.security = ["combo_sc"]; + const pc = new TestProtocolClient(); + const form: Form = { + href: "https://example.com/", + }; + ct.ensureClientSecurity(pc, form); + expect(pc.securitySchemes.length).equals(1); + expect(pc.securitySchemes[0].scheme).equals("combo"); + + const comboScheme = pc.securitySchemes[0] as AllOfSecurityScheme; + + expect(comboScheme.allOf).instanceOf(Array); + expect(comboScheme.allOf.length).equal(2); + expect(comboScheme.allOf[0].scheme).equals("combo"); + expect(comboScheme.allOf[1].scheme).equals("opcua-authentication"); + + // + const firstScheme = comboScheme.allOf[0] as OneOfSecurityScheme; + expect(firstScheme.scheme).equal("combo"); + expect(firstScheme.oneOf).instanceOf(Array); + + expect(firstScheme.oneOf.length).equal(2); + expect(firstScheme.oneOf[0].scheme).equal("opcua-channel-security"); + expect(firstScheme.oneOf[0].scheme).equal("opcua-channel-security"); + } + + @test "ensure combo security in form - allOf"() { + const ct = new ConsumedThing(WoTClientTest.servient); + ct.securityDefinitions = { + basic_sc: { + scheme: "basic", + }, + opcua_secure_channel_sc: { + scheme: "opcua-channel-security", + }, + opcua_authetication_sc: { + scheme: "opcua-authentication", + }, + combo_sc: { + scheme: "combo", + allOf: ["opcua_secure_channel_sc", "opcua_authetication_sc"], + }, + }; + ct.security = "basic"; + const pc = new TestProtocolClient(); + const form: Form = { + href: "https://example.com/", + security: ["combo_sc"], + }; + ct.ensureClientSecurity(pc, form); + expect(pc.securitySchemes.length).equals(1); + const comboScheme = pc.securitySchemes[0] as AllOfSecurityScheme; + + expect(comboScheme.allOf[0].scheme).equals("opcua-channel-security"); + expect(comboScheme.allOf[1].scheme).equals("opcua-authentication"); + } + + @test "ensure no infinite loop with recursive combo security"() { + const ct = new ConsumedThing(WoTClientTest.servient); + ct.securityDefinitions = { + // a badly designed combo that goes into infinite loop + combo_sc: { + scheme: "combo", + allOf: ["combo_sc", "combo_sc"], + }, + }; + ct.security = "basic"; + const pc = new TestProtocolClient(); + const form: Form = { + href: "https://example.com/", + security: ["combo_sc"], + }; + ct.ensureClientSecurity(pc, form); + expect(pc.securitySchemes.length).equals(1); + } + + @test "complex combo security with repeated elements"() { + const ct = new ConsumedThing(WoTClientTest.servient); + ct.securityDefinitions = { + // a badly designed combo that goes into infinite loop + a: { + scheme: "a", + }, + b: { + scheme: "b", + }, + c: { + scheme: "c", + }, + combo_a_and_b: { + scheme: "combo", + allOf: ["a", "b"], + }, + combo_a_and_c: { + scheme: "combo", + allOf: ["a", "c"], + }, + combo_a_or_b: { + scheme: "combo", + oneOf: ["a", "b"], + }, + combo_of_combo: { + scheme: "combo", + oneOf: ["combo_a_and_b", "combo_a_and_c"], + }, + }; + ct.security = ["combo_of_combo"]; + const pc = new TestProtocolClient(); + const form: Form = { + href: "https://example.com/", + }; + ct.ensureClientSecurity(pc, form); + expect(pc.securitySchemes.length).equals(1); + expect(pc.securitySchemes[0].scheme).equal("combo"); + const comboOfCombo = pc.securitySchemes[0] as OneOfSecurityScheme; + expect(comboOfCombo.oneOf).instanceOf(Array); + expect(comboOfCombo.oneOf.length).equal(2); + expect(comboOfCombo.oneOf[0].scheme).equal("combo"); + expect(comboOfCombo.oneOf[1].scheme).equal("combo"); + + const first = comboOfCombo.oneOf[0] as AllOfSecurityScheme; + expect(first.allOf).instanceOf(Array); + expect(first.allOf[0].scheme).equal("a"); + expect(first.allOf[1].scheme).equal("b"); + + const second = comboOfCombo.oneOf[1] as AllOfSecurityScheme; + expect(second.allOf).instanceOf(Array); + expect(second.allOf[0].scheme).equal("a"); + expect(second.allOf[1].scheme).equal("c"); + + // Verfy that a has been processed once - with strict equality + const a1 = first.allOf[0]; + const a2 = second.allOf[0]; + expect(a1).equals(a2); + } + + @test "invalid combo with allOf AND onOf should be detected and throw"() { + const ct = new ConsumedThing(WoTClientTest.servient); + ct.securityDefinitions = { + // a badly designed combo has allOf and oneOf + a: { + scheme: "a", + }, + b: { + scheme: "b", + }, + combo_oneOf_and_allof: { + scheme: "combo", + allOf: ["a", "b"], + oneOf: ["a", "b"], + }, + }; + ct.security = ["combo_oneOf_and_allof"]; + const pc = new TestProtocolClient(); + const form: Form = { + href: "https://example.com/", + }; + assert.throws(() => { + ct.ensureClientSecurity(pc, form); + }, /Combo SecurityScheme 'combo_oneOf_and_allof' is invalid/); + } + + @test "invalid combo with missing allOf and oneOf should be detected and throw"() { + const ct = new ConsumedThing(WoTClientTest.servient); + ct.securityDefinitions = { + // a badly designed combo has NO allOf and NO oneOf + + combo_without_oneOf_and_without_allof: { + scheme: "combo", + }, + }; + ct.security = ["combo_without_oneOf_and_without_allof"]; + const pc = new TestProtocolClient(); + const form: Form = { + href: "https://example.com/", + }; + assert.throws(() => { + ct.ensureClientSecurity(pc, form); + }, /Combo SecurityScheme 'combo_without_oneOf_and_without_allof' is invalid/); + } }