diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql index 51e78a504e..d400821fa3 100644 --- a/application/src/main/data/upgrade/3.7.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -99,3 +99,100 @@ ALTER TABLE widgets_bundle ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; ALTER TABLE tenant ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; -- ENTITIES VERSIONING UPDATE END + +-- OAUTH2 UPDATE START + +ALTER TABLE IF EXISTS oauth2_mobile RENAME TO mobile_app; +ALTER TABLE IF EXISTS oauth2_domain RENAME TO domain; +ALTER TABLE IF EXISTS oauth2_registration RENAME TO oauth2_client; + +ALTER TABLE domain ADD COLUMN IF NOT EXISTS oauth2_enabled boolean, + ADD COLUMN IF NOT EXISTS edge_enabled boolean, + ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080', + DROP COLUMN IF EXISTS domain_scheme; + +-- rename column domain_name to name +DO +$$ + BEGIN + IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='domain' and column_name='domain_name') THEN + ALTER TABLE domain RENAME COLUMN domain_name TO name; + END IF; + END +$$; + +-- delete duplicated domains +DELETE FROM domain d1 USING ( + SELECT MIN(ctid) as ctid, name + FROM domain + GROUP BY name HAVING COUNT(*) > 1 +) d2 WHERE d1.name = d2.name AND d1.ctid <> d2.ctid; + +ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS oauth2_enabled boolean, + ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080'; + +-- delete duplicated apps +DELETE FROM mobile_app m1 USING ( + SELECT MIN(ctid) as ctid, pkg_name + FROM mobile_app + GROUP BY pkg_name HAVING COUNT(*) > 1 +) m2 WHERE m1.pkg_name = m2.pkg_name AND m1.ctid <> m2.ctid; + +ALTER TABLE oauth2_client ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080', + ADD COLUMN IF NOT EXISTS title varchar(100); +UPDATE oauth2_client SET title = additional_info::jsonb->>'providerName' WHERE additional_info IS NOT NULL; + +CREATE TABLE IF NOT EXISTS domain_oauth2_client ( + domain_id uuid NOT NULL, + oauth2_client_id uuid NOT NULL, + CONSTRAINT fk_domain FOREIGN KEY (domain_id) REFERENCES domain(id) ON DELETE CASCADE, + CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS mobile_app_oauth2_client ( + mobile_app_id uuid NOT NULL, + oauth2_client_id uuid NOT NULL, + CONSTRAINT fk_domain FOREIGN KEY (mobile_app_id) REFERENCES mobile_app(id) ON DELETE CASCADE, + CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE +); + +-- migrate oauth2_params table +DO +$$ + BEGIN + IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'oauth2_params') THEN + UPDATE domain SET oauth2_enabled = p.enabled, + edge_enabled = p.edge_enabled + FROM oauth2_params p WHERE p.id = domain.oauth2_params_id; + + UPDATE mobile_app SET oauth2_enabled = p.enabled + FROM oauth2_params p WHERE p.id = mobile_app.oauth2_params_id; + + INSERT INTO domain_oauth2_client(domain_id, oauth2_client_id) + (SELECT d.id, r.id FROM domain d LEFT JOIN oauth2_client r on d.oauth2_params_id = r.oauth2_params_id + WHERE r.platforms IS NULL OR r.platforms IN ('','WEB')); + + INSERT INTO mobile_app_oauth2_client(mobile_app_id, oauth2_client_id) + (SELECT m.id, r.id FROM mobile_app m LEFT JOIN oauth2_client r on m.oauth2_params_id = r.oauth2_params_id + WHERE r.platforms IS NULL OR r.platforms IN ('','ANDROID','IOS')); + + ALTER TABLE mobile_app RENAME CONSTRAINT oauth2_mobile_pkey TO mobile_app_pkey; + ALTER TABLE domain RENAME CONSTRAINT oauth2_domain_pkey TO domain_pkey; + ALTER TABLE oauth2_client RENAME CONSTRAINT oauth2_registration_pkey TO oauth2_client_pkey; + + ALTER TABLE domain DROP COLUMN oauth2_params_id; + ALTER TABLE mobile_app DROP COLUMN oauth2_params_id; + ALTER TABLE oauth2_client DROP COLUMN oauth2_params_id; + + ALTER TABLE mobile_app ADD CONSTRAINT mobile_app_unq_key UNIQUE (pkg_name); + ALTER TABLE domain ADD CONSTRAINT domain_unq_key UNIQUE (name); + + DROP TABLE IF EXISTS oauth2_params; + -- drop deprecated tables + DROP TABLE IF EXISTS oauth2_client_registration_info; + DROP TABLE IF EXISTS oauth2_client_registration; + END IF; + END +$$; + +-- OAUTH2 UPDATE END \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index b125daa591..5966716ed8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -68,17 +68,20 @@ import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -370,6 +373,18 @@ public class ActorSystemContext { @Getter private NotificationRuleService notificationRuleService; + @Autowired + @Getter + private OAuth2ClientService oAuth2ClientService; + + @Autowired + @Getter + private DomainService domainService; + + @Autowired + @Getter + private MobileAppService mobileAppService; + @Autowired @Getter private SlackService slackService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ca404fc78a..ec10402821 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -83,17 +83,20 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -825,6 +828,21 @@ class DefaultTbContext implements TbContext { return mainCtx.getNotificationRuleService(); } + @Override + public OAuth2ClientService getOAuth2ClientService() { + return mainCtx.getOAuth2ClientService(); + } + + @Override + public DomainService getDomainService() { + return mainCtx.getDomainService(); + } + + @Override + public MobileAppService getMobileAppService() { + return mainCtx.getMobileAppService(); + } + @Override public SlackService getSlackService() { return mainCtx.getSlackService(); diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index 1784c6fa1b..542a7c71cb 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -37,8 +37,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; -import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory; @@ -70,7 +71,7 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza private ClientRegistrationRepository clientRegistrationRepository; @Autowired - private OAuth2Service oAuth2Service; + private OAuth2ClientService oAuth2ClientService; @Autowired private OAuth2AppTokenFactory oAuth2AppTokenFactory; @@ -115,14 +116,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza return request.getParameter("appToken"); } - private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) { - if (registrationId == null) { + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String oauth2ClientId, String redirectUriAction, String appPackage, String appToken) { + if (oauth2ClientId == null) { return null; } - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(oauth2ClientId); if (clientRegistration == null) { - throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); + throw new IllegalArgumentException("Invalid Client Registration with Id: " + oauth2ClientId); } Map attributes = new HashMap<>(); @@ -131,7 +132,7 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza if (StringUtils.isEmpty(appToken)) { throw new IllegalArgumentException("Invalid application token."); } else { - String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage); + String appSecret = this.oAuth2ClientService.findAppSecret(new OAuth2ClientId(UUID.fromString(oauth2ClientId)), appPackage); if (StringUtils.isEmpty(appSecret)) { throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package."); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 29b9db10ab..8021e71fd6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; @@ -76,11 +77,14 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RpcId; @@ -93,6 +97,8 @@ import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; @@ -119,13 +125,15 @@ import org.thingsboard.server.dao.device.ClaimDevicesService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; -import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -162,6 +170,9 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -236,7 +247,13 @@ public abstract class BaseController { protected DashboardService dashboardService; @Autowired - protected OAuth2Service oAuth2Service; + protected OAuth2ClientService oAuth2ClientService; + + @Autowired + protected DomainService domainService; + + @Autowired + protected MobileAppService mobileAppService; @Autowired protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService; @@ -612,6 +629,15 @@ public abstract class BaseController { case QUEUE: checkQueueId(new QueueId(entityId.getId()), operation); return; + case OAUTH2_CLIENT: + checkOauth2ClientId(new OAuth2ClientId(entityId.getId()), operation); + return; + case DOMAIN: + checkDomainId(new DomainId(entityId.getId()), operation); + return; + case MOBILE_APP: + checkMobileAppId(new MobileAppId(entityId.getId()), operation); + return; default: checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); } @@ -788,6 +814,18 @@ public abstract class BaseController { return queue; } + OAuth2Client checkOauth2ClientId(OAuth2ClientId oAuth2ClientId, Operation operation) throws ThingsboardException { + return checkEntityId(oAuth2ClientId, oAuth2ClientService::findOAuth2ClientById, operation); + } + + Domain checkDomainId(DomainId domainId, Operation operation) throws ThingsboardException { + return checkEntityId(domainId, domainService::findDomainById, operation); + } + + MobileApp checkMobileAppId(MobileAppId mobileAppId, Operation operation) throws ThingsboardException { + return checkEntityId(mobileAppId, mobileAppService::findMobileAppById, operation); + } + protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } @@ -874,4 +912,17 @@ public abstract class BaseController { } } + protected List getOAuth2ClientIds(UUID[] ids) throws ThingsboardException { + if (ids == null) { + return Collections.emptyList(); + } + List oAuth2ClientIds = new ArrayList<>(); + for (UUID id : ids) { + OAuth2ClientId oauth2ClientId = new OAuth2ClientId(id); + checkOauth2ClientId(oauth2ClientId, Operation.READ); + oAuth2ClientIds.add(oauth2ClientId); + } + return oAuth2ClientIds; + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DomainController.java b/application/src/main/java/org/thingsboard/server/controller/DomainController.java new file mode 100644 index 0000000000..41eea463df --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/DomainController.java @@ -0,0 +1,132 @@ +/** + * Copyright © 2016-2024 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.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.PathVariable; +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.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.domain.TbDomainService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.List; +import java.util.UUID; + +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class DomainController extends BaseController { + + private final TbDomainService tbDomainService; + + @ApiOperation(value = "Save or Update Domain (saveDomain)", + notes = "Create or update the Domain. When creating domain, platform generates Domain Id as " + UUID_WIKI_LINK + + "The newly created Domain Id will be present in the response. " + + "Specify existing Domain Id to update the domain. " + + "Referencing non-existing Domain Id will cause 'Not Found' error." + + "\n\nDomain name is unique for entire platform setup.\n\n" + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PostMapping(value = "/domain") + public Domain saveDomain( + @Parameter(description = "A JSON value representing the Domain.", required = true) + @RequestBody @Valid Domain domain, + @Parameter(description = "A list of oauth2 client registration ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception { + domain.setTenantId(getTenantId()); + checkEntity(domain.getId(), domain, Resource.DOMAIN); + return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser()); + } + + @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", + notes = "Update oauth2 clients for the specified domain. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PutMapping(value = "/domain/{id}/oauth2Clients") + public void updateOauth2Clients(@PathVariable UUID id, + @RequestBody UUID[] clientIds) throws ThingsboardException { + DomainId domainId = new DomainId(id); + Domain domain = checkDomainId(domainId, Operation.WRITE); + List oAuth2ClientIds = getOAuth2ClientIds(clientIds); + tbDomainService.updateOauth2Clients(domain, oAuth2ClientIds, getCurrentUser()); + } + + @ApiOperation(value = "Get Domain infos (getTenantDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @GetMapping(value = "/domain/infos") + public PageData getTenantDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on domain's name") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.DOMAIN, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return domainService.findDomainInfosByTenantId(getTenantId(), pageLink); + } + + @ApiOperation(value = "Get Domain info by Id (getDomainInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @GetMapping(value = "/domain/info/{id}") + public DomainInfo getDomainInfoById(@PathVariable UUID id) throws ThingsboardException { + DomainId domainId = new DomainId(id); + return checkEntityId(domainId, domainService::findDomainInfoById, Operation.READ); + } + + @ApiOperation(value = "Delete Domain by ID (deleteDomain)", + notes = "Deletes Domain by ID. Referencing non-existing domain Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @DeleteMapping(value = "/domain/{id}") + public void deleteDomain(@PathVariable UUID id) throws Exception { + DomainId domainId = new DomainId(id); + Domain domain = checkDomainId(domainId, Operation.DELETE); + tbDomainService.delete(domain, getCurrentUser()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java new file mode 100644 index 0000000000..fba17d7119 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2024 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.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.PathVariable; +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.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.mobile.TbMobileAppService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.List; +import java.util.UUID; + +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class MobileAppController extends BaseController { + + private final TbMobileAppService tbMobileAppService; + + @ApiOperation(value = "Save Or update Mobile app (saveMobileApp)", + notes = "Create or update the Mobile app. When creating mobile app, platform generates Mobile App Id as " + UUID_WIKI_LINK + + "The newly created Mobile App Id will be present in the response. " + + "Specify existing Mobile App Id to update the mobile app. " + + "Referencing non-existing Mobile App Id will cause 'Not Found' error." + + "\n\nMobile app package name is unique for entire platform setup.\n\n" + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PostMapping(value = "/mobileApp") + public MobileApp saveMobileApp( + @Parameter(description = "A JSON value representing the Mobile Application.", required = true) + @RequestBody @Valid MobileApp mobileApp, + @Parameter(description = "A list of entity oauth2 client ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception { + mobileApp.setTenantId(getTenantId()); + checkEntity(mobileApp.getId(), mobileApp, Resource.MOBILE_APP); + return tbMobileAppService.save(mobileApp, getOAuth2ClientIds(ids), getCurrentUser()); + } + + @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", + notes = "Update oauth2 clients of the specified mobile app. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PutMapping(value = "/mobileApp/{id}/oauth2Clients") + public void updateOauth2Clients(@PathVariable UUID id, + @RequestBody UUID[] clientIds) throws ThingsboardException { + MobileAppId mobileAppId = new MobileAppId(id); + MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.WRITE); + List oAuth2ClientIds = getOAuth2ClientIds(clientIds); + tbMobileAppService.updateOauth2Clients(mobileApp, oAuth2ClientIds, getCurrentUser()); + } + + @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @GetMapping(value = "/mobileApp/infos") + public PageData getTenantMobileAppInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on app's name") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return mobileAppService.findMobileAppInfosByTenantId(getTenantId(), pageLink); + } + + @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @GetMapping(value = "/mobileApp/info/{id}") + public MobileAppInfo getMobileAppInfoById(@PathVariable UUID id) throws ThingsboardException { + MobileAppId mobileAppId = new MobileAppId(id); + return checkEntityId(mobileAppId, mobileAppService::findMobileAppInfoById, Operation.READ); + } + + @ApiOperation(value = "Delete Mobile App by ID (deleteMobileApp)", + notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @DeleteMapping(value = "/mobileApp/{id}") + public void deleteMobileApp(@PathVariable UUID id) throws Exception { + MobileAppId mobileAppId = new MobileAppId(id); + MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.DELETE); + tbMobileAppService.delete(mobileApp, getCurrentUser()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 325cc3b025..261c4705a9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -16,60 +16,75 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; 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.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.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.oauth2client.TbOauth2ClientService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.utils.MiscUtils; +import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.UUID; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @RequestMapping("/api") +@RequiredArgsConstructor @Slf4j public class OAuth2Controller extends BaseController { - @Autowired - private OAuth2Configuration oAuth2Configuration; + private final OAuth2Configuration oAuth2Configuration; + + private final TbOauth2ClientService tbOauth2ClientService; @ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " + "to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " + "the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)") - @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) - @ResponseBody - public List getOAuth2Clients(HttpServletRequest request, - @Parameter(description = "Mobile application package name, to find OAuth2 clients " + - "where there is configured mobile application with such package name") - @RequestParam(required = false) String pkgName, - @Parameter(description = "Platform type to search OAuth2 clients for which " + - "the usage with this platform type is allowed in the settings. " + - "If platform type is not one of allowable values - it will just be ignored", - schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"})) - @RequestParam(required = false) String platform) throws ThingsboardException { + @PostMapping(value = "/noauth/oauth2/client") + public List getOAuth2Clients(HttpServletRequest request, + @Parameter(description = "Mobile application package name, to find OAuth2 clients " + + "where there is configured mobile application with such package name") + @RequestParam(required = false) String pkgName, + @Parameter(description = "Platform type to search OAuth2 clients for which " + + "the usage with this platform type is allowed in the settings. " + + "If platform type is not one of allowable values - it will just be ignored", + schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"})) + @RequestParam(required = false) String platform) { if (log.isDebugEnabled()) { log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); Enumeration headerNames = request.getHeaderNames(); @@ -80,31 +95,70 @@ public class OAuth2Controller extends BaseController { } PlatformType platformType = null; if (StringUtils.isNotEmpty(platform)) { - try { - platformType = PlatformType.valueOf(platform); - } catch (Exception e) { - } + platformType = PlatformType.valueOf(platform); + } + if (StringUtils.isNotEmpty(pkgName)) { + return oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platformType); + } else { + return oAuth2ClientService.findOAuth2ClientLoginInfosByDomainName(MiscUtils.getDomainNameAndPort(request)); } - return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request), pkgName, platformType); } - @ApiOperation(value = "Get current OAuth2 settings (getCurrentOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Save OAuth2 Client (saveOAuth2Client)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @PostMapping(value = "/oauth2/client") + public OAuth2Client saveOAuth2Client(@RequestBody @Valid OAuth2Client oAuth2Client) throws Exception { + TenantId tenantId = getTenantId(); + oAuth2Client.setTenantId(tenantId); + checkEntity(oAuth2Client.getId(), oAuth2Client, Resource.OAUTH2_CLIENT); + return tbOauth2ClientService.save(oAuth2Client, getCurrentUser()); + } + + @ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json") - @ResponseBody - public OAuth2Info getCurrentOAuth2Info() throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); - return oAuth2Service.findOAuth2Info(); + @GetMapping(value = "/oauth2/client/infos") + public PageData findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on client's title") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } - @ApiOperation(value = "Save OAuth2 settings (saveOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", + notes = "Fetch OAuth2 Client info objects based on the provided ids. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST) - @ResponseStatus(value = HttpStatus.OK) - public OAuth2Info saveOAuth2Info(@RequestBody OAuth2Info oauth2Info) throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE); - oAuth2Service.saveOAuth2Info(oauth2Info); - return oAuth2Service.findOAuth2Info(); + @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) + public List findTenantOAuth2ClientInfosByIds( + @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { + List oAuth2ClientIds = getOAuth2ClientIds(clientIds); + return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); + } + + @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @GetMapping(value = "/oauth2/client/{id}") + public OAuth2Client getOAuth2ClientById(@PathVariable UUID id) throws ThingsboardException { + OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id); + return checkEntityId(oAuth2ClientId, oAuth2ClientService::findOAuth2ClientById, Operation.READ); + } + + @ApiOperation(value = "Delete oauth2 client (deleteOauth2Client)", + notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @DeleteMapping(value = "/oauth2/client/{id}") + public void deleteOauth2Client(@PathVariable UUID id) throws Exception { + OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id); + OAuth2Client oAuth2Client = checkOauth2ClientId(oAuth2ClientId, Operation.DELETE); + tbOauth2ClientService.delete(oAuth2Client, getCurrentUser()); } @ApiOperation(value = "Get OAuth2 log in processing URL (getLoginProcessingUrl)", notes = "Returns the URL enclosed in " + @@ -112,10 +166,9 @@ public class OAuth2Controller extends BaseController { "further log in processing. This URL may be configured as 'security.oauth2.loginProcessingUrl' property in yml configuration file, or " + "as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/oauth2/loginProcessingUrl") public String getLoginProcessingUrl() throws ThingsboardException { - accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ); return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index f29255e414..740b19e5d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -201,7 +201,7 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE -> notificationEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); - case OAUTH2 -> oAuth2EdgeProcessor.processOAuth2Notification(tenantId, edgeNotificationMsg); + case DOMAIN, OAUTH2_CLIENT -> oAuth2EdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg); default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type); } } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 489fc6b6b8..2d9fa01796 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -29,13 +29,13 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.resource.ResourceService; @@ -167,7 +167,7 @@ public class EdgeContextComponent { private NotificationTemplateService notificationTemplateService; @Autowired - private OAuth2Service oAuth2Service; + private DomainService domainService; @Autowired private RateLimitService rateLimitService; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 0eb128d0b7..f36f9ee2c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -32,10 +32,10 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -204,11 +204,12 @@ public class EdgeEventSourcingListener { return !event.getCreated(); case API_USAGE_STATE, EDGE: return false; + case DOMAIN: + if (entity instanceof Domain domain) { + return domain.isPropagateToEdge(); + } } } - if (entity instanceof OAuth2Info oAuth2Info) { - return oAuth2Info.isEdgeEnabled(); - } // Default: If the entity doesn't match any of the conditions, consider it as valid. return true; } @@ -233,8 +234,6 @@ public class EdgeEventSourcingListener { private EdgeEventType getEdgeEventTypeForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return EdgeEventType.ALARM_COMMENT; - } else if (entity instanceof OAuth2Info) { - return EdgeEventType.OAUTH2; } return null; } @@ -242,8 +241,6 @@ public class EdgeEventSourcingListener { private String getBodyMsgForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return JacksonUtil.toString(entity); - } else if (entity instanceof OAuth2Info) { - return JacksonUtil.toString(entity); } return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 97b28c083f..c4e8f1ec7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -698,8 +698,10 @@ public final class EdgeGrpcSession implements Closeable { return ctx.getNotificationEdgeProcessor().convertNotificationTargetToDownlink(edgeEvent); case NOTIFICATION_TEMPLATE: return ctx.getNotificationEdgeProcessor().convertNotificationTemplateToDownlink(edgeEvent); - case OAUTH2: - return ctx.getOAuth2EdgeProcessor().convertOAuth2EventToDownlink(edgeEvent); + case OAUTH2_CLIENT: + return ctx.getOAuth2EdgeProcessor().convertOAuth2ClientEventToDownlink(edgeEvent, this.edgeVersion); + case DOMAIN: + return ctx.getOAuth2EdgeProcessor().convertOAuth2DomainEventToDownlink(edgeEvent, this.edgeVersion); default: log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent); return null; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 125fd48463..7426207a56 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -88,7 +88,7 @@ public class EdgeSyncCursor { fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); - fetchers.add(new OAuth2EdgeEventFetcher(ctx.getOAuth2Service())); + fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java index c628b042a2..7ed25924b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java @@ -17,16 +17,44 @@ package org.thingsboard.server.service.edge.rpc.constructor.oauth2; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @Component @TbCoreComponent public class OAuth2MsgConstructor { - public OAuth2UpdateMsg constructOAuth2UpdateMsg(OAuth2Info oAuth2Info) { - return OAuth2UpdateMsg.newBuilder().setEntity(JacksonUtil.toString(oAuth2Info)).build(); + public OAuth2ClientUpdateMsg constructOAuth2ClientUpdateMsg(UpdateMsgType msgType, OAuth2Client oAuth2Client) { + return OAuth2ClientUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(oAuth2Client)) + .setIdMSB(oAuth2Client.getId().getId().getMostSignificantBits()) + .setIdLSB(oAuth2Client.getId().getId().getLeastSignificantBits()).build(); + } + + public OAuth2ClientUpdateMsg constructOAuth2ClientDeleteMsg(OAuth2ClientId oAuth2ClientId) { + return OAuth2ClientUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(oAuth2ClientId.getId().getMostSignificantBits()) + .setIdLSB(oAuth2ClientId.getId().getLeastSignificantBits()).build(); + } + + public OAuth2DomainUpdateMsg constructOAuth2DomainUpdateMsg(UpdateMsgType msgType, DomainInfo domainInfo) { + return OAuth2DomainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(domainInfo)) + .setIdMSB(domainInfo.getId().getId().getMostSignificantBits()) + .setIdLSB(domainInfo.getId().getId().getLeastSignificantBits()).build(); + } + + public OAuth2DomainUpdateMsg constructOAuth2DomainDeleteMsg(DomainId domainId) { + return OAuth2DomainUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(domainId.getId().getMostSignificantBits()) + .setIdLSB(domainId.getId().getLeastSignificantBits()) + .build(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java index 982c1a8eaf..c93135be37 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java @@ -17,43 +17,32 @@ package org.thingsboard.server.service.edge.rpc.fetch; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; 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.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.oauth2.OAuth2Service; - -import java.util.ArrayList; -import java.util.List; +import org.thingsboard.server.dao.domain.DomainService; @AllArgsConstructor @Slf4j -public class OAuth2EdgeEventFetcher implements EdgeEventFetcher { +public class OAuth2EdgeEventFetcher extends BasePageableEdgeEventFetcher { - private final OAuth2Service oAuth2Service; + private final DomainService domainService; @Override - public PageLink getPageLink(int pageSize) { - return null; + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return domainService.findDomainInfosByTenantId(TenantId.SYS_TENANT_ID, pageLink); } @Override - public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { - OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info(); - if (!oAuth2Info.isEdgeEnabled()) { - return new PageData<>(); - } - List result = new ArrayList<>(); - result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2, - EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info))); - // returns PageData object to be in sync with other fetchers - return new PageData<>(result, 1, result.size(), false); + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, DomainInfo domainInfo) { + return EdgeUtils.constructEdgeEvent(TenantId.SYS_TENANT_ID, edge.getId(), EdgeEventType.DOMAIN, + EdgeEventActionType.ADDED, domainInfo.getId(), null); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index b1c1fc63c5..bb1d6383e1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -69,6 +69,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; @@ -76,7 +77,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -240,7 +241,10 @@ public abstract class BaseEdgeProcessor { protected NotificationTemplateService notificationTemplateService; @Autowired - protected OAuth2Service oAuth2Service; + protected OAuth2ClientService oAuth2ClientService; + + @Autowired + protected DomainService domainService; @Autowired @Lazy diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index f448766994..a55dd77e01 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -216,6 +216,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED: case UPDATED: @@ -233,24 +234,20 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements .addDeviceUpdateMsg(deviceUpdateMsg); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); if (deviceCredentials != null) { - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); } if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); - builder.addDeviceProfileUpdateMsg(((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); + builder.addDeviceProfileUpdateMsg(msgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); } downlinkMsg = builder.build(); } break; case DELETED: case UNASSIGNED_FROM_EDGE: - DeviceUpdateMsg deviceUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceDeleteMsg(deviceId); + DeviceUpdateMsg deviceUpdateMsg = msgConstructor.constructDeviceDeleteMsg(deviceId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg) @@ -259,8 +256,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements case CREDENTIALS_UPDATED: DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); if (deviceCredentials != null) { - DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg) @@ -270,11 +266,10 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements case RPC_CALL: return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceRpcCallMsg(((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) - .constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) + .addDeviceRpcCallMsg(msgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) .build(); } return downlinkMsg; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 464fa3ba54..f547cbc3a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -96,14 +96,14 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; + var msgConstructor = (DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = msgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) @@ -111,8 +111,7 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces } } case DELETED -> { - DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor) - deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileDeleteMsg(deviceProfileId); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = msgConstructor.constructDeviceProfileDeleteMsg(deviceProfileId); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceProfileUpdateMsg(deviceProfileUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java index adfb6f7d9f..999defcd02 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java @@ -15,49 +15,95 @@ */ package org.thingsboard.server.service.edge.rpc.processor.oauth2; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.domain.DomainInfo; 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.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; @Slf4j @Component @TbCoreComponent public class OAuth2EdgeProcessor extends BaseEdgeProcessor { - public DownlinkMsg convertOAuth2EventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertOAuth2DomainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) { + return null; + } + DomainId domainId = new DomainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; - OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class); - if (oAuth2Info != null && oAuth2Info.isEdgeEnabled()) { - OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info); - downlinkMsg = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addOAuth2UpdateMsg(oAuth2UpdateMsg) - .build(); + + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + DomainInfo domainInfo = domainService.findDomainInfoById(edgeEvent.getTenantId(), domainId); + if (domainInfo != null && domainInfo.isPropagateToEdge()) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = oAuth2MsgConstructor.constructOAuth2DomainUpdateMsg(msgType, domainInfo); + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2DomainUpdateMsg(oAuth2DomainUpdateMsg); + domainInfo.getOauth2ClientInfos().forEach(clientInfo -> { + OAuth2Client oauth2Client = oAuth2ClientService.findOAuth2ClientById(edgeEvent.getTenantId(), clientInfo.getId()); + OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = oAuth2MsgConstructor.constructOAuth2ClientUpdateMsg(msgType, oauth2Client); + builder.addOAuth2ClientUpdateMsg(oAuth2ClientUpdateMsg); + }); + downlinkMsg = builder.build(); + } + } + case DELETED -> { + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = oAuth2MsgConstructor.constructOAuth2DomainDeleteMsg(domainId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2DomainUpdateMsg(oAuth2DomainUpdateMsg) + .build(); + } } return downlinkMsg; } - public ListenableFuture processOAuth2Notification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { - OAuth2Info oAuth2Info = JacksonUtil.fromString(edgeNotificationMsg.getBody(), OAuth2Info.class); - if (oAuth2Info == null) { - return Futures.immediateFuture(null); + public DownlinkMsg convertOAuth2ClientEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) { + return null; + } + OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + boolean isPropagateToEdge = oAuth2ClientService.isPropagateOAuth2ClientToEdge(edgeEvent.getTenantId(), oAuth2ClientId); + if (!isPropagateToEdge) { + return null; + } + OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(edgeEvent.getTenantId(), oAuth2ClientId); + if (oAuth2Client != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = oAuth2MsgConstructor.constructOAuth2ClientUpdateMsg(msgType, oAuth2Client); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2ClientUpdateMsg(oAuth2ClientUpdateMsg) + .build(); + } + } + case DELETED -> { + OAuth2ClientUpdateMsg oAuth2ClientDeleteMsg = oAuth2MsgConstructor.constructOAuth2ClientDeleteMsg(oAuth2ClientId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addOAuth2ClientUpdateMsg(oAuth2ClientDeleteMsg) + .build(); + } } - EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); - EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); - return processActionForAllEdges(tenantId, type, actionType, null, JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()), null); + return downlinkMsg; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java new file mode 100644 index 0000000000..dc876c2cfa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2024 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.entitiy.domain; + +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.domain.DomainService; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +import java.util.List; + +@Service +@AllArgsConstructor +public class DefaultTbDomainService extends AbstractTbEntityService implements TbDomainService { + + private final DomainService domainService; + + @Override + public Domain save(Domain domain, List oAuth2Clients, User user) throws Exception { + ActionType actionType = domain.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = domain.getTenantId(); + try { + Domain savedDomain = checkNotNull(domainService.saveDomain(tenantId, domain)); + if (CollectionUtils.isNotEmpty(oAuth2Clients)) { + domainService.updateOauth2Clients(domain.getTenantId(), savedDomain.getId(), oAuth2Clients); + } + logEntityActionService.logEntityAction(tenantId, savedDomain.getId(), savedDomain, actionType, user, oAuth2Clients); + return savedDomain; + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.DOMAIN), domain, actionType, user, e, oAuth2Clients); + throw e; + } + } + + @Override + public void updateOauth2Clients(Domain domain, List oAuth2ClientIds, User user) { + ActionType actionType = ActionType.UPDATED; + TenantId tenantId = domain.getTenantId(); + DomainId domainId = domain.getId(); + try { + domainService.updateOauth2Clients(tenantId, domainId, oAuth2ClientIds); + logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, oAuth2ClientIds); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, e, oAuth2ClientIds); + throw e; + } + } + + @Override + public void delete(Domain domain, User user) { + ActionType actionType = ActionType.DELETED; + TenantId tenantId = domain.getTenantId(); + DomainId domainId = domain.getId(); + try { + domainService.deleteDomainById(tenantId, domainId); + logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, e); + throw e; + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/TbDomainService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/TbDomainService.java new file mode 100644 index 0000000000..744005ff36 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/TbDomainService.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 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.entitiy.domain; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.id.OAuth2ClientId; + +import java.util.List; + +public interface TbDomainService { + + Domain save(Domain domain, List oAuth2Clients, User user) throws Exception; + + void updateOauth2Clients(Domain domain, List oAuth2ClientIds, User user); + + void delete(Domain domain, User user); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java new file mode 100644 index 0000000000..8dbef1fc1e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2024 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.entitiy.mobile; + +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.dao.mobile.MobileAppService; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +import java.util.List; + +@Service +@AllArgsConstructor +public class DefaultTbMobileAppService extends AbstractTbEntityService implements TbMobileAppService { + + private final MobileAppService mobileAppService; + + @Override + public MobileApp save(MobileApp mobileApp, List oauth2Clients, User user) throws Exception { + ActionType actionType = mobileApp.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = mobileApp.getTenantId(); + try { + MobileApp savedMobileApp = checkNotNull(mobileAppService.saveMobileApp(tenantId, mobileApp)); + if (CollectionUtils.isNotEmpty(oauth2Clients)) { + mobileAppService.updateOauth2Clients(tenantId, savedMobileApp.getId(), oauth2Clients); + } + logEntityActionService.logEntityAction(tenantId, savedMobileApp.getId(), savedMobileApp, actionType, user); + return savedMobileApp; + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.MOBILE_APP), mobileApp, actionType, user, e); + throw e; + } + } + + @Override + public void updateOauth2Clients(MobileApp mobileApp, List oAuth2ClientIds, User user) { + ActionType actionType = ActionType.UPDATED; + TenantId tenantId = mobileApp.getTenantId(); + MobileAppId mobileAppId = mobileApp.getId(); + try { + mobileAppService.updateOauth2Clients(tenantId, mobileAppId, oAuth2ClientIds); + logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, oAuth2ClientIds); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, e, oAuth2ClientIds); + throw e; + } + } + + @Override + public void delete(MobileApp mobileApp, User user) { + ActionType actionType = ActionType.DELETED; + TenantId tenantId = mobileApp.getTenantId(); + MobileAppId mobileAppId = mobileApp.getId(); + try { + mobileAppService.deleteMobileAppById(tenantId, mobileAppId); + logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, e); + throw e; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java new file mode 100644 index 0000000000..bcc9cf7004 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 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.entitiy.mobile; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.mobile.MobileApp; + +import java.util.List; + +public interface TbMobileAppService { + + MobileApp save(MobileApp mobileApp, List oauth2Clients, User user) throws Exception; + + void updateOauth2Clients(MobileApp mobileApp, List oAuth2ClientIds, User user); + + void delete(MobileApp mobileApp, User user); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java new file mode 100644 index 0000000000..999bf3976c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 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.entitiy.oauth2client; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +@Service +@AllArgsConstructor +public class DefaultTbOauth2ClientService extends AbstractTbEntityService implements TbOauth2ClientService { + + private final OAuth2ClientService oAuth2ClientService; + + @Override + public OAuth2Client save(OAuth2Client oAuth2Client, User user) throws Exception { + ActionType actionType = oAuth2Client.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = oAuth2Client.getTenantId(); + try { + OAuth2Client savedClient = checkNotNull(oAuth2ClientService.saveOAuth2Client(tenantId, oAuth2Client)); + logEntityActionService.logEntityAction(tenantId, savedClient.getId(), savedClient, actionType, user); + return savedClient; + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.OAUTH2_CLIENT), oAuth2Client, actionType, user, e); + throw e; + } + } + + @Override + public void delete(OAuth2Client oAuth2Client, User user) { + ActionType actionType = ActionType.DELETED; + TenantId tenantId = oAuth2Client.getTenantId(); + OAuth2ClientId oAuth2ClientId = oAuth2Client.getId(); + try { + oAuth2ClientService.deleteOAuth2ClientById(tenantId, oAuth2ClientId); + logEntityActionService.logEntityAction(tenantId, oAuth2ClientId, oAuth2Client, actionType, user); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, oAuth2ClientId, oAuth2Client, actionType, user, e); + throw e; + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2MobileDao.java b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/TbOauth2ClientService.java similarity index 64% rename from dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2MobileDao.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/TbOauth2ClientService.java index ab1b981dc2..c4ac3c7839 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2MobileDao.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/TbOauth2ClientService.java @@ -13,16 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.oauth2; +package org.thingsboard.server.service.entitiy.oauth2client; -import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; -import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; -import java.util.List; -import java.util.UUID; +public interface TbOauth2ClientService { -public interface OAuth2MobileDao extends Dao { + OAuth2Client save(OAuth2Client oAuth2Client, User user) throws Exception; - List findByOAuth2ParamsId(UUID oauth2ParamsId); + void delete(OAuth2Client oAuth2Client, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index e49a42b57b..52ef9b203f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -69,7 +69,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; @@ -100,7 +100,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationTargetService; -import org.thingsboard.server.dao.oauth2.OAuth2MobileDao; +import org.thingsboard.server.dao.mobile.MobileAppDao; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -149,7 +149,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private final DeviceConnectivityConfiguration connectivityConfiguration; private final QueueService queueService; private final JwtSettingsService jwtSettingsService; - private final OAuth2MobileDao oAuth2MobileDao; + private final MobileAppDao mobileAppDao; private final NotificationSettingsService notificationSettingsService; private final NotificationTargetService notificationTargetService; @@ -308,17 +308,17 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { jwtSettingsService.saveJwtSettings(jwtSettings); } - List mobiles = oAuth2MobileDao.find(TenantId.SYS_TENANT_ID); + List mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, new PageLink(Integer.MAX_VALUE,0)).getData(); if (CollectionUtils.isNotEmpty(mobiles)) { mobiles.stream() - .filter(config -> !validateKeyLength(config.getAppSecret())) - .forEach(config -> { + .filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret())) + .forEach(mobileApp -> { log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " + "A new Application Secret has been added automatically for Mobile Application [{}]. " + "You can change the Application Secret using the Web UI: " + - "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", config.getPkgName()); - config.setAppSecret(generateRandomKey()); - oAuth2MobileDao.save(TenantId.SYS_TENANT_ID, config); + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); + mobileApp.setAppSecret(generateRandomKey()); + mobileAppDao.save(TenantId.SYS_TENANT_ID, mobileApp); }); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 70a39f0851..7699a4f7c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -614,13 +614,11 @@ public class DefaultTbClusterService implements TbClusterService { private void pushDeviceUpdateMessage(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { log.trace("{} Going to send edge update notification for device actor, device id {}, edge id {}", tenantId, entityId, edgeId); switch (action) { - case ASSIGNED_TO_EDGE: - pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), edgeId), null); - break; - case UNASSIGNED_FROM_EDGE: + case ASSIGNED_TO_EDGE -> pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), edgeId), null); + case UNASSIGNED_FROM_EDGE -> { EdgeId relatedEdgeId = findRelatedEdgeIdIfAny(tenantId, entityId); pushMsgToCore(new DeviceEdgeUpdateMsg(tenantId, new DeviceId(entityId.getId()), relatedEdgeId), null); - break; + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 1dc42218fb..4462183baa 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -108,7 +108,6 @@ import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpd import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -701,13 +700,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg, TbCallback callback) { - if (actorMsg.isPresent()) { - forwardToAppActor(id, actorMsg.get()); - } - callback.onSuccess(); - } - private void forwardToAppActor(UUID id, TbActorMsg actorMsg) { log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); actorContext.tell(actorMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index 917b0f4f82..a4960182b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; @@ -97,9 +97,9 @@ public abstract class AbstractOAuth2ClientMapper { private final Lock userCreationLock = new ReentrantLock(); - protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2Registration registration) { + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2Client oAuth2Client) { - OAuth2MapperConfig config = registration.getMapperConfig(); + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); @@ -143,9 +143,9 @@ public abstract class AbstractOAuth2ClientMapper { } } - if (registration.getAdditionalInfo() != null && - registration.getAdditionalInfo().has("providerName")) { - additionalInfo.put("authProviderName", registration.getAdditionalInfo().get("providerName").asText()); + if (oAuth2Client.getAdditionalInfo() != null && + oAuth2Client.getAdditionalInfo().has("providerName")) { + additionalInfo.put("authProviderName", oAuth2Client.getAdditionalInfo().get("providerName").asText()); } user.setAdditionalInfo(additionalInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java index 23a60f45b6..2e3b8c5586 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java @@ -25,7 +25,7 @@ import org.springframework.util.MultiValueMap; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; @@ -45,13 +45,13 @@ public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen private static final String EMAIL = "email"; @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client) { + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); Map attributes = updateAttributesFromRequestParams(request, token.getPrincipal().getAttributes()); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, oAuth2Client); } private static Map updateAttributesFromRequestParams(HttpServletRequest request, Map attributes) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java index 02fb9c6258..cad0eecfcd 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java @@ -20,7 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; @@ -33,12 +33,12 @@ import java.util.Map; public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client) { + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); Map attributes = token.getPrincipal().getAttributes(); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, oAuth2Client); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index e6cfb5a32a..e9c2b9583d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -26,7 +26,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; @@ -41,10 +41,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client auth2Client) { + OAuth2MapperConfig config = auth2Client.getMapperConfig(); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, auth2Client); } private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java index 5619788a21..889b7a828d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java @@ -25,7 +25,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -49,13 +49,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private OAuth2Configuration oAuth2Configuration; @Override - public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { - OAuth2MapperConfig config = registration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client) { + OAuth2MapperConfig config = oAuth2Client.getMapperConfig(); Map githubMapperConfig = oAuth2Configuration.getGithubMapper(); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); Map attributes = token.getPrincipal().getAttributes(); OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oAuth2User, registration); + return getOrCreateSecurityUserFromOAuth2User(oAuth2User, oAuth2Client); } private synchronized String getEmail(String emailUrl, String oauth2Token) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java index a27ac410c1..d65f3f66ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java @@ -17,9 +17,9 @@ package org.thingsboard.server.service.security.auth.oauth2; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.service.security.model.SecurityUser; public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration); + SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 4605819894..b85e618a6e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -31,10 +31,11 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.security.model.JwtPair; -import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.SecurityUser; @@ -56,7 +57,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS private final JwtTokenFactory tokenFactory; private final OAuth2ClientMapperProvider oauth2ClientMapperProvider; - private final OAuth2Service oAuth2Service; + private final OAuth2ClientService oAuth2ClientService; private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; private final SystemSecurityService systemSecurityService; @@ -64,13 +65,13 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS @Autowired public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory, final OAuth2ClientMapperProvider oauth2ClientMapperProvider, - final OAuth2Service oAuth2Service, + final OAuth2ClientService oAuth2ClientService, final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository, final SystemSecurityService systemSecurityService) { this.tokenFactory = tokenFactory; this.oauth2ClientMapperProvider = oauth2ClientMapperProvider; - this.oAuth2Service = oAuth2Service; + this.oAuth2ClientService = oAuth2ClientService; this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository; this.systemSecurityService = systemSecurityService; @@ -96,19 +97,19 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS try { OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; - OAuth2Registration registration = oAuth2Service.findRegistration(UUID.fromString(token.getAuthorizedClientRegistrationId())); + OAuth2Client oauth2Client = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(token.getAuthorizedClientRegistrationId()))); OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient( token.getAuthorizedClientRegistrationId(), token.getPrincipal().getName()); - OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType()); + OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType()); SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), - registration); + oauth2Client); clearAuthenticationAttributes(request, response); JwtPair tokenPair = tokenFactory.createTokenPair(securityUser); getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(baseUrl, tokenPair)); - systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, registration.getName(), null); + systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, oauth2Client.getName(), null); } catch (Exception e) { log.debug("Error occurred during processing authentication success result. " + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index d43663fb97..16a3c4be59 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -33,7 +33,9 @@ public enum Resource { USER(EntityType.USER), WIDGETS_BUNDLE(EntityType.WIDGETS_BUNDLE), WIDGET_TYPE(EntityType.WIDGET_TYPE), - OAUTH2_CONFIGURATION_INFO(), + OAUTH2_CLIENT(EntityType.OAUTH2_CLIENT), + DOMAIN(EntityType.DOMAIN), + MOBILE_APP(EntityType.MOBILE_APP), OAUTH2_CONFIGURATION_TEMPLATE(), TENANT_PROFILE(EntityType.TENANT_PROFILE), DEVICE_PROFILE(EntityType.DEVICE_PROFILE), diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index d906938e47..4790a07965 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -35,7 +35,9 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker); put(Resource.WIDGET_TYPE, systemEntityPermissionChecker); - put(Resource.OAUTH2_CONFIGURATION_INFO, PermissionChecker.allowAllPermissionChecker); + put(Resource.OAUTH2_CLIENT, PermissionChecker.allowAllPermissionChecker); + put(Resource.MOBILE_APP, PermissionChecker.allowAllPermissionChecker); + put(Resource.DOMAIN, PermissionChecker.allowAllPermissionChecker); put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); put(Resource.TB_RESOURCE, systemEntityPermissionChecker); diff --git a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java index 55319f1218..46e28c3632 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.stats.TbApiUsageStateClient; -import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; import org.thingsboard.server.queue.discovery.DiscoveryService; @@ -89,7 +89,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener platforms) { + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTenantId(tenantId); + oAuth2Client.setTitle(title); + oAuth2Client.setClientId(UUID.randomUUID().toString()); + oAuth2Client.setClientSecret(UUID.randomUUID().toString()); + oAuth2Client.setAuthorizationUri(UUID.randomUUID().toString()); + oAuth2Client.setAccessTokenUri(UUID.randomUUID().toString()); + oAuth2Client.setScope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + oAuth2Client.setPlatforms(platforms == null ? Collections.emptyList() : platforms); + oAuth2Client.setUserInfoUri(UUID.randomUUID().toString()); + oAuth2Client.setUserNameAttributeName(UUID.randomUUID().toString()); + oAuth2Client.setJwkSetUri(UUID.randomUUID().toString()); + oAuth2Client.setClientAuthenticationMethod(UUID.randomUUID().toString()); + oAuth2Client.setLoginButtonLabel(UUID.randomUUID().toString()); + oAuth2Client.setLoginButtonIcon(UUID.randomUUID().toString()); + oAuth2Client.setAdditionalInfo(JacksonUtil.newObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + oAuth2Client.setMapperConfig( + OAuth2MapperConfig.builder() + .allowUserCreation(true) + .activateUser(true) + .type(MapperType.CUSTOM) + .custom( + OAuth2CustomMapperConfig.builder() + .url(UUID.randomUUID().toString()) + .build() + ) + .build()); + return oAuth2Client; + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java new file mode 100644 index 0000000000..c899aeeddb --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java @@ -0,0 +1,139 @@ +/** + * Copyright © 2016-2024 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.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class DomainControllerTest extends AbstractControllerTest { + + static final TypeReference> PAGE_DATA_DOMAIN_TYPE_REF = new TypeReference<>() { + }; + static final TypeReference> PAGE_DATA_OAUTH2_CLIENT_TYPE_REF = new TypeReference<>() { + }; + + @Before + public void setUp() throws Exception { + loginSysAdmin(); + } + + @After + public void tearDown() throws Exception { + PageData pageData = doGetTypedWithPageLink("/api/domain/infos?", PAGE_DATA_DOMAIN_TYPE_REF, new PageLink(10, 0)); + for (Domain domain : pageData.getData()) { + doDelete("/api/domain/" + domain.getId().getId()) + .andExpect(status().isOk()); + } + + PageData clients = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0)); + for (OAuth2ClientInfo oAuth2ClientInfo : clients.getData()) { + doDelete("/api/oauth2/client/" + oAuth2ClientInfo.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + @Test + public void testSaveDomain() throws Exception { + PageData pageData = doGetTypedWithPageLink("/api/domain/infos?", PAGE_DATA_DOMAIN_TYPE_REF, new PageLink(10, 0)); + assertThat(pageData.getData()).isEmpty(); + + Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "my.test.domain", true, true); + Domain savedDomain = doPost("/api/domain", domain, Domain.class); + + PageData pageData2 = doGetTypedWithPageLink("/api/domain/infos?", PAGE_DATA_DOMAIN_TYPE_REF, new PageLink(10, 0)); + assertThat(pageData2.getData()).hasSize(1); + assertThat(pageData2.getData().get(0)).isEqualTo(new DomainInfo(savedDomain, Collections.emptyList())); + + DomainInfo retrievedDomainInfo = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId()); + assertThat(retrievedDomainInfo).isEqualTo(new DomainInfo(savedDomain, Collections.emptyList())); + + doDelete("/api/domain/" + savedDomain.getId().getId()); + doGet("/api/domain/info/{id}", savedDomain.getId().getId()) + .andExpect(status().isNotFound()); + } + + @Test + public void testSaveDomainWithoutName() throws Exception { + Domain domain = constructDomain(TenantId.SYS_TENANT_ID, null, true, true); + doPost("/api/domain", domain) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("name must not be blank"))); + } + + @Test + public void testUpdateDomainOauth2Clients() throws Exception { + Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "my.test.domain", true, true); + Domain savedDomain = doPost("/api/domain", domain, Domain.class); + + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client"); + OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class); + + doPut("/api/domain/" + savedDomain.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId())); + + DomainInfo retrievedDomainInfo = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId()); + assertThat(retrievedDomainInfo).isEqualTo(new DomainInfo(savedDomain, List.of(new OAuth2ClientInfo(savedOAuth2Client), + new OAuth2ClientInfo(savedOAuth2Client2)))); + + doPut("/api/domain/" + savedDomain.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId())); + DomainInfo retrievedDomainInfo2 = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId()); + assertThat(retrievedDomainInfo2).isEqualTo(new DomainInfo(savedDomain, List.of(new OAuth2ClientInfo(savedOAuth2Client2)))); + } + + @Test + public void testCreateDomainWithOauth2Clients() throws Exception { + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "my.test.domain", true, true); + Domain savedDomain = doPost("/api/domain?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), domain, Domain.class); + + DomainInfo retrievedDomainInfo = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId()); + assertThat(retrievedDomainInfo).isEqualTo(new DomainInfo(savedDomain, List.of(new OAuth2ClientInfo(savedOAuth2Client)))); + } + + private Domain constructDomain(TenantId tenantId, String domainName, boolean oauth2Enabled, boolean propagateToEdge) { + Domain domain = new Domain(); + domain.setTenantId(tenantId); + domain.setName(domainName); + domain.setOauth2Enabled(oauth2Enabled); + domain.setPropagateToEdge(propagateToEdge); + return domain; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index de530b5268..b636849774 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -81,7 +81,8 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; @@ -892,7 +893,8 @@ public class EdgeControllerTest extends AbstractControllerTest { EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); - edgeImitator.ignoreType(OAuth2UpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); edgeImitator.expectMessageAmount(27); edgeImitator.connect(); diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java index 55af29ab0b..88aa903c76 100644 --- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; @@ -39,15 +38,9 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.MapperType; -import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; -import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; -import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.ApiUsageStateFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -57,6 +50,8 @@ import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.TbApiUsageStateClient; +import org.thingsboard.server.dao.domain.DomainService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -65,10 +60,8 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -86,6 +79,12 @@ public class HomePageApiTest extends AbstractControllerTest { @Autowired private AdminSettingsService adminSettingsService; + @Autowired + private DomainService domainService; + + @Autowired + private OAuth2ClientService oAuth2ClientService; + @MockBean private MailService mailService; @@ -369,9 +368,11 @@ public class HomePageApiTest extends AbstractControllerTest { Assert.assertTrue(featuresInfo.isNotificationEnabled()); Assert.assertFalse(featuresInfo.isOauthEnabled()); - OAuth2Info oAuth2Info = createDefaultOAuth2Info(); + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); - doPost("/api/oauth2/config", oAuth2Info).andExpect(status().isOk()); + Domain domain = createDomain(TenantId.SYS_TENANT_ID, "my.home.domain", true, true); + doPost("/api/domain?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), domain, Domain.class); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); Assert.assertNotNull(featuresInfo); @@ -384,6 +385,8 @@ public class HomePageApiTest extends AbstractControllerTest { adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "notifications"); adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "twoFaSettings"); adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "sms"); + oAuth2ClientService.deleteOauth2ClientsByTenantId(TenantId.SYS_TENANT_ID); + domainService.deleteDomainsByTenantId(TenantId.SYS_TENANT_ID); } @Test @@ -493,43 +496,13 @@ public class HomePageApiTest extends AbstractControllerTest { return doPostWithResponse("/api/entitiesQuery/count", query, Long.class); } - private OAuth2Info createDefaultOAuth2Info() { - return new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("domain").scheme(SchemeType.MIXED).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo() - )) - .build() - )); + private Domain createDomain(TenantId tenantId, String domainName, boolean oauth2Enabled, boolean edgeEnabled) { + Domain domain = new Domain(); + domain.setTenantId(tenantId); + domain.setName(domainName); + domain.setOauth2Enabled(oauth2Enabled); + domain.setPropagateToEdge(edgeEnabled); + return domain; } - private OAuth2RegistrationInfo validRegistrationInfo() { - return OAuth2RegistrationInfo.builder() - .clientId(UUID.randomUUID().toString()) - .clientSecret(UUID.randomUUID().toString()) - .authorizationUri(UUID.randomUUID().toString()) - .accessTokenUri(UUID.randomUUID().toString()) - .scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())) - .platforms(Collections.emptyList()) - .userInfoUri(UUID.randomUUID().toString()) - .userNameAttributeName(UUID.randomUUID().toString()) - .jwkSetUri(UUID.randomUUID().toString()) - .clientAuthenticationMethod(UUID.randomUUID().toString()) - .loginButtonLabel(UUID.randomUUID().toString()) - .mapperConfig( - OAuth2MapperConfig.builder() - .type(MapperType.CUSTOM) - .custom( - OAuth2CustomMapperConfig.builder() - .url(UUID.randomUUID().toString()) - .build() - ) - .build() - ) - .build(); - } } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java new file mode 100644 index 0000000000..d19178242a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java @@ -0,0 +1,141 @@ +/** + * Copyright © 2016-2024 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.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class MobileAppControllerTest extends AbstractControllerTest { + + static final TypeReference> PAGE_DATA_MOBILE_APP_TYPE_REF = new TypeReference<>() { + }; + static final TypeReference> PAGE_DATA_OAUTH2_CLIENT_TYPE_REF = new TypeReference<>() { + }; + + @Before + public void setUp() throws Exception { + loginSysAdmin(); + } + + @After + public void tearDown() throws Exception { + PageData pageData = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + for (MobileApp mobileApp : pageData.getData()) { + doDelete("/api/mobileApp/" + mobileApp.getId().getId()) + .andExpect(status().isOk()); + } + + PageData clients = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0)); + for (OAuth2ClientInfo oAuth2ClientInfo : clients.getData()) { + doDelete("/api/oauth2/client/" + oAuth2ClientInfo.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + @Test + public void testSaveMobileApp() throws Exception { + PageData pageData = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + assertThat(pageData.getData()).isEmpty(); + + MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true); + MobileApp savedMobileApp = doPost("/api/mobileApp", mobileApp, MobileApp.class); + + PageData pageData2 = doGetTypedWithPageLink("/api/mobileApp/infos?", PAGE_DATA_MOBILE_APP_TYPE_REF, new PageLink(10, 0)); + assertThat(pageData2.getData()).hasSize(1); + assertThat(pageData2.getData().get(0)).isEqualTo(new MobileAppInfo(savedMobileApp, Collections.emptyList())); + + MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId()); + assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, Collections.emptyList())); + + doDelete("/api/mobileApp/" + savedMobileApp.getId().getId()); + doGet("/api/mobileApp/info/{id}", savedMobileApp.getId().getId()) + .andExpect(status().isNotFound()); + } + + @Test + public void testSaveMobileAppWithShortAppSecret() throws Exception { + MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce", true); + mobileApp.setAppSecret("short"); + doPost("/api/mobileApp", mobileApp) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("appSecret must be at least 16 and max 2048 characters"))); + } + + @Test + public void testUpdateMobileAppOauth2Clients() throws Exception { + MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true); + MobileApp savedMobileApp = doPost("/api/mobileApp", mobileApp, MobileApp.class); + + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + OAuth2Client oAuth2Client2 = createOauth2Client(TenantId.SYS_TENANT_ID, "test facebook client"); + OAuth2Client savedOAuth2Client2 = doPost("/api/oauth2/client", oAuth2Client2, OAuth2Client.class); + + doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId())); + + MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId()); + assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client), + new OAuth2ClientInfo(savedOAuth2Client2)))); + + doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId())); + MobileAppInfo retrievedMobileAppInfo2 = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId()); + assertThat(retrievedMobileAppInfo2).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client2)))); + } + + @Test + public void testCreateMobileAppWithOauth2Clients() throws Exception { + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + MobileApp mobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.test.package", true); + MobileApp savedMobileApp = doPost("/api/mobileApp?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), mobileApp, MobileApp.class); + + MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId()); + assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client)))); + } + + private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) { + MobileApp MobileApp = new MobileApp(); + MobileApp.setTenantId(tenantId); + MobileApp.setPkgName(mobileAppName); + MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + MobileApp.setOauth2Enabled(oauth2Enabled); + return MobileApp; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/Oauth2ClientControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/Oauth2ClientControllerTest.java new file mode 100644 index 0000000000..4e8ac72676 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/Oauth2ClientControllerTest.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2024 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.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class Oauth2ClientControllerTest extends AbstractControllerTest { + + static final TypeReference> PAGE_DATA_OAUTH2_CLIENT_TYPE_REF = new TypeReference<>() { + }; + + @Before + public void setUp() throws Exception { + loginSysAdmin(); + } + + @After + public void tearDown() throws Exception { + PageData pageData = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0)); + for (OAuth2ClientInfo oAuth2ClientInfo : pageData.getData()) { + doDelete("/api/oauth2/client/" + oAuth2ClientInfo.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + @Test + public void testSaveOauth2Client() throws Exception { + loginSysAdmin(); + PageData pageData = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0)); + assertThat(pageData.getData()).isEmpty(); + + OAuth2Client oAuth2Client = createOauth2Client(TenantId.SYS_TENANT_ID, "test google client"); + OAuth2Client savedOAuth2Client = doPost("/api/oauth2/client", oAuth2Client, OAuth2Client.class); + + PageData pageData2 = doGetTypedWithPageLink("/api/oauth2/client/infos?", PAGE_DATA_OAUTH2_CLIENT_TYPE_REF, new PageLink(10, 0)); + + assertThat(pageData2.getData()).hasSize(1); + assertThat(pageData2.getData().get(0)).isEqualTo(new OAuth2ClientInfo(savedOAuth2Client)); + + OAuth2Client retrievedOAuth2ClientInfo = doGet("/api/oauth2/client/{id}", OAuth2Client.class, savedOAuth2Client.getId().getId()); + assertThat(retrievedOAuth2ClientInfo).isEqualTo(savedOAuth2Client); + + doDelete("/api/oauth2/client/" + savedOAuth2Client.getId().getId()); + doGet("/api/oauth2/client/{id}", savedOAuth2Client.getId().getId()) + .andExpect(status().isNotFound()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index f7a066f7e0..bd8a59a975 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -63,7 +63,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; @@ -87,7 +86,8 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; @@ -144,7 +144,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.ignoreType(OAuth2UpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); edgeImitator.expectMessageAmount(24); edgeImitator.connect(); @@ -547,18 +548,6 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertTrue(customer.isPublic()); } - private void validateOAuth2() throws Exception { - Optional oAuth2UpdateMsgOpt = edgeImitator.findMessageByType(OAuth2UpdateMsg.class); - Assert.assertTrue(oAuth2UpdateMsgOpt.isPresent()); - OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2UpdateMsgOpt.get(); - OAuth2Info oAuth2Info = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true); - Assert.assertNotNull(oAuth2Info); - OAuth2Info auth2Info = doGet("/api/oauth2/config", OAuth2Info.class); - Assert.assertNotNull(auth2Info); - Assert.assertEquals(oAuth2Info, auth2Info); - testAutoGeneratedCodeByProtobuf(oAuth2UpdateMsg); - } - private void validateSyncCompleted() { Optional syncCompletedMsgOpt = edgeImitator.findMessageByType(SyncCompletedMsg.class); Assert.assertTrue(syncCompletedMsgOpt.isPresent()); diff --git a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java index beae36718d..10c6b245a6 100644 --- a/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java @@ -15,94 +15,167 @@ */ package org.thingsboard.server.edge; -import com.google.common.collect.Lists; import com.google.protobuf.AbstractMessage; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; -import org.thingsboard.server.common.data.oauth2.SchemeType; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.util.Arrays; import java.util.Collections; +import java.util.Optional; import java.util.UUID; @DaoSqlTest public class OAuth2EdgeTest extends AbstractEdgeTest { @Test - public void testOAuth2Support() throws Exception { + public void testOAuth2DomainSupport() throws Exception { loginSysAdmin(); - // enable oauth + // enable oauth and save domain edgeImitator.allowIgnoredTypes(); edgeImitator.expectMessageAmount(1); - OAuth2Info oAuth2Info = createDefaultOAuth2Info(); - oAuth2Info = doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class); + + Domain savedDomain = doPost("/api/domain", constructDomain(), Domain.class); Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg); - OAuth2UpdateMsg oAuth2UpdateMsg = (OAuth2UpdateMsg) latestMessage; - OAuth2Info result = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true); - Assert.assertEquals(oAuth2Info, result); + Assert.assertTrue(latestMessage instanceof OAuth2DomainUpdateMsg); + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = (OAuth2DomainUpdateMsg) latestMessage; + Domain result = JacksonUtil.fromString(oAuth2DomainUpdateMsg.getEntity(), Domain.class, true); + Assert.assertEquals(savedDomain, result); - // disable oauth support + // disable oauth support: no update of domain events is sending to Edge edgeImitator.expectMessageAmount(1); - oAuth2Info.setEnabled(false); - oAuth2Info.setEdgeEnabled(false); - doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class); + savedDomain.setPropagateToEdge(false); + doPost("/api/domain", savedDomain, Domain.class); Assert.assertFalse(edgeImitator.waitForMessages(5)); - edgeImitator.ignoreType(OAuth2UpdateMsg.class); + // delete domain + edgeImitator.expectMessageAmount(1); + doDelete("/api/domain/" + savedDomain.getId().getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2DomainUpdateMsg); + oAuth2DomainUpdateMsg = (OAuth2DomainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, oAuth2DomainUpdateMsg.getMsgType()); + Assert.assertEquals(savedDomain.getUuidId().getMostSignificantBits(), oAuth2DomainUpdateMsg.getIdMSB()); + Assert.assertEquals(savedDomain.getUuidId().getLeastSignificantBits(), oAuth2DomainUpdateMsg.getIdLSB()); + + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); + loginTenantAdmin(); + } + + @Test + public void testOAuth2ClientSupport() throws Exception { + loginSysAdmin(); + + // enable oauth and save domain + edgeImitator.allowIgnoredTypes(); + + edgeImitator.expectMessageAmount(2); + OAuth2Client savedOAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "test edge google client"); + savedOAuth2Client = doPost("/api/oauth2/client", savedOAuth2Client, OAuth2Client.class); + Domain savedDomain = doPost("/api/domain?oauth2ClientIds=" + savedOAuth2Client.getId().getId(), constructDomain(), Domain.class); + + Assert.assertTrue(edgeImitator.waitForMessages()); + Optional oAuth2DomainUpdateMsgOpt = edgeImitator.findMessageByType(OAuth2DomainUpdateMsg.class); + Assert.assertTrue(oAuth2DomainUpdateMsgOpt.isPresent()); + Domain result = JacksonUtil.fromString(oAuth2DomainUpdateMsgOpt.get().getEntity(), Domain.class, true); + Assert.assertEquals(savedDomain, result); + + Optional oAuth2ClientUpdateMsgOpt = edgeImitator.findMessageByType(OAuth2ClientUpdateMsg.class); + Assert.assertTrue(oAuth2ClientUpdateMsgOpt.isPresent()); + OAuth2Client clientResult = JacksonUtil.fromString(oAuth2ClientUpdateMsgOpt.get().getEntity(), OAuth2Client.class, true); + Assert.assertEquals(savedOAuth2Client, clientResult); + + // disable oauth support: no update of domain events and client events are sending to Edge + edgeImitator.expectMessageAmount(1); + savedDomain.setPropagateToEdge(false); + doPost("/api/domain", savedDomain, Domain.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + edgeImitator.expectMessageAmount(1); + savedOAuth2Client.setTitle("Updated title"); + doPost("/api/oauth2/client", savedOAuth2Client, OAuth2Client.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + // delete oauth2Client + edgeImitator.expectMessageAmount(1); + doDelete("/api/oauth2/client/" + savedOAuth2Client.getId().getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2ClientUpdateMsg); + OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = (OAuth2ClientUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, oAuth2ClientUpdateMsg.getMsgType()); + Assert.assertEquals(savedOAuth2Client.getUuidId().getMostSignificantBits(), oAuth2ClientUpdateMsg.getIdMSB()); + Assert.assertEquals(savedOAuth2Client.getUuidId().getLeastSignificantBits(), oAuth2ClientUpdateMsg.getIdLSB()); + + // delete domain + edgeImitator.expectMessageAmount(1); + doDelete("/api/domain/" + savedDomain.getId().getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof OAuth2DomainUpdateMsg); + OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = (OAuth2DomainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, oAuth2DomainUpdateMsg.getMsgType()); + Assert.assertEquals(savedDomain.getUuidId().getMostSignificantBits(), oAuth2DomainUpdateMsg.getIdMSB()); + Assert.assertEquals(savedDomain.getUuidId().getLeastSignificantBits(), oAuth2DomainUpdateMsg.getIdLSB()); + + edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); + edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); loginTenantAdmin(); } - private OAuth2Info createDefaultOAuth2Info() { - return new OAuth2Info(true, true, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("domain").scheme(SchemeType.MIXED).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo() - )) - .build() - )); + private OAuth2Client validClientInfo(TenantId tenantId, String title) { + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTenantId(tenantId); + oAuth2Client.setTitle(title); + oAuth2Client.setClientId(UUID.randomUUID().toString()); + oAuth2Client.setClientSecret(UUID.randomUUID().toString()); + oAuth2Client.setAuthorizationUri(UUID.randomUUID().toString()); + oAuth2Client.setAccessTokenUri(UUID.randomUUID().toString()); + oAuth2Client.setScope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + oAuth2Client.setPlatforms(Collections.emptyList()); + oAuth2Client.setUserInfoUri(UUID.randomUUID().toString()); + oAuth2Client.setUserNameAttributeName(UUID.randomUUID().toString()); + oAuth2Client.setJwkSetUri(UUID.randomUUID().toString()); + oAuth2Client.setClientAuthenticationMethod(UUID.randomUUID().toString()); + oAuth2Client.setLoginButtonLabel(UUID.randomUUID().toString()); + oAuth2Client.setLoginButtonIcon(UUID.randomUUID().toString()); + oAuth2Client.setAdditionalInfo(JacksonUtil.newObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + oAuth2Client.setMapperConfig( + OAuth2MapperConfig.builder() + .allowUserCreation(true) + .activateUser(true) + .type(MapperType.CUSTOM) + .custom( + OAuth2CustomMapperConfig.builder() + .url(UUID.randomUUID().toString()) + .build() + ) + .build()); + return oAuth2Client; } - private OAuth2RegistrationInfo validRegistrationInfo() { - return OAuth2RegistrationInfo.builder() - .clientId(UUID.randomUUID().toString()) - .clientSecret(UUID.randomUUID().toString()) - .authorizationUri(UUID.randomUUID().toString()) - .accessTokenUri(UUID.randomUUID().toString()) - .scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())) - .platforms(Collections.emptyList()) - .userInfoUri(UUID.randomUUID().toString()) - .userNameAttributeName(UUID.randomUUID().toString()) - .jwkSetUri(UUID.randomUUID().toString()) - .clientAuthenticationMethod(UUID.randomUUID().toString()) - .loginButtonLabel(UUID.randomUUID().toString()) - .mapperConfig( - OAuth2MapperConfig.builder() - .type(MapperType.CUSTOM) - .custom( - OAuth2CustomMapperConfig.builder() - .url(UUID.randomUUID().toString()) - .build() - ) - .build() - ) - .build(); + private Domain constructDomain() { + Domain domain = new Domain(); + domain.setTenantId(TenantId.SYS_TENANT_ID); + domain.setName("my.edge.domain"); + domain.setOauth2Enabled(true); + domain.setPropagateToEdge(true); + return domain; } } diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 93fea90c5a..fc9eeaa36e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.edge; import com.google.protobuf.AbstractMessage; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -45,6 +46,7 @@ public class UserEdgeTest extends AbstractEdgeTest { private BCryptPasswordEncoder passwordEncoder; @Test + @Ignore("Ignored until fix") public void testCreateUpdateDeleteTenantUser() throws Exception { // create user edgeImitator.expectMessageAmount(4); diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index d470ee686f..45dfed1bcb 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -47,7 +47,8 @@ import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; -import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; +import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; @@ -318,9 +319,14 @@ public class EdgeImitator { result.add(saveDownlinkMsg(resourceUpdateMsg)); } } - if (downlinkMsg.getOAuth2UpdateMsgCount() > 0) { - for (OAuth2UpdateMsg oAuth2UpdateMsg : downlinkMsg.getOAuth2UpdateMsgList()) { - result.add(saveDownlinkMsg(oAuth2UpdateMsg)); + if (downlinkMsg.getOAuth2ClientUpdateMsgCount() > 0) { + for (OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg : downlinkMsg.getOAuth2ClientUpdateMsgList()) { + result.add(saveDownlinkMsg(oAuth2ClientUpdateMsg)); + } + } + if (downlinkMsg.getOAuth2DomainUpdateMsgCount() > 0) { + for (OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg : downlinkMsg.getOAuth2DomainUpdateMsgList()) { + result.add(saveDownlinkMsg(oAuth2DomainUpdateMsg)); } } if (downlinkMsg.getNotificationTemplateUpdateMsgCount() > 0) { diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index 9e4fd65ab4..ac21141b75 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -49,7 +49,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; -import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -257,7 +257,7 @@ public abstract class BaseEdgeProcessorTest { protected ResourceService resourceService; @MockBean - protected OAuth2Service oAuth2Service; + protected OAuth2ClientService oAuth2ClientService; @MockBean @Lazy diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/domain/DomainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/domain/DomainService.java new file mode 100644 index 0000000000..f80e87926e --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/domain/DomainService.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 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.dao.domain; + +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +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.dao.entity.EntityDaoService; + +import java.util.List; + +public interface DomainService extends EntityDaoService { + + Domain saveDomain(TenantId tenantId, Domain domain); + + void deleteDomainById(TenantId tenantId, DomainId domainId); + + Domain findDomainById(TenantId tenantId, DomainId domainId); + + PageData findDomainInfosByTenantId(TenantId tenantId, PageLink pageLink); + + DomainInfo findDomainInfoById(TenantId tenantId, DomainId domainId); + + boolean isOauth2Enabled(TenantId tenantId); + + void updateOauth2Clients(TenantId tenantId, DomainId domainId, List oAuth2ClientIds); + + void deleteDomainsByTenantId(TenantId tenantId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java new file mode 100644 index 0000000000..2976022116 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/mobile/MobileAppService.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 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.dao.mobile; + +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.List; + +public interface MobileAppService extends EntityDaoService { + + MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp); + + void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId); + + MobileApp findMobileAppById(TenantId tenantId, MobileAppId mobileAppId); + + PageData findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink); + + MobileAppInfo findMobileAppInfoById(TenantId tenantId, MobileAppId mobileAppId); + + void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List oAuth2ClientIds); + + void deleteMobileAppsByTenantId(TenantId tenantId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java new file mode 100644 index 0000000000..317221fe0a --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientService.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2024 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.dao.oauth2; + +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2ClientService extends EntityDaoService { + + List findOAuth2ClientLoginInfosByDomainName(String domainName); + + List findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(String pkgName, PlatformType platformType); + + List findOAuth2ClientsByTenantId(TenantId tenantId); + + OAuth2Client saveOAuth2Client(TenantId tenantId, OAuth2Client oAuth2Client); + + OAuth2Client findOAuth2ClientById(TenantId tenantId, OAuth2ClientId providerId); + + String findAppSecret(OAuth2ClientId oAuth2ClientId, String pkgName); + + void deleteOAuth2ClientById(TenantId tenantId, OAuth2ClientId oAuth2ClientId); + + void deleteOauth2ClientsByTenantId(TenantId tenantId); + + PageData findOAuth2ClientInfosByTenantId(TenantId tenantId, PageLink pageLink); + + List findOAuth2ClientInfosByIds(TenantId tenantId, List oAuth2ClientIds); + + boolean isPropagateOAuth2ClientToEdge(TenantId tenantId, OAuth2ClientId oAuth2ClientId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java deleted file mode 100644 index 11129002e7..0000000000 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.oauth2; - -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.oauth2.PlatformType; - -import java.util.List; -import java.util.UUID; - -public interface OAuth2Service { - - List getOAuth2Clients(String domainScheme, String domainName, String pkgName, PlatformType platformType); - - void saveOAuth2Info(OAuth2Info oauth2Info); - - OAuth2Info findOAuth2Info(); - - OAuth2Registration findRegistration(UUID id); - - List findAllRegistrations(); - - String findAppSecret(UUID registrationId, String pkgName); -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 6748aebb50..b5ce79e20f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -57,7 +57,10 @@ public enum EntityType { NOTIFICATION_REQUEST(31), NOTIFICATION(32), NOTIFICATION_RULE(33), - QUEUE_STATS(34); + QUEUE_STATS(34), + OAUTH2_CLIENT(35), + DOMAIN(36), + MOBILE_APP(37); @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/domain/Domain.java b/common/data/src/main/java/org/thingsboard/server/common/data/domain/Domain.java new file mode 100644 index 0000000000..9efbad8a25 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/domain/Domain.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +public class Domain extends BaseData implements HasTenantId, HasName { + + @Schema(description = "JSON object with Tenant Id") + private TenantId tenantId; + @Schema(description = "Domain name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "name") + private String name; + @Schema(description = "Whether OAuth2 settings are enabled or not") + private boolean oauth2Enabled; + @Schema(description = "Whether OAuth2 settings are enabled on Edge or not") + private boolean propagateToEdge; + + public Domain() { + super(); + } + + public Domain(DomainId id) { + super(id); + } + + public Domain(Domain domain) { + super(domain); + this.tenantId = domain.tenantId; + this.name = domain.name; + this.oauth2Enabled = domain.oauth2Enabled; + this.propagateToEdge = domain.propagateToEdge; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainInfo.java similarity index 52% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java rename to common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainInfo.java index 0a2ace050c..55871e5d6b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainInfo.java @@ -13,30 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.domain; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import java.util.List; -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = true) @Data -@ToString -@Builder(toBuilder = true) -@NoArgsConstructor -@AllArgsConstructor @Schema -public class OAuth2Info { - @Schema(description = "Whether OAuth2 settings are enabled or not") - private boolean enabled; - @Schema(description = "Whether OAuth2 settings are enabled on Edge or not") - private boolean edgeEnabled; - @Schema(description = "List of configured OAuth2 clients. Cannot contain null values", requiredMode = Schema.RequiredMode.REQUIRED) - private List oauth2ParamsInfos; +public class DomainInfo extends Domain { + + @Schema(description = "List of available oauth2 clients") + private List oauth2ClientInfos; + + public DomainInfo(Domain domain, List oauth2ClientInfos) { + super(domain); + this.oauth2ClientInfos = oauth2ClientInfos; + } + + public DomainInfo() { + super(); + } + + public DomainInfo(DomainId domainId) { + super(domainId); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainOauth2Client.java similarity index 60% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainOauth2Client.java index 053762bdf0..91cbb8a7a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/domain/DomainOauth2Client.java @@ -13,26 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.domain; -import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import lombok.ToString; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; -@EqualsAndHashCode @Data -@ToString @NoArgsConstructor @AllArgsConstructor -@Builder -@Schema -public class OAuth2MobileInfo { - @Schema(description = "Application package name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) - private String pkgName; - @Schema(description = "Application secret. The length must be at least 16 characters", requiredMode = Schema.RequiredMode.REQUIRED) - private String appSecret; +@EqualsAndHashCode +public class DomainOauth2Client { + + private DomainId domainId; + private OAuth2ClientId oAuth2ClientId; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 44a3ed8efe..ce26253f5a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -45,7 +45,8 @@ public enum EdgeEventType { NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), TB_RESOURCE(true, EntityType.TB_RESOURCE), - OAUTH2(true, null); + OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), + DOMAIN(true, EntityType.DOMAIN); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2DomainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java similarity index 68% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2DomainId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java index 14899d9dc1..c3d4aab159 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2DomainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java @@ -17,17 +17,23 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -public class OAuth2DomainId extends UUIDBased { +public class DomainId extends UUIDBased implements EntityId { @JsonCreator - public OAuth2DomainId(@JsonProperty("id") UUID id) { + public DomainId(@JsonProperty("id") UUID id) { super(id); } - public static OAuth2DomainId fromString(String oauth2DomainId) { - return new OAuth2DomainId(UUID.fromString(oauth2DomainId)); + public static DomainId fromString(String oauth2DomainId) { + return new DomainId(UUID.fromString(oauth2DomainId)); + } + + @Override + public EntityType getEntityType() { + return EntityType.DOMAIN; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index ad2df69349..5a85e6ce67 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -105,6 +105,12 @@ public class EntityIdFactory { return new NotificationId(uuid); case QUEUE_STATS: return new QueueStatsId(uuid); + case OAUTH2_CLIENT: + return new OAuth2ClientId(uuid); + case MOBILE_APP: + return new MobileAppId(uuid); + case DOMAIN: + return new DomainId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } @@ -153,6 +159,10 @@ public class EntityIdFactory { return new NotificationTargetId(uuid); case NOTIFICATION_TEMPLATE: return new NotificationTemplateId(uuid); + case OAUTH2_CLIENT: + return new OAuth2ClientId(uuid); + case DOMAIN: + return new DomainId(uuid); } throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2MobileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java similarity index 68% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2MobileId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java index 6b280dfc60..114e4351f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2MobileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java @@ -17,17 +17,23 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -public class OAuth2MobileId extends UUIDBased { +public class MobileAppId extends UUIDBased implements EntityId{ @JsonCreator - public OAuth2MobileId(@JsonProperty("id") UUID id) { + public MobileAppId(@JsonProperty("id") UUID id) { super(id); } - public static OAuth2MobileId fromString(String oauth2MobileId) { - return new OAuth2MobileId(UUID.fromString(oauth2MobileId)); + public static MobileAppId fromString(String mobileAppId) { + return new MobileAppId(UUID.fromString(mobileAppId)); + } + + @Override + public EntityType getEntityType() { + return EntityType.MOBILE_APP; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2RegistrationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java similarity index 66% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2RegistrationId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java index 28156417db..f29067a6c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2RegistrationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java @@ -17,17 +17,23 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -public class OAuth2RegistrationId extends UUIDBased { +public class OAuth2ClientId extends UUIDBased implements EntityId { @JsonCreator - public OAuth2RegistrationId(@JsonProperty("id") UUID id) { + public OAuth2ClientId(@JsonProperty("id") UUID id) { super(id); } - public static OAuth2RegistrationId fromString(String oauth2RegistrationId) { - return new OAuth2RegistrationId(UUID.fromString(oauth2RegistrationId)); + public static OAuth2ClientId fromString(String oauth2ClientId) { + return new OAuth2ClientId(UUID.fromString(oauth2ClientId)); + } + + @Override + public EntityType getEntityType() { + return EntityType.OAUTH2_CLIENT; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java new file mode 100644 index 0000000000..ad207e5635 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileApp.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +public class MobileApp extends BaseData implements HasTenantId, HasName { + + @Schema(description = "JSON object with Tenant Id") + private TenantId tenantId; + @Schema(description = "Application package name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "pkgName") + private String pkgName; + @Schema(description = "Application secret. The length must be at least 16 characters", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty + @Length(fieldName = "appSecret", min = 16, max = 2048, message = "must be at least 16 and max 2048 characters") + private String appSecret; + @Schema(description = "Whether OAuth2 settings are enabled or not") + private boolean oauth2Enabled; + + public MobileApp() { + super(); + } + + public MobileApp(MobileAppId id) { + super(id); + } + + public MobileApp(MobileApp mobile) { + super(mobile); + this.tenantId = mobile.tenantId; + this.pkgName = mobile.pkgName; + this.appSecret = mobile.appSecret; + this.oauth2Enabled = mobile.oauth2Enabled; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @Schema(description = "Mobile app package name", example = "my.mobile.app", accessMode = Schema.AccessMode.READ_ONLY) + public String getName() { + return pkgName; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java new file mode 100644 index 0000000000..4d85028e36 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppInfo.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@Schema +public class MobileAppInfo extends MobileApp { + + @Schema(description = "List of available oauth2 clients") + private List oauth2ClientInfos; + + public MobileAppInfo(MobileApp mobileApp, List oauth2ClientInfos) { + super(mobileApp); + this.oauth2ClientInfos = oauth2ClientInfos; + } + + public MobileAppInfo() { + super(); + } + + public MobileAppInfo(MobileAppId mobileAppId) { + super(mobileAppId); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppOauth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppOauth2Client.java new file mode 100644 index 0000000000..2be75db92f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppOauth2Client.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MobileAppOauth2Client { + + private MobileAppId mobileAppId; + private OAuth2ClientId oAuth2ClientId; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java similarity index 51% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java index 36bae12b23..0a9924785b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java @@ -15,51 +15,115 @@ */ package org.thingsboard.server.common.data.oauth2; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; import lombok.ToString; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; import java.util.List; -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = true) @Data @ToString(exclude = {"clientSecret"}) -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Schema -public class OAuth2RegistrationInfo { +public class OAuth2Client extends BaseDataWithAdditionalInfo implements HasName, HasTenantId { + + @Schema(description = "JSON object with Tenant Id") + private TenantId tenantId; + @Schema(description = "Oauth2 client title") + @NotBlank + @Length(fieldName = "title", max = 100, message = "cannot be longer than 100 chars") + private String title; @Schema(description = "Config for mapping OAuth2 log in response to platform entities", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private OAuth2MapperConfig mapperConfig; @Schema(description = "OAuth2 client ID. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "clientId") private String clientId; @Schema(description = "OAuth2 client secret. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "clientSecret", max = 2048) private String clientSecret; @Schema(description = "Authorization URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "authorizationUri") private String authorizationUri; @Schema(description = "Access token URI of the OAuth2 provider. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "accessTokenUri") private String accessTokenUri; @Schema(description = "OAuth scopes that will be requested from OAuth2 platform. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty + @Length(fieldName = "scope") private List scope; @Schema(description = "User info URI of the OAuth2 provider") + @Length(fieldName = "userInfoUri") private String userInfoUri; @Schema(description = "Name of the username attribute in OAuth2 provider response. Cannot be empty") + @NotBlank + @Length(fieldName = "userNameAttributeName") private String userNameAttributeName; @Schema(description = "JSON Web Key URI of the OAuth2 provider") + @Length(fieldName = "jwkSetUri") private String jwkSetUri; @Schema(description = "Client authentication method to use: 'BASIC' or 'POST'. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "clientAuthenticationMethod") private String clientAuthenticationMethod; @Schema(description = "OAuth2 provider label. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @Length(fieldName = "loginButtonLabel") private String loginButtonLabel; @Schema(description = "Log in button icon for OAuth2 provider") + @Length(fieldName = "loginButtonIcon") private String loginButtonIcon; @Schema(description = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") + @Length(fieldName = "platforms") private List platforms; @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", requiredMode = Schema.RequiredMode.REQUIRED) private JsonNode additionalInfo; + + public OAuth2Client() { + super(); + } + + public OAuth2Client(OAuth2ClientId id) { + super(id); + } + + public OAuth2Client(OAuth2Client oAuth2Client) { + super(oAuth2Client); + this.tenantId = oAuth2Client.tenantId; + this.title = oAuth2Client.title; + this.mapperConfig = oAuth2Client.mapperConfig; + this.clientId = oAuth2Client.clientId; + this.clientSecret = oAuth2Client.clientSecret; + this.authorizationUri = oAuth2Client.authorizationUri; + this.accessTokenUri = oAuth2Client.accessTokenUri; + this.scope = oAuth2Client.scope; + this.userInfoUri = oAuth2Client.userInfoUri; + this.userNameAttributeName = oAuth2Client.userNameAttributeName; + this.jwkSetUri = oAuth2Client.jwkSetUri; + this.clientAuthenticationMethod = oAuth2Client.clientAuthenticationMethod; + this.loginButtonLabel = oAuth2Client.loginButtonLabel; + this.loginButtonIcon = oAuth2Client.loginButtonIcon; + this.platforms = oAuth2Client.platforms; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getName() { + return title; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java index 8194e02693..4e083545c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java @@ -15,31 +15,48 @@ */ package org.thingsboard.server.common.data.oauth2; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.id.OAuth2ClientId; + +import java.util.List; -@EqualsAndHashCode @Data -@NoArgsConstructor -@AllArgsConstructor @Schema -public class OAuth2ClientInfo { - - @Schema(description = "OAuth2 client name", example = "GitHub") - private String name; - @Schema(description = "Name of the icon, displayed on OAuth2 log in button", example = "github-logo") - private String icon; - @Schema(description = "URI for OAuth2 log in. On HTTP GET request to this URI, it redirects to the OAuth2 provider page", - example = "/oauth2/authorization/8352f191-2b4d-11ec-9ed1-cbf57c026ecc") - private String url; - - public OAuth2ClientInfo(OAuth2ClientInfo oauth2ClientInfo) { - this.name = oauth2ClientInfo.getName(); - this.icon = oauth2ClientInfo.getIcon(); - this.url = oauth2ClientInfo.getUrl(); +@EqualsAndHashCode(callSuper = true) +public class OAuth2ClientInfo extends BaseData implements HasName { + + @Schema(description = "Oauth2 client registration title (e.g. My google)") + private String title; + + @Schema(description = "Oauth2 client provider name (e.g. Google)") + private String providerName; + @Schema(description = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") + private List platforms; + + public OAuth2ClientInfo() { + super(); } + public OAuth2ClientInfo(OAuth2ClientId id) { + super(id); + } + + public OAuth2ClientInfo(OAuth2Client oAuth2Client) { + super(oAuth2Client); + this.title = oAuth2Client.getTitle(); + this.providerName = oAuth2Client.getAdditionalInfoField("providerName", JsonNode::asText,""); + this.platforms = oAuth2Client.getPlatforms(); + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getName() { + return title; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientLoginInfo.java similarity index 66% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientLoginInfo.java index dc70213e95..7662b8f9b0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientLoginInfo.java @@ -17,22 +17,23 @@ package org.thingsboard.server.common.data.oauth2; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import lombok.ToString; @EqualsAndHashCode @Data -@ToString @NoArgsConstructor @AllArgsConstructor -@Builder @Schema -public class OAuth2DomainInfo { - @Schema(description = "Domain scheme. Mixed scheme means than both HTTP and HTTPS are going to be used", requiredMode = Schema.RequiredMode.REQUIRED) - private SchemeType scheme; - @Schema(description = "Domain name. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) +public class OAuth2ClientLoginInfo { + + @Schema(description = "OAuth2 client name", example = "GitHub") private String name; + @Schema(description = "Name of the icon, displayed on OAuth2 log in button", example = "github-logo") + private String icon; + @Schema(description = "URI for OAuth2 log in. On HTTP GET request to this URI, it redirects to the OAuth2 provider page", + example = "/oauth2/authorization/8352f191-2b4d-11ec-9ed1-cbf57c026ecc") + private String url; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Domain.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Domain.java deleted file mode 100644 index 6fa0475361..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Domain.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.oauth2; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.id.OAuth2DomainId; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; - -@EqualsAndHashCode(callSuper = true) -@Data -@ToString -@NoArgsConstructor -public class OAuth2Domain extends BaseData { - - private OAuth2ParamsId oauth2ParamsId; - private String domainName; - private SchemeType domainScheme; - - public OAuth2Domain(OAuth2Domain domain) { - super(domain); - this.oauth2ParamsId = domain.oauth2ParamsId; - this.domainName = domain.domainName; - this.domainScheme = domain.domainScheme; - } -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java index fdf62d7e53..bf4eb2c9cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.oauth2; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import jakarta.validation.Valid; import lombok.Builder; import lombok.Data; @@ -32,6 +33,7 @@ public class OAuth2MapperConfig { @Schema(description = "Whether user credentials should be activated when user is created after successful authentication") private boolean activateUser; @Schema(description = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private MapperType type; @Valid @Schema(description = "Mapper config for BASIC and GITHUB mapper types") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java deleted file mode 100644 index fd9bbe527f..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.oauth2; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.id.OAuth2MobileId; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; - -@EqualsAndHashCode(callSuper = true) -@Data -@ToString -@NoArgsConstructor -public class OAuth2Mobile extends BaseData { - - private OAuth2ParamsId oauth2ParamsId; - private String pkgName; - private String appSecret; - - public OAuth2Mobile(OAuth2Mobile mobile) { - super(mobile); - this.oauth2ParamsId = mobile.oauth2ParamsId; - this.pkgName = mobile.pkgName; - this.appSecret = mobile.appSecret; - } -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java deleted file mode 100644 index c1b6124b8f..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.oauth2; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; - -import java.util.List; - -@EqualsAndHashCode -@Data -@ToString -@Builder(toBuilder = true) -@NoArgsConstructor -@AllArgsConstructor -@Schema -public class OAuth2ParamsInfo { - - @Schema(description = "List of configured domains where OAuth2 platform will redirect a user after successful " + - "authentication. Cannot be empty. There have to be only one domain with specific name with scheme type 'MIXED'. " + - "Configured domains with the same name must have different scheme types", requiredMode = Schema.RequiredMode.REQUIRED) - private List domainInfos; - @Schema(description = "Mobile applications settings. Application package name must be unique within the list", requiredMode = Schema.RequiredMode.REQUIRED) - private List mobileInfos; - @Schema(description = "List of OAuth2 provider settings. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) - private List clientRegistrations; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java deleted file mode 100644 index 76c1953387..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.oauth2; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; -import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; -import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; -import org.thingsboard.server.common.data.id.OAuth2RegistrationId; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -@Data -@ToString(exclude = {"clientSecret"}) -@NoArgsConstructor -public class OAuth2Registration extends BaseDataWithAdditionalInfo implements HasName { - - private OAuth2ParamsId oauth2ParamsId; - private OAuth2MapperConfig mapperConfig; - private String clientId; - private String clientSecret; - private String authorizationUri; - private String accessTokenUri; - private List scope; - private String userInfoUri; - private String userNameAttributeName; - private String jwkSetUri; - private String clientAuthenticationMethod; - private String loginButtonLabel; - private String loginButtonIcon; - private List platforms; - - public OAuth2Registration(OAuth2Registration registration) { - super(registration); - this.oauth2ParamsId = registration.oauth2ParamsId; - this.mapperConfig = registration.mapperConfig; - this.clientId = registration.clientId; - this.clientSecret = registration.clientSecret; - this.authorizationUri = registration.authorizationUri; - this.accessTokenUri = registration.accessTokenUri; - this.scope = registration.scope; - this.userInfoUri = registration.userInfoUri; - this.userNameAttributeName = registration.userNameAttributeName; - this.jwkSetUri = registration.jwkSetUri; - this.clientAuthenticationMethod = registration.clientAuthenticationMethod; - this.loginButtonLabel = registration.loginButtonLabel; - this.loginButtonIcon = registration.loginButtonIcon; - this.platforms = registration.platforms; - } - - @Override - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - public String getName() { - return loginButtonLabel; - } -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java index 31c31d893a..2305a32f4a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java @@ -33,6 +33,8 @@ public @interface Length { int max() default 255; + int min() default 0; + Class[] groups() default {}; Class[] payload() default {}; diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 0a36b5f56e..bfc95ea37c 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_7_0) + .setEdgeVersion(EdgeVersion.V_3_7_1) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 7c60985683..43ce1a222c 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -39,6 +39,7 @@ enum EdgeVersion { V_3_6_2 = 5; V_3_6_4 = 6; V_3_7_0 = 7; + V_3_7_1 = 8; } /** @@ -469,8 +470,18 @@ message ResourceUpdateMsg { string entity = 11; } -message OAuth2UpdateMsg { - string entity = 1; +message OAuth2ClientUpdateMsg { + UpdateMsgType msgType = 1; + optional int64 idMSB = 2; + optional int64 idLSB = 3; + optional string entity = 4; +} + +message OAuth2DomainUpdateMsg { + UpdateMsgType msgType = 1; + optional int64 idMSB = 2; + optional int64 idLSB = 3; + optional string entity = 4; } message NotificationRuleUpdateMsg { @@ -695,9 +706,9 @@ message DownlinkMsg { repeated TenantProfileUpdateMsg tenantProfileUpdateMsg = 27; repeated ResourceUpdateMsg resourceUpdateMsg = 28; repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 29; - repeated OAuth2UpdateMsg oAuth2UpdateMsg = 30; + repeated OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = 30; repeated NotificationRuleUpdateMsg notificationRuleUpdateMsg = 31; repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; + repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; } - diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 7c3bfddc08..74f1d0560f 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -54,7 +54,10 @@ enum EntityTypeProto { NOTIFICATION_REQUEST = 31; NOTIFICATION = 32; NOTIFICATION_RULE = 33; - QUEUE_STATS = 34; + QUEUE_STATS = 34; + OAUTH2_CLIENT = 35; + DOMAIN = 36; + MOBILE_APP = 37; } /** diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainDao.java new file mode 100644 index 0000000000..e4eaf57356 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainDao.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2024 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.dao.domain; + +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainOauth2Client; +import org.thingsboard.server.common.data.id.DomainId; +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.dao.Dao; + +import java.util.List; + +public interface DomainDao extends Dao { + + PageData findByTenantId(TenantId tenantId, PageLink pageLink); + + int countDomainByTenantIdAndOauth2Enabled(TenantId tenantId, boolean oauth2Enabled); + + List findOauth2ClientsByDomainId(TenantId tenantId, DomainId domainId); + + void addOauth2Client(DomainOauth2Client domainOauth2Client); + + void removeOauth2Client(DomainOauth2Client domainOauth2Client); + + void deleteByTenantId(TenantId tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java new file mode 100644 index 0000000000..b7a13313b7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java @@ -0,0 +1,160 @@ +/** + * Copyright © 2016-2024 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.dao.domain; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.domain.DomainOauth2Client; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class DomainServiceImpl extends AbstractEntityService implements DomainService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + + @Autowired + private OAuth2ClientDao oauth2ClientDao; + @Autowired + private DomainDao domainDao; + + @Override + public Domain saveDomain(TenantId tenantId, Domain domain) { + log.trace("Executing saveDomain [{}]", domain); + try { + Domain savedDomain = domainDao.save(tenantId, domain); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build()); + return savedDomain; + } catch (Exception e) { + checkConstraintViolation(e, + Map.of("domain_unq_key", "Domain with such name and scheme already exists!")); + throw e; + } + } + + @Override + public void updateOauth2Clients(TenantId tenantId, DomainId domainId, List oAuth2ClientIds) { + log.trace("Executing updateOauth2Clients, domainId [{}], oAuth2ClientIds [{}]", domainId, oAuth2ClientIds); + Set newClientList = oAuth2ClientIds.stream() + .map(clientId -> new DomainOauth2Client(domainId, clientId)) + .collect(Collectors.toSet()); + + List existingClients = domainDao.findOauth2ClientsByDomainId(tenantId, domainId); + List toRemoveList = existingClients.stream() + .filter(client -> !newClientList.contains(client)) + .toList(); + newClientList.removeIf(existingClients::contains); + + for (DomainOauth2Client client : toRemoveList) { + domainDao.removeOauth2Client(client); + } + for (DomainOauth2Client client : newClientList) { + domainDao.addOauth2Client(client); + } + } + + @Override + public void deleteDomainById(TenantId tenantId, DomainId domainId) { + log.trace("Executing deleteDomainById [{}]", domainId.getId()); + domainDao.removeById(tenantId, domainId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(domainId).build()); + } + + @Override + public Domain findDomainById(TenantId tenantId, DomainId domainId) { + log.trace("Executing findDomainInfo [{}] [{}]", tenantId, domainId); + return domainDao.findById(tenantId, domainId.getId()); + } + + @Override + public PageData findDomainInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findDomainInfosByTenantId [{}]", tenantId); + PageData domains = domainDao.findByTenantId(tenantId, pageLink); + return domains.mapData(this::getDomainInfo); + } + + @Override + public DomainInfo findDomainInfoById(TenantId tenantId, DomainId domainId) { + log.trace("Executing findDomainInfoById [{}] [{}]", tenantId, domainId); + Domain domain = domainDao.findById(tenantId, domainId.getId()); + return getDomainInfo(domain); + } + + @Override + public boolean isOauth2Enabled(TenantId tenantId) { + log.trace("Executing isOauth2Enabled [{}] ", tenantId); + return domainDao.countDomainByTenantIdAndOauth2Enabled(tenantId, true) > 0; + } + + @Override + public void deleteDomainsByTenantId(TenantId tenantId) { + log.trace("Executing deleteDomainsByTenantId, tenantId [{}]", tenantId); + domainDao.deleteByTenantId(tenantId); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteDomainsByTenantId(tenantId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(findDomainById(tenantId, new DomainId(entityId.getId()))); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteDomainById(tenantId, (DomainId) id); + } + + private DomainInfo getDomainInfo(Domain domain) { + if (domain == null) { + return null; + } + List clients = oauth2ClientDao.findByDomainId(domain.getUuidId()).stream() + .map(OAuth2ClientInfo::new) + .collect(Collectors.toList()); + return new DomainInfo(domain, clients); + } + + @Override + public EntityType getEntityType() { + return EntityType.DOMAIN; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java new file mode 100644 index 0000000000..8da92e1eb3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppDao.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 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.dao.mobile; + +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.List; + +public interface MobileAppDao extends Dao { + + PageData findByTenantId(TenantId tenantId, PageLink pageLink); + + List findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId); + + void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client); + + void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client); + + void deleteByTenantId(TenantId tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java new file mode 100644 index 0000000000..1e89b3c645 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2024 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.dao.mobile; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService { + + @Autowired + private OAuth2ClientDao oauth2ClientDao; + @Autowired + private MobileAppDao mobileAppDao; + + @Override + public MobileApp saveMobileApp(TenantId tenantId, MobileApp mobileApp) { + log.trace("Executing saveMobileApp [{}]", mobileApp); + try { + MobileApp savedMobileApp = mobileAppDao.save(tenantId, mobileApp); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); + return savedMobileApp; + } catch (Exception e) { + checkConstraintViolation(e, + Map.of("mobile_app_unq_key", "Mobile app with such package already exists!")); + throw e; + } + } + + @Override + public void deleteMobileAppById(TenantId tenantId, MobileAppId mobileAppId) { + log.trace("Executing deleteMobileAppById [{}]", mobileAppId.getId()); + mobileAppDao.removeById(tenantId, mobileAppId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(mobileAppId).build()); + } + + @Override + public MobileApp findMobileAppById(TenantId tenantId, MobileAppId mobileAppId) { + log.trace("Executing findMobileAppById [{}] [{}]", tenantId, mobileAppId); + return mobileAppDao.findById(tenantId, mobileAppId.getId()); + } + + @Override + public PageData findMobileAppInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findMobileAppInfosByTenantId [{}]", tenantId); + PageData mobiles = mobileAppDao.findByTenantId(tenantId, pageLink); + return mobiles.mapData(this::getMobileAppInfo); + } + + @Override + public MobileAppInfo findMobileAppInfoById(TenantId tenantId, MobileAppId mobileAppId) { + log.trace("Executing findMobileAppInfoById [{}] [{}]", tenantId, mobileAppId); + MobileApp mobileApp = mobileAppDao.findById(tenantId, mobileAppId.getId()); + if (mobileApp == null) { + return null; + } + return getMobileAppInfo(mobileApp); + } + + @Override + public void updateOauth2Clients(TenantId tenantId, MobileAppId mobileAppId, List oAuth2ClientIds) { + log.trace("Executing updateOauth2Clients, mobileAppId [{}], oAuth2ClientIds [{}]", mobileAppId, oAuth2ClientIds); + Set newClientList = oAuth2ClientIds.stream() + .map(clientId -> new MobileAppOauth2Client(mobileAppId, clientId)) + .collect(Collectors.toSet()); + + List existingClients = mobileAppDao.findOauth2ClientsByMobileAppId(tenantId, mobileAppId); + List toRemoveList = existingClients.stream() + .filter(client -> !newClientList.contains(client)) + .toList(); + newClientList.removeIf(existingClients::contains); + + for (MobileAppOauth2Client client : toRemoveList) { + mobileAppDao.removeOauth2Client(client); + } + for (MobileAppOauth2Client client : newClientList) { + mobileAppDao.addOauth2Client(client); + } + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) + .entityId(mobileAppId).created(false).build()); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(findMobileAppById(tenantId, new MobileAppId(entityId.getId()))); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteMobileAppById(tenantId, (MobileAppId) id); + } + + @Override + public void deleteMobileAppsByTenantId(TenantId tenantId) { + log.trace("Executing deleteMobileAppsByTenantId, tenantId [{}]", tenantId); + mobileAppDao.deleteByTenantId(tenantId); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteMobileAppsByTenantId(tenantId); + } + + private MobileAppInfo getMobileAppInfo(MobileApp mobileApp) { + List clients = oauth2ClientDao.findByMobileAppId(mobileApp.getUuidId()).stream() + .map(OAuth2ClientInfo::new) + .collect(Collectors.toList()); + return new MobileAppInfo(mobileApp, clients); + } + + @Override + public EntityType getEntityType() { + return EntityType.MOBILE_APP; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 27f1019a68..1fbab9c47c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -429,24 +429,37 @@ public class ModelConstants { public static final String RULE_NODE_STATE_DATA_PROPERTY = "state_data"; /** - * OAuth2 client registration constants. + * Domain constants. */ - public static final String OAUTH2_PARAMS_TABLE_NAME = "oauth2_params"; - public static final String OAUTH2_PARAMS_ENABLED_PROPERTY = "enabled"; - public static final String OAUTH2_PARAMS_EDGE_ENABLED_PROPERTY = "edge_enabled"; - public static final String OAUTH2_PARAMS_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String DOMAIN_TABLE_NAME = "domain"; + public static final String DOMAIN_NAME_PROPERTY = "name"; + public static final String DOMAIN_OAUTH2_ENABLED_PROPERTY = "oauth2_enabled"; + public static final String DOMAIN_PROPAGATE_TO_EDGE_PROPERTY = "edge_enabled"; - public static final String OAUTH2_REGISTRATION_TABLE_NAME = "oauth2_registration"; - public static final String OAUTH2_DOMAIN_TABLE_NAME = "oauth2_domain"; - public static final String OAUTH2_MOBILE_TABLE_NAME = "oauth2_mobile"; - public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id"; - public static final String OAUTH2_PKG_NAME_PROPERTY = "pkg_name"; - public static final String OAUTH2_APP_SECRET_PROPERTY = "app_secret"; + public static final String DOMAIN_OAUTH2_CLIENT_TABLE_NAME = "domain_oauth2_client"; + public static final String DOMAIN_OAUTH2_CLIENT_CLIENT_ID_PROPERTY = "oauth2_client_id"; + public static final String DOMAIN_OAUTH2_CLIENT_DOMAIN_ID_PROPERTY = "domain_id"; + /** + * Mobile application constants. + */ + public static final String MOBILE_APP_TABLE_NAME = "mobile_app"; + public static final String MOBILE_APP_PKG_NAME_PROPERTY = "pkg_name"; + public static final String MOBILE_APP_APP_SECRET_PROPERTY = "app_secret"; + public static final String MOBILE_APP_OAUTH2_ENABLED_PROPERTY = "oauth2_enabled"; + + public static final String MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME = "mobile_app_oauth2_client"; + public static final String MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY = "oauth2_client_id"; + public static final String MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY = "mobile_app_id"; + + + /** + * OAuth2 client constants. + */ + public static final String OAUTH2_CLIENT_TABLE_NAME = "oauth2_client"; public static final String OAUTH2_CLIENT_REGISTRATION_TEMPLATE_TABLE_NAME = "oauth2_client_registration_template"; public static final String OAUTH2_TEMPLATE_PROVIDER_ID_PROPERTY = "provider_id"; - public static final String OAUTH2_DOMAIN_NAME_PROPERTY = "domain_name"; - public static final String OAUTH2_DOMAIN_SCHEME_PROPERTY = "domain_scheme"; + public static final String OAUTH2_CLIENT_TITLE_PROPERTY = "title"; public static final String OAUTH2_CLIENT_ID_PROPERTY = "client_id"; public static final String OAUTH2_CLIENT_SECRET_PROPERTY = "client_secret"; public static final String OAUTH2_AUTHORIZATION_URI_PROPERTY = "authorization_uri"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java new file mode 100644 index 0000000000..1b0c7a4183 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainEntity.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2024 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.dao.model.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ModelConstants.DOMAIN_TABLE_NAME) +public class DomainEntity extends BaseSqlEntity { + + @Column(name = TENANT_ID_COLUMN) + private UUID tenantId; + + @Column(name = ModelConstants.DOMAIN_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.DOMAIN_OAUTH2_ENABLED_PROPERTY) + private Boolean oauth2Enabled; + + @Column(name = ModelConstants.DOMAIN_PROPAGATE_TO_EDGE_PROPERTY) + private Boolean propagateToEdge; + + public DomainEntity(Domain domain) { + super(domain); + if (domain.getTenantId() != null) { + this.tenantId = domain.getTenantId().getId(); + } + this.name = domain.getName(); + this.oauth2Enabled = domain.isOauth2Enabled(); + this.propagateToEdge = domain.isPropagateToEdge(); + } + + public DomainEntity() { + super(); + } + + @Override + public Domain toData() { + Domain domain = new Domain(); + domain.setId(new DomainId(id)); + if (tenantId != null) { + domain.setTenantId(TenantId.fromUUID(tenantId)); + } + domain.setCreatedTime(createdTime); + domain.setName(name); + domain.setOauth2Enabled(oauth2Enabled); + domain.setPropagateToEdge(propagateToEdge); + return domain; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainOauth2ClientCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainOauth2ClientCompositeKey.java new file mode 100644 index 0000000000..bdc9ca241c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainOauth2ClientCompositeKey.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 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.dao.model.sql; + +import jakarta.persistence.Transient; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.UUID; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class DomainOauth2ClientCompositeKey implements Serializable { + + @Transient + private static final long serialVersionUID = -245388185894468455L; + + private UUID domainId; + private UUID oauth2ClientId; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainOauth2ClientEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainOauth2ClientEntity.java new file mode 100644 index 0000000000..5b5efd489f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DomainOauth2ClientEntity.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 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.dao.model.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.Data; +import org.thingsboard.server.common.data.domain.DomainOauth2Client; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.ToData; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.DOMAIN_OAUTH2_CLIENT_DOMAIN_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.DOMAIN_OAUTH2_CLIENT_TABLE_NAME; + +@Data +@Entity +@Table(name = DOMAIN_OAUTH2_CLIENT_TABLE_NAME) +@IdClass(DomainOauth2ClientCompositeKey.class) +public final class DomainOauth2ClientEntity implements ToData { + + @Id + @Column(name = DOMAIN_OAUTH2_CLIENT_DOMAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID domainId; + + @Id + @Column(name = ModelConstants.DOMAIN_OAUTH2_CLIENT_CLIENT_ID_PROPERTY, columnDefinition = "uuid") + private UUID oauth2ClientId; + + + public DomainOauth2ClientEntity() { + super(); + } + + public DomainOauth2ClientEntity(DomainOauth2Client domainOauth2Client) { + domainId = domainOauth2Client.getDomainId().getId(); + oauth2ClientId = domainOauth2Client.getOAuth2ClientId().getId(); + } + + @Override + public DomainOauth2Client toData() { + DomainOauth2Client result = new DomainOauth2Client(); + result.setDomainId(new DomainId(domainId)); + result.setOAuth2ClientId(new OAuth2ClientId(oauth2ClientId)); + return result; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java similarity index 53% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java index d2dcb32a13..0313864d09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java @@ -20,53 +20,59 @@ import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.id.OAuth2MobileId; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; -import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN; + @Data @EqualsAndHashCode(callSuper = true) @Entity -@Table(name = ModelConstants.OAUTH2_MOBILE_TABLE_NAME) -public class OAuth2MobileEntity extends BaseSqlEntity { +@Table(name = ModelConstants.MOBILE_APP_TABLE_NAME) +public class MobileAppEntity extends BaseSqlEntity { - @Column(name = ModelConstants.OAUTH2_PARAMS_ID_PROPERTY) - private UUID oauth2ParamsId; + @Column(name = TENANT_ID_COLUMN) + private UUID tenantId; - @Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY) + @Column(name = ModelConstants.MOBILE_APP_PKG_NAME_PROPERTY) private String pkgName; - @Column(name = ModelConstants.OAUTH2_APP_SECRET_PROPERTY) + @Column(name = ModelConstants.MOBILE_APP_APP_SECRET_PROPERTY) private String appSecret; - public OAuth2MobileEntity() { + @Column(name = ModelConstants.MOBILE_APP_OAUTH2_ENABLED_PROPERTY) + private Boolean oauth2Enabled; + + public MobileAppEntity() { super(); } - public OAuth2MobileEntity(OAuth2Mobile mobile) { - if (mobile.getId() != null) { - this.setUuid(mobile.getId().getId()); - } - this.setCreatedTime(mobile.getCreatedTime()); - if (mobile.getOauth2ParamsId() != null) { - this.oauth2ParamsId = mobile.getOauth2ParamsId().getId(); + public MobileAppEntity(MobileApp mobile) { + super(mobile); + if (mobile.getTenantId() != null) { + this.tenantId = mobile.getTenantId().getId(); } this.pkgName = mobile.getPkgName(); this.appSecret = mobile.getAppSecret(); + this.oauth2Enabled = mobile.isOauth2Enabled(); } @Override - public OAuth2Mobile toData() { - OAuth2Mobile mobile = new OAuth2Mobile(); - mobile.setId(new OAuth2MobileId(id)); + public MobileApp toData() { + MobileApp mobile = new MobileApp(); + mobile.setId(new MobileAppId(id)); + if (tenantId != null) { + mobile.setTenantId(TenantId.fromUUID(tenantId)); + } mobile.setCreatedTime(createdTime); - mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); mobile.setPkgName(pkgName); mobile.setAppSecret(appSecret); + mobile.setOauth2Enabled(oauth2Enabled); return mobile; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java new file mode 100644 index 0000000000..b372751549 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientCompositeKey.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 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.dao.model.sql; + +import jakarta.persistence.Transient; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.UUID; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class MobileAppOauth2ClientCompositeKey implements Serializable { + + @Transient + private static final long serialVersionUID = -245388185894468455L; + + private UUID mobileAppId; + private UUID oauth2ClientId; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientEntity.java new file mode 100644 index 0000000000..b2c7a55855 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppOauth2ClientEntity.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2024 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.dao.model.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; +import lombok.Data; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; +import org.thingsboard.server.dao.model.ToData; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME; + +@Data +@Entity +@Table(name = MOBILE_APP_OAUTH2_CLIENT_TABLE_NAME) +@IdClass(MobileAppOauth2ClientCompositeKey.class) +public final class MobileAppOauth2ClientEntity implements ToData { + + @Id + @Column(name = MOBILE_APP_OAUTH2_CLIENT_MOBILE_APP_ID_PROPERTY, columnDefinition = "uuid") + private UUID mobileAppId; + + @Id + @Column(name = MOBILE_APP_OAUTH2_CLIENT_CLIENT_ID_PROPERTY, columnDefinition = "uuid") + private UUID oauth2ClientId; + + public MobileAppOauth2ClientEntity() { + super(); + } + + public MobileAppOauth2ClientEntity(MobileAppOauth2Client domainOauth2Provider) { + mobileAppId = domainOauth2Provider.getMobileAppId().getId(); + oauth2ClientId = domainOauth2Provider.getOAuth2ClientId().getId(); + } + + @Override + public MobileAppOauth2Client toData() { + MobileAppOauth2Client result = new MobileAppOauth2Client(); + result.setMobileAppId(new MobileAppId(mobileAppId)); + result.setOAuth2ClientId(new OAuth2ClientId(oauth2ClientId)); + return result; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java similarity index 82% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java index 45ef7466f9..8428d346ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java @@ -25,13 +25,13 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; -import org.thingsboard.server.common.data.id.OAuth2RegistrationId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -46,11 +46,13 @@ import java.util.stream.Collectors; @Data @EqualsAndHashCode(callSuper = true) @Entity -@Table(name = ModelConstants.OAUTH2_REGISTRATION_TABLE_NAME) -public class OAuth2RegistrationEntity extends BaseSqlEntity { +@Table(name = ModelConstants.OAUTH2_CLIENT_TABLE_NAME) +public class OAuth2ClientEntity extends BaseSqlEntity { - @Column(name = ModelConstants.OAUTH2_PARAMS_ID_PROPERTY) - private UUID oauth2ParamsId; + @Column(name = ModelConstants.TENANT_ID_COLUMN) + private UUID tenantId; + @Column(name = ModelConstants.OAUTH2_CLIENT_TITLE_PROPERTY) + private String title; @Column(name = ModelConstants.OAUTH2_CLIENT_ID_PROPERTY) private String clientId; @Column(name = ModelConstants.OAUTH2_CLIENT_SECRET_PROPERTY) @@ -112,32 +114,30 @@ public class OAuth2RegistrationEntity extends BaseSqlEntity @Column(name = ModelConstants.OAUTH2_ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; - public OAuth2RegistrationEntity() { + public OAuth2ClientEntity() { super(); } - public OAuth2RegistrationEntity(OAuth2Registration registration) { - if (registration.getId() != null) { - this.setUuid(registration.getId().getId()); + public OAuth2ClientEntity(OAuth2Client oAuth2Client) { + super(oAuth2Client); + if (oAuth2Client.getTenantId() != null) { + this.tenantId = oAuth2Client.getTenantId().getId(); } - this.setCreatedTime(registration.getCreatedTime()); - if (registration.getOauth2ParamsId() != null) { - this.oauth2ParamsId = registration.getOauth2ParamsId().getId(); - } - this.clientId = registration.getClientId(); - this.clientSecret = registration.getClientSecret(); - this.authorizationUri = registration.getAuthorizationUri(); - this.tokenUri = registration.getAccessTokenUri(); - this.scope = registration.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); - this.platforms = registration.getPlatforms() != null ? registration.getPlatforms().stream().map(Enum::name).reduce((result, element) -> result + "," + element).orElse("") : ""; - this.userInfoUri = registration.getUserInfoUri(); - this.userNameAttributeName = registration.getUserNameAttributeName(); - this.jwkSetUri = registration.getJwkSetUri(); - this.clientAuthenticationMethod = registration.getClientAuthenticationMethod(); - this.loginButtonLabel = registration.getLoginButtonLabel(); - this.loginButtonIcon = registration.getLoginButtonIcon(); - this.additionalInfo = registration.getAdditionalInfo(); - OAuth2MapperConfig mapperConfig = registration.getMapperConfig(); + this.title = oAuth2Client.getTitle(); + this.clientId = oAuth2Client.getClientId(); + this.clientSecret = oAuth2Client.getClientSecret(); + this.authorizationUri = oAuth2Client.getAuthorizationUri(); + this.tokenUri = oAuth2Client.getAccessTokenUri(); + this.scope = oAuth2Client.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); + this.platforms = oAuth2Client.getPlatforms() != null ? oAuth2Client.getPlatforms().stream().map(Enum::name).reduce((result, element) -> result + "," + element).orElse("") : ""; + this.userInfoUri = oAuth2Client.getUserInfoUri(); + this.userNameAttributeName = oAuth2Client.getUserNameAttributeName(); + this.jwkSetUri = oAuth2Client.getJwkSetUri(); + this.clientAuthenticationMethod = oAuth2Client.getClientAuthenticationMethod(); + this.loginButtonLabel = oAuth2Client.getLoginButtonLabel(); + this.loginButtonIcon = oAuth2Client.getLoginButtonIcon(); + this.additionalInfo = oAuth2Client.getAdditionalInfo(); + OAuth2MapperConfig mapperConfig = oAuth2Client.getMapperConfig(); if (mapperConfig != null) { this.allowUserCreation = mapperConfig.isAllowUserCreation(); this.activateUser = mapperConfig.isActivateUser(); @@ -164,11 +164,12 @@ public class OAuth2RegistrationEntity extends BaseSqlEntity } @Override - public OAuth2Registration toData() { - OAuth2Registration registration = new OAuth2Registration(); - registration.setId(new OAuth2RegistrationId(id)); + public OAuth2Client toData() { + OAuth2Client registration = new OAuth2Client(); + registration.setId(new OAuth2ClientId(id)); registration.setCreatedTime(createdTime); - registration.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); + registration.setTenantId(new TenantId(tenantId)); + registration.setTitle(title); registration.setAdditionalInfo(additionalInfo); registration.setMapperConfig( OAuth2MapperConfig.builder() diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientInfoEntity.java new file mode 100644 index 0000000000..731cf70fae --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientInfoEntity.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2024 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.dao.model.sql; + +import jakarta.persistence.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.dao.model.BaseSqlEntity; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +public class OAuth2ClientInfoEntity extends BaseSqlEntity { + + private String platforms; + private String title; + + public OAuth2ClientInfoEntity() { + super(); + } + + public OAuth2ClientInfoEntity(UUID id, long createdTime, String platforms, String title) { + this.id = id; + this.createdTime = createdTime; + this.platforms = platforms; + this.title = title; + } + + @Override + public OAuth2ClientInfo toData() { + OAuth2ClientInfo oAuth2ClientInfo = new OAuth2ClientInfo(); + oAuth2ClientInfo.setId(new OAuth2ClientId(id)); + oAuth2ClientInfo.setCreatedTime(createdTime); + oAuth2ClientInfo.setTitle(title); + oAuth2ClientInfo.setPlatforms(StringUtils.isNotEmpty(platforms) ? Arrays.stream(platforms.split(",")) + .map(str -> PlatformType.valueOf(str)).collect(Collectors.toList()) : Collections.emptyList()); + return oAuth2ClientInfo; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2DomainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2DomainEntity.java deleted file mode 100644 index 884ee9cf1d..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2DomainEntity.java +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.model.sql; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.id.OAuth2DomainId; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; -import org.thingsboard.server.common.data.oauth2.OAuth2Domain; -import org.thingsboard.server.common.data.oauth2.SchemeType; -import org.thingsboard.server.dao.model.BaseSqlEntity; -import org.thingsboard.server.dao.model.ModelConstants; - -import java.util.UUID; - -@Data -@EqualsAndHashCode(callSuper = true) -@Entity -@Table(name = ModelConstants.OAUTH2_DOMAIN_TABLE_NAME) -public class OAuth2DomainEntity extends BaseSqlEntity { - - @Column(name = ModelConstants.OAUTH2_PARAMS_ID_PROPERTY) - private UUID oauth2ParamsId; - - @Column(name = ModelConstants.OAUTH2_DOMAIN_NAME_PROPERTY) - private String domainName; - - @Enumerated(EnumType.STRING) - @Column(name = ModelConstants.OAUTH2_DOMAIN_SCHEME_PROPERTY) - private SchemeType domainScheme; - - public OAuth2DomainEntity() { - super(); - } - - public OAuth2DomainEntity(OAuth2Domain domain) { - if (domain.getId() != null) { - this.setUuid(domain.getId().getId()); - } - this.setCreatedTime(domain.getCreatedTime()); - if (domain.getOauth2ParamsId() != null) { - this.oauth2ParamsId = domain.getOauth2ParamsId().getId(); - } - this.domainName = domain.getDomainName(); - this.domainScheme = domain.getDomainScheme(); - } - - @Override - public OAuth2Domain toData() { - OAuth2Domain domain = new OAuth2Domain(); - domain.setId(new OAuth2DomainId(id)); - domain.setCreatedTime(createdTime); - domain.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); - domain.setDomainName(domainName); - domain.setDomainScheme(domainScheme); - return domain; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java deleted file mode 100644 index e163d3f19c..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.model.sql; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2Params; -import org.thingsboard.server.dao.model.BaseSqlEntity; -import org.thingsboard.server.dao.model.ModelConstants; - -import java.util.UUID; - -@Data -@EqualsAndHashCode(callSuper = true) -@Entity -@Table(name = ModelConstants.OAUTH2_PARAMS_TABLE_NAME) -@NoArgsConstructor -public class OAuth2ParamsEntity extends BaseSqlEntity { - - @Column(name = ModelConstants.OAUTH2_PARAMS_ENABLED_PROPERTY) - private Boolean enabled; - - @Column(name = ModelConstants.OAUTH2_PARAMS_EDGE_ENABLED_PROPERTY) - private Boolean edgeEnabled; - - @Column(name = ModelConstants.OAUTH2_PARAMS_TENANT_ID_PROPERTY) - private UUID tenantId; - - public OAuth2ParamsEntity(OAuth2Params oauth2Params) { - if (oauth2Params.getId() != null) { - this.setUuid(oauth2Params.getUuidId()); - } - this.setCreatedTime(oauth2Params.getCreatedTime()); - this.enabled = oauth2Params.isEnabled(); - this.edgeEnabled = oauth2Params.isEdgeEnabled(); - if (oauth2Params.getTenantId() != null) { - this.tenantId = oauth2Params.getTenantId().getId(); - } - } - - @Override - public OAuth2Params toData() { - OAuth2Params oauth2Params = new OAuth2Params(); - oauth2Params.setId(new OAuth2ParamsId(id)); - oauth2Params.setCreatedTime(createdTime); - oauth2Params.setTenantId(TenantId.fromUUID(tenantId)); - oauth2Params.setEnabled(enabled); - oauth2Params.setEdgeEnabled(edgeEnabled); - return oauth2Params; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 004c89bd54..c23ab1e3d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -21,7 +21,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import java.util.UUID; @@ -30,29 +32,29 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep private static final String defaultRedirectUriTemplate = "{baseUrl}/login/oauth2/code/{registrationId}"; @Autowired - private OAuth2Service oAuth2Service; + private OAuth2ClientService oAuth2ClientService; @Override public ClientRegistration findByRegistrationId(String registrationId) { - OAuth2Registration registration = oAuth2Service.findRegistration(UUID.fromString(registrationId)); - return registration == null ? - null : toSpringClientRegistration(registration); + OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(registrationId))); + return oAuth2Client == null ? + null : toSpringClientRegistration(oAuth2Client); } - private ClientRegistration toSpringClientRegistration(OAuth2Registration registration){ - String registrationId = registration.getUuidId().toString(); + private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ + String registrationId = oAuth2Client.getUuidId().toString(); return ClientRegistration.withRegistrationId(registrationId) - .clientName(registration.getName()) - .clientId(registration.getClientId()) - .authorizationUri(registration.getAuthorizationUri()) - .clientSecret(registration.getClientSecret()) - .tokenUri(registration.getAccessTokenUri()) - .scope(registration.getScope()) + .clientName(oAuth2Client.getName()) + .clientId(oAuth2Client.getClientId()) + .authorizationUri(oAuth2Client.getAuthorizationUri()) + .clientSecret(oAuth2Client.getClientSecret()) + .tokenUri(oAuth2Client.getAccessTokenUri()) + .scope(oAuth2Client.getScope()) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .userInfoUri(registration.getUserInfoUri()) - .userNameAttributeName(registration.getUserNameAttributeName()) - .jwkSetUri(registration.getJwkSetUri()) - .clientAuthenticationMethod(registration.getClientAuthenticationMethod().equals("POST") ? + .userInfoUri(oAuth2Client.getUserInfoUri()) + .userNameAttributeName(oAuth2Client.getUserNameAttributeName()) + .jwkSetUri(oAuth2Client.getJwkSetUri()) + .clientAuthenticationMethod(oAuth2Client.getClientAuthenticationMethod().equals("POST") ? ClientAuthenticationMethod.CLIENT_SECRET_POST : ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .redirectUri(defaultRedirectUriTemplate) .build(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java new file mode 100644 index 0000000000..cf40a331fb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientDao.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 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.dao.oauth2; + +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2ClientDao extends Dao { + + PageData findByTenantId(UUID tenantId, PageLink pageLink); + + List findEnabledByDomainName(String domainName); + + List findEnabledByPkgNameAndPlatformType(String pkgName, PlatformType platformType); + + List findByDomainId(UUID domainId); + + List findByMobileAppId(UUID mobileAppId); + + String findAppSecret(UUID id, String pkgName); + + void deleteByTenantId(UUID tenantId); + + List findByIds(UUID tenantId, List oAuth2ClientIds); + + boolean isPropagateToEdge(TenantId tenantId, UUID oAuth2ClientId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java new file mode 100644 index 0000000000..6ab4909421 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -0,0 +1,159 @@ +/** + * Copyright © 2016-2024 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.dao.oauth2; + +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +@Slf4j +@Service("OAuth2ClientService") +public class OAuth2ClientServiceImpl extends AbstractEntityService implements OAuth2ClientService { + + @Autowired + private OAuth2ClientDao oauth2ClientDao; + @Autowired + private DataValidator oAuth2ClientDataValidator; + + @Override + public List findOAuth2ClientLoginInfosByDomainName(String domainName) { + log.trace("Executing findOAuth2ClientLoginInfosByDomainName [{}] ", domainName); + return oauth2ClientDao.findEnabledByDomainName(domainName) + .stream() + .map(OAuth2Utils::toClientLoginInfo) + .collect(Collectors.toList()); + } + + @Override + public List findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(String pkgName, PlatformType platformType) { + log.trace("Executing findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType pkgName=[{}] platformType=[{}]",pkgName, platformType); + return oauth2ClientDao.findEnabledByPkgNameAndPlatformType(pkgName, platformType) + .stream() + .map(OAuth2Utils::toClientLoginInfo) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public OAuth2Client saveOAuth2Client(TenantId tenantId, OAuth2Client oAuth2Client) { + log.trace("Executing saveOAuth2Client [{}]", oAuth2Client); + oAuth2ClientDataValidator.validate(oAuth2Client, OAuth2Client::getTenantId); + OAuth2Client savedOauth2Client = oauth2ClientDao.save(tenantId, oAuth2Client); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedOauth2Client.getId()).entity(savedOauth2Client).build()); + return savedOauth2Client; + } + + @Override + public OAuth2Client findOAuth2ClientById(TenantId tenantId, OAuth2ClientId oAuth2ClientId) { + log.trace("Executing findOAuth2ClientById [{}]", oAuth2ClientId); + return oauth2ClientDao.findById(tenantId, oAuth2ClientId.getId()); + } + + @Override + public List findOAuth2ClientsByTenantId(TenantId tenantId) { + log.trace("Executing findOAuth2ClientsByTenantId [{}]", tenantId); + return oauth2ClientDao.findByTenantId(tenantId.getId(), new PageLink(Integer.MAX_VALUE)).getData(); + } + + @Override + public String findAppSecret(OAuth2ClientId oAuth2ClientId, String pkgName) { + log.trace("Executing findAppSecret oAuth2ClientId = [{}] pkgName = [{}]", oAuth2ClientId, pkgName); + return oauth2ClientDao.findAppSecret(oAuth2ClientId.getId(), pkgName); + } + + @Override + @Transactional + public void deleteOAuth2ClientById(TenantId tenantId, OAuth2ClientId oAuth2ClientId) { + log.trace("Executing deleteOAuth2ClientById [{}]", oAuth2ClientId); + oauth2ClientDao.removeById(tenantId, oAuth2ClientId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder() + .tenantId(tenantId) + .entityId(oAuth2ClientId) + .build()); + + } + + @Override + public void deleteOauth2ClientsByTenantId(TenantId tenantId) { + log.trace("Executing deleteOauth2ClientsByTenantId, tenantId [{}]", tenantId); + oauth2ClientDao.deleteByTenantId(tenantId.getId()); + } + + @Override + public PageData findOAuth2ClientInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findOAuth2ClientInfosByTenantId tenantId=[{}]", tenantId); + PageData clients = oauth2ClientDao.findByTenantId(tenantId.getId(), pageLink); + return clients.mapData(OAuth2ClientInfo::new); + } + + @Override + public List findOAuth2ClientInfosByIds(TenantId tenantId, List oAuth2ClientIds) { + log.trace("Executing findQueueStatsByIds, tenantId [{}], oAuth2ClientIds [{}]", tenantId, oAuth2ClientIds); + return oauth2ClientDao.findByIds(tenantId.getId(), oAuth2ClientIds) + .stream() + .map(OAuth2ClientInfo::new) + .collect(Collectors.toList()); + } + + @Override + public boolean isPropagateOAuth2ClientToEdge(TenantId tenantId, OAuth2ClientId oAuth2ClientId) { + log.trace("Executing isPropagateOAuth2ClientToEdge, tenantId [{}], oAuth2ClientId [{}]", tenantId, oAuth2ClientId); + return oauth2ClientDao.isPropagateToEdge(tenantId, oAuth2ClientId.getId()); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + deleteOauth2ClientsByTenantId(tenantId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(findOAuth2ClientById(tenantId, new OAuth2ClientId(entityId.getId()))); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteOAuth2ClientById(tenantId, (OAuth2ClientId) id); + } + + @Override + public EntityType getEntityType() { + return EntityType.OAUTH2_CLIENT; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java deleted file mode 100644 index 96a91f7d3b..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.oauth2; - -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.common.data.oauth2.SchemeType; -import org.thingsboard.server.dao.Dao; - -import java.util.List; -import java.util.UUID; - -public interface OAuth2RegistrationDao extends Dao { - - List findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(List domainSchemes, String domainName, String pkgName, PlatformType platformType); - - List findByOAuth2ParamsId(UUID oauth2ParamsId); - - String findAppSecret(UUID id, String pkgName); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java deleted file mode 100644 index 5846a684a8..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ /dev/null @@ -1,295 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.oauth2; - -import jakarta.transaction.Transactional; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.MapperType; -import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Domain; -import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; -import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; -import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Params; -import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; -import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.common.data.oauth2.SchemeType; -import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; -import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.IncorrectParameterException; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateString; - -@Slf4j -@Service -public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Service { - - public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_CLIENT_REGISTRATION_ID = "Incorrect clientRegistrationId "; - public static final String INCORRECT_DOMAIN_NAME = "Incorrect domainName "; - public static final String INCORRECT_DOMAIN_SCHEME = "Incorrect domainScheme "; - - @Autowired - private OAuth2ParamsDao oauth2ParamsDao; - @Autowired - private OAuth2RegistrationDao oauth2RegistrationDao; - @Autowired - private OAuth2DomainDao oauth2DomainDao; - @Autowired - private OAuth2MobileDao oauth2MobileDao; - - @Override - public List getOAuth2Clients(String domainSchemeStr, String domainName, String pkgName, PlatformType platformType) { - log.trace("Executing getOAuth2Clients [{}://{}] pkgName=[{}] platformType=[{}]", domainSchemeStr, domainName, pkgName, platformType); - if (domainSchemeStr == null) { - throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); - } - SchemeType domainScheme; - try { - domainScheme = SchemeType.valueOf(domainSchemeStr.toUpperCase()); - } catch (IllegalArgumentException e){ - throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); - } - validateString(domainName, dn -> INCORRECT_DOMAIN_NAME + dn); - return oauth2RegistrationDao.findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType( - Arrays.asList(domainScheme, SchemeType.MIXED), domainName, pkgName, platformType) - .stream() - .map(OAuth2Utils::toClientInfo) - .collect(Collectors.toList()); - } - - @Override - @Transactional - public void saveOAuth2Info(OAuth2Info oauth2Info) { - log.trace("Executing saveOAuth2Info [{}]", oauth2Info); - oauth2InfoValidator.accept(oauth2Info); - oauth2ParamsDao.deleteAll(); - oauth2Info.getOauth2ParamsInfos().forEach(oauth2ParamsInfo -> { - OAuth2Params oauth2Params = OAuth2Utils.infoToOAuth2Params(oauth2Info); - OAuth2Params savedOauth2Params = oauth2ParamsDao.save(TenantId.SYS_TENANT_ID, oauth2Params); - oauth2ParamsInfo.getClientRegistrations().forEach(registrationInfo -> { - OAuth2Registration registration = OAuth2Utils.toOAuth2Registration(savedOauth2Params.getId(), registrationInfo); - oauth2RegistrationDao.save(TenantId.SYS_TENANT_ID, registration); - }); - oauth2ParamsInfo.getDomainInfos().forEach(domainInfo -> { - OAuth2Domain domain = OAuth2Utils.toOAuth2Domain(savedOauth2Params.getId(), domainInfo); - oauth2DomainDao.save(TenantId.SYS_TENANT_ID, domain); - }); - if (oauth2ParamsInfo.getMobileInfos() != null) { - oauth2ParamsInfo.getMobileInfos().forEach(mobileInfo -> { - OAuth2Mobile mobile = OAuth2Utils.toOAuth2Mobile(savedOauth2Params.getId(), mobileInfo); - oauth2MobileDao.save(TenantId.SYS_TENANT_ID, mobile); - }); - } - }); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entity(oauth2Info).build()); - } - - @Override - public OAuth2Info findOAuth2Info() { - log.trace("Executing findOAuth2Info"); - OAuth2Info oauth2Info = new OAuth2Info(); - List oauth2ParamsList = oauth2ParamsDao.find(TenantId.SYS_TENANT_ID); - oauth2Info.setEnabled(oauth2ParamsList.stream().anyMatch(OAuth2Params::isEnabled)); - oauth2Info.setEdgeEnabled(oauth2ParamsList.stream().anyMatch(OAuth2Params::isEdgeEnabled)); - List oauth2ParamsInfos = new ArrayList<>(); - oauth2Info.setOauth2ParamsInfos(oauth2ParamsInfos); - oauth2ParamsList.stream().sorted(Comparator.comparing(BaseData::getUuidId)).forEach(oauth2Params -> { - List registrations = oauth2RegistrationDao.findByOAuth2ParamsId(oauth2Params.getId().getId()); - List domains = oauth2DomainDao.findByOAuth2ParamsId(oauth2Params.getId().getId()); - List mobiles = oauth2MobileDao.findByOAuth2ParamsId(oauth2Params.getId().getId()); - oauth2ParamsInfos.add(OAuth2Utils.toOAuth2ParamsInfo(registrations, domains, mobiles)); - }); - return oauth2Info; - } - - @Override - public OAuth2Registration findRegistration(UUID id) { - log.trace("Executing findRegistration [{}]", id); - validateId(id, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); - return oauth2RegistrationDao.findById(null, id); - } - - @Override - public String findAppSecret(UUID id, String pkgName) { - log.trace("Executing findAppSecret [{}][{}]", id, pkgName); - validateId(id, uuid -> INCORRECT_CLIENT_REGISTRATION_ID + uuid); - validateString(pkgName, "Incorrect package name"); - return oauth2RegistrationDao.findAppSecret(id, pkgName); - } - - - @Override - public List findAllRegistrations() { - log.trace("Executing findAllRegistrations"); - return oauth2RegistrationDao.find(TenantId.SYS_TENANT_ID); - } - - private final Consumer oauth2InfoValidator = oauth2Info -> { - if (oauth2Info == null - || oauth2Info.getOauth2ParamsInfos() == null) { - throw new DataValidationException("OAuth2 param infos should be specified!"); - } - for (OAuth2ParamsInfo oauth2Params : oauth2Info.getOauth2ParamsInfos()) { - if (oauth2Params.getDomainInfos() == null - || oauth2Params.getDomainInfos().isEmpty()) { - throw new DataValidationException("List of domain configuration should be specified!"); - } - for (OAuth2DomainInfo domainInfo : oauth2Params.getDomainInfos()) { - if (StringUtils.isEmpty(domainInfo.getName())) { - throw new DataValidationException("Domain name should be specified!"); - } - if (domainInfo.getScheme() == null) { - throw new DataValidationException("Domain scheme should be specified!"); - } - } - oauth2Params.getDomainInfos().stream() - .collect(Collectors.groupingBy(OAuth2DomainInfo::getName)) - .forEach((domainName, domainInfos) -> { - if (domainInfos.size() > 1 && domainInfos.stream().anyMatch(domainInfo -> domainInfo.getScheme() == SchemeType.MIXED)) { - throw new DataValidationException("MIXED scheme type shouldn't be combined with another scheme type!"); - } - domainInfos.stream() - .collect(Collectors.groupingBy(OAuth2DomainInfo::getScheme)) - .forEach((schemeType, domainInfosBySchemeType) -> { - if (domainInfosBySchemeType.size() > 1) { - throw new DataValidationException("Domain name and protocol must be unique within OAuth2 parameters!"); - } - }); - }); - if (oauth2Params.getMobileInfos() != null) { - for (OAuth2MobileInfo mobileInfo : oauth2Params.getMobileInfos()) { - if (StringUtils.isEmpty(mobileInfo.getPkgName())) { - throw new DataValidationException("Package should be specified!"); - } - if (StringUtils.isEmpty(mobileInfo.getAppSecret())) { - throw new DataValidationException("Application secret should be specified!"); - } - if (mobileInfo.getAppSecret().length() < 16) { - throw new DataValidationException("Application secret should be at least 16 characters!"); - } - } - oauth2Params.getMobileInfos().stream() - .collect(Collectors.groupingBy(OAuth2MobileInfo::getPkgName)) - .forEach((pkgName, mobileInfos) -> { - if (mobileInfos.size() > 1) { - throw new DataValidationException("Mobile app package name must be unique within OAuth2 parameters!"); - } - }); - } - if (oauth2Params.getClientRegistrations() == null || oauth2Params.getClientRegistrations().isEmpty()) { - throw new DataValidationException("Client registrations should be specified!"); - } - for (OAuth2RegistrationInfo clientRegistration : oauth2Params.getClientRegistrations()) { - if (StringUtils.isEmpty(clientRegistration.getClientId())) { - throw new DataValidationException("Client ID should be specified!"); - } - if (StringUtils.isEmpty(clientRegistration.getClientSecret())) { - throw new DataValidationException("Client secret should be specified!"); - } - if (StringUtils.isEmpty(clientRegistration.getAuthorizationUri())) { - throw new DataValidationException("Authorization uri should be specified!"); - } - if (StringUtils.isEmpty(clientRegistration.getAccessTokenUri())) { - throw new DataValidationException("Token uri should be specified!"); - } - if (CollectionUtils.isEmpty(clientRegistration.getScope())) { - throw new DataValidationException("Scope should be specified!"); - } - if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) { - throw new DataValidationException("User name attribute name should be specified!"); - } - if (StringUtils.isEmpty(clientRegistration.getClientAuthenticationMethod())) { - throw new DataValidationException("Client authentication method should be specified!"); - } - if (StringUtils.isEmpty(clientRegistration.getLoginButtonLabel())) { - throw new DataValidationException("Login button label should be specified!"); - } - OAuth2MapperConfig mapperConfig = clientRegistration.getMapperConfig(); - if (mapperConfig == null) { - throw new DataValidationException("Mapper config should be specified!"); - } - if (mapperConfig.getType() == null) { - throw new DataValidationException("Mapper config type should be specified!"); - } - if (mapperConfig.getType() == MapperType.BASIC) { - OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); - if (basicConfig == null) { - throw new DataValidationException("Basic config should be specified!"); - } - if (StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { - throw new DataValidationException("Email attribute key should be specified!"); - } - if (basicConfig.getTenantNameStrategy() == null) { - throw new DataValidationException("Tenant name strategy should be specified!"); - } - if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM - && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { - throw new DataValidationException("Tenant name pattern should be specified!"); - } - } - if (mapperConfig.getType() == MapperType.GITHUB) { - OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); - if (basicConfig == null) { - throw new DataValidationException("Basic config should be specified!"); - } - if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { - throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!"); - } - if (basicConfig.getTenantNameStrategy() == null) { - throw new DataValidationException("Tenant name strategy should be specified!"); - } - if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM - && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { - throw new DataValidationException("Tenant name pattern should be specified!"); - } - } - if (mapperConfig.getType() == MapperType.CUSTOM) { - OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom(); - if (customConfig == null) { - throw new DataValidationException("Custom config should be specified!"); - } - if (StringUtils.isEmpty(customConfig.getUrl())) { - throw new DataValidationException("Custom mapper URL should be specified!"); - } - } - } - } - }; -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java index 8d30fc905d..9a4702dac5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -15,117 +15,18 @@ */ package org.thingsboard.server.dao.oauth2; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.id.OAuth2ParamsId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Domain; -import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; -import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; -import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Params; -import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; - -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; public class OAuth2Utils { public static final String OAUTH2_AUTHORIZATION_PATH_TEMPLATE = "/oauth2/authorization/%s"; - public static OAuth2ClientInfo toClientInfo(OAuth2Registration registration) { - OAuth2ClientInfo client = new OAuth2ClientInfo(); + public static OAuth2ClientLoginInfo toClientLoginInfo(OAuth2Client registration) { + OAuth2ClientLoginInfo client = new OAuth2ClientLoginInfo(); client.setName(registration.getLoginButtonLabel()); client.setUrl(String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, registration.getUuidId().toString())); client.setIcon(registration.getLoginButtonIcon()); return client; } - public static OAuth2ParamsInfo toOAuth2ParamsInfo(List registrations, List domains, List mobiles) { - OAuth2ParamsInfo oauth2ParamsInfo = new OAuth2ParamsInfo(); - oauth2ParamsInfo.setClientRegistrations(registrations.stream().sorted(Comparator.comparing(BaseData::getUuidId)).map(OAuth2Utils::toOAuth2RegistrationInfo).collect(Collectors.toList())); - oauth2ParamsInfo.setDomainInfos(domains.stream().sorted(Comparator.comparing(BaseData::getUuidId)).map(OAuth2Utils::toOAuth2DomainInfo).collect(Collectors.toList())); - oauth2ParamsInfo.setMobileInfos(mobiles.stream().sorted(Comparator.comparing(BaseData::getUuidId)).map(OAuth2Utils::toOAuth2MobileInfo).collect(Collectors.toList())); - return oauth2ParamsInfo; - } - - public static OAuth2RegistrationInfo toOAuth2RegistrationInfo(OAuth2Registration registration) { - return OAuth2RegistrationInfo.builder() - .mapperConfig(registration.getMapperConfig()) - .clientId(registration.getClientId()) - .clientSecret(registration.getClientSecret()) - .authorizationUri(registration.getAuthorizationUri()) - .accessTokenUri(registration.getAccessTokenUri()) - .scope(registration.getScope()) - .platforms(registration.getPlatforms()) - .userInfoUri(registration.getUserInfoUri()) - .userNameAttributeName(registration.getUserNameAttributeName()) - .jwkSetUri(registration.getJwkSetUri()) - .clientAuthenticationMethod(registration.getClientAuthenticationMethod()) - .loginButtonLabel(registration.getLoginButtonLabel()) - .loginButtonIcon(registration.getLoginButtonIcon()) - .additionalInfo(registration.getAdditionalInfo()) - .build(); - } - - public static OAuth2DomainInfo toOAuth2DomainInfo(OAuth2Domain domain) { - return OAuth2DomainInfo.builder() - .name(domain.getDomainName()) - .scheme(domain.getDomainScheme()) - .build(); - } - - public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) { - return OAuth2MobileInfo.builder() - .pkgName(mobile.getPkgName()) - .appSecret(mobile.getAppSecret()) - .build(); - } - - public static OAuth2Params infoToOAuth2Params(OAuth2Info oauth2Info) { - OAuth2Params oauth2Params = new OAuth2Params(); - oauth2Params.setEnabled(oauth2Info.isEnabled()); - oauth2Params.setEdgeEnabled(oauth2Info.isEdgeEnabled()); - oauth2Params.setTenantId(TenantId.SYS_TENANT_ID); - return oauth2Params; - } - - public static OAuth2Registration toOAuth2Registration(OAuth2ParamsId oauth2ParamsId, OAuth2RegistrationInfo registrationInfo) { - OAuth2Registration registration = new OAuth2Registration(); - registration.setOauth2ParamsId(oauth2ParamsId); - registration.setMapperConfig(registrationInfo.getMapperConfig()); - registration.setClientId(registrationInfo.getClientId()); - registration.setClientSecret(registrationInfo.getClientSecret()); - registration.setAuthorizationUri(registrationInfo.getAuthorizationUri()); - registration.setAccessTokenUri(registrationInfo.getAccessTokenUri()); - registration.setScope(registrationInfo.getScope()); - registration.setPlatforms(registrationInfo.getPlatforms()); - registration.setUserInfoUri(registrationInfo.getUserInfoUri()); - registration.setUserNameAttributeName(registrationInfo.getUserNameAttributeName()); - registration.setJwkSetUri(registrationInfo.getJwkSetUri()); - registration.setClientAuthenticationMethod(registrationInfo.getClientAuthenticationMethod()); - registration.setLoginButtonLabel(registrationInfo.getLoginButtonLabel()); - registration.setLoginButtonIcon(registrationInfo.getLoginButtonIcon()); - registration.setAdditionalInfo(registrationInfo.getAdditionalInfo()); - return registration; - } - - public static OAuth2Domain toOAuth2Domain(OAuth2ParamsId oauth2ParamsId, OAuth2DomainInfo domainInfo) { - OAuth2Domain domain = new OAuth2Domain(); - domain.setOauth2ParamsId(oauth2ParamsId); - domain.setDomainName(domainInfo.getName()); - domain.setDomainScheme(domainInfo.getScheme()); - return domain; - } - - public static OAuth2Mobile toOAuth2Mobile(OAuth2ParamsId oauth2ParamsId, OAuth2MobileInfo mobileInfo) { - OAuth2Mobile mobile = new OAuth2Mobile(); - mobile.setOauth2ParamsId(oauth2ParamsId); - mobile.setPkgName(mobileInfo.getPkgName()); - mobile.setAppSecret(mobileInfo.getAppSecret()); - return mobile; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java index d13f2e6ad8..1623e5ea9a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.validation.Length; @Slf4j public class StringLengthValidator implements ConstraintValidator { private int max; + private int min; @Override public boolean isValid(Object value, ConstraintValidatorContext context) { @@ -34,14 +35,15 @@ public class StringLengthValidator implements ConstraintValidator= min && stringValue.length() <= max; } @Override public void initialize(Length constraintAnnotation) { this.max = constraintAnnotation.max(); + this.min = constraintAnnotation.min(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java new file mode 100644 index 0000000000..78197ed1ac --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/Oauth2ClientDataValidator.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2024 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.dao.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class Oauth2ClientDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, OAuth2Client oAuth2Client) { + OAuth2MapperConfig mapperConfig = oAuth2Client.getMapperConfig(); + if (mapperConfig.getType() == MapperType.BASIC) { + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig == null) { + throw new DataValidationException("Basic config should be specified!"); + } + if (StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { + throw new DataValidationException("Email attribute key should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == null) { + throw new DataValidationException("Tenant name strategy should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM + && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { + throw new DataValidationException("Tenant name pattern should be specified!"); + } + } + if (mapperConfig.getType() == MapperType.GITHUB) { + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig == null) { + throw new DataValidationException("Basic config should be specified!"); + } + if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { + throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!"); + } + if (basicConfig.getTenantNameStrategy() == null) { + throw new DataValidationException("Tenant name strategy should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM + && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { + throw new DataValidationException("Tenant name pattern should be specified!"); + } + } + if (mapperConfig.getType() == MapperType.CUSTOM) { + OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom(); + if (customConfig == null) { + throw new DataValidationException("Custom config should be specified!"); + } + if (StringUtils.isEmpty(customConfig.getUrl())) { + throw new DataValidationException("Custom mapper URL should be specified!"); + } + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2DomainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainOauth2ClientRepository.java similarity index 65% rename from dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2DomainRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainOauth2ClientRepository.java index eec1bc5f50..40e9d2166f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2DomainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainOauth2ClientRepository.java @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.oauth2; +package org.thingsboard.server.dao.sql.domain; import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.OAuth2DomainEntity; +import org.thingsboard.server.dao.model.sql.DomainOauth2ClientCompositeKey; +import org.thingsboard.server.dao.model.sql.DomainOauth2ClientEntity; import java.util.List; import java.util.UUID; -public interface OAuth2DomainRepository extends JpaRepository { +public interface DomainOauth2ClientRepository extends JpaRepository { - List findByOauth2ParamsId(UUID oauth2ParamsId); + List findAllByDomainId(UUID domainId); } - diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainRepository.java new file mode 100644 index 0000000000..bf3ad8f9cf --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/DomainRepository.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 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.dao.sql.domain; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.DomainEntity; + +import java.util.UUID; + +public interface DomainRepository extends JpaRepository { + + @Query("SELECT d FROM DomainEntity d WHERE d.tenantId = :tenantId AND " + + "(:searchText is NULL OR ilike(d.name, concat('%', :searchText, '%')) = true)") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + + @Transactional + @Modifying + @Query("DELETE FROM DomainEntity r WHERE r.tenantId = :tenantId") + void deleteByTenantId(@Param("tenantId") UUID tenantId); + + int countByTenantIdAndOauth2Enabled(UUID tenantId, boolean oauth2Enabled); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java new file mode 100644 index 0000000000..c2077bc2a1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/domain/JpaDomainDao.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2024 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.dao.sql.domain; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainOauth2Client; +import org.thingsboard.server.common.data.id.DomainId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +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.dao.DaoUtil; +import org.thingsboard.server.dao.domain.DomainDao; +import org.thingsboard.server.dao.model.sql.DomainEntity; +import org.thingsboard.server.dao.model.sql.DomainOauth2ClientCompositeKey; +import org.thingsboard.server.dao.model.sql.DomainOauth2ClientEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@SqlDao +public class JpaDomainDao extends JpaAbstractDao implements DomainDao { + + private final DomainRepository domainRepository; + private final DomainOauth2ClientRepository domainOauth2ClientRepository; + + @Override + protected Class getEntityClass() { + return DomainEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return domainRepository; + } + + @Override + public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(domainRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public int countDomainByTenantIdAndOauth2Enabled(TenantId tenantId, boolean enabled) { + return domainRepository.countByTenantIdAndOauth2Enabled(tenantId.getId(), enabled); + } + + @Override + public List findOauth2ClientsByDomainId(TenantId tenantId, DomainId domainId) { + return DaoUtil.convertDataList(domainOauth2ClientRepository.findAllByDomainId(domainId.getId())); + } + + @Override + public void addOauth2Client(DomainOauth2Client domainOauth2Client) { + domainOauth2ClientRepository.save(new DomainOauth2ClientEntity(domainOauth2Client)); + } + + @Override + public void removeOauth2Client(DomainOauth2Client domainOauth2Client) { + domainOauth2ClientRepository.deleteById(new DomainOauth2ClientCompositeKey(domainOauth2Client.getDomainId().getId(), + domainOauth2Client.getOAuth2ClientId().getId())); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + domainRepository.deleteByTenantId(tenantId.getId()); + } + + @Override + public EntityType getEntityType() { + return EntityType.DOMAIN; + } +} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java new file mode 100644 index 0000000000..0453481d08 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java @@ -0,0 +1,89 @@ +/** + * Copyright © 2016-2024 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.dao.sql.mobile; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppOauth2Client; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.mobile.MobileAppDao; +import org.thingsboard.server.dao.model.sql.MobileAppEntity; +import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey; +import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@SqlDao +public class JpaMobileAppDao extends JpaAbstractDao implements MobileAppDao { + + private final MobileAppRepository mobileAppRepository; + private final MobileAppOauth2ClientRepository mobileOauth2ProviderRepository; + + @Override + protected Class getEntityClass() { + return MobileAppEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return mobileAppRepository; + } + + @Override + public PageData findByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(mobileAppRepository.findByTenantId(tenantId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public List findOauth2ClientsByMobileAppId(TenantId tenantId, MobileAppId mobileAppId) { + return DaoUtil.convertDataList(mobileOauth2ProviderRepository.findAllByMobileAppId(mobileAppId.getId())); + } + + @Override + public void addOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) { + mobileOauth2ProviderRepository.save(new MobileAppOauth2ClientEntity(mobileAppOauth2Client)); + } + + @Override + public void removeOauth2Client(MobileAppOauth2Client mobileAppOauth2Client) { + mobileOauth2ProviderRepository.deleteById(new MobileAppOauth2ClientCompositeKey(mobileAppOauth2Client.getMobileAppId().getId(), + mobileAppOauth2Client.getOAuth2ClientId().getId())); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + mobileAppRepository.deleteByTenantId(tenantId.getId()); + } + + @Override + public EntityType getEntityType() { + return EntityType.MOBILE_APP; + } + +} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2MobileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java similarity index 63% rename from dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2MobileRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java index 2456494a74..9abf9b3b61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2MobileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppOauth2ClientRepository.java @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.oauth2; +package org.thingsboard.server.dao.sql.mobile; import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.OAuth2MobileEntity; +import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientCompositeKey; +import org.thingsboard.server.dao.model.sql.MobileAppOauth2ClientEntity; import java.util.List; import java.util.UUID; -public interface OAuth2MobileRepository extends JpaRepository { +public interface MobileAppOauth2ClientRepository extends JpaRepository { - List findByOauth2ParamsId(UUID oauth2ParamsId); + List findAllByMobileAppId(UUID mobileAppId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java new file mode 100644 index 0000000000..185a277644 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppRepository.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 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.dao.sql.mobile; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.MobileAppEntity; + +import java.util.UUID; + +public interface MobileAppRepository extends JpaRepository { + + @Query("SELECT a FROM MobileAppEntity a WHERE a.tenantId = :tenantId AND " + + "(:searchText is NULL OR ilike(a.pkgName, concat('%', :searchText, '%')) = true)") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + + @Transactional + @Modifying + @Query("DELETE FROM MobileAppEntity r WHERE r.tenantId = :tenantId") + void deleteByTenantId(@Param("tenantId") UUID tenantId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java new file mode 100644 index 0000000000..4a585186d9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientDao.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2024 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.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.OAuth2ClientId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2ClientEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; +import java.util.UUID; + +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; + +@Component +@RequiredArgsConstructor +@SqlDao +public class JpaOAuth2ClientDao extends JpaAbstractDao implements OAuth2ClientDao { + + private final OAuth2ClientRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ClientEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return repository; + } + + @Override + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(repository.findByTenantId(tenantId, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public List findEnabledByDomainName(String domainName) { + return DaoUtil.convertDataList(repository.findEnabledByDomainNameAndPlatformType(domainName, PlatformType.WEB.name())); + } + + @Override + public List findEnabledByPkgNameAndPlatformType(String pkgName, PlatformType platformType) { + return DaoUtil.convertDataList(repository.findEnabledByPkgNameAndPlatformType(pkgName, + platformType != null ? platformType.name() : null)); + } + + @Override + public List findByDomainId(UUID oauth2ParamsId) { + return DaoUtil.convertDataList(repository.findByDomainId(oauth2ParamsId)); + } + + @Override + public List findByMobileAppId(UUID mobileAppId) { + return DaoUtil.convertDataList(repository.findByMobileAppId(mobileAppId)); + } + + @Override + public String findAppSecret(UUID id, String pkgName) { + return repository.findAppSecret(id, pkgName); + } + + @Override + public void deleteByTenantId(UUID tenantId) { + repository.deleteByTenantId(tenantId); + } + + @Override + public List findByIds(UUID tenantId, List oAuth2ClientIds) { + return DaoUtil.convertDataList(repository.findByTenantIdAndIdIn(tenantId, toUUIDs(oAuth2ClientIds))); + } + + @Override + public boolean isPropagateToEdge(TenantId tenantId, UUID oAuth2ClientId) { + return repository.isPropagateToEdge(tenantId.getId(), oAuth2ClientId); + } + + @Override + public EntityType getEntityType() { + return EntityType.OAUTH2_CLIENT; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2DomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2DomainDao.java deleted file mode 100644 index 2eac50ba02..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2DomainDao.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.sql.oauth2; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.OAuth2Domain; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.sql.OAuth2DomainEntity; -import org.thingsboard.server.dao.oauth2.OAuth2DomainDao; -import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; - -import java.util.List; -import java.util.UUID; - -@Component -@RequiredArgsConstructor -@SqlDao -public class JpaOAuth2DomainDao extends JpaAbstractDao implements OAuth2DomainDao { - - private final OAuth2DomainRepository repository; - - @Override - protected Class getEntityClass() { - return OAuth2DomainEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return repository; - } - - @Override - public List findByOAuth2ParamsId(UUID oauth2ParamsId) { - return DaoUtil.convertDataList(repository.findByOauth2ParamsId(oauth2ParamsId)); - } - -} - diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2MobileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2MobileDao.java deleted file mode 100644 index c5c8a50636..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2MobileDao.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.sql.oauth2; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.sql.OAuth2MobileEntity; -import org.thingsboard.server.dao.oauth2.OAuth2MobileDao; -import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; - -import java.util.List; -import java.util.UUID; - -@Component -@RequiredArgsConstructor -@SqlDao -public class JpaOAuth2MobileDao extends JpaAbstractDao implements OAuth2MobileDao { - - private final OAuth2MobileRepository repository; - - @Override - protected Class getEntityClass() { - return OAuth2MobileEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return repository; - } - - @Override - public List findByOAuth2ParamsId(UUID oauth2ParamsId) { - return DaoUtil.convertDataList(repository.findByOauth2ParamsId(oauth2ParamsId)); - } - -} - diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ParamsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ParamsDao.java deleted file mode 100644 index e4de6c2ea8..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ParamsDao.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.sql.oauth2; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.OAuth2Params; -import org.thingsboard.server.dao.model.sql.OAuth2ParamsEntity; -import org.thingsboard.server.dao.oauth2.OAuth2ParamsDao; -import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; - -import java.util.UUID; - -@Component -@RequiredArgsConstructor -@SqlDao -public class JpaOAuth2ParamsDao extends JpaAbstractDao implements OAuth2ParamsDao { - private final OAuth2ParamsRepository repository; - - @Override - protected Class getEntityClass() { - return OAuth2ParamsEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return repository; - } - - @Override - public void deleteAll() { - repository.deleteAll(); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java deleted file mode 100644 index 09060d5106..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.sql.oauth2; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.common.data.oauth2.SchemeType; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.sql.OAuth2RegistrationEntity; -import org.thingsboard.server.dao.oauth2.OAuth2RegistrationDao; -import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; - -import java.util.List; -import java.util.UUID; - -@Component -@RequiredArgsConstructor -@SqlDao -public class JpaOAuth2RegistrationDao extends JpaAbstractDao implements OAuth2RegistrationDao { - - private final OAuth2RegistrationRepository repository; - - @Override - protected Class getEntityClass() { - return OAuth2RegistrationEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return repository; - } - - @Override - public List findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(List domainSchemes, String domainName, String pkgName, PlatformType platformType) { - return DaoUtil.convertDataList(repository.findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(domainSchemes, domainName, pkgName, - platformType != null ? "%" + platformType.name() + "%" : null)); - } - - @Override - public List findByOAuth2ParamsId(UUID oauth2ParamsId) { - return DaoUtil.convertDataList(repository.findByOauth2ParamsId(oauth2ParamsId)); - } - - @Override - public String findAppSecret(UUID id, String pkgName) { - return repository.findAppSecret(id, pkgName); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java new file mode 100644 index 0000000000..6805ca06ee --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRepository.java @@ -0,0 +1,89 @@ +/** + * Copyright © 2016-2024 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.dao.sql.oauth2; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.OAuth2ClientEntity; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2ClientRepository extends JpaRepository { + + @Query("SELECT с FROM OAuth2ClientEntity с WHERE с.tenantId = :tenantId AND " + + "(:searchText is NULL OR ilike(с.title, concat('%', :searchText, '%')) = true)") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT c " + + "FROM OAuth2ClientEntity c " + + "LEFT JOIN DomainOauth2ClientEntity dc on c.id = dc.oauth2ClientId " + + "LEFT JOIN DomainEntity domain on dc.domainId = domain.id " + + "WHERE domain.name = :domainName AND domain.oauth2Enabled = true " + + "AND (:platformFilter IS NULL OR c.platforms IS NULL OR c.platforms = '' OR ilike(c.platforms, CONCAT('%', :platformFilter, '%')) = true)") + List findEnabledByDomainNameAndPlatformType(@Param("domainName") String domainName, + @Param("platformFilter") String platformFilter); + + @Query("SELECT c " + + "FROM OAuth2ClientEntity c " + + "LEFT JOIN MobileAppOauth2ClientEntity mc on c.id = mc.oauth2ClientId " + + "LEFT JOIN MobileAppEntity app on mc.mobileAppId = app.id " + + "WHERE app.pkgName = :pkgName AND app.oauth2Enabled = true " + + "AND (:platformFilter IS NULL OR c.platforms IS NULL OR c.platforms = '' OR ilike(c.platforms, CONCAT('%', :platformFilter, '%')) = true)") + List findEnabledByPkgNameAndPlatformType(@Param("pkgName") String pkgName, + @Param("platformFilter") String platformFilter); + + @Query("SELECT c " + + "FROM OAuth2ClientEntity c " + + "LEFT JOIN DomainOauth2ClientEntity dc on dc.oauth2ClientId = c.id " + + "WHERE dc.domainId = :domainId ") + List findByDomainId(@Param("domainId") UUID domainId); + + @Query("SELECT c " + + "FROM OAuth2ClientEntity c " + + "LEFT JOIN MobileAppOauth2ClientEntity mc on mc.oauth2ClientId = c.id " + + "WHERE mc.mobileAppId = :mobileAppId ") + List findByMobileAppId(@Param("mobileAppId") UUID mobileAppId); + + @Query("SELECT m.appSecret " + + "FROM MobileAppEntity m " + + "LEFT JOIN MobileAppOauth2ClientEntity mp on m.id = mp.mobileAppId " + + "LEFT JOIN OAuth2ClientEntity p on mp.oauth2ClientId = p.id " + + "WHERE p.id = :clientId " + + "AND m.pkgName = :pkgName") + String findAppSecret(@Param("clientId") UUID id, + @Param("pkgName") String pkgName); + + @Transactional + @Modifying + @Query("DELETE FROM OAuth2ClientEntity t WHERE t.tenantId = :tenantId") + void deleteByTenantId(@Param("tenantId") UUID tenantId); + + List findByTenantIdAndIdIn(UUID tenantId, List uuids); + + @Query("SELECT COUNT(d) > 0 FROM DomainEntity d " + + "JOIN DomainOauth2ClientEntity doc ON d.id = doc.domainId " + + "WHERE d.tenantId = :tenantId AND doc.oauth2ClientId = :oAuth2ClientId AND d.propagateToEdge = true") + boolean isPropagateToEdge(@Param("tenantId") UUID tenantId, @Param("oAuth2ClientId") UUID oAuth2ClientId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2RegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2RegistrationRepository.java deleted file mode 100644 index a8315eb654..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2RegistrationRepository.java +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.sql.oauth2; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.thingsboard.server.common.data.oauth2.SchemeType; -import org.thingsboard.server.dao.model.sql.OAuth2RegistrationEntity; - -import java.util.List; -import java.util.UUID; - -public interface OAuth2RegistrationRepository extends JpaRepository { - - @Query("SELECT reg " + - "FROM OAuth2RegistrationEntity reg " + - "LEFT JOIN OAuth2ParamsEntity params on reg.oauth2ParamsId = params.id " + - "LEFT JOIN OAuth2DomainEntity domain on reg.oauth2ParamsId = domain.oauth2ParamsId " + - "WHERE params.enabled = true " + - "AND domain.domainName = :domainName " + - "AND domain.domainScheme IN (:domainSchemes) " + - "AND (:pkgName IS NULL OR EXISTS (SELECT mobile FROM OAuth2MobileEntity mobile WHERE mobile.oauth2ParamsId = reg.oauth2ParamsId AND mobile.pkgName = :pkgName)) " + - "AND (:platformFilter IS NULL OR reg.platforms IS NULL OR reg.platforms = '' OR reg.platforms LIKE :platformFilter)") - List findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(@Param("domainSchemes") List domainSchemes, - @Param("domainName") String domainName, - @Param("pkgName") String pkgName, - @Param("platformFilter") String platformFilter); - - List findByOauth2ParamsId(UUID oauth2ParamsId); - - @Query("SELECT mobile.appSecret " + - "FROM OAuth2MobileEntity mobile " + - "LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " + - "WHERE reg.id = :registrationId " + - "AND mobile.pkgName = :pkgName") - String findAppSecret(@Param("registrationId") UUID id, - @Param("pkgName") String pkgName); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index e86abdbb8b..8e6c38814a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -177,7 +177,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService platforms) { + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTenantId(tenantId); + oAuth2Client.setTitle(title); + oAuth2Client.setClientId(UUID.randomUUID().toString()); + oAuth2Client.setClientSecret(UUID.randomUUID().toString()); + oAuth2Client.setAuthorizationUri(UUID.randomUUID().toString()); + oAuth2Client.setAccessTokenUri(UUID.randomUUID().toString()); + oAuth2Client.setScope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + oAuth2Client.setPlatforms(platforms == null ? Collections.emptyList() : platforms); + oAuth2Client.setUserInfoUri(UUID.randomUUID().toString()); + oAuth2Client.setUserNameAttributeName(UUID.randomUUID().toString()); + oAuth2Client.setJwkSetUri(UUID.randomUUID().toString()); + oAuth2Client.setClientAuthenticationMethod(UUID.randomUUID().toString()); + oAuth2Client.setLoginButtonLabel(UUID.randomUUID().toString()); + oAuth2Client.setLoginButtonIcon(UUID.randomUUID().toString()); + oAuth2Client.setAdditionalInfo(JacksonUtil.newObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + oAuth2Client.setMapperConfig( + OAuth2MapperConfig.builder() + .allowUserCreation(true) + .activateUser(true) + .type(MapperType.CUSTOM) + .custom( + OAuth2CustomMapperConfig.builder() + .url(UUID.randomUUID().toString()) + .build() + ) + .build()); + return oAuth2Client; + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java new file mode 100644 index 0000000000..734e30137a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2024 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.dao.service; + +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.domain.DomainService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.dao.oauth2.OAuth2Utils.OAUTH2_AUTHORIZATION_PATH_TEMPLATE; + +@DaoSqlTest +public class DomainServiceTest extends AbstractServiceTest { + + @Autowired + protected DomainService domainService; + + @Autowired + protected OAuth2ClientService oAuth2ClientService; + + @After + public void after() { + domainService.deleteByTenantId(TenantId.SYS_TENANT_ID); + oAuth2ClientService.deleteByTenantId(TenantId.SYS_TENANT_ID); + } + + @Test + public void testSaveDomain() { + Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "test.domain.com", true, true); + Domain savedDomain = domainService.saveDomain(SYSTEM_TENANT_ID, domain); + + Domain retrievedDomain = domainService.findDomainById(savedDomain.getTenantId(), savedDomain.getId()); + assertThat(retrievedDomain).isEqualTo(savedDomain); + + // update domain name + savedDomain.setName("test.domain2.com"); + Domain updatedDomain = domainService.saveDomain(SYSTEM_TENANT_ID, savedDomain); + + Domain retrievedDomain2 = domainService.findDomainById(savedDomain.getTenantId(), savedDomain.getId()); + assertThat(retrievedDomain2).isEqualTo(updatedDomain); + + // check domain info + DomainInfo retrievedInfo = domainService.findDomainInfoById(SYSTEM_TENANT_ID, savedDomain.getId()); + assertThat(retrievedInfo).isEqualTo(new DomainInfo(updatedDomain, Collections.emptyList())); + + boolean oauth2Enabled = domainService.isOauth2Enabled(SYSTEM_TENANT_ID); + assertThat(oauth2Enabled).isTrue(); + + // update domain oauth2 enabled to false + updatedDomain.setOauth2Enabled(false); + domainService.saveDomain(SYSTEM_TENANT_ID, updatedDomain); + + boolean oauth2Enabled2 = domainService.isOauth2Enabled(SYSTEM_TENANT_ID); + assertThat(oauth2Enabled2).isFalse(); + + //delete domain + domainService.deleteDomainById(SYSTEM_TENANT_ID, savedDomain.getId()); + assertThat(domainService.findDomainById(SYSTEM_TENANT_ID, savedDomain.getId())).isNull(); + } + + @Test + public void testGetTenantDomains() { + List domains = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Domain oAuth2Client = constructDomain(TenantId.SYS_TENANT_ID, StringUtils.randomAlphabetic(5), true, false); + Domain savedOauth2Client = domainService.saveDomain(SYSTEM_TENANT_ID, oAuth2Client); + domains.add(savedOauth2Client); + } + PageData retrieved = domainService.findDomainInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10, 0)); + List domainInfos = domains.stream().map(domain -> new DomainInfo(domain, Collections.emptyList())).toList(); + assertThat(retrieved.getData()).containsOnlyOnceElementsOf(domainInfos); + } + + @Test + public void testGetDomainInfo() { + OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client"); + OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); + List oAuth2ClientInfosByIds = oAuth2ClientService.findOAuth2ClientInfosByIds(TenantId.SYS_TENANT_ID, List.of(savedOauth2Client.getId())); + + Domain domain = constructDomain(TenantId.SYS_TENANT_ID, "test.domain.com", true, true); + Domain savedDomain = domainService.saveDomain(SYSTEM_TENANT_ID, domain); + + domainService.updateOauth2Clients(TenantId.SYS_TENANT_ID, savedDomain.getId(), List.of(savedOauth2Client.getId())); + + // check domain info + DomainInfo retrievedInfo = domainService.findDomainInfoById(SYSTEM_TENANT_ID, savedDomain.getId()); + assertThat(retrievedInfo).isEqualTo(new DomainInfo(savedDomain, oAuth2ClientInfosByIds)); + + //find clients by domain name + List oauth2LoginInfo = oAuth2ClientService.findOAuth2ClientLoginInfosByDomainName(savedDomain.getName()); + assertThat(oauth2LoginInfo).containsOnly(new OAuth2ClientLoginInfo(savedOauth2Client.getLoginButtonLabel(), savedOauth2Client.getLoginButtonIcon(), String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, savedOauth2Client.getUuidId().toString()))); + } + + private Domain constructDomain(TenantId tenantId, String domainName, boolean oauth2Enabled, boolean propagateToEdge) { + Domain domain = new Domain(); + domain.setTenantId(tenantId); + domain.setName(domainName); + domain.setOauth2Enabled(oauth2Enabled); + domain.setPropagateToEdge(propagateToEdge); + return domain; + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java new file mode 100644 index 0000000000..ec093d0947 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/MobileAppServiceTest.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2024 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.dao.service; + +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.mobile.MobileAppService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.dao.oauth2.OAuth2Utils.OAUTH2_AUTHORIZATION_PATH_TEMPLATE; + +@DaoSqlTest +public class MobileAppServiceTest extends AbstractServiceTest { + + @Autowired + protected MobileAppService mobileAppService; + + @Autowired + protected OAuth2ClientService oAuth2ClientService; + + @After + public void after() { + mobileAppService.deleteByTenantId(TenantId.SYS_TENANT_ID); + oAuth2ClientService.deleteByTenantId(TenantId.SYS_TENANT_ID); + } + + @Test + public void testSaveMobileApp() { + MobileApp MobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "mobileApp.ce", true); + MobileApp savedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, MobileApp); + + MobileApp retrievedMobileApp = mobileAppService.findMobileAppById(savedMobileApp.getTenantId(), savedMobileApp.getId()); + assertThat(retrievedMobileApp).isEqualTo(savedMobileApp); + + // update MobileApp name + savedMobileApp.setPkgName("mobileApp.pe"); + MobileApp updatedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, savedMobileApp); + + MobileApp retrievedMobileApp2 = mobileAppService.findMobileAppById(savedMobileApp.getTenantId(), savedMobileApp.getId()); + assertThat(retrievedMobileApp2).isEqualTo(updatedMobileApp); + + //delete MobileApp + mobileAppService.deleteMobileAppById(SYSTEM_TENANT_ID, savedMobileApp.getId()); + assertThat(mobileAppService.findMobileAppById(SYSTEM_TENANT_ID, savedMobileApp.getId())).isNull(); + } + + @Test + public void testGetTenantMobileApps() { + List MobileApps = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + MobileApp oAuth2Client = validMobileApp(TenantId.SYS_TENANT_ID, StringUtils.randomAlphabetic(5), true); + MobileApp savedOauth2Client = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, oAuth2Client); + MobileApps.add(savedOauth2Client); + } + PageData retrieved = mobileAppService.findMobileAppInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10, 0)); + List MobileAppInfos = MobileApps.stream().map(MobileApp -> new MobileAppInfo(MobileApp, Collections.emptyList())).toList(); + assertThat(retrieved.getData()).containsOnlyOnceElementsOf(MobileAppInfos); + } + + @Test + public void tesGetMobileAppInfo() { + OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client"); + OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); + List oAuth2ClientInfosByIds = oAuth2ClientService.findOAuth2ClientInfosByIds(TenantId.SYS_TENANT_ID, List.of(savedOauth2Client.getId())); + + MobileApp MobileApp = validMobileApp(TenantId.SYS_TENANT_ID, "my.app", true); + MobileApp savedMobileApp = mobileAppService.saveMobileApp(SYSTEM_TENANT_ID, MobileApp); + + mobileAppService.updateOauth2Clients(TenantId.SYS_TENANT_ID, savedMobileApp.getId(), List.of(savedOauth2Client.getId())); + + // check MobileApp info + MobileAppInfo retrievedInfo = mobileAppService.findMobileAppInfoById(SYSTEM_TENANT_ID, savedMobileApp.getId()); + assertThat(retrievedInfo).isEqualTo(new MobileAppInfo(savedMobileApp, oAuth2ClientInfosByIds)); + + //find clients by MobileApp name + List oauth2LoginInfo = oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(savedMobileApp.getName(), null); + assertThat(oauth2LoginInfo).containsOnly(new OAuth2ClientLoginInfo(savedOauth2Client.getLoginButtonLabel(), savedOauth2Client.getLoginButtonIcon(), String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, savedOauth2Client.getUuidId().toString()))); + } + + private MobileApp validMobileApp(TenantId tenantId, String mobileAppName, boolean oauth2Enabled) { + MobileApp MobileApp = new MobileApp(); + MobileApp.setTenantId(tenantId); + MobileApp.setPkgName(mobileAppName); + MobileApp.setAppSecret(StringUtils.randomAlphanumeric(24)); + MobileApp.setOauth2Enabled(oauth2Enabled); + return MobileApp; + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ClientServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ClientServiceTest.java new file mode 100644 index 0000000000..5607dbfa75 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ClientServiceTest.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2024 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.dao.service; + +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DaoSqlTest +public class OAuth2ClientServiceTest extends AbstractServiceTest { + + @Autowired + protected OAuth2ClientService oAuth2ClientService; + + @After + public void after() { + oAuth2ClientService.deleteByTenantId(TenantId.SYS_TENANT_ID); + } + + @Test + public void testSaveOauth2Client() { + OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client", List.of(PlatformType.ANDROID)); + OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); + + OAuth2Client retrievedOauth2Client = oAuth2ClientService.findOAuth2ClientById(savedOauth2Client.getTenantId(), savedOauth2Client.getId()); + assertThat(retrievedOauth2Client).isEqualTo(savedOauth2Client); + + savedOauth2Client.setTitle("New title"); + OAuth2Client updatedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, savedOauth2Client); + + OAuth2Client retrievedOauth2Client2 = oAuth2ClientService.findOAuth2ClientById(savedOauth2Client.getTenantId(), savedOauth2Client.getId()); + assertThat(retrievedOauth2Client2).isEqualTo(updatedOauth2Client); + } + + @Test + public void testSaveOauth2ClientWithoutMapper() { + OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client", List.of(PlatformType.ANDROID)); + oAuth2Client.setMapperConfig(null); + + assertThatThrownBy(() -> { + oAuth2ClientService.saveOAuth2Client(TenantId.SYS_TENANT_ID, oAuth2Client); + }).hasMessageContaining("mapperConfig must not be null"); + } + + @Test + public void testSaveOauth2ClientWithoutCustomConfig() { + OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client", List.of(PlatformType.ANDROID)); + oAuth2Client.getMapperConfig().setCustom(null); + + assertThatThrownBy(() -> { + oAuth2ClientService.saveOAuth2Client(TenantId.SYS_TENANT_ID, oAuth2Client); + }).hasMessageContaining("Custom config should be specified!"); + } + + @Test + public void testSaveOauth2ClientWithoutCustomUrl() { + OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, "Test google client", List.of(PlatformType.ANDROID)); + oAuth2Client.getMapperConfig().setCustom(OAuth2CustomMapperConfig.builder().build()); + assertThatThrownBy(() -> { + oAuth2ClientService.saveOAuth2Client(TenantId.SYS_TENANT_ID, oAuth2Client); + }).hasMessageContaining("Custom mapper URL should be specified!"); + } + + @Test + public void testGetTenantOAuth2Clients() { + List oAuth2Clients = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + OAuth2Client oAuth2Client = validClientInfo(TenantId.SYS_TENANT_ID, StringUtils.randomAlphabetic(5)); + OAuth2Client savedOauth2Client = oAuth2ClientService.saveOAuth2Client(SYSTEM_TENANT_ID, oAuth2Client); + oAuth2Clients.add(savedOauth2Client); + } + List retrieved = oAuth2ClientService.findOAuth2ClientsByTenantId(TenantId.SYS_TENANT_ID); + assertThat(retrieved).containsOnlyOnceElementsOf(oAuth2Clients); + + PageData retrievedInfos = oAuth2ClientService.findOAuth2ClientInfosByTenantId(TenantId.SYS_TENANT_ID, new PageLink(10)); + List oAuth2ClientInfos = oAuth2Clients.stream().map(OAuth2ClientInfo::new).collect(Collectors.toList()); + assertThat(retrievedInfos.getData()).containsOnlyOnceElementsOf(oAuth2ClientInfos); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ServiceTest.java deleted file mode 100644 index 281709c9af..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ServiceTest.java +++ /dev/null @@ -1,668 +0,0 @@ -/** - * Copyright © 2016-2024 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.dao.service; - -import com.google.common.collect.Lists; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.jupiter.api.Assertions; -import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.oauth2.MapperType; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; -import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; -import org.thingsboard.server.common.data.oauth2.PlatformType; -import org.thingsboard.server.common.data.oauth2.SchemeType; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.oauth2.OAuth2Service; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -@DaoSqlTest -public class OAuth2ServiceTest extends AbstractServiceTest { - private static final OAuth2Info EMPTY_PARAMS = new OAuth2Info(false, false, Collections.emptyList()); - - @Autowired - protected OAuth2Service oAuth2Service; - - @Before - public void beforeRun() { - Assert.assertTrue(oAuth2Service.findAllRegistrations().isEmpty()); - } - - @After - public void after() { - oAuth2Service.saveOAuth2Info(EMPTY_PARAMS); - Assert.assertTrue(oAuth2Service.findAllRegistrations().isEmpty()); - Assert.assertTrue(oAuth2Service.findOAuth2Info().getOauth2ParamsInfos().isEmpty()); - } - - @Test - public void testSaveHttpAndMixedDomainsTogether() { - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - )) - .build() - )); - Assertions.assertThrows(DataValidationException.class, () -> { - oAuth2Service.saveOAuth2Info(oAuth2Info); - }); - } - - @Test - public void testSaveHttpsAndMixedDomainsTogether() { - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(), - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - )) - .build() - )); - Assertions.assertThrows(DataValidationException.class, () -> { - oAuth2Service.saveOAuth2Info(oAuth2Info); - }); - } - - @Test - public void testCreateAndFindParams() { - OAuth2Info oAuth2Info = createDefaultOAuth2Info(); - oAuth2Service.saveOAuth2Info(oAuth2Info); - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundOAuth2Info); - // TODO ask if it's safe to check equality on AdditionalProperties - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - } - - @Test - public void testDisableParams() { - OAuth2Info oAuth2Info = createDefaultOAuth2Info(); - oAuth2Info.setEnabled(true); - oAuth2Service.saveOAuth2Info(oAuth2Info); - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundOAuth2Info); - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - - oAuth2Info.setEnabled(false); - oAuth2Service.saveOAuth2Info(oAuth2Info); - OAuth2Info foundDisabledOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertEquals(oAuth2Info, foundDisabledOAuth2Info); - } - - @Test - public void testClearDomainParams() { - OAuth2Info oAuth2Info = createDefaultOAuth2Info(); - oAuth2Service.saveOAuth2Info(oAuth2Info); - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundOAuth2Info); - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - - oAuth2Service.saveOAuth2Info(EMPTY_PARAMS); - OAuth2Info foundAfterClearClientsParams = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundAfterClearClientsParams); - Assert.assertEquals(EMPTY_PARAMS, foundAfterClearClientsParams); - } - - @Test - public void testUpdateClientsParams() { - OAuth2Info oAuth2Info = createDefaultOAuth2Info(); - oAuth2Service.saveOAuth2Info(oAuth2Info); - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundOAuth2Info); - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - - OAuth2Info newOAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("test-domain").scheme(SchemeType.MIXED).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo() - )) - .build() - )); - oAuth2Service.saveOAuth2Info(newOAuth2Info); - OAuth2Info foundAfterUpdateOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundAfterUpdateOAuth2Info); - Assert.assertEquals(newOAuth2Info, foundAfterUpdateOAuth2Info); - } - - @Test - public void testGetOAuth2Clients() { - List firstGroup = Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - ); - List secondGroup = Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo() - ); - List thirdGroup = Lists.newArrayList( - validRegistrationInfo() - ); - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(firstGroup) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(secondGroup) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), - OAuth2DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(thirdGroup) - .build() - )); - - oAuth2Service.saveOAuth2Info(oAuth2Info); - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundOAuth2Info); - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - - List firstGroupClientInfos = firstGroup.stream() - .map(registrationInfo -> new OAuth2ClientInfo( - registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) - .collect(Collectors.toList()); - List secondGroupClientInfos = secondGroup.stream() - .map(registrationInfo -> new OAuth2ClientInfo( - registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) - .collect(Collectors.toList()); - List thirdGroupClientInfos = thirdGroup.stream() - .map(registrationInfo -> new OAuth2ClientInfo( - registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) - .collect(Collectors.toList()); - - List nonExistentDomainClients = oAuth2Service.getOAuth2Clients("http", "non-existent-domain", null, null); - Assert.assertTrue(nonExistentDomainClients.isEmpty()); - - List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", null, null); - Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpClients.size()); - firstGroupClientInfos.forEach(firstGroupClientInfo -> { - Assert.assertTrue( - firstDomainHttpClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) - && clientInfo.getName().equals(firstGroupClientInfo.getName())) - ); - }); - - List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain", null, null); - Assert.assertTrue(firstDomainHttpsClients.isEmpty()); - - List fourthDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "fourth-domain", null, null); - Assert.assertEquals(secondGroupClientInfos.size(), fourthDomainHttpClients.size()); - secondGroupClientInfos.forEach(secondGroupClientInfo -> { - Assert.assertTrue( - fourthDomainHttpClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(secondGroupClientInfo.getIcon()) - && clientInfo.getName().equals(secondGroupClientInfo.getName())) - ); - }); - List fourthDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "fourth-domain", null, null); - Assert.assertEquals(secondGroupClientInfos.size(), fourthDomainHttpsClients.size()); - secondGroupClientInfos.forEach(secondGroupClientInfo -> { - Assert.assertTrue( - fourthDomainHttpsClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(secondGroupClientInfo.getIcon()) - && clientInfo.getName().equals(secondGroupClientInfo.getName())) - ); - }); - - List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain", null, null); - Assert.assertEquals(firstGroupClientInfos.size() + secondGroupClientInfos.size(), secondDomainHttpClients.size()); - firstGroupClientInfos.forEach(firstGroupClientInfo -> { - Assert.assertTrue( - secondDomainHttpClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) - && clientInfo.getName().equals(firstGroupClientInfo.getName())) - ); - }); - secondGroupClientInfos.forEach(secondGroupClientInfo -> { - Assert.assertTrue( - secondDomainHttpClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(secondGroupClientInfo.getIcon()) - && clientInfo.getName().equals(secondGroupClientInfo.getName())) - ); - }); - - List secondDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "second-domain", null, null); - Assert.assertEquals(firstGroupClientInfos.size() + thirdGroupClientInfos.size(), secondDomainHttpsClients.size()); - firstGroupClientInfos.forEach(firstGroupClientInfo -> { - Assert.assertTrue( - secondDomainHttpsClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) - && clientInfo.getName().equals(firstGroupClientInfo.getName())) - ); - }); - thirdGroupClientInfos.forEach(thirdGroupClientInfo -> { - Assert.assertTrue( - secondDomainHttpsClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(thirdGroupClientInfo.getIcon()) - && clientInfo.getName().equals(thirdGroupClientInfo.getName())) - ); - }); - } - - @Test - public void testGetOAuth2ClientsForHttpAndHttps() { - List firstGroup = Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - ); - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(firstGroup) - .build() - )); - - oAuth2Service.saveOAuth2Info(oAuth2Info); - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertNotNull(foundOAuth2Info); - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - - List firstGroupClientInfos = firstGroup.stream() - .map(registrationInfo -> new OAuth2ClientInfo( - registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) - .collect(Collectors.toList()); - - List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", null, null); - Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpClients.size()); - firstGroupClientInfos.forEach(firstGroupClientInfo -> { - Assert.assertTrue( - firstDomainHttpClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) - && clientInfo.getName().equals(firstGroupClientInfo.getName())) - ); - }); - - List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain", null, null); - Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpsClients.size()); - firstGroupClientInfos.forEach(firstGroupClientInfo -> { - Assert.assertTrue( - firstDomainHttpsClients.stream().anyMatch(clientInfo -> - clientInfo.getIcon().equals(firstGroupClientInfo.getIcon()) - && clientInfo.getName().equals(firstGroupClientInfo.getName())) - ); - }); - } - - @Test - public void testGetDisabledOAuth2Clients() { - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo() - )) - .build() - )); - - oAuth2Service.saveOAuth2Info(oAuth2Info); - - List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain", null, null); - Assert.assertEquals(5, secondDomainHttpClients.size()); - - oAuth2Info.setEnabled(false); - oAuth2Service.saveOAuth2Info(oAuth2Info); - - List secondDomainHttpDisabledClients = oAuth2Service.getOAuth2Clients("http", "second-domain", null, null); - Assert.assertEquals(0, secondDomainHttpDisabledClients.size()); - } - - @Test - public void testFindAllRegistrations() { - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), - OAuth2DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo() - )) - .build() - )); - - oAuth2Service.saveOAuth2Info(oAuth2Info); - List foundRegistrations = oAuth2Service.findAllRegistrations(); - Assert.assertEquals(6, foundRegistrations.size()); - oAuth2Info.getOauth2ParamsInfos().stream() - .flatMap(paramsInfo -> paramsInfo.getClientRegistrations().stream()) - .forEach(registrationInfo -> - Assert.assertTrue( - foundRegistrations.stream() - .anyMatch(registration -> registration.getClientId().equals(registrationInfo.getClientId())) - ) - ); - } - - @Test - public void testFindRegistrationById() { - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), - OAuth2DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo() - )) - .build() - )); - - oAuth2Service.saveOAuth2Info(oAuth2Info); - List foundRegistrations = oAuth2Service.findAllRegistrations(); - foundRegistrations.forEach(registration -> { - OAuth2Registration foundRegistration = oAuth2Service.findRegistration(registration.getUuidId()); - Assert.assertEquals(registration, foundRegistration); - }); - } - - @Test - public void testFindAppSecret() { - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .mobileInfos(Lists.newArrayList( - validMobileInfo("com.test.pkg1", "testPkg1AppSecret"), - validMobileInfo("com.test.pkg2", "testPkg2AppSecret") - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo() - )) - .build() - )); - oAuth2Service.saveOAuth2Info(oAuth2Info); - - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - - List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", null); - Assert.assertEquals(3, firstDomainHttpClients.size()); - for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) { - String[] segments = clientInfo.getUrl().split("/"); - String registrationId = segments[segments.length-1]; - String appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg1"); - Assert.assertNotNull(appSecret); - Assert.assertEquals("testPkg1AppSecret", appSecret); - appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg2"); - Assert.assertNotNull(appSecret); - Assert.assertEquals("testPkg2AppSecret", appSecret); - appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg3"); - Assert.assertNull(appSecret); - } - } - - @Test - public void testFindClientsByPackageAndPlatform() { - OAuth2Info oAuth2Info = new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .mobileInfos(Lists.newArrayList( - validMobileInfo("com.test.pkg1", "testPkg1Callback"), - validMobileInfo("com.test.pkg2", "testPkg2Callback") - )) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)), - validRegistrationInfo("Facebook", Arrays.asList(PlatformType.IOS)), - validRegistrationInfo("GitHub", Collections.emptyList()) - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo() - )) - .build() - )); - oAuth2Service.saveOAuth2Info(oAuth2Info); - - OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); - Assert.assertEquals(oAuth2Info, foundOAuth2Info); - - List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", null, null); - Assert.assertEquals(3, firstDomainHttpClients.size()); - List pkg1Clients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", null); - Assert.assertEquals(3, pkg1Clients.size()); - List pkg1AndroidClients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", PlatformType.ANDROID); - Assert.assertEquals(2, pkg1AndroidClients.size()); - Assert.assertTrue(pkg1AndroidClients.stream().anyMatch(client -> client.getName().equals("Google"))); - Assert.assertTrue(pkg1AndroidClients.stream().anyMatch(client -> client.getName().equals("GitHub"))); - List pkg1IOSClients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", PlatformType.IOS); - Assert.assertEquals(2, pkg1IOSClients.size()); - Assert.assertTrue(pkg1IOSClients.stream().anyMatch(client -> client.getName().equals("Facebook"))); - Assert.assertTrue(pkg1IOSClients.stream().anyMatch(client -> client.getName().equals("GitHub"))); - } - - private OAuth2Info createDefaultOAuth2Info() { - return new OAuth2Info(true, false, Lists.newArrayList( - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo(), - validRegistrationInfo() - )) - .build(), - OAuth2ParamsInfo.builder() - .domainInfos(Lists.newArrayList( - OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() - )) - .mobileInfos(Collections.emptyList()) - .clientRegistrations(Lists.newArrayList( - validRegistrationInfo(), - validRegistrationInfo() - )) - .build() - )); - } - - private OAuth2RegistrationInfo validRegistrationInfo() { - return validRegistrationInfo(null, Collections.emptyList()); - } - - private OAuth2RegistrationInfo validRegistrationInfo(String label, List platforms) { - return OAuth2RegistrationInfo.builder() - .clientId(UUID.randomUUID().toString()) - .clientSecret(UUID.randomUUID().toString()) - .authorizationUri(UUID.randomUUID().toString()) - .accessTokenUri(UUID.randomUUID().toString()) - .scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())) - .platforms(platforms == null ? Collections.emptyList() : platforms) - .userInfoUri(UUID.randomUUID().toString()) - .userNameAttributeName(UUID.randomUUID().toString()) - .jwkSetUri(UUID.randomUUID().toString()) - .clientAuthenticationMethod(UUID.randomUUID().toString()) - .loginButtonLabel(label != null ? label : UUID.randomUUID().toString()) - .loginButtonIcon(UUID.randomUUID().toString()) - .additionalInfo(JacksonUtil.newObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())) - .mapperConfig( - OAuth2MapperConfig.builder() - .allowUserCreation(true) - .activateUser(true) - .type(MapperType.CUSTOM) - .custom( - OAuth2CustomMapperConfig.builder() - .url(UUID.randomUUID().toString()) - .build() - ) - .build() - ) - .build(); - } - - private OAuth2MobileInfo validMobileInfo(String pkgName, String appSecret) { - return OAuth2MobileInfo.builder().pkgName(pkgName) - .appSecret(appSecret != null ? appSecret : StringUtils.randomAlphanumeric(24)) - .build(); - } - -} diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 0aa51402c5..6d1fcec080 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -87,6 +87,8 @@ import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.domain.Domain; +import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeInfo; @@ -101,9 +103,12 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.QueueId; @@ -118,9 +123,12 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.mobile.MobileApp; +import org.thingsboard.server.common.data.mobile.MobileAppInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; -import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; @@ -2071,7 +2079,7 @@ public class RestClient implements Closeable { }).getBody(); } - public List getOAuth2Clients(String pkgName, PlatformType platformType) { + public List getOAuth2Clients(String pkgName, PlatformType platformType) { Map params = new HashMap<>(); StringBuilder urlBuilder = new StringBuilder(baseURL); urlBuilder.append("/api/noauth/oauth2Clients"); @@ -2092,16 +2100,106 @@ public class RestClient implements Closeable { urlBuilder.toString(), HttpMethod.POST, HttpEntity.EMPTY, - new ParameterizedTypeReference>() { + new ParameterizedTypeReference>() { }, params).getBody(); } - public OAuth2Info getCurrentOAuth2Info() { - return restTemplate.getForEntity(baseURL + "/api/oauth2/config", OAuth2Info.class).getBody(); + public List getTenantOAuth2Clients() { + return restTemplate.exchange( + baseURL + "/api/oauth2/client/infos", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional getOauth2ClientById(OAuth2ClientId oAuth2ClientId) { + try { + ResponseEntity oauth2Client = restTemplate.getForEntity(baseURL + "/api/oauth2/client/{id}", OAuth2Client.class, oAuth2ClientId.getId()); + return Optional.ofNullable(oauth2Client.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public OAuth2Client saveOAuth2Client(OAuth2Client oAuth2Client) { + return restTemplate.postForEntity(baseURL + "/api/oauth2/client", oAuth2Client, OAuth2Client.class).getBody(); + } + + public void deleteOauth2CLient(OAuth2ClientId oAuth2ClientId) { + restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId()); + } + + public List getTenantDomainInfos() { + return restTemplate.exchange( + baseURL + "/api/domain/infos", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional getDomainInfoById(DomainId domainId) { + try { + ResponseEntity domainInfo = restTemplate.getForEntity(baseURL + "/api/domain/info/{id}", DomainInfo.class, domainId.getId()); + return Optional.ofNullable(domainInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Domain saveDomain(Domain domain) { + return restTemplate.postForEntity(baseURL + "/api/domain", domain, Domain.class).getBody(); + } + + public void deleteDomain(DomainId domainId) { + restTemplate.delete(baseURL + "/api/domain/{id}", domainId.getId()); + } + + public void updateDomainOauth2Clients(DomainId domainId, UUID[] oauth2ClientIds) { + restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId()); + } + + public List getTenantMobileAppInfos() { + return restTemplate.exchange( + baseURL + "/api/mobileApp/infos", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional getMobileAppInfoById(MobileAppId mobileAppId) { + try { + ResponseEntity mobileAppInfo = restTemplate.getForEntity(baseURL + "/api/mobileApp/info/{id}", MobileAppInfo.class, mobileAppId.getId()); + return Optional.ofNullable(mobileAppInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public MobileApp saveMobileApp(MobileApp mobileApp) { + return restTemplate.postForEntity(baseURL + "/api/mobileApp", mobileApp, MobileApp.class).getBody(); + } + + public void deleteMobileApp(MobileAppId mobileAppId) { + restTemplate.delete(baseURL + "/api/mobileApp/{id}", mobileAppId.getId()); } - public OAuth2Info saveOAuth2Info(OAuth2Info oauth2Info) { - return restTemplate.postForEntity(baseURL + "/api/oauth2/config", oauth2Info, OAuth2Info.class).getBody(); + public void updateMobileAppOauth2Clients(MobileAppId mobileAppId, UUID[] oauth2ClientIds) { + restTemplate.postForLocation(baseURL + "/api/mobileApp/{id}/oauth2Clients", oauth2ClientIds, mobileAppId.getId()); } public String getLoginProcessingUrl() { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index fad13373c0..7dd4505f29 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -55,17 +55,20 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -346,6 +349,12 @@ public interface TbContext { NotificationRuleService getNotificationRuleService(); + OAuth2ClientService getOAuth2ClientService(); + + DomainService getDomainService(); + + MobileAppService getMobileAppService(); + SlackService getSlackService(); boolean isExternalNodeForceAck(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 152e193ea4..1e1b031259 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -26,13 +26,16 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.QueueStatsId; @@ -145,6 +148,15 @@ public class TenantIdLoader { case QUEUE_STATS: tenantEntity = ctx.getQueueStatsService().findQueueStatsById(ctxTenantId, new QueueStatsId(id)); break; + case OAUTH2_CLIENT: + tenantEntity = ctx.getOAuth2ClientService().findOAuth2ClientById(ctxTenantId, new OAuth2ClientId(id)); + break; + case DOMAIN: + tenantEntity = ctx.getDomainService().findDomainById(ctxTenantId, new DomainId(id)); + break; + case MOBILE_APP: + tenantEntity = ctx.getMobileAppService().findMobileAppById(ctxTenantId, new MobileAppId(id)); + break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 47fa03e08e..3e0e479c80 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -51,10 +52,12 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.mobile.MobileApp; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.rpc.Rpc; @@ -66,12 +69,15 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2ClientService; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; @@ -139,6 +145,12 @@ public class TenantIdLoaderTest { private NotificationRuleService notificationRuleService; @Mock private QueueStatsService queueStatsService; + @Mock + private OAuth2ClientService oAuth2ClientService; + @Mock + private DomainService domainService; + @Mock + private MobileAppService mobileAppService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -362,6 +374,24 @@ public class TenantIdLoaderTest { when(ctx.getQueueStatsService()).thenReturn(queueStatsService); doReturn(queueStats).when(queueStatsService).findQueueStatsById(eq(tenantId), any()); break; + case OAUTH2_CLIENT: + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTenantId(tenantId); + when(ctx.getOAuth2ClientService()).thenReturn(oAuth2ClientService); + doReturn(oAuth2Client).when(oAuth2ClientService).findOAuth2ClientById(eq(tenantId), any()); + break; + case DOMAIN: + Domain domain = new Domain(); + domain.setTenantId(tenantId); + when(ctx.getDomainService()).thenReturn(domainService); + doReturn(domain).when(domainService).findDomainById(eq(tenantId), any()); + break; + case MOBILE_APP: + MobileApp mobileApp = new MobileApp(); + mobileApp.setTenantId(tenantId); + when(ctx.getMobileAppService()).thenReturn(mobileAppService); + doReturn(mobileApp).when(mobileAppService).findMobileAppById(eq(tenantId), any()); + break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); } diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 51a70a46de..6f1c2bc116 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -42,7 +42,7 @@ import { TimeService } from '@core/services/time.service'; import { UtilsService } from '@core/services/utils.service'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; -import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models'; +import { OAuth2ClientLoginInfo, PlatformType } from '@shared/models/oauth2.models'; import { isMobileApp } from '@core/utils'; import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models'; import { UserPasswordPolicy } from '@shared/models/settings.models'; @@ -66,7 +66,7 @@ export class AuthService { } redirectUrl: string; - oauth2Clients: Array = null; + oauth2Clients: Array = null; twoFactorAuthProviders: Array = null; private refreshTokenSubject: ReplaySubject = null; @@ -223,9 +223,9 @@ export class AuthService { } } - public loadOAuth2Clients(): Observable> { - const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB; - return this.http.post>(url, + public loadOAuth2Clients(): Observable> { + const url = '/api/noauth/oauth2/client?platform=' + PlatformType.WEB; + return this.http.post>(url, null, defaultHttpOptions()).pipe( catchError(err => of([])), tap((OAuth2Clients) => { diff --git a/ui-ngx/src/app/core/http/domain.service.ts b/ui-ngx/src/app/core/http/domain.service.ts new file mode 100644 index 0000000000..bb6cd4a16b --- /dev/null +++ b/ui-ngx/src/app/core/http/domain.service.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { Domain, DomainInfo } from '@shared/models/oauth2.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; + +@Injectable({ + providedIn: 'root' +}) +export class DomainService { + + constructor( + private http: HttpClient + ) { + } + + public saveDomain(domain: Domain, oauth2ClientIds: Array, config?: RequestConfig): Observable { + return this.http.post(`/api/domain?oauth2ClientIds=${oauth2ClientIds.join(',')}`, + domain, defaultHttpOptionsFromConfig(config)); + } + + public updateOauth2Clients(id: string, oauth2ClientIds: Array, config?: RequestConfig): Observable { + return this.http.put(`/api/domain/${id}/oauth2Clients`, oauth2ClientIds, defaultHttpOptionsFromConfig(config)); + } + + public getTenantDomainInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/domain/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getDomainInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/domain/info/${id}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteDomain(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/domain/${id}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 7446d67601..e31e22e0fe 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -96,6 +96,7 @@ import { NotificationType } from '@shared/models/notification.models'; import { UserId } from '@shared/models/id/user-id'; import { AlarmService } from '@core/http/alarm.service'; import { ResourceService } from '@core/http/resource.service'; +import { OAuth2Service } from '@core/http/oauth2.service'; @Injectable({ providedIn: 'root' @@ -125,7 +126,8 @@ export class EntityService { private queueService: QueueService, private notificationService: NotificationService, private alarmService: AlarmService, - private resourceService: ResourceService + private resourceService: ResourceService, + private oauth2Service: OAuth2Service ) { } private getEntityObservable(entityType: EntityType, entityId: string, @@ -272,6 +274,9 @@ export class EntityService { case EntityType.QUEUE_STATS: observable = this.queueService.getQueueStatisticsByIds(entityIds, config); break; + case EntityType.OAUTH2_CLIENT: + observable = this.oauth2Service.findTenantOAuth2ClientInfosByIds(entityIds, config); + break; } return observable; } @@ -452,6 +457,11 @@ export class EntityService { case EntityType.QUEUE_STATS: pageLink.sortOrder.property = 'createdTime'; entitiesObservable = this.queueService.getQueueStatistics(pageLink, config); + break; + case EntityType.OAUTH2_CLIENT: + pageLink.sortOrder.property = 'createdTime'; + entitiesObservable = this.oauth2Service.findTenantOAuth2ClientInfos(pageLink, config); + break; } return entitiesObservable; } diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index 7b108e487c..a506d2805e 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -14,11 +14,13 @@ /// limitations under the License. /// -import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileAppSettings } from '@shared/models/mobile-app.models'; +import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; @Injectable({ providedIn: 'root' @@ -30,16 +32,25 @@ export class MobileAppService { ) { } - public getMobileAppSettings(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + public saveMobileApp(mobileApp: MobileApp, oauth2ClientIds: Array, config?: RequestConfig): Observable { + return this.http.post(`/api/mobileApp?oauth2ClientIds=${oauth2ClientIds.join(',')}`, + mobileApp, defaultHttpOptionsFromConfig(config)); + } + + public updateOauth2Clients(id: string, oauth2ClientRegistrationIds: Array, config?: RequestConfig): Observable { + return this.http.put(`/api/mobileApp/${id}/oauth2Clients`, oauth2ClientRegistrationIds, defaultHttpOptionsFromConfig(config)); + } + + public getTenantMobileAppInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/mobileApp/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } - public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + public getMobileAppInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/mobileApp/info/${id}`, defaultHttpOptionsFromConfig(config)); } - public getMobileAppDeepLink(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + public deleteMobileApp(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/mobileApp/${id}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/core/http/mobile-application.service.ts b/ui-ngx/src/app/core/http/mobile-application.service.ts new file mode 100644 index 0000000000..17ebdd3fef --- /dev/null +++ b/ui-ngx/src/app/core/http/mobile-application.service.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { MobileAppSettings } from '@shared/models/mobile-app.models'; + +@Injectable({ + providedIn: 'root' +}) +export class MobileApplicationService { + + constructor( + private http: HttpClient + ) {} + + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + } + + public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + } + + public getMobileAppDeepLink(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/oauth2.service.ts b/ui-ngx/src/app/core/http/oauth2.service.ts index a0ac0d5126..73a8743a48 100644 --- a/ui-ngx/src/app/core/http/oauth2.service.ts +++ b/ui-ngx/src/app/core/http/oauth2.service.ts @@ -18,7 +18,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { OAuth2ClientRegistrationTemplate, OAuth2Info } from '@shared/models/oauth2.models'; +import { OAuth2Client, OAuth2ClientInfo, OAuth2ClientRegistrationTemplate } from '@shared/models/oauth2.models'; +import { PageData } from '@shared/models/page/page-data'; +import { PageLink } from '@shared/models/page/page-link'; @Injectable({ providedIn: 'root' @@ -27,22 +29,35 @@ export class OAuth2Service { constructor( private http: HttpClient - ) { } - - public getOAuth2Settings(config?: RequestConfig): Observable { - return this.http.get(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config)); + ) { } public getOAuth2Template(config?: RequestConfig): Observable> { return this.http.get>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config)); } - public saveOAuth2Settings(OAuth2Setting: OAuth2Info, config?: RequestConfig): Observable { - return this.http.post('/api/oauth2/config', OAuth2Setting, - defaultHttpOptionsFromConfig(config)); + public saveOAuth2Client(oAuth2Client: OAuth2Client, config?: RequestConfig): Observable { + return this.http.post('/api/oauth2/client', oAuth2Client, defaultHttpOptionsFromConfig(config)); + } + + public findTenantOAuth2ClientInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/oauth2/client/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public findTenantOAuth2ClientInfosByIds(clientIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/oauth2/client/infos?clientIds=${clientIds.join(',')}`, defaultHttpOptionsFromConfig(config)) + } + + public getOAuth2ClientById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/oauth2/client/${id}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteOauth2Client(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/oauth2/client/${id}`, defaultHttpOptionsFromConfig(config)); } public getLoginProcessingUrl(config?: RequestConfig): Observable { - return this.http.get(`/api/oauth2/loginProcessingUrl`, defaultHttpOptionsFromConfig(config)); + return this.http.get('/api/oauth2/loginProcessingUrl', defaultHttpOptionsFromConfig(config)); } + } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 28ebbfaf79..ee4c36b6f2 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -78,6 +78,9 @@ export enum MenuId { security_settings_general = 'security_settings_general', two_fa = 'two_fa', oauth2 = 'oauth2', + domains = 'domains', + mobile_apps = 'mobile_apps', + clients = 'clients', audit_log = 'audit_log', alarms = 'alarms', dashboards = 'dashboards', @@ -405,6 +408,36 @@ export const menuSectionMap = new Map([ icon: 'mdi:shield-account' } ], + [ + MenuId.domains, + { + id: MenuId.domains, + name: 'admin.oauth2.domains', + type: 'link', + path: '/security-settings/oauth2/domains', + icon: 'domain' + } + ], + [ + MenuId.mobile_apps, + { + id: MenuId.mobile_apps, + name: 'admin.oauth2.mobile-apps', + type: 'link', + path: '/security-settings/oauth2/mobile-applications', + icon: 'smartphone' + } + ], + [ + MenuId.clients, + { + id: MenuId.clients, + name: 'admin.oauth2.clients', + type: 'link', + path: '/security-settings/oauth2/clients', + icon: 'public' + } + ], [ MenuId.audit_log, { @@ -669,7 +702,14 @@ const defaultUserMenuMap = new Map([ pages: [ {id: MenuId.security_settings_general}, {id: MenuId.two_fa}, - {id: MenuId.oauth2} + { + id: MenuId.oauth2, + pages: [ + {id: MenuId.domains}, + {id: MenuId.mobile_apps}, + {id: MenuId.clients} + ] + } ] } ] @@ -794,7 +834,8 @@ const defaultHomeSectionMap = new Map([ { name: 'admin.system-settings', places: [MenuId.general, MenuId.mail_server, - MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.two_fa, MenuId.resources_library, MenuId.queues] + MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.domains, MenuId.mobile_apps, + MenuId.clients, MenuId.two_fa, MenuId.resources_library, MenuId.queues] } ] ], diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 5aec80aacb..9bf9443404 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -165,12 +165,19 @@ [matTooltip]="cellTooltip(entity, column, row)" matTooltipPosition="above" [ngStyle]="cellStyle(entity, column, row)"> - - + + + + + + + + + + - - - @@ -209,7 +216,7 @@ matTooltipPosition="above" (click)="column.actionDescriptor.onAction($event, entity)"> - {{column.actionDescriptor.icon}} + {{column.actionDescriptor.iconFunction ? column.actionDescriptor.iconFunction(entity) : column.actionDescriptor.icon}} diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 99b6275dfe..6872b09d36 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -47,6 +47,7 @@ import { CellActionDescriptor, CellActionDescriptorType, EntityActionTableColumn, + EntityChipsEntityTableColumn, EntityColumn, EntityLinkTableColumn, EntityTableColumn, @@ -614,7 +615,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa columnsUpdated(resetData: boolean = false) { this.entityColumns = this.entitiesTableConfig.columns.filter( - (column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) + (column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn || + column instanceof EntityChipsEntityTableColumn) .map(column => column as EntityTableColumn>); this.actionColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityActionTableColumn) diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html new file mode 100644 index 0000000000..c81857689f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html @@ -0,0 +1,22 @@ + + + + {{ subEntity.name }} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ParamsRepository.java b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.scss similarity index 67% rename from dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ParamsRepository.java rename to ui-ngx/src/app/modules/home/components/entity/entity-chips.component.scss index ed637b269b..72098fff51 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ParamsRepository.java +++ b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.scss @@ -13,12 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.oauth2; -import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.OAuth2ParamsEntity; - -import java.util.UUID; - -public interface OAuth2ParamsRepository extends JpaRepository { +:host { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 4px 0; + a.tb-entity-chip { + padding: 4px 8px; + border-radius: 12px; + line-height: 16px; + background: #e0e0e0; + color: #212121; + white-space: nowrap; + border: none; + &:hover { + background: #b7b7b7; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts new file mode 100644 index 0000000000..cd9be9d79d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-chips.component.ts @@ -0,0 +1,63 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input } from '@angular/core'; +import { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { baseDetailsPageByEntityType, EntityType } from '@app/shared/public-api'; + +const entityTypeEntitiesPropertyKeyMap = new Map([ + [EntityType.DOMAIN, 'oauth2ClientInfos'], + [EntityType.MOBILE_APP, 'oauth2ClientInfos'] +]); + +@Component({ + selector: 'tb-entity-chips', + templateUrl: './entity-chips.component.html', + styleUrls: ['./entity-chips.component.scss'] +}) +export class EntityChipsComponent { + + @Input() + set entity(value: BaseData) { + this.entityValue = value; + this.update(); + } + + get entity(): BaseData { + return this.entityValue; + } + + entityDetailsPrefixUrl: string; + + subEntities: Array>; + + private entityValue?: BaseData; + + private subEntitiesKey: string; + + update(): void { + if (this.entity && this.entity.id) { + const entityType = this.entity.id.entityType as EntityType; + this.subEntitiesKey = entityTypeEntitiesPropertyKeyMap.get(entityType); + this.subEntities = this.entity?.[this.subEntitiesKey]; + if (this.subEntities.length) { + this.entityDetailsPrefixUrl = baseDetailsPageByEntityType.get(this.subEntities[0].id.entityType as EntityType); + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 64d9e816bd..a562002ac1 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -181,6 +181,7 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo import { SelectDashboardBreakpointComponent } from '@home/components/dashboard-page/layout/select-dashboard-breakpoint.component'; +import { EntityChipsComponent } from '@home/components/entity/entity-chips.component'; @NgModule({ declarations: @@ -322,7 +323,8 @@ import { RateLimitsComponent, RateLimitsTextComponent, RateLimitsDetailsDialogComponent, - SendNotificationButtonComponent + SendNotificationButtonComponent, + EntityChipsComponent ], imports: [ CommonModule, @@ -457,7 +459,8 @@ import { RateLimitsComponent, RateLimitsTextComponent, RateLimitsDetailsDialogComponent, - SendNotificationButtonComponent + SendNotificationButtonComponent, + EntityChipsComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index b484b9eed1..afc508d3d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -19,7 +19,7 @@ import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { BadgePosition, MobileAppSettings } from '@shared/models/mobile-app.models'; -import { MobileAppService } from '@core/http/mobile-app.service'; +import { MobileApplicationService } from '@core/http/mobile-application.service'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { Observable, Subject } from 'rxjs'; @@ -78,7 +78,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI constructor(protected store: Store, protected cd: ChangeDetectorRef, - private mobileAppService: MobileAppService, + private mobileAppService: MobileApplicationService, private utilsService: UtilsService, private elementRef: ElementRef, private imagePipe: ImagePipe, diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 9dd434101d..417c89434f 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -78,7 +78,7 @@ export interface HeaderActionDescriptor { onAction: ($event: MouseEvent) => void; } -export type EntityTableColumnType = 'content' | 'action' | 'link'; +export type EntityTableColumnType = 'content' | 'action' | 'link' | 'entityChips'; export class BaseEntityTableColumn> { constructor(public type: EntityTableColumnType, @@ -141,7 +141,15 @@ export class DateEntityTableColumn> extends EntityTabl } } -export type EntityColumn> = EntityTableColumn | EntityActionTableColumn | EntityLinkTableColumn; +export class EntityChipsEntityTableColumn> extends BaseEntityTableColumn { + constructor(public key: string, + public title: string, + public width: string = '0px') { + super('entityChips', key, title, width, false); + } +} + +export type EntityColumn> = EntityTableColumn | EntityActionTableColumn | EntityLinkTableColumn | EntityChipsEntityTableColumn; export class EntityTableConfig, P extends PageLink = PageLink, L extends BaseData = T> { diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index a01826bd08..7d3e9983f8 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { inject, Injectable, NgModule } from '@angular/core'; +import { inject, NgModule } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; @@ -22,9 +22,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { Authority } from '@shared/models/authority.enum'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; -import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component'; -import { forkJoin, Observable } from 'rxjs'; -import { OAuth2Service } from '@core/http/oauth2.service'; +import { forkJoin } from 'rxjs'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; @@ -41,23 +39,13 @@ import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; +import { oAuth2Routes } from '@home/pages/admin/oauth2/oauth2-routing.module'; import { ImageResourceType, IMAGES_URL_PREFIX, ResourceSubType } from '@shared/models/resource.models'; import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; import { ImageService } from '@core/http/image.service'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { MenuId } from '@core/services/menu.models'; -@Injectable() -export class OAuth2LoginProcessingUrlResolver implements Resolve { - - constructor(private oauth2Service: OAuth2Service) { - } - - resolve(): Observable { - return this.oauth2Service.getLoginProcessingUrl(); - } -} - export const scadaSymbolResolver: ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, @@ -400,21 +388,7 @@ const routes: Routes = [ } } }, - { - path: 'oauth2', - component: OAuth2SettingsComponent, - canDeactivate: [ConfirmOnExitGuard], - data: { - auth: [Authority.SYS_ADMIN], - title: 'admin.oauth2.oauth2', - breadcrumb: { - menuId: MenuId.oauth2 - } - }, - resolve: { - loginProcessingUrl: OAuth2LoginProcessingUrlResolver - } - }, + ...oAuth2Routes, ...auditLogsRoutes ] } @@ -424,7 +398,6 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], providers: [ - OAuth2LoginProcessingUrlResolver, ResourcesLibraryTableConfigResolver, QueuesTableConfigResolver ] diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index f44782e814..1904738299 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -23,7 +23,6 @@ import { MailServerComponent } from '@modules/home/pages/admin/mail-server.compo import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; -import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; @@ -35,6 +34,7 @@ import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit- import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; +import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; @NgModule({ declarations: @@ -44,7 +44,6 @@ import { WidgetComponentsModule } from '@home/components/widget/widget-component SmsProviderComponent, SendTestSmsDialogComponent, SecuritySettingsComponent, - OAuth2SettingsComponent, HomeSettingsComponent, ResourcesLibraryComponent, ResourcesTableHeaderComponent, @@ -59,7 +58,8 @@ import { WidgetComponentsModule } from '@home/components/widget/widget-component SharedModule, HomeComponentsModule, AdminRoutingModule, - WidgetComponentsModule + WidgetComponentsModule, + OAuth2Module ] }) export class AdminModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index 0f2527d833..7f2a48253f 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -21,7 +21,7 @@ import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { Subject, takeUntil } from 'rxjs'; -import { MobileAppService } from '@core/http/mobile-app.service'; +import { MobileApplicationService } from '@core/http/mobile-application.service'; import { BadgePosition, badgePositionTranslationsMap, @@ -45,7 +45,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf badgePositionTranslationsMap = badgePositionTranslationsMap; constructor(protected store: Store, - private mobileAppService: MobileAppService, + private mobileAppService: MobileApplicationService, private fb: FormBuilder) { super(store); this.buildMobileAppSettingsForm(); diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html deleted file mode 100644 index 2ea96f6387..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ /dev/null @@ -1,614 +0,0 @@ - -
- - - - admin.oauth2.oauth2 - - -
-
- - -
- -
-
-
- - {{ 'admin.oauth2.enable' | translate }} - - - {{ 'admin.oauth2.edge-enable' | translate }} - -
-
- -
- - - - - - {{ domainListTittle(oauth2ParamsInfo) }} - - - - - - - - - - -
-
-
-
-
-
- - admin.oauth2.protocol - - - {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} - - - - - admin.domain-name - - - {{ 'admin.error-verification-url' | translate }} - - - {{ 'admin.domain-name-max-length' | translate }} - - -
- - {{ 'admin.domain-name-unique' | translate }} - -
- -
- - admin.oauth2.redirect-uri-template - - - - - - - - - -
-
- -
- -
-
-
-
- -
-
-
- - -
-
- admin.oauth2.no-mobile-apps -
-
-
-
-
- - admin.oauth2.mobile-package - - admin.oauth2.mobile-package-hint - - - {{ 'admin.oauth2.mobile-package-unique' | translate }} - -
-
- - admin.oauth2.mobile-app-secret - - - - admin.oauth2.mobile-app-secret-hint - - {{ 'admin.oauth2.mobile-app-secret-required' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-base64' | translate }} - - -
-
-
- -
-
-
-
- -
-
-
-
-
admin.oauth2.providers
- -
- - - - {{ getProviderName(registration) }} - - - - - - - -
-
-
- - admin.oauth2.login-provider - - - {{ provider }} - - - -
-
- - admin.oauth2.allowed-platforms - - - {{ platformTypeTranslations.get(platform) | translate }} - - - -
-
- - admin.oauth2.client-id - - - {{ 'admin.oauth2.client-id-required' | translate }} - - - {{ 'admin.oauth2.client-id-max-length' | translate }} - - - - - admin.oauth2.client-secret - - - {{ 'admin.oauth2.client-secret-required' | translate }} - - - {{ 'admin.oauth2.client-secret-max-length' | translate }} - - -
- - - - - {{ 'admin.oauth2.custom-setting' | translate }} - - - - - -
- - admin.oauth2.access-token-uri - - - - {{ 'admin.oauth2.access-token-uri-required' | translate }} - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - - - - admin.oauth2.authorization-uri - - - - {{ 'admin.oauth2.authorization-uri-required' | translate }} - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - -
- -
- - admin.oauth2.jwk-set-uri - - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - - - - admin.oauth2.user-info-uri - - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - -
- - - admin.oauth2.client-authentication-method - - - {{ clientAuthenticationMethod | uppercase }} - - - - -
- - admin.oauth2.login-button-label - - - {{ 'admin.oauth2.login-button-label-required' | translate }} - - - - - admin.oauth2.login-button-icon - - -
- -
-
- - {{ 'admin.oauth2.allow-user-creation' | translate }} - - - {{ 'admin.oauth2.activate-user' | translate }} - -
-
- - - admin.oauth2.scope - - - {{scope}} - cancel - - - - - {{ 'admin.oauth2.scope-required' | translate }} - - - - - -
- - - admin.oauth2.user-name-attribute-name - - - {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} - - - -
- - admin.oauth2.type - - - {{ mapperConfigType }} - - - - -
- - admin.oauth2.email-attribute-key - - - {{ 'admin.oauth2.email-attribute-key-required' | translate }} - - - {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} - - - -
- - admin.oauth2.first-name-attribute-key - - - {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} - - - - - admin.oauth2.last-name-attribute-key - - - {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} - - -
- -
- - admin.oauth2.tenant-name-strategy - - - {{ tenantNameStrategy }} - - - - - - admin.oauth2.tenant-name-pattern - - - {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} - - - {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} - - -
- - - admin.oauth2.customer-name-pattern - - - {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} - - - -
- - admin.oauth2.default-dashboard-name - - - {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} - - - - - {{ 'admin.oauth2.always-fullscreen' | translate}} - -
-
- -
- - admin.oauth2.url - - - {{ 'admin.oauth2.url-required' | translate }} - - - {{ 'admin.oauth2.url-pattern' | translate }} - - - {{ 'admin.oauth2.url-max-length' | translate }} - - - -
- - common.username - - - {{ 'admin.oauth2.username-max-length' | translate }} - - - - - common.password - - - - {{ 'admin.oauth2.password-max-length' | translate }} - - -
-
-
-
-
-
-
- -
-
-
-
-
- -
- -
-
- -
-
-
-
-
-
-
-
- - -
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss deleted file mode 100644 index 639fb877f2..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import '../../../../../scss/constants'; - -:host{ - .gap-xs-12 { - @media #{$mat-xs} { - gap: 12px; - } - } - - .mdc-evolution-chip-set .mdc-evolution-chip { - margin-top: 0; - margin-bottom: 0; - } - - .checkbox-row { - margin-top: 1em; - } - - .registration-card{ - margin-bottom: 0.5em; - border: 1px solid rgba(0, 0, 0, 0.2); - - .custom-settings{ - font-size: 16px; - .mat-expansion-panel-header{ - padding: 0 2px; - } - } - } - - .container{ - margin-bottom: 16px; - - .tb-highlight{ - margin: 0; - } - - .tb-hint{ - padding-bottom: 0; - } - } - - .mat-expansion-panel { - .mat-expansion-panel-header { - &.mat-expanded { - height: 48px; - } - } - } -} - -:host ::ng-deep{ - .registration-card{ - .custom-settings{ - .mat-expansion-panel-body{ - padding: 0 2px 1em; - } - } - } - .domains-list, .apps-list { - margin-bottom: 1.5em; - } -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts deleted file mode 100644 index f1eabf2d0c..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ /dev/null @@ -1,613 +0,0 @@ -/// -/// Copyright © 2016-2024 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { - AbstractControl, - FormControl, - UntypedFormArray, - UntypedFormBuilder, - UntypedFormGroup, - ValidationErrors, - Validators -} from '@angular/forms'; -import { - ClientAuthenticationMethod, - DomainSchema, - domainSchemaTranslations, - MapperConfig, - MapperConfigBasic, - MapperConfigCustom, - MapperConfigType, - OAuth2ClientRegistrationTemplate, - OAuth2DomainInfo, - OAuth2Info, - OAuth2MobileInfo, - OAuth2ParamsInfo, - OAuth2RegistrationInfo, - PlatformType, - platformTypeTranslations, - TenantNameStrategy -} from '@shared/models/oauth2.models'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { PageComponent } from '@shared/components/page.component'; -import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; -import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { WINDOW } from '@core/services/window.service'; -import { forkJoin, Subscription } from 'rxjs'; -import { DialogService } from '@core/services/dialog.service'; -import { TranslateService } from '@ngx-translate/core'; -import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; -import { OAuth2Service } from '@core/http/oauth2.service'; -import { ActivatedRoute } from '@angular/router'; - -@Component({ - selector: 'tb-oauth2-settings', - templateUrl: './oauth2-settings.component.html', - styleUrls: ['./oauth2-settings.component.scss', './settings-card.scss'] -}) -export class OAuth2SettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { - - constructor(protected store: Store, - private route: ActivatedRoute, - private oauth2Service: OAuth2Service, - private fb: UntypedFormBuilder, - private dialogService: DialogService, - private translate: TranslateService, - @Inject(WINDOW) private window: Window) { - super(store); - } - - get oauth2ParamsInfos(): UntypedFormArray { - return this.oauth2SettingsForm.get('oauth2ParamsInfos') as UntypedFormArray; - } - - private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; - private DOMAIN_AND_PORT_REGEXP = /^(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?$/; - private subscriptions: Subscription[] = []; - private templates = new Map(); - private defaultProvider = { - additionalInfo: { - providerName: 'Custom' - }, - clientAuthenticationMethod: ClientAuthenticationMethod.POST, - userNameAttributeName: 'email', - mapperConfig: { - allowUserCreation: true, - activateUser: false, - type: MapperConfigType.BASIC, - basic: { - emailAttributeKey: 'email', - tenantNameStrategy: TenantNameStrategy.DOMAIN, - alwaysFullScreen: false - } - } - }; - - readonly separatorKeysCodes: number[] = [ENTER, COMMA]; - - oauth2SettingsForm: UntypedFormGroup; - oauth2Info: OAuth2Info; - - clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); - mapperConfigType = MapperConfigType; - mapperConfigTypes = Object.keys(MapperConfigType); - tenantNameStrategies = Object.keys(TenantNameStrategy); - protocols = Object.values(DomainSchema); - domainSchemaTranslations = domainSchemaTranslations; - platformTypes = Object.values(PlatformType); - platformTypeTranslations = platformTypeTranslations; - - templateProvider = ['Custom']; - - showMainLoadingBar = false; - - private loginProcessingUrl: string = this.route.snapshot.data.loginProcessingUrl; - - private static validateScope(control: AbstractControl): ValidationErrors | null { - const scope: string[] = control.value; - if (!scope || !scope.length) { - return { - required: true - }; - } - return null; - } - - ngOnInit(): void { - this.buildOAuth2SettingsForm(); - forkJoin([ - this.oauth2Service.getOAuth2Template(), - this.oauth2Service.getOAuth2Settings() - ]).subscribe( - ([templates, oauth2Info]) => { - this.initTemplates(templates); - this.oauth2Info = oauth2Info; - this.initOAuth2Settings(this.oauth2Info); - } - ); - } - - ngOnDestroy() { - super.ngOnDestroy(); - this.subscriptions.forEach((subscription) => { - subscription.unsubscribe(); - }); - } - - private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void { - templates.map(provider => { - delete provider.additionalInfo; - this.templates.set(provider.name, provider); - }); - this.templateProvider.push(...Array.from(this.templates.keys())); - this.templateProvider.sort(); - } - - private formBasicGroup(mapperConfigBasic?: MapperConfigBasic): UntypedFormGroup { - let tenantNamePattern; - if (mapperConfigBasic?.tenantNamePattern) { - tenantNamePattern = mapperConfigBasic.tenantNamePattern; - } else { - tenantNamePattern = {value: null, disabled: true}; - } - const basicGroup = this.fb.group({ - emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', - [Validators.required, Validators.maxLength(31)]], - firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : '', - Validators.maxLength(31)], - lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : '', - Validators.maxLength(31)], - tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN], - tenantNamePattern: [tenantNamePattern, [Validators.required, Validators.maxLength(255)]], - customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null, - Validators.maxLength(255)], - defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null, - Validators.maxLength(255)], - alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] - }); - - this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => { - if (domain === 'CUSTOM') { - basicGroup.get('tenantNamePattern').enable(); - } else { - basicGroup.get('tenantNamePattern').disable(); - } - })); - - return basicGroup; - } - - private formCustomGroup(mapperConfigCustom?: MapperConfigCustom): UntypedFormGroup { - return this.fb.group({ - url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, - [Validators.required, Validators.pattern(this.URL_REGEXP), Validators.maxLength(255)]], - username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null, Validators.maxLength(255)], - password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null, Validators.maxLength(255)] - }); - } - - private buildOAuth2SettingsForm(): void { - this.oauth2SettingsForm = this.fb.group({ - oauth2ParamsInfos: this.fb.array([]), - enabled: [false], - edgeEnabled: [{value: false, disabled: true}] - }); - - this.subscriptions.push(this.oauth2SettingsForm.get('enabled').valueChanges.subscribe(enabled => { - if (enabled) { - this.oauth2SettingsForm.get('edgeEnabled').enable(); - } else { - this.oauth2SettingsForm.get('edgeEnabled').patchValue(false); - this.oauth2SettingsForm.get('edgeEnabled').disable(); - } - })); - } - - private initOAuth2Settings(oauth2Info: OAuth2Info): void { - if (oauth2Info) { - this.oauth2SettingsForm.patchValue({enabled: oauth2Info.enabled, edgeEnabled: oauth2Info.edgeEnabled}); - oauth2Info.oauth2ParamsInfos.forEach((oauth2ParamsInfo) => { - this.oauth2ParamsInfos.push(this.buildOAuth2ParamsInfoForm(oauth2ParamsInfo)); - }); - } - } - - private uniqueDomainValidator(control: UntypedFormGroup): { [key: string]: boolean } | null { - if (control.parent?.value) { - const domain = control.value.name; - const listProtocols = control.parent.getRawValue() - .filter((domainInfo) => domainInfo.name === domain) - .map((domainInfo) => domainInfo.scheme); - if (listProtocols.length > 1 && listProtocols.indexOf(DomainSchema.MIXED) > -1 || - new Set(listProtocols).size !== listProtocols.length) { - return {unique: true}; - } - } - return null; - } - - private uniquePkgNameValidator(control: UntypedFormGroup): { [key: string]: boolean } | null { - if (control.parent?.value) { - const pkgName = control.value.pkgName; - const mobileInfosList = control.parent.getRawValue() - .filter((mobileInfo) => mobileInfo.pkgName === pkgName); - if (mobileInfosList.length > 1) { - return {unique: true}; - } - } - return null; - } - - public domainListTittle(control: AbstractControl): string { - const domainInfos = control.get('domainInfos').value as OAuth2DomainInfo[]; - if (domainInfos.length) { - const domainList = new Set(); - domainInfos.forEach((domain) => { - domainList.add(domain.name); - }); - return Array.from(domainList).join(', '); - } - return this.translate.instant('admin.oauth2.new-domain'); - } - - private buildOAuth2ParamsInfoForm(oauth2ParamsInfo?: OAuth2ParamsInfo): UntypedFormGroup { - const formOAuth2Params = this.fb.group({ - domainInfos: this.fb.array([], Validators.required), - mobileInfos: this.fb.array([]), - clientRegistrations: this.fb.array([], Validators.required) - }); - - if (oauth2ParamsInfo) { - oauth2ParamsInfo.domainInfos.forEach((domain) => { - this.domainInfos(formOAuth2Params).push(this.buildDomainInfoForm(domain)); - }); - oauth2ParamsInfo.mobileInfos.forEach((mobile) => { - this.mobileInfos(formOAuth2Params).push(this.buildMobileInfoForm(mobile)); - }); - oauth2ParamsInfo.clientRegistrations.forEach((registration) => { - this.clientRegistrations(formOAuth2Params).push(this.buildRegistrationForm(registration)); - }); - } else { - this.clientRegistrations(formOAuth2Params).push(this.buildRegistrationForm()); - this.domainInfos(formOAuth2Params).push(this.buildDomainInfoForm()); - } - - return formOAuth2Params; - } - - private buildDomainInfoForm(domainInfo?: OAuth2DomainInfo): UntypedFormGroup { - return this.fb.group({ - name: [domainInfo ? domainInfo.name : this.window.location.hostname, [ - Validators.required, Validators.maxLength(255), - Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]], - scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required] - }, {validators: this.uniqueDomainValidator}); - } - - private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): UntypedFormGroup { - return this.fb.group({ - pkgName: [mobileInfo?.pkgName, [Validators.required]], - appSecret: [mobileInfo?.appSecret, [Validators.required, this.base64Format]], - }, {validators: this.uniquePkgNameValidator}); - } - - private base64Format(control: FormControl): { [key: string]: boolean } | null { - if (control.value === '') { - return null; - } - try { - const value = atob(control.value); - if (value.length < 64) { - return {minLength: true}; - } - return null; - } catch (e) { - return {base64: true}; - } - } - - private buildRegistrationForm(registration?: OAuth2RegistrationInfo): UntypedFormGroup { - let additionalInfo = null; - if (isDefinedAndNotNull(registration?.additionalInfo)) { - additionalInfo = registration.additionalInfo; - if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { - additionalInfo.providerName = 'Custom'; - } - } - let defaultProviderName = 'Custom'; - if (this.templateProvider.indexOf('Google')) { - defaultProviderName = 'Google'; - } - - const clientRegistrationFormGroup = this.fb.group({ - additionalInfo: this.fb.group({ - providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required] - }), - platforms: [registration?.platforms ? registration.platforms : []], - loginButtonLabel: [registration?.loginButtonLabel ? registration.loginButtonLabel : null, Validators.required], - loginButtonIcon: [registration?.loginButtonIcon ? registration.loginButtonIcon : null], - clientId: [registration?.clientId ? registration.clientId : '', [Validators.required, Validators.maxLength(255)]], - clientSecret: [registration?.clientSecret ? registration.clientSecret : '', [Validators.required, Validators.maxLength(2048)]], - accessTokenUri: [registration?.accessTokenUri ? registration.accessTokenUri : '', - [Validators.required, - Validators.pattern(this.URL_REGEXP)]], - authorizationUri: [registration?.authorizationUri ? registration.authorizationUri : '', - [Validators.required, - Validators.pattern(this.URL_REGEXP)]], - scope: this.fb.array(registration?.scope ? registration.scope : [], OAuth2SettingsComponent.validateScope), - jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], - userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '', - [Validators.pattern(this.URL_REGEXP)]], - clientAuthenticationMethod: [ - registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, - Validators.required], - userNameAttributeName: [ - registration?.userNameAttributeName ? registration.userNameAttributeName : 'email', Validators.required], - mapperConfig: this.fb.group({ - allowUserCreation: [ - isDefinedAndNotNull(registration?.mapperConfig?.allowUserCreation) ? - registration.mapperConfig.allowUserCreation : true - ], - activateUser: [ - isDefinedAndNotNull(registration?.mapperConfig?.activateUser) ? registration.mapperConfig.activateUser : false - ], - type: [ - registration?.mapperConfig?.type ? registration.mapperConfig.type : MapperConfigType.BASIC, Validators.required - ] - } - ) - }); - - if (registration) { - this.changeMapperConfigType(clientRegistrationFormGroup, registration.mapperConfig.type, registration.mapperConfig); - } else { - this.changeMapperConfigType(clientRegistrationFormGroup, MapperConfigType.BASIC); - this.setProviderDefaultValue(defaultProviderName, clientRegistrationFormGroup); - } - - this.subscriptions.push(clientRegistrationFormGroup.get('mapperConfig.type').valueChanges.subscribe((value) => { - this.changeMapperConfigType(clientRegistrationFormGroup, value); - })); - - this.subscriptions.push(clientRegistrationFormGroup.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { - (clientRegistrationFormGroup.get('scope') as UntypedFormArray).clear(); - this.setProviderDefaultValue(provider, clientRegistrationFormGroup); - })); - - return clientRegistrationFormGroup; - } - - private setProviderDefaultValue(provider: string, clientRegistration: UntypedFormGroup) { - if (provider === 'Custom') { - clientRegistration.reset(this.defaultProvider, {emitEvent: false}); - clientRegistration.get('accessTokenUri').enable(); - clientRegistration.get('authorizationUri').enable(); - clientRegistration.get('jwkSetUri').enable(); - clientRegistration.get('userInfoUri').enable(); - } else { - const template = this.templates.get(provider); - template.clientId = ''; - template.clientSecret = ''; - template.scope.forEach(() => { - (clientRegistration.get('scope') as UntypedFormArray).push(this.fb.control('')); - }); - clientRegistration.get('accessTokenUri').disable(); - clientRegistration.get('authorizationUri').disable(); - clientRegistration.get('jwkSetUri').disable(); - clientRegistration.get('userInfoUri').disable(); - clientRegistration.patchValue(template); - } - } - - private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) { - const mapperConfig = control.get('mapperConfig') as UntypedFormGroup; - if (type === MapperConfigType.CUSTOM) { - mapperConfig.removeControl('basic'); - mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); - } else { - mapperConfig.removeControl('custom'); - if (!mapperConfig.get('basic')) { - mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); - } - if (type === MapperConfigType.GITHUB) { - mapperConfig.get('basic.emailAttributeKey').disable(); - mapperConfig.get('basic.emailAttributeKey').patchValue(null, {emitEvent: false}); - } else { - mapperConfig.get('basic.emailAttributeKey').enable(); - } - } - } - - save(): void { - const setting = this.oauth2SettingsForm.getRawValue(); - this.oauth2Service.saveOAuth2Settings(setting).subscribe( - (oauth2Settings) => { - this.oauth2Info = oauth2Settings; - this.oauth2SettingsForm.patchValue(this.oauth2SettingsForm, {emitEvent: false}); - this.oauth2SettingsForm.markAsUntouched(); - this.oauth2SettingsForm.markAsPristine(); - } - ); - } - - confirmForm(): UntypedFormGroup { - return this.oauth2SettingsForm; - } - - addScope(event: MatChipInputEvent, control: AbstractControl): void { - const input = event.chipInput.inputElement; - const value = event.value; - const controller = control.get('scope') as UntypedFormArray; - if ((value.trim() !== '')) { - controller.push(this.fb.control(value.trim())); - controller.markAsDirty(); - } - - if (input) { - input.value = ''; - } - } - - removeScope(i: number, control: AbstractControl): void { - const controller = control.get('scope') as UntypedFormArray; - controller.removeAt(i); - controller.markAsTouched(); - controller.markAsDirty(); - } - - addOAuth2ParamsInfo(): void { - this.oauth2ParamsInfos.push(this.buildOAuth2ParamsInfoForm()); - } - - deleteOAuth2ParamsInfo($event: Event, index: number): void { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); - } - - const domainName = this.domainListTittle(this.oauth2ParamsInfos.at(index)); - this.dialogService.confirm( - this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domainName || ''}), - this.translate.instant('admin.oauth2.delete-domain-text'), null, - this.translate.instant('action.delete') - ).subscribe((data) => { - if (data) { - this.oauth2ParamsInfos.removeAt(index); - this.oauth2ParamsInfos.markAsTouched(); - this.oauth2ParamsInfos.markAsDirty(); - } - }); - } - - clientRegistrations(control: AbstractControl): UntypedFormArray { - return control.get('clientRegistrations') as UntypedFormArray; - } - - domainInfos(control: AbstractControl): UntypedFormArray { - return control.get('domainInfos') as UntypedFormArray; - } - - mobileInfos(control: AbstractControl): UntypedFormArray { - return control.get('mobileInfos') as UntypedFormArray; - } - - addRegistration(control: AbstractControl): void { - this.clientRegistrations(control).push(this.buildRegistrationForm()); - } - - deleteRegistration($event: Event, control: AbstractControl, index: number): void { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); - } - - const providerName = this.clientRegistrations(control).at(index).get('additionalInfo.providerName').value; - this.dialogService.confirm( - this.translate.instant('admin.oauth2.delete-registration-title', {name: providerName || ''}), - this.translate.instant('admin.oauth2.delete-registration-text'), null, - this.translate.instant('action.delete') - ).subscribe((data) => { - if (data) { - this.clientRegistrations(control).removeAt(index); - this.clientRegistrations(control).markAsTouched(); - this.clientRegistrations(control).markAsDirty(); - } - }); - } - - toggleEditMode(control: AbstractControl, path: string) { - control.get(path).disabled ? control.get(path).enable() : control.get(path).disable(); - } - - getProviderName(controller: AbstractControl): string { - return controller.get('additionalInfo.providerName').value; - } - - isCustomProvider(controller: AbstractControl): boolean { - return this.getProviderName(controller) === 'Custom'; - } - - getHelpLink(controller: AbstractControl): string { - const provider = controller.get('additionalInfo.providerName').value; - if (provider === null || provider === 'Custom') { - return ''; - } - return this.templates.get(provider).helpLink; - } - - addDomainInfo(control: AbstractControl): void { - this.domainInfos(control).push(this.buildDomainInfoForm({ - name: '', - scheme: DomainSchema.HTTPS - })); - } - - removeDomainInfo($event: Event, control: AbstractControl, index: number): void { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); - } - this.domainInfos(control).removeAt(index); - this.domainInfos(control).markAsTouched(); - this.domainInfos(control).markAsDirty(); - } - - addMobileInfo(control: AbstractControl): void { - this.mobileInfos(control).push(this.buildMobileInfoForm({ - pkgName: '', - appSecret: btoa(randomAlphanumeric(64)) - })); - } - - removeMobileInfo($event: Event, control: AbstractControl, index: number): void { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); - } - this.mobileInfos(control).removeAt(index); - this.mobileInfos(control).markAsTouched(); - this.mobileInfos(control).markAsDirty(); - } - - redirectURI(control: AbstractControl, schema?: DomainSchema): string { - const domainInfo = control.value as OAuth2DomainInfo; - if (domainInfo.name !== '') { - let protocol; - if (isDefined(schema)) { - protocol = schema.toLowerCase(); - } else { - protocol = domainInfo.scheme === DomainSchema.MIXED ? DomainSchema.HTTPS.toLowerCase() : domainInfo.scheme.toLowerCase(); - } - return `${protocol}://${domainInfo.name}${this.loginProcessingUrl}`; - } - return ''; - } - - redirectURIMixed(control: AbstractControl): string { - return this.redirectURI(control, DomainSchema.HTTP); - } - - trackByParams(index: number): number { - return index; - } - - trackByItem(i, item) { - return item; - } -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html new file mode 100644 index 0000000000..dcce273be2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html @@ -0,0 +1,49 @@ + +
+ +

{{ 'admin.oauth2.add-client' | translate }}

+ +
+ +
+ + +
+
+ +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts new file mode 100644 index 0000000000..4bd0aa7f28 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts @@ -0,0 +1,80 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; +import { getProviderHelpLink, OAuth2Client } from '@shared/models/oauth2.models'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; +import { ErrorStateMatcher } from '@angular/material/core'; + +@Component({ + selector: 'tb-client-dialog', + templateUrl: './client-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ClientDialogComponent}], + styleUrls: [] +}) +export class ClientDialogComponent extends DialogComponent implements OnDestroy { + + submitted = false; + + @ViewChild('clientComponent', {static: true}) clientComponent: ClientComponent; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + private fb: FormBuilder, + private oauth2Service: OAuth2Service, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + super(store, router, dialogRef); + } + + ngAfterViewInit(): void { + setTimeout(() => { + this.clientComponent.entityForm.markAsDirty(); + }, 0); + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save() { + this.submitted = true; + if (this.clientComponent.entityForm.valid) { + this.oauth2Service.saveOAuth2Client(this.clientComponent.entityFormValue()).subscribe( + (client) => { + this.dialogRef.close(client); + } + ); + } + } + + helpLinkId() { + return getProviderHelpLink(this.clientComponent.entityForm.get('additionalInfo.providerName')?.value); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.html new file mode 100644 index 0000000000..fd66a026da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.html @@ -0,0 +1,18 @@ + +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts new file mode 100644 index 0000000000..891285d4fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { OAuth2Client, OAuth2ClientInfo } from '@shared/models/oauth2.models'; +import { PageLink } from '@shared/models/page/page-link'; + +@Component({ + selector: 'tb-client-table-header', + templateUrl: './client-table-header.component.html', + styleUrls: [] +}) +export class ClientTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html new file mode 100644 index 0000000000..45b35c89c4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html @@ -0,0 +1,331 @@ + +
+
+ + admin.oauth2.title + + + {{ 'admin.oauth2.client-title-required' | translate }} + + +
+
+
+ + admin.oauth2.provider + + + {{ provider }} + + + +
+ + admin.oauth2.allowed-platforms + + + {{ platformTypeTranslations.get(platform) | translate }} + + + +
+
+ + admin.oauth2.client-id + + + {{ 'admin.oauth2.client-id-required' | translate }} + + + {{ 'admin.oauth2.client-id-max-length' | translate }} + + + + admin.oauth2.client-secret + + + {{ 'admin.oauth2.client-secret-required' | translate }} + + + {{ 'admin.oauth2.client-secret-max-length' | translate }} + + +
+ + + + admin.oauth2.advanced-settings + + + +
+
+ + + {{ "admin.oauth2.general" | translate }} + {{ 'admin.oauth2.mapper' | translate }} + +
+
+
+ + admin.oauth2.access-token-uri + + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + + + admin.oauth2.authorization-uri + + + {{ 'admin.oauth2.authorization-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + +
+
+ + admin.oauth2.jwk-set-uri + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + + + admin.oauth2.user-info-uri + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + +
+
+ + admin.oauth2.client-authentication-method + + + {{ clientAuthenticationMethod | uppercase }} + + + +
+
+ + admin.oauth2.login-button-label + + + {{ 'admin.oauth2.login-button-label-required' | translate }} + + + + admin.oauth2.login-button-icon + + +
+
+
+ + {{ 'admin.oauth2.allow-user-creation' | translate }} + +
+
+ + {{ 'admin.oauth2.activate-user' | translate }} + +
+
+
+ + admin.oauth2.scope + + + {{scope}} + cancel + + + + + {{ 'admin.oauth2.scope-required' | translate }} + + +
+ + +
+
+
+ + admin.oauth2.user-name-attribute-name + + + {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} + + +
+
+
+ + admin.oauth2.type + + + {{ mapperType }} + + + +
+
+
+ + admin.oauth2.email-attribute-key + + + {{ 'admin.oauth2.email-attribute-key-required' | translate }} + + + {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} + + +
+
+ + admin.oauth2.first-name-attribute-key + + + {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} + + + + admin.oauth2.last-name-attribute-key + + + {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} + + +
+
+ + admin.oauth2.tenant-name-strategy + + + {{ tenantNameStrategy }} + + + + + admin.oauth2.tenant-name-pattern + + + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + + + {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} + + +
+
+ + admin.oauth2.customer-name-pattern + + + {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} + + +
+
+ + admin.oauth2.default-dashboard-name + + + {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} + + + + {{ 'admin.oauth2.always-fullscreen' | translate}} + +
+
+
+
+ + admin.oauth2.url + + + {{ 'admin.oauth2.url-required' | translate }} + + + {{ 'admin.oauth2.url-pattern' | translate }} + + + {{ 'admin.oauth2.url-max-length' | translate }} + + +
+
+ + common.username + + + {{ 'admin.oauth2.username-max-length' | translate }} + + + + common.password + + + {{ 'admin.oauth2.password-max-length' | translate }} + + +
+ + {{ 'admin.oauth2.send-token' | translate}} + +
+
+
+
+
+
+
diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.scss similarity index 66% rename from dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java rename to ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.scss index 52bde2cd8c..b7dcc9050d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.scss @@ -13,16 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.oauth2; -import org.thingsboard.server.common.data.oauth2.OAuth2Domain; -import org.thingsboard.server.dao.Dao; +::ng-deep { + .mat-expansion-panel { + .mat-expansion-panel-header { + height: 48px; + padding: 0 12px; -import java.util.List; -import java.util.UUID; + &.mat-expanded { + height: 48px; + } + } -public interface OAuth2DomainDao extends Dao { - - List findByOAuth2ParamsId(UUID oauth2ParamsId); + .mat-expansion-panel-body { + padding: 0 12px; + } + &.configuration-panel { + border: 1px solid rgba(0, 0, 0, 0.2); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts new file mode 100644 index 0000000000..cf60b3ed51 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts @@ -0,0 +1,360 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { + ClientAuthenticationMethod, + MapperType, + OAuth2BasicMapperConfig, + OAuth2Client, + OAuth2ClientInfo, + OAuth2ClientRegistrationTemplate, + OAuth2CustomMapperConfig, + OAuth2MapperConfig, + PlatformType, + platformTypeTranslations, + TenantNameStrategyType +} from '@shared/models/oauth2.models'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { + AbstractControl, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + Validators +} from '@angular/forms'; +import { isDefinedAndNotNull } from '@core/utils'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { Subscription } from 'rxjs'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { PageLink } from '@shared/models/page/page-link'; +import { coerceBoolean } from '@app/shared/decorators/coercion'; + +@Component({ + selector: 'tb-client', + templateUrl: './client.component.html', + styleUrls: ['./client.component.scss'] +}) +export class ClientComponent extends EntityComponent { + + @Input() + @coerceBoolean() + readonly createNewDialog = false; + + templateProvider = ['Custom']; + private templates = new Map(); + private defaultProvider = { + additionalInfo: { + providerName: 'Custom' + }, + clientAuthenticationMethod: ClientAuthenticationMethod.POST, + userNameAttributeName: 'email', + mapperConfig: { + allowUserCreation: true, + activateUser: false, + type: MapperType.BASIC, + basic: { + emailAttributeKey: 'email', + tenantNameStrategy: TenantNameStrategyType.DOMAIN, + alwaysFullScreen: false + } + } + }; + + private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; + + private subscriptions: Array = []; + + public static validateScope(control: AbstractControl): ValidationErrors | null { + const scope: string[] = control.value; + if (!scope || !scope.length) { + return { + required: true + }; + } + return null; + } + + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + + clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); + mapperType = MapperType; + mapperTypes = Object.keys(MapperType); + tenantNameStrategies = Object.keys(TenantNameStrategyType); + platformTypes = Object.values(PlatformType); + platformTypeTranslations = platformTypeTranslations; + generalSettingsMode = true; + + constructor(protected store: Store, + protected translate: TranslateService, + private oauth2Service: OAuth2Service, + @Optional() @Inject('entity') protected entityValue: OAuth2Client, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: UntypedFormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + this.oauth2Service.getOAuth2Template().subscribe(templates => { + this.initTemplates(templates); + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } + + buildForm(entity: OAuth2Client): UntypedFormGroup { + return this.fb.group({ + title: [entity?.title ? entity.title : '', [Validators.required]], + additionalInfo: this.fb.group({ + providerName: [entity?.additionalInfo?.providerName ? entity?.additionalInfo?.providerName : '', Validators.required] + }), + platforms: [entity?.platforms ? entity.platforms : []], + clientId: [entity?.clientId ? entity.clientId : '', [Validators.required, Validators.maxLength(255)]], + clientSecret: [entity?.clientSecret ? entity.clientSecret : '', [Validators.required, Validators.maxLength(2048)]], + accessTokenUri: [entity?.accessTokenUri ? entity.accessTokenUri : '', + [Validators.required, Validators.pattern(this.URL_REGEXP)]], + authorizationUri: [entity?.authorizationUri ? entity.authorizationUri : '', + [Validators.required, Validators.pattern(this.URL_REGEXP)]], + jwkSetUri: [entity?.jwkSetUri ? entity.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], + userInfoUri: [entity?.userInfoUri ? entity.userInfoUri : '', [Validators.pattern(this.URL_REGEXP)]], + clientAuthenticationMethod: [entity?.clientAuthenticationMethod ? + entity.clientAuthenticationMethod : ClientAuthenticationMethod.POST, Validators.required], + loginButtonLabel: [entity?.loginButtonLabel ? entity.loginButtonLabel : null, Validators.required], + loginButtonIcon: [entity?.loginButtonIcon ? entity.loginButtonIcon : null], + userNameAttributeName: [entity?.userNameAttributeName ? entity.userNameAttributeName : 'email', Validators.required], + scope: this.fb.array(entity?.scope ? entity.scope : [], ClientComponent.validateScope), + mapperConfig: this.fb.group({ + allowUserCreation: [isDefinedAndNotNull(entity?.mapperConfig?.allowUserCreation) ? + entity.mapperConfig.allowUserCreation : true], + activateUser: [isDefinedAndNotNull(entity?.mapperConfig?.activateUser) ? entity.mapperConfig.activateUser : false], + type: [entity?.mapperConfig?.type ? entity.mapperConfig.type : MapperType.BASIC, Validators.required] + }) + }); + } + + updateForm(entity: OAuth2Client) { + this.entityForm.patchValue({ + title: entity.title, + additionalInfo: { + providerName: entity.additionalInfo.providerName + }, + platforms: entity.platforms, + clientId: entity.clientId, + clientSecret: entity.clientSecret, + accessTokenUri: entity.accessTokenUri, + authorizationUri: entity.authorizationUri, + jwkSetUri: entity.jwkSetUri, + userInfoUri: entity.userInfoUri, + clientAuthenticationMethod: entity.clientAuthenticationMethod, + loginButtonLabel: entity.loginButtonLabel, + loginButtonIcon: entity.loginButtonIcon, + userNameAttributeName: entity.userNameAttributeName, + mapperConfig: { + allowUserCreation: entity.mapperConfig.allowUserCreation, + activateUser: entity.mapperConfig.activateUser, + type: entity.mapperConfig.type + } + }, {emitEvent: false}); + + this.changeMapperConfigType(this.entityForm, this.entityValue.mapperConfig.type, this.entityValue.mapperConfig); + + const scopeControls = this.entityForm.get('scope') as UntypedFormArray; + if (entity.scope.length === scopeControls.length) { + scopeControls.patchValue(entity.scope, {emitEvent: false}); + } else { + const scopeControls: Array = []; + if (entity.scope) { + for (const scope of entity.scope) { + scopeControls.push(this.fb.control(scope, [Validators.required])); + } + } + this.entityForm.setControl('scope', this.fb.array(scopeControls)); + } + } + + getProviderName(): string { + return this.entityForm.get('additionalInfo.providerName').value; + } + + isCustomProvider(): boolean { + return this.getProviderName() === 'Custom'; + } + + trackByParams(index: number): number { + return index; + } + + trackByItem(i, item) { + return item; + } + + addScope(event: MatChipInputEvent, control: AbstractControl): void { + const input = event.chipInput.inputElement; + const value = event.value; + const controller = control.get('scope') as UntypedFormArray; + if ((value.trim() !== '')) { + controller.push(this.fb.control(value.trim())); + controller.markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + removeScope(i: number, control: AbstractControl): void { + const controller = control.get('scope') as UntypedFormArray; + controller.removeAt(i); + controller.markAsTouched(); + controller.markAsDirty(); + } + + private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void { + templates.map(provider => { + delete provider.additionalInfo; + this.templates.set(provider.name, provider); + }); + this.templateProvider.push(...Array.from(this.templates.keys())); + this.templateProvider.sort(); + + let additionalInfo = null; + if (isDefinedAndNotNull(this.entityValue?.additionalInfo)) { + additionalInfo = this.entityValue.additionalInfo; + if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { + additionalInfo.providerName = 'Custom'; + } + } + let defaultProviderName = 'Custom'; + if (this.templateProvider.indexOf('Google')) { + defaultProviderName = 'Google'; + } + + this.entityForm.get('additionalInfo.providerName').setValue(additionalInfo?.providerName ? + additionalInfo?.providerName : defaultProviderName); + + this.changeMapperConfigType(this.entityForm, MapperType.BASIC); + this.setProviderDefaultValue(defaultProviderName, this.entityForm); + + this.subscriptions.push(this.entityForm.get('mapperConfig.type').valueChanges.subscribe((value) => { + this.changeMapperConfigType(this.entityForm, value); + })); + + this.subscriptions.push(this.entityForm.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { + (this.entityForm.get('scope') as UntypedFormArray).clear(); + this.setProviderDefaultValue(provider, this.entityForm); + })); + } + + private changeMapperConfigType(control: AbstractControl, type: MapperType, predefinedValue?: OAuth2MapperConfig) { + const mapperConfig = control.get('mapperConfig') as UntypedFormGroup; + if (type === MapperType.CUSTOM) { + mapperConfig.removeControl('basic'); + mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); + } else { + mapperConfig.removeControl('custom'); + if (!mapperConfig.get('basic')) { + mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); + } else if (predefinedValue?.basic) { + mapperConfig.get('basic').patchValue(predefinedValue.basic, {emitEvent: false}); + mapperConfig.get('basic.tenantNameStrategy').updateValueAndValidity({onlySelf: true}); + } + if (type === MapperType.GITHUB) { + mapperConfig.get('basic.emailAttributeKey').disable(); + mapperConfig.get('basic.emailAttributeKey').patchValue(null, {emitEvent: false}); + } else { + if (this.createNewDialog || this.isEdit || this.isAdd) { + mapperConfig.get('basic.emailAttributeKey').enable(); + } + } + } + } + + private formCustomGroup(mapperConfigCustom?: OAuth2CustomMapperConfig): UntypedFormGroup { + const customGroup = this.fb.group({ + url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, + [Validators.required, Validators.pattern(this.URL_REGEXP), Validators.maxLength(255)]], + username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null, Validators.maxLength(255)], + password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null, Validators.maxLength(255)], + sendToken: [isDefinedAndNotNull(mapperConfigCustom?.sendToken) ? mapperConfigCustom.sendToken : false] + }); + if (!this.createNewDialog && !(this.isEdit || this.isAdd)) { + customGroup.disable(); + } + return customGroup; + } + + private formBasicGroup(mapperConfigBasic?: OAuth2BasicMapperConfig): UntypedFormGroup { + let tenantNamePattern; + if (mapperConfigBasic?.tenantNamePattern) { + tenantNamePattern = mapperConfigBasic.tenantNamePattern; + } else { + tenantNamePattern = {value: null, disabled: true}; + } + const basicGroup = this.fb.group({ + emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', + [Validators.required, Validators.maxLength(31)]], + firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : '', + Validators.maxLength(31)], + lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : '', + Validators.maxLength(31)], + tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategyType.DOMAIN], + tenantNamePattern: [tenantNamePattern, [Validators.required, Validators.maxLength(255)]], + customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null, + Validators.maxLength(255)], + defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null, + Validators.maxLength(255)], + alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] + }); + + if (!this.createNewDialog && !(this.isEdit || this.isAdd)) { + basicGroup.disable(); + } + + this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => { + if (domain === 'CUSTOM') { + basicGroup.get('tenantNamePattern').enable(); + } else { + basicGroup.get('tenantNamePattern').disable(); + } + })); + + return basicGroup; + } + + private setProviderDefaultValue(provider: string, clientRegistration: UntypedFormGroup) { + if (provider === 'Custom') { + clientRegistration.reset(this.defaultProvider, {emitEvent: false}); + } else { + const template = this.templates.get(provider); + template.clientId = ''; + template.clientSecret = ''; + template.scope.forEach(() => { + (clientRegistration.get('scope') as UntypedFormArray).push(this.fb.control('')); + }); + clientRegistration.patchValue(template); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts new file mode 100644 index 0000000000..d1d760d908 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { + getProviderHelpLink, + OAuth2Client, + OAuth2ClientInfo, + platformTypeTranslations +} from '@shared/models/oauth2.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; +import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/client-table-header.component'; +import { Direction } from '@shared/models/page/sort-order'; +import { PageLink } from '@shared/models/page/page-link'; + +@Injectable() +export class ClientsTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private translate: TranslateService, + private datePipe: DatePipe, + private utilsService: UtilsService, + private oauth2Service: OAuth2Service) { + this.config.tableTitle = this.translate.instant('admin.oauth2.clients'); + this.config.selectionEnabled = false; + this.config.entityType = EntityType.OAUTH2_CLIENT; + this.config.rowPointer = true; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.OAUTH2_CLIENT); + this.config.entityResources = { + helpLinkId: null, + helpLinkIdForEntity(entity: OAuth2Client): string { + return getProviderHelpLink(entity.additionalInfo.providerName); + } + }; + this.config.entityComponent = ClientComponent; + this.config.headerComponent = ClientTableHeaderComponent; + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + this.config.displayPagination = false; + this.config.pageMode = false; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('title', 'admin.oauth2.title', '170px'), + new EntityTableColumn('providerName', 'admin.oauth2.provider', '170px'), + new EntityTableColumn('platforms', 'admin.oauth2.allowed-platforms', '100%', + (clientInfo) => { + return clientInfo.platforms && clientInfo.platforms.length ? + clientInfo.platforms.map(platform => this.translate.instant(platformTypeTranslations.get(platform))).join(', ') : + this.translate.instant('admin.oauth2.all-platforms'); + }, () => ({}), false) + ); + + this.config.deleteEntityTitle = (client) => this.translate.instant('admin.oauth2.delete-client-title', {clientName: client.title}); + this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-client-text'); + this.config.entitiesFetchFunction = pageLink => this.oauth2Service.findTenantOAuth2ClientInfos(pageLink); + this.config.loadEntity = id => this.oauth2Service.getOAuth2ClientById(id.id); + this.config.saveEntity = client => { + return this.oauth2Service.saveOAuth2Client(client); + } + this.config.deleteEntity = id => this.oauth2Service.deleteOauth2Client(id.id); + } + + resolve(): EntityTableConfig { + return this.config; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts new file mode 100644 index 0000000000..4bccc41dc7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts @@ -0,0 +1,147 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { + DateEntityTableColumn, + EntityActionTableColumn, + EntityChipsEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { DomainInfo } from '@shared/models/oauth2.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { DomainService } from '@app/core/http/domain.service'; +import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.component'; +import { isEqual } from '@core/utils'; +import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; +import { Direction } from '@app/shared/models/page/sort-order'; +import { map } from 'rxjs'; + +@Injectable() +export class DomainTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private translate: TranslateService, + private datePipe: DatePipe, + private utilsService: UtilsService, + private domainService: DomainService) { + this.config.tableTitle = this.translate.instant('admin.oauth2.domains'); + this.config.selectionEnabled = false; + this.config.entityType = EntityType.DOMAIN; + this.config.rowPointer = true; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DOMAIN); + this.config.entityResources = entityTypeResources.get(EntityType.DOMAIN); + this.config.entityComponent = DomainComponent; + this.config.headerComponent = DomainTableHeaderComponent; + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + this.config.displayPagination = false; + this.config.pageMode = false; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('name', 'admin.oauth2.domain-name', '170px'), + new EntityChipsEntityTableColumn('oauth2ClientInfos', 'admin.oauth2.clients', '40%'), + new EntityActionTableColumn('oauth2Enabled', 'admin.oauth2.enable', + { + name: '', + nameFunction: (domain) => + this.translate.instant(domain.oauth2Enabled ? 'admin.oauth2.disable' : 'admin.oauth2.enable'), + icon: 'mdi:toggle-switch', + iconFunction: (domain) => domain.oauth2Enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', + isEnabled: () => true, + onAction: ($event, entity) => this.toggleEnableOAuth($event, entity) + }), + new EntityActionTableColumn('propagateToEdge', 'admin.oauth2.edge', + { + name: '', + nameFunction: (domain) => + this.translate.instant(domain.propagateToEdge ? 'admin.oauth2.edge-disable' : 'admin.oauth2.edge-enable'), + icon: 'mdi:toggle-switch', + iconFunction: (entity) => entity.propagateToEdge ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', + isEnabled: () => true, + onAction: ($event, entity) => this.togglePropagateToEdge($event, entity) + }) + ); + + this.config.deleteEntityTitle = (domain) => this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domain.name}); + this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-domain-text'); + this.config.entitiesFetchFunction = pageLink => this.domainService.getTenantDomainInfos(pageLink); + this.config.loadEntity = id => this.domainService.getDomainInfoById(id.id); + this.config.saveEntity = (domain, originalDomain) => { + const clientsIds = domain.oauth2ClientInfos as Array || []; + if (domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), + originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) { + this.domainService.updateOauth2Clients(domain.id.id, clientsIds).subscribe(); + } + delete domain.oauth2ClientInfos; + return this.domainService.saveDomain(domain, domain.id ? [] : clientsIds).pipe( + map(domain => { + (domain as DomainInfo).oauth2ClientInfos = clientsIds; + return domain; + }) + ); + } + this.config.deleteEntity = id => this.domainService.deleteDomain(id.id); + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private toggleEnableOAuth($event: Event, domain: DomainInfo): void { + if ($event) { + $event.stopPropagation(); + } + + const modifiedDomain: DomainInfo = { + ...domain, + oauth2Enabled: !domain.oauth2Enabled + }; + + this.domainService.saveDomain(modifiedDomain, domain.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), + {ignoreLoading: true}) + .subscribe((result) => { + domain.oauth2Enabled = result.oauth2Enabled; + this.config.getTable().detectChanges(); + }); + } + + private togglePropagateToEdge($event: Event, domain: DomainInfo): void { + if ($event) { + $event.stopPropagation(); + } + + const modifiedDomain: DomainInfo = { + ...domain, + propagateToEdge: !domain.propagateToEdge + }; + + this.domainService.saveDomain(modifiedDomain, domain.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), + {ignoreLoading: true}) + .subscribe((result) => { + domain.propagateToEdge = result.propagateToEdge; + this.config.getTable().detectChanges(); + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.html new file mode 100644 index 0000000000..fd66a026da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.html @@ -0,0 +1,18 @@ + +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts new file mode 100644 index 0000000000..3b9f7c6a3c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DomainInfo } from '@shared/models/oauth2.models'; + +@Component({ + selector: 'tb-domain-table-header', + templateUrl: './domain-table-header.component.html', + styleUrls: [] +}) +export class DomainTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html new file mode 100644 index 0000000000..eaa85ce87a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html @@ -0,0 +1,70 @@ + +
+
+ + admin.oauth2.domain-name + + + {{ 'admin.oauth2.domain-name-required' | translate }} + + + {{ 'admin.domain-name-max-length' | translate }} + + + {{ 'admin.error-verification-url' | translate }} + + +
+
+ + admin.oauth2.redirect-url-template + + + + +
+
+ + {{ 'admin.oauth2.enable' | translate }} + +
+ + + +
+ + {{ 'admin.oauth2.edge' | translate }} + +
+
+ diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts new file mode 100644 index 0000000000..d54c969bfe --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts @@ -0,0 +1,107 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { DomainInfo } from '@shared/models/oauth2.models'; +import { AppState } from '@core/core.state'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { WINDOW } from '@core/services/window.service'; +import { isDefinedAndNotNull } from '@core/utils'; +import { MatButton } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; +import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; +import { EntityType } from '@shared/models/entity-type.models'; + +@Component({ + selector: 'tb-domain', + templateUrl: './domain.component.html', + styleUrls: [] +}) +export class DomainComponent extends EntityComponent { + + private loginProcessingUrl: string = ''; + + entityType = EntityType; + + constructor(protected store: Store, + protected translate: TranslateService, + private oauth2Service: OAuth2Service, + @Inject('entity') protected entityValue: DomainInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: UntypedFormBuilder, + @Inject(WINDOW) private window: Window, + private dialog: MatDialog) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + this.entityForm.get('name').setValue(this.window.location.hostname); + this.entityForm.markAsDirty(); + this.oauth2Service.getLoginProcessingUrl().subscribe(url => { + this.loginProcessingUrl = url; + }); + } + + buildForm(entity: DomainInfo): UntypedFormGroup { + return this.fb.group({ + name: [entity?.name ? entity.name : '', [ + Validators.required, Validators.maxLength(255), Validators.pattern('^(?:\\w+(?::\\w+)?@)?[^\\s/]+(?::\\d+)?$')]], + oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true, + oauth2ClientInfos: entity?.oauth2ClientInfos ? entity.oauth2ClientInfos.map(info => info.id.id) : [], + propagateToEdge: isDefinedAndNotNull(entity?.propagateToEdge) ? entity.propagateToEdge : false + }); + } + + updateForm(entity: DomainInfo) { + this.entityForm.patchValue({ + name: entity.name, + oauth2Enabled: entity.oauth2Enabled, + oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info), + propagateToEdge: entity.propagateToEdge + }); + } + + redirectURI(): string { + const domainName = this.entityForm.get('name').value; + return domainName !== '' ? `${domainName}${this.loginProcessingUrl}` : ''; + } + + createClient($event: Event, button: MatButton) { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.dialog.open(ClientDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: {} + }).afterClosed() + .subscribe((client) => { + if (client) { + const formValue = this.entityForm.get('oauth2ClientInfos').value ? + [...this.entityForm.get('oauth2ClientInfos').value] : []; + formValue.push(client.id.id); + this.entityForm.get('oauth2ClientInfos').patchValue(formValue); + this.entityForm.get('oauth2ClientInfos').markAsDirty(); + } + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts new file mode 100644 index 0000000000..7585cfff46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts @@ -0,0 +1,143 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { + CellActionDescriptorType, + DateEntityTableColumn, + EntityActionTableColumn, + EntityChipsEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { isEqual } from '@core/utils'; +import { Direction } from '@app/shared/models/page/sort-order'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; +import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; +import { map } from 'rxjs'; + +@Injectable() +export class MobileAppTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private translate: TranslateService, + private datePipe: DatePipe, + private utilsService: UtilsService, + private mobileAppService: MobileAppService) { + this.config.tableTitle = this.translate.instant('admin.oauth2.mobile-apps'); + this.config.selectionEnabled = false; + this.config.entityType = EntityType.MOBILE_APP; + this.config.rowPointer = true; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); + this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP); + this.config.entityComponent = MobileAppComponent; + this.config.headerComponent = MobileAppTableHeaderComponent; + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + this.config.displayPagination = false; + this.config.pageMode = false; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('pkgName', 'admin.oauth2.mobile-package', '170px'), + new EntityTableColumn('appSecret', 'admin.oauth2.mobile-app-secret', '350px', + (entity) => entity.appSecret ? this.appSecretText(entity) : '', () => ({}), + false, () => ({}), () => undefined, false, + { + name: this.translate.instant('admin.oauth2.copy-mobile-app-secret'), + icon: 'content_copy', + style: { + 'padding-top': '1px', + 'font-size': '16px', + color: 'rgba(0,0,0,.87)' + }, + isEnabled: (entity) => !!entity.appSecret, + onAction: ($event, entity) => entity.appSecret, + type: CellActionDescriptorType.COPY_BUTTON + }), + new EntityChipsEntityTableColumn('oauth2ClientInfos', 'admin.oauth2.clients', '20%'), + new EntityActionTableColumn('oauth2Enabled', 'admin.oauth2.enable', + { + name: '', + nameFunction: (app) => + this.translate.instant(app.oauth2Enabled ? 'admin.oauth2.disable' : 'admin.oauth2.enable'), + icon: 'mdi:toggle-switch', + iconFunction: (entity) => entity.oauth2Enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', + isEnabled: () => true, + onAction: ($event, entity) => this.toggleEnableOAuth($event, entity) + }) + ); + + this.config.deleteEntityTitle = (app) => this.translate.instant('admin.oauth2.delete-mobile-app-title', {applicationName: app.pkgName}); + this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-mobile-app-text'); + this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppInfos(pageLink); + this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); + this.config.saveEntity = (mobileApp, originalMobileApp) => { + const clientsIds = mobileApp.oauth2ClientInfos as Array || []; + if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(), + originalMobileApp.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) { + this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds).subscribe(); + } + delete mobileApp.oauth2ClientInfos; + return this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds).pipe( + map(mobileApp => { + (mobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds; + return mobileApp; + }) + ); + } + this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private toggleEnableOAuth($event: Event, mobileApp: MobileAppInfo): void { + if ($event) { + $event.stopPropagation(); + } + + const modifiedMobileApp: MobileAppInfo = { + ...mobileApp, + oauth2Enabled: !mobileApp.oauth2Enabled + }; + + this.mobileAppService.saveMobileApp(modifiedMobileApp, mobileApp.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), + {ignoreLoading: true}) + .subscribe((result) => { + mobileApp.oauth2Enabled = result.oauth2Enabled; + this.config.getTable().detectChanges(); + }); + } + + private appSecretText(entity): string { + let text = entity.appSecret; + if (text.length > 40) { + text = `${text.slice(0, 40)}…`; + } + return text; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html new file mode 100644 index 0000000000..fd66a026da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html @@ -0,0 +1,18 @@ + +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts new file mode 100644 index 0000000000..ca4bbaf74b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MobileAppInfo } from '@shared/models/oauth2.models'; + +@Component({ + selector: 'tb-mobile-app-table-header', + templateUrl: './mobile-app-table-header.component.html', + styleUrls: [] +}) +export class MobileAppTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html new file mode 100644 index 0000000000..f280592925 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html @@ -0,0 +1,71 @@ + +
+
+ + admin.oauth2.mobile-package + + admin.oauth2.mobile-package-hint + + {{ 'admin.oauth2.mobile-package-unique' | translate }} + + +
+
+ + admin.oauth2.mobile-app-secret + + + + admin.oauth2.mobile-app-secret-hint + + {{ 'admin.oauth2.mobile-app-secret-required' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-base64' | translate }} + + +
+
+ + {{ 'admin.oauth2.enable' | translate }} + +
+ + + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts new file mode 100644 index 0000000000..2cca292fc3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts @@ -0,0 +1,110 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { MobileAppInfo } from '@shared/models/oauth2.models'; +import { AppState } from '@core/core.state'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngrx/store'; +import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { WINDOW } from '@core/services/window.service'; +import { isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; +import { MatButton } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; +import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; +import { EntityType } from '@shared/models/entity-type.models'; + +@Component({ + selector: 'tb-mobile-app', + templateUrl: './mobile-app.component.html', + styleUrls: [] +}) +export class MobileAppComponent extends EntityComponent { + + entityType = EntityType; + + constructor(protected store: Store, + protected translate: TranslateService, + private oauth2Service: OAuth2Service, + @Inject('entity') protected entityValue: MobileAppInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: UntypedFormBuilder, + @Inject(WINDOW) private window: Window, + private dialog: MatDialog) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + buildForm(entity: MobileAppInfo): UntypedFormGroup { + return this.fb.group({ + pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required]], + appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), + [Validators.required, this.base64Format]], + oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true, + oauth2ClientInfos: entity?.oauth2ClientInfos ? entity.oauth2ClientInfos.map(info => info.id.id) : [] + }); + } + + updateForm(entity: MobileAppInfo) { + this.entityForm.patchValue({ + pkgName: entity.pkgName, + appSecret: entity.appSecret, + oauth2Enabled: entity.oauth2Enabled, + oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info) + }) + } + + createClient($event: Event, button: MatButton) { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.dialog.open(ClientDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: {} + }).afterClosed() + .subscribe((client) => { + if (client) { + const formValue = this.entityForm.get('oauth2ClientInfos').value ? + [...this.entityForm.get('oauth2ClientInfos').value] : []; + formValue.push(client.id.id); + this.entityForm.get('oauth2ClientInfos').patchValue(formValue); + this.entityForm.get('oauth2ClientInfos').markAsDirty(); + } + }); + } + + private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { + if (control.value === '') { + return null; + } + try { + const value = atob(control.value); + if (value.length < 64) { + return {minLength: true}; + } + return null; + } catch (e) { + return {base64: true}; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts new file mode 100644 index 0000000000..22c172ca14 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts @@ -0,0 +1,150 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable, NgModule } from '@angular/core'; +import { Resolve, RouterModule, Routes } from '@angular/router'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { Authority } from '@shared/models/authority.enum'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { Observable } from 'rxjs'; +import { ClientsTableConfigResolver } from './clients/clients-table-config.resolver'; +import { DomainTableConfigResolver } from '@home/pages/admin/oauth2/domains/domain-table-config.resolver'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { MobileAppTableConfigResolver } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver'; +import { MenuId } from '@core/services/menu.models'; + +@Injectable() +export class OAuth2LoginProcessingUrlResolver implements Resolve { + + constructor(private oauth2Service: OAuth2Service) { + } + + resolve(): Observable { + return this.oauth2Service.getLoginProcessingUrl(); + } +} + +export const oAuth2Routes: Routes = [ + { + path: 'oauth2', + component: RouterTabsComponent, + data: { + auth: [Authority.SYS_ADMIN], + breadcrumb: { + label: 'admin.oauth2.oauth2', + icon: 'mdi:shield-account' + } + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.SYS_ADMIN], + redirectTo: '/security-settings/oauth2/domains' + } + }, + { + path: 'domains', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.oauth2.domains', + breadcrumb: { + menuId: MenuId.domains + } + }, + resolve: { + entitiesTableConfig: DomainTableConfigResolver + } + }, + { + path: 'mobile-applications', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.oauth2.mobile-apps', + breadcrumb: { + menuId: MenuId.mobile_apps + } + }, + resolve: { + entitiesTableConfig: MobileAppTableConfigResolver + } + }, + { + path: 'clients', + data: { + title: 'admin.oauth2.clients', + breadcrumb: { + menuId:MenuId.clients + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.oauth2.clients' + }, + resolve: { + entitiesTableConfig: ClientsTableConfigResolver + } + }, + { + path: 'details', + children: [ + { + path: ':entityId', + component: EntityDetailsPageComponent, + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'public' + } as BreadCrumbConfig, + auth: [Authority.SYS_ADMIN], + title: 'admin.oauth2.clients', + hideTabs: true, + backNavigationCommands: ['../..'] + }, + resolve: { + entitiesTableConfig: ClientsTableConfigResolver + } + } + ] + } + ] + } + ] + } +]; + +@NgModule({ + providers: [ + OAuth2LoginProcessingUrlResolver, + ClientsTableConfigResolver, + DomainTableConfigResolver, + MobileAppTableConfigResolver + ], + imports: [RouterModule.forChild(oAuth2Routes)], + exports: [RouterModule] +}) +export class Oauth2RoutingModule { +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts new file mode 100644 index 0000000000..545b9a6a63 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts @@ -0,0 +1,48 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; +import { Oauth2RoutingModule } from '@home/pages/admin/oauth2/oauth2-routing.module'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { CommonModule } from '@angular/common'; +import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/client-table-header.component'; +import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.component'; +import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; +import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; +import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; +import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; + +@NgModule({ + declarations: [ + ClientComponent, + ClientDialogComponent, + ClientTableHeaderComponent, + DomainComponent, + DomainTableHeaderComponent, + MobileAppComponent, + MobileAppTableHeaderComponent + ], + imports: [ + Oauth2RoutingModule, + CommonModule, + SharedModule, + HomeComponentsModule + ] +}) +export class OAuth2Module { +} diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index 9b0a32baed..83ab1bbe4b 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -23,7 +23,7 @@ import { UntypedFormBuilder } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; import { Router } from '@angular/router'; -import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; +import { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models'; @Component({ selector: 'tb-login', @@ -38,7 +38,7 @@ export class LoginComponent extends PageComponent implements OnInit { username: '', password: '' }); - oauth2Clients: Array = null; + oauth2Clients: Array = null; constructor(protected store: Store, private authService: AuthService, @@ -73,7 +73,7 @@ export class LoginComponent extends PageComponent implements OnInit { } } - getOAuth2Uri(oauth2Client: OAuth2ClientInfo): string { + getOAuth2Uri(oauth2Client: OAuth2ClientLoginInfo): string { let result = ""; if (this.authService.redirectUrl) { result += "?prevUri=" + this.authService.redirectUrl; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 6ab3e3081a..829e49257f 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -91,6 +91,10 @@ export const HelpLinks = { slackSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/slack-settings`, securitySettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/security-settings`, oauth2Settings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/oauth-2-support/`, + oauth2Apple: 'https://developer.apple.com/sign-in-with-apple/get-started/', + oauth2Facebook: 'https://developers.facebook.com/docs/facebook-login/web#logindialog', + oauth2Github: 'https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app', + oauth2Google: 'https://developers.google.com/google-ads/api/docs/start', ruleEngine: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/overview/`, ruleNodeCheckRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node`, ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node`, diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 045e95983c..51fefefdf2 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -44,7 +44,10 @@ export enum EntityType { NOTIFICATION_REQUEST = 'NOTIFICATION_REQUEST', NOTIFICATION_RULE = 'NOTIFICATION_RULE', NOTIFICATION_TARGET = 'NOTIFICATION_TARGET', - NOTIFICATION_TEMPLATE = 'NOTIFICATION_TEMPLATE' + NOTIFICATION_TEMPLATE = 'NOTIFICATION_TEMPLATE', + OAUTH2_CLIENT = 'OAUTH2_CLIENT', + DOMAIN = 'DOMAIN', + MOBILE_APP = 'MOBILE_APP' } export enum AliasEntityType { @@ -423,6 +426,42 @@ export const entityTypeTranslations = new Map([ [EntityType.OTA_PACKAGE, '/features/otaUpdates'], [EntityType.QUEUE, '/settings/queues'], [EntityType.WIDGETS_BUNDLE, '/resources/widgets-library/widgets-bundles/details'], - [EntityType.WIDGET_TYPE, '/resources/widgets-library/widget-types/details'] + [EntityType.WIDGET_TYPE, '/resources/widgets-library/widget-types/details'], + [EntityType.OAUTH2_CLIENT, '/security-settings/oauth2/clients/details'], + [EntityType.DOMAIN, '/security-settings/oauth2/clients/details'], + [EntityType.MOBILE_APP, '/security-settings/oauth2/clients/details'] ]); export interface EntitySubtype { diff --git a/ui-ngx/src/app/shared/models/id/domain-id.ts b/ui-ngx/src/app/shared/models/id/domain-id.ts new file mode 100644 index 0000000000..69d592d342 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/domain-id.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class DomainId implements EntityId { + entityType = EntityType.DOMAIN + id: string; + + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/mobile-app-id.ts b/ui-ngx/src/app/shared/models/id/mobile-app-id.ts new file mode 100644 index 0000000000..2148bc2f5c --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/mobile-app-id.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class MobileAppId implements EntityId { + entityType = EntityType.MOBILE_APP + id: string; + + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/oauth2-client-id.ts b/ui-ngx/src/app/shared/models/id/oauth2-client-id.ts new file mode 100644 index 0000000000..6e69a663ac --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/oauth2-client-id.ts @@ -0,0 +1,27 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class OAuth2ClientId implements EntityId { + entityType = EntityType.OAUTH2_CLIENT + id: string; + + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index 9e3d3fa1c5..dd024715d5 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -14,31 +14,15 @@ /// limitations under the License. /// +import { OAuth2ClientId } from '@shared/models/id/oauth2-client-id'; +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { HasTenantId } from './entity.models'; +import { DomainId } from './id/domain-id'; import { HasUUID } from '@shared/models/id/has-uuid'; +import { MobileAppId } from '@shared/models/id/mobile-app-id'; -export interface OAuth2Info { - enabled: boolean; - edgeEnabled: boolean; - oauth2ParamsInfos: OAuth2ParamsInfo[]; -} - -export interface OAuth2ParamsInfo { - clientRegistrations: OAuth2RegistrationInfo[]; - domainInfos: OAuth2DomainInfo[]; - mobileInfos: OAuth2MobileInfo[]; -} - -export interface OAuth2DomainInfo { - name: string; - scheme: DomainSchema; -} - -export interface OAuth2MobileInfo { - pkgName: string; - appSecret: string; -} - -export enum DomainSchema{ +export enum DomainSchema { HTTP = 'HTTP', HTTPS = 'HTTPS', MIXED = 'MIXED' @@ -52,34 +36,13 @@ export const domainSchemaTranslations = new Map( ] ); -export enum MapperConfigType{ - BASIC = 'BASIC', - CUSTOM = 'CUSTOM', - GITHUB = 'GITHUB', - APPLE = 'APPLE' -} - -export enum TenantNameStrategy{ - DOMAIN = 'DOMAIN', - EMAIL = 'EMAIL', - CUSTOM = 'CUSTOM' -} - export enum PlatformType { WEB = 'WEB', ANDROID = 'ANDROID', IOS = 'IOS' } -export const platformTypeTranslations = new Map( - [ - [PlatformType.WEB, 'admin.oauth2.platform-web'], - [PlatformType.ANDROID, 'admin.oauth2.platform-android'], - [PlatformType.IOS, 'admin.oauth2.platform-ios'] - ] -); - -export interface OAuth2ClientRegistrationTemplate extends OAuth2RegistrationInfo{ +export interface OAuth2ClientRegistrationTemplate extends OAuth2RegistrationInfo { comment: string; createdTime: number; helpLink: string; @@ -101,7 +64,7 @@ export interface OAuth2RegistrationInfo { userInfoUri: string; clientAuthenticationMethod: ClientAuthenticationMethod; userNameAttributeName: string; - mapperConfig: MapperConfig; + mapperConfig: OAuth2MapperConfig; additionalInfo: string; } @@ -110,33 +73,131 @@ export enum ClientAuthenticationMethod { POST = 'POST' } -export interface MapperConfig { +export interface Domain extends BaseData, HasTenantId { + tenantId?: TenantId; + name: string; + oauth2Enabled: boolean; + propagateToEdge: boolean; +} + +export interface HasOauth2Clients { + oauth2ClientInfos?: Array | Array; +} + +export interface DomainInfo extends Domain, HasOauth2Clients { + oauth2ClientInfos?: Array | Array; +} + +export interface MobileApp extends BaseData, HasTenantId { + tenantId?: TenantId; + pkgName: string; + appSecret: string; + oauth2Enabled: boolean; +} + +export interface MobileAppInfo extends MobileApp, HasOauth2Clients { + oauth2ClientInfos?: Array | Array; +} + +export interface OAuth2Client extends BaseData, HasTenantId { + tenantId?: TenantId; + title: string; + mapperConfig: OAuth2MapperConfig; + clientId: string; + clientSecret: string; + authorizationUri: string; + accessTokenUri: string; + scope: Array; + userInfoUri?: string; + userNameAttributeName: string; + jwkSetUri?: string; + clientAuthenticationMethod: ClientAuthenticationMethod; + loginButtonLabel: string; + loginButtonIcon?: string; + platforms?: Array; + additionalInfo: any; +} + +export interface OAuth2MapperConfig { allowUserCreation: boolean; activateUser: boolean; - type: MapperConfigType; - basic?: MapperConfigBasic; - custom?: MapperConfigCustom; + type: MapperType; + basic?: OAuth2BasicMapperConfig; + custom?: OAuth2CustomMapperConfig +} + +export enum MapperType { + BASIC = 'BASIC', + CUSTOM = 'CUSTOM', + GITHUB = 'GITHUB', + APPLE = 'APPLE' } -export interface MapperConfigBasic { - emailAttributeKey: string; +export interface OAuth2BasicMapperConfig { + emailAttributeKey?: string; firstNameAttributeKey?: string; lastNameAttributeKey?: string; - tenantNameStrategy: TenantNameStrategy; + tenantNameStrategy?: TenantNameStrategyType; tenantNamePattern?: string; customerNamePattern?: string; defaultDashboardName?: string; alwaysFullScreen?: boolean; } -export interface MapperConfigCustom { - url: string; +export enum TenantNameStrategyType { + DOMAIN = 'DOMAIN', + EMAIL = 'EMAIL', + CUSTOM = 'CUSTOM' +} + +export interface OAuth2CustomMapperConfig { + url?: string; username?: string; password?: string; + sendToken: boolean; +} + +export const platformTypeTranslations = new Map( + [ + [PlatformType.WEB, 'admin.oauth2.platform-web'], + [PlatformType.ANDROID, 'admin.oauth2.platform-android'], + [PlatformType.IOS, 'admin.oauth2.platform-ios'] + ] +); + +export interface OAuth2ClientInfo extends BaseData { + title: string; + providerName: string; + platforms?: Array; } -export interface OAuth2ClientInfo { +export interface OAuth2ClientLoginInfo { name: string; - icon?: string; + icon: string; url: string; } + +export function getProviderHelpLink(provider: Provider): string { + if (providerHelpLinkMap.has(provider)) { + return providerHelpLinkMap.get(provider); + } + return 'oauth2Settings'; +} + +export enum Provider { + CUSTOM = 'Custom', + FACEBOOK = 'Facebook', + GOOGLE = 'Google', + GITHUB = 'Github', + APPLE = 'Apple' +} + +const providerHelpLinkMap = new Map( + [ + [Provider.CUSTOM, 'oauth2Settings'], + [Provider.APPLE, 'oauth2Apple'], + [Provider.FACEBOOK, 'oauth2Facebook'], + [Provider.GITHUB, 'oauth2Github'], + [Provider.GOOGLE, 'oauth2Google'], + ] +) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index a447f3290b..c1a27bd7bb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -222,6 +222,28 @@ "always-fullscreen": "Always fullscreen", "authorization-uri": "Authorization URI", "authorization-uri-required": "Authorization URI is required.", + "add-client": "Add OAuth 2.0 client", + "client-details": "OAuth 2.0 client details", + "client": "OAuth 2.0 client", + "clients": "OAuth 2.0 clients", + "no-oauth2-clients": "No OAuth 2.0 clients found", + "search-oauth2-clients": "Search OAuth 2.0 clients", + "delete-client-title": "Are you sure you want to delete the OAuth 2.0 client '{{clientName}}'?", + "delete-client-text": "Be careful, after the confirmation the client and all related data will become unrecoverable.", + "delete-mobile-app-title": "Are you sure you want to delete the mobile application '{{applicationName}}'?", + "delete-mobile-app-text": "Be careful, after the confirmation the mobile application and all related data will become unrecoverable.", + "title": "Title", + "client-title-required": "Title is required", + "advanced-settings": "Advanced settings", + "domain-details": "Domain details", + "no-domains": "No domains found", + "search-domains": "Search domains", + "mobile-app-details": "Mobile application details", + "add-mobile-app": "Add mobile application", + "no-mobile-apps": "Search mobile applications", + "search-mobile-apps": "No mobile applications found", + "send-token": "Send token", + "create-new": "Create new", "client-authentication-method": "Client authentication method", "client-id": "Client ID", "client-id-required": "Client ID is required.", @@ -254,7 +276,7 @@ "login-provider": "Login provider", "mapper": "Mapper", "new-domain": "New domain", - "oauth2": "OAuth2", + "oauth2": "OAuth 2.0", "password-max-length": "Password should be less than 256", "redirect-uri-template": "Redirect URI template", "copy-redirect-uri": "Copy redirect URI", @@ -282,11 +304,13 @@ "domain-schema-http": "HTTP", "domain-schema-https": "HTTPS", "domain-schema-mixed": "HTTP+HTTPS", - "enable": "Enable OAuth2 settings", - "edge-enable": "Propagate to Edge", + "enable": "Enable OAuth 2.0 settings", + "disable": "Disable OAuth 2.0 settings", + "edge": "Propagate to Edge", + "edge-enable": "Enable propagation to Edge", + "edge-disable": "Disable propagation to Edge", "domains": "Domains", "mobile-apps": "Mobile applications", - "no-mobile-apps": "No applications configured", "mobile-package": "Application package", "mobile-package-placeholder": "Ex.: my.example.app", "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", @@ -298,7 +322,6 @@ "mobile-app-secret-base64": "Application secret must be base64 format.", "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", "copy-mobile-app-secret": "Copy application secret", - "add-mobile-app": "Add application", "delete-mobile-app": "Delete application info", "providers": "Providers", "platform-web": "Web", @@ -312,6 +335,7 @@ "provider": "Provider", "redirect-url": "Redirect URI", "domain-name": "Domain name", + "domain-name-required": "Domain name is required", "redirect-url-template": "Redirect URI template", "microsoft-tenant-id": "Directory (tenant) Id", "microsoft-tenant-id-required": "Directory (tenant) Id is required", @@ -2335,7 +2359,16 @@ "type-notification-template": "Notification template", "type-notification-templates": "Notification templates", "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", - "link": "link" + "link": "link", + "type-oauth2-client": "OAuth 2.0 client", + "type-oauth2-clients": "OAuth 2.0 clients", + "list-of-oauth2-clients": "{ count, plural, =1 {One OAuth 2.0 client} other {List of # OAuth 2.0 clients} }", + "type-domain": "Domain", + "type-domains": "Domains", + "list-of-domains": "{ count, plural, =1 {One Domain} other {List of # Domains} }", + "type-mobile-app": "Mobile application", + "type-mobile-apps": "Mobile applications", + "list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }" }, "entity-field": { "created-time": "Created time",