Skip to content

Commit

Permalink
forbid wildcard in label filters (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eskibear committed Nov 8, 2023
1 parent 582cd1a commit b4455bb
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 53 deletions.
51 changes: 44 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"rollup-plugin-dts": "^5.3.0",
"sinon": "^15.2.0",
"tslib": "^2.6.0",
"typescript": "^5.1.6"
"typescript": "^5.1.6",
"uuid": "^9.0.1"
},
"dependencies": {
"@azure/app-configuration": "^1.4.1",
Expand Down
25 changes: 24 additions & 1 deletion src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A

public async load() {
const keyValues: [key: string, value: unknown][] = [];
const selectors = this.options?.selectors ?? [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];

// validate selectors
const selectors = getValidSelectors(this.options?.selectors);

for (const selector of selectors) {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: selector.keyFilter,
Expand Down Expand Up @@ -105,3 +108,23 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A
return headers;
}
}

function getValidSelectors(selectors?: { keyFilter: string, labelFilter?: string }[]) {
if (!selectors || selectors.length === 0) {
// Default selector: key: *, label: \0
return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];
}
return selectors.map(selectorCandidate => {
const selector = { ...selectorCandidate };
if (!selector.keyFilter) {
throw new Error("Key filter cannot be null or empty.");
}
if (!selector.labelFilter) {
selector.labelFilter = LabelFilter.Null;
}
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
throw new Error("The characters '*' and ',' are not supported in label filters.");
}
return selector;
});
}
21 changes: 20 additions & 1 deletion src/AzureAppConfigurationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,26 @@ export const MaxRetries = 2;
export const MaxRetryDelayInMs = 60000;

export interface AzureAppConfigurationOptions {
selectors?: { keyFilter: string, labelFilter: string }[];
/**
* Specify what key-values to include in the configuration provider. include multiple sets of key-values
*
* @property keyFilter:
* The key filter to apply when querying Azure App Configuration for key-values.
* An asterisk `*` can be added to the end to return all key-values whose key begins with the key filter.
* e.g. key filter `abc*` returns all key-values whose key starts with `abc`.
* A comma `,` can be used to select multiple key-values. Comma separated filters must exactly match a key to select it.
* Using asterisk to select key-values that begin with a key filter while simultaneously using comma separated key filters is not supported.
* E.g. the key filter `abc*,def` is not supported. The key filters `abc*` and `abc,def` are supported.
* For all other cases the characters: asterisk `*`, comma `,`, and backslash `\` are reserved. Reserved characters must be escaped using a backslash (\).
* e.g. the key filter `a\\b\,\*c*` returns all key-values whose key starts with `a\b,*c`.
*
* @property labelFilter:
* The label filter to apply when querying Azure App Configuration for key-values.
* By default, the "null label" will be used, matching key-values without a label.
* The characters asterisk `*` and comma `,` are not supported.
* Backslash `\` character is reserved and must be escaped using another backslash `\`.
*/
selectors?: { keyFilter: string, labelFilter?: string }[];
trimKeyPrefixes?: string[];
clientOptions?: AppConfigurationClientOptions;
keyVaultOptions?: AzureAppConfigurationKeyVaultOptions;
Expand Down
87 changes: 48 additions & 39 deletions test/load.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,30 @@ const {
createMockedConnectionString,
createMockedEnpoint,
createMockedTokenCredential,
createMockedKeyValue,
} = require("./utils/testHelper");

const mockedKVs = [{
value: "red",
key: "app.settings.fontColor",
label: null,
contentType: "",
lastModified: "2023-05-04T04:34:24.000Z",
tags: {},
etag: "210fjkPIWZMjFTi_qyEEmmsJjtUjj0YQl-Y3s1m6GLw",
isReadOnly: false
value: "red",
}, {
value: "40",
key: "app.settings.fontSize",
label: null,
contentType: "",
lastModified: "2023-05-04T04:32:56.000Z",
tags: {},
etag: "GdmsLWq3mFjFodVEXUYRmvFr3l_qRiKAW_KdpFbxZKk",
isReadOnly: false
value: "40",
}, {
value: "TestValue",
key: "TestKey",
label: "Test",
contentType: "",
lastModified: "2023-05-04T04:32:56.000Z",
tags: {},
etag: "GdmsLWq3mFjFodVEXUYRmvFr3l_qRiKAW_KdpFbxZKk",
isReadOnly: false
value: "TestValue",
}, {
key: "TestKey",
label: "Prod",
value: "TestValueForProd",
}, {
value: null,
key: "KeyForNullValue",
label: "",
contentType: "",
lastModified: "2023-05-04T04:32:56.000Z",
tags: {},
etag: "GdmsLWq3mFjFodVEXUYRmvFr3l_qRiKAW_KdpFbxZKk",
isReadOnly: false
value: null,
}, {
value: "",
key: "KeyForEmptyValue",
label: "",
contentType: "",
lastModified: "2023-05-04T04:32:56.000Z",
tags: {},
etag: "GdmsLWq3mFjFodVEXUYRmvFr3l_qRiKAW_KdpFbxZKk",
isReadOnly: false
}];
value: "",
}].map(createMockedKeyValue);

