Skip to content

[feature] Allow using of the complex attribute names for encryptedNative and protectedNative #3032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
## __WORK IN PROGRESS__ - Lucy
* (@foxriver76) fixed the edge-case problem on Windows (if adapter calls `readDir` on single file)
* (@foxriver76) fixed setting negative numbers via `state set` cli command
* (@GermanBluefox) corrected typing for `checkPasswordAsync` command and added caching of multilingual names
* (@GermanBluefox) corrected typing for `checkPasswordAsync` command and added caching of mulit-languages names
* (@GermanBluefox) Added support for the complex attribute names for the encrypted and protected native configuration properties: `encryptedNative` and `protectedNative`
* (@GermanBluefox) Improvement of `adapter.findForeignObject` (typing and optional parameters fixed)
* (@GermanBluefox) Improvement of JSON schema for `io-package.json` and type definitions

Expand Down
2 changes: 1 addition & 1 deletion packages/adapter/src/lib/_Types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export interface AdapterOptions {

Check warning on line 1 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
subscribesChange?: (

Check warning on line 2 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
subs: Record<
string,
{
regex: RegExp;

Check warning on line 6 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
}
>,
) => void;
Expand Down Expand Up @@ -70,7 +70,7 @@

type Invoice = 'free' | (string & {});

export interface SuitableLicense {

Check warning on line 73 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
/** Name of the license type, not necessarily matching adapter */
product: string;
/** E-Mail of a license owner */
Expand All @@ -95,18 +95,18 @@
decoded: {
/** E-Mail of license owner */
email: string;
comment: string;

Check warning on line 98 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
/** License type, eg private */
type: string;
/** Adapter name */
name: string;
/** Address of license owner */
address: {
Country: string;

Check warning on line 105 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
Name: string;

Check warning on line 106 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
AddressLine1: string;

Check warning on line 107 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
AddressLine2: string;

Check warning on line 108 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
ZIP: string;

Check warning on line 109 in packages/adapter/src/lib/_Types.ts

View workflow job for this annotation

GitHub Actions / Eslint

Missing JSDoc comment
City: string;
};
ltype: string;
Expand Down Expand Up @@ -347,7 +347,7 @@
newConfig: Record<string, any>;
}

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

export interface InternalGetEncryptedConfigOptions {
attribute: string;
Expand Down
43 changes: 29 additions & 14 deletions packages/adapter/src/lib/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import {
isMessageboxSupported,
listInstalledNodeModules,
requestModuleNameByUrl,
deleteObjectAttribute,
setObjectAttribute,
getObjectAttribute,
} from '@/lib/adapter/utils.js';

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

obj.native = mergedConfig;

return this.setForeignObjectAsync(configObjId, obj);
return this.setForeignObject(configObjId, obj);
}

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

async getEncryptedConfig(attribute: string, callback?: GetEncryptedConfigCallback): Promise<string | void>;
async getEncryptedConfig(
attribute: string,
callback?: GetEncryptedConfigCallback,
): Promise<string | string[] | void>;

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

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

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

const value = (this.config as InternalAdapterConfig)[attribute];
const value = getObjectAttribute(this.config, attribute);

if (typeof value === 'string') {
if (Array.isArray(value)) {
const secret = await this.getSystemSecret();
const result: string[] = [];
for (let i = 0; i < value.length; i++) {
if (typeof value[i] === 'string') {
result[i] = tools.decrypt(secret, value[i]);
}
}
return tools.maybeCallbackWithError(callback, null, result);
} else if (typeof value === 'string') {
const secret = await this.getSystemSecret();
return tools.maybeCallbackWithError(callback, null, tools.decrypt(secret, value));
}

return tools.maybeCallbackWithError(callback, `Attribute "${attribute}" not found`);
}

Expand Down Expand Up @@ -3466,7 +3482,7 @@ export class AdapterClass extends EventEmitter {

// check that alias is valid if given
if (obj.common && 'alias' in obj.common && obj.common.alias.id) {
// if alias is object validate read and write
// if alias is object, validate read and write
if (typeof obj.common.alias.id === 'object') {
try {
this._utils.validateId(obj.common.alias.id.write, true, null);
Expand Down Expand Up @@ -4431,7 +4447,7 @@ export class AdapterClass extends EventEmitter {
this.name !== id.split('.')[2]
) {
for (const attr of obj.protectedNative) {
delete obj.native[attr];
deleteObjectAttribute(obj.native, attr);
}
}
}
Expand Down Expand Up @@ -4600,7 +4616,7 @@ export class AdapterClass extends EventEmitter {
this.name !== obj._id.split('.')[2]
) {
for (const attr of obj.protectedNative) {
delete obj.native[attr];
deleteObjectAttribute(obj.native, attr);
}
}

Expand Down Expand Up @@ -11359,7 +11375,7 @@ export class AdapterClass extends EventEmitter {
this.name !== obj._id.split('.')[2]
) {
for (const attr of obj.protectedNative) {
delete obj.native[attr];
deleteObjectAttribute(obj.native, attr);
}
}

Expand Down Expand Up @@ -11457,7 +11473,7 @@ export class AdapterClass extends EventEmitter {
/**
* Initialize the adapter
*
* @param adapterConfig the AdapterOptions or the InstanceObject, is null/undefined if it is install process
* @param adapterConfig the AdapterOptions or the InstanceObject, is null/undefined if it is an installation process
*/
private async _initAdapter(adapterConfig?: AdapterOptions | ioBroker.InstanceObject | null): Promise<void> {
await this._initLogging();
Expand Down Expand Up @@ -11535,7 +11551,7 @@ export class AdapterClass extends EventEmitter {
instance = 0;
adapterConfig = adapterConfig || {
// @ts-expect-error protectedNative exists on instance objects
common: { mode: 'once', name: name, protectedNative: [] },
common: { mode: 'once', name, protectedNative: [] },
native: {},
};
}
Expand Down Expand Up @@ -11650,8 +11666,7 @@ export class AdapterClass extends EventEmitter {
if (typeof this.config[attr] === 'string') {
promises.push(
this.getEncryptedConfig(attr)
// @ts-expect-error
.then(decryptedValue => (this.config[attr] = decryptedValue))
.then(decryptedValue => setObjectAttribute(this.config, attr, decryptedValue))
.catch(e =>
this._logger.error(
`${this.namespaceLog} Can not decrypt attribute ${attr}: ${e.message}`,
Expand Down
150 changes: 144 additions & 6 deletions packages/adapter/src/lib/adapter/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,15 @@ export function encryptArray(options: EncryptArrayOptions): void {
const { secret, obj, keys } = options;

for (const attr of keys) {
const val = obj[attr];
if (typeof val === 'string') {
obj[attr] = encrypt(secret, val);
const val = getObjectAttribute(obj, attr);
if (Array.isArray(val)) {
const encrypted: string[] = [];
for (let i = 0; i < val.length; i++) {
encrypted[i] = typeof val[i] === 'string' ? encrypt(secret, val[i]) : val[i];
}
setObjectAttribute(obj, attr, encrypted);
} else if (typeof val === 'string') {
setObjectAttribute(obj, attr, encrypt(secret, val));
}
}
}
Expand All @@ -80,9 +86,15 @@ export function decryptArray(options: EncryptArrayOptions): void {
const { secret, obj, keys } = options;

for (const attr of keys) {
const val = obj[attr];
if (typeof val === 'string') {
obj[attr] = decrypt(secret, val);
const val = getObjectAttribute(obj, attr);
if (Array.isArray(val)) {
const decrypted: string[] = [];
for (let i = 0; i < val.length; i++) {
decrypted[i] = typeof val[i] === 'string' ? decrypt(secret, val[i]) : val[i];
}
setObjectAttribute(obj, attr, decrypted);
} else if (typeof val === 'string') {
setObjectAttribute(obj, attr, decrypt(secret, val));
}
}
}
Expand Down Expand Up @@ -149,3 +161,129 @@ export async function requestModuleNameByUrl(url: string): Promise<string> {

return res.stdout.trim();
}

/**
* Get attribute of an object with complex names
*
* @param obj - object to get the attribute from
* @param attrParts - attribute parts
* @param index - index of attribute part
*/
function _getObjectAttribute(obj: Record<string, any>, attrParts: string[], index: number): any {
if (index === attrParts.length - 1) {
return obj[attrParts[index]];
}
if (!obj[attrParts[index]] || typeof obj[attrParts[index]] !== 'object') {
return;
}
if (Array.isArray(obj[attrParts[index]])) {
const result: any = [];
for (let i = 0; i < obj[attrParts[index]].length; i++) {
result.push(_getObjectAttribute(obj[attrParts[index]][i], attrParts, index + 1));
}
return result;
}

return _getObjectAttribute(obj[attrParts[index]], attrParts, index + 1);
}

/**
* Get attribute of an object with complex or simple names
*
* @param obj - object to get the attribute from
* @param attr - attribute name, can be complex like `attr1.attr2.attr3`
* @return could be a value or an array
*/
export function getObjectAttribute(obj: Record<string, any>, attr: string): any {
// Optimization for 98% of the cases
if (!attr.includes('.')) {
return obj[attr];
}
return _getObjectAttribute(obj, attr.split('.'), 0);
}

/**
* Set attribute in an object with complex names
*
* @param obj - object to get the attribute from
* @param value - value to set (Could be an array)
* @param attrParts - attribute parts
* @param index - index of attribute part
*/
function _setObjectAttribute(obj: Record<string, any>, value: any, attrParts: string[], index: number): any {
if (index === attrParts.length - 1) {
obj[attrParts[index]] = value;
return;
}
if (!obj[attrParts[index]] || typeof obj[attrParts[index]] !== 'object') {
return;
}
if (Array.isArray(obj[attrParts[index]])) {
if (!Array.isArray(value)) {
throw new Error('Value is not an array');
}
for (let i = 0; i < obj[attrParts[index]].length; i++) {
_setObjectAttribute(obj[attrParts[index]][i], value[i], attrParts, index + 1);
}
return;
}

_setObjectAttribute(obj[attrParts[index]], value, attrParts, index + 1);
}

/**
* Set attribute in an object with complex or simple names
*
* @param obj - object to get the attribute from
* @param attr - attribute name, can be complex like `attr1.attr2.attr3`
* @param value - value to set (could be a value or an array)
*/
export function setObjectAttribute(obj: Record<string, any>, attr: string, value: any): void {
// Optimization for 98% of the cases
if (!attr.includes('.')) {
obj[attr] = value;
return;
}
_setObjectAttribute(obj, value, attr.split('.'), 0);
}

/**
* Delete attribute in an object with complex names
*
* @param obj - object to get the attribute from
* @param attrParts - attribute parts
* @param index - index of attribute part
*/
function _deleteObjectAttribute(obj: Record<string, any>, attrParts: string[], index: number): any {
if (index === attrParts.length - 1) {
delete obj[attrParts[index]];
return;
}
if (!obj[attrParts[index]] || typeof obj[attrParts[index]] !== 'object') {
return;
}
if (Array.isArray(obj[attrParts[index]])) {
for (let i = 0; i < obj[attrParts[index]].length; i++) {
_deleteObjectAttribute(obj[attrParts[index]][i], attrParts, index + 1);
}
return;
}

_deleteObjectAttribute(obj[attrParts[index]], attrParts, index + 1);
}

/**
* Delete attribute in an object with complex names
*
* @param obj - object to get the attribute from
* @param attr - attribute name, can be complex like `attr1.attr2.attr3`
*/
export function deleteObjectAttribute(obj: Record<string, any>, attr: string): void {
// Optimization for 98% of the cases
if (!attr.includes('.')) {
delete obj[attr];
return;
}

_deleteObjectAttribute(obj, attr.split('.'), 0);
}
Loading
Loading