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 index 81a1abbc47..da289bfd54 100644 --- 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 @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistration; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; import java.util.List; @@ -28,13 +29,13 @@ public interface OAuth2Service { List getOAuth2Clients(String domainName); - List getSystemOAuth2ClientRegistrations(TenantId tenantId); + OAuth2ClientsParams saveSystemOAuth2ClientsParams(OAuth2ClientsParams oAuth2ClientsParams); - List getTenantOAuth2ClientRegistrations(TenantId tenantId); + OAuth2ClientsParams saveTenantOAuth2ClientsParams(TenantId tenantId, OAuth2ClientsParams oAuth2ClientsParams); - OAuth2ClientRegistration saveSystemOAuth2ClientRegistration(OAuth2ClientRegistration clientRegistration); + OAuth2ClientsParams getSystemOAuth2ClientsParams(TenantId tenantId); - OAuth2ClientRegistration saveTenantOAuth2ClientRegistration(TenantId tenantId, String domainName, OAuth2ClientRegistration clientRegistration); + OAuth2ClientsParams getTenantOAuth2ClientsParams(TenantId tenantId); void deleteDomainOAuth2ClientRegistrationByTenant(TenantId tenantId); 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 index 6ce4ae1599..3dd3c7d42a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -16,24 +16,35 @@ package org.thingsboard.server.dao.oauth2; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.tenant.TenantService; -import java.util.Collections; -import java.util.List; -import java.util.UUID; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; @Slf4j @Service @@ -44,65 +55,158 @@ public class OAuth2ServiceImpl implements OAuth2Service { private static final String OAUTH2_CLIENT_REGISTRATIONS_PARAMS = "oauth2ClientRegistrationsParams"; private static final String OAUTH2_CLIENT_REGISTRATIONS_DOMAIN_NAME_PREFIX = "oauth2ClientRegistrationsDomainNamePrefix"; + private static final String ALLOW_OAUTH2_CONFIGURATION = "allowOAuth2Configuration"; + + + private static final String SYSTEM_SETTINGS_OAUTH2_VALUE = "value"; + private static final String OAUTH2_AUTHORIZATION_PATH_TEMPLATE = "/oauth2/authorization/%s"; @Autowired private AdminSettingsService adminSettingsService; + @Autowired + private AttributesService attributesService; + + @Autowired + private TenantService tenantService; + @Override public List getOAuth2Clients(String domainName) { return Collections.emptyList(); } @Override - public List getSystemOAuth2ClientRegistrations(TenantId tenantId) { - return null; - } - - @Override - public List getTenantOAuth2ClientRegistrations(TenantId tenantId) { - return null; - } - - @Override - public OAuth2ClientRegistration saveSystemOAuth2ClientRegistration(OAuth2ClientRegistration clientRegistration) { + public OAuth2ClientsParams saveSystemOAuth2ClientsParams(OAuth2ClientsParams oAuth2ClientsParams) { // TODO check by registration ID in entities - AdminSettings clientRegistrationParamsSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, OAUTH2_CLIENT_REGISTRATIONS_PARAMS); - if (clientRegistrationParamsSettings == null) { - clientRegistrationParamsSettings = new AdminSettings(); - clientRegistrationParamsSettings.setKey(OAUTH2_CLIENT_REGISTRATIONS_PARAMS); - ObjectNode node = mapper.createObjectNode(); - clientRegistrationParamsSettings.setJsonValue(node); + for (OAuth2ClientRegistration clientRegistration : oAuth2ClientsParams.getClientRegistrations()) { + validator.accept(clientRegistration); } + AdminSettings clientRegistrationParamsSettings = new AdminSettings(); + clientRegistrationParamsSettings.setKey(OAUTH2_CLIENT_REGISTRATIONS_PARAMS); + ObjectNode clientRegistrationsNode = mapper.createObjectNode(); + + oAuth2ClientsParams.setDomainName(""); String json; try { - json = mapper.writeValueAsString(clientRegistration); + json = mapper.writeValueAsString(oAuth2ClientsParams); } catch (JsonProcessingException e) { log.error("Unable to convert OAuth2 Client Registration Params to JSON!", e); throw new IncorrectParameterException("Unable to convert OAuth2 Client Registration Params to JSON!"); } - ObjectNode oldClientRegistrations = (ObjectNode) clientRegistrationParamsSettings.getJsonValue(); - oldClientRegistrations.put(clientRegistration.getRegistrationId(), json); + clientRegistrationsNode.put(SYSTEM_SETTINGS_OAUTH2_VALUE, json); + + clientRegistrationParamsSettings.setJsonValue(clientRegistrationsNode); + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, clientRegistrationParamsSettings); - // TODO ask if that's worth it - return getClientRegistration(clientRegistration.getRegistrationId()); + + return getSystemOAuth2ClientsParams(TenantId.SYS_TENANT_ID); } @Override - public OAuth2ClientRegistration saveTenantOAuth2ClientRegistration(TenantId tenantId, String domainName, OAuth2ClientRegistration clientRegistration) { + public OAuth2ClientsParams saveTenantOAuth2ClientsParams(TenantId tenantId, OAuth2ClientsParams oAuth2ClientsParams) { // TODO ask what if tenant saves config for several different domain names, do we need to check it // TODO check by registration ID in system - return null; + for (OAuth2ClientRegistration clientRegistration : oAuth2ClientsParams.getClientRegistrations()) { + validator.accept(clientRegistration); + } + String clientRegistrationsKey = constructClientRegistrationsKey(oAuth2ClientsParams.getDomainName()); + AdminSettings existentAdminSettingsByKey = adminSettingsService.findAdminSettingsByKey(tenantId, clientRegistrationsKey); + if (StringUtils.isEmpty(oAuth2ClientsParams.getAdminSettingsId())) { + if (existentAdminSettingsByKey == null) { + existentAdminSettingsByKey = saveOAuth2ClientSettings(tenantId, clientRegistrationsKey); + oAuth2ClientsParams.setAdminSettingsId(existentAdminSettingsByKey.getId().getId().toString()); + } else { + log.error("Current domain name [{}] already registered in the system!", oAuth2ClientsParams.getDomainName()); + throw new IncorrectParameterException("Current domain name [" + oAuth2ClientsParams.getDomainName() + "] already registered in the system!"); + } + } else { + AdminSettings existentOAuth2ClientsSettingsById = adminSettingsService.findAdminSettingsById( + tenantId, + new AdminSettingsId(UUID.fromString(oAuth2ClientsParams.getAdminSettingsId())) + ); + + if (existentOAuth2ClientsSettingsById == null) { + log.error("Admin setting ID is already set in login white labeling object, but doesn't exist in the database"); + throw new IllegalStateException("Admin setting ID is already set in login white labeling object, but doesn't exist in the database"); + } + + if (!existentOAuth2ClientsSettingsById.getKey().equals(clientRegistrationsKey)) { + if (existentAdminSettingsByKey == null) { + adminSettingsService.deleteAdminSettingsByKey(tenantId, existentOAuth2ClientsSettingsById.getKey()); + AdminSettings newOAuth2ClientsSettings = saveOAuth2ClientSettings(tenantId, clientRegistrationsKey); + oAuth2ClientsParams.setAdminSettingsId(newOAuth2ClientsSettings.getId().getId().toString()); + } else { + log.error("Current domain name [{}] already registered in the system!", oAuth2ClientsParams.getDomainName()); + throw new IncorrectParameterException("Current domain name [" + oAuth2ClientsParams.getDomainName() + "] already registered in the system!"); + } + } + } + String json; + try { + json = mapper.writeValueAsString(oAuth2ClientsParams); + } catch (JsonProcessingException e) { + log.error("Unable to convert OAuth2 Client Registration Params to JSON!", e); + throw new IncorrectParameterException("Unable to convert OAuth2 Client Registration Params to JSON!"); + } + List attributes = new ArrayList<>(); + long ts = System.currentTimeMillis(); + attributes.add(new BaseAttributeKvEntry(new StringDataEntry(OAUTH2_CLIENT_REGISTRATIONS_PARAMS, json), ts)); + try { + // TODO ask if I need here .get() + attributesService.save(tenantId, tenantId, DataConstants.SERVER_SCOPE, attributes).get(); + } catch (Exception e) { + log.error("Unable to save OAuth2 Client Registration Params to attributes!", e); + throw new IncorrectParameterException("Unable to save OAuth2 Client Registration Params to attributes!"); + } + return getTenantOAuth2ClientsParams(tenantId); + } + + @Override + public OAuth2ClientsParams getSystemOAuth2ClientsParams(TenantId tenantId) { + AdminSettings oauth2ClientsParamsSettings = adminSettingsService.findAdminSettingsByKey(tenantId, OAUTH2_CLIENT_REGISTRATIONS_PARAMS); + String json = null; + if (oauth2ClientsParamsSettings != null) { + json = oauth2ClientsParamsSettings.getJsonValue().get(SYSTEM_SETTINGS_OAUTH2_VALUE).asText(); + } + return constructOAuth2ClientsParams(json); + } + + @Override + public OAuth2ClientsParams getTenantOAuth2ClientsParams(TenantId tenantId) { + ListenableFuture jsonFuture; + if (isOAuth2ClientRegistrationAllowed(tenantId)) { + jsonFuture = getOAuth2ClientsParamsAttribute(tenantId); + } else { + jsonFuture = Futures.immediateFuture(""); + } + try { + return Futures.transform(jsonFuture, this::constructOAuth2ClientsParams, MoreExecutors.directExecutor()).get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to read OAuth2 Clients Params from attributes!", e); + throw new RuntimeException("Failed to read OAuth2 Clients Params from attributes!", e); + } } @Override public void deleteDomainOAuth2ClientRegistrationByTenant(TenantId tenantId) { - + OAuth2ClientsParams params = getTenantOAuth2ClientsParams(tenantId); + if (!StringUtils.isEmpty(params.getDomainName())) { + // TODO don't we need to delete from attributes? + String oauth2ClientsParamsKey = constructClientRegistrationsKey(params.getDomainName()); + adminSettingsService.deleteAdminSettingsByKey(tenantId, oauth2ClientsParamsKey); + } } @Override public boolean isOAuth2ClientRegistrationAllowed(TenantId tenantId) { - return false; + Tenant tenant = tenantService.findTenantById(tenantId); + JsonNode allowOAuth2ConfigurationJsonNode = tenant.getAdditionalInfo() != null ? tenant.getAdditionalInfo().get(ALLOW_OAUTH2_CONFIGURATION) : null; + if (allowOAuth2ConfigurationJsonNode == null) { + return true; + } else { + return allowOAuth2ConfigurationJsonNode.asBoolean(); + } } @Override @@ -110,6 +214,35 @@ public class OAuth2ServiceImpl implements OAuth2Service { return null; } + private ListenableFuture getOAuth2ClientsParamsAttribute(TenantId tenantId) { + ListenableFuture> attributeKvEntriesFuture; + try { + attributeKvEntriesFuture = attributesService.find(tenantId, tenantId, DataConstants.SERVER_SCOPE, + Collections.singletonList(OAUTH2_CLIENT_REGISTRATIONS_PARAMS)); + } catch (Exception e) { + log.error("Unable to read OAuth2 Clients Params from attributes!", e); + throw new IncorrectParameterException("Unable to read OAuth2 Clients Params from attributes!"); + } + return Futures.transform(attributeKvEntriesFuture, attributeKvEntries -> { + if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) { + AttributeKvEntry kvEntry = attributeKvEntries.get(0); + return kvEntry.getValueAsString(); + } else { + return ""; + } + }, MoreExecutors.directExecutor()); + } + + private AdminSettings saveOAuth2ClientSettings(TenantId tenantId, String clientRegistrationsKey) { + AdminSettings oauth2ClientsSettings = new AdminSettings(); + oauth2ClientsSettings.setKey(clientRegistrationsKey); + ObjectNode node = mapper.createObjectNode(); + node.put("entityType", EntityType.TENANT.name()); + node.put("entityId", tenantId.toString()); + oauth2ClientsSettings.setJsonValue(node); + return adminSettingsService.saveAdminSettings(tenantId, oauth2ClientsSettings); + } + private String constructClientRegistrationsKey(String domainName) { String clientRegistrationsKey; if (StringUtils.isEmpty(domainName)) { @@ -119,4 +252,103 @@ public class OAuth2ServiceImpl implements OAuth2Service { } return clientRegistrationsKey; } + + private OAuth2ClientsParams constructOAuth2ClientsParams(String json) { + OAuth2ClientsParams result = null; + if (!StringUtils.isEmpty(json)) { + try { + result = mapper.readValue(json, OAuth2ClientsParams.class); + } catch (IOException e) { + log.error("Unable to read OAuth2 Clients Params from JSON!", e); + throw new IncorrectParameterException("Unable to read OAuth2 Clients Params from JSON!"); + } + } + if (result == null) { + result = new OAuth2ClientsParams(); + } + return result; + } + + private final Consumer validator = clientRegistration -> { + if (StringUtils.isEmpty(clientRegistration.getRegistrationId())) { + throw new DataValidationException("Registration ID should be specified!"); + } + 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.getTokenUri())) { + throw new DataValidationException("Token uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getRedirectUriTemplate())) { + throw new DataValidationException("Redirect uri template should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getScope())) { + throw new DataValidationException("Scope should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getAuthorizationGrantType())) { + throw new DataValidationException("Authorization grant type should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getUserInfoUri())) { + throw new DataValidationException("User info uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) { + throw new DataValidationException("User name attribute name should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getJwkSetUri())) { + throw new DataValidationException("Jwk set uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getClientAuthenticationMethod())) { + throw new DataValidationException("Client authentication method should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getClientName())) { + throw new DataValidationException("Client name 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.getBasicConfig(); + 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.CUSTOM) { + OAuth2CustomMapperConfig customConfig = mapperConfig.getCustomConfig(); + 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!"); + } + if (StringUtils.isEmpty(customConfig.getUsername())) { + throw new DataValidationException("Custom mapper username should be specified!"); + } + if (StringUtils.isEmpty(customConfig.getPassword())) { + throw new DataValidationException("Custom mapper password should be specified!"); + } + } + }; }