Skip to content

Commit 16f7418

Browse files
[feature] Allow using of the complex attribute names for encryptedNative and protectedNative (#3032)
* Allow using of the complex attribute names for `encryptedNative` and `protectedNative` * Added tests * Fixing * Fixing tests * Added changelog for 7.0.7 * Added support for the complex attribute names for the encrypted and protected native configuration properties: `encryptedNative` and `protectedNative` with arrays * Fixing tests * Allow that some passwords are not defined by encryption
1 parent e73202c commit 16f7418

File tree

8 files changed

+287
-62
lines changed

8 files changed

+287
-62
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
## __WORK IN PROGRESS__ - Lucy
88
* (@foxriver76) fixed the edge-case problem on Windows (if adapter calls `readDir` on single file)
99
* (@foxriver76) fixed setting negative numbers via `state set` cli command
10-
* (@GermanBluefox) corrected typing for `checkPasswordAsync` command and added caching of multilingual names
10+
* (@GermanBluefox) corrected typing for `checkPasswordAsync` command and added caching of mulit-languages names
11+
* (@GermanBluefox) Added support for the complex attribute names for the encrypted and protected native configuration properties: `encryptedNative` and `protectedNative`
1112
* (@GermanBluefox) Improvement of `adapter.findForeignObject` (typing and optional parameters fixed)
1213
* (@GermanBluefox) Improvement of JSON schema for `io-package.json` and type definitions
1314

packages/adapter/src/lib/_Types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ export interface InternalUpdateConfigOptions {
347347
newConfig: Record<string, any>;
348348
}
349349

350-
export type GetEncryptedConfigCallback = (error: Error | null | undefined, result?: string) => void;
350+
export type GetEncryptedConfigCallback = (error: Error | null | undefined, result?: string | string[]) => void;
351351

352352
export interface InternalGetEncryptedConfigOptions {
353353
attribute: string;

packages/adapter/src/lib/adapter/adapter.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import {
3333
isMessageboxSupported,
3434
listInstalledNodeModules,
3535
requestModuleNameByUrl,
36+
deleteObjectAttribute,
37+
setObjectAttribute,
38+
getObjectAttribute,
3639
} from '@/lib/adapter/utils.js';
3740

3841
import type { Client as StatesInRedisClient } from '@iobroker/db-states-redis';
@@ -2583,7 +2586,7 @@ export class AdapterClass extends EventEmitter {
25832586

25842587
obj.native = mergedConfig;
25852588

2586-
return this.setForeignObjectAsync(configObjId, obj);
2589+
return this.setForeignObject(configObjId, obj);
25872590
}
25882591

25892592
/**
@@ -2607,7 +2610,10 @@ export class AdapterClass extends EventEmitter {
26072610
return this.setForeignObjectAsync(configObjId, obj);
26082611
}
26092612

2610-
async getEncryptedConfig(attribute: string, callback?: GetEncryptedConfigCallback): Promise<string | void>;
2613+
async getEncryptedConfig(
2614+
attribute: string,
2615+
callback?: GetEncryptedConfigCallback,
2616+
): Promise<string | string[] | void>;
26112617

26122618
/**
26132619
* Reads the encrypted parameter from config.
@@ -2617,22 +2623,32 @@ export class AdapterClass extends EventEmitter {
26172623
* @param attribute - attribute name in native configuration part
26182624
* @param [callback] - optional callback
26192625
*/
2620-
getEncryptedConfig(attribute: unknown, callback: unknown): Promise<string | void> {
2626+
getEncryptedConfig(attribute: unknown, callback: unknown): Promise<string | string[] | void> {
26212627
Validator.assertString(attribute, 'attribute');
26222628
Validator.assertOptionalCallback(callback, 'callback');
26232629

26242630
return this._getEncryptedConfig({ attribute, callback });
26252631
}
26262632

2627-
private async _getEncryptedConfig(options: InternalGetEncryptedConfigOptions): Promise<string | void> {
2633+
private async _getEncryptedConfig(options: InternalGetEncryptedConfigOptions): Promise<string | string[] | void> {
26282634
const { attribute, callback } = options;
26292635

2630-
const value = (this.config as InternalAdapterConfig)[attribute];
2636+
const value = getObjectAttribute(this.config, attribute);
26312637

2632-
if (typeof value === 'string') {
2638+
if (Array.isArray(value)) {
2639+
const secret = await this.getSystemSecret();
2640+
const result: string[] = [];
2641+
for (let i = 0; i < value.length; i++) {
2642+
if (typeof value[i] === 'string') {
2643+
result[i] = tools.decrypt(secret, value[i]);
2644+
}
2645+
}
2646+
return tools.maybeCallbackWithError(callback, null, result);
2647+
} else if (typeof value === 'string') {
26332648
const secret = await this.getSystemSecret();
26342649
return tools.maybeCallbackWithError(callback, null, tools.decrypt(secret, value));
26352650
}
2651+
26362652
return tools.maybeCallbackWithError(callback, `Attribute "${attribute}" not found`);
26372653
}
26382654

@@ -3466,7 +3482,7 @@ export class AdapterClass extends EventEmitter {
34663482

34673483
// check that alias is valid if given
34683484
if (obj.common && 'alias' in obj.common && obj.common.alias.id) {
3469-
// if alias is object validate read and write
3485+
// if alias is object, validate read and write
34703486
if (typeof obj.common.alias.id === 'object') {
34713487
try {
34723488
this._utils.validateId(obj.common.alias.id.write, true, null);
@@ -4431,7 +4447,7 @@ export class AdapterClass extends EventEmitter {
44314447
this.name !== id.split('.')[2]
44324448
) {
44334449
for (const attr of obj.protectedNative) {
4434-
delete obj.native[attr];
4450+
deleteObjectAttribute(obj.native, attr);
44354451
}
44364452
}
44374453
}
@@ -4600,7 +4616,7 @@ export class AdapterClass extends EventEmitter {
46004616
this.name !== obj._id.split('.')[2]
46014617
) {
46024618
for (const attr of obj.protectedNative) {
4603-
delete obj.native[attr];
4619+
deleteObjectAttribute(obj.native, attr);
46044620
}
46054621
}
46064622

@@ -11359,7 +11375,7 @@ export class AdapterClass extends EventEmitter {
1135911375
this.name !== obj._id.split('.')[2]
1136011376
) {
1136111377
for (const attr of obj.protectedNative) {
11362-
delete obj.native[attr];
11378+
deleteObjectAttribute(obj.native, attr);
1136311379
}
1136411380
}
1136511381

@@ -11457,7 +11473,7 @@ export class AdapterClass extends EventEmitter {
1145711473
/**
1145811474
* Initialize the adapter
1145911475
*
11460-
* @param adapterConfig the AdapterOptions or the InstanceObject, is null/undefined if it is install process
11476+
* @param adapterConfig the AdapterOptions or the InstanceObject, is null/undefined if it is an installation process
1146111477
*/
1146211478
private async _initAdapter(adapterConfig?: AdapterOptions | ioBroker.InstanceObject | null): Promise<void> {
1146311479
await this._initLogging();
@@ -11535,7 +11551,7 @@ export class AdapterClass extends EventEmitter {
1153511551
instance = 0;
1153611552
adapterConfig = adapterConfig || {
1153711553
// @ts-expect-error protectedNative exists on instance objects
11538-
common: { mode: 'once', name: name, protectedNative: [] },
11554+
common: { mode: 'once', name, protectedNative: [] },
1153911555
native: {},
1154011556
};
1154111557
}
@@ -11650,8 +11666,7 @@ export class AdapterClass extends EventEmitter {
1165011666
if (typeof this.config[attr] === 'string') {
1165111667
promises.push(
1165211668
this.getEncryptedConfig(attr)
11653-
// @ts-expect-error
11654-
.then(decryptedValue => (this.config[attr] = decryptedValue))
11669+
.then(decryptedValue => setObjectAttribute(this.config, attr, decryptedValue))
1165511670
.catch(e =>
1165611671
this._logger.error(
1165711672
`${this.namespaceLog} Can not decrypt attribute ${attr}: ${e.message}`,

packages/adapter/src/lib/adapter/utils.ts

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,15 @@ export function encryptArray(options: EncryptArrayOptions): void {
6464
const { secret, obj, keys } = options;
6565

6666
for (const attr of keys) {
67-
const val = obj[attr];
68-
if (typeof val === 'string') {
69-
obj[attr] = encrypt(secret, val);
67+
const val = getObjectAttribute(obj, attr);
68+
if (Array.isArray(val)) {
69+
const encrypted: string[] = [];
70+
for (let i = 0; i < val.length; i++) {
71+
encrypted[i] = typeof val[i] === 'string' ? encrypt(secret, val[i]) : val[i];
72+
}
73+
setObjectAttribute(obj, attr, encrypted);
74+
} else if (typeof val === 'string') {
75+
setObjectAttribute(obj, attr, encrypt(secret, val));
7076
}
7177
}
7278
}
@@ -80,9 +86,15 @@ export function decryptArray(options: EncryptArrayOptions): void {
8086
const { secret, obj, keys } = options;
8187

8288
for (const attr of keys) {
83-
const val = obj[attr];
84-
if (typeof val === 'string') {
85-
obj[attr] = decrypt(secret, val);
89+
const val = getObjectAttribute(obj, attr);
90+
if (Array.isArray(val)) {
91+
const decrypted: string[] = [];
92+
for (let i = 0; i < val.length; i++) {
93+
decrypted[i] = typeof val[i] === 'string' ? decrypt(secret, val[i]) : val[i];
94+
}
95+
setObjectAttribute(obj, attr, decrypted);
96+
} else if (typeof val === 'string') {
97+
setObjectAttribute(obj, attr, decrypt(secret, val));
8698
}
8799
}
88100
}
@@ -149,3 +161,129 @@ export async function requestModuleNameByUrl(url: string): Promise<string> {
149161

150162
return res.stdout.trim();
151163
}
164+
165+
/**
166+
* Get attribute of an object with complex names
167+
*
168+
* @param obj - object to get the attribute from
169+
* @param attrParts - attribute parts
170+
* @param index - index of attribute part
171+
*/
172+
function _getObjectAttribute(obj: Record<string, any>, attrParts: string[], index: number): any {
173+
if (index === attrParts.length - 1) {
174+
return obj[attrParts[index]];
175+
}
176+
if (!obj[attrParts[index]] || typeof obj[attrParts[index]] !== 'object') {
177+
return;
178+
}
179+
if (Array.isArray(obj[attrParts[index]])) {
180+
const result: any = [];
181+
for (let i = 0; i < obj[attrParts[index]].length; i++) {
182+
result.push(_getObjectAttribute(obj[attrParts[index]][i], attrParts, index + 1));
183+
}
184+
return result;
185+
}
186+
187+
return _getObjectAttribute(obj[attrParts[index]], attrParts, index + 1);
188+
}
189+
190+
/**
191+
* Get attribute of an object with complex or simple names
192+
*
193+
* @param obj - object to get the attribute from
194+
* @param attr - attribute name, can be complex like `attr1.attr2.attr3`
195+
* @return could be a value or an array
196+
*/
197+
export function getObjectAttribute(obj: Record<string, any>, attr: string): any {
198+
// Optimization for 98% of the cases
199+
if (!attr.includes('.')) {
200+
return obj[attr];
201+
}
202+
return _getObjectAttribute(obj, attr.split('.'), 0);
203+
}
204+
205+
/**
206+
* Set attribute in an object with complex names
207+
*
208+
* @param obj - object to get the attribute from
209+
* @param value - value to set (Could be an array)
210+
* @param attrParts - attribute parts
211+
* @param index - index of attribute part
212+
*/
213+
function _setObjectAttribute(obj: Record<string, any>, value: any, attrParts: string[], index: number): any {
214+
if (index === attrParts.length - 1) {
215+
obj[attrParts[index]] = value;
216+
return;
217+
}
218+
if (!obj[attrParts[index]] || typeof obj[attrParts[index]] !== 'object') {
219+
return;
220+
}
221+
if (Array.isArray(obj[attrParts[index]])) {
222+
if (!Array.isArray(value)) {
223+
throw new Error('Value is not an array');
224+
}
225+
for (let i = 0; i < obj[attrParts[index]].length; i++) {
226+
_setObjectAttribute(obj[attrParts[index]][i], value[i], attrParts, index + 1);
227+
}
228+
return;
229+
}
230+
231+
_setObjectAttribute(obj[attrParts[index]], value, attrParts, index + 1);
232+
}
233+
234+
/**
235+
* Set attribute in an object with complex or simple names
236+
*
237+
* @param obj - object to get the attribute from
238+
* @param attr - attribute name, can be complex like `attr1.attr2.attr3`
239+
* @param value - value to set (could be a value or an array)
240+
*/
241+
export function setObjectAttribute(obj: Record<string, any>, attr: string, value: any): void {
242+
// Optimization for 98% of the cases
243+
if (!attr.includes('.')) {
244+
obj[attr] = value;
245+
return;
246+
}
247+
_setObjectAttribute(obj, value, attr.split('.'), 0);
248+
}
249+
250+
/**
251+
* Delete attribute in an object with complex names
252+
*
253+
* @param obj - object to get the attribute from
254+
* @param attrParts - attribute parts
255+
* @param index - index of attribute part
256+
*/
257+
function _deleteObjectAttribute(obj: Record<string, any>, attrParts: string[], index: number): any {
258+
if (index === attrParts.length - 1) {
259+
delete obj[attrParts[index]];
260+
return;
261+
}
262+
if (!obj[attrParts[index]] || typeof obj[attrParts[index]] !== 'object') {
263+
return;
264+
}
265+
if (Array.isArray(obj[attrParts[index]])) {
266+
for (let i = 0; i < obj[attrParts[index]].length; i++) {
267+
_deleteObjectAttribute(obj[attrParts[index]][i], attrParts, index + 1);
268+
}
269+
return;
270+
}
271+
272+
_deleteObjectAttribute(obj[attrParts[index]], attrParts, index + 1);
273+
}
274+
275+
/**
276+
* Delete attribute in an object with complex names
277+
*
278+
* @param obj - object to get the attribute from
279+
* @param attr - attribute name, can be complex like `attr1.attr2.attr3`
280+
*/
281+
export function deleteObjectAttribute(obj: Record<string, any>, attr: string): void {
282+
// Optimization for 98% of the cases
283+
if (!attr.includes('.')) {
284+
delete obj[attr];
285+
return;
286+
}
287+
288+
_deleteObjectAttribute(obj, attr.split('.'), 0);
289+
}

0 commit comments

Comments
 (0)