From 515646da6756d69a9b187aa6805ee826cba43281 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Mon, 24 Oct 2022 11:38:08 +0300 Subject: [PATCH 001/116] merge w/ master and refactoring --- .../system/widget_bundles/input_widgets.json | 3 +- .../lib/multiple-input-widget.component.html | 35 ++++++++++++++ .../lib/multiple-input-widget.component.ts | 48 +++++++++++++++++-- ...ple-attributes-key-settings.component.html | 3 ++ ...tiple-attributes-key-settings.component.ts | 6 ++- .../json-object-edit-dialog.component.html | 4 +- .../json-object-edit-dialog.component.ts | 6 +++ ui-ngx/src/app/shared/shared.module.ts | 1 + .../assets/locale/locale.constant-en_US.json | 2 + 9 files changed, 99 insertions(+), 9 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 3c9a9fe718..f21c059119 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -324,9 +324,8 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n \n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "settingsDirective": "tb-update-string-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html index 550f7cde49..54de73fc66 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -85,6 +85,41 @@ {{key.label}} +
+ + {{key.label}} + + + {{key.settings.icon}} + + icon + + + + + {{ getErrorMessageText(key.settings,'required') }} + + + {{ 'value.json-value-invalid' | translate }} + + +
(JsonObjectEditDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + jsonValue: formControl.value, + title: key.settings.dialogLabel, + saveLabel: key.settings.saveButtonLabel, + undoLabel: key.settings.saveButtonLabel + } + }).afterClosed().subscribe( + (res) => { + if (!isEqual(res, formControl.value)) { + formControl.patchValue(res); + formControl.markAsDirty(); + if(!this.settings.showActionButtons) { + this.inputChanged(source, key); + } + } + } + ); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html index 22823ba778..b3e205ac9e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html @@ -48,6 +48,9 @@ {{ 'widgets.input-widgets.datakey-value-type-integer' | translate }} + + {{ 'widgets.input-widgets.datakey-value-type-json' | translate }} + {{ 'widgets.input-widgets.datakey-value-type-boolean-checkbox' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts index 4884166be5..f4a4f36a34 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -24,6 +24,7 @@ import { dataKeySelectOptionValidator } from '@home/components/widget/lib/settings/input/datakey-select-option.component'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { MultipleInputWidgetDataKeyValueType } from '@home/components/widget/lib/multiple-input-widget.component'; @Component({ selector: 'tb-update-multiple-attributes-key-settings', @@ -130,7 +131,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings protected updateValidators(emitEvent: boolean) { const dataKeyHidden: boolean = this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value; - const dataKeyValueType: string = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; + + const dataKeyValueType: MultipleInputWidgetDataKeyValueType = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; const isEditable: string = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value; const useCustomIcon: boolean = this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').value; @@ -244,7 +246,7 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings const dataKeyHidden: boolean = this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value; const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; const dataKeyValueType: string = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; - return !dataKeyHidden && (required || (['integer', 'double', 'dateTime', 'date', 'time'].includes(dataKeyValueType))); + return !dataKeyHidden && (required || (['integer', 'double', 'dateTime', 'date', 'time', 'JSON'].includes(dataKeyValueType))); } } diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html index b7949d59b7..4f91113c79 100644 --- a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html @@ -45,12 +45,12 @@ type="button" [disabled]="(isLoading$ | async)" (click)="cancel()" cdkFocusInitial> - {{ 'action.cancel' | translate }} + {{ undoButtonLabel }}
diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts index 3f4672ffb0..673bdddd78 100644 --- a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts @@ -26,6 +26,8 @@ import { TranslateService } from '@ngx-translate/core'; export interface JsonObjectEditDialogData { jsonValue: object; title?: string; + saveLabel?: string; + undoLabel?: string; } @Component({ @@ -37,6 +39,8 @@ export class JsonObjectEditDialogComponent extends DialogComponent Date: Mon, 24 Oct 2022 12:18:09 +0300 Subject: [PATCH 002/116] minor corrections --- .../main/data/json/system/widget_bundles/input_widgets.json | 3 ++- .../components/widget/lib/multiple-input-widget.component.ts | 1 - .../input/update-multiple-attributes-key-settings.component.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index f21c059119..3c9a9fe718 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -324,8 +324,9 @@ "templateHtml": "
\n
\n
\n
\n
\n \n {{ settings.showLabel ? labelValue : '' }}\n \n \n {{requiredErrorMessage}}\n \n \n
\n \n
\n \n \n
\n
\n \n
\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\n
\n
\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\n
\n
\n
\n
", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet utils;\nlet translate;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showLabel = utils.defaultValue(settings.showLabel, true);\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n settings.isRequired = utils.defaultValue(settings.isRequired, true);\n $scope.settings = settings;\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || translate.instant('widgets.input-widgets.entity-attribute-required');\n $scope.labelValue = utils.customTranslation(settings.labelValue, settings.labelValue) || translate.instant('widgets.input-widgets.value');\n \n var validators = [$scope.validators.minLength(settings.minLength),\n $scope.validators.maxLength(settings.maxLength)];\n \n if (settings.isRequired) {\n validators.push($scope.validators.required);\n }\n \n $scope.attributeUpdateFormGroup = $scope.fb.group({\n currentValue: [undefined, validators]\n });\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function () {\n $scope.isFocused = false;\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n \n var value = $scope.attributeUpdateFormGroup.get('currentValue').value;\n \n if (!$scope.attributeUpdateFormGroup.get('currentValue').value.length) {\n value = null;\n }\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('currentValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n\n $scope.changeFocus = function () {\n if ($scope.attributeUpdateFormGroup.get('currentValue').value === $scope.originalValue) {\n $scope.isFocused = false;\n }\n }\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n if (!$scope.isFocused) {\n $scope.originalValue = self.ctx.data[0].data[0][1];\n $scope.attributeUpdateFormGroup.get('currentValue').patchValue($scope.originalValue);\n self.ctx.detectChanges();\n }\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showLabel\":{\n \"title\":\"Show label\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxLength\": {\n \"title\": \"Max length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"minLength\": {\n \"title\": \"Min length\",\n \"type\": \"number\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n },\n \"isRequired\":{\n \"title\":\"Required\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showResultMessage\",\n \"showLabel\",\n \"isRequired\",\n \"labelValue\",\n \"requiredErrorMessage\",\n \"maxLength\",\n \"minLength\"\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-string-attribute-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showResultMessage\":true,\"showLabel\":true,\"isRequired\":true},\"title\":\"Update server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts index 1a1a85c599..b0d6aa58dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -91,7 +91,6 @@ interface MultipleInputWidgetDataKeySettings { maxValue?: number; requiredErrorMessage?: string; invalidDateErrorMessage?: string; - invalidJSONErrorMessage?: string; minValueErrorMessage?: string; maxValueErrorMessage?: string; useCustomIcon: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts index f4a4f36a34..2d9ca8db35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -131,7 +131,6 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings protected updateValidators(emitEvent: boolean) { const dataKeyHidden: boolean = this.updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value; - const dataKeyValueType: MultipleInputWidgetDataKeyValueType = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; const isEditable: string = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value; From 864eae2cd71f981af645feeeb7de150581de97bc Mon Sep 17 00:00:00 2001 From: WHBANG <38547944+WHBANG@users.noreply.github.com> Date: Wed, 16 Nov 2022 12:42:24 +0800 Subject: [PATCH 003/116] update org.springframework.security:spring-security-oauth2-client 5.7.1 to 5.7.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5f5aa3703b..8e09fa474d 100755 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 2.7.0 5.3.20 5.5.12 - 5.7.1 + 5.7.5 2.7.0 3.7.1 0.7.0 From 5eebe2e6ffa5daf9f794a6a1bec5fba17523cbbd Mon Sep 17 00:00:00 2001 From: Seraphym-Tuhai Date: Fri, 6 Jan 2023 12:02:44 +0200 Subject: [PATCH 004/116] add guide for run tests on built UI --- msa/black-box-tests/README.md | 8 +++++--- .../server/msa/TestProperties.java | 19 +++---------------- .../src/test/resources/config.properties | 2 -- 3 files changed, 8 insertions(+), 21 deletions(-) delete mode 100644 msa/black-box-tests/src/test/resources/config.properties diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index ae0e8b79a0..df28271864 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -42,6 +42,8 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -Dsuite=all - - - +### To run a separate test manually on a built UI: +1. Add the black-box-tests module in the [pom.xml](../pom.xml) +2. Add Vm Option "-DrunLocal=true -Dtb.baseUiUrl=http://localhost:4200/" in Run -> Edit Configuration -> Edit Configuration Templates -> TestNG +3. Go to the test class you need in the [UI tests directory](../black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests) and run the test in it +or go to the [resources](../black-box-tests/src/test/resources) and run test suite you need \ No newline at end of file diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java index b5d91647c1..e5ccc62f4c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java @@ -36,33 +36,20 @@ public class TestProperties { if (instance.isActive()) { return HTTPS_URL; } - return getProperties().getProperty("tb.baseUrl"); + return System.getProperty("tb.baseUrl", "http://localhost:8080"); } public static String getBaseUiUrl() { if (instance.isActive()) { return "https://host.docker.internal"; } - return getProperties().getProperty("tb.baseUrl"); + return System.getProperty("tb.baseUiUrl", "http://localhost:8080"); } public static String getWebSocketUrl() { if (instance.isActive()) { return WSS_URL; } - return getProperties().getProperty("tb.wsUrl"); + return System.getProperty("tb.wsUrl", "ws://localhost:8080"); } - - private static Properties getProperties() { - if (properties == null) { - try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) { - properties = new Properties(); - properties.load(input); - } catch (IOException ex) { - log.error("Exception while reading test properties " + ex.getMessage()); - } - } - return properties; - } - } diff --git a/msa/black-box-tests/src/test/resources/config.properties b/msa/black-box-tests/src/test/resources/config.properties deleted file mode 100644 index 419c73185d..0000000000 --- a/msa/black-box-tests/src/test/resources/config.properties +++ /dev/null @@ -1,2 +0,0 @@ -tb.baseUrl=http://localhost:8080 -tb.wsUrl=ws://localhost:8080 From 166c543faf14b8a1db3198f0acf6628b1f49c056 Mon Sep 17 00:00:00 2001 From: Seraphym-Tuhai Date: Wed, 11 Jan 2023 13:24:30 +0200 Subject: [PATCH 005/116] add one more step in README.md --- msa/black-box-tests/README.md | 3 ++- .../src/test/resources/addAsMavenProject.png | Bin 0 -> 125747 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 msa/black-box-tests/src/test/resources/addAsMavenProject.png diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index df28271864..1907991dd3 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -43,7 +43,8 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -Dsuite=all ### To run a separate test manually on a built UI: -1. Add the black-box-tests module in the [pom.xml](../pom.xml) +1. Add the black-box-tests module in the [pom.xml](../pom.xml) or add as a Maven project as in the picture +![addAsMavenProject.png](../black-box-tests/src/test/resources/addAsMavenProject.png) 2. Add Vm Option "-DrunLocal=true -Dtb.baseUiUrl=http://localhost:4200/" in Run -> Edit Configuration -> Edit Configuration Templates -> TestNG 3. Go to the test class you need in the [UI tests directory](../black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests) and run the test in it or go to the [resources](../black-box-tests/src/test/resources) and run test suite you need \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/addAsMavenProject.png b/msa/black-box-tests/src/test/resources/addAsMavenProject.png new file mode 100644 index 0000000000000000000000000000000000000000..0f5e9bbb3a4515ddc5b5233ced129b6e384071c6 GIT binary patch literal 125747 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%0Du&d@OwuCG_Fr;s`?0( zPmLbGQM-Pra65(X{D-P-oZ|Dip1B~9CEq8^P*RA&#yyp`$HP|Z*wA9gcy0;R*gG!< z9m|BQo&8BgdNnyvP%&&RXI(2P-DstXlGCeZYdIV;mY%qHuKvE2t$_zc3JV4-Ez}+8 zP7)zQJgDv8J~$vw7tQci{rU|+aN5|SuhDYfJMm>#gfKMaQ^25kI{uax2@TW`2LOs~ z8|PCxn8_07rU~NF;kHRCqStLaAEJ62g3HYW9bXvtcumnJ+;gWaTg)}H$nyoC$Dx8TA# zGJD70zxl$&dZw11c%)IV({q+KH(1;j&K=o7uB|-RJ1YF?;_u6#l+A(%e5AxR=PY`9 z5>p+nr>`~N{Y^|euYB+8%1dKk>e_D>wPcz8{jsz%nN&_zcu*rhM4;^2 z(mMMppH%dS2FCA_m&?mU$@68e4Tos(Y|tO6_Yub18%i!6druxNtxpOvhDS7umY1DV zOLZmW@lpNTSev!VT~qX~)smF*`~!gwn+vIk-UhiyPe+VDtx&YfXy&E9M6Fk$znpb}eQ(2p^jr{a+A@B_%oa)syE+cJTR@vPL^N<(RC$)Af+fZEdG_=RQ{Vlt;gK` zx77;VETB6AO>jY2N$*(739BfPX|R~%5rgm325?}1M^(G@NV}O?`@2$UCSwQl;<|`! zI%t4-=oig0G_T;e@60c1vV&q7tJ_1y(Qw?1b|Z5a6C@q(MNB`KeZf1o=G}1D3FJ9t z>K{Sx_h$Z=H>h$G^Aa6PVBOiaoj(qQNQ~#K)y0cM9Xs2|zI0$3gffkFr&;nnUt%%- z3MSKj2Dy{B#lrTW1PVjT0NDK`&F8*fiLFooKWya@L zbjx70+eD?0G(G)qSdlsJ79d5~#}C(J;@;;QoRJo9PgN=F31Soc5X3a${}#&c_YDd{ z3&r4vcwfI0zaM)&48nAt9|c=X@H@n$7`M`-%~ zm7!q?+57wLu4}gJ*c@qX&eF{BL&@uV|D+3Cv>lGm8#kEjgW=uk64DT64FD-8X#f)Fx&aa_1R`X80KGq7(TAz2msw!2fm%*W& z!uf$$sF{k&znz;Ai3~+JJyA`$$W`nMu+RTHug|Md{@kThu9OD=S}W}}GB6(CaT&tq zSk0l2^v#T?=rJ-Qp*uf9RK+cl<}&t#Vy=64M~F3XhH^Ke>%gP)2-@Nv-Pfn8PuKU_ zkij<6lQz?GOvc9XXm}76B8z$u+PW0PQ^D}|glH4kc z2_wdbj^zD5MH?0Ivb-!<8RzfS%h7WFv77zuj7SBp_4&1n>2J(&yFo_-nQ`>0l<_JF_y+@=?t!G9v`<5vw@p2GC)aA2K(svjr6JSp%Vkp0rHVMeB%6=*$H@|EJpzO1D7PuAHjVj6;%G5CSjmfk%yO`bLhyK9ZI%_8p zh)tO6rjm~1LGiXLSm7R$wv#h(aRJp8j2O(6;UEjybbCREhN5?m-~mG29lN{T>oeu* z44Yq}!1<^pbBrINmhbdm6oe78mSb1#FJEWY3s3xSZabgcSL)$Z+BS;Y&#KAr_>Hzw{V61~ zy(s6*na%QKvBepkB0M?2aYgWy&vSvCe+TYkTSrW%^S?MpFZ)f|ZSM{(RpxFoa84F+zF*tvVImrGN<-Gk0gxIZ zd`09mz3B45GN&l~Lo&h9yrlt|;U$^roW?qgfar0{OveZ9MXh=E7uXn%j7W0&xeXs( z{=$6W81z4WqS6NN@nn3wytP}(7YXO$OyJ$NjFRe$ML6B?`(->fw<4>8gTYzAQfs_; zTpH3d4u=;{q3=50`k)iPM^Q1*UBin4MI17=!K%Q_)MH%#$%ZfG9f1eVfr*tDbsu%o z#8ulIxDyd#J6$6Au4Ghk%uIgbun%AtVK}A`V8Dv$56!Q{#51O9Ph1kbjxP;KPS+j@ zwW3wllINEKtox+E)6~QQzV78$XtuEM#~tiV zj;Pr1aMXPbbBAN3TTM>GIzHH|HWM{|MrFs7=mcqKx$cSE<106auBhm_)M^`i^m=B> z`juabSc3s=MR3G)GIwoRC|!yL{cm7X#%(&6gKdr|{m`yU#s-ztQX3ENmVepY@#94t zM*IH8+aFk)a)=lK`w>pjxlkTw9Iy}sH7x9wgDsuMzS#5ey@n^w@nQNrw`R>rD<8@c3#J7=8TGl`93r<^1G&*K7^5{16>Vv|eTDY9m?0naRkX zO}Vs%@9tj=gH5L|IThUaGmJza5lLTwuX&0sxkggcwV>9b6SnLS%)$yZsy?m@Wk>!E zF~_s$azh!?SE*Qkr0>?HeHU56qc%e?bxq2{qmlP@9hHb4)9T31o4kBuuF5*-jrBz9 zml>4`nh#jm`|h|hygkb1Vp-RC2NgmPK_0`g)5s3U(JQ6G=`ZMWFv@vjHsxk7 zKPYX6W9k5<`#Y~7vRx2iMw`zcSJJMj<$ngGoAASzN$&G*!!To9-{eHFlW^c4ycmZ1 z58t9Eb?%q`93vxUzNgeaBXT4jO3>C7uwZAN-kVuTU0*=}8#qfiIwZL8!UDd!S@K}8 z6E#d&3glAq-A>b@A`@e#N|Sio=?Xl>%PrUOZ!^F^tm%l!jZRE&2T)_k3cK(~tF}Lc z9Q$AZ^5%cTRBb5@;hO$8^k%!Ce{4p`r@RcQl&Ox_KM=&y;JR8(@pE~Egrqso0ZO0X zoJ9f#5D}YP^NWJ}%~-TF2wH*oA>31dk>bTDWW;cLkpIJtRt&av)0=T&fK&Jhfo!x9 z{fCnB!<)se1GW?8KH@~&eVG_B8o?vOX@YW0pA;vJ! zC?4uy>rqx|y{{S$&KcgfWd`)K5RO>Pe}%#l#K7tZ4(YQ14q%>?B7~1_ZR=L2n}^4H zolymernS|SW=NGaVR?m{;N2}9;<{hF%^6Bp5UD(h0e&|DB4%QWAuh8EXa_Jb9`26B zUR$oHD1!b0@q4mt9x@-B9yWjCBMP(Ki6__q>_(0W=avvzx@qJeR?l%$zz^!Fqx!! z;1~>5w~=Yo>Gg(VBwbAj0F&7CvkC8`tka*M^F7E0pd{|>?u3HEj_^K$Oz(x213BdoFG2Gws)M_+4pjav=N1-F}lR-_C_{5W+LuOs+TO%}}@LerfZ$vp7G`DqsFn z_L|EwM-N2xnJuhho9}&uBu+>jJ;5c;ST+mL5P7|;^nn4`w%^Tb19t}k^NL0BEvB?_ z4KqL04ht;A9hwQ`tFfOq1hARca%eiq1iGg6q5uF1ev8z44JK}CR@2!hMf^)sv(@`o z12~LGEjfqCSkT+kWZ1G9su)Zy<2<*rGg$BKcI1 z(@Vx}c8;xxaiQiGO{okm9t~a=xuP_tE0U>5#FUkU_7SHkOUhIQlUl5KwTsS}8{4`; zx`vkY<58&u#umC)3^~>HA_pcxjDtg4gckDf<>dOSpgzUP1CDrx|>62ssGIm~7COhC<(FwZ6X|9|DEu+tCVw;`H{`FWmo<1iv z#X$$RZ_|hnq=Gk89MHIOkEPhARCmeeI)v66I%b$QL=I##sMA8bLg>g{W>T(gN&(?l z8zy%qxJ{#7?=WJ}tVGM%EJ-Q}uye{>Ynk}|$Dyk^&WrI6p$ffmxhzXVA$Uqh>X(s; zE*zQ&k7(dq-JndsE*qBkF{ehDN>hTtX<9LX<(wW{m(Gv=IC-qHf@$BP>!mxt%G>xG zpzR*CFDr=n4bx%2By#XQR}moQABYhYWJxS{5GkjlM<@qDk9Y&E*NH3 ztl4_bZiIIIVmX@0Xthi6JF~A(2q!AqV=0*8xwzlEdv=%i+Jb%r3x`LUv6O>lm~;L@ zJL2C0iJ#8s7pUfrgTU`2P+<~3<$CN>h3;?Ekq?e$V={#6RdT78c_A1Z8r45ktBv)oV zM0&M_$aS5z7F9IH_>d4Rf^A0ZjT$n=ASYjVS>k2d;x^4&#lQ%ANJMB)E(R;T;4JLq z&re$gdF_5@sk0o_*COi`rWOJ9Sv(8ZpPU_>+^Pv?+N(u5abH1wY^Guu-b7btur%jq zP8#B)+}-8bN9D0bX4bhOzgSrGvO2rGaO;o10P^%?PP&=Ce~Gf>gko)^q7?bHkS~Rl ztVGD5$~j~Dc=idXAFm)H1oFGuw+JWcJb~kDn?1l+o28sfY&FAAI62Z)y}RLiF)oy&m+80 zfoi8zH?^hexX_)X%LD&=Zsw}2{}+>Y!_R}y=F3sP6y7y_#nGT2V?~6FwUyG+gs1Od z8PiQo3?LdRXudH)<-t!`R77LCDt(}lB=`X6^p^3#suUpdKK-Nbq3+3ZKIhJ^%HC*= zi}p6eo&re)3;6a`N29mA(DMXYw~7xD6AE*tw_7|fR7A5FP=9D$SSXY znl}ei)f{Gc$HV4BAao8-T_p2uYL5FjHm2zCO0J&?qn|D@;BgqoXix}eh5FXnkAkk8 z-s~`x+4B^|@$eB!;COJC*xEW_90uU#YbsS|b?KBE3cYc4lW_@rAWo-?di;QaK^Ca? zXQh=5ERIKH%X16fG!aiRs@nA^aVp^_}WJigI-eJ#(z{V)-n^ns=0&(dkRR#=Kmzc%hJ4t3EgdP!whmgy$U zZ6Cc^s0ql$)e-#rv4vpb@zn{=h-xO|Fa;0XK0c=`(y=-=nn>`xwivCKB_~CgsQ?x_ zQBM(#*3xez)S)Ul1D6_!5OZV_;BO*{Vi}LNoh9)FV!IRUh=93>@))8|llg6YxBxNs zZv#gr@2`yzi0X~x^VSZ*j2alnSJ>c;XrVR(Rrs0O_i97kKJ3{Sc5AHoC~MSHo#dl# ziITZa%y}{CNG>ojTOJb#TRQ3Af9pam1qe0o0?Ug8#?wWRQh@U zAOji7SEe2h|4v^gt@YFFf6!5)?@VT^4~c=detYZ*AlxjgoG1)+@-4~t@$1sX0^ zQZwejVDg!t*LhUbhMlvXa+w1X(F0eTRruN8vh_}pN**_R!f7x7e>$<{`DzrN=In>( zGs3-Wm4hWj=5Q}E4(*}(!2Zuad_2CzFxsew(a2|1s4OZ%5jbT12OkBN7Ak(P-s$}L z3TA-r7e)oJlx=pb9v`~`x)V!n-2HFQEu}9kZ@dyk2vxj4SzInIrrFgk%t!_{6p%^I z*pC>-jf_x;gd{gvb{$nPt3{3j17ly2^BU?|6=#?Y1QWCKrWu`Le|*Z6=AB_OF)%@> zC2nQ7V6WT5H7%&pDh(;voP_#J=B0dqO03e{+6doNx?oK^+3qW21s7j?E@M9Dhpav& z%~|1PSAN|V&>h>d)|4MdtR`hdev&i%!H-#_$9;S~{Mu&8lw@`j@O8@+o4af=-fIIrHs2IhHuB!AC?gd+XKkEVtoVIL33GFM^i32npY|Y~fJde-&pR7>f@M z{#AehPSVF|GN`8XAIi!VkS7{Goh=jF1O5HZ8OW&iP0g=_uBqTdZO16~6{=f7O`KcVaWC`lv{Ro<`yF}ZkS+i7c7bX7}2-1Rg zmgv74LWgrQl`HQ1beJ0i6D0VNn`NO-NXS?iGvZFTKLDo&HAJ=I`YB~JS1P4fQOT*E z*dD~htJs9A&;fv(DC))Zy*W*fR%3l?6aD`Zn5vSI5>e8IQ4hX`3>hUPoC5sZr|)6F zu^4LvP90NU!$bGNKwwXnZA417`M@?zRUt+Bj`wcyPyesl9{-tWSVm& zWaOkJfedG$wt?C~Esu&N=niK>t?B=XT=~+&Yc;L#s93m@3NcSU=KXgaBRW0&SNPHO zn7;4Y7UvIFsJ9S+X`5L*y8ktC@{E)yMaV=Tm(IuS`x)W)Z~sy9h?Hb>`FLQsjZSev zN4ZM9hl&_WY4-e$bKv!GHfB4a!@~LSeZMwJsRr$Wh9rRW|A=Ugxo8~&S}rtY#qSHu zL{)N=pgXzj?SCpWO1+&QT*E0j)DgX{v{QQiPnF&>xkx}q%yYy)sR5@7X=Z|?28X+% z5IQJW$`M;RsQ{U^&BLwiovwf`!Qf!5c+PiZM0nMcr-OR&CxM5Qun1%DnU z@p&I!RC)(3`)=%YfBN76g+PE5ad5B8-Q-i z;b>O{RkzyC1OTii|0$zTNb_nMn?Uwcl>H$Ym;4ozXuv8gaMU%il+`OM3<-cb1~Mma6$M-?&ygJgAX z&=8-Ar&8)Tgt9*~E3;qTayb$@=!gBkv;ZdxIN*TM72HdaWvvSNx;eyLf^D3JUZytX zwk`VCb%xcWk)y(`ctS=Y7SChcTOoW8sF>vmYW)=4AM73rhLqF=hd@;7kJ! zI0lV4PRWAiI1H=CxZebjdU!W72r&l+FA@zmGVCnM7X6z@G_?^Qq2``a_v8u!(6E*U-OX6f z%?gQi2LG8A^r0COhh516r37r-rq_ufMwNrxKq_NY`k3p8x5PIF60aug{@RYX%qN4 z-6=klyB3HoON9A@-Pwdg8lT)xb>jEayr}pw4`B$;Wd>!GCTH@dLJ31nmgOuBkOS zJHm99{MF+;Wq=ZM9?~uS$RwO}(2KdDlz9zGC`hjGfKDs5#cF9ff61dwH(O!QsJe?f zEH@;UBMgVh__ zfKMM-7WSPLy6jI6CIo6o`E?fO|JE5=ZM4&~u_=?HeSr3li9rJt6&EW~V}YyWvK6qf zv8~tYtrw!s?vNudye}&?7(Ys?G>FqS=H#7rKYP{n>_KDFyi%gsjT?Q}X)dnz_ivUcw`wx%AhOS6)K;DFRyX4JUOfaQnibSr%b|X&;Uqf!$?wK0w!7Ww zUk@zJ^l<;UTy%`67%=cp;8)CKOS~botThvxcZ8R=w-}EfH`0XRmmP(trtb|dng$FV zmW-dbPW1^-7hZOQabSV*I!q!6)QGz7&B@I@WW+L;AxL7*;plo1oRqYpJ--y%@yc;< z*1&yqq}TT5F%ZsZv)B|raex@qJ&@(~{qs?)b-Njn0T|Nqa(4J5WKDQpsdeN{6^A34qdgU+&D`BTt<)Z5k4&lFjbCP%Pld;ZDU9sUzt>d4}4kEi%IrR0vV z|4KhX(aVCRKsM(A2vIxl7r+Vd4byyc&@>^yOQ-?je?A?uZP%!FDPK7g6Eb%B5iuK* z$sl!?ZSWQ2|CH&n%U=e_qth`iqZM;Mye_PnTsoK&DGj5e>b5&;Da^rT92e_^kUsE$ z^XmnFHhYKSaYh1biZ3d7Q{*{Wb)WBQ7~307Zwh2%O>PnfJY=WAwZ=9N&2^T(&nrN^ z*E!ajI2ab_!sq{wvA2$DtM9gjgHyb?ODXOYcPVbg-QC??Tf8{MrMSDhySo*4CrGd_ zeU5zRz307S+%fW3cCz$2z3;Y@w(GB}x|icV z`h_jhseUSvr93~Y%Rzt(k_{W%H>r7*mUaW{%Pov!UAysd?#^0USu@sU`3-n8DLHl#DQ9f7x@4$t#r1Lk?|E`4H z@dGirfjdH~O7eclc^qJ38dr#gzZsa2R@^U{Ye3-rz($L_%iw+XF!ql?wJnk2(q3NC zc9Y+XRJxXTUdqI)#mItkj_~Jy0j3_~ls8Xo)T3lwx=`?mDJCpVC>n zFeUY7mp%c(#)J+{euDU`6dA-X zX3|vPS3}Tw9STuJ z&vG8I%2+*;-3*xmS zmz!63S%%n?*y)l5o`&zi`wp^x2k741egomM!~NEOcXxXLhl~?PtsHQ3--YrBb_RUFIAWw&%r3pZIwl0ALHToJtgY{D9BfoLzy< zTpFE($xPoPasH)5RB=0qvBYODLES3-qD-xdd;fyxeZ{gSfl{N>)Hf=>%G;ru>A!Uz za_XNb2}#gfiYdnbtIScfZ@flIt?9z60!jD){y+0OZhxO6f^RMa8Z@?#5Gd4ra7*PH zxQ0BIym2#ZFhD_S5YJuv7_;7_a%M=$aCg=nJl%!6n8LiLDEjd68L2 zO?&yO^I zdURyBes`GKou{G+{U16KW(d4)_G$wcYkcrnJvl8MV-ez!i@l9Uo2*FLnKMI7)Sdw= z!v_EtCvC?5nX&~urrpoyQNAc@<|bd-oG{n=<1CH7+?TOGHt**K=^y`<(H>PKO)@%z!X<3~cxef_Q1o%iSRS}KcUWmK_r>#4U(@8_*5 zM|zJO3kjtCMH@-cx#L)ZBLcTd$-BIe16%rQ9DlEBF$GA2yQ^jRnDUzm_bw0Mt;}rq z85QvlR(-nXp{~y7$24-^-Gd$K(NvKw)88FMJI`aFKl`V3ua2{`YN}h8*>$GYdRjgz zUtN4wXQRHwM(f*ENW&R|ixO*s{`|=iLh(KN{7=RlNxRWn@_ORq@Q>8~sD2?@G;pqD zWJ+2(5eZ)A&p2#xIPJ9peA_=aJ8;-TVq$~l_KbSOIhCdHQ^tfqC<>0Hfyk;kr?Z|v3e(pBZVTU{h)+OCUq(WF5FEVS9u zcn)ds^S64zz^PbN(Fb?X34D29;k|sMj{R~iSSO~vpoZm|8{s8Nlq+=Nvc=T3uIn_S%h=M-&H*dJjt&cv2QCjH`|x4jdt;&Wtg?PpZ}aIjH)ymp zueRGq(6PS#a{S9}vS;=$&#YfXL~!C4C|9+G4U+0!)}|d-7r@yZ^(E5~m2A&-uiHHu zRS@V(?iUSqPSV5T-hvuJk>4rJ^=ooYJcrd;UYEZuS)E&fa%699#^HzWb6)8 z{nAjy1%3DYid=O)XSyY0Z>tkM|1`Bl9n#W^lgm*-U#;x86Y66NOq7cFfgO<-qeBH~7c|opM z{vE{IDk-=6`;>?ESKT@2buJqJ*u&B;O*j?U>$gixKf;F$ zm{251yW9I)$Qo!`mQeC|g~2Q(cs}SpzCH8|!1!H3d0g^e-G}MFKyLbZGitIkGUmm{ zH$Q^i5*L)GN@<$6%q5+Tz$y8$WSfCKzoyPe2R-KF`P74JXDs@E$cy$J0P+KS+0shd zf1K~{jxQc{JIh7sirIC2XfR^KXEBPx36+h|GV*)ssn>RUJ6Y<|oJ>Q555Y2|NQ!7y z2j}&#Zfw}%8-NJ z1TXO#b!PO7KTp}3=h1@iOsoJs)|9gvB16h~lm!apHjFt1GH|2gm65UR>!&!KPnwc` z*4BR<005-9ix$@Ko;SZZ$-R@4PbZmt`>NB%lCDM`+A+mw_A*|j82LHoW+TG_Y=#Ox zLLLU)jltgc&7(JVoF#zj*0L$2kGK3&O|Z!^W~{vK48QW1*_<6tPa}T7T_Pbv)FUt%$g+1 zDx#0FLa?Dy@6d+<-c`pxB)0qAD7h_68{~W4c-2$v?D{`TO}2DViAngkb!6Uez4ObQ zpbCbRc<*v@iu&-~P1)HLy**{W@9e3l>1JNvWHtF;x7F+#hk;s_RkCnTEMuPiJWO;a zW*!(Ll6mufeK^4UZLM4^lkH+Ka3V>HHV%LbloM4(6afUF;lCo1^J}tAVn$+{B zbTJ-oY_(WVm7Li;O=i{=b$VfSf)G~Mgsb2h@JmboyuS+m2>%r+4gmlkE~>3_zgYF< ze(xNCCdB_8jUqRjabR9R1tuvV%bR5!?Rq*MV$bNPsc|vkZA3(Kod%OcpXJj& zvnq}5V`E{x4lHp#1b-0Cv+M`~+a&}tD;yE)Z6=@`yK0pf{)%da&39}l(zXb=Jfw=T ze;#03rb?!@8R@Pu{{9Zsf4CGRC7$1CRd0nyuc>W4noJ7IHMfWqA)%5<(yE(n0J9}@ zA7cBGB~4{GUmI>BN&K&$WB1r$;Bya1-k5p#eJj--JZmhKp~N;u$J(5LrM4EJow0vz zA0TU9e5j?xWKMx{wpKJ#{Pw!_R?+gif7JVC`1n}S#jW*w*xPB9Untt^nYunh>kF3Y zGOOT?_v6qN37=Iq$@7$L(ifI_40tB4sg_*7RAuwY&y=56S60sXd^8%D4Nu@ABlK3g zwgv_$cz8f9bJSCa!?eu4hmG((^i|9|-UEe{pqmlBKIotoBW!6HfZyF*_QJx#J{ET8 zZQB-dCV!iT(f7%$*ZV4e-=^lr>)j#2*C!@y8G6GwVLqI904f0@G{9vk%D(Gi5>^3& z_W`^L7#Rl>YAR1fVGPtdKIooTzr-{2&7`t1x=>tqtFj}m{u1b zO?172dkxcp4{Y%{?H$9Qn?clw-gmx7x^sBQuCIn&Bwu%AcQ(!0i<>gT79_|N#FI@x zKByORQ=M-nob+jH*g*zfC6>46`b8ZaERwwK6(J)dzrDR}1P5!%$t}KLp*lDVL$Gq% zD*4>6+`m3wV&ibKvv)iM`xE2hW*Dn=<@_8>E4B4TjS)>T!pC!a0%|K~i{i-0NVT%pgE-E^0>zhL5d!N{Y!3(~zSZOt z*oCwJ(uY&q3i9V!=5-Osr-;>34t@Y>+tCJ`Qo9W|Qoi)8T3m|qG=277Vl<)h=8Kud zZB5taNg*JB3|@YtSzun-AU7f0E82#wwA>6$%zf)>!+!+;2wN=|%qZaiY^*ZTkW0VeH)c!W~Ky>;85?bT3j<@%)@Y|fxnap zK^J4swWC2170`VLkFYmFODn-qr|6QrHOxpn8&KfBz6f07j1c70FZy-=9HSvX@NkJR zBQQDDx%$0gy)RxD?EH!mW{`z;qqEG=G3TgbHMNmFPKEv z?sFXJhY*VHv&2zm@aTklW_`eq9?X8bE9#t?85tRYbGujvBgV?w+0n@uO7t4BPk`y> z=5LjiH*bDslEW>CIP$NL;7CzS@OfpbQl|sd<^Oa=bz3t&rJYYIe?@114u8M`4hO36 z&rYpM+qG3TOZo#mX5hAbJw*%*@~;+)vNw{D{P{mJnQXc1Ych0#mM~D^u2celuv)0x z_!(OJdXDtLj!4F4<2CQXwQbNU_{PuW5m5ay%f14o(+sBj^S>FO0j@ti_?kyjJT}L4(-xPshA8~sTp@*clYx4$F6CEH2BJus~ejN{J3&8c5&~- zL(+*V_r&QH4fcMHj>1;?}CRY4pE5T$6~M7P21^$@%of0J=BNxKr9R zHCEv5&VHN;eP{QXhl{Na{g>~8 zZzIfySG2poOnzmPYfv1YVV(eTItnZUUM~WxnVWhi!QG9@lil3hP6`hUy(*$>ZRxl+ znh%6dwBC7pwSdj`1ZpzkP7Um;{9fy=r_*$69PaR4r7EPI6sUW(!fUCnZ>UqTCV5p6 za(6S5zhzNkzsUa(r^Q5Q*u@cKq|&&5G_)RFoF*MhYh5txoc1a4`btOoy}6^!$YB6@ zX1yUz|6{X;&H)?TT`|Cn7WZIXj0|8^d^20Zo5%8kTH`g&-ZF@`7^ieUgf06tio; z(Ds)$n(%N!?so46%{Fr`iNR>8CDC%}pj>qL%GJ6lC?$&eDl)=j3% zu+0{Q7Hn#HNc;<*@*Df+3I0x$qSXlb=(vUkn zzsAIvZ?4TAJ6_&U_3;}^aIs>i{CqlW$%31Tgw~XF$oSE_7=AIfq~Ct#`=vNb7n*vs zhbHEl9lx_yEwKQvR>n3w=o}p@wEEfm6Rb6Ugfs|+>S6`4X^aOp8r#k-KN7zPOH8WG z8>yxzyr#F7*knSPUB!I?NBwhWTD3K&s>%!nCvycMcWs0;ndAE>wB#Uj*%Eq~yN-9R zhyFolf5P`Esx&C&h{^~VD*J^JW^lhOW9_$@(}}(#T)Ic%5&L7s^q8@5LjYaIXfr^6q@=K}f#7DnH)wksSXgDLy;T&Z;iccl@Pypg` zdir~R8Yg8F(XonaGFD)3kn+%iGEN#XFy*i|(T%=YTJR2&DIutk;Uk6ruva+(mIj~x_HGYuK zx%IjGw;G<|9!PlLxUv0W?gRkP%bg4jExV#1IU-?rGEss%3my&M(`zj8r2UP7B~~!7 zo$}w8%c%$w3^ok(?Jv2~N0rypLWA~EW8d$$6Z`%XQ!2<++KfEkOnLUbQ@`vrh3-ze zVCsYNJgh#g`?nX2VIO;@A--nbsooo=ha!yxCZ`ygWMW+vbBJ9MG?SG2Cv`z@xWXna95&Z7cf zmTiHhbZfa~(WA!g62+L+gEJoPLQO{v$*MVH0KKoudPen^b*)7If<6GiD-%NF%oHK; z%a^W~KjvRDUKgtT>ulJ;@R1Huk(T_b7yY}m-IXzGm~@s6lJWLV1wB&+*%GzJn&Kmn zf2vO^-IzdrJprtkS9TN?9WJY&6cGH>f`O|RgTb=haZL8Km2=-|od($Hh9Up}*Ju3v z{6H5<{XZu-#CS$kKB<}71%Mpw>eQUMV+cTXX(RQP+=Lx9iJq^~G`L&$L?Zx~Ba<{H zv#srU_zp)_O0*t)C4kgdVU@u|yBzar0atymiH1l8-)ytMVq`D`{Z5_qQNymxl8>&i z;?<)d52Inm_UwRCh^gJXgY9U+6_0GeJRKt7BI_=CDPd5a!24n!^^rvk2Kna03haq> zsAY+vH9bkvaRV*;ex}D&!&jU664>X^iU)b~SVq6EvV}0XF67dq)2~;f2~R}jOL#w# z(_wPJY5c9)A_sxZGXt@Sokd5>>b{5(QDCeIf7?eNSN62H5Ogi-M$s=JEY5%^V!S+K5{J_9FoG=nFgo+E^My(YA8gkoGb0rDtYYAGG zkXj9xE+`6-_5b+yq%$6s;lmpLE3%d@!tE==E`L9Cv1()y01E~2K^AMmhxDd4A?RkvImKID=wlZ zr>VYna(vzW{tPe4DlpIqDyR=KsIQPX8|^S|cBDXQL1z(R)i|V+tmkcd?!?fWS6RA&g)O0U1D)|BEurrsq6P9PaG}hXH3qYG9XJjB>-5Q7pca_%6KH3JJ z8L=!enclXNs!I@{^yQR~z3!9^NekayK+)(m5{4;KSe6Zs(O4wMo^kTkl;ig#;6nm- zs^rHdBkx=fcF7!_H8-R4IACA_)d=g=>?1>yX+vfP=LQP5!Ref1U-8i+GlfX$3Ct=~ z073;yhkJX3Ua!EYyQs(%Mh4$N8#o12J^QyO!kV7mML`zZxh$6viIj9CfC1)~V9Kjx z421QbIG|8s^=&+-C+}2WH@VI?p3{D09QWnODL?r*t@gda!>sRoU0DRCRB_ZWpl-8lRc)tN2i?zoGh(1pX|~+6r7QR z>E==sD9-n&_3rgKb*OV30}rr_LVVHQ8B|sE11M{qz3ghawj?FcwJDNUg8?71-@BfK zkTSN?Xq*0(w!`!E;H(4(t~|+wt-)1!dXutA^R)s^kjg6o4IotY^ZtG$jQ8ya#!ocM z6`y?%!wLkbA5|L9oe6|{S!>J31YL`YPM7wY?%vUW;w=v1!3-E`Ielwm?T1v$uWM^c zf-f@Vj!0hQy%RBNRD_FUyJJC)7Z5+rQ*dpUwC|1!ls>>(*rsT35lTe?+tPK*h)R6S zoxM3>rWu=VxNw_S0~cq3^a|-@W$bwAOGta>N%surKdn~~KA~kQ$Kt;t;-sj@lCw?aD{W`@@dX0k-d z8E7?#IXI{4cPwMMupsd8UoLFQE|}1`g#&b zB(2{k2_d+LFdj8+;+6FXx!bI6u(c+!zG(_~*Cx^=2?Lfwk~U6h#?DSnl8w2}$dId* zYnF!zORce#g|tG2zc7O`g`HUX(*;Yvhu11;E;JON7Ug{c2;h?~MkP=Og8ryVZ;BTBR#+Z^lHL7>46(#cj77^vDsp@Mb zwA*jQOtkVgpjQL6cfSU!DSwKN*Q4qGO7)$(^f?i8D=fN@LFVx@&f@$QgB(|4y>ohH zPAMvPNa1JhyNxMZ4mx8#HL2DR5q>~@6PqQB;PUZ_RdAa3b@i5(SK=0C7kldQJoS(L ztb0(Wq}b+69qMz5p&-A6whRh}OKptA01!5II%h#X8L6WFSoosAJJSjYFg*D?l4wW4 zZLln(k~G=*>)>L)kh(Quu=MOtcgoWl(qG`tJWykMB)kgX$4wvbK?WZOr@XQy>9mHQ z-L*XKMAiq34;q%I0@p9c+W)}wjpaLLj^$}#kqDf6TMpzG7B$^U=49#qhJRRl4!qQ>|! z*UrV$w+JcrsX}#Mw|OO&uvcmDsba#xN;Xw@ep0t)a27*z|5Y5&?{;GF#YWIoKUT!067HDV(2Q$~YEay77n??`MUAa*+;9h*SgEJxegUVox zs*DHH5QasxBPf2UNRH<>KK`DTR+Xe}mf%m{9c}xtCgSJER)=t%YUb|L+1@_Euy=!~ z@9*|!f=#NEGM%U_J7Oe0?&%N1d0HW8iw{XYz=tw9X#==(U-vfPhJs)lT@OI0PT_4a z?(^}|d!}kx`dgx#2bSXL63lviitg$pN!M%+E;hfN>qk=I-rOb(kisiV(FlCE#_Q+< zYbLP|$TH3(2vX?&G^YLIjN1+qXUO1(m7xzAN@yF_w-2aZYDDiZUPYQ#c90O=o11Ub zr&XLB9FwLSp~2mSxg{`U03rD3x6qGJ^!VmsVu-9G0_x;gw*d#ZxaSeSSwms#2qDpp ziC~F!J$n{=l`EPA+BS{8vQaKAlCwF!W-a|Wn>k$#_p?qc?=YrbSX#+aqiamRPeg+P zOz;@3$sZWZ9JY|-o3d( zPYYBmU(SkiiWLX|(A8ZSQT|ZUv_TtNWng9*?nxVh&dh7-*eQ#2=h0_gqAE{%cmRS! zF|QD!V8SKQ8bjhHX|p5X_&Ql{>Ex>2^H1;wp(T&qn7;a{+k(yLs{}lRL3bjA@mEoc zO-8pu1lD^2u2;hZ_oLP3Wg>~4-5UiWrV~nI?4Vx;D0;OCq|&A}Jtu<%L-_XPF6`rQ ztuX1~QC|6u@mu(|%k~v**=ako92c0RfNoId&20-&Rr}k`X6DV*RI|~0uPY2(tQPa7s(-9q`-C|60{VxIbFvpye(_67%V!Sqtj6-6` zZtCNtAZ-_?AF+{jbqTu|IBj^fvh?sPpo-|bTL(wI{exrEDcERfIeH-Ut%1izsHP<* zDLGcJk+BLgid9g89&aL2qHtOKgX@wmuVYiaC?cTG&)PDax9G6~uTT&O-En2-mb;{V z+C<4g%y5*cY+LABlOA^sgQdw;9v-`4F=mRp!0KpxG4RaaUo66mI&Qr&K;8d*syQ^? z-UZI_^Q@cS)HeY|{`*kyS_ki>>71YIUOQijV^Ph$o}wz;3{ zIJkImq~PEaowiYW-2awcN)+9C9}(VQyce7)L87 zA$knKFXhC!HU5+;AMdWCXohm=Qb0hYy{g8CFoE9$F<6iF{YR%X%m6xKZKE%?^c4 z2KRQH1W2ntIvU~(NhE!{S|iZOJw13QW=M`!(f)ZiK|%%bClD_C9VlM(5zu@wGjjUr zRj2$HtfzwWLPwDI1S>h9_FnD>GSI>6n}e;C9Vs}Tjxy>bPcl@Yb(NQo$rSn-msNu_ zK&E#kt=qOT1*ZPK;Pnb-jUCSuwY}OSmoP+E9&QAE#7R?C+;9)t5&mrn(UD*8is$!E3*~1 zl#*(cXi6Zl#+ReB^-DhwGslf^pY5@O*TknS`oDuNNRuvUU^UqJ z;v*T2rLwPoMsU70DE%^w4P5ZAt}|0|@4HWe8Ww%_)xu`<=~4*?2UcX5pfhSWoloXeHM6B6M$m{`QJ7ps< zd+DKmSk}@q)>`%@v#$uKexUE+v&KVtJCWkU0QH*52pCvY^?C7 zHui*l)*@pbZS|HLsQ>hLGWYl-jYgcE$T~*|`gkx%uyFkr^9h9M^)N7Sn3X@&&~q*N zG{~>@C&}&T^=m_#k;5|5%tWj}7ZX(F*5E{s8Yxqk6PW})Xf+NwOHq4{;LWYTb(J3c zwTEl12TAShX!|#Z4rw`-m5BH*-}o(C%!NhN!G4Xl!j9|U2HJ>$B8aH@5cGz6>%JGw zPVM*Y?Dz&Gv%>*F^ChGO>?Co|LGDKzpWO}eS>s^8N9AQN8#HEM6Ri#BV zxM;*kFaYrAlAkNLuB`BA8J>Sr5S6F=;I2H-TdkXe~{>TqxpSaA+s{x+3nhwOMn4b<5v+2l4*A_`ZSa=#2SAa#pL0E z1o&!iu2hkcTSlidWTWy#w=2-vozFo6)YNn*8iVVpnRtn}L{_SbY9L%%Ctto$snVQ& z^2vCT-Th{-yztuY4HWe-{9*_u0;RXCYLhjlAR`FU;E zJwWYfEet8}#u3Quu7_D*gzr07AozMQE4>s1UOKV}#(mSVA-7jxuRDyCF?^r;wRb#W zH7lgHfKDL>+X_#|6sgEe_mZIo#NgQX?m%wjt&ASLZ0--BGO^Qd$$mCkX>LJeBdYLi zrO=!ft^4pJ+SOmfvr4K4bLWH1q|PQ;pcgJ%rxiO3x4?l zlBWA#jM$pXaD!BXaNgW24tTHyzuP)MdYk75x#l$u7-q2l931%W!Hp_tt*E`w(Y|Ku zHsXEHgXPb*LBM*cKwkzuuGRU**`Yq*YGZB7PhdvaQ|^0cYo4p z(qpjE=gqAP2K>_Yfyc$y^&EVOcZ?h=U_ndu}+*GZi_uR zeiZxo>&Ia-rwY`9>6W3vR)N+QUxOa|Ncy1y6g^JorY@wJ|DiHsZ=C;y%IJTPS!rh+UYH5%W2QcJ>Uu1V z$8L7ij>yihp!oiQM{_8D$17OEV%myICQNPhj`U%+Lmr${WP;Q2EEJK#@ZC^FYL)iJ z2_bEt{Pk<=bJ*qE7%YH!-t2W-w!!d@2bY+JiQ`@*_ec%thy+`06s{W zvl11Esc~eIr$h+doXP2eXqTTu==-trTTO^eZ3K|qHtx~Twz9=IL!_67J+9O&o6q3C6 zqf2a#Sem5afk#?0cD{H%R3Quo1T+8ay*Vs0w(k2$ZN_;*yQhrV;2((;0#^V4Y9B|Q z|7ZZC;f|rrn>WcL|8{>7028lrGhUR`8!)b!hY@*$ey^t3*?Ppm|B=A{OF`!~8PXFE%pqR+d|JvldQ zd0gx%Bx?t`h)p8<(nWZYJcF#hX#dShv%mU}dq=A8+CxVn*km+q=l`6F0 zQ5GqZ!TupGc|tt~XR(wy;4z}%l6OS-q9u9)5P)oDsjt6f@p-(v-=b|ZPk|{}npw#D z`+V=Vq*BJ#M!6OP08nsjZEJH%yz&7V0q{ z|BL`PJtC*2Vr(6!M=dFvmP*>RllebXD*zx>$wgI%$Fod6)Ur}O2Ae+peCzq?KF)*< z|A;|T*_Yb#Kk1}mtc3UmcccOk&)S?-XdNFy`A@nUnAkCBX2@>{Es%T~^w0r;cBk3x zWMtD5SyODq(b#OI){QCt9tja6EFa8Day9-~D5Na8jux5ae<*B&I^g?tZN%IzIwGRm zG$;zrLYUlcR$MYzBJsUq1yC@)ob3kbIUphGFX%f;*+2vGt9KmrZ9G!7XMiy)JXmw1 z^eO_rp*Td~Uk@9+IXeoX$)AIR{tg5EU5;PC@Kj1|y7R#ede&4-La>u%O(K#RC(8Co zqKfnQ7o$`gN7W**9@Sg?(|^-kS$JTY3-?hOntok#>tEp}Jb=6Zd;=?(uF_opRLw~E zE=D)7F1*(%TsC$+ljvw#pa6>37x_C=k{NAJy?a1)uR)7Sp#-r!Wj64IIpXsDH9;Dd zw5&Sii!On@PQ{V|Gj##xG=HyXMrz}v{ku(HLJ$MFI*tq5hSS}E~!Il(`|C#4!L?kXLDiZEHY zB84t0rJ83Lz7o1)tYJk{6H$ltjh@roCPZ(9u|-*_I?VUa;wdgz3VvK(nZ(QV%J#!w z#e)uxv0nMXZQ*HMPX}&XGTY=A4;=tU?DMunkV!6~DL@d@-nEt-oU}7IORyBVrYfTmdWcef zPW=MPI^6y}QG-l%0bKfbV&%399s7R~Znvho!loonNZQ$Z)9!*zPnKsZ;!;(ZMtKRM zmS}h;~+a z+E!37w~8}5!_va?;ye4oz3+R^Yh#4-L(?+{BsDeICx}Qzld#~d32rJ?JulXM9$gRP zZ(y%~qBug!+ciQBES3G<=m=~6+0Ui;3wPxz5xCG>va%q}UVDvj_fazKccD6ct&a%g zO5V#2$2Hejd!u+_B0BbP1)X|y0%^LE(UNTLJ=#*w+M4&gF-aMSryn7~EX_3NVtb$e z_@`cxY3(wdQhLb|5H|N)DjJEst?kD2G_$kOd(Jd>gl#$AA=Iz~k`#QkM4}cWkxHp* zur7O&F2ZN{C{#zYS$Zz|d^LsLE1S&rrE;Yu6*neh!cnoo(fBGQ>*PixlWlbTQyKR4 z3cQ$fS>$jKH<^@U87!UkSAN^*PiMGj#29THpXQf-Rh?JbpDaO6eWC4@x^J#`ou|N| z_Vmt&=l@zAotCa8o~ing zeVmEgHiXEEA>0Tk&M#1B;nGD8#J94ySVDXuH`+1I?}N%Xg33EsO~VdH>E<#3p`eWi zY@1Iw-Xq3D;rTAIDtgPrwkyyddVi1ZB$Hd%82-XDD|KJIH$qUBH1f-iOi_4zu{Jp_ zwC1ETVojBwe9NmJS$?fnT-mGPD_bXwRT6!6GyG8Q=#a=0*N0geikS8U43l05=Sal6 z8zf{zWRfTSYE^y9g?bb?FdReC{*=V9vG{O6tYE4acQWMdKQaC_U3xgs--{xEl7RhC6{*#n48MS7>P0%ILeD!VDcLb7U1XpB2EbUX}6#xRrR{_Z(j8&M6vd{XG?R- zfQa16LN{|xUTv^VmzjzbZyGVyK2}qF!PH-z05z4Qq{-}2Oz|!&^A=*M znYKMWK}-?3S=AY&$**b6{SCr;Wi~Br=LYr?qAMHTSa~bhPtzF*ZkLXIr9sA5gF9lYSbu&2-It*#%)$4>G0x;6EK9} zXxroW&WsS$DmM@OpD@9p+e9X(B27K~hiMn301L;kcXJZZWj+OKA~%VF|1X@u>%U%o zJlXWHwp2YnueF02aK8GW-FE)7>o9Z|$LjmR!{Babyt95Xy(@MvB9f8!@2*BcO=gXU zD?9n6oR}VGhua-y9Kx^2HH1D(8q&1dzcIu}zYjMEB#Imo6P$vMw3gcU(tn0?=@ICU z$yO(;C9N}_t9|2x+q!CCBDsGU&+@0!w)u+}7-g$~Oam(s_CB7V3(58GgK0wzdE6xlUp;BX=6ETtcIde!hTYR8V?Z{o!f2iwGaTL22Fo1Wpc^?Ey>n5 z>@f>doqDL~*k8dTgZtNvOiQN|@9DKJl?(!g9qAFZI)EL)aW6G1(0m#`XMJTv;yK#S z6sEsJytu9{r5#0Q8ct0&=AcK@taFn|Sf6|TD;MC4F6RFuOz}D)%HfqTv5`Y^X=r3F z{*3HtUQ3Kz>7jnd9kd-+%OEwK?tW%s_%QqkE;{+2FhVoq{q-2XaSOyiV?1?wa(Gx; zh|tfs1vZQYi+%+dVMwx74T_DnPeu4aD1`%{E$l^kjIUls;R|7wCafKc_K{6k`hs3o z^-1jWe3e)iyzREu;=XI4$G@3S!IK4#L_zGoSlWLLqfNmR=x1m{+4uDP?rBvuL2mf- zI+(dHe9O{`t}P}ypTUF+t({NiZsikY5gr-6}g<}wNlX35FoCVM4 zqoD#eK+c-jr3b~VhVq=~{}ltt!Lg+p=YG+_{C`J38^3u22zT6Vu8C;dAH{Cbc0MdS z!OT!cewgEBofp?k@UB|hi%L-RxwKhKXo@@CxiqI^;PNj}7Tx`ZP60dCf)(5hG1K$S zyHk3;q+Gx2KXTNjO-7|Ca zyU>H6s?luQf*|_yb9#qZ3#UD9WBaCMA@{~QaRcb?w715=WIKeMmI62uZcLm@<%|vH z7u6-Y-yOdB?mo36=+*_g3`854E`RIfKD+|=T_?l?7_Sf}?i`xLh~lqf5l*pQeGvd* zr=at=mq`MBOg9Ub9q7+rwK(*-^)$+Q0XuYyotK~CS`Ill#Q2UZi$6e4K)?sU-4$Cwjg*?hG67mB(zgBP)YUvD3i%T z)z;UUZxe6V2ciQT7!B|ATdd_#wB8%HIo28|@6+AXJwEpED>^$ow<<&#KSL{W? z=hW9GAtTMLOuv?-_8LrUYiSCax_|=yRD)k7Lsa5^NUIN9rtkruK(%90@aKlLjO3of*SesoRZ65gFBh@od)dFaC-Zd90gctgzd}m>kd>%>=TqlqA#R9{jjN?^@9jgx5U}2hqfQygBJN@;pe$VCvS(r9>Ev-5Ej@V39$0qm<97;qtTKoOQ zsq>?1++V1F&DT4x)Eah5!z8O#0j8m)wm?$o0Ai1;Cw+UD7(&x08?Ku-J8cxT2m%>< z9VR<vbFt2nv1n0=K{l0Aae%Dxespt<5O{RF?z0!zrHfXKl zksrR%6Ozwl^1zmav)R1GaVLAH2t1Fq`W_@2 zpEuTe@etjp+$!hc6e2XIFG_n^|^$H~%A!1)Izo`snKIkkO zcE9+m^y->P45IA9l1q%%rUS$%{)M7{s_PuH~q0$_fFk=9Go4p_wwQ(>i zuP%qvyj4M7=q)Nm*kEy!sW*MtIDyFOAV&pMt8EGvmoimK`mh@=*_9UO$=DfaaXXn{ zCG}dacO6W!;S`i~X{R|*TON+J28{~r;Wi44#wbdOB9Eu(QVdnb`lDNLF4H+K(XkPTOD8~U-z1!jRQkivWDwI7$uDs@5}7SLat;nu zr_^Va9$=^IYYTmHX$n|1G&KmT8LXUDM8}Y~9ZnNBpLaMP#4$+igo;U&6SqjSf7<7C z%BN=GW1DCyvI_U^fuAL_>Vd;ZY4Uo$z6-_Y!LY&$ts!9^Pko7rB&MOJS}Z7vDur|x zNp`m3hI+lYRoNi08;02hQ~>v+Z?roN@f7_>GR^;?5$`5OOvOChOH`}w7rh;xnjsu* zu64RIhnW|*T^)We>vCp};^(Vm=GcGPE;rguNK7Sh{nL@suBbm#l#O{;xq1Kg<`C0) zvF@K)H3K!h!QuY^^dKmGn$4D{ncdnV8C+D&e!WC&@XBd3Z}+8^Hz%Xfb>jF$_By_-@qQ!{D5O-!?6`N>cc)d&is##nLu7n8e&#m@(Di}>_)q* z34(ylH&j{mSRwbPDk@|-Qtm+ihzwyMt)#EB#L2;yYq9~tx(wnoez^(Gmg5I|!n~XP zf3&>?Ok8chFS@YeP^`F>wrKIod{sX6vS6$OFq71_BI<7Ik3MS zez40PTeBW_hi;S+2KCZG=6W&t9Qqu zlMkJdIc{voBrXwOn6E`l)^Y85z9;h{^EPUs4_zu@gHqZPF>zChS6$(eqfaiUI-RjqHdC+33Qq{MEd&*vpG5LH+Mxldb4S!s zm<0u2N}XzJcMMwXvQ_C$;^l$=Nt^shfdGZygdL^mSK$N490|q=#-rclC|;|?=X1=& zJv04-z9aX|?#*9iCfOoG|4jb$A2dLKC=_%nN7XaViLrc@qt2gLLV2#XBaY$H*B>uJ^v&LnGJ}bhetTmgoWj^$O``O z=a-8&RwD9$X?UOGg>z$XP&JN*rN}tGX1c`@{!6bL#a;>#Qj>U1?b2WG9Aypr@d{IF z=uIwispMMqe9FlT1dUIW)}P~umoZ>IJN;b+7JvOufTwj(f^k-prr;%?1)B)P z2yEo&?nKi`dc$lnsG0$y`PhYX-HxY{>Bzd@YXbWhiM3y~isf6BmYtYbYIokZROr^` zRaYw_0+uq$(PZ@}JeNatqoiG6Iy{1Z7tn6vZ`Gy?a+---jTJ=J37B$&^j}N^8{+$7_khoDc-vnL>0hIDIeL1^JiY zjegd(KtD{CRaHwdKif96!WK2PO&m%Q(pHr;_@xw}7uFu%gh%~By5EY8*Ql78ek-bUrQW2`x;VX(Pr0Dp%Mtw zqgv{@E`eb;ZfW5fOJEBt4KhUlCjR0qs^(8}X;lZ`kzyoVes(BSs)c(%EDlJb189c# z==)L3lnJsQ!V=b-kNo)Ti2G%VD4<#oJ5*kB+wJ+F%WAkS$pHhEFX$5Pj~CfeQXVoD zz%4P~ruvhj*CG8rEO9}vVJ`^$CgJ}j%S=+SPiRz4@J){-+U-&mV*xI0M>Q375)IvLPT*WLY*evz%csup5VOB6I9M6i){ z77%hCza3*@-3WEmGcw^|d04aJb+#I`+K{H`q{Dxccs)!5FNChF&EqJ}{ssa42-gg4 z+o?B`V^pcpURHvll!i)1h;<2`F|&o=9EbU*QSk~lNK*{zm^{iM*{j|?EJ~tQmpc@jS}@4#1ER)vk;hw<=xT2LmRjwexwqXHjznnx7t4Fu z{K>)=uae7I_#R~sPQ@8flemP!?cLjcqXL0IBg6D@+0ICmG%H-!9IlAdqfA|EWNUMqq4c zawk*8n7ge1ROg@wjDF<59mr{L$=W)8MY_zyvQ%FGdm1`l8*O5Vvet$x7I)v7vqS|| zkxm5Bg|(KaUKTk~Rf`*$y&u#hZvX0k&f}n6_2PwsKO8SXrwls`qD#|@N0t6kDC{*$ zc(v$KeunimEpIw6CvTNMXknvAP47Eq)m{-%adSc3E0vt)D2pn0hx>31tye&*;+A>v z)+K^YrW-{k@J2vmZQ*Fzb@O7_@keGv1v@k`lmr50>UH8b+y;sVF3^)3?W#b(`_A*` zv7I3~VRTKepveO9D2E1Qn%RLq`U!Cg(^%QKpjf9HqOH%^+g}Xze{?I(sXEJ0TfPUc zmi`SK434ZI0&?6pwH+ZbIN8hpM^cp4_UKQx17P1k!pxC?U;E19zE7j1cvwn`d^S{h zW@aX6ZO_T@*FGIgS-&+)nLSmn)u9q~b6vuLsInKznm)I7ka` zYPpMZc7tqE*$-3GRQYuzq*jTAriW6`1823K3KYwKU!{Q+zB5F!jwp(GeZjDGckbN? zJ6fD?N>F>lY!!F>2V*-9L#eeS@3Ax%bb@?kZt)U3qoh~oL!~dpBoFwr z_BX@ptE3O0^1H*}7nUS*%GYlI6;;EhN;>m<_-PJb4e=KT^FDUnconVtUuJ%Ux~cK> zA$N3mul%{+k!$|pNw{Nnty=qH_`fTo94vp5(f%E;OQigZ5_pewL!i&cZyEE=55~vn zXxFOf^xy;}U7WT^{|KKeYtYfLfpWZ1CS|RBw~Hv4ws{QpdP;i_hJj|Nki zMP*DxS=G_dQCYS1P@}&1{@VHBdj9n2Wd5r7!BKTrNjW7oCDpWmZ7k$hVF=0q2|ODN z77Ug!e2>T0@eN`&bbICZ>tsC`JAJ{u4DX?b`{>xTtnkxj_GAPXtwaMSxkX|q0^tC( zf=X_^kk!|@Yl%-##PWMHDPU1HZ)Mn?(!Bk?fA?N^CoMheh|Oi&;$dV}O-wL5XL`Gu zP04wN0`EI+q$fxHTA;l14Y$6kO&6+myXWn~iiV-y>v~}dzI_HAP!;fo%bC@)!Yfr# zZ-z4}9m{%b@z(4W<|MQ`c^;f)xk@H=jFXg5IjK0{^d0@MuA17)r0;=bU=T@VudPOQ zsP3@Yij-Stjla(K=SlPB)%@5mzjV@)FU-YOC67(V#X~aiJQkKsE*v}gkB?e6M)(#d zf*JYwj}d75@8Bi&Xa5RB6~H%bug-XpM6BFzTPy|#pd|5h#qT6Br%gBOvS#Zq{o!!- z1OjPP1i+sL?{0$VKvNjU-hFSvPrePP?AR z6-sJN5W+_je$+wGx%B73xcZjorA&1B&s77^au`0@%%V;3cNf^FSZd_0ub%$z7=uHu z@;*Y-b>sNs!+++hhvPTF5ADo-LP1aqc9(z5Cez=oG2f@xfBkhx(7iI(|Jp0}Ky!Cm zF2k$$D~! zN?zANx-dd29mRJeYTr&9cZ2;dDD_v=0Mp1ngT6 zUMlG<++2McT^MaFJFv3%9@Nm_z^`-4_{I5ni}#{I?(L==4g*N2oIa(&UO2xvmIhvj z41H8j!abxe7%>ywZM>*QJa%!T+jR>`cl*)+ok34r$EWwy_n61bJ+JqBzUlV2SApT2 zG#yzDnZ{LXYirbCun}@OTbfXk#I!jqpv%I-Xp?&H-t<5S=!#oDEYf@3XmmJ3ngzce ztiA*v`N7^473}MB`&LM+waEq+WMCKdQyGDxCB8;Ue9iRoV3~6INZfGtUU2n|$N~M0 zr^0oQqML1(%KNY4#InlfA4Izy2+hDVhOM<9-rqm|ye8wJWRSK~h922!Aq*%u)r|JX zQNNUwXsJAZbOuR3hP2>wB;gVY^33ifXG$&_FJWzZiiY4qmx^R*VKF8ucsMH!~A zSiXOLsI}o-L8lr_Tmxi$Z=otAcA--IbhV&rd&9cmYAZ{c)!rTXFNY$L<5CRPqWlOJOmK z!lW1odx-|4m&Ou1-_rpevdHv2SS}nR(Kh`(D|qvWc8PBdC^G+2()aG z{zvAttxe2IHPP!)CIic;l=Xw6W**mjT}i z%KLHGqthxDCqk=}hg>0qHJ$;a7EAXc;7O)kW%3 zQdrBMjh)ADRb!1OY{#ojGt=To&a2w7g+$a>u`3N+lDE^g#-Jz#Hod3)wNmV&d*@T~ zaQ=)+YLLMo9o-}m%Yo@T-TB7Q;HKas3szTCGsk+&x2UEj`%B;FH+;!vZaIIiTUV76 z9RMBUEF~qi?y(*D3+h^>mJ?{{^62C+H0LBG?L`eo+N-JK<<6n+Za`unwd9wT6;bHq-2 zO6c`b!0Cz|%8|7!d0Q{R@`{xa^js)m0!W1^kANL$OBkkZ)7KZKe`bh{h0~Xf(A3i0 z#JLq0pUUi51B;Q+Og`0!&XH;M(hm;{NzQNgL@D62Fx28N3ubCot79jss1=#qjflW5 z3&F@ff|Z4XLwsh2&whV()l*o zl91-zz&XqG>3!b0bdr>gmO9zpN%&7%jrHig@WP+XpmFx75taNjLF#-qARTyHu> z7HB;Hg}mmq899bpJU%8#5$Rqan29z@ z#4ZgFu8!ukj;o&f{ri;MqB=GdwpSD8h+5{4{6BNF_fr`xGqv!a(Kef6f;vhx)8bbbF5Q#xDz@?`W zya?K@Y%FUVCM5hA8G*4OKPj)@ksB7oh=@l-G9sFMQdoXcuLsz0lK>rIOV`t_gWGoj zT0>C269w%DRe*>6`kmp4=QRHg-;81B!Z$_rH+TM9G?hr0^Xc3r6}mtol-q25W~0mC zjMvenK-6>B5!tRlt;h#8+Ha*{L(9bfqZi#cb#GqHKVFqSf)Da zOh>egn=tEdy?eZbu4;8dptYh44)qLM$Ov=g|6@?n8}viZ7R#S! zYzJK>Yi9K*b|l@*O`d`JH5op1?PEw3Cp4%ViC$Pz0oRE2x{Fa8+2rOSrKuA&WdI@2 zmbJ4|DQL?h3XHDUy!tGw%c(>x3mO*-0BpHG4ENVLgg^SXa19CcH|(U8G$`_L5c#QD zB&LC!116*~uW=M_qpd%XYw(PnJhU+J^76BIi)RAdVz1oO#*e`*tIpOKJ||^G(*?Vg zCu;{Jr-9r&!x$trxO?CwrK%`Yr?|SJ7T0H4gpQ&nk3Xp9!g@^$@2iOSCE~#;JD=X+))y7vm)If&^mm>+(phjA_x#ZO zBam?CV>z_+L2-p>Jl+EnMbp3blP5bsq-3rsv6+rnMB6rGRz^z2m?gD*Y}xO96WxER zDn>>UNl%;perq|Nv%G`>?HgZ8z$Xg_SO8nxdMWXPc)|4tMH@|aiXH z-5%sYS<5#SpSxA1xpy2ExcQ!ODvem3np+$5dk^x+wH^F^-{uiU*MIu-&6T%+8a}Zy?#Z@Pg=Q(_pdRY3b{Ba zzrEF|q@y>GadXn%Y>Id-Wnh_wj+`5QvlDzST`P`B0Dk~HEXRGQphoj2;)nY{kaWeMyXqRy2gbdt|ItI7l=lf)&3*iBxp9XP`qQ9Yr@h6GJlcTt9_)ETfz_>vjGWq^woe zSBVZgJV4Webnc3d2n1HmM<@Nr@06)ThZP>+kvObBap_E>@u~dJFD>FO1gG6VYjClJ zN@OBV2GJhgYagD&_|+l8617!&!-&>Zu}o0CG~JuCMaJb2?dj>YsTs@c&LYU{+s5dm zFYmS}(}b6czF{m88bnu(58nKV2|*x9axJ{D3Upf#P9MU7uJ|h#OT(_VZFPy${pAj; z8r`DlD4VvGGqSs`64ZRl=|j`#64cVrU=7Sazo2j-L(?5A3aB8 z9T1Wj%tW|R&BQDW?DhN`)Pz!Z*GjrwU}$Z+YtQqL1%>eW3HfQr<^nP1%k$R~QlG4f z3uE4`hTKm&&zlasqnK*7bre(_Gg1Risk0yP+do5Ila!iL6V`+4rv)6+n*0WBqX^ea)k=C|`!KJPcoOKJJ|%@7K$ z+I*XQaW$_qGr6G^yL+V!tmYC}z+Q>x$)8yz*Xk>18ePK#4LIIt(Q-#o4IFlj@Hdir z)Mu?@Fzbd^wRE8|9A%;aNP&J^TlyK=I!=`qrtG@r!M~u3?0a=AQlZ)#MDa4*w-%=6 z2RWj)N7Zny8^0SLXMKmfh>dI;H_}ze|9{<-+86C1jWT@K=oj_v4hKEs<(t$C&{>qKTRT1LMiKEJ&k zk6=Iy>29y>VtD1Dz|{~AUF`7du$&emDw1^o1Y97V*f74+^zwyNB+$9;6Afv4jLoi zwW?w-+wN?muFAeciHJhBZ1n9@P?rsg?fvm1V5O+NoJ8xh!QL;!H&mc*^|Pt<7HqYr z18UXa#2&bhwH~l~nNTvO%gA0#x{u9xoN@5+5f1w1tQ*9w?_+zv?hiY!YbisH?(=Gv zNL7K$Skx>GT%~T_A2Pf@#o@kuw*Itz_K@%sQG8nx#W?hqV>j}M4iyep?t>mWaR%Vi zGnrPGKlrn#br6>XKfN}$;$ga4>zZYdP1zO{Z<``s3h3K@tDr71{ta_s+MLkWf#>Ru z8e@j5#_(l2qR!hV3UeQxx@Q=-wtHn_0T26Z1U-K*==tljreKhB-zUd&yIako7nENaR#sG(6d1+802XEqPMrMjX4&+&AKFLi z)d(@XYwpLhG>KgP>;=o9tD4UT6#oV5y zN?Dip?!;Xx?P`U$7YDNS$}4n^SDf`k^QC70Yh31_nj4@^!|7MTldT-?+p zJMO>00zA_vRwS61D&Eatx19R`Y~-}qf;R=K&Z=hAR%KsFbdpJXgn24MV7IsLHmr0l z-jodM*D)w_>KErp$ud$rM)W*I8)~aP4 zP<)p?8YJJ3<}bG}=XDa!gmyGn@a5H@=x3Yct0y;9gbs8a)p#0Vj@<+)2nT+f{mEYO z&AEq)^+@Bxz8$Bsk^JE?ouC6rnBBG?e=jtWEA7wYIG;@lbgbB1CzF)Pp(lj*tV{}1 z_afSGxr05N2V&FWa8KyBCUX5(Wl-C!$B8W_uZompf!`!X}nJBuK#^q3>TeJ!Iv>fCxx z4>ndZn%MX!^W0Mst2%w?mxivGY=8`CALUC|sNdHXR#2ghV|+V7bm6jfl1|}qYVs#r zP#_*hbtgY&YRCD%b3!-PXA4|h**TZ8(7_Io%W5r*L-UGnH+;G$6jf&>gFp0wFXJj8 zZo8w^)ZiO+7GUcRgxi{oL%h?O|B*-Zj#KEZVl*pN#=M-9wzrz0QBBFAlIrYCcZTuvt@qf} z$lIWWhkLCJUPn!BnB&*X1Js@mdjmhiaI$KnDSoC~6FGb3wtm8oQi)s;s(O3T`&dRi zOHO80xrw-ggCs`=-6Mr#(mVCPJdR>O8&vf6b+4S~o6>>OJ7i(M-x2RZ56^vZX1fUh zw=%Dix1S3V^-%*flN|GM4~^s`W#@)8`V(Z=XD$$q99=sLVY+X|M}-@3sQb9Pcv$tdoNK-@uLN)+ELEu z)p%W|Vo}jO1*GA~{obM#og+~-x8$*KZ=Utd&$!~h2~*?a+K(K$F7HxK?vQi-le(!B zz}P~))8$+wRNTMnJB}T>4Qu;3GFw$XT>r{Mwt>_IRsdE=`}vtfQR?OkGko8~DqEGn z)Ownk%DCvTh1mU_ooiK@;N9q4C>1W(dp>x~jGd0$4{Up30VDuEz-C4i_>Bw`roqje zj@N+0^Yj5G@>9LlY;EC3GyoW8(q~acX}oxM^A#0fl=r;TS@LwV=U-tk!-J2%YuCuEPZjw1^$Rok{ zNV#7eVJ6VWPO-7owI?OG9&F$zz}3j@on)BXK<;w)UyZ0`)_LwvS3pAk@KvpJ*SyYj zQ_~bcY)oeyQ4<^ey7%7uxBtk(8qr@mgA7V%jB;3r*&^`mI6$HF+`MvaIJPPR#%kj% z1inKS5uHx}KQcTvC~&Y(3vri_?5(ZYtDxlXwIgPK8h*M#vZa2XhNAJTPTWSX{qWw& zS6j$AZScJA$Z9KOMJ$s*HiB8^oJcw>D=$1HA(%0HbvZ}flou&vgLwe;qL9Bk$3RBT z`A(8E!}<0-m9j;l)o*(}(C3`d@$c=*-Nn=D!h7`TdbAm{`XW^o4M>gJMXE|Fm~9Ew z=A4?1s|F6X{;ft|3A%S}J2q{gbhMQHZvJcH_F5y0TwaJx!|SYZQQgW~?dF6hqm!s4 zlB6TVpVoF><>MQX)etA3qH*5Ok&XD#=VzI6EjQTk)FY*+6v8%1QUvb!rU7=tt@q>0 zoeS_6W82Dmx4Gs49W+m%$x}u)m$&;qiUT8+uri`O!nqnd(>g>E8VF$wpEQvg@O&EO z(=}=CW#^i(I~g{wcRL2M8@-MTb*JxJ%8+5-SdtQ|IATg8Cx%8|;VDmQC~)S}@eC;y z4asWL$tmbw69Z#aaEKoR?@BllO1J%1^DODC%T@VJb{};^ zJ-4b@Iowbd8B2s$ovwS((c#oQw+9H%_R=H4Mt@#IDpDPv?zz#cu#ln&_Be^{p6IFJJY~!QmtjP+*MX{Fqt79HWYV` zr0p&%LZ=r8V|c;=@87$>S<(5YCvy8Qw)+=+E3Kfad%RlPSdtSg`^E+}A1pr`)gKR$ z->egL`UI3ndesfOJ_<&&Pn>k6DRzbUaT+}N^Qm!os;=gv?v@1=u{_6cnXRIlZ~J=~ z!g5$zaX&0NJBy!1oK5|FS6Ap_Sp!ZAju~A*qZ7T_m6~nEkIG%tBpITo?nE9H#jp&B3&Oc#|-n7vr_r=~^5 z-Sh6@TD4GX+P@zEn0C{nvb|*;nF1{&ES8WiD&wH`Em>~0ZVViA>LC^NkqnxV#Ru%6 zM5rYAA+cEz3k9LKx0k22?O1;iyQt#Ep*Wj@A=w@@ONoJgX0t%SHIp?v7w*|5;^Obz zEPltrG)A!2LJCyK+ishU0&o*+MTYL)g4Qx$T%D`qx_2x3sz#?}5dr49&at}kvv<$| z)cN&rKpnbcts@#U03juMTuV#&j-@P)7j@9N+`2wG7*fo)wBy5ar!{oqHLnH=f(4)( zM9K``PUes6{C`b-c*9FYP=Fa5%MVoiFwg-2mzrvsY8H1CO7JefQ%V<)T z7`%Oy9%2(ggKt2!X3Ttx3_VR3+&)$s=F#AZzBC5vaH8Yx4 zA2j9?&kmV5(%AG_85{pTpQc2WGIlx^Hja^%MLp))dh;g()v{1@=DNCKEqe^>vGYhI zyv12Fe*dzjwv&^Sa_B8H=U_2)kM8y0XaDz?F)HH?sB7h9sdx0!yrvsxAV<$c+Kcs* zZWOxv&T+36$_O~%D=q;k4qck5BF6jh!GVyQ-H)mHY^?Yns|mPC=faYnuT=#HF0_lM z>u1iBC_|`GLp2ZB%pahlu(WjHewUMTsA`_!JJ}bEYQM1OLu`u{{vE)II0Qn_xHdQnQwo>_@7D@(t1P*&ec6P0s{KZ#Dzhd+Z<*;e9xS5|U{H{;>pZ>P{X!xEHra673c2szWmN_9 zErs|0OR;{Fs1$(H{DjYhI8XkVS7TuJATPje?ID;&`_TPNx4o6z_{}8M$i(d+QI(Bvr5;xvXMb22NpSC*>t#6c>YU;kd)l;0B>AE~Tk%aUdeBQe4I;Oyw z3KhjA$TC4;=rgKpV%Q7*@J_q@Jc}fRT>bW*-qp@vIKw>O6FPKj2Zw}lgJHq*ZKm9h z^e?LGIpjFL0hLUir@cr9s|nLduPndPV50Om5jzQTMIx49%6Q2C6jv+kx0w#53p87y zs6W^*I3(?!)jCslo$oOWuyi99J0H<9^uRqjDi0~HBWq|BH zFk+0@sCH99RHN(UYGHsHysYPC+&e~*i$!qi*cz()i%aJlEo<-)46sT%r1y*n{y>eW zdg;WGhJg9I9TO=~xjk1F`w-Pu&cooi75jkk*r@`naW=7h5-x%3b6m z&OPeD!A1;`_PJj1OQj>4$}jLA{3q>4mwxjGr}bwZsreFeV{v3;(!WNgMrm3l*wGH> z^>-&@@3iEN_>^U9BP4Q=0RUf#>MLEe;=!OV9WcQ;pz`7b44E*U18TkHtD!VwIqlP0 zeW2$;j@FHZnq$CUO2Z-S+&XH8kc9$Vlcb}Ce?qI<%+OT6bUrl<`S6(iXpG|YyJ<_a zvCiYgH_Z3U_$4uj$ft6dlm17y&P(?zAUh57dy!;KT!7m1YhdN$ z=BF^(r)0!}$F6Rdz9xI)@~endh{n+gegPkLy?4ykW@o-QyCvsXW{JWg^ZDq@GcOdF zKY!!s0MbT&+5QMkAGMU`0)X1qY%{0|~s9^M1!l-NOh>Px!*^(AbxURVndtG=0;r zcG?>bAXwNOz#N=Doqsx{1({YaG6X`m=L=Al$Fk~iEq;bI0Cfk%{wO+&-HYwumaNR& zH#ZHY!4KU9QF~KNsr@<^o?V|sQ^VIYzfJpfM93g8y(zTTp%e5NqzoHzw|$y(^d!%O z=*7t0UyuHz!lM>2Cl`Lpz0Q8}DCTa?NK!^r>H9-;FZ@n}vsjRCMZX(!z&mK8^s7v4 zO4^twt|kJL*7^b%(65ipr`8)4|+b|oNnci?D#!G@D6jf1MLV+TJnp2fOI}V z=G==d#wVTPLo#79`NhCACwvS~7fJ5X7^skoSW<)LGi}j9kzzDkN)h&}u+r2ff-u5& z>%vvHUcH|l8a1K}xxOe6dU@B+5enUZV9ICE8qwsGx5z>bU49OXl47S;BZ1{BPf~t3w9Ay8%E?8MvQHl&j6MQL za&*Y(XJ7bvBx$hy%5p&^auE1$>)=9&38{Ng-CbXM8uqbhZDHnUBQEgn?0iozQgVB& zlNaik$kux+fDmy?c9j16cua2+n`4FBYsh%MK#RP`1ox`8-kQ^afCMK4w(pk;I}IX% z&x!o^9p1r7F9yR?C72pzFm!6k{6H%7QdQ*EKOJ%|_a&ffJzVCV`{uL>Jfz-UHY~A4 zieS`z`LGEaF2lbW>b<3uh{J{v&mUM}Q1>#vZiNVRjF{*^2I25E7?M#^mrv3kmOWDu z_llBYAKa>?Kekq5&{DONRusCX4k1!5KfPWJYYJNOw@-X+=3t0B0-k<{5r~9(6 zem<`$Rl6~&8?jAk@$xyu!bPW!B-3Nt0h+)_p(*ZXS3dZS!?L2|h{em+Q$+T;l_ur+ zUv&|qXzpVBmIG1f*Kh1iO2VaBJjXAu1QHuwztP(X#;e-ww>BK=Z%-BWzFAHJ2poH} z1lmeTn>6liFA_GA^JZp_RlRHKz*~Pf6IvDLKwF!edJM0TK&aY6P=6ALM#Kn)^RLc59GiF`3?yclEbsG=%q8f-K-{|MdrWty ztTuCP=(Jki+-%Tfj8H?O1kjRmSj2EY0~=FokM|;GoXC{;ex3Bv zz;KiAZT_ELfcUV+nuLs&8*y`Sw^})eltH^S&}SF7PA6!em-5YQ2A`8Td#R0hB9r+^ zv;B$n4U8^BuF<=TSi#FhV}>M-Tt#z}XgvCs6~+=6AdG&464V_EQ_)sltQFrtXY+fk z%!(Rt;xXAcYeZVHL?a^5H=1QF>D(3@t0dh+b^6U~mBs%D#8f;YY_8zlO04c_K+uNzUwBi4lpk%@HnA*<=^ZWOVjx=1mgPDU%tZs|lJ&h*+6e z?wm=G7^r~`R9C$m@WXB#0h3dv+Dz6Bk>e$7*EKo;0=nrR6-hOTv$`IeRNyU zXk_|9)7B!u{sIwD1AQ~Ou%EM@tvIYviMc}o>9jAHRZ@Oe(+toA&(PDAAg&KB))JSm zoUk-CbZWC%Fl?W5ZP;)@2QuV0o-2ezuB)@vX$CoQX@-pNzpap7CW!xQEj?byq`XmI zA%wCFR!f^$RtN)ZW-?qR>{POn*e9ciX}w{c`O}AbGV0n}CfnI5Si`sD3uMivy3M~H zHANPp@Ymd+-VCuEGFuAY6{ZFIhC>g3FCi)84|_kZ*Hx%e23D2|Q?*yk>fU~Lu?Wxv z&F~uW5tn9{TGFnxmBaY=BEy~*M$E3s&dOYUHZDpwRk<2<79`wqy}fsFyAA3q$nSSQ zEGKJNxazT|!2*3&Q%Em1-?#2_wy0e5D!vbg;Tti z*EjGKRlZD|ktqs`Vof+a?_?@P2s9lIN z{{Ho{VQrXxwa&ohYr77X;v>~`{%>C9E;+*S6^_Nqp4$(DWHY>|N-mw|%~Tmdoo#he zI-Xj~)g*6Yb6Xz$XoDH3#BzGJL)Ao*1qpx;9KRw?zY;zKvIMz5C^7f>_`^LSUUkcj zDM9IrowJ|AxI*JLc4%ufUoSgJ*N9*TNu-Co1_6FPT9#ql2=sRx> zoUBoNd*#w|e93|gZ8;m9j%MP!+?n!u2Q3WJCzB!|r7Jq7ej!NPzHiF2#=^ue+c^dAKN_33i+gH!*fu_A^Q2BAC0uG1CN0Z z*-$SBx1^PMDw0H4z@WR=y(;fK_-C;d9a=K}%MjC=H#bb}+5rpIy5suG2IQlwyo0?A z7%zsI-C+CLvbUFb-l#0>a+^T8SP$`4_D<#Z&}R$D9r7f_h8$+<_e;T3H^mXt{vGGo zI#0U~&2#TR~^W%zLy5FCa@p(5l$k+w@t(jm66 zP(-31!=Me1^#pwzEpcuUfR=)BaZ2E=bDo zWnre^w=>Kx{(qC2uU!XRP2SSMZvQd`Djez@k`;H~%IR2pDfMCAe27Nux1p8tc6J~< zc6twG-}3*eaJ}a=1{E*;It3&-FS{9Ms%rDa1VfmA&?BYrAE*?y45!2VA`%9RWVr*y zmvG@2fjPAe&Hr$9jelYeg4wUR!~9Z73v{sUX!M}H0($hT2PO!h77vc^P$%)|UPN+) z@mQv(L-aZGA6@e!-OMbV&sB(3Gj!$}DK({+dALFgGjlWIZ~$B3TtjTv_qS~_uG>vR zT|jS~gj%=Qe*Bkd3$U`OJZ#5^Zu`>Q+-05qG_mg>qFeZpP#wW;`n41rbHuG7sixQpkMQA`$|O9^8>HRtSk?6pi4s0 zrWqtPhTYYrGfi;3Qk}5QXZ(ce!Gr;i8vu~voIGB4+I;y;hN{_prFtqYhXXmZw)#~5 zeH%;Z&k57iM>IKU7={v?op-E%&-lwfjv&_E}!0nyzxED(FhaPo%ch;F!q$5?WE z|Nj&)-5mEfa4?nsnrgy(FR=4n>bMg?#Mt@CP=3@uKT1J5vGwmz8R%4;f}nPz&F=?E ze_b_ILD1#&Zw|rae{%LCUBEys!YJq4Kct9pVI&MtopC`ig?Z6f@6crE)!Ju_uemo| zQwpg^l5`#)_p9-5Z`Pkq&eYCr)PzM(6MCFT?ruNdTn4~}e-tNaMk&>Z>qNhm(2=fB-@?Kj-Z>kmeB zL7{DyU(Rjtgt%G0NIwYUBlPJ_9XV3faR*Wh`2|?6uV+%|Q*}g4>ysQS&gqnt9(>xq z8%%-S&3ZUzgfcF?h@{iX=Wq&PD04lpT#*-{98Y2W=1>tmX|BCR)S${}K_IjEYe5P_ z5xx7TX?Y!aw|s*cE!)2baVV&?Mxdv$ehA+bG3;+UWPuANv-<`^>O8<&1$hq42h+t{`Y@)t#DL(<(F|i$8|KW81vB zn(q?5zI=s@!0?Lz*5E;}wOBCe!PY%m( zi(AZpkw=hr7l2CeS@AQ!gRt?k2uQOW1iQDV5vuM)om@4#QkDEb5Z->|@%7k)qPB|D zBlFZsbI^ZJKag72g&Ks1&5J#_xgsq_jAkn+>9i@W@=k{8xqjJwQznISg`;Rx`|Et= zZW+!gBF~mMd|vd8B+l({$T` zxhsu97rfUY27Igp&YP`?`f(Bz+Vu4J6j$%6=iM~{KvHzIVKH_wRliSeUmH~~>YLZ5 z9-qN#BinqREMm=^jN3Go?E~VraUG;@(O#L3Sgh@C-J80|bDezX6@hLw_W=C^qJ@O{tr3!v|Iyz(X6aCq!(PfUCaX)B`yL<^vabP;|$FN&XOWpkx$egOQ zJ`T`MmS7#GqN}G7q#cp-8!_vhtv0|9;iB`tXu74)N%4_M(-?c)U2e9AykbHaFHj`B zppjb&L#9!5P>BOGp176tr{fe)DClH>KDf}1suOYPm0;)PqJxS*#TmWV3|gXh{BB() z5&EWP1@E>TDGo!KgquyM+Hlj9dEanZF)h625eLXDd8i|49 zdU|eO4e9F^q_;QKYczh}ILS#!ruwm!?{x!EVOdyGX5a&3ayqebvo)IqgQQ=j z_#OgkFEpvs1+Gk^2gvt(gVQa|5dn+&E1Kq41iw#M2UXhQ2zL7wt{a+t_SFa|IpntR zVw(iMTbVX5+-yjrFtO6FsyaW7ieHr{q1~Uesj2swPfHz|GO9f z(sTdv7iQoAoBbZs(XBUly<_K)%7+CYIAkl+Mdjl&$dI64Ohr(jWlT9W|YL8c?lC zgtgs7roJOY)~`~PT9P|$U$N)};6VBE!G&;IX!$puqww5Hdn87sg z{l-jfu!3jQZ}F1Vz6uoqQYPD$;t(0@)c*W9qG5Ugq9i7B(w+u!Fo`2DIfKZW+z;dS zN;+9Wi)a_YkIZt8y94tHt#w8DC!D<<=>jBrdJ`hfQi4`(*d%fBsi{wg(R0_zB+ms^ z&XRK<`1trD5MVc3@~b~{Jl>X#g!XRyH_D@&8P2DqMd0sZhG(<|_b(p<`<~T>}JncXtTx79a$N;OVBIVg+>SJAOd zu11y9Gt1wA4cX1qd$X>WdV{u4j!iDjwS^BW0xJ52#SPij4I8^-X>Dg^>uIyMg;a$` z$D{Q6M43|FTcvV`i+*`S4DlbR+IgofdjwN&5Y7RBVa=2nr}wxKL)?J$|LN7yrkp};EN9u(#qow_z*Bdj(@2^GE>wVjO}L-4bAF4d^|`j zs11$0z#TA983Gwgh`6sXej%O~^DRVACTldb_SfS0m54K4OT`dxrK>i#4neKe7M|U` z8psGAt{-F9TXH=^EZ%cb-G4zszWp&izR*rNxs%7hCL=Suz6|sU`sXAGoe3(7^;t2e z58p9x1&bM7Pj_C%qi4#al*(>=>4br^oc*Yih5C2Y_um?1OpjoQ=`qo<+08&9!cSp2 zRd%f-!*IhZ&7sTleEN*jU;kjl#~hw0ui|G$xK5-07nQ?0Dgk=FR(`Lp&;Rz+426J6 z0n?hxtSZM%a1}g>Y<@7>n>PBoY)OWetYs!Z`gI=-6Vs&vEsCJ$<*-T07FaUm&1bep zUr=2?zQ65u4|gr)&1;|ViK#Mw>$rXVsk&ww?aFl< zHrmLzvf*L$b$HqPE-e<^R}1!-GZB6Zq0G0kd(n|iOlDtMtu&8 z)Zx^`=M?*r^7Iajx22_=-hPqTayx64w5MG%ow)c8N5u8wBaWu?He+)Zf1nK3_zzp1 z4j)%m6OLNFQ@dllR8}GD#98vtv9-7Zl5IaD<@pLu&+{ppt%b(m)Pm0b$Iy~}y5dwt z3<9il#|hyi(w`^Qnxa*U4AR^L(8G3!0>;b}bPG$$F67o)U5~3wajRLadCjh3*Yg5T zdq+xPHNp;ho-B83ne;M!;+gfoH7@1N7S*6>gN~Hg)M(*LVYxi3-Py z<9s~zv#bu4Oa<>lFVtf9cmb5_2|31U(>ovH(Eu81rfaZM6e`qt2ms>5ujPoaNAMR8 z$#EeK+|spB_;`GWav zKnFW*cHL=P-X&;R{=uYIkIi8#u91g`1Sp0b~EjZ#;} zaP)HCD>=6M<(OQRI-HYfKUw0EW_OveU2{e2vo2^OdtkvI zvKpcVjNR$k8)Y>hZnfa4&dM|-|4FZPXj-ojq+P^X=!gDNqkuMQj46;6N8hH8 z;!X1+59r3Zu;gOkyBzdc@@}O>& zRQfu)ZF?N?qarV6pOJmQJ)vkjVd@KJW$~-VQL#MNviu*oQGKr+#?TSLHGlm&##ytA zXtcx2Cr~;a;;w8^FYgs)U# zM6GzmO82mq50a0H-5$XK;6H|!L%c-%4E?`;pR>-Hv(B61AYt(Ra~WNTT*rJMf%h2R z4XYJTOkmTf2~3WI>NF{8Q!K>#QuqZP`{u{Ma3 z`r&Dg$1jr?Bu1}L%g%hm<)=uZCrvDD+>^tU0&}c8cS&jn;2Q`1hE)j(2_8c`qCukY zv7I+tOzeplWj~mv7rGDVEv*e@Hzx6bUX)|;QNQ-x=B{U z34^^>J3bJnjf3|D4&a?Z26XO_5)uI_?P2g;0D_7Y7rzR+Bxd`ejQ_z)21v`gsc3J8 z;)o-n!4u{mI{Hspn~=zKQv3NfFa`62sWr7*m7w>N;=_g%l7jJC*il3I(bKv0_%?0T zLRsUtf}PcLEeEYvZ37u^gLE^`gN*_O1ixJx%ZKmc*B;Pt@aIPJ&N8wALzfqKW-L&v zj8GRd)T3=PH-OQvFGB%pPiNlh{|R8Drud*eca=c@{UHh*BP=}aBzmz|*(W@|T$plr6~6LZ)?NBkDe6uoM`VTH z_O|x?g-+`)fC?MZWYO?siy#~BUu9f+skmKVKVqZA_kmc$yfTn*>JUf&dP!&=*H_NC zXr3>+KCuyW11HDq%LVw!Fb!YqT&cl*ymXG3>PE+5k%Rz9btl*$SI*#wmm+kI-XL8| zbH!I>z_B0p6ZxFoNFitf1kr_=?Les~sBcYj?ELP6{Uhs{MwSPJgm$w3mqD9vpI%JH zbQA-Y3qTJroneK`=3Tos(Og*Zc+I&KUsfC9mi3)y<(}3^DJ|Lw{eLExH5rKkg8%Nv zS~>yfTL4R5-WENt8jGrFyWUo?p5Q*99G6;yfBN|y{8JcDN_nI5X?*hRY@O45g@;FT zL5t6H=E3Q~`t?jjO-pHW0dq|?HOnmRJo<{LD2k|%yu9FTx}W+_0Jve4o9io#;4Q(2 zw)H6C-msQ5x7NGge0OV(vfaz6Nceg1BRsXL3F^Z~R29+5x;JW{P!cGl$xHQ?IeY~$ zL}Ua8GRH4A9Kj)f;`03zA^z!N?0Hq&L|tksW~*NjqllxKAKczmOxFSj23X1c06^ga z3xEYol%^EDO6kfDj=Du#*DNv`62E)ImqM9x$Wxqz@Lh>G5*86M3`io!B|XU}&{=FO zo4()3a8a}$EYw`)&Y zYbqSyp9W#}jO0PXdf2l)Mlw5ZX;zf`O;1$F ze(*HugvnW^sd@-22&Rm51eDnOdoKX!lWxG+fK%&Ettk+;hbX!9i+eY^y(5ajL0!Hp zf#Yk+^98gOuH36_H*E3_5)^=!Gx)~|!!c;n&n;2UTN~esO3I?BHVEid;FO~dJU)7~ zz`qy^oSgUc{WyHJz(ZJ!(M*ixc)qmLN3&5MlJ{jn zqZo#N@6qfcPI$+Gts45hl>_ya6(d{OnSflK4bx-H;K>oeKfQ{7!Nu=Faeu=_1m+h} zw~*4_3zhfI*WHm)XK_-=pmWqsTnW&RL(D4FIKffK9qfnBH?t}>V-Xedgnu0`FkfBi zV^|+F^6}m{TOmy%gE|@>RxP)oK|U_5;U#{a;=p8x6$NjjF|A0o`z#nUZPF#5&@}CFbV? z{xJK*>T_#O7#2Ft;pdHoTAjKmM_YjyoGKkMP#ao98T^GkCSz3}<3wSCKufyLr&Ry1 zPI4FXnLG8dF!!L>hO5fShuWiUW2w&SH>!k(=A+?_e{XvinC@>wLgphx)r197-iJTg zH?lAQznxfz=X9IN*T}E)r6PtEMKhykpPM?IAo^;v&zEqB=<&PL&-=Oq)Y?!ga@P<gM%A2dbjlH_|en?yS2`|z#% zmuf18K4&IQ5P%U_PFJpgX!WtVaO695A>gAhqb-its8Crc(0rxcvQEkTUi0|ui;%C5 za~R}v;V-OZYj!|5HVmNCv<49t0!9HKsAUtYLBR6yKn4PqS47y`Dm0pJZ%10+E#Ef| zXD;rhFMTcqO^7ryw;`%`VXllBO;nV4G&WzJ`mcwpD+`5L)!d#L0Qif>+Ugkxyu2!V zKSo*(qR$?k$>WI@fDqdi>QC~`d52^Xz=%C)nhQiD_)u0sQ*n%}b%tKYmLRf`frVj% zSvjlsdQC(hwI5z(DfY=?#uB%6f!{!=JLM283!4F}k^I=(FeU-#D~jhc1rME?uXFqG zvm}p+DE@PWoxS7hmD7Qr28f4yiipji8RonAL(;IxkfI_*!R*iI+iQ3+cU`OYWKJ-y zNQnVd^<<*)gyl{FF9UkXx35vCZe~8NjiVtzr3`H`^Ee8;l@viwr^^5f>atg|Rt26P zOwnRzeRc$D^wi0@&7mr5`Ki?UtP>nK1!+n%B6M;yBfh4T6pp3x2J~7Xy;2sfe}(

x)CLFjymj((}C!|^O4+{-IVn36<(H2mt(Bl_} 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�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 Date: Thu, 12 Jan 2023 17:16:17 +0200 Subject: [PATCH 006/116] Improve Device Profile Entity --- .../server/dao/model/ModelConstants.java | 3 + .../dao/model/sql/DeviceProfileEntity.java | 15 ++++ .../main/resources/sql/schema-entities.sql | 71 ++++++++++++++++--- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index e24bde91fb..3736a236e6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -185,6 +185,9 @@ public class ModelConstants { public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key"; public static final String DEVICE_PROFILE_FIRMWARE_ID_PROPERTY = "firmware_id"; public static final String DEVICE_PROFILE_SOFTWARE_ID_PROPERTY = "software_id"; + public static final String DEVICE_PROFILE_CERTIFICATE_HASH_PROPERTY = "certificate_hash"; + public static final String DEVICE_PROFILE_CERTIFICATE_VALUE_PROPERTY = "certificate_value"; + public static final String DEVICE_PROFILE_CERTIFICATE_REGEX_PATTERN_PROPERTY = "certificate_regex_pattern"; /** * Asset profile constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index 893c9d549c..2aaaf61197 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -106,6 +106,15 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) private UUID externalId; + @Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_VALUE_PROPERTY) + private String certificateValue; + + @Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_HASH_PROPERTY) + private String certificateHash; + + @Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_REGEX_PATTERN_PROPERTY) + private String certificateRegexPattern; + public DeviceProfileEntity() { super(); } @@ -123,6 +132,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl this.image = deviceProfile.getImage(); this.transportType = deviceProfile.getTransportType(); this.provisionType = deviceProfile.getProvisionType(); + this.certificateHash = deviceProfile.getCertificateHash(); + this.certificateValue = deviceProfile.getCertificateValue(); + this.certificateRegexPattern = deviceProfile.getCertificateRegexPattern(); this.description = deviceProfile.getDescription(); this.isDefault = deviceProfile.isDefault(); this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class); @@ -182,6 +194,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl deviceProfile.setDefaultDashboardId(new DashboardId(defaultDashboardId)); } deviceProfile.setProvisionDeviceKey(provisionDeviceKey); + deviceProfile.setCertificateHash(certificateHash); + deviceProfile.setCertificateValue(certificateValue); + deviceProfile.setCertificateRegexPattern(certificateRegexPattern); if (firmwareId != null) { deviceProfile.setFirmwareId(new OtaPackageId(firmwareId)); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 73039274a6..c4c3317009 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -271,24 +271,75 @@ CREATE TABLE IF NOT EXISTS device_profile ( provision_type varchar(255), profile_data jsonb, description varchar, - search_text varchar(255), + search_text varchar +( + 255 +), is_default boolean, tenant_id uuid, firmware_id uuid, software_id uuid, default_rule_chain_id uuid, default_dashboard_id uuid, - default_queue_name varchar(255), + default_queue_name varchar +( + 255 +), provision_device_key varchar, + certificate_value varchar; + certificate_hash varchar, + certificate_regex_pattern varchar +( + 255 +), external_id uuid, - CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), - CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), - CONSTRAINT device_profile_external_id_unq_key UNIQUE (tenant_id, external_id), - CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id), - CONSTRAINT fk_default_dashboard_device_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id), - CONSTRAINT fk_firmware_device_profile FOREIGN KEY (firmware_id) REFERENCES ota_package(id), - CONSTRAINT fk_software_device_profile FOREIGN KEY (software_id) REFERENCES ota_package(id) -); + CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE +( + certificate_hash +), + CONSTRAINT device_profile_name_unq_key UNIQUE +( + tenant_id, + name +), + CONSTRAINT device_provision_key_unq_key UNIQUE +( + provision_device_key +), + CONSTRAINT device_profile_external_id_unq_key UNIQUE +( + tenant_id, + external_id +), + CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY +( + default_rule_chain_id +) REFERENCES rule_chain +( + id +), + CONSTRAINT fk_default_dashboard_device_profile FOREIGN KEY +( + default_dashboard_id +) REFERENCES dashboard +( + id +), + CONSTRAINT fk_firmware_device_profile FOREIGN KEY +( + firmware_id +) REFERENCES ota_package +( + id +), + CONSTRAINT fk_software_device_profile FOREIGN KEY +( + software_id +) REFERENCES ota_package +( + id +) + ); DO $$ From 5d35fa1f261dfa2e421a786a184b87b04776f461 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 12 Jan 2023 17:18:29 +0200 Subject: [PATCH 007/116] Add ability to upgrade device credentials if chain match with uploaded device profile certificate. --- .../transport/DefaultTransportApiService.java | 62 +++++++++- common/cluster-api/src/main/proto/queue.proto | 65 ++++++---- .../dao/device/DeviceCredentialsService.java | 4 +- .../dao/device/DeviceProfileService.java | 2 + .../server/common/data/DeviceProfile.java | 9 +- .../mqtt/MqttSslHandlerProvider.java | 60 ++++++++- .../transport/mqtt/MqttTransportHandler.java | 115 +++++++++++++----- .../common/transport/TransportService.java | 9 ++ ...idateDeviceProfileCredentialsResponse.java | 28 +++++ .../service/DefaultTransportService.java | 29 ++++- .../server/common/transport/util/SslUtil.java | 17 +++ .../dao/device/DeviceCredentialsDao.java | 8 ++ .../device/DeviceCredentialsServiceImpl.java | 10 +- .../dao/device/DeviceProfileCacheKey.java | 14 ++- .../server/dao/device/DeviceProfileDao.java | 2 + .../dao/device/DeviceProfileServiceImpl.java | 10 ++ .../device/DeviceCredentialsRepository.java | 5 + .../sql/device/DeviceProfileRepository.java | 2 + .../sql/device/JpaDeviceCredentialsDao.java | 5 + .../dao/sql/device/JpaDeviceProfileDao.java | 5 + 20 files changed, 388 insertions(+), 73 deletions(-) create mode 100644 common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index f8b42ecc1e..9bc61d6493 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -65,8 +65,8 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; @@ -90,11 +90,15 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetSnmpDevicesRespon import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceProfileCredentialsResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceProfileX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; @@ -108,6 +112,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH; @@ -128,6 +134,7 @@ public class DefaultTransportApiService implements TransportApiService { private final TbTenantProfileCache tenantProfileCache; private final TbApiUsageStateService apiUsageStateService; private final DeviceService deviceService; + private final DeviceProfileService deviceProfileService; private final RelationService relationService; private final DeviceCredentialsService deviceCredentialsService; private final DbCallbackExecutorService dbCallbackExecutorService; @@ -159,6 +166,13 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); + } else if (transportApiRequestMsg.hasValidateProfileX509CertRequestMsg()) { + ValidateDeviceProfileX509CertRequestMsg msg = transportApiRequestMsg.getValidateProfileX509CertRequestMsg(); + result = validateDeviceProfileCertificate(msg.getHash()); + } else if (transportApiRequestMsg.hasUpdateOrCreateDeviceCertRequestMsg()) { + UpdateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getUpdateOrCreateDeviceCertRequestMsg(); + DeviceProfile deviceProfile = deviceProfileCache.find(new DeviceProfileId(new UUID(msg.getDeviceProfileIdMSB(), msg.getDeviceProfileIdLSB()))); + result = updateOrCreateDeviceCredentials(msg.getHash(), msg.getValue(), msg.getCommonName(), deviceProfile, DeviceCredentialsType.X509_CERTIFICATE); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { @@ -226,6 +240,31 @@ public class DefaultTransportApiService implements TransportApiService { } } + private ListenableFuture validateDeviceProfileCertificate(String credentialsId) { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(credentialsId); + if (deviceProfile != null) { + return getDeviceProfileInfo(deviceProfile); + } + return getEmptyTransportApiResponseFuture(); + } + + private ListenableFuture updateOrCreateDeviceCredentials(String credentialsId, + String credentialsValue, + String deviceCN, + DeviceProfile deviceProfile, + DeviceCredentialsType credentialsType) { + String deviceName = extractRegex(deviceCN, deviceProfile.getCertificateRegexPattern()); + // find deviceCredentials by deviceName (device exists) + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByTenantIdAndDeviceName(deviceProfile.getTenantId(), deviceName); + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == credentialsType) { + deviceCredentials.setCredentialsId(credentialsId); + deviceCredentials.setCredentialsValue(credentialsValue); + deviceCredentialsService.updateDeviceCredentials(deviceProfile.getTenantId(), deviceCredentials); + return getDeviceInfo(deviceCredentials); + } + return getEmptyTransportApiResponseFuture(); + } + private ListenableFuture validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); if (credentials != null) { @@ -507,6 +546,18 @@ public class DefaultTransportApiService implements TransportApiService { }, MoreExecutors.directExecutor()); } + private ListenableFuture getDeviceProfileInfo(DeviceProfile deviceProfile) { + ValidateDeviceProfileCredentialsResponseMsg.Builder builder = ValidateDeviceProfileCredentialsResponseMsg.newBuilder() + .setDeviceProfileIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) + .setIsDeviceProfileFound(true); + + return Futures.immediateFuture( + TransportApiResponseMsg.newBuilder() + .setValidateDeviceProfileResponseMsg(builder.build()) + .build()); + } + private DeviceInfoProto getDeviceInfoProto(Device device) throws JsonProcessingException { DeviceInfoProto.Builder builder = DeviceInfoProto.newBuilder() .setTenantIdMSB(device.getTenantId().getId().getMostSignificantBits()) @@ -664,4 +715,13 @@ public class DefaultTransportApiService implements TransportApiService { private Long checkLong(Long l) { return l != null ? l : 0; } + + private String extractRegex(String commonName, String regex) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(commonName); + if (matcher.find()) { + return matcher.group(0); + } + return commonName; + } } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 5a28d42546..405b96726a 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -169,6 +169,18 @@ message ValidateDeviceX509CertRequestMsg { string hash = 1; } +message ValidateDeviceProfileX509CertRequestMsg { + string hash = 1; +} + +message UpdateOrCreateDeviceX509CertRequestMsg { + string hash = 1; + string value = 2; + string commonName = 3; + int64 deviceProfileIdMSB = 4; + int64 deviceProfileIdLSB = 5; +} + message ValidateBasicMqttCredRequestMsg { string clientId = 1; string userName = 2; @@ -181,6 +193,12 @@ message ValidateDeviceCredentialsResponseMsg { bytes profileBody = 3; } +message ValidateDeviceProfileCredentialsResponseMsg { + int64 deviceProfileIdMSB = 1; + int64 deviceProfileIdLSB = 2; + bool isDeviceProfileFound = 3; +} + message GetOrCreateDeviceFromGatewayRequestMsg { int64 gatewayIdMSB = 1; int64 gatewayIdLSB = 2; @@ -885,33 +903,36 @@ message VersionControlResponseMsg { message TransportApiRequestMsg { ValidateDeviceTokenRequestMsg validateTokenRequestMsg = 1; ValidateDeviceX509CertRequestMsg validateX509CertRequestMsg = 2; - GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3; - GetEntityProfileRequestMsg entityProfileRequestMsg = 4; - LwM2MRequestMsg lwM2MRequestMsg = 5; - ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; - ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7; - ValidateDeviceLwM2MCredentialsRequestMsg validateDeviceLwM2MCredentialsRequestMsg = 8; - GetResourceRequestMsg resourceRequestMsg = 9; - GetOtaPackageRequestMsg otaPackageRequestMsg = 10; - GetSnmpDevicesRequestMsg snmpDevicesRequestMsg = 11; - GetDeviceRequestMsg deviceRequestMsg = 12; - GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; - GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14; + ValidateDeviceProfileX509CertRequestMsg validateProfileX509CertRequestMsg = 3; + GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 4; + GetEntityProfileRequestMsg entityProfileRequestMsg = 5; + LwM2MRequestMsg lwM2MRequestMsg = 6; + ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 7; + ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 8; + ValidateDeviceLwM2MCredentialsRequestMsg validateDeviceLwM2MCredentialsRequestMsg = 9; + GetResourceRequestMsg resourceRequestMsg = 10; + GetOtaPackageRequestMsg otaPackageRequestMsg = 11; + GetSnmpDevicesRequestMsg snmpDevicesRequestMsg = 12; + GetDeviceRequestMsg deviceRequestMsg = 13; + GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 14; + GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 15; + UpdateOrCreateDeviceX509CertRequestMsg updateOrCreateDeviceCertRequestMsg = 16; } /* Response from ThingsBoard Core Service to Transport Service */ message TransportApiResponseMsg { ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1; - GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; - GetEntityProfileResponseMsg entityProfileResponseMsg = 3; - ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 4; - GetSnmpDevicesResponseMsg snmpDevicesResponseMsg = 5; - LwM2MResponseMsg lwM2MResponseMsg = 6; - GetResourceResponseMsg resourceResponseMsg = 7; - GetOtaPackageResponseMsg otaPackageResponseMsg = 8; - GetDeviceResponseMsg deviceResponseMsg = 9; - GetDeviceCredentialsResponseMsg deviceCredentialsResponseMsg = 10; - repeated GetQueueRoutingInfoResponseMsg getQueueRoutingInfoResponseMsgs = 11; + ValidateDeviceProfileCredentialsResponseMsg validateDeviceProfileResponseMsg = 2; + GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 3; + GetEntityProfileResponseMsg entityProfileResponseMsg = 4; + ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 5; + GetSnmpDevicesResponseMsg snmpDevicesResponseMsg = 6; + LwM2MResponseMsg lwM2MResponseMsg = 7; + GetResourceResponseMsg resourceResponseMsg = 8; + GetOtaPackageResponseMsg otaPackageResponseMsg = 9; + GetDeviceResponseMsg deviceResponseMsg = 10; + GetDeviceCredentialsResponseMsg deviceCredentialsResponseMsg = 11; + repeated GetQueueRoutingInfoResponseMsg getQueueRoutingInfoResponseMsgs = 12; } /* Messages that are handled by ThingsBoard Core Service */ diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java index 42d6d10a1a..150106e3cb 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.dao.device; +import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; -import com.fasterxml.jackson.databind.JsonNode; public interface DeviceCredentialsService { @@ -26,6 +26,8 @@ public interface DeviceCredentialsService { DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId); + DeviceCredentials findDeviceCredentialsByTenantIdAndDeviceName(TenantId tenantId, String deviceName); + DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index 2e65e159cd..6ead3e08c5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -39,6 +39,8 @@ public interface DeviceProfileService extends EntityDaoService { PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType); + DeviceProfile findDeviceProfileByCertificateHash(String credentialsId); + DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName); DeviceProfile createDefaultDeviceProfile(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index a66f3a40d9..2a5b11f1ff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; @@ -68,6 +66,13 @@ public class DeviceProfile extends SearchTextBased implements H private DeviceTransportType transportType; @ApiModelProperty(position = 15, value = "Provisioning strategy.") private DeviceProfileProvisionType provisionType; + @ApiModelProperty(position = 16, value = "CA certificate value") + private String certificateValue; + @ApiModelProperty(position = 17, value = "CA certificate hash") + private String certificateHash; + @ApiModelProperty(position = 18, value = "Regex for fetch deviceName from CN") + private String certificateRegexPattern; + @ApiModelProperty(position = 7, value = "Reference to the rule chain. " + "If present, the specified rule chain will be used to process all messages related to device, including telemetry, attribute updates, etc. " + "Otherwise, the root rule chain will be used to process those messages.") diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 98aaca7f06..ed2326946f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -24,12 +24,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.common.transport.util.SslUtil; @@ -142,8 +143,15 @@ public class MqttSslHandlerProvider { } @Override - public void checkClientTrusted(X509Certificate[] chain, - String authType) throws CertificateException { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + String deviceCN = SslUtil.parseCommonName(chain[0]); + String deviceCert; + String deviceCredentialsValue = SslUtil.getCertificateString(chain[0]); + try { + deviceCert = EncryptionUtil.getSha3Hash(SslUtil.getCertificateString(chain[0])); + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } String credentialsBody = null; for (X509Certificate cert : chain) { try { @@ -152,13 +160,53 @@ public class MqttSslHandlerProvider { final String[] credentialsBodyHolder = new String[1]; CountDownLatch latch = new CountDownLatch(1); transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback() { + new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { if (!StringUtils.isEmpty(msg.getCredentials())) { credentialsBodyHolder[0] = msg.getCredentials(); + latch.countDown(); + } else { + transportService.process(DeviceTransportType.MQTT, + TransportProtos.ValidateDeviceProfileX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceProfileCredentialsResponse msg) { + if (msg.isDeviceProfileFound()) { + transportService.process(DeviceTransportType.MQTT, + TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg.newBuilder() + .setHash(deviceCert) + .setCommonName(deviceCN) + .setDeviceProfileIdMSB(msg.getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(msg.getDeviceProfileId().getId().getLeastSignificantBits()) + .build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + System.out.println("msg.getCredentials() = " + msg.getCredentials()); + credentialsBodyHolder[0] = msg.getCredentials(); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + } + ); + } else { + latch.countDown(); + } + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + }); } - latch.countDown(); } @Override @@ -168,7 +216,7 @@ public class MqttSslHandlerProvider { } }); latch.await(10, TimeUnit.SECONDS); - if (strCert.equals(credentialsBodyHolder[0])) { + if (deviceCredentialsValue.equals(credentialsBodyHolder[0])) { credentialsBody = credentialsBodyHolder[0]; break; } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 7515a1fcd2..5044d5a35e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -21,7 +21,6 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.MqttConnAckMessage; -import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import io.netty.handler.codec.mqtt.MqttFixedHeader; @@ -63,12 +62,12 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; @@ -80,7 +79,7 @@ import org.thingsboard.server.transport.mqtt.util.ReturnCodeResolver; import javax.net.ssl.SSLPeerUnverifiedException; import java.io.IOException; import java.net.InetSocketAddress; -import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; @@ -90,16 +89,15 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.amazonaws.util.StringUtils.UTF8; -import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT; import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP; import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK; -import static io.netty.handler.codec.mqtt.MqttMessageType.UNSUBACK; import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE; import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; @@ -810,9 +808,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement deviceSessionCtx.setProvisionOnly(true); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SUCCESS, msg)); } else { - X509Certificate cert; - if (sslHandler != null && (cert = getX509Certificate()) != null) { - processX509CertConnect(ctx, cert, msg); + X509Certificate[] chain; + if (sslHandler != null && (chain = getX509Certificate()) != null) { + processX509CertConnect(ctx, chain, msg); } else { processAuthTokenConnect(ctx, msg); } @@ -848,27 +846,82 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement }); } - private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert, MqttConnectMessage connectMessage) { + private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate[] chain, MqttConnectMessage connectMessage) { try { - if (!context.isSkipValidityCheckForClientCert()) { - cert.checkValidity(); + String deviceCN = SslUtil.parseCommonName(chain[0]); + String deviceCertHash = EncryptionUtil.getSha3Hash(SslUtil.getCertificateString(chain[0])); + for (X509Certificate cert : chain) { + try { + String strCert = SslUtil.getCertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + final ValidateDeviceCredentialsResponse[] validateDeviceCredentialsResponses = new ValidateDeviceCredentialsResponse[1]; + CountDownLatch latch = new CountDownLatch(1); + transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + validateDeviceCredentialsResponses[0] = msg; + latch.countDown(); + } else { + transportService.process(DeviceTransportType.MQTT, + TransportProtos.ValidateDeviceProfileX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceProfileCredentialsResponse msg) { + if (msg.isDeviceProfileFound()) { + transportService.process(DeviceTransportType.MQTT, + TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg.newBuilder() + .setHash(deviceCertHash) + .setCommonName(deviceCN) + .setDeviceProfileIdMSB(msg.getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(msg.getDeviceProfileId().getId().getLeastSignificantBits()) + .build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + validateDeviceCredentialsResponses[0] = msg; + latch.countDown(); + } + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + } + ); + } else { + latch.countDown(); + } + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + }); + } + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + if (validateDeviceCredentialsResponses[0] != null && validateDeviceCredentialsResponses[0].hasDeviceInfo()) { + onValidateDeviceResponse(validateDeviceCredentialsResponses[0], ctx, connectMessage); + break; + } + } catch (InterruptedException | CertificateEncodingException e) { + log.error(e.getMessage(), e); + } } - String strCert = SslUtil.getCertificateString(cert); - String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - transportService.process(DeviceTransportType.MQTT, ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback<>() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - onValidateDeviceResponse(msg, ctx, connectMessage); - } - - @Override - public void onError(Throwable e) { - log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e); - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - ctx.close(); - } - }); } catch (Exception e) { context.onAuthFailure(address); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.NOT_AUTHORIZED_5, connectMessage)); @@ -877,17 +930,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private X509Certificate getX509Certificate() { + private X509Certificate[] getX509Certificate() { try { - Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificates(); - if (certChain.length > 0) { - return (X509Certificate) certChain[0]; - } + return (X509Certificate[]) sslHandler.engine().getSession().getPeerCertificates(); } catch (SSLPeerUnverifiedException e) { log.warn(e.getMessage()); return null; } - return null; } private MqttConnAckMessage createMqttConnAckMsg(ReturnCode returnCode, MqttConnectMessage msg) { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index f1d3f7e26c..21e668cf37 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; @@ -52,8 +53,10 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMs import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; +import org.thingsboard.server.gen.transport.TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceProfileX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; @@ -87,6 +90,12 @@ public interface TransportService { void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateDeviceProfileX509CertRequestMsg msg, + TransportServiceCallback callback); + + void process(DeviceTransportType transportType, UpdateOrCreateDeviceX509CertRequestMsg msg, + TransportServiceCallback callback); + void process(ValidateDeviceLwM2MCredentialsRequestMsg msg, TransportServiceCallback callback); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java new file mode 100644 index 0000000000..cfbe79bf96 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.auth; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.DeviceProfileId; + +@Data +@Builder +public class ValidateDeviceProfileCredentialsResponse { + + private final DeviceProfileId deviceProfileId; + private final boolean isDeviceProfileFound; +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index bf03122b18..a41c1d6055 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -71,8 +71,8 @@ import org.thingsboard.server.common.transport.TransportTenantProfileCache; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.limits.TransportRateLimitService; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; @@ -97,6 +97,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbTransportQueueFactory; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.queue.util.AfterStartUp; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbTransportComponent; import javax.annotation.PostConstruct; @@ -427,6 +428,32 @@ public class DefaultTransportService implements TransportService { AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } + @Override + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceProfileX509CertRequestMsg requestMsg, + TransportServiceCallback callback) { + log.trace("Processing msg: {}", requestMsg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setValidateProfileX509CertRequestMsg(requestMsg).build()); + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.ValidateDeviceProfileCredentialsResponseMsg msg = tmp.getValue().getValidateDeviceProfileResponseMsg(); + ValidateDeviceProfileCredentialsResponse.ValidateDeviceProfileCredentialsResponseBuilder result = ValidateDeviceProfileCredentialsResponse.builder(); + DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(msg.getDeviceProfileIdMSB(), msg.getDeviceProfileIdLSB())); + result.deviceProfileId(deviceProfileId); + result.isDeviceProfileFound(msg.getIsDeviceProfileFound()); + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } + + @Override + public void process(DeviceTransportType transportType, TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg requestMsg, + TransportServiceCallback callback) { + log.trace("Processing msg: {}", requestMsg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder() + .setUpdateOrCreateDeviceCertRequestMsg(requestMsg).build()); + doProcess(transportType, protoMsg, callback); + } + @Override public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index 45a09a9899..51ed2cd948 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -16,11 +16,17 @@ package org.thingsboard.server.common.transport.util; import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.springframework.util.Base64Utils; import org.thingsboard.server.common.msg.EncryptionUtil; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; /** * @author Valerii Sosliuk @@ -35,4 +41,15 @@ public class SslUtil { throws CertificateEncodingException { return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded())); } + + public static String parseCommonName(X509Certificate certificate) { + X500Name x500name; + try { + x500name = new JcaX509CertificateHolder(certificate).getSubject(); + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } + RDN cn = x500name.getRDNs(BCStyle.CN)[0]; + return IETFUtils.valueToString(cn.getFirst().getValue()); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java index 10363ac5d7..b6758af59c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java @@ -53,4 +53,12 @@ public interface DeviceCredentialsDao extends Dao { */ DeviceCredentials findByCredentialsId(TenantId tenantId, String credentialsId); + /** + * Find device credentials by device name. + * + * @param deviceName the device name + * @return the device credentials object + */ + DeviceCredentials findByTenantIdAndDeviceName(TenantId tenantId, String deviceName); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index c4cbabfb3d..c21031fe65 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -16,15 +16,12 @@ package org.thingsboard.server.dao.device; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.util.SecurityUtil; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; @@ -86,6 +83,13 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService, ExportableEntityDa DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey); DeviceProfile findByName(TenantId tenantId, String profileName); + + DeviceProfile findByCertificateHash(String certificateHash); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 9dad6a22d9..41a51176f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -53,6 +53,7 @@ import java.util.Map; import java.util.Optional; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateString; @Service("DeviceProfileDaoService") @Slf4j @@ -61,6 +62,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService deviceProfileDao.findByCertificateHash(certificateHash), true); + } + @Override public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String name) { log.trace("Executing findOrCreateDefaultDeviceProfile"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java index 0dd35a39b8..7642b45d5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.sql.device; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity; import java.util.UUID; @@ -28,4 +30,7 @@ public interface DeviceCredentialsRepository extends JpaRepository Date: Fri, 13 Jan 2023 17:27:08 +0200 Subject: [PATCH 008/116] Fix sql formatting and incorrect proto-message updating --- common/cluster-api/src/main/proto/queue.proto | 50 ++++++------ .../mqtt/MqttSslHandlerProvider.java | 15 ++-- .../transport/mqtt/MqttTransportHandler.java | 6 +- .../sql/device/JpaDeviceCredentialsDao.java | 2 +- .../main/resources/sql/schema-entities.sql | 76 +++++-------------- 5 files changed, 52 insertions(+), 97 deletions(-) diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 405b96726a..ace1f3cf6d 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -903,36 +903,36 @@ message VersionControlResponseMsg { message TransportApiRequestMsg { ValidateDeviceTokenRequestMsg validateTokenRequestMsg = 1; ValidateDeviceX509CertRequestMsg validateX509CertRequestMsg = 2; - ValidateDeviceProfileX509CertRequestMsg validateProfileX509CertRequestMsg = 3; - GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 4; - GetEntityProfileRequestMsg entityProfileRequestMsg = 5; - LwM2MRequestMsg lwM2MRequestMsg = 6; - ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 7; - ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 8; - ValidateDeviceLwM2MCredentialsRequestMsg validateDeviceLwM2MCredentialsRequestMsg = 9; - GetResourceRequestMsg resourceRequestMsg = 10; - GetOtaPackageRequestMsg otaPackageRequestMsg = 11; - GetSnmpDevicesRequestMsg snmpDevicesRequestMsg = 12; - GetDeviceRequestMsg deviceRequestMsg = 13; - GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 14; - GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 15; - UpdateOrCreateDeviceX509CertRequestMsg updateOrCreateDeviceCertRequestMsg = 16; + GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3; + GetEntityProfileRequestMsg entityProfileRequestMsg = 4; + LwM2MRequestMsg lwM2MRequestMsg = 5; + ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; + ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7; + ValidateDeviceLwM2MCredentialsRequestMsg validateDeviceLwM2MCredentialsRequestMsg = 8; + GetResourceRequestMsg resourceRequestMsg = 9; + GetOtaPackageRequestMsg otaPackageRequestMsg = 10; + GetSnmpDevicesRequestMsg snmpDevicesRequestMsg = 11; + GetDeviceRequestMsg deviceRequestMsg = 12; + GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; + GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14; + UpdateOrCreateDeviceX509CertRequestMsg updateOrCreateDeviceCertRequestMsg = 15; + ValidateDeviceProfileX509CertRequestMsg validateProfileX509CertRequestMsg = 16; } /* Response from ThingsBoard Core Service to Transport Service */ message TransportApiResponseMsg { ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1; - ValidateDeviceProfileCredentialsResponseMsg validateDeviceProfileResponseMsg = 2; - GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 3; - GetEntityProfileResponseMsg entityProfileResponseMsg = 4; - ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 5; - GetSnmpDevicesResponseMsg snmpDevicesResponseMsg = 6; - LwM2MResponseMsg lwM2MResponseMsg = 7; - GetResourceResponseMsg resourceResponseMsg = 8; - GetOtaPackageResponseMsg otaPackageResponseMsg = 9; - GetDeviceResponseMsg deviceResponseMsg = 10; - GetDeviceCredentialsResponseMsg deviceCredentialsResponseMsg = 11; - repeated GetQueueRoutingInfoResponseMsg getQueueRoutingInfoResponseMsgs = 12; + GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; + GetEntityProfileResponseMsg entityProfileResponseMsg = 3; + ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 4; + GetSnmpDevicesResponseMsg snmpDevicesResponseMsg = 5; + LwM2MResponseMsg lwM2MResponseMsg = 6; + GetResourceResponseMsg resourceResponseMsg = 7; + GetOtaPackageResponseMsg otaPackageResponseMsg = 8; + GetDeviceResponseMsg deviceResponseMsg = 9; + GetDeviceCredentialsResponseMsg deviceCredentialsResponseMsg = 10; + repeated GetQueueRoutingInfoResponseMsg getQueueRoutingInfoResponseMsgs = 11; + ValidateDeviceProfileCredentialsResponseMsg validateDeviceProfileResponseMsg = 12; } /* Messages that are handled by ThingsBoard Core Service */ diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index ed2326946f..237ff86375 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -145,13 +145,8 @@ public class MqttSslHandlerProvider { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { String deviceCN = SslUtil.parseCommonName(chain[0]); - String deviceCert; - String deviceCredentialsValue = SslUtil.getCertificateString(chain[0]); - try { - deviceCert = EncryptionUtil.getSha3Hash(SslUtil.getCertificateString(chain[0])); - } catch (CertificateEncodingException e) { - throw new RuntimeException(e); - } + String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]); + String clientDeviceCertHash = EncryptionUtil.getSha3Hash(clientDeviceCertValue); String credentialsBody = null; for (X509Certificate cert : chain) { try { @@ -175,7 +170,8 @@ public class MqttSslHandlerProvider { if (msg.isDeviceProfileFound()) { transportService.process(DeviceTransportType.MQTT, TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg.newBuilder() - .setHash(deviceCert) + .setHash(clientDeviceCertHash) + .setValue(clientDeviceCertValue) .setCommonName(deviceCN) .setDeviceProfileIdMSB(msg.getDeviceProfileId().getId().getMostSignificantBits()) .setDeviceProfileIdLSB(msg.getDeviceProfileId().getId().getLeastSignificantBits()) @@ -183,7 +179,6 @@ public class MqttSslHandlerProvider { new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { - System.out.println("msg.getCredentials() = " + msg.getCredentials()); credentialsBodyHolder[0] = msg.getCredentials(); latch.countDown(); } @@ -216,7 +211,7 @@ public class MqttSslHandlerProvider { } }); latch.await(10, TimeUnit.SECONDS); - if (deviceCredentialsValue.equals(credentialsBodyHolder[0])) { + if (clientDeviceCertValue.equals(credentialsBodyHolder[0])) { credentialsBody = credentialsBodyHolder[0]; break; } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 5044d5a35e..803a8ff3ca 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -849,7 +849,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate[] chain, MqttConnectMessage connectMessage) { try { String deviceCN = SslUtil.parseCommonName(chain[0]); - String deviceCertHash = EncryptionUtil.getSha3Hash(SslUtil.getCertificateString(chain[0])); + String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]); + String clientDeviceCertHash = EncryptionUtil.getSha3Hash(clientDeviceCertValue); for (X509Certificate cert : chain) { try { String strCert = SslUtil.getCertificateString(cert); @@ -872,7 +873,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (msg.isDeviceProfileFound()) { transportService.process(DeviceTransportType.MQTT, TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg.newBuilder() - .setHash(deviceCertHash) + .setHash(clientDeviceCertHash) + .setValue(clientDeviceCertValue) .setCommonName(deviceCN) .setDeviceProfileIdMSB(msg.getDeviceProfileId().getId().getMostSignificantBits()) .setDeviceProfileIdLSB(msg.getDeviceProfileId().getId().getLeastSignificantBits()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java index a56a13c98a..01a746c9b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java @@ -69,6 +69,6 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao Date: Thu, 19 Jan 2023 15:18:15 +0200 Subject: [PATCH 009/116] Add device profile upgrade --- .../src/main/data/upgrade/3.4.3/schema_update.sql | 12 ++++++++++++ dao/src/main/resources/sql/schema-entities.sql | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/application/src/main/data/upgrade/3.4.3/schema_update.sql b/application/src/main/data/upgrade/3.4.3/schema_update.sql index e6fd32bc2d..87e9e98aef 100644 --- a/application/src/main/data/upgrade/3.4.3/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.3/schema_update.sql @@ -24,3 +24,15 @@ CREATE TABLE IF NOT EXISTS alarm_comment ( CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE ) PARTITION BY RANGE (created_time); CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id); + + + +-- DEVICE PROFILE CERTIFICATE START + +ALTER TABLE device_profile + ADD COLUMN IF NOT EXISTS certificate_hash varchar, + ADD COLUMN IF NOT EXISTS certificate_value varchar, + ADD COLUMN IF NOT EXISTS certificate_regex_pattern varchar(255), + DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key, + ADD CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash); +-- DEVICE PROFILE CERTIFICATE END diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 5d2921370a..6a2996bff7 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -292,7 +292,7 @@ CREATE TABLE IF NOT EXISTS device_profile ( default_dashboard_id uuid, default_queue_name varchar(255), provision_device_key varchar, - certificate_value varchar; + certificate_value varchar, certificate_hash varchar, certificate_regex_pattern varchar(255), default_edge_rule_chain_id uuid, From 18c1440a488dcc6594d2a5dbb5457ec0e37e32de Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 19 Jan 2023 15:21:48 +0200 Subject: [PATCH 010/116] Add validation on device profile create or update --- .../server/common/data/DeviceProfile.java | 2 +- .../mqtt/MqttSslHandlerProvider.java | 4 +- .../transport/mqtt/MqttTransportHandler.java | 111 +++++------------- .../dao/device/DeviceProfileServiceImpl.java | 24 +++- .../validator/DeviceProfileDataValidator.java | 54 +++++++++ 5 files changed, 107 insertions(+), 88 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index d0efc8cb5b..54989e1aab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -70,7 +70,7 @@ public class DeviceProfile extends SearchTextBased implements H private String certificateValue; @ApiModelProperty(position = 17, value = "CA certificate hash") private String certificateHash; - @ApiModelProperty(position = 18, value = "Regex for fetch deviceName from CN") + @ApiModelProperty(position = 18, value = "Regex to fetch deviceName from CN") private String certificateRegexPattern; @ApiModelProperty(position = 7, value = "Reference to the rule chain. " + diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 237ff86375..2b4a24e116 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -124,7 +124,7 @@ public class MqttSslHandlerProvider { static class ThingsboardMqttX509TrustManager implements X509TrustManager { private final X509TrustManager trustManager; - private TransportService transportService; + private final TransportService transportService; ThingsboardMqttX509TrustManager(X509TrustManager trustManager, TransportService transportService) { this.trustManager = trustManager; @@ -220,7 +220,7 @@ public class MqttSslHandlerProvider { } } if (credentialsBody == null) { - throw new CertificateException("Invalid Device Certificate"); + throw new CertificateException("Invalid Certificate's chain"); } } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 803a8ff3ca..9159759c28 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -62,7 +62,6 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; -import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.common.transport.util.SslUtil; @@ -79,7 +78,7 @@ import org.thingsboard.server.transport.mqtt.util.ReturnCodeResolver; import javax.net.ssl.SSLPeerUnverifiedException; import java.io.IOException; import java.net.InetSocketAddress; -import java.security.cert.CertificateEncodingException; +import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; @@ -89,7 +88,6 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -808,9 +806,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement deviceSessionCtx.setProvisionOnly(true); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SUCCESS, msg)); } else { - X509Certificate[] chain; - if (sslHandler != null && (chain = getX509Certificate()) != null) { - processX509CertConnect(ctx, chain, msg); + X509Certificate cert; + if (sslHandler != null && (cert = getX509Certificate()) != null) { + processX509CertConnect(ctx, cert, msg); } else { processAuthTokenConnect(ctx, msg); } @@ -846,84 +844,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement }); } - private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate[] chain, MqttConnectMessage connectMessage) { + private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert, MqttConnectMessage connectMessage) { try { - String deviceCN = SslUtil.parseCommonName(chain[0]); - String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]); - String clientDeviceCertHash = EncryptionUtil.getSha3Hash(clientDeviceCertValue); - for (X509Certificate cert : chain) { - try { - String strCert = SslUtil.getCertificateString(cert); - String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - final ValidateDeviceCredentialsResponse[] validateDeviceCredentialsResponses = new ValidateDeviceCredentialsResponse[1]; - CountDownLatch latch = new CountDownLatch(1); - transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback<>() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - if (!StringUtils.isEmpty(msg.getCredentials())) { - validateDeviceCredentialsResponses[0] = msg; - latch.countDown(); - } else { - transportService.process(DeviceTransportType.MQTT, - TransportProtos.ValidateDeviceProfileX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback<>() { - @Override - public void onSuccess(ValidateDeviceProfileCredentialsResponse msg) { - if (msg.isDeviceProfileFound()) { - transportService.process(DeviceTransportType.MQTT, - TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg.newBuilder() - .setHash(clientDeviceCertHash) - .setValue(clientDeviceCertValue) - .setCommonName(deviceCN) - .setDeviceProfileIdMSB(msg.getDeviceProfileId().getId().getMostSignificantBits()) - .setDeviceProfileIdLSB(msg.getDeviceProfileId().getId().getLeastSignificantBits()) - .build(), - new TransportServiceCallback<>() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - if (!StringUtils.isEmpty(msg.getCredentials())) { - validateDeviceCredentialsResponses[0] = msg; - latch.countDown(); - } - } - - @Override - public void onError(Throwable e) { - log.error(e.getMessage(), e); - latch.countDown(); - } - } - ); - } else { - latch.countDown(); - } - } - - @Override - public void onError(Throwable e) { - log.error(e.getMessage(), e); - latch.countDown(); - } - }); - } - } - - @Override - public void onError(Throwable e) { - log.error(e.getMessage(), e); - latch.countDown(); - } - }); - latch.await(10, TimeUnit.SECONDS); - if (validateDeviceCredentialsResponses[0] != null && validateDeviceCredentialsResponses[0].hasDeviceInfo()) { - onValidateDeviceResponse(validateDeviceCredentialsResponses[0], ctx, connectMessage); - break; - } - } catch (InterruptedException | CertificateEncodingException e) { - log.error(e.getMessage(), e); - } + if (!context.isSkipValidityCheckForClientCert()) { + cert.checkValidity(); } + String strCert = SslUtil.getCertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + onValidateDeviceResponse(msg, ctx, connectMessage); + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + } + }); } catch (Exception e) { context.onAuthFailure(address); ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.NOT_AUTHORIZED_5, connectMessage)); @@ -932,13 +871,17 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private X509Certificate[] getX509Certificate() { + private X509Certificate getX509Certificate() { try { - return (X509Certificate[]) sslHandler.engine().getSession().getPeerCertificates(); + Certificate[] certChains = sslHandler.engine().getSession().getPeerCertificates(); + if (certChains.length > 1) { + return (X509Certificate) certChains[0]; + } } catch (SSLPeerUnverifiedException e) { log.warn(e.getMessage()); return null; } + return null; } private MqttConnAckMessage createMqttConnAckMsg(ReturnCode returnCode, MqttConnectMessage msg) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 41a51176f4..8cd08739b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; @@ -51,6 +52,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -62,7 +65,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService rootCa = new HashSet<>(); + try { + String filename = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar); + FileInputStream is = new FileInputStream(filename); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + String password = "changeit"; + keystore.load(is, password.toCharArray()); + + PKIXParameters params = new PKIXParameters(keystore); + for (TrustAnchor ta : params.getTrustAnchors()) { + X509Certificate cert = ta.getTrustedCert(); + rootCa.add(EncryptionUtil.getSha3Hash(getCertificateString(cert))); + } + } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | + InvalidAlgorithmParameterException | IOException ignored) { + } + return rootCa.contains(deviceProfileHash); + } + + private String getCertificateString(Certificate cert) throws CertificateEncodingException { + return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded())); + } } From b4a9ba26e7b77f09bd367a54f34bdaef2bcc7676 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 20 Jan 2023 19:00:19 +0200 Subject: [PATCH 011/116] Add new field: update database and upgrade script --- application/src/main/data/upgrade/3.4.3/schema_update.sql | 2 ++ dao/src/main/resources/sql/schema-entities.sql | 1 + 2 files changed, 3 insertions(+) diff --git a/application/src/main/data/upgrade/3.4.3/schema_update.sql b/application/src/main/data/upgrade/3.4.3/schema_update.sql index 87e9e98aef..78203951ec 100644 --- a/application/src/main/data/upgrade/3.4.3/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.3/schema_update.sql @@ -33,6 +33,8 @@ ALTER TABLE device_profile ADD COLUMN IF NOT EXISTS certificate_hash varchar, ADD COLUMN IF NOT EXISTS certificate_value varchar, ADD COLUMN IF NOT EXISTS certificate_regex_pattern varchar(255), + ADD COLUMN IF NOT EXISTS allow_create_device_by_x509 boolean, DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key, ADD CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash); + -- DEVICE PROFILE CERTIFICATE END diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 6a2996bff7..eed6f237b6 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -297,6 +297,7 @@ CREATE TABLE IF NOT EXISTS device_profile ( certificate_regex_pattern varchar(255), default_edge_rule_chain_id uuid, external_id uuid, + allow_create_device_by_x509 boolean, CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash), CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), From 7957da4a9e2ab7922074848018e8dcde8e2bab23 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 20 Jan 2023 19:02:14 +0200 Subject: [PATCH 012/116] Updated: send full chain to kafka with single message and do whole logic on core --- .../transport/DefaultTransportApiService.java | 148 +++++++++++++----- common/cluster-api/src/main/proto/queue.proto | 22 +-- .../dao/device/DeviceCredentialsService.java | 2 - .../server/common/data/DeviceProfile.java | 13 +- .../data/DeviceProfileProvisionType.java | 3 +- ...509CertificateProvisionConfiguration.java} | 18 ++- .../DeviceProfileProvisionConfiguration.java | 3 +- .../server/common/msg/EncryptionUtil.java | 5 + .../mqtt/MqttSslHandlerProvider.java | 104 ++++-------- .../transport/mqtt/MqttTransportHandler.java | 13 +- .../common/transport/TransportService.java | 11 +- .../service/DefaultTransportService.java | 30 +--- .../server/common/transport/util/SslUtil.java | 12 ++ .../dao/device/DeviceCredentialsDao.java | 8 - .../device/DeviceCredentialsServiceImpl.java | 7 - .../server/dao/model/ModelConstants.java | 1 + .../dao/model/sql/DeviceProfileEntity.java | 5 + .../validator/DeviceProfileDataValidator.java | 20 +-- .../device/DeviceCredentialsRepository.java | 5 - .../sql/device/JpaDeviceCredentialsDao.java | 5 - 20 files changed, 211 insertions(+), 224 deletions(-) rename common/{transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java => data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java} (58%) diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 9bc61d6493..0bf0ff8035 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -25,6 +25,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.ota.OtaPackageDataCache; @@ -33,6 +34,7 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; @@ -65,6 +67,7 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProvisionService; @@ -90,11 +93,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetSnmpDevicesRespon import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceProfileCredentialsResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceProfileX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -105,6 +105,13 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.resource.TbResourceService; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -166,13 +173,9 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); - } else if (transportApiRequestMsg.hasValidateProfileX509CertRequestMsg()) { - ValidateDeviceProfileX509CertRequestMsg msg = transportApiRequestMsg.getValidateProfileX509CertRequestMsg(); - result = validateDeviceProfileCertificate(msg.getHash()); - } else if (transportApiRequestMsg.hasUpdateOrCreateDeviceCertRequestMsg()) { - UpdateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getUpdateOrCreateDeviceCertRequestMsg(); - DeviceProfile deviceProfile = deviceProfileCache.find(new DeviceProfileId(new UUID(msg.getDeviceProfileIdMSB(), msg.getDeviceProfileIdLSB()))); - result = updateOrCreateDeviceCredentials(msg.getHash(), msg.getValue(), msg.getCommonName(), deviceProfile, DeviceCredentialsType.X509_CERTIFICATE); + } else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) { + TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg(); + result = validateOrCreateDeviceX509Certificate(msg.getCertificate(), DeviceCredentialsType.X509_CERTIFICATE); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { @@ -240,27 +243,43 @@ public class DefaultTransportApiService implements TransportApiService { } } - private ListenableFuture validateDeviceProfileCertificate(String credentialsId) { - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(credentialsId); - if (deviceProfile != null) { - return getDeviceProfileInfo(deviceProfile); - } - return getEmptyTransportApiResponseFuture(); - } + private ListenableFuture validateOrCreateDeviceX509Certificate(String certChain, DeviceCredentialsType credentialsType) { + try { + List chain = getX509CertificateChainFromString(certChain); + String updateDeviceCertificateValue = SslUtil.getCertificateString(chain.get(0)); + String updateDeviceCertificateHash = EncryptionUtil.getSha3Hash(updateDeviceCertificateValue); + + for (X509Certificate cert: chain) { + String certificateValue = SslUtil.getCertificateString(cert); + String certificateHash = EncryptionUtil.getSha3Hash(certificateValue); + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash); + if (credentials != null && credentials.getCredentialsType() == credentialsType) { + return getDeviceInfo(credentials); + } + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash); + if (deviceProfile != null) { + String deviceCN = extractDeviceNameFromCNByRegEx(SslUtil.parseCommonName(chain.get(0)), deviceProfile.getCertificateRegexPattern()); + String deviceName = extractDeviceNameFromCNByRegEx(deviceCN, deviceProfile.getCertificateRegexPattern()); + Device device = deviceService.findDeviceByTenantIdAndName(deviceProfile.getTenantId(), deviceName); + if (device != null) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == credentialsType) { + deviceCredentials = updateDeviceCredentials(device.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + } else if (deviceCredentials == null) { + deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + } + return getDeviceInfo(deviceCredentials); + } else if (deviceProfile.getProvisionType() == DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE && deviceProfile.isAllowCreateNewDevicesByX509Strategy()) { + Device savedDevice = createDevice(deviceProfile.getTenantId(), deviceProfile.getId(), deviceName, deviceProfile.getName()); + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); + deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + return getDeviceInfo(deviceCredentials); + } - private ListenableFuture updateOrCreateDeviceCredentials(String credentialsId, - String credentialsValue, - String deviceCN, - DeviceProfile deviceProfile, - DeviceCredentialsType credentialsType) { - String deviceName = extractRegex(deviceCN, deviceProfile.getCertificateRegexPattern()); - // find deviceCredentials by deviceName (device exists) - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByTenantIdAndDeviceName(deviceProfile.getTenantId(), deviceName); - if (deviceCredentials != null && deviceCredentials.getCredentialsType() == credentialsType) { - deviceCredentials.setCredentialsId(credentialsId); - deviceCredentials.setCredentialsValue(credentialsValue); - deviceCredentialsService.updateDeviceCredentials(deviceProfile.getTenantId(), deviceCredentials); - return getDeviceInfo(deviceCredentials); + } + } + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); } return getEmptyTransportApiResponseFuture(); } @@ -546,18 +565,6 @@ public class DefaultTransportApiService implements TransportApiService { }, MoreExecutors.directExecutor()); } - private ListenableFuture getDeviceProfileInfo(DeviceProfile deviceProfile) { - ValidateDeviceProfileCredentialsResponseMsg.Builder builder = ValidateDeviceProfileCredentialsResponseMsg.newBuilder() - .setDeviceProfileIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) - .setDeviceProfileIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) - .setIsDeviceProfileFound(true); - - return Futures.immediateFuture( - TransportApiResponseMsg.newBuilder() - .setValidateDeviceProfileResponseMsg(builder.build()) - .build()); - } - private DeviceInfoProto getDeviceInfoProto(Device device) throws JsonProcessingException { DeviceInfoProto.Builder builder = DeviceInfoProto.newBuilder() .setTenantIdMSB(device.getTenantId().getId().getMostSignificantBits()) @@ -716,7 +723,7 @@ public class DefaultTransportApiService implements TransportApiService { return l != null ? l : 0; } - private String extractRegex(String commonName, String regex) { + private String extractDeviceNameFromCNByRegEx(String commonName, String regex) { Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(commonName); if (matcher.find()) { @@ -724,4 +731,59 @@ public class DefaultTransportApiService implements TransportApiService { } return commonName; } + + private List getX509CertificateChainFromString(String certificateChain) { + List chain = new ArrayList<>(); + String[] test = Arrays.stream(certificateChain.split("-----BEGIN CERTIFICATE-----")).filter(e -> e.trim().length() > 0).map(EncryptionUtil::certTrimNewLines).toArray(String[]::new); + Arrays.stream(test).forEach(s -> { + try { + chain.add(readCertFile(s)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return chain; + } + + private X509Certificate readCertFile(String fileContent) throws Exception { + X509Certificate certificate = null; + if (fileContent != null && !fileContent.trim().isEmpty()) { + fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replaceAll("\\s", ""); + byte[] decoded = Base64.decodeBase64(fileContent); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + try (InputStream inStream = new ByteArrayInputStream(decoded)) { + certificate = (X509Certificate) certFactory.generateCertificate(inStream); + } + } + return certificate; + } + + private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue, + String certificateHash, DeviceCredentialsType credentialsType) { + deviceCredentials.setCredentialsId(certificateHash); + deviceCredentials.setCredentialsValue(certificateValue); + deviceCredentials.setCredentialsType(credentialsType); + return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + } + + private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, String certificateValue, + String certificateHash, DeviceCredentialsType credentialsType) { + DeviceCredentials createDevCredentials = new DeviceCredentials(); + createDevCredentials.setDeviceId(deviceId); + createDevCredentials.setCredentialsType(credentialsType); + createDevCredentials.setCredentialsId(certificateHash); + createDevCredentials.setCredentialsValue(certificateValue); + return deviceCredentialsService.createDeviceCredentials(tenantId, createDevCredentials); + } + + private Device createDevice(TenantId tenantId, DeviceProfileId deviceProfileId, String deviceName, String type) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setDeviceProfileId(deviceProfileId); + device.setName(deviceName); + device.setType(type); + return deviceService.saveDevice(device); + } } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index ace1f3cf6d..b9741429f7 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -169,16 +169,8 @@ message ValidateDeviceX509CertRequestMsg { string hash = 1; } -message ValidateDeviceProfileX509CertRequestMsg { - string hash = 1; -} - -message UpdateOrCreateDeviceX509CertRequestMsg { - string hash = 1; - string value = 2; - string commonName = 3; - int64 deviceProfileIdMSB = 4; - int64 deviceProfileIdLSB = 5; +message ValidateOrCreateDeviceX509CertRequestMsg { + string certificate = 1; } message ValidateBasicMqttCredRequestMsg { @@ -193,12 +185,6 @@ message ValidateDeviceCredentialsResponseMsg { bytes profileBody = 3; } -message ValidateDeviceProfileCredentialsResponseMsg { - int64 deviceProfileIdMSB = 1; - int64 deviceProfileIdLSB = 2; - bool isDeviceProfileFound = 3; -} - message GetOrCreateDeviceFromGatewayRequestMsg { int64 gatewayIdMSB = 1; int64 gatewayIdLSB = 2; @@ -915,8 +901,7 @@ message TransportApiRequestMsg { GetDeviceRequestMsg deviceRequestMsg = 12; GetDeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 13; GetAllQueueRoutingInfoRequestMsg getAllQueueRoutingInfoRequestMsg = 14; - UpdateOrCreateDeviceX509CertRequestMsg updateOrCreateDeviceCertRequestMsg = 15; - ValidateDeviceProfileX509CertRequestMsg validateProfileX509CertRequestMsg = 16; + ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateX509CertRequestMsg = 15; } /* Response from ThingsBoard Core Service to Transport Service */ @@ -932,7 +917,6 @@ message TransportApiResponseMsg { GetDeviceResponseMsg deviceResponseMsg = 9; GetDeviceCredentialsResponseMsg deviceCredentialsResponseMsg = 10; repeated GetQueueRoutingInfoResponseMsg getQueueRoutingInfoResponseMsgs = 11; - ValidateDeviceProfileCredentialsResponseMsg validateDeviceProfileResponseMsg = 12; } /* Messages that are handled by ThingsBoard Core Service */ diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java index 150106e3cb..1c0f2578ed 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java @@ -26,8 +26,6 @@ public interface DeviceCredentialsService { DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId); - DeviceCredentials findDeviceCredentialsByTenantIdAndDeviceName(TenantId tenantId, String deviceName); - DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 54989e1aab..b9b1053d99 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -66,12 +66,15 @@ public class DeviceProfile extends SearchTextBased implements H private DeviceTransportType transportType; @ApiModelProperty(position = 15, value = "Provisioning strategy.") private DeviceProfileProvisionType provisionType; - @ApiModelProperty(position = 16, value = "CA certificate value") + @ApiModelProperty(position = 16, value = "CA certificate value. ") private String certificateValue; - @ApiModelProperty(position = 17, value = "CA certificate hash") + @ApiModelProperty(position = 17, value = "CA certificate hash. ") private String certificateHash; - @ApiModelProperty(position = 18, value = "Regex to fetch deviceName from CN") + @ApiModelProperty(position = 18, value = "Regex to fetch deviceName from CN. ") private String certificateRegexPattern; + @ApiModelProperty(position = 19, value = "Allow to create new devices by x509 provision strategy. ") + private boolean allowCreateNewDevicesByX509Strategy; + @ApiModelProperty(position = 7, value = "Reference to the rule chain. " + "If present, the specified rule chain will be used to process all messages related to device, including telemetry, attribute updates, etc. " + @@ -128,6 +131,10 @@ public class DeviceProfile extends SearchTextBased implements H this.firmwareId = deviceProfile.getFirmwareId(); this.softwareId = deviceProfile.getSoftwareId(); this.defaultEdgeRuleChainId = deviceProfile.getDefaultEdgeRuleChainId(); + this.allowCreateNewDevicesByX509Strategy = deviceProfile.isAllowCreateNewDevicesByX509Strategy(); + this.certificateRegexPattern = deviceProfile.getCertificateRegexPattern(); + this.certificateValue = deviceProfile.getCertificateValue(); + this.certificateHash = deviceProfile.getCertificateHash(); this.externalId = deviceProfile.getExternalId(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java index 319ebadcbc..7ee03d7227 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java @@ -18,5 +18,6 @@ package org.thingsboard.server.common.data; public enum DeviceProfileProvisionType { DISABLED, ALLOW_CREATE_NEW_DEVICES, - CHECK_PRE_PROVISIONED_DEVICES + CHECK_PRE_PROVISIONED_DEVICES, + ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java similarity index 58% rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java rename to common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java index cfbe79bf96..e84d09829f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/ValidateDeviceProfileCredentialsResponse.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java @@ -13,16 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.transport.auth; -import lombok.Builder; +package org.thingsboard.server.common.data.device.profile; + import lombok.Data; -import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data -@Builder -public class ValidateDeviceProfileCredentialsResponse { +public class AllowCreatingNewDevicesByX509CertificateProvisionConfiguration implements DeviceProfileProvisionConfiguration { + + private final String provisionDeviceSecret; + + @Override + public DeviceProfileProvisionType getType() { + return DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE; + } - private final DeviceProfileId deviceProfileId; - private final boolean isDeviceProfileFound; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java index b29641fe6f..7a70c00b98 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -31,7 +31,8 @@ import java.io.Serializable; @JsonSubTypes({ @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), - @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")}) + @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES"), + @JsonSubTypes.Type(value = AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.class, name = "ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE")}) public interface DeviceProfileProvisionConfiguration extends Serializable { String getProvisionDeviceSecret(); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index c57c3a885b..6bccac1591 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -35,6 +35,11 @@ public class EncryptionUtil { .replaceAll("-----END CERTIFICATE-----", ""); } + public static String certTrimNewLinesWithoutBeginEnd(String input) { + return input.replaceAll("\n", "") + .replaceAll("\r", ""); + } + public static String pubkTrimNewLines(String input) { return input.replaceAll("-----BEGIN PUBLIC KEY-----", "") .replaceAll("\n", "") diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 2b4a24e116..67b48464c8 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.mqtt; import io.netty.handler.ssl.SslHandler; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.units.qual.C; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -26,11 +27,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; -import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.common.transport.util.SslUtil; @@ -144,84 +143,37 @@ public class MqttSslHandlerProvider { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - String deviceCN = SslUtil.parseCommonName(chain[0]); String clientDeviceCertValue = SslUtil.getCertificateString(chain[0]); - String clientDeviceCertHash = EncryptionUtil.getSha3Hash(clientDeviceCertValue); - String credentialsBody = null; - for (X509Certificate cert : chain) { - try { - String strCert = SslUtil.getCertificateString(cert); - String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - final String[] credentialsBodyHolder = new String[1]; - CountDownLatch latch = new CountDownLatch(1); - transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback<>() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - if (!StringUtils.isEmpty(msg.getCredentials())) { - credentialsBodyHolder[0] = msg.getCredentials(); - latch.countDown(); - } else { - transportService.process(DeviceTransportType.MQTT, - TransportProtos.ValidateDeviceProfileX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback<>() { - @Override - public void onSuccess(ValidateDeviceProfileCredentialsResponse msg) { - if (msg.isDeviceProfileFound()) { - transportService.process(DeviceTransportType.MQTT, - TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg.newBuilder() - .setHash(clientDeviceCertHash) - .setValue(clientDeviceCertValue) - .setCommonName(deviceCN) - .setDeviceProfileIdMSB(msg.getDeviceProfileId().getId().getMostSignificantBits()) - .setDeviceProfileIdLSB(msg.getDeviceProfileId().getId().getLeastSignificantBits()) - .build(), - new TransportServiceCallback<>() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - credentialsBodyHolder[0] = msg.getCredentials(); - latch.countDown(); - } - - @Override - public void onError(Throwable e) { - log.error(e.getMessage(), e); - latch.countDown(); - } - } - ); - } else { - latch.countDown(); - } - } - - @Override - public void onError(Throwable e) { - log.error(e.getMessage(), e); - latch.countDown(); - } - }); - } + final String[] credentialsBodyHolder = new String[1]; + CountDownLatch latch = new CountDownLatch(1); + try { + String certificateChain = SslUtil.getCertificateChainString(chain); + transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg + .newBuilder().setCertificate(certificateChain).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + credentialsBodyHolder[0] = msg.getCredentials(); } - - @Override - public void onError(Throwable e) { - log.error(e.getMessage(), e); - latch.countDown(); - } - }); - latch.await(10, TimeUnit.SECONDS); - if (clientDeviceCertValue.equals(credentialsBodyHolder[0])) { - credentialsBody = credentialsBodyHolder[0]; - break; - } - } catch (InterruptedException | CertificateEncodingException e) { - log.error(e.getMessage(), e); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + // to fix this error, cuz no one can understand this ... + log.error(e.getMessage(), e); + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) { + throw new CertificateException("Invalid Certificate's chain"); } - } - if (credentialsBody == null) { - throw new CertificateException("Invalid Certificate's chain"); + } catch (CertificateEncodingException | InterruptedException e) { + log.error(e.getMessage(), e); } } + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 9159759c28..813a6f1ed2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -67,6 +67,7 @@ import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; @@ -851,7 +852,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } String strCert = SslUtil.getCertificateString(cert); String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), + transportService.process(DeviceTransportType.MQTT, ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { @@ -860,7 +861,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { - log.error(e.getMessage(), e); + log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e); + ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); + ctx.close(); } }); } catch (Exception e) { @@ -873,9 +876,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private X509Certificate getX509Certificate() { try { - Certificate[] certChains = sslHandler.engine().getSession().getPeerCertificates(); - if (certChains.length > 1) { - return (X509Certificate) certChains[0]; + Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificates(); + if (certChain.length > 1) { + return (X509Certificate) certChain[0]; } } catch (SSLPeerUnverifiedException e) { log.warn(e.getMessage()); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 21e668cf37..3e2e23e43a 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -20,7 +20,6 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; -import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; @@ -53,12 +52,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMs import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; -import org.thingsboard.server.gen.transport.TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceProfileX509CertRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg; import java.util.List; import java.util.concurrent.ExecutorService; @@ -90,10 +88,7 @@ public interface TransportService { void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback); - void process(DeviceTransportType transportType, ValidateDeviceProfileX509CertRequestMsg msg, - TransportServiceCallback callback); - - void process(DeviceTransportType transportType, UpdateOrCreateDeviceX509CertRequestMsg msg, + void process(DeviceTransportType transportType, ValidateOrCreateDeviceX509CertRequestMsg msg, TransportServiceCallback callback); void process(ValidateDeviceLwM2MCredentialsRequestMsg msg, diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index a41c1d6055..4639ce0430 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -71,7 +71,6 @@ import org.thingsboard.server.common.transport.TransportTenantProfileCache; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; -import org.thingsboard.server.common.transport.auth.ValidateDeviceProfileCredentialsResponse; import org.thingsboard.server.common.transport.limits.TransportRateLimitService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; @@ -429,35 +428,16 @@ public class DefaultTransportService implements TransportService { } @Override - public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceProfileX509CertRequestMsg requestMsg, - TransportServiceCallback callback) { - log.trace("Processing msg: {}", requestMsg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), - TransportApiRequestMsg.newBuilder().setValidateProfileX509CertRequestMsg(requestMsg).build()); - ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { - TransportProtos.ValidateDeviceProfileCredentialsResponseMsg msg = tmp.getValue().getValidateDeviceProfileResponseMsg(); - ValidateDeviceProfileCredentialsResponse.ValidateDeviceProfileCredentialsResponseBuilder result = ValidateDeviceProfileCredentialsResponse.builder(); - DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(msg.getDeviceProfileIdMSB(), msg.getDeviceProfileIdLSB())); - result.deviceProfileId(deviceProfileId); - result.isDeviceProfileFound(msg.getIsDeviceProfileFound()); - return result.build(); - }, MoreExecutors.directExecutor()); - AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); - } - - @Override - public void process(DeviceTransportType transportType, TransportProtos.UpdateOrCreateDeviceX509CertRequestMsg requestMsg, - TransportServiceCallback callback) { - log.trace("Processing msg: {}", requestMsg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder() - .setUpdateOrCreateDeviceCertRequestMsg(requestMsg).build()); + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { + log.trace("Processing msg: {}", msg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateX509CertRequestMsg(msg).build()); doProcess(transportType, protoMsg, callback); } @Override - public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateX509CertRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateOrCreateX509CertRequestMsg(msg).build()); doProcess(transportType, protoMsg, callback); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index 51ed2cd948..bc6f38f42c 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; +import java.util.Arrays; /** * @author Valerii Sosliuk @@ -42,6 +43,17 @@ public class SslUtil { return EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded())); } + public static String getCertificateChainString(Certificate[] chain) + throws CertificateEncodingException { + String begin = "-----BEGIN CERTIFICATE-----"; + String end = "-----END CERTIFICATE-----"; + StringBuilder stringBuilder = new StringBuilder(); + for (Certificate cert: chain) { + stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLinesWithoutBeginEnd(Base64Utils.encodeToString(cert.getEncoded()))).append(end).append("\n"); + } + return stringBuilder.toString(); + } + public static String parseCommonName(X509Certificate certificate) { X500Name x500name; try { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java index b6758af59c..10363ac5d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java @@ -53,12 +53,4 @@ public interface DeviceCredentialsDao extends Dao { */ DeviceCredentials findByCredentialsId(TenantId tenantId, String credentialsId); - /** - * Find device credentials by device name. - * - * @param deviceName the device name - * @return the device credentials object - */ - DeviceCredentials findByTenantIdAndDeviceName(TenantId tenantId, String deviceName); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index c21031fe65..138a8aaa9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -83,13 +83,6 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService impl @Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_REGEX_PATTERN_PROPERTY) private String certificateRegexPattern; + @Column(name = ModelConstants.ALLOW_CREATE_NEW_DEVICES_BY_X509_PROVISION) + private boolean allowCreateDevice; + public DeviceProfileEntity() { super(); } @@ -138,6 +141,7 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl this.certificateHash = deviceProfile.getCertificateHash(); this.certificateValue = deviceProfile.getCertificateValue(); this.certificateRegexPattern = deviceProfile.getCertificateRegexPattern(); + this.allowCreateDevice = deviceProfile.isAllowCreateNewDevicesByX509Strategy(); this.description = deviceProfile.getDescription(); this.isDefault = deviceProfile.isDefault(); this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class); @@ -203,6 +207,7 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl deviceProfile.setCertificateHash(certificateHash); deviceProfile.setCertificateValue(certificateValue); deviceProfile.setCertificateRegexPattern(certificateRegexPattern); + deviceProfile.setAllowCreateNewDevicesByX509Strategy(allowCreateDevice); if (firmwareId != null) { deviceProfile.setFirmwareId(new OtaPackageId(firmwareId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 492ecf939a..0bf618e367 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -133,14 +133,14 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator Date: Tue, 24 Jan 2023 16:37:05 +0200 Subject: [PATCH 013/116] Fix CacheKey toString and validation logs --- .../transport/DefaultTransportApiService.java | 4 +-- common/cluster-api/src/main/proto/queue.proto | 1 + .../server/common/data/DeviceProfile.java | 8 ++--- .../mqtt/MqttSslHandlerProvider.java | 6 ++-- .../transport/mqtt/MqttTransportHandler.java | 2 +- .../dao/device/DeviceProfileCacheKey.java | 8 +++-- .../dao/device/DeviceProfileEvictEvent.java | 1 + .../dao/device/DeviceProfileServiceImpl.java | 31 ++++++++++++++----- .../validator/DeviceProfileDataValidator.java | 13 ++++---- 9 files changed, 46 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 0bf0ff8035..24f6de76d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -258,8 +258,7 @@ public class DefaultTransportApiService implements TransportApiService { } DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash); if (deviceProfile != null) { - String deviceCN = extractDeviceNameFromCNByRegEx(SslUtil.parseCommonName(chain.get(0)), deviceProfile.getCertificateRegexPattern()); - String deviceName = extractDeviceNameFromCNByRegEx(deviceCN, deviceProfile.getCertificateRegexPattern()); + String deviceName = extractDeviceNameFromCNByRegEx(SslUtil.parseCommonName(chain.get(0)), deviceProfile.getCertificateRegexPattern()); Device device = deviceService.findDeviceByTenantIdAndName(deviceProfile.getTenantId(), deviceName); if (device != null) { DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); @@ -275,7 +274,6 @@ public class DefaultTransportApiService implements TransportApiService { deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); return getDeviceInfo(deviceCredentials); } - } } } catch (CertificateEncodingException e) { diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index b9741429f7..9f62eca36b 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -422,6 +422,7 @@ message CredentialsDataProto { ValidateDeviceTokenRequestMsg validateDeviceTokenRequestMsg = 1; ValidateDeviceX509CertRequestMsg validateDeviceX509CertRequestMsg = 2; ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 3; + ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateDeviceX509CertRequestMsg = 4; } message ProvisionDeviceRequestMsg { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index b9b1053d99..e226261227 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -66,13 +66,13 @@ public class DeviceProfile extends SearchTextBased implements H private DeviceTransportType transportType; @ApiModelProperty(position = 15, value = "Provisioning strategy.") private DeviceProfileProvisionType provisionType; - @ApiModelProperty(position = 16, value = "CA certificate value. ") + @ApiModelProperty(position = 18, value = "CA certificate value. ") private String certificateValue; - @ApiModelProperty(position = 17, value = "CA certificate hash. ") + @ApiModelProperty(position = 19, value = "CA certificate hash. ") private String certificateHash; - @ApiModelProperty(position = 18, value = "Regex to fetch deviceName from CN. ") + @ApiModelProperty(position = 20, value = "Regex to fetch deviceName from CN. ") private String certificateRegexPattern; - @ApiModelProperty(position = 19, value = "Allow to create new devices by x509 provision strategy. ") + @ApiModelProperty(position = 21, value = "Allow to create new devices by x509 provision strategy. ") private boolean allowCreateNewDevicesByX509Strategy; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 67b48464c8..3224556e09 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.mqtt; import io.netty.handler.ssl.SslHandler; import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.units.qual.C; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -161,14 +160,13 @@ public class MqttSslHandlerProvider { @Override public void onError(Throwable e) { - // to fix this error, cuz no one can understand this ... - log.error(e.getMessage(), e); + log.trace("Failed to process certificate chain: {}", certificateChain, e); latch.countDown(); } }); latch.await(10, TimeUnit.SECONDS); if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) { - throw new CertificateException("Invalid Certificate's chain"); + throw new CertificateException("Invalid Certificate's chain. Cannot find such device credentials."); } } catch (CertificateEncodingException | InterruptedException e) { log.error(e.getMessage(), e); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 813a6f1ed2..00d318544f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -877,7 +877,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private X509Certificate getX509Certificate() { try { Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificates(); - if (certChain.length > 1) { + if (certChain.length > 0) { return (X509Certificate) certChain[0]; } } catch (SSLPeerUnverifiedException e) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java index 228720f0e7..fb44092bd4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java @@ -56,14 +56,18 @@ public class DeviceProfileCacheKey implements Serializable { return new DeviceProfileCacheKey(tenantId, null, null, null, true); } + /** + * IMPORTANT: Method toString() has to return unique value, if you add additional field to this class, please also refactor toString(). + */ @Override public String toString() { if (deviceProfileId != null) { return deviceProfileId.toString(); } else if (defaultProfile) { return tenantId.toString(); - } else { - return tenantId + "_" + name; + } else if (certificateHash != null) { + return certificateHash; } + return tenantId + "_" + name; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java index b5b4b27a6b..acd34ff8e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java @@ -27,5 +27,6 @@ public class DeviceProfileEvictEvent { private final String oldName; private final DeviceProfileId deviceProfileId; private final boolean defaultProfile; + private final String certificateHash; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 8cd08739b3..ffa25d16d6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -98,6 +98,9 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService 1; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 0bf618e367..d737a48a9b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -138,7 +138,7 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator rootCa = new HashSet<>(); + boolean getRootCAFromJavaCacerts(String deviceProfileHash) { try { String filename = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar); FileInputStream is = new FileInputStream(filename); @@ -408,12 +407,14 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator Date: Tue, 31 Jan 2023 18:04:45 +0200 Subject: [PATCH 014/116] Add unit tests and update regex pattern to convert chain to list --- .../transport/DefaultTransportApiService.java | 85 +++++---- .../BaseDeviceProfileControllerTest.java | 19 ++ .../DefaultTransportApiServiceTest.java | 174 ++++++++++++++++++ .../mqtt/AbstractMqttIntegrationTest.java | 7 + .../mqtt/MqttTestConfigProperties.java | 5 + common/cluster-api/src/main/proto/queue.proto | 1 - .../server/common/msg/EncryptionUtil.java | 6 +- .../mqtt/MqttSslHandlerProvider.java | 3 +- .../server/common/transport/util/SslUtil.java | 3 +- .../dao/device/DeviceProfileServiceImpl.java | 27 ++- 10 files changed, 272 insertions(+), 58 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 24f6de76d7..f434df2faa 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -107,11 +107,9 @@ import org.thingsboard.server.service.resource.TbResourceService; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -243,41 +241,44 @@ public class DefaultTransportApiService implements TransportApiService { } } - private ListenableFuture validateOrCreateDeviceX509Certificate(String certChain, DeviceCredentialsType credentialsType) { + protected ListenableFuture validateOrCreateDeviceX509Certificate(String certChain, DeviceCredentialsType credentialsType) { + List chain = convertX509CertificateChainToList(certChain); + String updateDeviceCertificateValue = chain.get(0); + String updateDeviceCertificateHash = EncryptionUtil.getSha3Hash(updateDeviceCertificateValue); + String deviceCommonName = ""; try { - List chain = getX509CertificateChainFromString(certChain); - String updateDeviceCertificateValue = SslUtil.getCertificateString(chain.get(0)); - String updateDeviceCertificateHash = EncryptionUtil.getSha3Hash(updateDeviceCertificateValue); - - for (X509Certificate cert: chain) { - String certificateValue = SslUtil.getCertificateString(cert); - String certificateHash = EncryptionUtil.getSha3Hash(certificateValue); - DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash); - if (credentials != null && credentials.getCredentialsType() == credentialsType) { - return getDeviceInfo(credentials); + deviceCommonName = SslUtil.parseCommonName(readCertFile(chain.get(0))); + } catch (Exception ignored) { + } + for (String certificateValue : chain) { + String certificateHash = EncryptionUtil.getSha3Hash(certificateValue); + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash); + if (credentials != null && credentials.getCredentialsType() == credentialsType) { + return getDeviceInfo(credentials); + } + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash); + if (deviceProfile != null) { + String deviceName = extractDeviceNameFromCNByRegEx(deviceCommonName, deviceProfile.getCertificateRegexPattern()); + if (deviceName == null) { + log.error("Device name cannot be unmatched from CN!"); + return getEmptyTransportApiResponseFuture(); } - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash); - if (deviceProfile != null) { - String deviceName = extractDeviceNameFromCNByRegEx(SslUtil.parseCommonName(chain.get(0)), deviceProfile.getCertificateRegexPattern()); - Device device = deviceService.findDeviceByTenantIdAndName(deviceProfile.getTenantId(), deviceName); - if (device != null) { - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); - if (deviceCredentials != null && deviceCredentials.getCredentialsType() == credentialsType) { - deviceCredentials = updateDeviceCredentials(device.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); - } else if (deviceCredentials == null) { - deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); - } - return getDeviceInfo(deviceCredentials); - } else if (deviceProfile.getProvisionType() == DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE && deviceProfile.isAllowCreateNewDevicesByX509Strategy()) { - Device savedDevice = createDevice(deviceProfile.getTenantId(), deviceProfile.getId(), deviceName, deviceProfile.getName()); - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); - deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); - return getDeviceInfo(deviceCredentials); + Device device = deviceService.findDeviceByTenantIdAndName(deviceProfile.getTenantId(), deviceName); + if (device != null) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == credentialsType) { + deviceCredentials = updateDeviceCredentials(device.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + } else if (deviceCredentials == null) { + deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); } + return getDeviceInfo(deviceCredentials); + } else if (deviceProfile.getProvisionType() == DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE && deviceProfile.isAllowCreateNewDevicesByX509Strategy()) { + Device savedDevice = createDevice(deviceProfile.getTenantId(), deviceProfile.getId(), deviceName, deviceProfile.getName()); + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); + deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + return getDeviceInfo(deviceCredentials); } } - } catch (CertificateEncodingException e) { - throw new RuntimeException(e); } return getEmptyTransportApiResponseFuture(); } @@ -727,19 +728,17 @@ public class DefaultTransportApiService implements TransportApiService { if (matcher.find()) { return matcher.group(0); } - return commonName; + return null; } - private List getX509CertificateChainFromString(String certificateChain) { - List chain = new ArrayList<>(); - String[] test = Arrays.stream(certificateChain.split("-----BEGIN CERTIFICATE-----")).filter(e -> e.trim().length() > 0).map(EncryptionUtil::certTrimNewLines).toArray(String[]::new); - Arrays.stream(test).forEach(s -> { - try { - chain.add(readCertFile(s)); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + private List convertX509CertificateChainToList(String certificateChain) { + List chain = new ArrayList<>(); + String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(certificateChain); + while (matcher.find()) { + chain.add(EncryptionUtil.certTrimNewLines(matcher.group())); + } return chain; } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java index 3fe80b1112..43b8e51025 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -298,6 +298,25 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); } + @Test + public void testSaveDeviceProfileWithSameCertificateHash() throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); + deviceProfile.setCertificateHash("Certificate Hash"); + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk()); + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2"); + deviceProfile2.setCertificateHash("Certificate Hash"); + + Mockito.reset(tbClusterService, auditLogService); + + String msgError = "Device profile with such certificate hash already exists"; + doPost("/api/deviceProfile", deviceProfile2) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(msgError))); + + testNotifyEntityEqualsOneTimeServiceNeverError(deviceProfile, savedTenant.getId(), + tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + } + @Test public void testChangeDeviceProfileTypeNull() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); diff --git a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java new file mode 100644 index 0000000000..4a2ff07978 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java @@ -0,0 +1,174 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.transport; + + +import com.google.common.util.concurrent.Futures; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.server.cache.ota.OtaPackageDataCache; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceProvisionService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.resource.TbResourceService; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = DefaultTransportApiService.class) +public class DefaultTransportApiServiceTest { + + @MockBean + protected TbDeviceProfileCache deviceProfileCache; + @MockBean + protected TbTenantProfileCache tenantProfileCache; + @MockBean + protected TbApiUsageStateService apiUsageStateService; + @MockBean + protected DeviceService deviceService; + @MockBean + protected DeviceProfileService deviceProfileService; + @MockBean + protected RelationService relationService; + @MockBean + protected DeviceCredentialsService deviceCredentialsService; + @MockBean + protected DbCallbackExecutorService dbCallbackExecutorService; + @MockBean + protected TbClusterService tbClusterService; + @MockBean + protected DataDecodingEncodingService dataDecodingEncodingService; + @MockBean + protected DeviceProvisionService deviceProvisionService; + @MockBean + protected TbResourceService resourceService; + @MockBean + protected OtaPackageService otaPackageService; + @MockBean + protected OtaPackageDataCache otaPackageDataCache; + @MockBean + protected QueueService queueService; + @SpyBean + DefaultTransportApiService service; + + private final String deviceCertificate = "-----BEGIN CERTIFICATE-----Device certificate value-----END CERTIFICATE-----"; + private final String deviceProfileCertificate = "-----BEGIN CERTIFICATE-----Device profile certificate value-----END CERTIFICATE-----"; + + @Test + public void validateExistingDeviceX509Certificate() { + var device = createDevice(); + when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); + + var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId()); + when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials); + + service.validateOrCreateDeviceX509Certificate(deviceCertificate, DeviceCredentialsType.X509_CERTIFICATE); + verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any()); + } + + @Test + public void updateExistingDeviceX509Certificate() { + var deviceProfile = createDeviceProfile(deviceProfileCertificate); + when(deviceProfileService.findDeviceProfileByCertificateHash(any())).thenReturn(deviceProfile); + + var device = createDevice(); + when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device); + when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); + + var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId()); + when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); + when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); + + service.validateOrCreateDeviceX509Certificate(deviceProfileCertificate, DeviceCredentialsType.X509_CERTIFICATE); + verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any()); + verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); + verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); + verify(deviceCredentialsService, times(1)).updateDeviceCredentials(any(), any()); + } + + @Test + public void createDeviceByX509Provision() { + var deviceProfile = createDeviceProfile(deviceProfileCertificate); + when(deviceProfileService.findDeviceProfileByCertificateHash(any())).thenReturn(deviceProfile); + + var device = createDevice(); + when(deviceService.saveDevice(any())).thenReturn(device); + when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); + + var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId()); + when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); + when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); + + service.validateOrCreateDeviceX509Certificate(deviceProfileCertificate, DeviceCredentialsType.X509_CERTIFICATE); + verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any()); + verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); + verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); + verify(deviceCredentialsService, times(1)).updateDeviceCredentials(any(), any()); + } + + private DeviceCredentials createDeviceCredentials(String certificateValue, DeviceId deviceId) { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(deviceId); + deviceCredentials.setCredentialsValue(certificateValue); + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(certificateValue)); + deviceCredentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + return deviceCredentials; + } + + private DeviceProfile createDeviceProfile(String certificateValue) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setCertificateValue(certificateValue); + deviceProfile.setCertificateHash(EncryptionUtil.getSha3Hash(certificateValue)); + deviceProfile.setCertificateRegexPattern("^$"); + deviceProfile.setAllowCreateNewDevicesByX509Strategy(true); + deviceProfile.setProvisionType(DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE); + return deviceProfile; + } + + private Device createDevice() { + Device device = new Device(); + device.setId(new DeviceId(UUID.randomUUID())); + return device; + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 917e41d1b4..69b1f87502 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.AllowCreatingNewDevicesByX509CertificateProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; @@ -94,6 +95,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg deviceProfile.setProvisionType(provisionType); deviceProfile.setProvisionDeviceKey(config.getProvisionKey()); deviceProfile.setDescription(transportPayloadType.name() + " Test"); + deviceProfile.setAllowCreateNewDevicesByX509Strategy(config.allowCreatingNewDeviceByX509Strategy); + deviceProfile.setCertificateValue("Device Profile certificate value"); + deviceProfile.setCertificateRegexPattern(config.getRegEx()); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration(); @@ -144,6 +148,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg case CHECK_PRE_PROVISIONED_DEVICES: provisionConfiguration = new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(config.getProvisionSecret()); break; + case ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE: + provisionConfiguration = new AllowCreatingNewDevicesByX509CertificateProvisionConfiguration(config.getProvisionSecret()); + break; case DISABLED: default: provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(config.getProvisionSecret()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java index bc535b424b..4c57f413c9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java @@ -40,9 +40,14 @@ public class MqttTestConfigProperties { boolean enableCompatibilityWithJsonPayloadFormat; boolean useJsonPayloadFormatForDefaultDownlinkTopics; boolean sendAckOnValidationException; + boolean allowCreatingNewDeviceByX509Strategy; DeviceProfileProvisionType provisionType; String provisionKey; String provisionSecret; + String x509DeviceCertificate; + String x509DeviceProfileCertificate; + String commonName; + String regEx; } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 9f62eca36b..b9741429f7 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -422,7 +422,6 @@ message CredentialsDataProto { ValidateDeviceTokenRequestMsg validateDeviceTokenRequestMsg = 1; ValidateDeviceX509CertRequestMsg validateDeviceX509CertRequestMsg = 2; ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 3; - ValidateOrCreateDeviceX509CertRequestMsg validateOrCreateDeviceX509CertRequestMsg = 4; } message ProvisionDeviceRequestMsg { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index 6bccac1591..4cdd61eeec 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -35,9 +35,11 @@ public class EncryptionUtil { .replaceAll("-----END CERTIFICATE-----", ""); } - public static String certTrimNewLinesWithoutBeginEnd(String input) { + public static String certTrimNewLinesForChainInDeviceProfile(String input) { return input.replaceAll("\n", "") - .replaceAll("\r", ""); + .replaceAll("\r", "") + .replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n") + .replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n"); } public static String pubkTrimNewLines(String input) { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 3224556e09..d73743e6b5 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -41,7 +41,6 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.concurrent.CountDownLatch; @@ -168,7 +167,7 @@ public class MqttSslHandlerProvider { if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) { throw new CertificateException("Invalid Certificate's chain. Cannot find such device credentials."); } - } catch (CertificateEncodingException | InterruptedException e) { + } catch (Exception e) { log.error(e.getMessage(), e); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index bc6f38f42c..4245cae349 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; -import java.util.Arrays; /** * @author Valerii Sosliuk @@ -49,7 +48,7 @@ public class SslUtil { String end = "-----END CERTIFICATE-----"; StringBuilder stringBuilder = new StringBuilder(); for (Certificate cert: chain) { - stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLinesWithoutBeginEnd(Base64Utils.encodeToString(cert.getEncoded()))).append(end).append("\n"); + stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLines(Base64Utils.encodeToString(cert.getEncoded()))).append(end).append("\n"); } return stringBuilder.toString(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index ffa25d16d6..48e6a5b6b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -48,6 +48,10 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import java.io.ByteArrayInputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -342,17 +346,15 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService 1; + private String formatCertificateValue(String certificateValue) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream inputStream = new ByteArrayInputStream(certificateValue.getBytes()); + Certificate[] certificates = cf.generateCertificates(inputStream).toArray(new Certificate[0]); + if (certificates.length > 1) { + return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue); + } + return EncryptionUtil.certTrimNewLines(certificateValue); + } catch (CertificateException e) { + throw new RuntimeException(e); + } } } From 0943b80d8e4f074950648b3e0047462e037699f8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 1 Feb 2023 16:49:00 +0200 Subject: [PATCH 015/116] Fix schema and micro refactoring --- .../main/data/upgrade/3.4.3/schema_update.sql | 3 --- .../transport/DefaultTransportApiService.java | 11 ++++++--- .../DefaultTransportApiServiceTest.java | 15 ++++++++---- .../mqtt/AbstractMqttIntegrationTest.java | 7 ------ .../mqtt/MqttTestConfigProperties.java | 5 ---- common/cluster-api/src/main/proto/queue.proto | 2 +- .../server/common/data/DeviceProfile.java | 11 +-------- .../data/DeviceProfileProvisionType.java | 2 +- .../DeviceProfileProvisionConfiguration.java | 2 +- ...rtificateChainProvisionConfiguration.java} | 11 ++++++--- .../mqtt/MqttSslHandlerProvider.java | 2 +- .../dao/device/DeviceProfileServiceImpl.java | 23 ++++++++++++------- .../dao/model/sql/DeviceProfileEntity.java | 15 ------------ .../main/resources/sql/schema-entities.sql | 3 --- 14 files changed, 47 insertions(+), 65 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/device/profile/{AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java => X509CertificateChainProvisionConfiguration.java} (67%) diff --git a/application/src/main/data/upgrade/3.4.3/schema_update.sql b/application/src/main/data/upgrade/3.4.3/schema_update.sql index 78203951ec..2aaa574bb4 100644 --- a/application/src/main/data/upgrade/3.4.3/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.3/schema_update.sql @@ -31,9 +31,6 @@ CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id) ALTER TABLE device_profile ADD COLUMN IF NOT EXISTS certificate_hash varchar, - ADD COLUMN IF NOT EXISTS certificate_value varchar, - ADD COLUMN IF NOT EXISTS certificate_regex_pattern varchar(255), - ADD COLUMN IF NOT EXISTS allow_create_device_by_x509 boolean, DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key, ADD CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash); diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index f434df2faa..6a3780d529 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfig import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -173,7 +174,7 @@ public class DefaultTransportApiService implements TransportApiService { result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); } else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) { TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg(); - result = validateOrCreateDeviceX509Certificate(msg.getCertificate(), DeviceCredentialsType.X509_CERTIFICATE); + result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain(), DeviceCredentialsType.X509_CERTIFICATE); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { @@ -258,7 +259,11 @@ public class DefaultTransportApiService implements TransportApiService { } DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash); if (deviceProfile != null) { - String deviceName = extractDeviceNameFromCNByRegEx(deviceCommonName, deviceProfile.getCertificateRegexPattern()); + X509CertificateChainProvisionConfiguration x509Configuration = new X509CertificateChainProvisionConfiguration(); + if (deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) { + x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration(); + } + String deviceName = extractDeviceNameFromCNByRegEx(deviceCommonName, x509Configuration.getCertificateRegExPattern()); if (deviceName == null) { log.error("Device name cannot be unmatched from CN!"); return getEmptyTransportApiResponseFuture(); @@ -272,7 +277,7 @@ public class DefaultTransportApiService implements TransportApiService { deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); } return getDeviceInfo(deviceCredentials); - } else if (deviceProfile.getProvisionType() == DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE && deviceProfile.isAllowCreateNewDevicesByX509Strategy()) { + } else if (deviceProfile.getProvisionType() == DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN && x509Configuration.isAllowCreateNewDevicesByX509Certificate()) { Device savedDevice = createDevice(deviceProfile.getTenantId(), deviceProfile.getId(), deviceName, deviceProfile.getName()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); diff --git a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java index 4a2ff07978..71a2f8b70c 100644 --- a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java @@ -29,6 +29,9 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -158,11 +161,15 @@ public class DefaultTransportApiServiceTest { private DeviceProfile createDeviceProfile(String certificateValue) { DeviceProfile deviceProfile = new DeviceProfile(); - deviceProfile.setCertificateValue(certificateValue); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration(); + provision.setCertificateValue(certificateValue); + provision.setCertificateRegExPattern("^$"); + provision.setAllowCreateNewDevicesByX509Certificate(true); + deviceProfileData.setProvisionConfiguration(provision); + deviceProfile.setProfileData(deviceProfileData); deviceProfile.setCertificateHash(EncryptionUtil.getSha3Hash(certificateValue)); - deviceProfile.setCertificateRegexPattern("^$"); - deviceProfile.setAllowCreateNewDevicesByX509Strategy(true); - deviceProfile.setProvisionType(DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE); + deviceProfile.setProvisionType(DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN); return deviceProfile; } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 69b1f87502..917e41d1b4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.AllowCreatingNewDevicesByX509CertificateProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; @@ -95,9 +94,6 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg deviceProfile.setProvisionType(provisionType); deviceProfile.setProvisionDeviceKey(config.getProvisionKey()); deviceProfile.setDescription(transportPayloadType.name() + " Test"); - deviceProfile.setAllowCreateNewDevicesByX509Strategy(config.allowCreatingNewDeviceByX509Strategy); - deviceProfile.setCertificateValue("Device Profile certificate value"); - deviceProfile.setCertificateRegexPattern(config.getRegEx()); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration(); @@ -148,9 +144,6 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg case CHECK_PRE_PROVISIONED_DEVICES: provisionConfiguration = new CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration(config.getProvisionSecret()); break; - case ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE: - provisionConfiguration = new AllowCreatingNewDevicesByX509CertificateProvisionConfiguration(config.getProvisionSecret()); - break; case DISABLED: default: provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(config.getProvisionSecret()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java index 4c57f413c9..bc535b424b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java @@ -40,14 +40,9 @@ public class MqttTestConfigProperties { boolean enableCompatibilityWithJsonPayloadFormat; boolean useJsonPayloadFormatForDefaultDownlinkTopics; boolean sendAckOnValidationException; - boolean allowCreatingNewDeviceByX509Strategy; DeviceProfileProvisionType provisionType; String provisionKey; String provisionSecret; - String x509DeviceCertificate; - String x509DeviceProfileCertificate; - String commonName; - String regEx; } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index b9741429f7..6e9ec38be8 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -170,7 +170,7 @@ message ValidateDeviceX509CertRequestMsg { } message ValidateOrCreateDeviceX509CertRequestMsg { - string certificate = 1; + string certificateChain = 1; } message ValidateBasicMqttCredRequestMsg { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index e226261227..05f8ae19ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -66,14 +66,8 @@ public class DeviceProfile extends SearchTextBased implements H private DeviceTransportType transportType; @ApiModelProperty(position = 15, value = "Provisioning strategy.") private DeviceProfileProvisionType provisionType; - @ApiModelProperty(position = 18, value = "CA certificate value. ") - private String certificateValue; - @ApiModelProperty(position = 19, value = "CA certificate hash. ") + @ApiModelProperty(position = 18, value = "CA certificate hash. ") private String certificateHash; - @ApiModelProperty(position = 20, value = "Regex to fetch deviceName from CN. ") - private String certificateRegexPattern; - @ApiModelProperty(position = 21, value = "Allow to create new devices by x509 provision strategy. ") - private boolean allowCreateNewDevicesByX509Strategy; @ApiModelProperty(position = 7, value = "Reference to the rule chain. " + @@ -131,9 +125,6 @@ public class DeviceProfile extends SearchTextBased implements H this.firmwareId = deviceProfile.getFirmwareId(); this.softwareId = deviceProfile.getSoftwareId(); this.defaultEdgeRuleChainId = deviceProfile.getDefaultEdgeRuleChainId(); - this.allowCreateNewDevicesByX509Strategy = deviceProfile.isAllowCreateNewDevicesByX509Strategy(); - this.certificateRegexPattern = deviceProfile.getCertificateRegexPattern(); - this.certificateValue = deviceProfile.getCertificateValue(); this.certificateHash = deviceProfile.getCertificateHash(); this.externalId = deviceProfile.getExternalId(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java index 7ee03d7227..f2daf71cca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileProvisionType.java @@ -19,5 +19,5 @@ public enum DeviceProfileProvisionType { DISABLED, ALLOW_CREATE_NEW_DEVICES, CHECK_PRE_PROVISIONED_DEVICES, - ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE + X509_CERTIFICATE_CHAIN } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java index 7a70c00b98..048e10739f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -32,7 +32,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES"), - @JsonSubTypes.Type(value = AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.class, name = "ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE")}) + @JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")}) public interface DeviceProfileProvisionConfiguration extends Serializable { String getProvisionDeviceSecret(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java similarity index 67% rename from common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java index e84d09829f..0b500f3e52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreatingNewDevicesByX509CertificateProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java @@ -17,16 +17,21 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data -public class AllowCreatingNewDevicesByX509CertificateProvisionConfiguration implements DeviceProfileProvisionConfiguration { +@NoArgsConstructor +public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration { - private final String provisionDeviceSecret; + private String provisionDeviceSecret; + private String certificateValue; + private String certificateRegExPattern; + private boolean allowCreateNewDevicesByX509Certificate; @Override public DeviceProfileProvisionType getType() { - return DeviceProfileProvisionType.ALLOW_CREATING_NEW_DEVICES_BY_X509_CERTIFICATE; + return DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN; } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index d73743e6b5..d5ff77837b 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -147,7 +147,7 @@ public class MqttSslHandlerProvider { try { String certificateChain = SslUtil.getCertificateChainString(chain); transportService.process(DeviceTransportType.MQTT, TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg - .newBuilder().setCertificate(certificateChain).build(), + .newBuilder().setCertificateChain(certificateChain).build(), new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 48e6a5b6b6..f558eb2c7e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -134,8 +135,12 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService impl @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) private UUID externalId; - @Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_VALUE_PROPERTY) - private String certificateValue; - @Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_HASH_PROPERTY) private String certificateHash; - @Column(name = ModelConstants.DEVICE_PROFILE_CERTIFICATE_REGEX_PATTERN_PROPERTY) - private String certificateRegexPattern; - - @Column(name = ModelConstants.ALLOW_CREATE_NEW_DEVICES_BY_X509_PROVISION) - private boolean allowCreateDevice; - public DeviceProfileEntity() { super(); } @@ -139,9 +130,6 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl this.transportType = deviceProfile.getTransportType(); this.provisionType = deviceProfile.getProvisionType(); this.certificateHash = deviceProfile.getCertificateHash(); - this.certificateValue = deviceProfile.getCertificateValue(); - this.certificateRegexPattern = deviceProfile.getCertificateRegexPattern(); - this.allowCreateDevice = deviceProfile.isAllowCreateNewDevicesByX509Strategy(); this.description = deviceProfile.getDescription(); this.isDefault = deviceProfile.isDefault(); this.profileData = JacksonUtil.convertValue(deviceProfile.getProfileData(), ObjectNode.class); @@ -205,9 +193,6 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl } deviceProfile.setProvisionDeviceKey(provisionDeviceKey); deviceProfile.setCertificateHash(certificateHash); - deviceProfile.setCertificateValue(certificateValue); - deviceProfile.setCertificateRegexPattern(certificateRegexPattern); - deviceProfile.setAllowCreateNewDevicesByX509Strategy(allowCreateDevice); if (firmwareId != null) { deviceProfile.setFirmwareId(new OtaPackageId(firmwareId)); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index eed6f237b6..bd61d9a7c3 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -292,12 +292,9 @@ CREATE TABLE IF NOT EXISTS device_profile ( default_dashboard_id uuid, default_queue_name varchar(255), provision_device_key varchar, - certificate_value varchar, certificate_hash varchar, - certificate_regex_pattern varchar(255), default_edge_rule_chain_id uuid, external_id uuid, - allow_create_device_by_x509 boolean, CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash), CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name), CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key), From 058c97e4de6e6af470a4e5d7dae661253f6bb4e7 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 2 Feb 2023 10:20:24 +0200 Subject: [PATCH 016/116] Fix headers and remove extra constants from Model Constants --- .../service/transport/DefaultTransportApiServiceTest.java | 5 ++--- .../profile/X509CertificateChainProvisionConfiguration.java | 2 +- .../server/dao/device/DeviceProfileServiceImpl.java | 3 +-- .../org/thingsboard/server/dao/model/ModelConstants.java | 3 --- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java index 71a2f8b70c..0309bec131 100644 --- a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,8 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; -import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; -import org.thingsboard.server.common.data.device.profile.DeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.X509CertificateChainProvisionConfiguration; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java index 0b500f3e52..9257053b4f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2022 The Thingsboard Authors + * Copyright © 2016-2023 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 9d5c0608d3..767fd805cf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -379,9 +379,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService 1) { return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue); } - return EncryptionUtil.certTrimNewLines(certificateValue); } catch (CertificateException ignored) {} - return certificateValue; + return EncryptionUtil.certTrimNewLines(certificateValue); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 76a5423359..7b0ca50d0d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -186,10 +186,7 @@ public class ModelConstants { public static final String DEVICE_PROFILE_FIRMWARE_ID_PROPERTY = "firmware_id"; public static final String DEVICE_PROFILE_SOFTWARE_ID_PROPERTY = "software_id"; public static final String DEVICE_PROFILE_CERTIFICATE_HASH_PROPERTY = "certificate_hash"; - public static final String DEVICE_PROFILE_CERTIFICATE_VALUE_PROPERTY = "certificate_value"; - public static final String DEVICE_PROFILE_CERTIFICATE_REGEX_PATTERN_PROPERTY = "certificate_regex_pattern"; public static final String DEVICE_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY = "default_edge_rule_chain_id"; - public static final String ALLOW_CREATE_NEW_DEVICES_BY_X509_PROVISION = "allow_create_device_by_x509"; /** * Asset profile constants. From 5bf7b6162e95ece7e2a7a6607e64c6ffb1381fb6 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 10 Mar 2023 15:48:52 +0200 Subject: [PATCH 017/116] Fix sql schema --- application/src/main/data/upgrade/3.4.4/schema_update.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index 9f8a81daaf..d208585ed7 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -353,7 +353,7 @@ $$; ALTER TABLE device_profile ADD COLUMN IF NOT EXISTS certificate_hash varchar, -DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key, + DROP CONSTRAINT IF EXISTS device_profile_credentials_hash_unq_key, ADD CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash); -- DEVICE PROFILE CERTIFICATE END From 74ce748059383e9a37e449f356036a266ed88e39 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 20 Mar 2023 12:10:08 +0200 Subject: [PATCH 018/116] changed default value for ws message queue limit per session from 0 to 1000 --- application/src/main/data/upgrade/3.4.4/schema_update.sql | 7 +++++++ .../dao/service/validator/TenantProfileDataValidator.java | 7 +++++++ ui-ngx/src/app/shared/models/tenant.model.ts | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index d71fc158ef..9a5ac9b32b 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -347,3 +347,10 @@ END $$; -- ALARM FUNCTIONS END + +-- TENANT PROFILE START + +update tenant_profile SET profile_data = jsonb_set(profile_data, array['configuration','wsMsgQueueLimitPerSession'], to_jsonb(1000)) + WHERE profile_data::json#>>'{configuration,wsMsgQueueLimitPerSession}' = '0'; + +-- TENANT PROFILE END diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java index 0477ccf32f..9a003e235c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -57,6 +58,12 @@ public class TenantProfileDataValidator extends DataValidator { if (tenantProfile.getProfileData().getConfiguration() == null) { throw new DataValidationException("Tenant profile data configuration should be specified!"); } + if (tenantProfile.getProfileData().getConfiguration() instanceof DefaultTenantProfileConfiguration) { + DefaultTenantProfileConfiguration configuration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); + if (configuration.getWsMsgQueueLimitPerSession() == 0) { + throw new DataValidationException("WS message queue limit per session should be greater than 0"); + } + } if (tenantProfile.isDefault()) { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); if (defaultTenantProfile != null && !defaultTenantProfile.getId().equals(tenantProfile.getId())) { diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 990852ed8c..45843a951c 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -110,7 +110,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxWsSessionsPerCustomer: 0, maxWsSessionsPerRegularUser: 0, maxWsSessionsPerPublicUser: 0, - wsMsgQueueLimitPerSession: 0, + wsMsgQueueLimitPerSession: 1000, maxWsSubscriptionsPerTenant: 0, maxWsSubscriptionsPerCustomer: 0, maxWsSubscriptionsPerRegularUser: 0, From b42d663670029753785e32fcb4a0a3f88a6fc469 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 21 Mar 2023 12:25:42 +0200 Subject: [PATCH 019/116] updated UI to display another default value for wsMessageQueuePerSession (1000 instead of 0) --- application/src/main/data/upgrade/3.4.4/schema_update.sql | 6 ------ .../server/controller/plugin/TbWebSocketHandler.java | 4 +++- application/src/main/resources/thingsboard.yml | 1 + .../dao/service/validator/TenantProfileDataValidator.java | 6 ------ .../default-tenant-profile-configuration.component.html | 3 +++ ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index 9a5ac9b32b..88ee90a53f 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -348,9 +348,3 @@ $$; -- ALARM FUNCTIONS END --- TENANT PROFILE START - -update tenant_profile SET profile_data = jsonb_set(profile_data, array['configuration','wsMsgQueueLimitPerSession'], to_jsonb(1000)) - WHERE profile_data::json#>>'{configuration,wsMsgQueueLimitPerSession}' = '0'; - --- TENANT PROFILE END diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index f43607b24c..2a4c150e18 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -81,6 +81,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr private long sendTimeout; @Value("${server.ws.ping_timeout:30000}") private long pingTimeout; + @Value("${server.ws.max_queue_messages_per_session:1000}") + private int wsQueueMessagesPerSession; private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); private final ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); @@ -142,7 +144,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef, tenantProfileConfiguration != null && tenantProfileConfiguration.getWsMsgQueueLimitPerSession() > 0 ? - tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500)); + tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : wsQueueMessagesPerSession)); externalSessionMap.put(externalSessionId, internalSessionId); processInWebSocketService(sessionRef, SessionEvent.onEstablished()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 10c4c9edac..a689483056 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -65,6 +65,7 @@ server: max_per_user: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_MAX_PER_USER:10}" max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}" max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}" + max_queue_messages_per_session: "${TB_SERVER_WS_MAX_QUEUE_MESSAGES_PER_SESSION:1000}" rest: server_side_rpc: # Minimum value of the server side RPC timeout. May override value provided in the REST API call. diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java index 9a003e235c..6f8dfceede 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java @@ -58,12 +58,6 @@ public class TenantProfileDataValidator extends DataValidator { if (tenantProfile.getProfileData().getConfiguration() == null) { throw new DataValidationException("Tenant profile data configuration should be specified!"); } - if (tenantProfile.getProfileData().getConfiguration() instanceof DefaultTenantProfileConfiguration) { - DefaultTenantProfileConfiguration configuration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); - if (configuration.getWsMsgQueueLimitPerSession() == 0) { - throw new DataValidationException("WS message queue limit per session should be greater than 0"); - } - } if (tenantProfile.isDefault()) { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); if (defaultTenantProfile != null && !defaultTenantProfile.getId().equals(tenantProfile.getId())) { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 35ea628784..90f84eab4a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -410,6 +410,9 @@ {{ 'tenant-profile.too-small-value-one' | translate}} + + {{ 'tenant-profile.value-will-be-dropped-to-default' | translate}} +

diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 7f0b1badeb..e1f29fadbf 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3322,6 +3322,7 @@ "incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60", "too-small-value-zero": "The value must be bigger than 0", "too-small-value-one": "The value must be bigger than 1", + "value-will-be-dropped-to-default": "0 value will be dropped to default", "cassandra-tenant-limits-configuration": "Cassandra query for tenant", "ws-limit-max-sessions-per-tenant": "Sessions per tenant maximum number", "ws-limit-max-sessions-per-customer": "Sessions per customer maximum number", From 0e019967e2e45bdc33139e80b1fb720ae53c1e78 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 22 Mar 2023 12:13:59 +0200 Subject: [PATCH 020/116] Feature X509 Device provision added ui form --- ...ile-provision-configuration.component.html | 36 ++++++++++++++++--- ...ofile-provision-configuration.component.ts | 16 ++++++++- ui-ngx/src/app/shared/models/device.models.ts | 6 ++-- .../assets/locale/locale.constant-en_US.json | 13 +++++++ 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index b588a5ec34..ffe4a4732a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -27,18 +27,20 @@ {{ 'device-profile.provision-strategy-required' | translate }} -
+
device-profile.provision-device-key - + --> {{ 'device-profile.provision-device-key-required' | translate }} @@ -46,17 +48,41 @@ device-profile.provision-device-secret - + --> {{ 'device-profile.provision-device-secret-required' | translate }}
+
+
+ + {{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }} + +
+ + device-profile.provision-strategy-x509.certificate-value + + + {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} + + + + device-profile.provision-strategy-x509.cn-regex-variable + + + {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 872571dc20..17ccd40df0 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -86,7 +86,10 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup = this.fb.group({ type: [DeviceProvisionType.DISABLED, Validators.required], provisionDeviceSecret: [{value: null, disabled: true}, Validators.required], - provisionDeviceKey: [{value: null, disabled: true}, Validators.required] + provisionDeviceKey: [{value: null, disabled: true}, Validators.required], + certificateValue: [{value: null, disabled: true}, Validators.required], + certificateRegExPattern: [{value: '[\\w]*', disabled: true}, Validators.required], + allowCreateNewDevicesByX509Certificate: [{value: true, disabled: true}] }); this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => { if (type === DeviceProvisionType.DISABLED) { @@ -94,6 +97,17 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false}); this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null); + this.provisionConfigurationFormGroup.get('certificateValue').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateValue').patchValue(null); + this.provisionConfigurationFormGroup.get('certificateRegExPattern').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue(null); + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(null); + + } else if (type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { + this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false}); } else { const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value; if (!provisionDeviceSecret || !provisionDeviceSecret.length) { diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index ae601b0039..382b32799b 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -62,7 +62,8 @@ export enum CoapTransportDeviceType { export enum DeviceProvisionType { DISABLED = 'DISABLED', ALLOW_CREATE_NEW_DEVICES = 'ALLOW_CREATE_NEW_DEVICES', - CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES' + CHECK_PRE_PROVISIONED_DEVICES = 'CHECK_PRE_PROVISIONED_DEVICES', + X509_CERTIFICATE_CHAIN = 'X509_CERTIFICATE_CHAIN' } export interface DeviceConfigurationFormInfo { @@ -110,7 +111,8 @@ export const deviceProvisionTypeTranslationMap = new MapMore", + "allow-create-new-devices": "Create new devices", + "hint-allow-create-new-devices": "Hint: if selected new devices will be created with correct device credentials from client certificate.", + "certificate-value": "Certificate in PEM format", + "certificate-value-required": "Certificate in PEM format is required", + "cn-regex-variable": "CN Regular Expression variable", + "cn-regex-variable-required": "CN Regular Expression variable is required", + "hint-cn-regex-variable-title": "Examples of RegEx usage:", + "hint-cn-regex-variable-examples": "1. Pattern: .* - matches any character (until line terminators)
CN sample: DeviceName\\nAdditionalInfo
Pattern matches: DeviceName OR DeviceName\\nAdditionalInfo

2. Pattern: ^([^@]+) - matches any string that starts with one or more characters that are not the “@” symbol (@ could be replaced by any other symbol)
CN sample: DeviceName@AdditionalInfo
Pattern matches: DeviceName

3. Pattern: [\\w]*$ (equivalent to [a-zA-Z0-9_]*$) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
CN sample: AdditionalInfo2110#DeviceName_01
Pattern matches: DeviceNane_01", + "hint-cn-regex-variable-note": "Note: Client will get error response in case regex is failed to match." + }, "condition": "Condition", "condition-type": "Condition type", "condition-type-simple": "Simple", From d3803f828dba5b90545f47ecf7280caee71cb2e5 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 22 Mar 2023 13:51:56 +0200 Subject: [PATCH 021/116] Device profile provision strategy -> fix patch values form for X509 Certificate Chain --- ...ile-provision-configuration.component.html | 13 ++++++------- ...ofile-provision-configuration.component.ts | 19 ++++++++++++++++--- .../profile/device-profile.component.ts | 10 ++++++++-- ui-ngx/src/app/shared/models/device.models.ts | 3 +++ .../assets/locale/locale.constant-en_US.json | 5 ++--- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index ffe4a4732a..8b44586f50 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -33,14 +33,14 @@ device-profile.provision-device-key - + {{ 'device-profile.provision-device-key-required' | translate }} @@ -48,14 +48,14 @@ device-profile.provision-device-secret - + {{ 'device-profile.provision-device-secret-required' | translate }} @@ -80,9 +80,8 @@ {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} + device-profile.provision-strategy-x509.hint-cn-regex-variable -
-
-
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 17ccd40df0..bb936080da 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -88,8 +88,8 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu provisionDeviceSecret: [{value: null, disabled: true}, Validators.required], provisionDeviceKey: [{value: null, disabled: true}, Validators.required], certificateValue: [{value: null, disabled: true}, Validators.required], - certificateRegExPattern: [{value: '[\\w]*', disabled: true}, Validators.required], - allowCreateNewDevicesByX509Certificate: [{value: true, disabled: true}] + certificateRegExPattern: [{value: null, disabled: true}, Validators.required], + allowCreateNewDevicesByX509Certificate: [{value: null, disabled: true}] }); this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => { if (type === DeviceProvisionType.DISABLED) { @@ -103,8 +103,19 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue(null); this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').disable({emitEvent: false}); this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(null); - } else if (type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { + const certificateValue: string = this.provisionConfigurationFormGroup.get('certificateValue').value; + if (!certificateValue || !certificateValue.length) { + this.provisionConfigurationFormGroup.get('certificateValue').patchValue(null, {emitEvent: false}); + } + const certificateRegExPattern: string = this.provisionConfigurationFormGroup.get('certificateRegExPattern').value; + if (!certificateRegExPattern || !certificateRegExPattern.length) { + this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue('[\\w]*', {emitEvent: false}); + } + const allowCreateNewDevicesByX509Certificate: boolean | null = this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').value; + if (typeof allowCreateNewDevicesByX509Certificate !== 'boolean') { + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(true, {emitEvent: false}); + } this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false}); @@ -119,6 +130,8 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu } this.provisionConfigurationFormGroup.get('provisionDeviceSecret').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('provisionDeviceKey').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateValue').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateRegExPattern').disable({emitEvent: false}); } }); this.provisionConfigurationFormGroup.valueChanges.subscribe(() => { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts index 9adcf9c168..9a6d150ac1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -104,7 +104,10 @@ export class DeviceProfileComponent extends EntityComponent { const deviceProvisionConfiguration: DeviceProvisionConfiguration = { type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED, provisionDeviceKey: entity?.provisionDeviceKey, - provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret + provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret, + certificateValue: entity?.profileData?.provisionConfiguration?.certificateValue, + certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern, + allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate }; const form = this.fb.group( { @@ -185,7 +188,10 @@ export class DeviceProfileComponent extends EntityComponent { const deviceProvisionConfiguration: DeviceProvisionConfiguration = { type: entity?.provisionType ? entity.provisionType : DeviceProvisionType.DISABLED, provisionDeviceKey: entity?.provisionDeviceKey, - provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret + provisionDeviceSecret: entity?.profileData?.provisionConfiguration?.provisionDeviceSecret, + certificateValue: entity?.profileData?.provisionConfiguration?.certificateValue, + certificateRegExPattern: entity?.profileData?.provisionConfiguration?.certificateRegExPattern, + allowCreateNewDevicesByX509Certificate: entity?.profileData?.provisionConfiguration?.allowCreateNewDevicesByX509Certificate }; this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({type: entity.type}, {emitEvent: false}); diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 382b32799b..de240934c4 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -322,6 +322,9 @@ export interface DeviceProvisionConfiguration { type: DeviceProvisionType; provisionDeviceSecret?: string; provisionDeviceKey?: string; + certificateValue?: string; + certificateRegExPattern?: string; + allowCreateNewDevicesByX509Certificate?: boolean; } export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d6e70ddd51..09ae98d682 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1504,9 +1504,8 @@ "certificate-value-required": "Certificate in PEM format is required", "cn-regex-variable": "CN Regular Expression variable", "cn-regex-variable-required": "CN Regular Expression variable is required", - "hint-cn-regex-variable-title": "Examples of RegEx usage:", - "hint-cn-regex-variable-examples": "1. Pattern: .* - matches any character (until line terminators)
CN sample: DeviceName\\nAdditionalInfo
Pattern matches: DeviceName OR DeviceName\\nAdditionalInfo

2. Pattern: ^([^@]+) - matches any string that starts with one or more characters that are not the “@” symbol (@ could be replaced by any other symbol)
CN sample: DeviceName@AdditionalInfo
Pattern matches: DeviceName

3. Pattern: [\\w]*$ (equivalent to [a-zA-Z0-9_]*$) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
CN sample: AdditionalInfo2110#DeviceName_01
Pattern matches: DeviceNane_01", - "hint-cn-regex-variable-note": "Note: Client will get error response in case regex is failed to match." + "hint-cn-regex-variable": "Required to fetch device name from device's X509 certificate's common name.", + "regex-examples": "Examples of RegEx usage:
  1. Pattern: .* - matches any character (until line terminators)
    CN sample: DeviceName\\nAdditionalInfo
    Pattern matches: DeviceName OR DeviceName\\nAdditionalInfo

  2. Pattern: ^([^@]+) - matches any string that starts with one or more characters that are not the @ symbol (@ could be replaced by any other symbol)
    CN sample: DeviceName@AdditionalInfo
    Pattern matches: DeviceName

  3. Pattern: [\\w]*$ (equivalent to [a-zA-Z0-9_]*$) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
    CN sample: AdditionalInfo2110#DeviceName_01
    Pattern matches: DeviceNane_01
Note: Client will get error response in case regex is failed to match." }, "condition": "Condition", "condition-type": "Condition type", From 86423fdc7b2e2e3755e61efd172b2303de5b0c67 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 22 Mar 2023 17:31:19 +0200 Subject: [PATCH 022/116] Device profile provision X509 Certificate Chain added dynamic readMore for hint; updated text in hint --- ...ice-profile-provision-configuration.component.html | 11 +++++++---- ...evice-profile-provision-configuration.component.ts | 1 + ui-ngx/src/assets/locale/locale.constant-en_US.json | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index 8b44586f50..99cddbd576 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -27,13 +27,13 @@ {{ 'device-profile.provision-strategy-required' | translate }} -
device-profile.provision-device-key - <
-
+ {{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index bb936080da..687bc069b1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -62,6 +62,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu deviceProvisionType = DeviceProvisionType; deviceProvisionTypes = Object.keys(DeviceProvisionType); deviceProvisionTypeTranslateMap = deviceProvisionTypeTranslationMap; + readMore: boolean; private requiredValue: boolean; get required(): boolean { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 09ae98d682..e9e839ce8b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -61,7 +61,9 @@ "done": "Done", "print": "Print", "restore": "Restore", - "confirm": "Confirm" + "confirm": "Confirm", + "more": "More", + "less": "Less" }, "aggregation": { "aggregation": "Aggregation", @@ -1497,7 +1499,7 @@ "provision-secret-copied-message": "Provision secret has been copied to clipboard", "provision-strategy-x509": { "certificate-chain": "X509 Certificates Chain", - "hint-certificate-chain": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication. This strategy can check for pre-provisioned devices, update X.509 device credentials, or create new devices. The user uploads X.509 certificate to the device profile and sets a regular expression to fetch the device name from Common Name (CN)... More", + "hint-certificate-chain": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication. This strategy can check for pre-provisioned devices, update X.509 device credentials, or create new devices. The user uploads X.509 certificate to the device profile and sets a regular expression to fetch the device name from Common Name (CN).

Client certificates must be signed by X.509 certificate, pre-uploaded for this device profile to provision devices by the strategy. The client must establish a TLS connection using the entire chain of certificates (this chain must include device profile X.509 certificate on the last level). If a device already exists with outdated X.509 credentials, this strategy automatically updates it with the device certificate's credentials from the chain.

Important: Uploaded certificates should be neither root nor intermediate certificates that are provided by a well-known Certificate Authority (CA).", "allow-create-new-devices": "Create new devices", "hint-allow-create-new-devices": "Hint: if selected new devices will be created with correct device credentials from client certificate.", "certificate-value": "Certificate in PEM format", @@ -1505,7 +1507,7 @@ "cn-regex-variable": "CN Regular Expression variable", "cn-regex-variable-required": "CN Regular Expression variable is required", "hint-cn-regex-variable": "Required to fetch device name from device's X509 certificate's common name.", - "regex-examples": "Examples of RegEx usage:
  1. Pattern: .* - matches any character (until line terminators)
    CN sample: DeviceName\\nAdditionalInfo
    Pattern matches: DeviceName OR DeviceName\\nAdditionalInfo

  2. Pattern: ^([^@]+) - matches any string that starts with one or more characters that are not the @ symbol (@ could be replaced by any other symbol)
    CN sample: DeviceName@AdditionalInfo
    Pattern matches: DeviceName

  3. Pattern: [\\w]*$ (equivalent to [a-zA-Z0-9_]*$) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
    CN sample: AdditionalInfo2110#DeviceName_01
    Pattern matches: DeviceNane_01
Note: Client will get error response in case regex is failed to match." + "regex-examples": "Examples of RegEx usage:
  1. Pattern: .* - matches any character (until line terminators)
    CN sample: DeviceName\\nAdditionalInfo
    Pattern matches: DeviceName

  2. Pattern: ^([^@]+) - matches any string that starts with one or more characters that are not the @ symbol (@ could be replaced by any other symbol)
    CN sample: DeviceName@AdditionalInfo
    Pattern matches: DeviceName

  3. Pattern: [\\w]*$ (equivalent to [a-zA-Z0-9_]*$) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
    CN sample: AdditionalInfo2110#DeviceName_01
    Pattern matches: DeviceName_01
Note: Client will get error response in case regex is failed to match." }, "condition": "Condition", "condition-type": "Condition type", From 8703a2083efd9de64e51e03ddae7271be43b9d27 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 23 Mar 2023 11:21:44 +0200 Subject: [PATCH 023/116] Device provision feature X509: 1. Added ng-templates with ngSwitchCase attribute 2. Fix NG0300: Multiple components match node with tagname button 3. Fix max-width for tb-hints --- ...ile-provision-configuration.component.html | 130 ++++++++++-------- ...ofile-provision-configuration.component.ts | 4 +- 2 files changed, 72 insertions(+), 62 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index 99cddbd576..3b2b92ce98 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -27,64 +27,74 @@ {{ 'device-profile.provision-strategy-required' | translate }} -
- - device-profile.provision-device-key - - - - {{ 'device-profile.provision-device-key-required' | translate }} - - - - device-profile.provision-device-secret - - - - {{ 'device-profile.provision-device-secret-required' | translate }} - - -
-
- - - {{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }} - -
- - device-profile.provision-strategy-x509.certificate-value - - - {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} - - - - device-profile.provision-strategy-x509.cn-regex-variable - - - {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} - - device-profile.provision-strategy-x509.hint-cn-regex-variable - -
-
+
+ + + + + + + + + + {{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }} + +
+ + device-profile.provision-strategy-x509.certificate-value + + + {{ 'device-profile.provision-strategy-x509.certificate-value-required' | translate }} + + + + device-profile.provision-strategy-x509.cn-regex-variable + + + {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} + + device-profile.provision-strategy-x509.hint-cn-regex-variable + +
+
+
+ +
+ + device-profile.provision-device-key + + + + {{ 'device-profile.provision-device-key-required' | translate }} + + + + device-profile.provision-device-secret + + + + {{ 'device-profile.provision-device-secret-required' | translate }} + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 687bc069b1..beab4828a5 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -32,7 +32,7 @@ import { DeviceProvisionType, deviceProvisionTypeTranslationMap } from '@shared/models/device.models'; -import { generateSecret, isDefinedAndNotNull } from '@core/utils'; +import { generateSecret, isBoolean, isDefinedAndNotNull } from '@core/utils'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -114,7 +114,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue('[\\w]*', {emitEvent: false}); } const allowCreateNewDevicesByX509Certificate: boolean | null = this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').value; - if (typeof allowCreateNewDevicesByX509Certificate !== 'boolean') { + if (isBoolean(allowCreateNewDevicesByX509Certificate)) { this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(true, {emitEvent: false}); } this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false}); From d603e7dbb709c59f0ba4143c869eeabff9b1dde3 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 23 Mar 2023 11:52:41 +0200 Subject: [PATCH 024/116] Device profile Transport configuration MQTT profile set max-width tb-hint --- .../mqtt-device-profile-transport-configuration.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 1ef7892ecb..9e3ae02098 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -163,6 +163,6 @@ {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} -
+ From 1498f264f68f2252c0f2f7ee4eaa1cd6824db390 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 23 Mar 2023 12:20:20 +0200 Subject: [PATCH 025/116] MQTT device profile fix tb-hint --- .../mqtt-device-profile-transport-configuration.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 9e3ae02098..4dc8b2a7b8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -163,6 +163,6 @@ {{ 'device-profile.mqtt-send-ack-on-validation-exception' | translate }} - +
From a3615f11fe1c286da9eab4444168ba03873ebd5a Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 23 Mar 2023 14:03:06 +0200 Subject: [PATCH 026/116] Device profile feature X509: - fix edit form after creation; - added fields iteration in provisionConfigurationFormGroup for type DISABLED; --- ...ofile-provision-configuration.component.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index beab4828a5..465588fa24 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -94,16 +94,13 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu }); this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => { if (type === DeviceProvisionType.DISABLED) { - this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceSecret').patchValue(null, {emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceKey').patchValue(null); - this.provisionConfigurationFormGroup.get('certificateValue').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('certificateValue').patchValue(null); - this.provisionConfigurationFormGroup.get('certificateRegExPattern').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue(null); - this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(null); + for (const field in this.provisionConfigurationFormGroup.controls) { + if (field !== 'type') { + const control = this.provisionConfigurationFormGroup.get(field); + control.disable({emitEvent: false}); + control.patchValue(null, {emitEvent: false}); + } + } } else if (type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { const certificateValue: string = this.provisionConfigurationFormGroup.get('certificateValue').value; if (!certificateValue || !certificateValue.length) { @@ -114,12 +111,14 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('certificateRegExPattern').patchValue('[\\w]*', {emitEvent: false}); } const allowCreateNewDevicesByX509Certificate: boolean | null = this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').value; - if (isBoolean(allowCreateNewDevicesByX509Certificate)) { + if (!isBoolean(allowCreateNewDevicesByX509Certificate)) { this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').patchValue(true, {emitEvent: false}); } this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); } else { const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value; if (!provisionDeviceSecret || !provisionDeviceSecret.length) { @@ -133,6 +132,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('provisionDeviceKey').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('certificateValue').disable({emitEvent: false}); this.provisionConfigurationFormGroup.get('certificateRegExPattern').disable({emitEvent: false}); + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').disable({emitEvent: false}); } }); this.provisionConfigurationFormGroup.valueChanges.subscribe(() => { From 24d69651cb39fe6bedfdac64be89f8dc0b41dd8c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 23 Mar 2023 15:20:08 +0200 Subject: [PATCH 027/116] Fix space inside EncryptionUtil and fix failed-test, while doing validation --- .../java/org/thingsboard/server/common/msg/EncryptionUtil.java | 3 ++- .../server/dao/device/DeviceProfileServiceImpl.java | 2 +- transport/mqtt/src/main/resources/tb-mqtt-transport.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index 668dd28edc..e944a8dd92 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -39,7 +39,8 @@ public class EncryptionUtil { return input.replaceAll("\n", "") .replaceAll("\r", "") .replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n") - .replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n"); + .replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n") + .trim(); } public static String pubkTrimNewLines(String input) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 6d19190199..c4ee12395f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -136,7 +136,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService Date: Fri, 24 Mar 2023 13:33:28 +0200 Subject: [PATCH 028/116] Device profile provision - fix for X509 provision strategy in provisionConfigurationFormGroup: - added resetFormControls() to reset redundant form controls; - allowCreateNewDevicesByX509Certificate added required validator; --- ...ofile-provision-configuration.component.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 465588fa24..caebc701d2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -90,7 +90,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu provisionDeviceKey: [{value: null, disabled: true}, Validators.required], certificateValue: [{value: null, disabled: true}, Validators.required], certificateRegExPattern: [{value: null, disabled: true}, Validators.required], - allowCreateNewDevicesByX509Certificate: [{value: null, disabled: true}] + allowCreateNewDevicesByX509Certificate: [{value: null, disabled: true}, Validators.required] }); this.provisionConfigurationFormGroup.get('type').valueChanges.subscribe((type) => { if (type === DeviceProvisionType.DISABLED) { @@ -117,8 +117,6 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu this.provisionConfigurationFormGroup.get('certificateValue').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('certificateRegExPattern').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').enable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceSecret').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('provisionDeviceKey').disable({emitEvent: false}); } else { const provisionDeviceSecret: string = this.provisionConfigurationFormGroup.get('provisionDeviceSecret').value; if (!provisionDeviceSecret || !provisionDeviceSecret.length) { @@ -130,9 +128,6 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu } this.provisionConfigurationFormGroup.get('provisionDeviceSecret').enable({emitEvent: false}); this.provisionConfigurationFormGroup.get('provisionDeviceKey').enable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('certificateValue').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('certificateRegExPattern').disable({emitEvent: false}); - this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').disable({emitEvent: false}); } }); this.provisionConfigurationFormGroup.valueChanges.subscribe(() => { @@ -178,6 +173,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu private updateModel(): void { let deviceProvisionConfiguration: DeviceProvisionConfiguration = null; + this.resetFormControls(this.provisionConfigurationFormGroup.value); if (this.provisionConfigurationFormGroup.valid) { deviceProvisionConfiguration = this.provisionConfigurationFormGroup.getRawValue(); } @@ -194,4 +190,15 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu horizontalPosition: 'right' })); } + + private resetFormControls(value: DeviceProvisionConfiguration) { + if (value.type === DeviceProvisionType.CHECK_PRE_PROVISIONED_DEVICES || value.type === DeviceProvisionType.ALLOW_CREATE_NEW_DEVICES) { + this.provisionConfigurationFormGroup.get('certificateValue').reset({value: null, disabled: true}, {emitEvent: false}); + this.provisionConfigurationFormGroup.get('certificateRegExPattern').reset({value: null, disabled: true}, {emitEvent: false}); + this.provisionConfigurationFormGroup.get('allowCreateNewDevicesByX509Certificate').reset({value: null, disabled: true}, {emitEvent: false}); + } else if (value.type === DeviceProvisionType.X509_CERTIFICATE_CHAIN) { + this.provisionConfigurationFormGroup.get('provisionDeviceSecret').reset({value: null, disabled: true}, {emitEvent: false}); + this.provisionConfigurationFormGroup.get('provisionDeviceKey').reset({value: null, disabled: true}, {emitEvent: false}); + } + } } From be826687eb9bee9b29b969f4d7d366f3f3c89745 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 24 Mar 2023 14:42:01 +0200 Subject: [PATCH 029/116] Device profile provision strategy X509: updated hint locale hint-allow-create-new-devices --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e9e839ce8b..3dceb760ef 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1501,7 +1501,7 @@ "certificate-chain": "X509 Certificates Chain", "hint-certificate-chain": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication. This strategy can check for pre-provisioned devices, update X.509 device credentials, or create new devices. The user uploads X.509 certificate to the device profile and sets a regular expression to fetch the device name from Common Name (CN).

Client certificates must be signed by X.509 certificate, pre-uploaded for this device profile to provision devices by the strategy. The client must establish a TLS connection using the entire chain of certificates (this chain must include device profile X.509 certificate on the last level). If a device already exists with outdated X.509 credentials, this strategy automatically updates it with the device certificate's credentials from the chain.

Important: Uploaded certificates should be neither root nor intermediate certificates that are provided by a well-known Certificate Authority (CA).", "allow-create-new-devices": "Create new devices", - "hint-allow-create-new-devices": "Hint: if selected new devices will be created with correct device credentials from client certificate.", + "hint-allow-create-new-devices": "Hint: if selected new devices will be created and client certificate will be used as device credentials.", "certificate-value": "Certificate in PEM format", "certificate-value-required": "Certificate in PEM format is required", "cn-regex-variable": "CN Regular Expression variable", From 2ac861876e1bc0f78e618cce7a15285158a8f0ce Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 29 Mar 2023 11:40:41 +0300 Subject: [PATCH 030/116] Refactoring: fix conflict, add logs --- .../update/DefaultCacheCleanupService.java | 3 + .../transport/DefaultTransportApiService.java | 77 +++++++++++-------- .../src/main/resources/thingsboard.yml | 8 +- .../DefaultTransportApiServiceTest.java | 17 ++-- ...ertificateChainProvisionConfiguration.java | 7 +- .../mqtt/MqttSslHandlerProvider.java | 10 ++- .../server/common/transport/util/SslUtil.java | 1 + .../dao/device/DeviceProfileServiceImpl.java | 22 +++--- 8 files changed, 85 insertions(+), 60 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index bd840b2f52..1e54d8d48f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -82,6 +82,9 @@ public class DefaultCacheCleanupService implements CacheCleanupService { log.info("Clearing cache to upgrade from version 3.4.2 to 3.4.3 ..."); clearCacheByName("repositorySettings"); break; + case "3.4.4": + log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0"); + clearCacheByName("deviceProfiles"); default: //Do nothing, since cache cleanup is optional. } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index ba0c2e48ca..ca886cf49b 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -174,7 +174,7 @@ public class DefaultTransportApiService implements TransportApiService { result = validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE); } else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) { TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg(); - result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain(), DeviceCredentialsType.X509_CERTIFICATE); + result = validateOrCreateDeviceX509Certificate(msg.getCertificateChain()); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { @@ -242,19 +242,12 @@ public class DefaultTransportApiService implements TransportApiService { } } - protected ListenableFuture validateOrCreateDeviceX509Certificate(String certChain, DeviceCredentialsType credentialsType) { + protected ListenableFuture validateOrCreateDeviceX509Certificate(String certChain) { List chain = convertX509CertificateChainToList(certChain); - String updateDeviceCertificateValue = chain.get(0); - String updateDeviceCertificateHash = EncryptionUtil.getSha3Hash(updateDeviceCertificateValue); - String deviceCommonName = ""; - try { - deviceCommonName = SslUtil.parseCommonName(readCertFile(chain.get(0))); - } catch (Exception ignored) { - } for (String certificateValue : chain) { String certificateHash = EncryptionUtil.getSha3Hash(certificateValue); DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(certificateHash); - if (credentials != null && credentials.getCredentialsType() == credentialsType) { + if (credentials != null && credentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { return getDeviceInfo(credentials); } DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash); @@ -262,26 +255,31 @@ public class DefaultTransportApiService implements TransportApiService { X509CertificateChainProvisionConfiguration x509Configuration = new X509CertificateChainProvisionConfiguration(); if (deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) { x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration(); + } else { + log.warn("Device Profile provision configuration is not X509CertificateChainProvisionConfiguration"); } - String deviceName = extractDeviceNameFromCNByRegEx(deviceCommonName, x509Configuration.getCertificateRegExPattern()); + String deviceName = extractDeviceNameFromCertificateCNByRegEx(chain.get(0), x509Configuration.getCertificateRegExPattern()); if (deviceName == null) { - log.error("Device name cannot be unmatched from CN!"); - return getEmptyTransportApiResponseFuture(); + log.warn("Device name has to be extract by regex from CN."); } Device device = deviceService.findDeviceByTenantIdAndName(deviceProfile.getTenantId(), deviceName); + String updateDeviceCertificateValue = chain.get(0); + String updateDeviceCertificateHash = EncryptionUtil.getSha3Hash(updateDeviceCertificateValue); if (device != null) { DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); - if (deviceCredentials != null && deviceCredentials.getCredentialsType() == credentialsType) { - deviceCredentials = updateDeviceCredentials(device.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { + deviceCredentials = updateDeviceCredentials(device.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, DeviceCredentialsType.X509_CERTIFICATE); } else if (deviceCredentials == null) { - deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), updateDeviceCertificateValue, updateDeviceCertificateHash, DeviceCredentialsType.X509_CERTIFICATE); } return getDeviceInfo(deviceCredentials); } else if (deviceProfile.getProvisionType() == DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN && x509Configuration.isAllowCreateNewDevicesByX509Certificate()) { Device savedDevice = createDevice(deviceProfile.getTenantId(), deviceProfile.getId(), deviceName, deviceProfile.getName()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); - deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, credentialsType); + deviceCredentials = updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials, updateDeviceCertificateValue, updateDeviceCertificateHash, DeviceCredentialsType.X509_CERTIFICATE); return getDeviceInfo(deviceCredentials); + } else { + log.info("Device doesn't exist and cannot be created due incorrect configuration for X509CertificateChainProvisionConfiguration"); } } } @@ -728,12 +726,16 @@ public class DefaultTransportApiService implements TransportApiService { return l != null ? l : 0; } - private String extractDeviceNameFromCNByRegEx(String commonName, String regex) { - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(commonName); - if (matcher.find()) { - return matcher.group(0); - } + private String extractDeviceNameFromCertificateCNByRegEx(String x509Value, String regex) { + try { + String commonName = SslUtil.parseCommonName(readCertFile(x509Value)); + log.trace("Extract CN [{}] by regex pattern [{}]", commonName, regex); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(commonName); + if (matcher.find()) { + return matcher.group(0); + } + } catch (Exception ignored) {} return null; } @@ -748,23 +750,26 @@ public class DefaultTransportApiService implements TransportApiService { return chain; } - private X509Certificate readCertFile(String fileContent) throws Exception { + private X509Certificate readCertFile(String fileContent) { X509Certificate certificate = null; - if (fileContent != null && !fileContent.trim().isEmpty()) { - fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replaceAll("\\s", ""); - byte[] decoded = Base64.decodeBase64(fileContent); - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - try (InputStream inStream = new ByteArrayInputStream(decoded)) { - certificate = (X509Certificate) certFactory.generateCertificate(inStream); + try { + if (fileContent != null && !fileContent.trim().isEmpty()) { + fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replaceAll("\\s", ""); + byte[] decoded = Base64.decodeBase64(fileContent); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + try (InputStream inStream = new ByteArrayInputStream(decoded)) { + certificate = (X509Certificate) certFactory.generateCertificate(inStream); + } } - } + } catch (Exception ignored) {} return certificate; } private DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials, String certificateValue, String certificateHash, DeviceCredentialsType credentialsType) { + log.trace("Updating device credentials [{}] with certificate id [{}]", deviceCredentials, certificateHash); deviceCredentials.setCredentialsId(certificateHash); deviceCredentials.setCredentialsValue(certificateValue); deviceCredentials.setCredentialsType(credentialsType); @@ -773,6 +778,7 @@ public class DefaultTransportApiService implements TransportApiService { private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, String certificateValue, String certificateHash, DeviceCredentialsType credentialsType) { + log.trace("Creating new deviceCredentials for device [{}] with certificate id [{}]", deviceId, certificateHash); DeviceCredentials createDevCredentials = new DeviceCredentials(); createDevCredentials.setDeviceId(deviceId); createDevCredentials.setCredentialsType(credentialsType); @@ -782,11 +788,14 @@ public class DefaultTransportApiService implements TransportApiService { } private Device createDevice(TenantId tenantId, DeviceProfileId deviceProfileId, String deviceName, String type) { + log.trace("Creating new device for deviceProfile [{}] with device name [{}]", deviceProfileId, deviceName); Device device = new Device(); device.setTenantId(tenantId); device.setDeviceProfileId(deviceProfileId); device.setName(deviceName); device.setType(type); - return deviceService.saveDevice(device); + device = deviceService.saveDevice(device); + tbClusterService.onDeviceUpdated(device, null); + return device; } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d96e50ca7f..c7102f40a0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -735,7 +735,7 @@ transport: # MQTT SSL configuration ssl: # Enable/disable SSL support - enabled: "${MQTT_SSL_ENABLED:false}" + enabled: "${MQTT_SSL_ENABLED:true}" # MQTT SSL bind address bind_address: "${MQTT_SSL_BIND_ADDRESS:0.0.0.0}" # MQTT SSL bind port @@ -749,11 +749,11 @@ transport: # PEM server credentials pem: # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) - cert_file: "${MQTT_SSL_PEM_CERT:mqttserver.pem}" + cert_file: "${MQTT_SSL_PEM_CERT:/home/developer/server.pem}" # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; - key_file: "${MQTT_SSL_PEM_KEY:mqttserver_key.pem}" + key_file: "${MQTT_SSL_PEM_KEY:/home/developer/server_key.pem}" # Server certificate private key password (optional) - key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}" + key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:}" # Keystore server credentials keystore: # Type of the key store (JKS or PKCS12) diff --git a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java index 0309bec131..e98513826a 100644 --- a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java @@ -96,33 +96,34 @@ public class DefaultTransportApiServiceTest { private final String deviceCertificate = "-----BEGIN CERTIFICATE-----Device certificate value-----END CERTIFICATE-----"; private final String deviceProfileCertificate = "-----BEGIN CERTIFICATE-----Device profile certificate value-----END CERTIFICATE-----"; + private final String[] chain = new String[]{deviceCertificate, deviceProfileCertificate}; @Test public void validateExistingDeviceX509Certificate() { var device = createDevice(); when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); - var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId()); + var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials); - service.validateOrCreateDeviceX509Certificate(deviceCertificate, DeviceCredentialsType.X509_CERTIFICATE); + service.validateOrCreateDeviceX509Certificate(chain[0]); verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any()); } @Test public void updateExistingDeviceX509Certificate() { - var deviceProfile = createDeviceProfile(deviceProfileCertificate); + var deviceProfile = createDeviceProfile(chain[1]); when(deviceProfileService.findDeviceProfileByCertificateHash(any())).thenReturn(deviceProfile); var device = createDevice(); when(deviceService.findDeviceByTenantIdAndName(any(), any())).thenReturn(device); when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); - var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId()); + var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); - service.validateOrCreateDeviceX509Certificate(deviceProfileCertificate, DeviceCredentialsType.X509_CERTIFICATE); + service.validateOrCreateDeviceX509Certificate(chain[1]); verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any()); verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); @@ -131,18 +132,18 @@ public class DefaultTransportApiServiceTest { @Test public void createDeviceByX509Provision() { - var deviceProfile = createDeviceProfile(deviceProfileCertificate); + var deviceProfile = createDeviceProfile(chain[1]); when(deviceProfileService.findDeviceProfileByCertificateHash(any())).thenReturn(deviceProfile); var device = createDevice(); when(deviceService.saveDevice(any())).thenReturn(device); when(deviceService.findDeviceByIdAsync(any(), any())).thenReturn(Futures.immediateFuture(device)); - var deviceCredentials = createDeviceCredentials(deviceCertificate, device.getId()); + var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); - service.validateOrCreateDeviceX509Certificate(deviceProfileCertificate, DeviceCredentialsType.X509_CERTIFICATE); + service.validateOrCreateDeviceX509Certificate(chain[1]); verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any()); verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java index 9257053b4f..34fee34ab3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java @@ -24,11 +24,16 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; @NoArgsConstructor public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration { - private String provisionDeviceSecret; private String certificateValue; private String certificateRegExPattern; private boolean allowCreateNewDevicesByX509Certificate; + @Override + public String getProvisionDeviceSecret() { + // ignore device secret for this strategy + return null; + } + @Override public DeviceProfileProvisionType getType() { return DeviceProfileProvisionType.X509_CERTIFICATE_CHAIN; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 6025a44cb4..4b6e15292a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -164,8 +164,14 @@ public class MqttSslHandlerProvider { } }); latch.await(10, TimeUnit.SECONDS); - if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) { - throw new CertificateException("Invalid Certificate's chain. Cannot find such device credentials."); + if (chain.length == 1) { + if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) { + throw new CertificateException("Invalid Device Certificate"); + } + } else { + if (!clientDeviceCertValue.equals(credentialsBodyHolder[0])) { + throw new CertificateException("Invalid Chain of X509 Certificates"); + } } } catch (Exception e) { log.error(e.getMessage(), e); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index 51ec881490..da04d50f50 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -58,6 +58,7 @@ public class SslUtil { try { x500name = new JcaX509CertificateHolder(certificate).getSubject(); } catch (CertificateEncodingException e) { + log.warn("Cannot parse CN from device certificate"); throw new RuntimeException(e); } RDN cn = x500name.getRDNs(BCStyle.CN)[0]; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index c4ee12395f..3b4be7aef4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -135,12 +135,11 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService Date: Wed, 29 Mar 2023 13:26:14 +0300 Subject: [PATCH 031/116] Fix yml extra-config --- application/src/main/resources/thingsboard.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index c7102f40a0..d96e50ca7f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -735,7 +735,7 @@ transport: # MQTT SSL configuration ssl: # Enable/disable SSL support - enabled: "${MQTT_SSL_ENABLED:true}" + enabled: "${MQTT_SSL_ENABLED:false}" # MQTT SSL bind address bind_address: "${MQTT_SSL_BIND_ADDRESS:0.0.0.0}" # MQTT SSL bind port @@ -749,11 +749,11 @@ transport: # PEM server credentials pem: # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) - cert_file: "${MQTT_SSL_PEM_CERT:/home/developer/server.pem}" + cert_file: "${MQTT_SSL_PEM_CERT:mqttserver.pem}" # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; - key_file: "${MQTT_SSL_PEM_KEY:/home/developer/server_key.pem}" + key_file: "${MQTT_SSL_PEM_KEY:mqttserver_key.pem}" # Server certificate private key password (optional) - key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:}" + key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: # Type of the key store (JKS or PKCS12) From e842c3da080cc8ff3af08ca1a6ff330a93f2d243 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 29 Mar 2023 18:53:36 +0300 Subject: [PATCH 032/116] Rewrite test using self-signed test x509 certificate --- .../transport/DefaultTransportApiService.java | 6 ++- .../DefaultTransportApiServiceTest.java | 52 ++++++++++++++++--- .../resources/mqtt/x509ChainProvision.pem | 28 ++++++++++ 3 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 application/src/test/resources/mqtt/x509ChainProvision.pem diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index ca886cf49b..f1ff95d3f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -252,15 +252,17 @@ public class DefaultTransportApiService implements TransportApiService { } DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByCertificateHash(certificateHash); if (deviceProfile != null) { - X509CertificateChainProvisionConfiguration x509Configuration = new X509CertificateChainProvisionConfiguration(); + X509CertificateChainProvisionConfiguration x509Configuration; if (deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) { x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration(); } else { log.warn("Device Profile provision configuration is not X509CertificateChainProvisionConfiguration"); + return getEmptyTransportApiResponseFuture(); } String deviceName = extractDeviceNameFromCertificateCNByRegEx(chain.get(0), x509Configuration.getCertificateRegExPattern()); if (deviceName == null) { - log.warn("Device name has to be extract by regex from CN."); + log.warn("Cannot extract device name from device's CN using regex [{}]", x509Configuration.getCertificateRegExPattern()); + return getEmptyTransportApiResponseFuture(); } Device device = deviceService.findDeviceByTenantIdAndName(deviceProfile.getTenantId(), deviceName); String updateDeviceCertificateValue = chain.get(0); diff --git a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java index e98513826a..a31d867242 100644 --- a/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.transport; import com.google.common.util.concurrent.Futures; import lombok.extern.slf4j.Slf4j; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.mock.mockito.MockBean; @@ -49,7 +50,14 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.resource.TbResourceService; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -94,9 +102,20 @@ public class DefaultTransportApiServiceTest { @SpyBean DefaultTransportApiService service; - private final String deviceCertificate = "-----BEGIN CERTIFICATE-----Device certificate value-----END CERTIFICATE-----"; - private final String deviceProfileCertificate = "-----BEGIN CERTIFICATE-----Device profile certificate value-----END CERTIFICATE-----"; - private final String[] chain = new String[]{deviceCertificate, deviceProfileCertificate}; + private String certificateChain; + private String[] chain; + + @Before + public void setUp() { + String filePath = "src/test/resources/mqtt/x509ChainProvisionTest.pem"; + try { + certificateChain = Files.readString(Paths.get(filePath)); + certificateChain = certTrimNewLinesForChainInDeviceProfile(certificateChain); + chain = fetchLeafCertificateFromChain(certificateChain); + } catch (IOException e) { + throw new RuntimeException(e); + } + } @Test public void validateExistingDeviceX509Certificate() { @@ -106,7 +125,7 @@ public class DefaultTransportApiServiceTest { var deviceCredentials = createDeviceCredentials(chain[0], device.getId()); when(deviceCredentialsService.findDeviceCredentialsByCredentialsId(any())).thenReturn(deviceCredentials); - service.validateOrCreateDeviceX509Certificate(chain[0]); + service.validateOrCreateDeviceX509Certificate(certificateChain); verify(deviceCredentialsService, times(1)).findDeviceCredentialsByCredentialsId(any()); } @@ -123,7 +142,7 @@ public class DefaultTransportApiServiceTest { when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); - service.validateOrCreateDeviceX509Certificate(chain[1]); + service.validateOrCreateDeviceX509Certificate(certificateChain); verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any()); verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); @@ -143,7 +162,7 @@ public class DefaultTransportApiServiceTest { when(deviceCredentialsService.findDeviceCredentialsByDeviceId(any(), any())).thenReturn(deviceCredentials); when(deviceCredentialsService.updateDeviceCredentials(any(), any())).thenReturn(deviceCredentials); - service.validateOrCreateDeviceX509Certificate(chain[1]); + service.validateOrCreateDeviceX509Certificate(certificateChain); verify(deviceProfileService, times(1)).findDeviceProfileByCertificateHash(any()); verify(deviceService, times(1)).findDeviceByTenantIdAndName(any(), any()); verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any()); @@ -164,7 +183,7 @@ public class DefaultTransportApiServiceTest { DeviceProfileData deviceProfileData = new DeviceProfileData(); X509CertificateChainProvisionConfiguration provision = new X509CertificateChainProvisionConfiguration(); provision.setCertificateValue(certificateValue); - provision.setCertificateRegExPattern("^$"); + provision.setCertificateRegExPattern("([^@]+)"); provision.setAllowCreateNewDevicesByX509Certificate(true); deviceProfileData.setProvisionConfiguration(provision); deviceProfile.setProfileData(deviceProfileData); @@ -178,4 +197,23 @@ public class DefaultTransportApiServiceTest { device.setId(new DeviceId(UUID.randomUUID())); return device; } + + public static String certTrimNewLinesForChainInDeviceProfile(String input) { + return input.replaceAll("\n", "") + .replaceAll("\r", "") + .replaceAll("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\n") + .replaceAll("-----END CERTIFICATE-----", "\n-----END CERTIFICATE-----\n") + .trim(); + } + + private String[] fetchLeafCertificateFromChain(String value) { + List chain = new ArrayList<>(); + String regex = "-----BEGIN CERTIFICATE-----\\s*.*?\\s*-----END CERTIFICATE-----"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) { + chain.add(matcher.group(0)); + } + return chain.toArray(new String[0]); + } } diff --git a/application/src/test/resources/mqtt/x509ChainProvision.pem b/application/src/test/resources/mqtt/x509ChainProvision.pem new file mode 100644 index 0000000000..b2ec300f78 --- /dev/null +++ b/application/src/test/resources/mqtt/x509ChainProvision.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1ow +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qK +QiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaC +kQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8G +A1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZ +uccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlow +bjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGlu +Z3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVBy +b3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72k +rDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMy +LydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8G +A1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZ +NGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A== +-----END CERTIFICATE----- From 07e00e487c666ce000d1144a0c2b14d2e893ba1c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 30 Mar 2023 09:45:38 +0300 Subject: [PATCH 033/116] Rename file for x509 test chain --- .../mqtt/{x509ChainProvision.pem => x509ChainProvisionTest.pem} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename application/src/test/resources/mqtt/{x509ChainProvision.pem => x509ChainProvisionTest.pem} (100%) diff --git a/application/src/test/resources/mqtt/x509ChainProvision.pem b/application/src/test/resources/mqtt/x509ChainProvisionTest.pem similarity index 100% rename from application/src/test/resources/mqtt/x509ChainProvision.pem rename to application/src/test/resources/mqtt/x509ChainProvisionTest.pem From 99bc639cbe33eff4d26449584913a113c3326fe3 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 30 Mar 2023 16:03:39 +0300 Subject: [PATCH 034/116] Device profile -> Device provisioning -> X509 feature: - change tb-hints Info and Regex examples as tb-help-popup - fix locales for hint- --- ...ile-provision-configuration.component.html | 22 ++++++++++++------- .../en_US/device-profile/x509-chain-hint.md | 18 +++++++++++++++ .../x509-chain-regex-examples.md | 15 +++++++++++++ .../assets/locale/locale.constant-en_US.json | 7 +++--- 4 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 ui-ngx/src/assets/help/en_US/device-profile/x509-chain-hint.md create mode 100644 ui-ngx/src/assets/help/en_US/device-profile/x509-chain-regex-examples.md diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index 3b2b92ce98..6c5da8b531 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -35,14 +35,18 @@ -
- - {{ readMore ? (' ' + ('action.less' | translate)) : ('action.more' | translate) }} +
+ +
{{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }} -
+
device-profile.provision-strategy-x509.certificate-value @@ -53,14 +57,16 @@ device-profile.provision-strategy-x509.cn-regex-variable +
{{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} - device-profile.provision-strategy-x509.hint-cn-regex-variable + device-profile.provision-strategy-x509.cn-regex-variable-hint
-
-
diff --git a/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-hint.md b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-hint.md new file mode 100644 index 0000000000..f1aa10f921 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-hint.md @@ -0,0 +1,18 @@ +##### X509 Certificate Chain info + +X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication. + +This strategy can: +* check for pre-provisioned devices +* update X.509 device credentials +* create new devices + +The user uploads X.509 certificate to the device profile and sets a regular expression to fetch the device name from *Common Name (CN)*. + +Client certificates must be signed by X.509 certificate, pre-uploaded for this device profile to provision devices by the strategy. + +The client must establish a TLS connection using the entire chain of certificates (this chain must include device profile X.509 certificate on the last level). + +If a device already exists with outdated X.509 credentials, this strategy automatically updates it with the device certificate's credentials from the chain. + +Important: Uploaded certificates should be neither root nor intermediate certificates that are provided by a well-known *Certificate Authority (CA)*. diff --git a/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-regex-examples.md b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-regex-examples.md new file mode 100644 index 0000000000..bb8a069473 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/device-profile/x509-chain-regex-examples.md @@ -0,0 +1,15 @@ +#### Examples of RegEx usage + +* **Pattern:** .* - matches any character (until line terminators) +
**CN sample:** DeviceName\nAdditionalInfo +
**Pattern matches:** DeviceName + +* **Pattern:** ^([^@]+) - matches any string that starts with one or more characters that are not the @ symbol (@ could be replaced by any other symbol) +
**CN sample:** DeviceName@AdditionalInfo +
**Pattern matches:** DeviceName + +* **Pattern:** [\w]*$ (equivalent to [a-zA-Z0-9_]\*$) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string +
**CN sample:** AdditionalInfo2110#DeviceName_01 +
**Pattern matches:** DeviceName_01 + +**Note:** Client will get error response in case regex is failed to match. diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index ec9a151ede..de26259c00 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1505,15 +1505,14 @@ "provision-secret-copied-message": "Provision secret has been copied to clipboard", "provision-strategy-x509": { "certificate-chain": "X509 Certificates Chain", - "hint-certificate-chain": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication. This strategy can check for pre-provisioned devices, update X.509 device credentials, or create new devices. The user uploads X.509 certificate to the device profile and sets a regular expression to fetch the device name from Common Name (CN).

Client certificates must be signed by X.509 certificate, pre-uploaded for this device profile to provision devices by the strategy. The client must establish a TLS connection using the entire chain of certificates (this chain must include device profile X.509 certificate on the last level). If a device already exists with outdated X.509 credentials, this strategy automatically updates it with the device certificate's credentials from the chain.

Important: Uploaded certificates should be neither root nor intermediate certificates that are provided by a well-known Certificate Authority (CA).", + "certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.", "allow-create-new-devices": "Create new devices", - "hint-allow-create-new-devices": "Hint: if selected new devices will be created and client certificate will be used as device credentials.", + "allow-create-new-devices-hint": "Hint: if selected new devices will be created and client certificate will be used as device credentials.", "certificate-value": "Certificate in PEM format", "certificate-value-required": "Certificate in PEM format is required", "cn-regex-variable": "CN Regular Expression variable", "cn-regex-variable-required": "CN Regular Expression variable is required", - "hint-cn-regex-variable": "Required to fetch device name from device's X509 certificate's common name.", - "regex-examples": "Examples of RegEx usage:
  1. Pattern: .* - matches any character (until line terminators)
    CN sample: DeviceName\\nAdditionalInfo
    Pattern matches: DeviceName

  2. Pattern: ^([^@]+) - matches any string that starts with one or more characters that are not the @ symbol (@ could be replaced by any other symbol)
    CN sample: DeviceName@AdditionalInfo
    Pattern matches: DeviceName

  3. Pattern: [\\w]*$ (equivalent to [a-zA-Z0-9_]*$) - matches zero or more occurences of any word character (letter, digit or underscore) at the end of the string
    CN sample: AdditionalInfo2110#DeviceName_01
    Pattern matches: DeviceName_01
Note: Client will get error response in case regex is failed to match." + "cn-regex-variable-hint": "Required to fetch device name from device's X509 certificate's common name." }, "condition": "Condition", "condition-type": "Condition type", From 722b6f992cc5d61d979cee219ed93aedf8d1a71a Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 30 Mar 2023 16:37:47 +0300 Subject: [PATCH 035/116] Device profile -> Device provision stratetgy X509 minor changes in tb-help-popup html --- ...ce-profile-provision-configuration.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html index 6c5da8b531..afbaa924ac 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.html @@ -37,11 +37,11 @@
-
+ trigger-text="{{ 'action.more' | translate }}">
{{ 'device-profile.provision-strategy-x509.allow-create-new-devices' | translate }} @@ -57,11 +57,11 @@ device-profile.provision-strategy-x509.cn-regex-variable -
+ [tb-help-popup-style]="{maxWidth: '820px'}"> {{ 'device-profile.provision-strategy-x509.cn-regex-variable-required' | translate }} From 98528f73eba0d81c44320223cd81483f8f1b30e6 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 30 Mar 2023 16:54:06 +0300 Subject: [PATCH 036/116] Remove 'Hint:' in locale allow-create-new-devices-hint --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index de26259c00..6f9653ca96 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1507,7 +1507,7 @@ "certificate-chain": "X509 Certificates Chain", "certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.", "allow-create-new-devices": "Create new devices", - "allow-create-new-devices-hint": "Hint: if selected new devices will be created and client certificate will be used as device credentials.", + "allow-create-new-devices-hint": "If selected new devices will be created and client certificate will be used as device credentials.", "certificate-value": "Certificate in PEM format", "certificate-value-required": "Certificate in PEM format is required", "cn-regex-variable": "CN Regular Expression variable", From bcfab48a79c78e538406dc1710108bf844f2c37c Mon Sep 17 00:00:00 2001 From: kalytka Date: Fri, 31 Mar 2023 11:30:33 +0300 Subject: [PATCH 037/116] Added label parsing in select entity dialog --- .../select-entity-dialog.component.html | 2 +- .../dialogs/select-entity-dialog.component.ts | 24 ++++++++++++++++++- .../components/widget/lib/maps/leaflet-map.ts | 22 ++++++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html index a2169cc129..fd0013236d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html @@ -31,7 +31,7 @@ entity.entity - {{ entity.entityName }} + {{ parseName(entity) }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts index 9822f6dca9..f7e94dd390 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts @@ -22,9 +22,18 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { FormattedData } from '@shared/models/widget.models'; +import { GenericFunction } from '@home/components/widget/lib/maps/map-models'; +import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils'; +import { parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; export interface SelectEntityDialogData { entities: FormattedData[]; + labelSettings: { + showLabel: boolean; + useLabelFunction: boolean; + parsedLabelFunction: GenericFunction; + label: string; + }; } @Component({ @@ -33,7 +42,6 @@ export interface SelectEntityDialogData { styleUrls: ['./select-entity-dialog.component.scss'] }) export class SelectEntityDialogComponent extends DialogComponent { - selectEntityFormGroup: UntypedFormGroup; constructor(protected store: Store, @@ -50,6 +58,20 @@ export class SelectEntityDialogComponent extends DialogComponent { let entities; + let labelSettings; switch (shapes) { case 'Polygon': case 'Rectangle': entities = this.datasources.filter(pData => !this.isValidPolygonPosition(pData)); + labelSettings = { + showLabel: this.options.showPolygonLabel, + useLabelFunction: this.options.usePolygonLabelFunction, + parsedLabelFunction: this.options.parsedPolygonLabelFunction, + label: this.options.polygonLabel + }; break; case 'Marker': entities = this.datasources.filter(mData => !this.extractPosition(mData)); + labelSettings = { + showLabel: this.options.showLabel, + useLabelFunction: this.options.useLabelFunction, + parsedLabelFunction: this.options.parsedLabelFunction, + label: this.options.label + }; break; case 'Circle': entities = this.datasources.filter(mData => !this.isValidCircle(mData)); + labelSettings = { + showLabel: this.options.showCircleLabel, + useLabelFunction: this.options.useCircleLabelFunction, + parsedLabelFunction: this.options.parsedCircleLabelFunction, + label: this.options.circleLabel + }; break; default: return of(null); @@ -205,7 +224,8 @@ export default abstract class LeafletMap { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - entities + entities, + labelSettings } }).afterClosed(); } From e2114f49c06fdef137dfecc6672132b8b8653b48 Mon Sep 17 00:00:00 2001 From: kalytka Date: Fri, 31 Mar 2023 13:14:20 +0300 Subject: [PATCH 038/116] Added parsing to the point tooltip, Update parsing HTML forms --- .../dialogs/select-entity-dialog.component.ts | 8 ++++++-- .../components/widget/lib/maps/leaflet-map.ts | 16 ++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts index f7e94dd390..9402da4c15 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts @@ -65,7 +65,9 @@ export class SelectEntityDialogComponent extends DialogComponent Date: Fri, 31 Mar 2023 14:57:10 +0300 Subject: [PATCH 039/116] Refactoring --- .../select-entity-dialog.component.html | 2 +- .../dialogs/select-entity-dialog.component.ts | 35 +++---------------- .../components/widget/lib/maps/leaflet-map.ts | 22 ++++++------ .../components/widget/lib/maps/maps-utils.ts | 29 ++++++++++++++- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html index fd0013236d..99740ac601 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html @@ -31,7 +31,7 @@ entity.entity - {{ parseName(entity) }} + {{ entity.entityParseName }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts index 9402da4c15..f2b3d09b5c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts @@ -20,20 +20,11 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormattedData } from '@shared/models/widget.models'; -import { GenericFunction } from '@home/components/widget/lib/maps/map-models'; -import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils'; -import { parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; export interface SelectEntityDialogData { entities: FormattedData[]; - labelSettings: { - showLabel: boolean; - useLabelFunction: boolean; - parsedLabelFunction: GenericFunction; - label: string; - }; } @Component({ @@ -42,13 +33,13 @@ export interface SelectEntityDialogData { styleUrls: ['./select-entity-dialog.component.scss'] }) export class SelectEntityDialogComponent extends DialogComponent { - selectEntityFormGroup: UntypedFormGroup; + selectEntityFormGroup: FormGroup; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: SelectEntityDialogData, public dialogRef: MatDialogRef, - public fb: UntypedFormBuilder) { + public fb: FormBuilder) { super(store, router, dialogRef); this.selectEntityFormGroup = this.fb.group( @@ -58,25 +49,7 @@ export class SelectEntityDialogComponent extends DialogComponent, @@ -83,3 +86,27 @@ export function isJSON(data: string): boolean { return false; } } + +interface labelSettings { + showLabel: boolean; + useLabelFunction: boolean; + parsedLabelFunction: GenericFunction; + label: string; +} + +export function entitiesParseName(entities: FormattedData[], labelSettings: labelSettings): FormattedData[] { + for (const entity of entities) { + if (labelSettings?.showLabel) { + const pattern = labelSettings.useLabelFunction ? safeExecute(labelSettings.parsedLabelFunction, + [entity, entities, entity.dsIndex]) : labelSettings.label; + const markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); + const replaceInfoLabelMarker = processDataPattern(pattern, entity); + const div = document.createElement('div'); + div.innerHTML = fillDataPattern(markerLabelText, replaceInfoLabelMarker, entity); + entity.entityParseName = div.textContent || div.innerText || ''; + } else { + entity.entityParseName = entity.entityName; + } + } + return entities; +} From 1d2368e98f0f6f299c9c233b8a8dcd911478916d Mon Sep 17 00:00:00 2001 From: kalytka Date: Mon, 3 Apr 2023 11:03:58 +0300 Subject: [PATCH 040/116] Refactoring --- .../app/modules/home/components/widget/lib/maps/maps-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index 585f0cd54a..4aad829eac 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -95,13 +95,13 @@ interface labelSettings { } export function entitiesParseName(entities: FormattedData[], labelSettings: labelSettings): FormattedData[] { + const div = document.createElement('div'); for (const entity of entities) { if (labelSettings?.showLabel) { const pattern = labelSettings.useLabelFunction ? safeExecute(labelSettings.parsedLabelFunction, [entity, entities, entity.dsIndex]) : labelSettings.label; const markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true); const replaceInfoLabelMarker = processDataPattern(pattern, entity); - const div = document.createElement('div'); div.innerHTML = fillDataPattern(markerLabelText, replaceInfoLabelMarker, entity); entity.entityParseName = div.textContent || div.innerText || ''; } else { From 27595dc1b7277c433dbe297103a350fa4b666f92 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 3 Apr 2023 11:17:36 +0300 Subject: [PATCH 041/116] UI: Disables the ability for the user to interact with the tooltip element --- ui-ngx/src/app/shared/shared.module.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 6108fdbeaf..d58498911d 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -53,7 +53,7 @@ import { MatStepperModule } from '@angular/material/stepper'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipModule } from '@angular/material/tooltip'; import { MatListModule } from '@angular/material/list'; import { DatetimeAdapter, MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; @@ -220,6 +220,12 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) useValue: { hideSingleSelectionIndicator: true } + }, + { + provide: MAT_TOOLTIP_DEFAULT_OPTIONS, + useValue: { + disableTooltipInteractivity: true + } } ], declarations: [ From 6421f6596bb83bb9a0fe58ea057369dba73954f2 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 3 Apr 2023 19:39:28 +0300 Subject: [PATCH 042/116] UI: Implement Sys Admin home page --- .../system/DefaultSystemInfoService.java | 6 +- .../entity/entities-table.component.ts | 23 +- .../entity/entity-table-component.models.ts | 2 + .../home-links/home-links-routing.module.ts | 20 +- .../pages/home-links/sys_admin_home_page.raw | 1704 +++++++++++++++++ .../shared/components/markdown.component.ts | 5 +- .../src/app/shared/models/page/page-link.ts | 2 + ui-ngx/src/assets/home/greetings_bg.svg | 86 + .../assets/locale/locale.constant-en_US.json | 1 + ui-ngx/src/styles.scss | 6 +- 10 files changed, 1845 insertions(+), 10 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw create mode 100644 ui-ngx/src/assets/home/greetings_bg.svg diff --git a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java index caf3abefa0..a2e9f0a72a 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.SystemInfo; import org.thingsboard.server.common.data.SystemInfoData; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -50,6 +51,7 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import javax.annotation.Nullable; import javax.annotation.PreDestroy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; @@ -154,13 +156,13 @@ public class DefaultSystemInfoService extends TbApplicationEventListener clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo()); BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData))); - doSave(Collections.singletonList(clusterDataKv)); + doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv)); } private void saveCurrentMonolithSystemInfo() { long ts = System.currentTimeMillis(); List tsList = new ArrayList<>(); - + tsList.add(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", false))); getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", (long) v)))); getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", (long) v)))); getDiscSpaceUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", (long) v)))); diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 9d2ffac552..be2b6f97a4 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -63,7 +63,7 @@ import { isDefined, isEmptyStr, isEqual, isString, isUndefined } from '@core/uti import { HasUUID } from '@shared/models/id/has-uuid'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; -import { IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models'; +import { EntitiesTableAction, IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models'; import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; @Component({ @@ -231,8 +231,11 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa const routerQueryParams: PageQueryParam = this.route.snapshot.queryParams; let sortOrder: SortOrder = null; + let initialAction: EntitiesTableAction = null; if (this.pageMode) { - if (this.entitiesTableConfig.defaultSortOrder || routerQueryParams.hasOwnProperty('direction') || routerQueryParams.hasOwnProperty('property')) { + initialAction = routerQueryParams?.action; + if (this.entitiesTableConfig.defaultSortOrder || routerQueryParams.hasOwnProperty('direction') + || routerQueryParams.hasOwnProperty('property')) { sortOrder = { property: routerQueryParams?.property || this.entitiesTableConfig.defaultSortOrder.property, direction: routerQueryParams?.direction || this.entitiesTableConfig.defaultSortOrder.direction @@ -282,6 +285,22 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.updatePaginationSubscriptions(); }, 0); } + if (this.pageMode) { + if (initialAction) { + const queryParams: PageQueryParam = {}; + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: '', + replaceUrl: true + }); + } + if (initialAction === 'add') { + setTimeout(() => { + this.addEntity(null); + }, 0); + } + } } ngAfterViewInit() { diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index bbc8d76951..033747f6d3 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -33,6 +33,8 @@ import { } from '@home/models/entity/entities-table-config.models'; import { ActivatedRoute } from '@angular/router'; +export type EntitiesTableAction = 'add'; + export interface IEntitiesTableComponent { entitiesTableConfig: EntityTableConfig>; translations: EntityTypeTranslation; diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index 19da00e0f7..be408da234 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -22,15 +22,31 @@ import { Authority } from '@shared/models/authority.enum'; import { Observable } from 'rxjs'; import { HomeDashboard } from '@shared/models/dashboard.models'; import { DashboardService } from '@core/http/dashboard.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { map } from 'rxjs/operators'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import sysAdminHomePageDashboardJson from '!raw-loader!./sys_admin_home_page.raw'; @Injectable() export class HomeDashboardResolver implements Resolve { - constructor(private dashboardService: DashboardService) { + constructor(private dashboardService: DashboardService, + private store: Store) { } resolve(): Observable { - return this.dashboardService.getHomeDashboard(); + return this.dashboardService.getHomeDashboard().pipe( + map((dashboard) => { + if (!dashboard) { + if (getCurrentAuthUser(this.store).authority === Authority.SYS_ADMIN) { + dashboard = JSON.parse(sysAdminHomePageDashboardJson); + dashboard.hideDashboardToolbar = true; + } + } + return dashboard; + }) + ); } } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw b/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw new file mode 100644 index 0000000000..9a34cdf51a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw @@ -0,0 +1,1704 @@ +{ + "title": "System Administrator Home Page", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "04a9afdf-fbb4-4eaf-0fcc-a190c6202c62": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "ae870700-071f-b3bc-406c-16ba554c5a55", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "tenantsCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "\n
\n
${tenantsCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-header {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 28px;\n line-height: 36px;\n}\n" + }, + "title": "Tenants", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "04a9afdf-fbb4-4eaf-0fcc-a190c6202c62" + }, + "cf1b7a07-2a16-cab7-2528-ef5caba4ac38": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "ca4d90e4-f9ae-6fca-6b09-85815a48d52b", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "tenantProfilesCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "\n
\n
${tenantProfilesCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-header {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 28px;\n line-height: 36px;\n\n}\n" + }, + "title": "Tenant Profiles", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "cf1b7a07-2a16-cab7-2528-ef5caba4ac38" + }, + "551a27a0-cd23-a70c-0de2-c97ab6673faa": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "devicesCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'device.devices' | translate }}
\n
\n
${devicesCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" + }, + "title": "Devices", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "551a27a0-cd23-a70c-0de2-c97ab6673faa" + }, + "a41682b0-2f23-23e9-0807-e8b47c1c1795": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "619cdf00-a042-3b55-124e-194c1b28c236", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "assetsCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'asset.assets' | translate }}
\n
\n
${assetsCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" + }, + "title": "Assets", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "a41682b0-2f23-23e9-0807-e8b47c1c1795" + }, + "1abf533c-aee7-080d-0274-d047e1d5dc63": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "usersCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'user.users' | translate }}
\n
\n
${usersCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" + }, + "title": "Users", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "1abf533c-aee7-080d-0274-d047e1d5dc63" + }, + "5d02c63b-bcb1-aaf3-f35d-de4dbaca96c2": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "customersCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
{{ 'customer.customers' | translate }}
\n
\n
${customersCount:0}
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}" + }, + "title": "Customers", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "5d02c63b-bcb1-aaf3-f35d-de4dbaca96c2" + }, + "e70b51f8-04dd-985f-be81-d0c76c589d4a": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680183320196, + "endTimeMs": 1680269720196 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
Get started
\n\n \n {{ index + 1 }}\n \n -1\">\n \n
\n
Create Tenant and Tenant Administrator
\n {{ 'tenant.tenants' | translate }}\n
\n
\n

\n A tenant is an individual or organization that owns or produces devices and assets. Tenants can create Tenant administrator users.\n

\n

\n Follow the documentation on how to do it: \n

\n descriptionHow to create Tenant\n
\n 0\">\n \n
\n
Configure feature: Mail server
\n {{ 'admin.settings' | translate }}\n
\n
\n

\n Users will receive activation and password reset emails from the System Administrator via an SMTP server.\n

\n

\n Follow the documentation on how to do it:\n

\n descriptionHow to configure Mail server\n
\n 1\">\n \n
\n
Configure feature: SMS provider
\n {{ 'admin.settings' | translate }}\n
\n
\n

\n To distribute SMS messages about alarms, the System Administrator can configure SMS providers.\n

\n

\n Follow the documentation on how to do it:\n

\n descriptionHow to configure SMS provider\n
\n 2\">\n \n
\n
Configure feature: 2FA
\n {{ 'admin.settings' | translate }}\n
\n
\n

\n Adding two-factor authentication to your account increases security and prevents malicious access.\n

\n

\n Follow the documentation on how to do it:\n

\n descriptionHow to configure 2FA\n
\n 3\">\n \n
\n
Configure feature: OAuth 2
\n {{ 'admin.settings' | translate }}\n
\n
\n

\n You can provide your customers with Single Sign-On functionality and automatically create tenants, customers, or sub-customers by using external user management platforms that support OAuth 2.0.\n

\n

\n Follow the documentation on how to do it:\n

\n descriptionHow to configure OAuth 2\n
\n 4\">\n \n
\n
Configure feature: Notifications
\n {{ 'admin.settings' | translate }}\n
\n
\n

\n Some text\n

\n

\n Follow the documentation on how to do it:\n

\n descriptionHow to configure Notifications\n
\n
\n
\n \n
\n\n", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-title {\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.76);\n padding-bottom: 24px;\n}\n" + }, + "title": "Get started", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "widgetCss": ".mat-stepper-vertical.tb-get-started {\n /** border-radius: 12px; \n background: rgba(0, 0, 0, 0.03); **/\n}\n\n.tb-get-started .mat-vertical-stepper-header:not([aria-selected=\"true\"]):not(:hover) {\n /** background: #fff;**/\n}\n\n.tb-get-started .mat-vertical-stepper-header {\n border-radius: 12px;\n padding: 12px;\n}\n\n.tb-get-started .mat-vertical-stepper-header[aria-selected=\"true\"] {\n background: rgba(0, 0, 0, 0.03);\n border-bottom-right-radius: 0px;\n border-bottom-left-radius: 0px;\n}\n\n.tb-get-started .mat-vertical-content-container {\n margin-left: 0px;\n border-bottom-left-radius: 12px;\n border-bottom-right-radius: 12px;\n background: rgba(0, 0, 0, 0.03);\n}\n\n.tb-get-started .mat-stepper-vertical-line::before {\n content: none;\n}\n\n.tb-get-started .mat-step-label {\n width: 100%;\n}\n\n.tb-get-started .mat-step-label {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-get-started .mat-step-label.mat-step-label-selected {\n font-weight: 500;\n font-size: 16px;\n line-height: 24px;\n color: rgba(0, 0, 0, 0.87);\n}\n\n.tb-get-started .mat-vertical-content p {\n font-style: normal;\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.54);\n}\n", + "pageSize": 1024, + "noDataDisplayMessage": "", + "actions": { + "elementClick": [ + { + "name": "getting-started-finish", + "icon": "more_horiz", + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "customPretty", + "customHtml": "
\n
\n \n \n
\n
\n
\n done\n
\n
\n
\n
Welcome on board
\n
You did great with it!
\n
\n
\n", + "customCss": ".tb-congrat-dialog .mat-mdc-dialog-container .mdc-dialog__surface {\n border-radius: 8px;\n background-image: url(/assets/home/greetings_bg.svg);\n background-repeat: no-repeat;\n}\n\n.tb-congrat-dialog-container {\n width: 500px;\n height: 290px;\n}\n\n.tb-done-container {\n width: 70px;\n height: 70px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.05);\n}\n\n.tb-done-icon {\n width: 62px;\n height: 62px;\n line-height: 56px;\n font-size: 36px;\n text-align: center;\n border-radius: 50%;\n border: 2px solid #305680;\n background: #FFFFFF;\n}\n\n.tb-done-welcome-title {\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.87);\n padding-bottom: 8px;\n}\n\n.tb-done-welcome-text {\n font-weight: 400;\n font-size: 16px;\n line-height: 24px;\n letter-spacing: 0.15px;\n color: rgba(0, 0, 0, 0.54);\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenGreetingDialog();\n\nfunction openGreetingDialog() {\n customDialog.customDialog(htmlTemplate, GreetingDialogController).subscribe();\n}\n\nfunction GreetingDialogController(instance) {\n let vm = instance;\n vm.dialogRef.addPanelClass(\"tb-congrat-dialog\");\n \n vm.close = function() {\n vm.dialogRef.close(null);\n };\n}\n\n", + "customResources": [], + "openInSeparateDialog": false, + "openInPopover": false, + "id": "d7d0b8a5-ead7-a29c-2c10-5c9704fb3739" + } + ] + } + }, + "row": 0, + "col": 0, + "id": "e70b51f8-04dd-985f-be81-d0c76c589d4a" + }, + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
Transport messages\n info\n
\n
\n \n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n" + }, + "title": "Transport messages", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f" + }, + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "timeseries_bars_flot", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "Transport messages", + "color": "#305680", + "settings": {}, + "_hash": 0.2880464219129071, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": [] + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": true, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1680443065451, + "endTimeMs": 1680529465451 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "stack": true, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": 0, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "return value % 1000 === 0 ? ((value / 1000) + 'k') : '';" + }, + "defaultBarWidth": 1800000, + "barAlignment": "left", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "Transport messages", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "widgetStyle": {}, + "useDashboardTimewindow": false, + "showLegend": false, + "actions": {}, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", + "pageSize": 1024, + "noDataDisplayMessage": "", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } + }, + "row": 0, + "col": 0, + "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" + }, + "4b5e47ed-c197-a937-d727-041ba8decec2": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "clusterMode", + "type": "timeseries", + "label": "clusterMode", + "color": "#2196f3", + "settings": {}, + "_hash": 0.7272123990942316 + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#ffffff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextFunction": "let clusterMode = data[0]['clusterMode'];\nlet targetState = 'monolith_system_info';\nif (clusterMode === 'true') {\n targetState = 'cluster_system_info';\n}\nreturn '
';\n", + "applyDefaultMarkdownStyle": false + }, + "title": "Transport messages", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": { + "border": "none", + "borderRadius": "0", + "boxShadow": "none" + }, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "4b5e47ed-c197-a937-d727-041ba8decec2" + }, + "76607897-dfd0-8946-7451-3de6a39e4bbf": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "cpuUsage", + "type": "timeseries", + "label": "cpuUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117 + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
CPU
\n
\n
${cpuUsage:0}%
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" + }, + "title": "CPU", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-right: 6px;\n}", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "76607897-dfd0-8946-7451-3de6a39e4bbf" + }, + "8c2f02bc-ca69-2955-8730-4e945f77da28": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "memoryUsage", + "type": "timeseries", + "label": "memoryUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
RAM
\n
\n
${memoryUsage:0}%
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" + }, + "title": "RAM", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-right: 6px;\n margin-left: 6px;\n}", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "8c2f02bc-ca69-2955-8730-4e945f77da28" + }, + "e3686199-5117-fa5a-d59c-aada6269653e": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "discUsage", + "type": "timeseries", + "label": "discUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
Disk
\n
\n
${discUsage:0}%
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" + }, + "title": "Disk", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-left: 6px;\n}", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "e3686199-5117-fa5a-d59c-aada6269653e" + }, + "eace9148-b02a-48fe-1a95-acd9928aa8c5": { + "isSystemType": true, + "bundleAlias": "charts", + "typeAlias": "basic_timeseries", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "cpuUsage", + "type": "timeseries", + "label": "CPU", + "color": "#305680", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": false, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.9347575372081658, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "RAM", + "color": "#ac3bc9", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": false, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.31887216598848855, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "Disk", + "color": "#40d0ae", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 3, + "fillLines": false, + "showPoints": false, + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showPointShape": "circle", + "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "showSeparateAxis": false, + "axisPosition": "left", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.26499182606431004, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "latestDataKeys": null + } + ], + "timewindow": { + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "timewindowMs": 3600000, + "quickInterval": "CURRENT_DAY", + "interval": 10000 + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": false, + "horizontalLines": false, + "outlineWidth": 0, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": 0, + "max": 100, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 4, + "smoothLines": true, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [] + }, + "title": "System Info Chart", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": false, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": ".tb-widget-container {\n display: block;\n height: calc(100% - 12px);\n margin-top: 12px;\n}\n\ntr.tb-legend-keys td .tb-legend-line {\n height: 5px !important;\n vertical-align: middle;\n}\n\ntr.tb-legend-keys td .tb-legend-label {\n\tcursor: pointer;\n\tuser-select: none;\n\tfont-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.54);\n}", + "pageSize": 1024, + "units": "%", + "noDataDisplayMessage": "", + "showLegend": true, + "legendConfig": { + "direction": "row", + "position": "top", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": false, + "showLatest": false + }, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "eace9148-b02a-48fe-1a95-acd9928aa8c5" + } + }, + "states": { + "default": { + "name": "System Administrator Home Page", + "root": true, + "layouts": { + "main": { + "widgets": { + "04a9afdf-fbb4-4eaf-0fcc-a190c6202c62": { + "sizeX": 10, + "sizeY": 4, + "row": 0, + "col": 0 + }, + "cf1b7a07-2a16-cab7-2528-ef5caba4ac38": { + "sizeX": 10, + "sizeY": 4, + "row": 0, + "col": 10 + }, + "551a27a0-cd23-a70c-0de2-c97ab6673faa": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 0 + }, + "a41682b0-2f23-23e9-0807-e8b47c1c1795": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 5 + }, + "1abf533c-aee7-080d-0274-d047e1d5dc63": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 10 + }, + "5d02c63b-bcb1-aaf3-f35d-de4dbaca96c2": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 15 + }, + "e70b51f8-04dd-985f-be81-d0c76c589d4a": { + "sizeX": 16, + "sizeY": 26, + "row": 0, + "col": 38 + }, + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "sizeX": 18, + "sizeY": 8, + "row": 11, + "col": 20 + }, + "4b5e47ed-c197-a937-d727-041ba8decec2": { + "sizeX": 18, + "sizeY": 11, + "row": 0, + "col": 20 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 54, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + }, + "transport_messages": { + "name": "Transport messages", + "root": false, + "layouts": { + "main": { + "widgets": { + "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { + "sizeX": 24, + "sizeY": 11, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#ffffff", + "columns": 24, + "margin": 0, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70 + } + } + } + }, + "monolith_system_info": { + "name": "Monolith System Info", + "root": false, + "layouts": { + "main": { + "widgets": { + "76607897-dfd0-8946-7451-3de6a39e4bbf": { + "sizeX": 8, + "sizeY": 3, + "row": 0, + "col": 0 + }, + "8c2f02bc-ca69-2955-8730-4e945f77da28": { + "sizeX": 8, + "sizeY": 3, + "row": 0, + "col": 8 + }, + "e3686199-5117-fa5a-d59c-aada6269653e": { + "sizeX": 8, + "sizeY": 3, + "row": 0, + "col": 16 + }, + "eace9148-b02a-48fe-1a95-acd9928aa8c5": { + "sizeX": 24, + "sizeY": 8, + "row": 3, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "rgba(0,0,0,0)", + "columns": 24, + "margin": 0, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70 + } + } + } + }, + "cluster_system_info": { + "name": "Cluster System Info", + "root": false, + "layouts": { + "main": { + "widgets": {}, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%" + } + } + } + } + }, + "entityAliases": { + "ae870700-071f-b3bc-406c-16ba554c5a55": { + "id": "ae870700-071f-b3bc-406c-16ba554c5a55", + "alias": "Tenants", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT" + } + }, + "ca4d90e4-f9ae-6fca-6b09-85815a48d52b": { + "id": "ca4d90e4-f9ae-6fca-6b09-85815a48d52b", + "alias": "TenantProfiles", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "TENANT" + } + }, + "a1ddb8fa-90ff-5598-e7f2-e254194d055d": { + "id": "a1ddb8fa-90ff-5598-e7f2-e254194d055d", + "alias": "Devices", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "DEVICE" + } + }, + "619cdf00-a042-3b55-124e-194c1b28c236": { + "id": "619cdf00-a042-3b55-124e-194c1b28c236", + "alias": "Assets", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "ASSET" + } + }, + "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2": { + "id": "1d97ff7f-8b42-5882-f87b-16f3d0dee4f2", + "alias": "Users", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "USER" + } + }, + "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31": { + "id": "0dd2b154-59f4-0f97-da1a-d85f5b5cfe31", + "alias": "Customers", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "CUSTOMER" + } + }, + "d9229b29-3f46-de8d-7fe8-eb0c43c75079": { + "id": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "alias": "Api Usage State", + "filter": { + "type": "apiUsageState", + "resolveMultiple": true + } + } + }, + "filters": {}, + "timewindow": { + "displayValue": "", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168326072, + "endTimeMs": 1680254726072 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true, + "titleColor": "rgba(0,0,0,0.870588)", + "showDashboardLogo": false, + "dashboardLogoUrl": null, + "hideToolbar": true, + "showFilters": true, + "showUpdateDashboardImage": true, + "dashboardCss": ".tb-widget-container > .tb-widget {\n border: 1px solid rgba(0, 0, 0, 0.05);\n box-shadow: 0px 5px 16px rgba(0, 0, 0, 0.04);\n border-radius: 12px;\n}\n\n.tb-home-widget-title {\n font-style: normal;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n}\n\n.tb-home-widget-link {\n position: relative;\n border-bottom: none;\n}\n\n.tb-home-widget-link:hover {\n border-bottom: none;\n}\n\n.tb-home-widget-link:focus {\n border-bottom: none;\n}\n\n.tb-home-widget-link::after {\n content: 'arrow_forward';\n display: inline-block;\n position: absolute;\n top: -2px;\n right: -28px;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n}\n\n.tb-home-widget-link:hover::after {\n color: inherit;\n}\n\n.tb-home-widget-info-icon {\n color: rgba(0, 0, 0, 0.12);\n font-size: 16px;\n width: 16px;\n height: 16px;\n line-height: 15px;\n vertical-align: middle;\n}\n\n.tb-widget-container > .tb-widget .tb-timewindow {\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n color: rgba(0, 0, 0, 0.54);\n}" + } + }, + "externalId": null, + "name": "System Administrator Home Page" +} \ No newline at end of file diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 4ed77b66d2..5af7ccce6b 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -23,7 +23,7 @@ import { EventEmitter, Inject, Injector, - Input, + Input, NgZone, OnChanges, Output, Renderer2, @@ -90,6 +90,7 @@ export class TbMarkdownComponent implements OnChanges { constructor(private help: HelpService, private cd: ChangeDetectorRef, + private zone: NgZone, public markdownService: MarkdownService, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type, private dynamicComponentFactoryService: DynamicComponentFactoryService, @@ -97,7 +98,7 @@ export class TbMarkdownComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (isDefinedAndNotNull(this.data)) { - this.render(this.data); + this.zone.run(() => this.render(this.data)); } } diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index e55ac494e0..586b246f1e 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -18,6 +18,7 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { getDescendantProp, isObject } from '@core/utils'; import { SortDirection } from '@angular/material/sort'; +import { EntitiesTableAction } from '@home/models/entity/entity-table-component.models'; export const MAX_SAFE_PAGE_SIZE = 2147483647; @@ -27,6 +28,7 @@ export interface PageQueryParam extends Partial{ textSearch?: string; pageSize?: number; page?: number; + action?: EntitiesTableAction; } export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction { diff --git a/ui-ngx/src/assets/home/greetings_bg.svg b/ui-ngx/src/assets/home/greetings_bg.svg new file mode 100644 index 0000000000..6bc782e0e4 --- /dev/null +++ b/ui-ngx/src/assets/home/greetings_bg.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 7d9eed195b..258fc3ef7b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3459,6 +3459,7 @@ "tenant-profile": "Tenant profile", "tenant-profiles": "Tenant profiles", "add": "Add tenant profile", + "add-profile": "Add profile", "edit": "Edit tenant profile", "tenant-profile-details": "Tenant profile details", "no-tenant-profiles-text": "No tenant profiles found", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 369d83ec27..5fe414dd29 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -74,7 +74,7 @@ body { line-height: normal; } -a { +a:not(.mdc-button) { font-weight: 400; color: #106cc8; text-decoration: none; @@ -85,7 +85,9 @@ a { a:hover, a:focus { - border-bottom: 1px solid #4054b2; + &:not(.mdc-button) { + border-bottom: 1px solid #4054b2; + } } p { From fae2427d8ba774d834c49ad7a47ec5fa9c3597c8 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 4 Apr 2023 11:59:33 +0300 Subject: [PATCH 043/116] refactoring --- .../server/controller/plugin/TbWebSocketHandler.java | 2 +- application/src/main/resources/thingsboard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 822a38c3fd..88bdf0f709 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -84,7 +84,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke private long sendTimeout; @Value("${server.ws.ping_timeout:30000}") private long pingTimeout; - @Value("${server.ws.max_queue_messages_per_session:1000}") + @Value("${server.ws.default_queue_messages_per_session:1000}") private int wsQueueMessagesPerSession; private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e776497717..a4c82aac99 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -65,7 +65,7 @@ server: max_per_user: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_MAX_PER_USER:10}" max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}" max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}" - max_queue_messages_per_session: "${TB_SERVER_WS_MAX_QUEUE_MESSAGES_PER_SESSION:1000}" + default_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}" rest: server_side_rpc: # Minimum value of the server side RPC timeout. May override value provided in the REST API call. From 193c1d030613b3b9ddb03a584fd20fe1d508e105 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 4 Apr 2023 12:37:24 +0300 Subject: [PATCH 044/116] UI: Fix disable time interval in timewindow panel --- .../app/shared/components/time/timeinterval.component.ts | 4 +--- .../shared/components/time/timewindow-panel.component.ts | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.ts b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts index 9d7605c59d..dca556b221 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.ts +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts @@ -64,8 +64,6 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor { @coerceBoolean() isEdit = false; - hideFlagValue = false; - @Input() @coerceBoolean() hideFlag = false; @@ -208,7 +206,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor { } onHideFlagChange() { - this.hideFlagChange.emit(this.hideFlagValue); + this.hideFlagChange.emit(this.hideFlag); } onTimeInputChange(type: string) { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 7b30759cdf..171a692e44 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -157,9 +157,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { disabled: hideTimezone }] }); - this.updateValidators(); - this.timewindowForm.get('aggregation.type').valueChanges.subscribe(() => { - this.updateValidators(); + this.updateValidators(this.timewindowForm.get('aggregation.type').value); + this.timewindowForm.get('aggregation.type').valueChanges.subscribe((aggregationType: AggregationType) => { + this.updateValidators(aggregationType); }); } @@ -172,8 +172,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { return limit; } - private updateValidators() { - const aggType = this.timewindowForm.get('aggregation.type').value; + private updateValidators(aggType: AggregationType) { if (aggType !== AggregationType.NONE) { this.timewindowForm.get('aggregation.limit').clearValidators(); } else { From 6041796e7c0e80a64c7e4b194cc9ee694a883278 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 4 Apr 2023 17:27:00 +0300 Subject: [PATCH 045/116] UI: Sys Admin Home Page --- .../src/app/modules/home/components/tokens.ts | 3 + .../cluster-info-table.component.html | 77 ++ .../cluster-info-table.component.scss | 71 ++ .../home-page/cluster-info-table.component.ts | 236 ++++++ .../lib/home-page/home-page-widgets.module.ts | 35 + .../widget/lib/markdown-widget.component.html | 2 +- .../widget/lib/markdown-widget.component.ts | 3 +- .../widget/widget-components.module.ts | 7 +- .../pages/home-links/sys_admin_home_page.raw | 761 +++++++++++------- .../models/telemetry/telemetry.models.ts | 23 +- .../fonts/MaterialIconsRound-Regular.otf | Bin 0 -> 400092 bytes ui-ngx/src/assets/fonts/material-icons.css | 16 + ui-ngx/src/index.html | 1 + 13 files changed, 954 insertions(+), 281 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts create mode 100644 ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf diff --git a/ui-ngx/src/app/modules/home/components/tokens.ts b/ui-ngx/src/app/modules/home/components/tokens.ts index 5ddd9fe661..abba6a0636 100644 --- a/ui-ngx/src/app/modules/home/components/tokens.ts +++ b/ui-ngx/src/app/modules/home/components/tokens.ts @@ -23,6 +23,9 @@ export const SHARED_HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = export const HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = new InjectionToken>('HOME_COMPONENTS_MODULE_TOKEN'); +export const WIDGET_COMPONENTS_MODULE_TOKEN: InjectionToken> = + new InjectionToken>('WIDGET_COMPONENTS_MODULE_TOKEN'); + export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken> = new InjectionToken>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html new file mode 100644 index 0000000000..d2807d34dd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.html @@ -0,0 +1,77 @@ + + + + + Service id + + + {{ serviceData.serviceId }} + + + + + Service type + + + {{ serviceData.serviceType }} + + + + + CPU + + + {{ serviceData.cpuUsage }}% + + + + + RAM + + + {{ serviceData.memoryUsage }}% + + + + + Disk + + + {{ serviceData.discUsage }}% + + + + +
+No data diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss new file mode 100644 index 0000000000..0d76956dfc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + span.no-data-found { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + } + + .tb-resource-usage { + cursor: default; + &.tb-status-warn, &.tb-status-critical { + position: relative; + overflow: visible; + &:before { + position: absolute; + top: 15px; + left: -24px; + font-family: 'Material Icons Round'; + font-size: 20px; + line-height: 1; + } + } + &.tb-status-warn { + color: #FAA405; + &:before { + content: "warning"; + color: #FAA405; + } + } + &.tb-status-critical { + color: #D12730; + &:before { + content: "warning"; + color: #D12730; + } + } + } +} + +:host ::ng-deep { + .mdc-data-table__header-cell { + font-weight: 500; + font-size: 11px; + line-height: 20px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.54); + } + .mdc-data-table__cell { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.76); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts new file mode 100644 index 0000000000..527612ccc0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/cluster-info-table.component.ts @@ -0,0 +1,236 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, ChangeDetectionStrategy, Component, NgZone, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount, share } from 'rxjs/operators'; +import { MatSort } from '@angular/material/sort'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { EntityDataUpdate, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { EntityKeyType } from '@shared/models/query/query.models'; + +export interface SystemInfoData { + serviceId: string; + serviceType: string; + cpuUsage: number; + cpuCount: number; + memoryUsage: number; + totalMemory: number; + discUsage: number; + totalDiscSpace: number; +} + +@Component({ + selector: 'tb-cluster-info-table', + templateUrl: './cluster-info-table.component.html', + styleUrls: ['./cluster-info-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ClusterInfoTableComponent extends PageComponent implements OnInit, AfterViewInit { + + @ViewChild(MatSort) sort: MatSort; + + displayedColumns = ['serviceId', 'serviceType', 'cpuUsage', 'memoryUsage', 'discUsage']; + + dataSource: SystemInfoDataSource; + + pageLink: PageLink; + + constructor(protected store: Store, + private telemetryWsService: TelemetryWebsocketService, + private zone: NgZone) { + super(store); + } + + ngOnInit() { + this.dataSource = new SystemInfoDataSource(this.telemetryWsService, this.zone); + const sortOrder: SortOrder = { + property: 'serviceId', + direction: Direction.ASC + }; + this.pageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder); + this.dataSource.loadData(this.pageLink); + } + + ngAfterViewInit() { + this.sort.sortChange.subscribe(() => this.updateData()); + } + + updateData() { + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadData(this.pageLink); + } + + statusClass(value: number): string { + let status = ''; + if (value > 85) { + status = 'tb-status-critical'; + } else if (value > 75) { + status = 'tb-status-warn'; + } + return status; + } + + infoTooltip(serviceData: SystemInfoData, type: 'cpu' | 'ram' | 'disc'): string { + let tooltip = ''; + let value: number; + switch (type) { + case 'cpu': + value = serviceData.cpuUsage; + tooltip += value + ' / 100 %'; + break; + case 'ram': + value = serviceData.memoryUsage; + const memoryUsage = serviceData.memoryUsage; + const totalMemory = serviceData.totalMemory / 1073741824; + const usedMemory = (totalMemory * (memoryUsage / 100)).toFixed(0); + tooltip += usedMemory + ' / ' + totalMemory.toFixed(0) + ' Gb'; + break; + case 'disc': + value = serviceData.discUsage; + const discUsage = serviceData.discUsage; + const totalDiscSpace = serviceData.totalDiscSpace / 1073741824; + const usedDisc = (totalDiscSpace * (discUsage / 100)).toFixed(0); + tooltip += usedDisc + ' / ' + totalDiscSpace.toFixed(0) + ' Gb'; + break; + } + if (value > 85) { + switch (type) { + case 'cpu': + tooltip += '\n\nCritically high CPU usage. To avoid system failure, optimize system performance.'; + break; + case 'ram': + tooltip += '\n\nCritically low reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.'; + break; + case 'disc': + tooltip += '\n\nCritically low disk space. To avoid data loss, free up or expand the disk space.'; + break; + } + } else if (value > 75) { + switch (type) { + case 'cpu': + tooltip += '\n\nRunning high on CPU usage. To avoid system failure, optimize system performance.'; + break; + case 'ram': + tooltip += '\n\nRunning low on reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.'; + break; + case 'disc': + tooltip += '\n\nRunning low on disk space. To avoid data loss, free up or expand the disk space.'; + break; + } + } + return tooltip; + } +} + +export class SystemInfoDataSource implements DataSource { + + private systemDataSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + private allSystemData: Observable>; + + private telemetrySubscriber: TelemetrySubscriber; + + constructor(private telemetryWsService: TelemetryWebsocketService, + private zone: NgZone) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.systemDataSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.systemDataSubject.complete(); + this.pageDataSubject.complete(); + if (this.telemetrySubscriber) { + this.telemetrySubscriber.unsubscribe(); + this.telemetrySubscriber = null; + } + } + + loadData(pageLink: PageLink): void { + this.getAllSystemData().pipe( + map((data) => pageLink.filterData(data)), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.onSystemData(pageData.data); + this.pageDataSubject.next(pageData); + } + ); + } + + getAllSystemData(): Observable> { + if (!this.allSystemData) { + this.telemetrySubscriber = TelemetrySubscriber.createEntityFilterLatestSubscription( + this.telemetryWsService, { type: AliasFilterType.apiUsageState }, this.zone, + [ { key: 'clusterSystemData', type: EntityKeyType.TIME_SERIES } ]); + this.telemetrySubscriber.subscribe(); + this.allSystemData = this.telemetrySubscriber.entityData$.pipe( + map((update) => this.toSystemInfoData(update)), + publishReplay(1), + refCount() + ); + } + return this.allSystemData; + } + + private toSystemInfoData(entityDataUpdate: EntityDataUpdate): Array { + const entityData = entityDataUpdate.data?.data ? entityDataUpdate.data?.data : entityDataUpdate.update; + if (entityData && entityData.length) { + const latest = entityData[0].latest; + if (latest && latest[EntityKeyType.TIME_SERIES]) { + const latestTs = latest[EntityKeyType.TIME_SERIES]; + const tsValue = latestTs.clusterSystemData; + if (tsValue && tsValue.value) { + return JSON.parse(tsValue.value); + } + } + } + return []; + } + + protected onSystemData(data: SystemInfoData[]) { + this.systemDataSubject.next(data); + } + + isEmpty(): Observable { + return this.systemDataSubject.pipe( + map((entities) => !entities.length), + share() + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements), + share() + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts new file mode 100644 index 0000000000..0e14236720 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; +import { ClusterInfoTableComponent } from '@home/components/widget/lib/home-page/cluster-info-table.component'; + +@NgModule({ + declarations: + [ + ClusterInfoTableComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + ClusterInfoTableComponent + ] +}) +export class HomePageWidgetsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index 1c3df367c4..16fb6090e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -, private utils: UtilsService, @Inject(HOME_COMPONENTS_MODULE_TOKEN) public homeComponentsModule: Type, + @Inject(WIDGET_COMPONENTS_MODULE_TOKEN) public widgetComponentsModule: Type, private cd: ChangeDetectorRef) { super(store); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 6fd404a31e..61b94a37a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -42,6 +42,8 @@ import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component'; import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-widget.component'; import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; +import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module'; +import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; @NgModule({ declarations: @@ -70,6 +72,7 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di CommonModule, SharedModule, RpcWidgetsModule, + HomePageWidgetsModule, SharedHomeComponentsModule ], exports: [ @@ -79,6 +82,7 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di EntitiesHierarchyWidgetComponent, EdgesOverviewWidgetComponent, RpcWidgetsModule, + HomePageWidgetsModule, DateRangeNavigatorWidgetComponent, JsonInputWidgetComponent, MultipleInputWidgetComponent, @@ -92,7 +96,8 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di ], providers: [ CustomDialogService, - ImportExportService + ImportExportService, + {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } ] }) export class WidgetComponentsModule { diff --git a/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw b/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw index 9a34cdf51a..4aaa1d7d81 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw +++ b/ui-ngx/src/app/modules/home/pages/home-links/sys_admin_home_page.raw @@ -868,7 +868,13 @@ "label": "clusterMode", "color": "#2196f3", "settings": {}, - "_hash": 0.7272123990942316 + "_hash": 0.7272123990942316, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": false, + "postFuncBody": "" } ] } @@ -903,7 +909,7 @@ "padding": "0px", "settings": { "useMarkdownTextFunction": true, - "markdownTextFunction": "let clusterMode = data[0]['clusterMode'];\nlet targetState = 'monolith_system_info';\nif (clusterMode === 'true') {\n targetState = 'cluster_system_info';\n}\nreturn '
';\n", + "markdownTextFunction": "let html = '
';\nlet clusterMode = data[0]['clusterMode'];\nif (clusterMode === 'true') {\n html += '';\n} else {\n html += '';\n}\nhtml += '
';\nreturn html;", "applyDefaultMarkdownStyle": false }, "title": "Transport messages", @@ -915,7 +921,6 @@ "enableFullscreen": false, "widgetStyle": { "border": "none", - "borderRadius": "0", "boxShadow": "none" }, "titleStyle": { @@ -957,188 +962,42 @@ "color": "#2196f3", "settings": {}, "_hash": 0.659021918423117 - } - ] - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "16px", - "settings": { - "useMarkdownTextFunction": false, - "markdownTextPattern": "
\n
\n
CPU
\n
\n
${cpuUsage:0}%
\n
", - "applyDefaultMarkdownStyle": false, - "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" - }, - "title": "CPU", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": false, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-right: 6px;\n}", - "pageSize": 1024, - "noDataDisplayMessage": "" - }, - "row": 0, - "col": 0, - "id": "76607897-dfd0-8946-7451-3de6a39e4bbf" - }, - "8c2f02bc-ca69-2955-8730-4e945f77da28": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", - "type": "latest", - "title": "New widget", - "image": null, - "description": null, - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", - "filterId": null, - "dataKeys": [ + }, { - "name": "memoryUsage", + "name": "cpuCount", "type": "timeseries", - "label": "memoryUsage", - "color": "#2196f3", + "label": "cpuCount", + "color": "#4caf50", "settings": {}, - "_hash": 0.659021918423117, + "_hash": 0.5332753234169949 + }, + { + "name": "cpuUsage", + "type": "timeseries", + "label": "status", + "color": "#f44336", + "settings": {}, + "_hash": 0.025084892962804473, "aggregationType": "NONE", "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - } - ] - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1680168340431, - "endTimeMs": 1680254740431 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "16px", - "settings": { - "useMarkdownTextFunction": false, - "markdownTextPattern": "
\n
\n
RAM
\n
\n
${memoryUsage:0}%
\n
", - "applyDefaultMarkdownStyle": false, - "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" - }, - "title": "RAM", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": false, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-right: 6px;\n margin-left: 6px;\n}", - "pageSize": 1024, - "noDataDisplayMessage": "" - }, - "row": 0, - "col": 0, - "id": "8c2f02bc-ca69-2955-8730-4e945f77da28" - }, - "e3686199-5117-fa5a-d59c-aada6269653e": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", - "type": "latest", - "title": "New widget", - "image": null, - "description": null, - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", - "filterId": null, - "dataKeys": [ + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, { - "name": "discUsage", + "name": "cpuUsage", "type": "timeseries", - "label": "discUsage", - "color": "#2196f3", + "label": "statusTooltip", + "color": "#ffc107", "settings": {}, - "_hash": 0.659021918423117, + "_hash": 0.8542003006820891, "aggregationType": "NONE", "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = 'Critically high CPU usage. To avoid system failure, optimize system performance.';\n} else if (value > 75) {\n text = 'Running high on CPU usage. To avoid system failure, optimize system performance.';\n}\nreturn text;" } ] } @@ -1173,11 +1032,11 @@ "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "
\n
\n
Disk
\n
\n
${discUsage:0}%
\n
", + "markdownTextPattern": "
\n
\n
CPU
\n
\n
\n
\n ${cpuUsage:0}%\n | ${cpuCount:0} cores\n
\n
", "applyDefaultMarkdownStyle": false, - "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n" + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-start;\n}\n\n.tb-card-title {\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-status {\n width: 24px;\n height: 24px;\n position: relative;\n}\n\n.tb-status.status-ok {\n background: #F3F6FA;\n border-radius: 4px;\n}\n\n.tb-status:after {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n}\n\n.tb-status.status-ok:after {\n content: \"check\";\n color: #198038;\n font-weight: 600;\n}\n\n.tb-status.status-warn:after {\n content: \"warning\";\n color: #FAA405;\n}\n\n.tb-status.status-critical:after {\n content: \"warning\";\n color: #D12730;\n}\n\n.tb-value-container {\n user-select: none;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n.tb-total {\n padding-left: 4px;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.54);\n}\n" }, - "title": "Disk", + "title": "CPU", "showTitleIcon": false, "iconColor": "rgba(0, 0, 0, 0.87)", "iconSize": "24px", @@ -1191,13 +1050,13 @@ }, "showLegend": false, "useDashboardTimewindow": true, - "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-left: 6px;\n}", + "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-right: 6px;\n}", "pageSize": 1024, "noDataDisplayMessage": "" }, "row": 0, "col": 0, - "id": "e3686199-5117-fa5a-d59c-aada6269653e" + "id": "76607897-dfd0-8946-7451-3de6a39e4bbf" }, "eace9148-b02a-48fe-1a95-acd9928aa8c5": { "isSystemType": true, @@ -1380,7 +1239,7 @@ "tickDecimals": 0, "ticksFormatter": "" }, - "shadowSize": 4, + "shadowSize": 0, "smoothLines": true, "comparisonEnabled": false, "timeForComparison": "previousInterval", @@ -1424,84 +1283,447 @@ "row": 0, "col": 0, "id": "eace9148-b02a-48fe-1a95-acd9928aa8c5" - } - }, - "states": { - "default": { - "name": "System Administrator Home Page", - "root": true, - "layouts": { - "main": { - "widgets": { - "04a9afdf-fbb4-4eaf-0fcc-a190c6202c62": { - "sizeX": 10, - "sizeY": 4, - "row": 0, - "col": 0 - }, - "cf1b7a07-2a16-cab7-2528-ef5caba4ac38": { - "sizeX": 10, - "sizeY": 4, - "row": 0, - "col": 10 - }, - "551a27a0-cd23-a70c-0de2-c97ab6673faa": { - "sizeX": 5, - "sizeY": 3, - "row": 4, - "col": 0 - }, - "a41682b0-2f23-23e9-0807-e8b47c1c1795": { - "sizeX": 5, - "sizeY": 3, - "row": 4, - "col": 5 - }, - "1abf533c-aee7-080d-0274-d047e1d5dc63": { - "sizeX": 5, - "sizeY": 3, - "row": 4, - "col": 10 - }, - "5d02c63b-bcb1-aaf3-f35d-de4dbaca96c2": { - "sizeX": 5, - "sizeY": 3, - "row": 4, - "col": 15 - }, - "e70b51f8-04dd-985f-be81-d0c76c589d4a": { - "sizeX": 16, - "sizeY": 26, - "row": 0, - "col": 38 - }, - "d70cc256-4c7b-ee06-9905-b8c5e546605f": { - "sizeX": 18, - "sizeY": 8, - "row": 11, - "col": 20 - }, - "4b5e47ed-c197-a937-d727-041ba8decec2": { - "sizeX": 18, - "sizeY": 11, - "row": 0, - "col": 20 - } - }, - "gridSettings": { - "backgroundColor": "#eeeeee", - "columns": 54, - "margin": 12, - "backgroundSizeMode": "100%", - "autoFillHeight": true, - "backgroundImageUrl": null, - "mobileAutoFillHeight": false, - "mobileRowHeight": 70 - } - } - } }, - "transport_messages": { + "d755208a-81cf-edb4-5db8-f4859f5359cc": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "memoryUsage", + "type": "timeseries", + "label": "memoryUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "totalMemory", + "type": "timeseries", + "label": "totalMemory", + "color": "#4caf50", + "settings": {}, + "_hash": 0.5332753234169949, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value / 1073741824;" + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "status", + "color": "#f44336", + "settings": {}, + "_hash": 0.025084892962804473, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "memoryUsage", + "type": "timeseries", + "label": "statusTooltip", + "color": "#ffc107", + "settings": {}, + "_hash": 0.8542003006820891, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = 'Critically low reserve of RAM. To avoid system failure,
optimize system performance or increase the size of RAM.';\n} else if (value > 75) {\n text = 'Running low on reserve of RAM. To avoid system failure,
optimize system performance or increase the size of RAM.';\n}\nreturn text;" + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextFunction": "const memoryUsage = data[0]['memoryUsage'];\nconst totalMemory = data[0]['totalMemory'];\nconst usedMemory = (totalMemory * (memoryUsage / 100)).toFixed(0);\n\n\nreturn '
' +\n '
' +\n '
RAM
' +\n '
' +\n '
' +\n '
' +\n '${memoryUsage:0}%' +\n '| ${totalMemory:0} Gb' +\n '
' +\n '
';", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-start;\n}\n\n.tb-card-title {\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-status {\n width: 24px;\n height: 24px;\n position: relative;\n}\n\n.tb-status.status-ok {\n background: #F3F6FA;\n border-radius: 4px;\n}\n\n.tb-status:after {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n}\n\n.tb-status.status-ok:after {\n content: \"check\";\n color: #198038;\n font-weight: 600;\n}\n\n.tb-status.status-warn:after {\n content: \"warning\";\n color: #FAA405;\n}\n\n.tb-status.status-critical:after {\n content: \"warning\";\n color: #D12730;\n}\n\n.tb-value-container {\n user-select: none;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n.tb-total {\n padding-left: 4px;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.54);\n}\n" + }, + "title": "RAM", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-right: 6px;\n}", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "d755208a-81cf-edb4-5db8-f4859f5359cc" + }, + "6ab892b7-0cb8-cb1e-f5d3-679ef8f25bd7": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "d9229b29-3f46-de8d-7fe8-eb0c43c75079", + "filterId": null, + "dataKeys": [ + { + "name": "discUsage", + "type": "timeseries", + "label": "discUsage", + "color": "#2196f3", + "settings": {}, + "_hash": 0.659021918423117, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "totalDiscSpace", + "type": "timeseries", + "label": "totalDiscSpace", + "color": "#4caf50", + "settings": {}, + "_hash": 0.5332753234169949, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value / 1073741824;" + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "status", + "color": "#f44336", + "settings": {}, + "_hash": 0.025084892962804473, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var status = 'status-ok';\nif (value > 85) {\n status = 'status-critical';\n} else if (value > 75) {\n status = 'status-warn';\n}\nreturn status;" + }, + { + "name": "discUsage", + "type": "timeseries", + "label": "statusTooltip", + "color": "#ffc107", + "settings": {}, + "_hash": 0.8542003006820891, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "var text = '';\nif (value > 85) {\n text = 'Critically low disk space. To avoid data loss, free up or expand the disk space.';\n} else if (value > 75) {\n text = 'Running low on disk space. To avoid data loss, free up or expand the disk space.';\n}\nreturn text;" + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextFunction": "const discUsage = data[0]['discUsage'];\nconst totalDiscSpace = data[0]['totalDiscSpace'];\nconst usedDisc = (totalDiscSpace * (discUsage / 100)).toFixed(0);\n\n\nreturn '
' +\n '
' +\n '
Disk
' +\n '
' +\n '
' +\n '
' +\n '${discUsage:0}%' +\n '| ${totalDiscSpace:0} Gb' +\n '
' +\n '
';", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-start;\n}\n\n.tb-card-title {\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-status {\n width: 24px;\n height: 24px;\n position: relative;\n}\n\n.tb-status.status-ok {\n background: #F3F6FA;\n border-radius: 4px;\n}\n\n.tb-status:after {\n position: absolute;\n top: 2px;\n left: 2px;\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n}\n\n.tb-status.status-ok:after {\n content: \"check\";\n color: #198038;\n font-weight: 600;\n}\n\n.tb-status.status-warn:after {\n content: \"warning\";\n color: #FAA405;\n}\n\n.tb-status.status-critical:after {\n content: \"warning\";\n color: #D12730;\n}\n\n.tb-value-container {\n user-select: none;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 26px;\n line-height: 36px;\n}\n\n.tb-total {\n padding-left: 4px;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n color: rgba(0, 0, 0, 0.54);\n}\n" + }, + "title": "Disk", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": ".tb-widget-container {\n display: block;\n height: 100%;\n margin-right: 6px;\n}", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "6ab892b7-0cb8-cb1e-f5d3-679ef8f25bd7" + }, + "8acbf5df-f9fc-114d-216f-86f081aa4779": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "markdown_card", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": null, + "entityAliasId": "ae870700-071f-b3bc-406c-16ba554c5a55", + "filterId": null, + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "tenantProfilesCount", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8491768696709192, + "aggregationType": null, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1680168340431, + "endTimeMs": 1680254740431 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "useMarkdownTextFunction": false, + "markdownTextPattern": "
\n
\n
\n
Functions
\n
\n Switch to PE\n
\n
\n
\n
Alarms
\n
Dashboards
\n
Entities & Relations
\n
Profiles
\n
Advanced features
\n
\n
\n
Notification center
\n
API usage
\n
Customers
\n
Customers hierarchy
\n
Roles & Permissions
\n
\n
\n
Groups
\n
Integrations
\n
Solution templates
\n
Scheduler
\n
White-labeling
\n
\n
\n
", + "applyDefaultMarkdownStyle": false, + "markdownCss": ".tb-card-header {\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n margin-top: 18px;\n}\n\n.feature {\n position: relative;\n cursor: default;\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n.feature:before {\n content: \"check\";\n font-family: 'Material Icons Round';\n font-size: 20px;\n line-height: 1;\n color: #198038;\n font-weight: 600;\n position: relative;\n background: #F3F6FA;\n border-radius: 4px;\n width: 24px;\n height: 24px;\n margin-right: 8px;\n vertical-align: middle;\n}\n\n.feature.pe:before {\n content: \"close\";\n background: #F4F4F4;\n color: rgba(0, 0, 0, 0.12);\n}\n\n.feature.pe:after {\n position: relative;\n display: inline-block;\n margin-left: 4px;\n vertical-align: baseline;\n top: -0.5em;\n content: \"PE\";\n color: #305680;\n font-weight: 700;\n font-size: 12px;\n line-height: 12px;\n letter-spacing: 0.0015em;\n}\n\n.feature+.feature {\n margin-top: 10px;\n}\n" + }, + "title": "Functions", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "8acbf5df-f9fc-114d-216f-86f081aa4779" + } + }, + "states": { + "default": { + "name": "System Administrator Home Page", + "root": true, + "layouts": { + "main": { + "widgets": { + "04a9afdf-fbb4-4eaf-0fcc-a190c6202c62": { + "sizeX": 10, + "sizeY": 4, + "row": 0, + "col": 0 + }, + "cf1b7a07-2a16-cab7-2528-ef5caba4ac38": { + "sizeX": 10, + "sizeY": 4, + "row": 0, + "col": 10 + }, + "551a27a0-cd23-a70c-0de2-c97ab6673faa": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 0 + }, + "a41682b0-2f23-23e9-0807-e8b47c1c1795": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 5 + }, + "1abf533c-aee7-080d-0274-d047e1d5dc63": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 10 + }, + "5d02c63b-bcb1-aaf3-f35d-de4dbaca96c2": { + "sizeX": 5, + "sizeY": 3, + "row": 4, + "col": 15 + }, + "e70b51f8-04dd-985f-be81-d0c76c589d4a": { + "sizeX": 16, + "sizeY": 26, + "row": 0, + "col": 38 + }, + "d70cc256-4c7b-ee06-9905-b8c5e546605f": { + "sizeX": 18, + "sizeY": 8, + "row": 11, + "col": 20 + }, + "4b5e47ed-c197-a937-d727-041ba8decec2": { + "sizeX": 18, + "sizeY": 11, + "row": 0, + "col": 20 + }, + "8acbf5df-f9fc-114d-216f-86f081aa4779": { + "sizeX": 27, + "sizeY": 7, + "row": 19, + "col": 11 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 54, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + }, + "transport_messages": { "name": "Transport messages", "root": false, "layouts": { @@ -1539,23 +1761,23 @@ "row": 0, "col": 0 }, - "8c2f02bc-ca69-2955-8730-4e945f77da28": { + "eace9148-b02a-48fe-1a95-acd9928aa8c5": { + "sizeX": 24, + "sizeY": 8, + "row": 3, + "col": 0 + }, + "d755208a-81cf-edb4-5db8-f4859f5359cc": { "sizeX": 8, "sizeY": 3, "row": 0, "col": 8 }, - "e3686199-5117-fa5a-d59c-aada6269653e": { + "6ab892b7-0cb8-cb1e-f5d3-679ef8f25bd7": { "sizeX": 8, "sizeY": 3, "row": 0, "col": 16 - }, - "eace9148-b02a-48fe-1a95-acd9928aa8c5": { - "sizeX": 24, - "sizeY": 8, - "row": 3, - "col": 0 } }, "gridSettings": { @@ -1570,21 +1792,6 @@ } } } - }, - "cluster_system_info": { - "name": "Cluster System Info", - "root": false, - "layouts": { - "main": { - "widgets": {}, - "gridSettings": { - "backgroundColor": "#eeeeee", - "columns": 24, - "margin": 10, - "backgroundSizeMode": "100%" - } - } - } } }, "entityAliases": { diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 44af6afc30..17a7031099 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -25,7 +25,7 @@ import { AlarmData, AlarmDataQuery, EntityCountQuery, EntityData, - EntityDataQuery, + EntityDataQuery, EntityFilter, EntityKey, TsValue } from '@shared/models/query/query.models'; @@ -599,6 +599,27 @@ export class TelemetrySubscriber extends WsSubscriber { return subscriber; } + public static createEntityFilterLatestSubscription(telemetryService: TelemetryWebsocketService, + entityFilter: EntityFilter, zone: NgZone, + latestKeys: EntityKey[] = null): TelemetrySubscriber { + const entityDataQuery: EntityDataQuery = { + entityFilter, + pageLink: { + pageSize: 1, + page: 0 + }, + latestValues: latestKeys || [] + }; + const cmd = new EntityDataCmd(); + cmd.query = entityDataQuery; + cmd.latestCmd = { + keys: latestKeys || [] + }; + const subscriber = new TelemetrySubscriber(telemetryService, zone); + subscriber.subscriptionCommands.push(cmd); + return subscriber; + } + constructor(private telemetryService: TelemetryWebsocketService, protected zone?: NgZone) { super(telemetryService, zone); } diff --git a/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf b/ui-ngx/src/assets/fonts/MaterialIconsRound-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..dacf094b51c5037c3ecdfd2f0dbabc073a38d652 GIT binary patch literal 400092 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ï(1( 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 c@Q?lexHApGPf{6*EmmJF@g4qO|NE=|0wn(7 +