describe("load", function () {
before(() => {
Expand Down Expand Up @@ -118,8 +94,7 @@ describe("load", function () {
expect(settings.get("fontColor")).eq("red");
expect(settings.has("fontSize")).eq(true);
expect(settings.get("fontSize")).eq("40");
expect(settings.has("TestKey")).eq(true);
expect(settings.get("TestKey")).eq("TestValue");
expect(settings.has("TestKey")).eq(false);
});

it("should trim longest key prefix first", async () => {
Expand All @@ -136,8 +111,7 @@ describe("load", function () {
expect(settings.get("fontColor")).eq("red");
expect(settings.has("fontSize")).eq(true);
expect(settings.get("fontSize")).eq("40");
expect(settings.has("Key")).eq(true);
expect(settings.get("Key")).eq("TestValue");
expect(settings.has("TestKey")).eq(false);
});

it("should support null/empty value", async () => {
Expand All @@ -149,4 +123,39 @@ describe("load", function () {
expect(settings.has("KeyForEmptyValue")).eq(true);
expect(settings.get("KeyForEmptyValue")).eq("");
});

it("should not support * or , in label filters", async () => {
const connectionString = createMockedConnectionString();
const loadWithWildcardLabelFilter = load(connectionString, {
selectors: [{
keyFilter: "app.*",
labelFilter: "*"
}]
});
expect(loadWithWildcardLabelFilter).to.eventually.rejected;

const loadWithMultipleLabelFilter = load(connectionString, {
selectors: [{
keyFilter: "app.*",
labelFilter: "labelA,labelB"
}]
});
expect(loadWithMultipleLabelFilter).to.eventually.rejected;
});

it("should override config settings with same key but different label", async () => {
const connectionString = createMockedConnectionString();
const settings = await load(connectionString, {
selectors: [{
keyFilter: "Test*",
labelFilter: "Test"
}, {
keyFilter: "Test*",
labelFilter: "Prod"
}]
});
expect(settings).not.undefined;
expect(settings.has("TestKey")).eq(true);
expect(settings.get("TestKey")).eq("TestValueForProd");
});
})
31 changes: 27 additions & 4 deletions test/utils/testHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@ const sinon = require("sinon");
const { AppConfigurationClient } = require("@azure/app-configuration");
const { ClientSecretCredential } = require("@azure/identity");
const { SecretClient } = require("@azure/keyvault-secrets");
const uuid = require("uuid");

const TEST_CLIENT_ID = "62e76eb5-218e-4f90-8261-000000000000";
const TEST_TENANT_ID = "72f988bf-86f1-41af-91ab-000000000000";
const TEST_CLIENT_SECRET = "Q158Q~2JtUwVbuq0Mzm9ocH2umTB000000000000";

function mockAppConfigurationClientListConfigurationSettings(kvList) {
function* testKvSetGnerator() {
yield* kvList;
function* testKvSetGnerator(kvs) {
yield* kvs;
}
sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake(() => testKvSetGnerator());
sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => {
const keyFilter = listOptions.keyFilter ?? "*";
const labelFilter = listOptions.labelFilter ?? "*";
const kvs = kvList.filter(kv => {
const keyMatched = keyFilter.endsWith("*") ? kv.key.startsWith(keyFilter.slice(0, keyFilter.length - 1)) : kv.key === keyFilter;
const labelMatched = labelFilter.endsWith("*") ? kv.label.startsWith(labelFilter.slice(0, labelFilter.length - 1))
: (labelFilter === "\0" ? kv.label === null : kv.label === labelFilter); // '\0' in labelFilter, null in config setting.
return keyMatched && labelMatched;
})
return testKvSetGnerator(kvs);
});
}

// uriValueList: [["<secretUri>", "value"], ...]
Expand Down Expand Up @@ -74,6 +85,17 @@ const createMockedJsonKeyValue = (key, value) => ({
isReadOnly: false
});

const createMockedKeyValue = (props) => (Object.assign({
value: "TestValue",
key: "TestKey",
label: null,
contentType: "",
lastModified: new Date().toISOString(),
tags: {},
etag: uuid.v4(),
isReadOnly: false
}, props));

module.exports = {
sinon,
mockAppConfigurationClientListConfigurationSettings,
Expand All @@ -84,5 +106,6 @@ module.exports = {
createMockedConnectionString,
createMockedTokenCredential,
createMockedKeyVaultReference,
createMockedJsonKeyValue
createMockedJsonKeyValue,
createMockedKeyValue
}

0 comments on commit b4455bb

Please sign in to comment.