committed by
GitHub
2043 changed files with 102994 additions and 30865 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,140 @@ |
|||
-- |
|||
-- 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. |
|||
-- |
|||
|
|||
ALTER TABLE device |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE device_profile |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE asset |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE rule_chain |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE rule_node |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE dashboard |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE customer |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE widgets_bundle |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
ALTER TABLE entity_view |
|||
ADD COLUMN IF NOT EXISTS external_id UUID; |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id); |
|||
CREATE INDEX IF NOT EXISTS idx_rule_node_type ON rule_node(type); |
|||
|
|||
ALTER TABLE admin_settings |
|||
ADD COLUMN IF NOT EXISTS tenant_id uuid NOT NULL DEFAULT '13814000-1dd2-11b2-8080-808080808080'; |
|||
|
|||
CREATE TABLE IF NOT EXISTS queue ( |
|||
id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY, |
|||
created_time bigint NOT NULL, |
|||
tenant_id uuid, |
|||
name varchar(255), |
|||
topic varchar(255), |
|||
poll_interval int, |
|||
partitions int, |
|||
consumer_per_partition boolean, |
|||
pack_processing_timeout bigint, |
|||
submit_strategy varchar(255), |
|||
processing_strategy varchar(255), |
|||
additional_info varchar |
|||
); |
|||
|
|||
CREATE TABLE IF NOT EXISTS user_auth_settings ( |
|||
id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY, |
|||
created_time bigint NOT NULL, |
|||
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id), |
|||
two_fa_settings varchar |
|||
); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); |
|||
|
|||
ALTER TABLE tenant_profile DROP COLUMN IF EXISTS isolated_tb_core; |
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'device_external_id_unq_key') THEN |
|||
ALTER TABLE device ADD CONSTRAINT device_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'device_profile_external_id_unq_key') THEN |
|||
ALTER TABLE device_profile ADD CONSTRAINT device_profile_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'asset_external_id_unq_key') THEN |
|||
ALTER TABLE asset ADD CONSTRAINT asset_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'rule_chain_external_id_unq_key') THEN |
|||
ALTER TABLE rule_chain ADD CONSTRAINT rule_chain_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'dashboard_external_id_unq_key') THEN |
|||
ALTER TABLE dashboard ADD CONSTRAINT dashboard_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'customer_external_id_unq_key') THEN |
|||
ALTER TABLE customer ADD CONSTRAINT customer_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'widgets_bundle_external_id_unq_key') THEN |
|||
ALTER TABLE widgets_bundle ADD CONSTRAINT widgets_bundle_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
DO |
|||
$$ |
|||
BEGIN |
|||
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'entity_view_external_id_unq_key') THEN |
|||
ALTER TABLE entity_view ADD CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id); |
|||
END IF; |
|||
END; |
|||
$$; |
|||
|
|||
@ -0,0 +1,234 @@ |
|||
-- |
|||
-- 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. |
|||
-- |
|||
|
|||
CREATE TABLE IF NOT EXISTS rule_node_debug_event ( |
|||
id uuid NOT NULL, |
|||
tenant_id uuid NOT NULL , |
|||
ts bigint NOT NULL, |
|||
entity_id uuid NOT NULL, |
|||
service_id varchar, |
|||
e_type varchar, |
|||
e_entity_id uuid, |
|||
e_entity_type varchar, |
|||
e_msg_id uuid, |
|||
e_msg_type varchar, |
|||
e_data_type varchar, |
|||
e_relation_type varchar, |
|||
e_data varchar, |
|||
e_metadata varchar, |
|||
e_error varchar |
|||
) PARTITION BY RANGE (ts); |
|||
|
|||
CREATE TABLE IF NOT EXISTS rule_chain_debug_event ( |
|||
id uuid NOT NULL, |
|||
tenant_id uuid NOT NULL, |
|||
ts bigint NOT NULL, |
|||
entity_id uuid NOT NULL, |
|||
service_id varchar NOT NULL, |
|||
e_message varchar, |
|||
e_error varchar |
|||
) PARTITION BY RANGE (ts); |
|||
|
|||
CREATE TABLE IF NOT EXISTS stats_event ( |
|||
id uuid NOT NULL, |
|||
tenant_id uuid NOT NULL, |
|||
ts bigint NOT NULL, |
|||
entity_id uuid NOT NULL, |
|||
service_id varchar NOT NULL, |
|||
e_messages_processed bigint NOT NULL, |
|||
e_errors_occurred bigint NOT NULL |
|||
) PARTITION BY RANGE (ts); |
|||
|
|||
CREATE TABLE IF NOT EXISTS lc_event ( |
|||
id uuid NOT NULL, |
|||
tenant_id uuid NOT NULL, |
|||
ts bigint NOT NULL, |
|||
entity_id uuid NOT NULL, |
|||
service_id varchar NOT NULL, |
|||
e_type varchar NOT NULL, |
|||
e_success boolean NOT NULL, |
|||
e_error varchar |
|||
) PARTITION BY RANGE (ts); |
|||
|
|||
CREATE TABLE IF NOT EXISTS error_event ( |
|||
id uuid NOT NULL, |
|||
tenant_id uuid NOT NULL, |
|||
ts bigint NOT NULL, |
|||
entity_id uuid NOT NULL, |
|||
service_id varchar NOT NULL, |
|||
e_method varchar NOT NULL, |
|||
e_error varchar |
|||
) PARTITION BY RANGE (ts); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_rule_node_debug_event_main |
|||
ON rule_node_debug_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_rule_chain_debug_event_main |
|||
ON rule_chain_debug_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_stats_event_main |
|||
ON stats_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_lc_event_main |
|||
ON lc_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_error_event_main |
|||
ON error_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); |
|||
|
|||
CREATE OR REPLACE FUNCTION to_safe_json(p_json text) RETURNS json |
|||
LANGUAGE plpgsql AS |
|||
$$ |
|||
BEGIN |
|||
return REPLACE(p_json, '\u0000', '' )::json; |
|||
EXCEPTION |
|||
WHEN OTHERS THEN |
|||
return '{}'::json; |
|||
END; |
|||
$$; |
|||
|
|||
-- Useful to migrate old events to the new table structure; |
|||
CREATE OR REPLACE PROCEDURE migrate_regular_events(IN start_ts_in_ms bigint, IN end_ts_in_ms bigint, IN partition_size_in_hours int) |
|||
LANGUAGE plpgsql AS |
|||
$$ |
|||
DECLARE |
|||
partition_size_in_ms bigint; |
|||
p record; |
|||
table_name varchar; |
|||
BEGIN |
|||
partition_size_in_ms = partition_size_in_hours * 3600 * 1000; |
|||
|
|||
FOR p IN SELECT DISTINCT event_type as event_type, (created_time - created_time % partition_size_in_ms) as partition_ts FROM event e WHERE e.event_type in ('STATS', 'LC_EVENT', 'ERROR') and ts >= start_ts_in_ms and ts < end_ts_in_ms |
|||
LOOP |
|||
IF p.event_type = 'STATS' THEN |
|||
table_name := 'stats_event'; |
|||
ELSEIF p.event_type = 'LC_EVENT' THEN |
|||
table_name := 'lc_event'; |
|||
ELSEIF p.event_type = 'ERROR' THEN |
|||
table_name := 'error_event'; |
|||
END IF; |
|||
RAISE NOTICE '[%] Partition to create : [%-%]', table_name, p.partition_ts, (p.partition_ts + partition_size_in_ms); |
|||
EXECUTE format('CREATE TABLE IF NOT EXISTS %s_%s PARTITION OF %s FOR VALUES FROM ( %s ) TO ( %s )', table_name, p.partition_ts, table_name, p.partition_ts, (p.partition_ts + partition_size_in_ms)); |
|||
END LOOP; |
|||
|
|||
INSERT INTO stats_event |
|||
SELECT id, |
|||
tenant_id, |
|||
ts, |
|||
entity_id, |
|||
body ->> 'server', |
|||
(body ->> 'messagesProcessed')::bigint, |
|||
(body ->> 'errorsOccurred')::bigint |
|||
FROM |
|||
(select id, tenant_id, ts, entity_id, to_safe_json(body) as body |
|||
FROM event WHERE ts >= start_ts_in_ms and ts < end_ts_in_ms AND event_type = 'STATS' AND to_safe_json(body) ->> 'server' IS NOT NULL |
|||
) safe_event |
|||
ON CONFLICT DO NOTHING; |
|||
|
|||
INSERT INTO lc_event |
|||
SELECT id, |
|||
tenant_id, |
|||
ts, |
|||
entity_id, |
|||
body ->> 'server', |
|||
body ->> 'event', |
|||
(body ->> 'success')::boolean, |
|||
body ->> 'error' |
|||
FROM |
|||
(select id, tenant_id, ts, entity_id, to_safe_json(body) as body |
|||
FROM event WHERE ts >= start_ts_in_ms and ts < end_ts_in_ms AND event_type = 'LC_EVENT' AND to_safe_json(body) ->> 'server' IS NOT NULL |
|||
) safe_event |
|||
ON CONFLICT DO NOTHING; |
|||
|
|||
INSERT INTO error_event |
|||
SELECT id, |
|||
tenant_id, |
|||
ts, |
|||
entity_id, |
|||
body ->> 'server', |
|||
body ->> 'method', |
|||
body ->> 'error' |
|||
FROM |
|||
(select id, tenant_id, ts, entity_id, to_safe_json(body) as body |
|||
FROM event WHERE ts >= start_ts_in_ms and ts < end_ts_in_ms AND event_type = 'ERROR' AND to_safe_json(body) ->> 'server' IS NOT NULL |
|||
) safe_event |
|||
ON CONFLICT DO NOTHING; |
|||
|
|||
END |
|||
$$; |
|||
|
|||
-- Useful to migrate old debug events to the new table structure; |
|||
CREATE OR REPLACE PROCEDURE migrate_debug_events(IN start_ts_in_ms bigint, IN end_ts_in_ms bigint, IN partition_size_in_hours int) |
|||
LANGUAGE plpgsql AS |
|||
$$ |
|||
DECLARE |
|||
partition_size_in_ms bigint; |
|||
p record; |
|||
table_name varchar; |
|||
BEGIN |
|||
partition_size_in_ms = partition_size_in_hours * 3600 * 1000; |
|||
|
|||
FOR p IN SELECT DISTINCT event_type as event_type, (created_time - created_time % partition_size_in_ms) as partition_ts FROM event e WHERE e.event_type in ('DEBUG_RULE_NODE', 'DEBUG_RULE_CHAIN') and ts >= start_ts_in_ms and ts < end_ts_in_ms |
|||
LOOP |
|||
IF p.event_type = 'DEBUG_RULE_NODE' THEN |
|||
table_name := 'rule_node_debug_event'; |
|||
ELSEIF p.event_type = 'DEBUG_RULE_CHAIN' THEN |
|||
table_name := 'rule_chain_debug_event'; |
|||
END IF; |
|||
RAISE NOTICE '[%] Partition to create : [%-%]', table_name, p.partition_ts, (p.partition_ts + partition_size_in_ms); |
|||
EXECUTE format('CREATE TABLE IF NOT EXISTS %s_%s PARTITION OF %s FOR VALUES FROM ( %s ) TO ( %s )', table_name, p.partition_ts, table_name, p.partition_ts, (p.partition_ts + partition_size_in_ms)); |
|||
END LOOP; |
|||
|
|||
INSERT INTO rule_node_debug_event |
|||
SELECT id, |
|||
tenant_id, |
|||
ts, |
|||
entity_id, |
|||
body ->> 'server', |
|||
body ->> 'type', |
|||
(body ->> 'entityId')::uuid, |
|||
body ->> 'entityName', |
|||
(body ->> 'msgId')::uuid, |
|||
body ->> 'msgType', |
|||
body ->> 'dataType', |
|||
body ->> 'relationType', |
|||
body ->> 'data', |
|||
body ->> 'metadata', |
|||
body ->> 'error' |
|||
FROM |
|||
(select id, tenant_id, ts, entity_id, to_safe_json(body) as body |
|||
FROM event WHERE ts >= start_ts_in_ms and ts < end_ts_in_ms AND event_type = 'DEBUG_RULE_NODE' AND to_safe_json(body) ->> 'server' IS NOT NULL |
|||
) safe_event |
|||
ON CONFLICT DO NOTHING; |
|||
|
|||
INSERT INTO rule_chain_debug_event |
|||
SELECT id, |
|||
tenant_id, |
|||
ts, |
|||
entity_id, |
|||
body ->> 'server', |
|||
body ->> 'message', |
|||
body ->> 'error' |
|||
FROM |
|||
(select id, tenant_id, ts, entity_id, to_safe_json(body) as body |
|||
FROM event WHERE ts >= start_ts_in_ms and ts < end_ts_in_ms AND event_type = 'DEBUG_RULE_CHAIN' AND to_safe_json(body) ->> 'server' IS NOT NULL |
|||
) safe_event |
|||
ON CONFLICT DO NOTHING; |
|||
END |
|||
$$; |
|||
|
|||
UPDATE tb_user |
|||
SET additional_info = REPLACE(additional_info, '"lang":"ja_JA"', '"lang":"ja_JP"') |
|||
WHERE additional_info LIKE '%"lang":"ja_JA"%'; |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* 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.controller; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
public class AutoCommitController extends BaseController { |
|||
|
|||
@Autowired |
|||
private EntitiesVersionControlService vcService; |
|||
|
|||
protected ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception { |
|||
if (vcService != null) { |
|||
return vcService.autoCommit(user, entityId); |
|||
} else { |
|||
// We do not support auto-commit for rule engine
|
|||
return Futures.immediateFailedFuture(new RuntimeException("Operation not supported!")); |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,519 @@ |
|||
/** |
|||
* 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.controller; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.MoreExecutors; |
|||
import io.swagger.annotations.ApiOperation; |
|||
import io.swagger.annotations.ApiParam; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.springframework.web.context.request.async.DeferredResult; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.EntityIdFactory; |
|||
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.data.sync.vc.BranchInfo; |
|||
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; |
|||
import org.thingsboard.server.common.data.sync.vc.EntityDataInfo; |
|||
import org.thingsboard.server.common.data.sync.vc.EntityVersion; |
|||
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; |
|||
import org.thingsboard.server.common.data.sync.vc.VersionLoadResult; |
|||
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; |
|||
import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest; |
|||
import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.permission.Operation; |
|||
import org.thingsboard.server.service.security.permission.Resource; |
|||
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.BRANCH_PARAM_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; |
|||
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; |
|||
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; |
|||
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; |
|||
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; |
|||
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; |
|||
import static org.thingsboard.server.controller.ControllerConstants.VC_REQUEST_ID_PARAM_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.VERSION_ID_PARAM_DESCRIPTION; |
|||
|
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequestMapping("/api/entities/vc") |
|||
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
|||
@RequiredArgsConstructor |
|||
public class EntitiesVersionControlController extends BaseController { |
|||
|
|||
private final EntitiesVersionControlService versionControlService; |
|||
|
|||
|
|||
@ApiOperation(value = "Save entities version (saveEntitiesVersion)", notes = "" + |
|||
"Creates a new version of entities (or a single entity) by request.\n" + |
|||
"Supported entity types: CUSTOMER, ASSET, RULE_CHAIN, DASHBOARD, DEVICE_PROFILE, DEVICE, ENTITY_VIEW, WIDGETS_BUNDLE." + NEW_LINE + |
|||
"There are two available types of request: `SINGLE_ENTITY` and `COMPLEX`. " + |
|||
"Each of them contains version name (`versionName`) and name of a branch (`branch`) to create version (commit) in. " + |
|||
"If specified branch does not exists in a remote repo, then new empty branch will be created. " + |
|||
"Request of the `SINGLE_ENTITY` type has id of an entity (`entityId`) and additional configuration (`config`) " + |
|||
"which has following options: \n" + |
|||
"- `saveRelations` - whether to add inbound and outbound relations of type COMMON to created entity version;\n" + |
|||
"- `saveAttributes` - to save attributes of server scope (and also shared scope for devices);\n" + |
|||
"- `saveCredentials` - when saving a version of a device, to add its credentials to the version." + NEW_LINE + |
|||
"An example of a `SINGLE_ENTITY` version create request:\n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"{\n" + |
|||
" \"type\": \"SINGLE_ENTITY\",\n" + |
|||
"\n" + |
|||
" \"versionName\": \"Version 1.0\",\n" + |
|||
" \"branch\": \"dev\",\n" + |
|||
"\n" + |
|||
" \"entityId\": {\n" + |
|||
" \"entityType\": \"DEVICE\",\n" + |
|||
" \"id\": \"b79448e0-d4f4-11ec-847b-0f432358ab48\"\n" + |
|||
" },\n" + |
|||
" \"config\": {\n" + |
|||
" \"saveRelations\": true,\n" + |
|||
" \"saveAttributes\": true,\n" + |
|||
" \"saveCredentials\": false\n" + |
|||
" }\n" + |
|||
"}" + |
|||
MARKDOWN_CODE_BLOCK_END + NEW_LINE + |
|||
"Second request type (`COMPLEX`), additionally to `branch` and `versionName`, contains following properties:\n" + |
|||
"- `entityTypes` - a structure with entity types to export and configuration for each entity type; " + |
|||
" this configuration has all the options available for `SINGLE_ENTITY` and additionally has these ones: \n" + |
|||
" - `allEntities` and `entityIds` - if you want to save the version of all entities of the entity type " + |
|||
" then set `allEntities` param to true, otherwise set it to false and specify the list of specific entities (`entityIds`);\n" + |
|||
" - `syncStrategy` - synchronization strategy to use for this entity type: when set to `OVERWRITE` " + |
|||
" then the list of remote entities of this type will be overwritten by newly added entities. If set to " + |
|||
" `MERGE` - existing remote entities of this entity type will not be removed, new entities will just " + |
|||
" be added on top (or existing remote entities will be updated).\n" + |
|||
"- `syncStrategy` - default synchronization strategy to use when it is not specified for an entity type." + NEW_LINE + |
|||
"Example for this type of request:\n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"{\n" + |
|||
" \"type\": \"COMPLEX\",\n" + |
|||
"\n" + |
|||
" \"versionName\": \"Devices and profiles: release 2\",\n" + |
|||
" \"branch\": \"master\",\n" + |
|||
"\n" + |
|||
" \"syncStrategy\": \"OVERWRITE\",\n" + |
|||
" \"entityTypes\": {\n" + |
|||
" \"DEVICE\": {\n" + |
|||
" \"syncStrategy\": null,\n" + |
|||
" \"allEntities\": true,\n" + |
|||
" \"saveRelations\": true,\n" + |
|||
" \"saveAttributes\": true,\n" + |
|||
" \"saveCredentials\": true\n" + |
|||
" },\n" + |
|||
" \"DEVICE_PROFILE\": {\n" + |
|||
" \"syncStrategy\": \"MERGE\",\n" + |
|||
" \"allEntities\": false,\n" + |
|||
" \"entityIds\": [\n" + |
|||
" \"b79448e0-d4f4-11ec-847b-0f432358ab48\"\n" + |
|||
" ],\n" + |
|||
" \"saveRelations\": true\n" + |
|||
" }\n" + |
|||
" }\n" + |
|||
"}" + |
|||
MARKDOWN_CODE_BLOCK_END + NEW_LINE + |
|||
"Response wil contain generated request UUID, that can be then used to retrieve " + |
|||
"status of operation via `getVersionCreateRequestStatus`.\n" + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@PostMapping("/version") |
|||
public DeferredResult<UUID> saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws Exception { |
|||
SecurityUser user = getCurrentUser(); |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE); |
|||
return wrapFuture(versionControlService.saveEntitiesVersion(user, request)); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get version create request status (getVersionCreateRequestStatus)", notes = "" + |
|||
"Returns the status of previously made version create request. " + NEW_LINE + |
|||
"This status contains following properties:\n" + |
|||
"- `done` - whether request processing is finished;\n" + |
|||
"- `version` - created version info: timestamp, version id (commit hash), commit name and commit author;\n" + |
|||
"- `added` - count of items that were created in the remote repo;\n" + |
|||
"- `modified` - modified items count;\n" + |
|||
"- `removed` - removed items count;\n" + |
|||
"- `error` - error message, if an error occurred while handling the request." + NEW_LINE + |
|||
"An example of successful status:\n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"{\n" + |
|||
" \"done\": true,\n" + |
|||
" \"added\": 10,\n" + |
|||
" \"modified\": 2,\n" + |
|||
" \"removed\": 5,\n" + |
|||
" \"version\": {\n" + |
|||
" \"timestamp\": 1655198528000,\n" + |
|||
" \"id\":\"8a834dd389ed80e0759ba8ee338b3f1fd160a114\",\n" + |
|||
" \"name\": \"My devices v2.0\",\n" + |
|||
" \"author\": \"John Doe\"\n" + |
|||
" },\n" + |
|||
" \"error\": null\n" + |
|||
"}" + |
|||
MARKDOWN_CODE_BLOCK_END + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping(value = "/version/{requestId}/status") |
|||
public VersionCreationResult getVersionCreateRequestStatus(@ApiParam(value = VC_REQUEST_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable UUID requestId) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE); |
|||
return versionControlService.getVersionCreateStatus(getCurrentUser(), requestId); |
|||
} |
|||
|
|||
@ApiOperation(value = "List entity versions (listEntityVersions)", notes = "" + |
|||
"Returns list of versions for a specific entity in a concrete branch. \n" + |
|||
"You need to specify external id of an entity to list versions for. This is `externalId` property of an entity, " + |
|||
"or otherwise if not set - simply id of this entity. \n" + |
|||
"If specified branch does not exist - empty page data will be returned. " + NEW_LINE + |
|||
"Each version info item has timestamp, id, name and author. Version id can then be used to restore the version. " + |
|||
PAGE_DATA_PARAMETERS + NEW_LINE + |
|||
"Response example: \n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"{\n" + |
|||
" \"data\": [\n" + |
|||
" {\n" + |
|||
" \"timestamp\": 1655198593000,\n" + |
|||
" \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" + |
|||
" \"name\": \"Devices and assets - v2.0\",\n" + |
|||
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" + |
|||
" },\n" + |
|||
" {\n" + |
|||
" \"timestamp\": 1655198528000,\n" + |
|||
" \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" + |
|||
" \"name\": \"Update my device\",\n" + |
|||
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" + |
|||
" },\n" + |
|||
" {\n" + |
|||
" \"timestamp\": 1655198280000,\n" + |
|||
" \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" + |
|||
" \"name\": \"Devices and assets - v1.0\",\n" + |
|||
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" + |
|||
" }\n" + |
|||
" ],\n" + |
|||
" \"totalPages\": 1,\n" + |
|||
" \"totalElements\": 3,\n" + |
|||
" \"hasNext\": false\n" + |
|||
"}" + |
|||
MARKDOWN_CODE_BLOCK_END + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping(value = "/version/{entityType}/{externalEntityUuid}", params = {"branch", "pageSize", "page"}) |
|||
public DeferredResult<PageData<EntityVersion>> listEntityVersions(@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable EntityType entityType, |
|||
@ApiParam(value = "A string value representing external entity id. This is `externalId` property of an entity, or otherwise if not set - simply id of this entity.") |
|||
@PathVariable UUID externalEntityUuid, |
|||
@ApiParam(value = BRANCH_PARAM_DESCRIPTION) |
|||
@RequestParam String branch, |
|||
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) |
|||
@RequestParam int pageSize, |
|||
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) |
|||
@RequestParam int page, |
|||
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION) |
|||
@RequestParam(required = false) String textSearch, |
|||
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") |
|||
@RequestParam(required = false) String sortProperty, |
|||
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) |
|||
@RequestParam(required = false) String sortOrder) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); |
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink)); |
|||
} |
|||
|
|||
@ApiOperation(value = "List entity type versions (listEntityTypeVersions)", notes = "" + |
|||
"Returns list of versions of an entity type in a branch. This is a collected list of versions that were created " + |
|||
"for entities of this type in a remote branch. \n" + |
|||
"If specified branch does not exist - empty page data will be returned. " + |
|||
"The response structure is the same as for `listEntityVersions` API method." + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping(value = "/version/{entityType}", params = {"branch", "pageSize", "page"}) |
|||
public DeferredResult<PageData<EntityVersion>> listEntityTypeVersions(@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable EntityType entityType, |
|||
@ApiParam(value = BRANCH_PARAM_DESCRIPTION, required = true) |
|||
@RequestParam String branch, |
|||
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) |
|||
@RequestParam int pageSize, |
|||
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) |
|||
@RequestParam int page, |
|||
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION) |
|||
@RequestParam(required = false) String textSearch, |
|||
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") |
|||
@RequestParam(required = false) String sortProperty, |
|||
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) |
|||
@RequestParam(required = false) String sortOrder) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink)); |
|||
} |
|||
|
|||
@ApiOperation(value = "List all versions (listVersions)", notes = "" + |
|||
"Lists all available versions in a branch for all entity types. \n" + |
|||
"If specified branch does not exist - empty page data will be returned. " + |
|||
"The response format is the same as for `listEntityVersions` API method." + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping(value = "/version", params = {"branch", "pageSize", "page"}) |
|||
public DeferredResult<PageData<EntityVersion>> listVersions(@ApiParam(value = BRANCH_PARAM_DESCRIPTION, required = true) |
|||
@RequestParam String branch, |
|||
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) |
|||
@RequestParam int pageSize, |
|||
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) |
|||
@RequestParam int page, |
|||
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION) |
|||
@RequestParam(required = false) String textSearch, |
|||
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") |
|||
@RequestParam(required = false) String sortProperty, |
|||
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) |
|||
@RequestParam(required = false) String sortOrder) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink)); |
|||
} |
|||
|
|||
|
|||
@ApiOperation(value = "List entities at version (listEntitiesAtVersion)", notes = "" + |
|||
"Returns a list of remote entities of a specific entity type that are available at a concrete version. \n" + |
|||
"Each entity item in the result has `externalId` property. " + |
|||
"Entities order will be the same as in the repository." + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping(value = "/entity/{entityType}/{versionId}") |
|||
public DeferredResult<List<VersionedEntityInfo>> listEntitiesAtVersion(@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable EntityType entityType, |
|||
@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable String versionId) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), versionId, entityType)); |
|||
} |
|||
|
|||
@ApiOperation(value = "List all entities at version (listAllEntitiesAtVersion)", notes = "" + |
|||
"Returns a list of all remote entities available in a specific version. " + |
|||
"Response type is the same as for listAllEntitiesAtVersion API method. \n" + |
|||
"Returned entities order will be the same as in the repository." + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping(value = "/entity/{versionId}") |
|||
public DeferredResult<List<VersionedEntityInfo>> listAllEntitiesAtVersion(@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable String versionId) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), versionId)); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get entity data info (getEntityDataInfo)", notes = "" + |
|||
"Retrieves short info about the remote entity by external id at a concrete version. \n" + |
|||
"Returned entity data info contains following properties: " + |
|||
"`hasRelations` (whether stored entity data contains relations), `hasAttributes` (contains attributes) and " + |
|||
"`hasCredentials` (whether stored device data has credentials)." + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping("/info/{versionId}/{entityType}/{externalEntityUuid}") |
|||
public DeferredResult<EntityDataInfo> getEntityDataInfo(@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable String versionId, |
|||
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable EntityType entityType, |
|||
@ApiParam(value = "A string value representing external entity id", required = true) |
|||
@PathVariable UUID externalEntityUuid) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); |
|||
return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId)); |
|||
} |
|||
|
|||
@ApiOperation(value = "Compare entity data to version (compareEntityDataToVersion)", notes = "" + |
|||
"Returns an object with current entity data and the one at a specific version. " + |
|||
"Entity data structure is the same as stored in a repository. " + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping(value = "/diff/{entityType}/{internalEntityUuid}", params = {"versionId"}) |
|||
public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable EntityType entityType, |
|||
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable UUID internalEntityUuid, |
|||
@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true) |
|||
@RequestParam String versionId) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid); |
|||
return wrapFuture(versionControlService.compareEntityDataToVersion(getCurrentUser(), entityId, versionId)); |
|||
} |
|||
|
|||
@ApiOperation(value = "Load entities version (loadEntitiesVersion)", notes = "" + |
|||
"Loads specific version of remote entities (or single entity) by request. " + |
|||
"Supported entity types: CUSTOMER, ASSET, RULE_CHAIN, DASHBOARD, DEVICE_PROFILE, DEVICE, ENTITY_VIEW, WIDGETS_BUNDLE." + NEW_LINE + |
|||
"There are multiple types of request. Each of them requires branch name (`branch`) and version id (`versionId`). " + |
|||
"Request of type `SINGLE_ENTITY` is needed to restore a concrete version of a specific entity. It contains " + |
|||
"id of a remote entity (`externalEntityId`) and additional configuration (`config`):\n" + |
|||
"- `loadRelations` - to update relations list (in case `saveRelations` option was enabled during version creation);\n" + |
|||
"- `loadAttributes` - to load entity attributes (if `saveAttributes` config option was enabled);\n" + |
|||
"- `loadCredentials` - to update device credentials (if `saveCredentials` option was enabled during version creation)." + NEW_LINE + |
|||
"An example of such request:\n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"{\n" + |
|||
" \"type\": \"SINGLE_ENTITY\",\n" + |
|||
" \n" + |
|||
" \"branch\": \"dev\",\n" + |
|||
" \"versionId\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" + |
|||
" \n" + |
|||
" \"externalEntityId\": {\n" + |
|||
" \"entityType\": \"DEVICE\",\n" + |
|||
" \"id\": \"b7944123-d4f4-11ec-847b-0f432358ab48\"\n" + |
|||
" },\n" + |
|||
" \"config\": {\n" + |
|||
" \"loadRelations\": false,\n" + |
|||
" \"loadAttributes\": true,\n" + |
|||
" \"loadCredentials\": true\n" + |
|||
" }\n" + |
|||
"}" + |
|||
MARKDOWN_CODE_BLOCK_END + NEW_LINE + |
|||
"Another request type (`ENTITY_TYPE`) is needed to load specific version of the whole entity types. " + |
|||
"It contains a structure with entity types to load and configs for each entity type (`entityTypes`). " + |
|||
"For each specified entity type, the method will load all remote entities of this type that are present " + |
|||
"at the version. A config for each entity type contains the same options as in `SINGLE_ENTITY` request type, and " + |
|||
"additionally contains following options:\n" + |
|||
"- `removeOtherEntities` - to remove local entities that are not present on the remote - basically to " + |
|||
" overwrite local entity type with the remote one;\n" + |
|||
"- `findExistingEntityByName` - when you are loading some remote entities that are not yet present at this tenant, " + |
|||
" try to find existing entity by name and update it rather than create new." + NEW_LINE + |
|||
"Here is an example of the request to completely restore version of the whole device entity type:\n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"{\n" + |
|||
" \"type\": \"ENTITY_TYPE\",\n" + |
|||
"\n" + |
|||
" \"branch\": \"dev\",\n" + |
|||
" \"versionId\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" + |
|||
"\n" + |
|||
" \"entityTypes\": {\n" + |
|||
" \"DEVICE\": {\n" + |
|||
" \"removeOtherEntities\": true,\n" + |
|||
" \"findExistingEntityByName\": false,\n" + |
|||
" \"loadRelations\": true,\n" + |
|||
" \"loadAttributes\": true,\n" + |
|||
" \"loadCredentials\": true\n" + |
|||
" }\n" + |
|||
" }\n" + |
|||
"}" + |
|||
MARKDOWN_CODE_BLOCK_END + NEW_LINE + |
|||
"The response will contain generated request UUID that is to be used to check the status of operation " + |
|||
"via `getVersionLoadRequestStatus`." + |
|||
TENANT_AUTHORITY_PARAGRAPH) |
|||
@PostMapping("/entity") |
|||
public UUID loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws Exception { |
|||
SecurityUser user = getCurrentUser(); |
|||
accessControlService.checkPermission(user, Resource.VERSION_CONTROL, Operation.WRITE); |
|||
return versionControlService.loadEntitiesVersion(user, request); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get version load request status (getVersionLoadRequestStatus)", notes = "" + |
|||
"Returns the status of previously made version load request. " + |
|||
"The structure contains following parameters:\n" + |
|||
"- `done` - if the request was successfully processed;\n" + |
|||
"- `result` - a list of load results for each entity type:\n" + |
|||
" - `created` - created entities count;\n" + |
|||
" - `updated` - updated entities count;\n" + |
|||
" - `deleted` - removed entities count.\n" + |
|||
"- `error` - if an error occurred during processing, error info:\n" + |
|||
" - `type` - error type;\n" + |
|||
" - `source` - an external id of remote entity;\n" + |
|||
" - `target` - if failed to find referenced entity by external id - this external id;\n" + |
|||
" - `message` - error message." + NEW_LINE + |
|||
"An example of successfully processed request status:\n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"{\n" + |
|||
" \"done\": true,\n" + |
|||
" \"result\": [\n" + |
|||
" {\n" + |
|||
" \"entityType\": \"DEVICE\",\n" + |
|||
" \"created\": 10,\n" + |
|||
" \"updated\": 5,\n" + |
|||
" \"deleted\": 5\n" + |
|||
" },\n" + |
|||
" {\n" + |
|||
" \"entityType\": \"ASSET\",\n" + |
|||
" \"created\": 4,\n" + |
|||
" \"updated\": 0,\n" + |
|||
" \"deleted\": 8\n" + |
|||
" }\n" + |
|||
" ]\n" + |
|||
"}" + |
|||
MARKDOWN_CODE_BLOCK_END + |
|||
TENANT_AUTHORITY_PARAGRAPH |
|||
) |
|||
@GetMapping(value = "/entity/{requestId}/status") |
|||
public VersionLoadResult getVersionLoadRequestStatus(@ApiParam(value = VC_REQUEST_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable UUID requestId) throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE); |
|||
return versionControlService.getVersionLoadStatus(getCurrentUser(), requestId); |
|||
} |
|||
|
|||
|
|||
@ApiOperation(value = "List branches (listBranches)", notes = "" + |
|||
"Lists branches available in the remote repository. \n\n" + |
|||
"Response example: \n" + |
|||
MARKDOWN_CODE_BLOCK_START + |
|||
"[\n" + |
|||
" {\n" + |
|||
" \"name\": \"master\",\n" + |
|||
" \"default\": true\n" + |
|||
" },\n" + |
|||
" {\n" + |
|||
" \"name\": \"dev\",\n" + |
|||
" \"default\": false\n" + |
|||
" },\n" + |
|||
" {\n" + |
|||
" \"name\": \"dev-2\",\n" + |
|||
" \"default\": false\n" + |
|||
" }\n" + |
|||
"]" + |
|||
MARKDOWN_CODE_BLOCK_END) |
|||
@GetMapping("/branches") |
|||
public DeferredResult<List<BranchInfo>> listBranches() throws Exception { |
|||
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); |
|||
final TenantId tenantId = getTenantId(); |
|||
ListenableFuture<List<BranchInfo>> branches = versionControlService.listBranches(tenantId); |
|||
return wrapFuture(Futures.transform(branches, remoteBranches -> { |
|||
List<BranchInfo> infos = new ArrayList<>(); |
|||
BranchInfo defaultBranch; |
|||
String defaultBranchName = versionControlService.getVersionControlSettings(tenantId).getDefaultBranch(); |
|||
if (StringUtils.isNotEmpty(defaultBranchName)) { |
|||
defaultBranch = new BranchInfo(defaultBranchName, true); |
|||
} else { |
|||
defaultBranch = remoteBranches.stream().filter(BranchInfo::isDefault).findFirst().orElse(null); |
|||
} |
|||
if (defaultBranch != null) { |
|||
infos.add(defaultBranch); |
|||
} |
|||
infos.addAll(remoteBranches.stream().filter(b -> !b.equals(defaultBranch)) |
|||
.map(b -> new BranchInfo(b.getName(), false)).collect(Collectors.toList())); |
|||
return infos; |
|||
}, MoreExecutors.directExecutor())); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,272 @@ |
|||
/** |
|||
* 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.controller; |
|||
|
|||
import io.swagger.annotations.ApiOperation; |
|||
import io.swagger.annotations.ApiParam; |
|||
import lombok.Data; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.PutMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; |
|||
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; |
|||
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; |
|||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; |
|||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; |
|||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
|
|||
import javax.validation.Valid; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api/2fa") |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
public class TwoFactorAuthConfigController extends BaseController { |
|||
|
|||
private final TwoFaConfigManager twoFaConfigManager; |
|||
private final TwoFactorAuthService twoFactorAuthService; |
|||
|
|||
|
|||
@ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)", |
|||
notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE + |
|||
"Example:\n" + |
|||
"```\n{\n \"configs\": {\n" + |
|||
" \"EMAIL\": {\n \"providerType\": \"EMAIL\",\n \"useByDefault\": true,\n \"email\": \"tenant@thingsboard.org\"\n },\n" + |
|||
" \"TOTP\": {\n \"providerType\": \"TOTP\",\n \"useByDefault\": false,\n \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=P6Z2TLYTASOGP6LCJZAD24ETT5DACNNX\"\n },\n" + |
|||
" \"SMS\": {\n \"providerType\": \"SMS\",\n \"useByDefault\": false,\n \"phoneNumber\": \"+380501253652\"\n }\n" + |
|||
" }\n}\n```" + |
|||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@GetMapping("/account/settings") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException { |
|||
SecurityUser user = getCurrentUser(); |
|||
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()).orElse(null); |
|||
} |
|||
|
|||
|
|||
@ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)", |
|||
notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE + |
|||
"For TOTP, this will return a corresponding account config template " + |
|||
"with a generated OTP auth URL (with new random secret key for each API call) that can be then " + |
|||
"converted to a QR code to scan with an authenticator app. Example:\n" + |
|||
"```\n{\n" + |
|||
" \"providerType\": \"TOTP\",\n" + |
|||
" \"useByDefault\": false,\n" + |
|||
" \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=PNJDNWJVAK4ZTUYT7RFGPQLXA7XGU7PX\"\n" + |
|||
"}\n```" + NEW_LINE + |
|||
"For EMAIL, the generated config will contain email from user's account:\n" + |
|||
"```\n{\n" + |
|||
" \"providerType\": \"EMAIL\",\n" + |
|||
" \"useByDefault\": false,\n" + |
|||
" \"email\": \"tenant@thingsboard.org\"\n" + |
|||
"}\n```" + NEW_LINE + |
|||
"For SMS 2FA this method will just return a config with empty/default values as there is nothing to generate/preset:\n" + |
|||
"```\n{\n" + |
|||
" \"providerType\": \"SMS\",\n" + |
|||
" \"useByDefault\": false,\n" + |
|||
" \"phoneNumber\": null\n" + |
|||
"}\n```" + NEW_LINE + |
|||
"Will throw an error (Bad Request) if the provider is not configured for usage. " + |
|||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PostMapping("/account/config/generate") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public TwoFaAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true) |
|||
@RequestParam TwoFaProviderType providerType) throws Exception { |
|||
SecurityUser user = getCurrentUser(); |
|||
return twoFactorAuthService.generateNewAccountConfig(user, providerType); |
|||
} |
|||
|
|||
@ApiOperation(value = "Submit 2FA account config (submitTwoFaAccountConfig)", |
|||
notes = "Submit 2FA account config to prepare for a future verification. " + |
|||
"Basically, this method will send a verification code for a given account config, if this has " + |
|||
"sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE + |
|||
"Example of EMAIL 2FA account config:\n" + |
|||
"```\n{\n" + |
|||
" \"providerType\": \"EMAIL\",\n" + |
|||
" \"useByDefault\": true,\n" + |
|||
" \"email\": \"separate-email-for-2fa@thingsboard.org\"\n" + |
|||
"}\n```" + NEW_LINE + |
|||
"Example of SMS 2FA account config:\n" + |
|||
"```\n{\n" + |
|||
" \"providerType\": \"SMS\",\n" + |
|||
" \"useByDefault\": false,\n" + |
|||
" \"phoneNumber\": \"+38012312321\"\n" + |
|||
"}\n```" + NEW_LINE + |
|||
"For TOTP this method does nothing." + NEW_LINE + |
|||
"Will throw an error (Bad Request) if submitted account config is not valid, " + |
|||
"or if the provider is not configured for usage. " + |
|||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PostMapping("/account/config/submit") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception { |
|||
SecurityUser user = getCurrentUser(); |
|||
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); |
|||
} |
|||
|
|||
@ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)", |
|||
notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " + NEW_LINE + |
|||
"Returns whole account's 2FA settings object.\n" + |
|||
"Will throw an error (Bad Request) if the provider is not configured for usage. " + |
|||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PostMapping("/account/config") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig, |
|||
@RequestParam(required = false) String verificationCode) throws Exception { |
|||
SecurityUser user = getCurrentUser(); |
|||
if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) { |
|||
throw new IllegalArgumentException("2FA provider is already configured"); |
|||
} |
|||
|
|||
boolean verificationSuccess; |
|||
if (accountConfig.getProviderType() != TwoFaProviderType.BACKUP_CODE) { |
|||
verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false); |
|||
} else { |
|||
verificationSuccess = true; |
|||
} |
|||
if (verificationSuccess) { |
|||
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); |
|||
} else { |
|||
throw new IllegalArgumentException("Verification code is incorrect"); |
|||
} |
|||
} |
|||
|
|||
@ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes = |
|||
"Update config for a given provider type. \n" + |
|||
"Update request example:\n" + |
|||
"```\n{\n \"useByDefault\": true\n}\n```\n" + |
|||
"Returns whole account's 2FA settings object.\n" + |
|||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PutMapping("/account/config") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType, |
|||
@RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException { |
|||
SecurityUser user = getCurrentUser(); |
|||
|
|||
TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) |
|||
.orElseThrow(() -> new IllegalArgumentException("Config for " + providerType + " 2FA provider not found")); |
|||
accountConfig.setUseByDefault(updateRequest.isUseByDefault()); |
|||
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); |
|||
} |
|||
|
|||
@ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes = |
|||
"Delete 2FA config for a given 2FA provider type. \n" + |
|||
"Returns whole account's 2FA settings object.\n" + |
|||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@DeleteMapping("/account/config") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException { |
|||
SecurityUser user = getCurrentUser(); |
|||
return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType); |
|||
} |
|||
|
|||
|
|||
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = |
|||
"Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" + |
|||
"Example of response:\n" + |
|||
"```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + |
|||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER |
|||
) |
|||
@GetMapping("/providers") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException { |
|||
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true) |
|||
.map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream() |
|||
.map(TwoFaProviderConfig::getProviderType) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
|
|||
@ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)", |
|||
notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " + |
|||
"If 2FA is not configured, then an empty response will be returned." + |
|||
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping("/settings") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
|||
public PlatformTwoFaSettings getPlatformTwoFaSettings() throws ThingsboardException { |
|||
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null); |
|||
} |
|||
|
|||
@ApiOperation(value = "Save platform 2FA settings (savePlatformTwoFaSettings)", |
|||
notes = "Save 2FA settings for platform. The settings have following properties:\n" + |
|||
"- `providers` - the list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list. \n\n" + |
|||
"- `minVerificationCodeSendPeriod` - minimal period in seconds to wait after verification code send request to send next request. \n" + |
|||
"- `verificationCodeCheckRateLimit` - rate limit configuration for verification code checking.\n" + |
|||
"The format is standard: 'amountOfRequests:periodInSeconds'. The value of '1:60' would limit verification " + |
|||
"code checking requests to one per minute.\n" + |
|||
"- `maxVerificationFailuresBeforeUserLockout` - maximum number of verification failures before a user gets disabled.\n" + |
|||
"- `totalAllowedTimeForVerification` - total amount of time in seconds allotted for verification. " + |
|||
"Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.\n" + NEW_LINE + |
|||
"TOTP 2FA provider config has following settings:\n" + |
|||
"- `issuerName` - issuer name that will be displayed in an authenticator app near a username. Must not be blank.\n\n" + |
|||
"For SMS 2FA provider:\n" + |
|||
"- `smsVerificationMessageTemplate` - verification message template. Available template variables " + |
|||
"are ${code} and ${userEmail}. It must not be blank and must contain verification code variable.\n" + |
|||
"- `verificationCodeLifetime` - verification code lifetime in seconds. Required to be positive.\n\n" + |
|||
"For EMAIL provider type:\n" + |
|||
"- `verificationCodeLifetime` - the same as for SMS." + NEW_LINE + |
|||
"Example of the settings:\n" + |
|||
"```\n{\n" + |
|||
" \"providers\": [\n" + |
|||
" {\n" + |
|||
" \"providerType\": \"TOTP\",\n" + |
|||
" \"issuerName\": \"TB\"\n" + |
|||
" },\n" + |
|||
" {\n" + |
|||
" \"providerType\": \"EMAIL\",\n" + |
|||
" \"verificationCodeLifetime\": 60\n" + |
|||
" },\n" + |
|||
" {\n" + |
|||
" \"providerType\": \"SMS\",\n" + |
|||
" \"verificationCodeLifetime\": 60,\n" + |
|||
" \"smsVerificationMessageTemplate\": \"Here is your verification code: ${code}\"\n" + |
|||
" }\n" + |
|||
" ],\n" + |
|||
" \"minVerificationCodeSendPeriod\": 60,\n" + |
|||
" \"verificationCodeCheckRateLimit\": \"3:900\",\n" + |
|||
" \"maxVerificationFailuresBeforeUserLockout\": 10,\n" + |
|||
" \"totalAllowedTimeForVerification\": 600\n" + |
|||
"}\n```" + |
|||
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@PostMapping("/settings") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
|||
public PlatformTwoFaSettings savePlatformTwoFaSettings(@ApiParam(value = "Settings value", required = true) |
|||
@RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException { |
|||
return twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings); |
|||
} |
|||
|
|||
|
|||
@Data |
|||
public static class TwoFaAccountConfigUpdateRequest { |
|||
private boolean useByDefault; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
/** |
|||
* 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.controller; |
|||
|
|||
import io.swagger.annotations.ApiOperation; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.audit.ActionType; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; |
|||
import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig; |
|||
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; |
|||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; |
|||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; |
|||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; |
|||
import org.thingsboard.server.service.security.model.JwtTokenPair; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
|||
import org.thingsboard.server.service.security.system.SystemSecurityService; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
import java.util.Optional; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api/auth/2fa") |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
public class TwoFactorAuthController extends BaseController { |
|||
|
|||
private final TwoFactorAuthService twoFactorAuthService; |
|||
private final TwoFaConfigManager twoFaConfigManager; |
|||
private final JwtTokenFactory tokenFactory; |
|||
private final SystemSecurityService systemSecurityService; |
|||
private final UserService userService; |
|||
|
|||
|
|||
@ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)", |
|||
notes = "Request 2FA verification code." + NEW_LINE + |
|||
"To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " + |
|||
"which is issued on username/password auth if 2FA is enabled." + NEW_LINE + |
|||
"The API method is rate limited (using rate limit config from TwoFactorAuthSettings). " + |
|||
"Will return a Bad Request error if provider is not configured for usage, " + |
|||
"and Too Many Requests error if rate limits are exceeded.") |
|||
@PostMapping("/verification/send") |
|||
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") |
|||
public void requestTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType) throws Exception { |
|||
SecurityUser user = getCurrentUser(); |
|||
twoFactorAuthService.prepareVerificationCode(user, providerType, true); |
|||
} |
|||
|
|||
@ApiOperation(value = "Check 2FA verification code (checkTwoFaVerificationCode)", |
|||
notes = "Checks 2FA verification code, and if it is correct the method returns a regular access and refresh token pair." + NEW_LINE + |
|||
"The API method is rate limited (using rate limit config from TwoFactorAuthSettings), and also will block a user " + |
|||
"after X unsuccessful verification attempts if such behavior is configured (in TwoFactorAuthSettings)." + NEW_LINE + |
|||
"Will return a Bad Request error if provider is not configured for usage, " + |
|||
"and Too Many Requests error if rate limits are exceeded.") |
|||
@PostMapping("/verification/check") |
|||
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") |
|||
public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType, |
|||
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { |
|||
SecurityUser user = getCurrentUser(); |
|||
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); |
|||
if (verificationSuccess) { |
|||
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); |
|||
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); |
|||
return tokenFactory.createTokenPair(user); |
|||
} else { |
|||
ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS); |
|||
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
|
|||
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = |
|||
"Get the list of 2FA provider infos available for user to use. Example:\n" + |
|||
"```\n[\n" + |
|||
" {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + |
|||
" {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" + |
|||
" {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" + |
|||
"]\n```") |
|||
@GetMapping("/providers") |
|||
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") |
|||
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException { |
|||
SecurityUser user = getCurrentUser(); |
|||
Optional<PlatformTwoFaSettings> platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true); |
|||
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()) |
|||
.map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList()) |
|||
.stream().map(config -> { |
|||
String contact = null; |
|||
switch (config.getProviderType()) { |
|||
case SMS: |
|||
String phoneNumber = ((SmsTwoFaAccountConfig) config).getPhoneNumber(); |
|||
contact = StringUtils.obfuscate(phoneNumber, 2, '*', phoneNumber.indexOf('+') + 1, phoneNumber.length()); |
|||
break; |
|||
case EMAIL: |
|||
String email = ((EmailTwoFaAccountConfig) config).getEmail(); |
|||
contact = StringUtils.obfuscate(email, 2, '*', 0, email.indexOf('@')); |
|||
break; |
|||
} |
|||
return TwoFaProviderInfo.builder() |
|||
.type(config.getProviderType()) |
|||
.isDefault(config.isUseByDefault()) |
|||
.contact(contact) |
|||
.minVerificationCodeSendPeriod(platformTwoFaSettings.get().getMinVerificationCodeSendPeriod()) |
|||
.build(); |
|||
}) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
@Data |
|||
@AllArgsConstructor |
|||
@Builder |
|||
public static class TwoFaProviderInfo { |
|||
private TwoFaProviderType type; |
|||
private boolean isDefault; |
|||
private String contact; |
|||
private Integer minVerificationCodeSendPeriod; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
/** |
|||
* 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.apiusage; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
|||
import org.thingsboard.server.common.msg.tools.TbRateLimits; |
|||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; |
|||
|
|||
import java.util.Map; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.function.Function; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class DefaultRateLimitService implements RateLimitService { |
|||
|
|||
private final TbTenantProfileCache tenantProfileCache; |
|||
|
|||
private final Map<String, Map<TenantId, TbRateLimits>> rateLimits = new ConcurrentHashMap<>(); |
|||
|
|||
@Override |
|||
public boolean checkEntityExportLimit(TenantId tenantId) { |
|||
return checkLimit(tenantId, "entityExport", DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit); |
|||
} |
|||
|
|||
@Override |
|||
public boolean checkEntityImportLimit(TenantId tenantId) { |
|||
return checkLimit(tenantId, "entityImport", DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit); |
|||
} |
|||
|
|||
private boolean checkLimit(TenantId tenantId, String rateLimitsKey, Function<DefaultTenantProfileConfiguration, String> rateLimitConfigExtractor) { |
|||
String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration() |
|||
.map(rateLimitConfigExtractor).orElse(null); |
|||
|
|||
Map<TenantId, TbRateLimits> rateLimits = this.rateLimits.get(rateLimitsKey); |
|||
if (StringUtils.isEmpty(rateLimitConfig)) { |
|||
if (rateLimits != null) { |
|||
rateLimits.remove(tenantId); |
|||
if (rateLimits.isEmpty()) { |
|||
this.rateLimits.remove(rateLimitsKey); |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
if (rateLimits == null) { |
|||
rateLimits = new ConcurrentHashMap<>(); |
|||
this.rateLimits.put(rateLimitsKey, rateLimits); |
|||
} |
|||
TbRateLimits rateLimit = rateLimits.get(tenantId); |
|||
if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) { |
|||
rateLimit = new TbRateLimits(rateLimitConfig); |
|||
rateLimits.put(tenantId, rateLimit); |
|||
} |
|||
|
|||
return rateLimit.tryConsume(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
/** |
|||
* 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.apiusage; |
|||
|
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
public interface RateLimitService { |
|||
|
|||
boolean checkEntityExportLimit(TenantId tenantId); |
|||
|
|||
boolean checkEntityImportLimit(TenantId tenantId); |
|||
|
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
/** |
|||
* 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.edge.rpc; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.thingsboard.server.common.data.edge.EdgeEvent; |
|||
import org.thingsboard.server.common.data.edge.EdgeEventActionType; |
|||
import org.thingsboard.server.common.data.edge.EdgeEventType; |
|||
import org.thingsboard.server.common.data.id.EdgeId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
public final class EdgeEventUtils { |
|||
|
|||
private EdgeEventUtils() { |
|||
} |
|||
|
|||
public static EdgeEvent constructEdgeEvent(TenantId tenantId, |
|||
EdgeId edgeId, |
|||
EdgeEventType type, |
|||
EdgeEventActionType action, |
|||
EntityId entityId, |
|||
JsonNode body) { |
|||
EdgeEvent edgeEvent = new EdgeEvent(); |
|||
edgeEvent.setTenantId(tenantId); |
|||
edgeEvent.setEdgeId(edgeId); |
|||
edgeEvent.setType(type); |
|||
edgeEvent.setAction(action); |
|||
if (entityId != null) { |
|||
edgeEvent.setEntityId(entityId.getId()); |
|||
} |
|||
edgeEvent.setBody(body); |
|||
return edgeEvent; |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
/** |
|||
* 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.edge.rpc.constructor; |
|||
|
|||
import com.google.protobuf.ByteString; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.OtaPackage; |
|||
import org.thingsboard.server.common.data.id.OtaPackageId; |
|||
import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; |
|||
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
@Component |
|||
@TbCoreComponent |
|||
public class OtaPackageMsgConstructor { |
|||
|
|||
public OtaPackageUpdateMsg constructOtaPackageUpdatedMsg(UpdateMsgType msgType, OtaPackage otaPackage) { |
|||
OtaPackageUpdateMsg.Builder builder = OtaPackageUpdateMsg.newBuilder() |
|||
.setMsgType(msgType) |
|||
.setIdMSB(otaPackage.getId().getId().getMostSignificantBits()) |
|||
.setIdLSB(otaPackage.getId().getId().getLeastSignificantBits()) |
|||
.setType(otaPackage.getType().name()) |
|||
.setTitle(otaPackage.getTitle()) |
|||
.setVersion(otaPackage.getVersion()) |
|||
.setTag(otaPackage.getTag()); |
|||
|
|||
if (otaPackage.getDeviceProfileId() != null) { |
|||
builder.setDeviceProfileIdMSB(otaPackage.getDeviceProfileId().getId().getMostSignificantBits()) |
|||
.setDeviceProfileIdLSB(otaPackage.getDeviceProfileId().getId().getLeastSignificantBits()); |
|||
} |
|||
|
|||
if (otaPackage.getUrl() != null) { |
|||
builder.setUrl(otaPackage.getUrl()); |
|||
} |
|||
if (otaPackage.getAdditionalInfo() != null) { |
|||
builder.setAdditionalInfo(JacksonUtil.toString(otaPackage.getAdditionalInfo())); |
|||
} |
|||
if (otaPackage.getFileName() != null) { |
|||
builder.setFileName(otaPackage.getFileName()); |
|||
} |
|||
if (otaPackage.getContentType() != null) { |
|||
builder.setContentType(otaPackage.getContentType()); |
|||
} |
|||
if (otaPackage.getChecksumAlgorithm() != null) { |
|||
builder.setChecksumAlgorithm(otaPackage.getChecksumAlgorithm().name()); |
|||
} |
|||
if (otaPackage.getChecksum() != null) { |
|||
builder.setChecksum(otaPackage.getChecksum()); |
|||
} |
|||
if (otaPackage.getDataSize() != null) { |
|||
builder.setDataSize(otaPackage.getDataSize()); |
|||
} |
|||
if (otaPackage.getData() != null) { |
|||
builder.setData(ByteString.copyFrom(otaPackage.getData().array())); |
|||
} |
|||
return builder.build(); |
|||
} |
|||
|
|||
public OtaPackageUpdateMsg constructOtaPackageDeleteMsg(OtaPackageId otaPackageId) { |
|||
return OtaPackageUpdateMsg.newBuilder() |
|||
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) |
|||
.setIdMSB(otaPackageId.getId().getMostSignificantBits()) |
|||
.setIdLSB(otaPackageId.getId().getLeastSignificantBits()).build(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
/** |
|||
* 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.edge.rpc.constructor; |
|||
|
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.id.QueueId; |
|||
import org.thingsboard.server.common.data.queue.ProcessingStrategy; |
|||
import org.thingsboard.server.common.data.queue.Queue; |
|||
import org.thingsboard.server.common.data.queue.SubmitStrategy; |
|||
import org.thingsboard.server.gen.edge.v1.ProcessingStrategyProto; |
|||
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; |
|||
import org.thingsboard.server.gen.edge.v1.SubmitStrategyProto; |
|||
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
@Component |
|||
@TbCoreComponent |
|||
public class QueueMsgConstructor { |
|||
|
|||
public QueueUpdateMsg constructQueueUpdatedMsg(UpdateMsgType msgType, Queue queue) { |
|||
QueueUpdateMsg.Builder builder = QueueUpdateMsg.newBuilder() |
|||
.setMsgType(msgType) |
|||
.setIdMSB(queue.getId().getId().getMostSignificantBits()) |
|||
.setIdLSB(queue.getId().getId().getLeastSignificantBits()) |
|||
.setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) |
|||
.setTenantIdLSB(queue.getTenantId().getId().getLeastSignificantBits()) |
|||
.setName(queue.getName()) |
|||
.setTopic(queue.getTopic()) |
|||
.setPollInterval(queue.getPollInterval()) |
|||
.setPartitions(queue.getPartitions()) |
|||
.setConsumerPerPartition(queue.isConsumerPerPartition()) |
|||
.setPackProcessingTimeout(queue.getPackProcessingTimeout()) |
|||
.setSubmitStrategy(createSubmitStrategyProto(queue.getSubmitStrategy())) |
|||
.setProcessingStrategy(createProcessingStrategyProto(queue.getProcessingStrategy())); |
|||
return builder.build(); |
|||
} |
|||
|
|||
private ProcessingStrategyProto createProcessingStrategyProto(ProcessingStrategy processingStrategy) { |
|||
return ProcessingStrategyProto.newBuilder() |
|||
.setType(processingStrategy.getType().name()) |
|||
.setRetries(processingStrategy.getRetries()) |
|||
.setFailurePercentage(processingStrategy.getFailurePercentage()) |
|||
.setPauseBetweenRetries(processingStrategy.getPauseBetweenRetries()) |
|||
.setMaxPauseBetweenRetries(processingStrategy.getMaxPauseBetweenRetries()) |
|||
.build(); |
|||
} |
|||
|
|||
private SubmitStrategyProto createSubmitStrategyProto(SubmitStrategy submitStrategy) { |
|||
return SubmitStrategyProto.newBuilder() |
|||
.setType(submitStrategy.getType().name()) |
|||
.setBatchSize(submitStrategy.getBatchSize()) |
|||
.build(); |
|||
} |
|||
|
|||
public QueueUpdateMsg constructQueueDeleteMsg(QueueId queueId) { |
|||
return QueueUpdateMsg.newBuilder() |
|||
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) |
|||
.setIdMSB(queueId.getId().getMostSignificantBits()) |
|||
.setIdLSB(queueId.getId().getLeastSignificantBits()).build(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
/** |
|||
* 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.edge.rpc.constructor.rule; |
|||
|
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.NodeConnectionInfo; |
|||
import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; |
|||
import org.thingsboard.server.common.data.rule.RuleChainMetaData; |
|||
import org.thingsboard.server.common.data.rule.RuleNode; |
|||
import org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto; |
|||
import org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto; |
|||
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; |
|||
import org.thingsboard.server.gen.edge.v1.RuleNodeProto; |
|||
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.NavigableSet; |
|||
|
|||
@Slf4j |
|||
@AllArgsConstructor |
|||
public abstract class AbstractRuleChainMetadataConstructor implements RuleChainMetadataConstructor { |
|||
|
|||
@Override |
|||
public RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(TenantId tenantId, |
|||
UpdateMsgType msgType, |
|||
RuleChainMetaData ruleChainMetaData) { |
|||
try { |
|||
RuleChainMetadataUpdateMsg.Builder builder = RuleChainMetadataUpdateMsg.newBuilder(); |
|||
builder.setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) |
|||
.setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()); |
|||
constructRuleChainMetadataUpdatedMsg(tenantId, builder, ruleChainMetaData); |
|||
builder.setMsgType(msgType); |
|||
return builder.build(); |
|||
} catch (JsonProcessingException ex) { |
|||
log.error("Can't construct RuleChainMetadataUpdateMsg", ex); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
protected abstract void constructRuleChainMetadataUpdatedMsg(TenantId tenantId, |
|||
RuleChainMetadataUpdateMsg.Builder builder, |
|||
RuleChainMetaData ruleChainMetaData) throws JsonProcessingException; |
|||
|
|||
protected List<NodeConnectionInfoProto> constructConnections(List<NodeConnectionInfo> connections) { |
|||
List<NodeConnectionInfoProto> result = new ArrayList<>(); |
|||
if (connections != null && !connections.isEmpty()) { |
|||
for (NodeConnectionInfo connection : connections) { |
|||
result.add(constructConnection(connection)); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private NodeConnectionInfoProto constructConnection(NodeConnectionInfo connection) { |
|||
return NodeConnectionInfoProto.newBuilder() |
|||
.setFromIndex(connection.getFromIndex()) |
|||
.setToIndex(connection.getToIndex()) |
|||
.setType(connection.getType()) |
|||
.build(); |
|||
} |
|||
|
|||
protected List<RuleNodeProto> constructNodes(List<RuleNode> nodes) throws JsonProcessingException { |
|||
List<RuleNodeProto> result = new ArrayList<>(); |
|||
if (nodes != null && !nodes.isEmpty()) { |
|||
for (RuleNode node : nodes) { |
|||
result.add(constructNode(node)); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private RuleNodeProto constructNode(RuleNode node) throws JsonProcessingException { |
|||
return RuleNodeProto.newBuilder() |
|||
.setIdMSB(node.getId().getId().getMostSignificantBits()) |
|||
.setIdLSB(node.getId().getId().getLeastSignificantBits()) |
|||
.setType(node.getType()) |
|||
.setName(node.getName()) |
|||
.setDebugMode(node.isDebugMode()) |
|||
.setConfiguration(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getConfiguration())) |
|||
.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.writeValueAsString(node.getAdditionalInfo())) |
|||
.build(); |
|||
} |
|||
|
|||
protected List<RuleChainConnectionInfoProto> constructRuleChainConnections(List<RuleChainConnectionInfo> ruleChainConnections, |
|||
NavigableSet<Integer> removedNodeIndexes) throws JsonProcessingException { |
|||
List<RuleChainConnectionInfoProto> result = new ArrayList<>(); |
|||
if (ruleChainConnections != null && !ruleChainConnections.isEmpty()) { |
|||
for (RuleChainConnectionInfo ruleChainConnectionInfo : ruleChainConnections) { |
|||
if (!removedNodeIndexes.isEmpty()) { // 3_3_0 only
|
|||
int fromIndex = ruleChainConnectionInfo.getFromIndex(); |
|||
// decrease index because of removed nodes
|
|||
for (Integer removedIndex : removedNodeIndexes) { |
|||
if (fromIndex > removedIndex) { |
|||
fromIndex = fromIndex - 1; |
|||
} |
|||
} |
|||
ruleChainConnectionInfo.setFromIndex(fromIndex); |
|||
ObjectNode additionalInfo = (ObjectNode) ruleChainConnectionInfo.getAdditionalInfo(); |
|||
if (additionalInfo.get("ruleChainNodeId") == null) { |
|||
additionalInfo.put("ruleChainNodeId", "rule-chain-node-UNDEFINED"); |
|||
} |
|||
} |
|||
result.add(constructRuleChainConnection(ruleChainConnectionInfo)); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private RuleChainConnectionInfoProto constructRuleChainConnection(RuleChainConnectionInfo ruleChainConnectionInfo) throws JsonProcessingException { |
|||
return RuleChainConnectionInfoProto.newBuilder() |
|||
.setFromIndex(ruleChainConnectionInfo.getFromIndex()) |
|||
.setTargetRuleChainIdMSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getMostSignificantBits()) |
|||
.setTargetRuleChainIdLSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getLeastSignificantBits()) |
|||
.setType(ruleChainConnectionInfo.getType()) |
|||
.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.writeValueAsString(ruleChainConnectionInfo.getAdditionalInfo())) |
|||
.build(); |
|||
} |
|||
} |
|||
@ -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.service.edge.rpc.constructor.rule; |
|||
|
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.RuleChainMetaData; |
|||
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; |
|||
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; |
|||
|
|||
public interface RuleChainMetadataConstructor { |
|||
|
|||
RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(TenantId tenantId, |
|||
UpdateMsgType msgType, |
|||
RuleChainMetaData ruleChainMetaData); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* 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.edge.rpc.constructor.rule; |
|||
|
|||
import org.thingsboard.server.gen.edge.v1.EdgeVersion; |
|||
|
|||
public final class RuleChainMetadataConstructorFactory { |
|||
|
|||
public static RuleChainMetadataConstructor getByEdgeVersion(EdgeVersion edgeVersion) { |
|||
switch (edgeVersion) { |
|||
case V_3_3_0: |
|||
return new RuleChainMetadataConstructorV330(); |
|||
case V_3_3_3: |
|||
case V_3_4_0: |
|||
default: |
|||
return new RuleChainMetadataConstructorV340(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
/** |
|||
* 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.edge.rpc.constructor.rule; |
|||
|
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode; |
|||
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; |
|||
import org.thingsboard.rule.engine.flow.TbRuleChainOutputNode; |
|||
import org.thingsboard.server.common.data.id.RuleChainId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.NodeConnectionInfo; |
|||
import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; |
|||
import org.thingsboard.server.common.data.rule.RuleChainMetaData; |
|||
import org.thingsboard.server.common.data.rule.RuleNode; |
|||
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.NavigableSet; |
|||
import java.util.TreeSet; |
|||
import java.util.UUID; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Slf4j |
|||
public class RuleChainMetadataConstructorV330 extends AbstractRuleChainMetadataConstructor { |
|||
|
|||
private static final String RULE_CHAIN_INPUT_NODE = TbRuleChainInputNode.class.getName(); |
|||
private static final String TB_RULE_CHAIN_OUTPUT_NODE = TbRuleChainOutputNode.class.getName(); |
|||
|
|||
@Override |
|||
protected void constructRuleChainMetadataUpdatedMsg(TenantId tenantId, |
|||
RuleChainMetadataUpdateMsg.Builder builder, |
|||
RuleChainMetaData ruleChainMetaData) throws JsonProcessingException { |
|||
List<RuleNode> supportedNodes = filterNodes(ruleChainMetaData.getNodes()); |
|||
|
|||
NavigableSet<Integer> removedNodeIndexes = getRemovedNodeIndexes(ruleChainMetaData.getNodes(), ruleChainMetaData.getConnections()); |
|||
List<NodeConnectionInfo> connections = filterConnections(ruleChainMetaData.getNodes(), ruleChainMetaData.getConnections(), removedNodeIndexes); |
|||
|
|||
List<RuleChainConnectionInfo> ruleChainConnections = new ArrayList<>(); |
|||
if (ruleChainMetaData.getRuleChainConnections() != null) { |
|||
ruleChainConnections.addAll(ruleChainMetaData.getRuleChainConnections()); |
|||
} |
|||
ruleChainConnections.addAll(addRuleChainConnections(ruleChainMetaData.getNodes(), ruleChainMetaData.getConnections())); |
|||
builder.addAllNodes(constructNodes(supportedNodes)) |
|||
.addAllConnections(constructConnections(connections)) |
|||
.addAllRuleChainConnections(constructRuleChainConnections(ruleChainConnections, removedNodeIndexes)); |
|||
if (ruleChainMetaData.getFirstNodeIndex() != null) { |
|||
Integer firstNodeIndex = ruleChainMetaData.getFirstNodeIndex(); |
|||
// decrease index because of removed nodes
|
|||
for (Integer removedIndex : removedNodeIndexes) { |
|||
if (firstNodeIndex > removedIndex) { |
|||
firstNodeIndex = firstNodeIndex - 1; |
|||
} |
|||
} |
|||
builder.setFirstNodeIndex(firstNodeIndex); |
|||
} else { |
|||
builder.setFirstNodeIndex(-1); |
|||
} |
|||
} |
|||
|
|||
private NavigableSet<Integer> getRemovedNodeIndexes(List<RuleNode> nodes, List<NodeConnectionInfo> connections) { |
|||
TreeSet<Integer> removedIndexes = new TreeSet<>(); |
|||
for (NodeConnectionInfo connection : connections) { |
|||
for (int i = 0; i < nodes.size(); i++) { |
|||
RuleNode node = nodes.get(i); |
|||
if (node.getType().equalsIgnoreCase(RULE_CHAIN_INPUT_NODE) |
|||
|| node.getType().equalsIgnoreCase(TB_RULE_CHAIN_OUTPUT_NODE)) { |
|||
if (connection.getFromIndex() == i || connection.getToIndex() == i) { |
|||
removedIndexes.add(i); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return removedIndexes.descendingSet(); |
|||
} |
|||
|
|||
private List<NodeConnectionInfo> filterConnections(List<RuleNode> nodes, |
|||
List<NodeConnectionInfo> connections, |
|||
NavigableSet<Integer> removedNodeIndexes) { |
|||
List<NodeConnectionInfo> result = new ArrayList<>(); |
|||
if (connections != null) { |
|||
result = connections.stream().filter(conn -> { |
|||
for (int i = 0; i < nodes.size(); i++) { |
|||
RuleNode node = nodes.get(i); |
|||
if (node.getType().equalsIgnoreCase(RULE_CHAIN_INPUT_NODE) |
|||
|| node.getType().equalsIgnoreCase(TB_RULE_CHAIN_OUTPUT_NODE)) { |
|||
if (conn.getFromIndex() == i || conn.getToIndex() == i) { |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
return true; |
|||
}).map(conn -> { |
|||
NodeConnectionInfo newConn = new NodeConnectionInfo(); |
|||
newConn.setFromIndex(conn.getFromIndex()); |
|||
newConn.setToIndex(conn.getToIndex()); |
|||
newConn.setType(conn.getType()); |
|||
return newConn; |
|||
}).collect(Collectors.toList()); |
|||
} |
|||
|
|||
// decrease index because of removed nodes
|
|||
for (Integer removedIndex : removedNodeIndexes) { |
|||
for (NodeConnectionInfo newConn : result) { |
|||
if (newConn.getToIndex() > removedIndex) { |
|||
newConn.setToIndex(newConn.getToIndex() - 1); |
|||
} |
|||
if (newConn.getFromIndex() > removedIndex) { |
|||
newConn.setFromIndex(newConn.getFromIndex() - 1); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private List<RuleNode> filterNodes(List<RuleNode> nodes) { |
|||
List<RuleNode> result = new ArrayList<>(); |
|||
for (RuleNode node : nodes) { |
|||
if (RULE_CHAIN_INPUT_NODE.equals(node.getType()) |
|||
|| TB_RULE_CHAIN_OUTPUT_NODE.equals(node.getType())) { |
|||
log.trace("Skipping not supported rule node {}", node); |
|||
} else { |
|||
result.add(node); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private List<RuleChainConnectionInfo> addRuleChainConnections(List<RuleNode> nodes, List<NodeConnectionInfo> connections) { |
|||
List<RuleChainConnectionInfo> result = new ArrayList<>(); |
|||
for (int i = 0; i < nodes.size(); i++) { |
|||
RuleNode node = nodes.get(i); |
|||
if (node.getType().equalsIgnoreCase(RULE_CHAIN_INPUT_NODE)) { |
|||
for (NodeConnectionInfo connection : connections) { |
|||
if (connection.getToIndex() == i) { |
|||
RuleChainConnectionInfo e = new RuleChainConnectionInfo(); |
|||
e.setFromIndex(connection.getFromIndex()); |
|||
TbRuleChainInputNodeConfiguration configuration = JacksonUtil.treeToValue(node.getConfiguration(), TbRuleChainInputNodeConfiguration.class); |
|||
e.setTargetRuleChainId(new RuleChainId(UUID.fromString(configuration.getRuleChainId()))); |
|||
e.setAdditionalInfo(node.getAdditionalInfo()); |
|||
e.setType(connection.getType()); |
|||
result.add(e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* 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.edge.rpc.constructor.rule; |
|||
|
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.RuleChainMetaData; |
|||
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; |
|||
|
|||
import java.util.TreeSet; |
|||
|
|||
@Slf4j |
|||
public class RuleChainMetadataConstructorV340 extends AbstractRuleChainMetadataConstructor { |
|||
|
|||
@Override |
|||
protected void constructRuleChainMetadataUpdatedMsg(TenantId tenantId, |
|||
RuleChainMetadataUpdateMsg.Builder builder, |
|||
RuleChainMetaData ruleChainMetaData) throws JsonProcessingException { |
|||
builder.addAllNodes(constructNodes(ruleChainMetaData.getNodes())) |
|||
.addAllConnections(constructConnections(ruleChainMetaData.getConnections())) |
|||
.addAllRuleChainConnections(constructRuleChainConnections(ruleChainMetaData.getRuleChainConnections(), new TreeSet<>())); |
|||
if (ruleChainMetaData.getFirstNodeIndex() != null) { |
|||
builder.setFirstNodeIndex(ruleChainMetaData.getFirstNodeIndex()); |
|||
} else { |
|||
builder.setFirstNodeIndex(-1); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue