diff --git a/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json b/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json new file mode 100644 index 0000000000..9b059e81c1 --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json @@ -0,0 +1,20 @@ +{ + "providerId": "Facebook", + "accessTokenUri": "https://graph.facebook.com/v2.8/oauth/access_token", + "authorizationUri": "https://www.facebook.com/v2.8/dialog/oauth", + "scope": ["email","public_profile"], + "jwkSetUri": null, + "userInfoUri": "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email", + "clientAuthenticationMethod": "BASIC", + "userNameAttributeName": "email", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "first_name", + "lastNameAttributeKey": "last_name", + "tenantNameStrategy": "DOMAIN" + }, + "comment": null, + "loginButtonIcon": "mdi:facebook", + "loginButtonLabel": "Facebook", + "helpLink": "https://developers.facebook.com/docs/facebook-login/web#logindialog" +} diff --git a/application/src/main/data/json/system/oauth2_config_templates/github_config.json b/application/src/main/data/json/system/oauth2_config_templates/github_config.json new file mode 100644 index 0000000000..6c3a51723f --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/github_config.json @@ -0,0 +1,17 @@ +{ + "providerId": "Github", + "accessTokenUri": "https://github.com/login/oauth/access_token", + "authorizationUri": "https://github.com/login/oauth/authorize", + "scope": ["read:user","user:email"], + "jwkSetUri": null, + "userInfoUri": "https://api.github.com/user", + "clientAuthenticationMethod": "BASIC", + "userNameAttributeName": "login", + "basic": { + "tenantNameStrategy": "DOMAIN" + }, + "comment": "In order to log into ThingsBoard you need to have user's email. You may configure and use Custom OAuth2 Mapper to get email information. Please refer to Github Documentation", + "loginButtonIcon": "mdi:github", + "loginButtonLabel": "Github", + "helpLink": "https://docs.github.com/en/developers/apps/creating-an-oauth-app" +} diff --git a/application/src/main/data/json/system/oauth2_config_templates/google_config.json b/application/src/main/data/json/system/oauth2_config_templates/google_config.json new file mode 100644 index 0000000000..2756bcc957 --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/google_config.json @@ -0,0 +1,21 @@ +{ + "providerId": "Google", + "additionalInfo": null, + "accessTokenUri": "https://oauth2.googleapis.com/token", + "authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth", + "scope": ["email","openid","profile"], + "jwkSetUri": "https://www.googleapis.com/oauth2/v3/certs", + "userInfoUri": "https://openidconnect.googleapis.com/v1/userinfo", + "clientAuthenticationMethod": "BASIC", + "userNameAttributeName": "email", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "given_name", + "lastNameAttributeKey": "family_name", + "tenantNameStrategy": "DOMAIN" + }, + "comment": null, + "loginButtonIcon": "mdi:google", + "loginButtonLabel": "Google", + "helpLink": "https://developers.google.com/adwords/api/docs/guides/authentication" +} diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql index c1591e7831..871f55d694 100644 --- a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql +++ b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql @@ -14,6 +14,75 @@ -- limitations under the License. -- +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + provider_id varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + comment varchar, + login_button_icon varchar(255), + login_button_label varchar(255), + help_link varchar(255), + CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) +); + CREATE TABLE IF NOT EXISTS device_profile ( id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY, created_time bigint NOT NULL, diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java new file mode 100644 index 0000000000..de34db0eaf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,267 @@ +/** + * Copyright © 2016-2020 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.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.utils.MiscUtils; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +@Service +@Slf4j +public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; + public static final String DEFAULT_LOGIN_PROCESSING_URI = "/login/oauth2/code/"; + private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; + private static final char PATH_DELIMITER = '/'; + + private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher( + DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); + private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @Autowired(required = false) + private OAuth2Configuration oauth2Configuration; + + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + String registrationId = this.resolveRegistrationId(request); + String redirectUriAction = getAction(request, "login"); + return resolve(request, registrationId, redirectUriAction); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) { + if (registrationId == null) { + return null; + } + String redirectUriAction = getAction(request, "authorize"); + return resolve(request, registrationId, redirectUriAction); + } + + private String getAction(HttpServletRequest request, String defaultAction) { + String action = request.getParameter("action"); + if (action == null) { + return defaultAction; + } + return action; + } + + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) { + if (registrationId == null) { + return null; + } + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + if (clientRegistration == null) { + throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); + } + + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); + + OAuth2AuthorizationRequest.Builder builder; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.authorizationCode(); + Map additionalParameters = new HashMap<>(); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) && + clientRegistration.getScopes().contains(OidcScopes.OPENID)) { + // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // scope + // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. + addNonceParameters(attributes, additionalParameters); + } + if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { + addPkceParameters(attributes, additionalParameters); + } + builder.additionalParameters(additionalParameters); + } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.implicit(); + } else { + throw new IllegalArgumentException("Invalid Authorization Grant Type (" + + clientRegistration.getAuthorizationGrantType().getValue() + + ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); + } + + String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction); + + return builder + .clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(redirectUriStr) + .scopes(clientRegistration.getScopes()) + .state(this.stateGenerator.generateKey()) + .attributes(attributes) + .build(); + } + + private String resolveRegistrationId(HttpServletRequest request) { + if (this.authorizationRequestMatcher.matches(request)) { + return this.authorizationRequestMatcher + .matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME); + } + return null; + } + + /** + * Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:
+ * - baseUrl (e.g. https://localhost/app)
+ * - baseScheme (e.g. https)
+ * - baseHost (e.g. localhost)
+ * - basePort (e.g. :8080)
+ * - basePath (e.g. /app)
+ * - registrationId (e.g. google)
+ * - action (e.g. login)
+ *

+ * Null variables are provided as empty strings. + *

+ * Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL + * + * @return expanded URI + */ + private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) { + Map uriVariables = new HashMap<>(); + uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replacePath(request.getContextPath()) + .replaceQuery(null) + .fragment(null) + .build(); + String scheme = uriComponents.getScheme(); + uriVariables.put("baseScheme", scheme == null ? "" : scheme); + String host = uriComponents.getHost(); + uriVariables.put("baseHost", host == null ? "" : host); + // following logic is based on HierarchicalUriComponents#toUriString() + int port = uriComponents.getPort(); + uriVariables.put("basePort", port == -1 ? "" : ":" + port); + String path = uriComponents.getPath(); + if (StringUtils.hasLength(path)) { + if (path.charAt(0) != PATH_DELIMITER) { + path = PATH_DELIMITER + path; + } + } + uriVariables.put("basePath", path == null ? "" : path); + uriVariables.put("baseUrl", uriComponents.toUriString()); + + uriVariables.put("action", action == null ? "" : action); + + String redirectUri = getRedirectUri(request); + log.trace("Redirect URI - {}.", redirectUri); + + return UriComponentsBuilder.fromUriString(redirectUri) + .buildAndExpand(uriVariables) + .toUriString(); + } + + private String getRedirectUri(HttpServletRequest request) { + String loginProcessingUri = oauth2Configuration != null ? oauth2Configuration.getLoginProcessingUrl() : DEFAULT_LOGIN_PROCESSING_URI; + + String scheme = MiscUtils.getScheme(request); + String domainName = MiscUtils.getDomainName(request); + int port = MiscUtils.getPort(request); + String baseUrl = scheme + "://" + domainName; + if (needsPort(scheme, port)){ + baseUrl += ":" + port; + } + return baseUrl + loginProcessingUri; + } + + private boolean needsPort(String scheme, int port) { + boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80; + boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443; + return !isHttpDefault && !isHttpsDefault; + } + + /** + * Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests. + * + * @param attributes where the {@link OidcParameterNames#NONCE} is stored for the authentication request + * @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is added for the authentication request + * + * @since 5.2 + * @see 3.1.2.1. Authentication Request + */ + private void addNonceParameters(Map attributes, Map additionalParameters) { + try { + String nonce = this.secureKeyGenerator.generateKey(); + String nonceHash = createHash(nonce); + attributes.put(OidcParameterNames.NONCE, nonce); + additionalParameters.put(OidcParameterNames.NONCE, nonceHash); + } catch (NoSuchAlgorithmException e) { } + } + + /** + * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests + * + * @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request + * @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually, + * {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request. + * + * @since 5.2 + * @see 1.1. Protocol Flow + * @see 4.1. Client Creates a Code Verifier + * @see 4.2. Client Creates the Code Challenge + */ + private void addPkceParameters(Map attributes, Map additionalParameters) { + String codeVerifier = this.secureKeyGenerator.generateKey(); + attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); + try { + String codeChallenge = createHash(codeVerifier); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + } catch (NoSuchAlgorithmException e) { + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); + } + } + + private static String createHash(String value) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } +} diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index a96843ce2c..4894ecd6ae 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -175,6 +176,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"); } + @Autowired + private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver; + @Override protected void configure(HttpSecurity http) throws Exception { http.headers().cacheControl().and().frameOptions().disable() @@ -207,8 +211,10 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); - if (oauth2Configuration != null && oauth2Configuration.isEnabled()) { + if (oauth2Configuration != null) { http.oauth2Login() + .authorizationEndpoint().authorizationRequestResolver(oAuth2AuthorizationRequestResolver) + .and() .loginPage("/oauth2Login") .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl()) .successHandler(oauth2AuthenticationSuccessHandler) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 9f6c6f3b44..798b71e114 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -38,10 +38,8 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; @@ -84,9 +82,6 @@ public class AuthController extends BaseController { @Autowired private AuditLogService auditLogService; - @Autowired - private OAuth2Service oauth2Service; - @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/auth/user", method = RequestMethod.GET) public @ResponseBody User getUser() throws ThingsboardException { @@ -336,14 +331,4 @@ public class AuthController extends BaseController { throw handleException(e); } } - - @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) - @ResponseBody - public List getOAuth2Clients() throws ThingsboardException { - try { - return oauth2Service.getOAuth2Clients(); - } catch (Exception e) { - throw handleException(e); - } - } } 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 8434f67d1d..87ddfbb088 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -27,22 +27,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceInfo; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.EntityViewInfo; -import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.TenantInfo; -import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; @@ -50,6 +35,7 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; @@ -93,6 +79,8 @@ 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.model.ModelConstants; +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; +import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantProfileService; @@ -175,6 +163,12 @@ public abstract class BaseController { @Autowired protected DashboardService dashboardService; + @Autowired + protected OAuth2Service oAuth2Service; + + @Autowired + protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService; + @Autowired protected ComponentDiscoveryService componentDescriptorService; diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java new file mode 100644 index 0000000000..35ed01f03e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.List; + +@RestController +@TbCoreComponent +@RequestMapping("/api/oauth2/config/template") +@Slf4j +public class OAuth2ConfigTemplateController extends BaseController { + private static final String CLIENT_REGISTRATION_TEMPLATE_ID = "clientRegistrationTemplateId"; + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(@RequestBody OAuth2ClientRegistrationTemplate clientRegistrationTemplate) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.WRITE); + return oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/{clientRegistrationTemplateId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteClientRegistrationTemplate(@PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException { + checkParameter(CLIENT_REGISTRATION_TEMPLATE_ID, strClientRegistrationTemplateId); + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE); + OAuth2ClientRegistrationTemplateId clientRegistrationTemplateId = new OAuth2ClientRegistrationTemplateId(toUUID(strClientRegistrationTemplateId)); + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public List getClientRegistrationTemplates() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); + return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java new file mode 100644 index 0000000000..ecd74bef39 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; +import org.thingsboard.server.utils.MiscUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class OAuth2Controller extends BaseController { + @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) + @ResponseBody + public List getOAuth2Clients(HttpServletRequest request) throws ThingsboardException { + try { + return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainName(request)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public OAuth2ClientsParams getCurrentOAuth2Params() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); + return oAuth2Service.findOAuth2Params(); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public OAuth2ClientsParams saveOAuth2Params(@RequestBody OAuth2ClientsParams oauth2Params) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE); + oAuth2Service.saveOAuth2Params(oauth2Params); + return oAuth2Service.findOAuth2Params(); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 7bcf38080e..c5afb06483 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -184,6 +184,7 @@ public class ThingsboardInstallService { dataUpdateService.updateData("3.1.1"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); + systemDataLoaderService.createOAuth2Templates(); break; default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); @@ -216,6 +217,7 @@ public class ThingsboardInstallService { systemDataLoaderService.createDefaultTenantProfiles(); systemDataLoaderService.createAdminSettings(); systemDataLoaderService.loadSystemWidgets(); + systemDataLoaderService.createOAuth2Templates(); // systemDataLoaderService.loadSystemPlugins(); // systemDataLoaderService.loadSystemRules(); 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 bb875ec5c4..1a0f9eca65 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 @@ -193,6 +193,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); } + @Override + public void createOAuth2Templates() throws Exception { + installScripts.createOAuth2Templates(); + } + @Override public void loadDemoData() throws Exception { Tenant demoTenant = new Tenant(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index cd74753f31..f432851ffc 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -26,11 +26,13 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -61,6 +63,7 @@ public class InstallScripts { public static final String DEMO_DIR = "demo"; public static final String RULE_CHAINS_DIR = "rule_chains"; public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; + public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String DASHBOARDS_DIR = "dashboards"; public static final String JSON_EXT = ".json"; @@ -80,6 +83,9 @@ public class InstallScripts { @Autowired private WidgetsBundleService widgetsBundleService; + @Autowired + private OAuth2ConfigTemplateService oAuth2TemplateService; + public Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } @@ -228,4 +234,22 @@ public class InstallScripts { throw new RuntimeException("Unable to load dashboard from json", e); } } + + public void createOAuth2Templates() throws Exception { + Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR); + try (DirectoryStream dirStream = Files.newDirectoryStream(oauth2ConfigTemplatesDir, path -> path.toString().endsWith(JSON_EXT))) { + dirStream.forEach( + path -> { + try { + JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile()); + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class); + oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + } catch (Exception e) { + log.error("Unable to load oauth2 config templates from json: [{}]", path.toString()); + throw new RuntimeException("Unable to load oauth2 config templates from json", e); + } + } + ); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index b588c2dff2..b1c670a14c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -23,6 +23,8 @@ public interface SystemDataLoaderService { void createAdminSettings() throws Exception; + void createOAuth2Templates() throws Exception; + void loadSystemWidgets() throws Exception; void updateSystemWidgets() throws Exception; 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 c69721542f..651e234f0a 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 @@ -17,7 +17,6 @@ package org.thingsboard.server.service.security.auth.oauth2; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Strings; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -34,7 +33,6 @@ import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; @@ -49,7 +47,6 @@ import org.thingsboard.server.service.security.model.UserPrincipal; import java.io.IOException; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java new file mode 100644 index 0000000000..246e74245d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2020 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.security.auth.oauth2; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.text.StrSubstitutor; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.dao.oauth2.OAuth2User; + +import java.util.Map; + +@Slf4j +public class BasicMapperUtils { + private static final String START_PLACEHOLDER_PREFIX = "%{"; + private static final String END_PLACEHOLDER_PREFIX = "}"; + + public static OAuth2User getOAuth2User(String email, Map attributes, OAuth2MapperConfig config) { + OAuth2User oauth2User = new OAuth2User(); + oauth2User.setEmail(email); + oauth2User.setTenantName(getTenantName(email, attributes, config)); + if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) { + String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey()); + oauth2User.setLastName(lastName); + } + if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) { + String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey()); + oauth2User.setFirstName(firstName); + } + if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) { + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); + String customerName = sub.replace(config.getBasic().getCustomerNamePattern()); + oauth2User.setCustomerName(customerName); + } + oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen()); + if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) { + oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName()); + } + return oauth2User; + } + + public static String getTenantName(String email, Map attributes, OAuth2MapperConfig config) { + switch (config.getBasic().getTenantNameStrategy()) { + case EMAIL: + return email; + case DOMAIN: + return email.substring(email .indexOf("@") + 1); + case CUSTOM: + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); + return sub.replace(config.getBasic().getTenantNamePattern()); + default: + throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!"); + } + } + + public static String getStringAttributeByKey(Map attributes, String key) { + String result = null; + try { + result = (String) attributes.get(key); + } catch (Exception e) { + log.warn("Can't convert attribute to String by key " + key); + } + return result; + } +} 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 2170479f92..73da9e539f 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 @@ -16,11 +16,9 @@ package org.thingsboard.server.service.security.auth.oauth2; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.text.StrSubstitutor; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; @@ -30,62 +28,12 @@ import java.util.Map; @Slf4j public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { - private static final String START_PLACEHOLDER_PREFIX = "%{"; - private static final String END_PLACEHOLDER_PREFIX = "}"; - private static final String EMAIL_TENANT_STRATEGY = "email"; - private static final String DOMAIN_TENANT_STRATEGY = "domain"; - private static final String CUSTOM_TENANT_STRATEGY = "custom"; - @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) { - OAuth2User oauth2User = new OAuth2User(); + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { Map attributes = token.getPrincipal().getAttributes(); - String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); - oauth2User.setEmail(email); - oauth2User.setTenantName(getTenantName(attributes, config)); - if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) { - String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey()); - oauth2User.setLastName(lastName); - } - if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) { - String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey()); - oauth2User.setFirstName(firstName); - } - if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) { - StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); - String customerName = sub.replace(config.getBasic().getCustomerNamePattern()); - oauth2User.setCustomerName(customerName); - } - oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen()); - if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) { - oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName()); - } + String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); } - - private String getTenantName(Map attributes, OAuth2ClientMapperConfig config) { - switch (config.getBasic().getTenantNameStrategy()) { - case EMAIL_TENANT_STRATEGY: - return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); - case DOMAIN_TENANT_STRATEGY: - String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); - return email.substring(email .indexOf("@") + 1); - case CUSTOM_TENANT_STRATEGY: - StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); - return sub.replace(config.getBasic().getTenantNamePattern()); - default: - throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!"); - } - } - - private String getStringAttributeByKey(Map attributes, String key) { - String result = null; - try { - result = (String) attributes.get(key); - } catch (Exception e) { - log.warn("Can't convert attribute to String by key " + key); - } - return result; - } } 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 42ba95b4ee..a85da830b0 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 @@ -23,28 +23,34 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; -import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; @Service(value = "customOAuth2ClientMapper") @Slf4j public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + private static final String PROVIDER_ACCESS_TOKEN = "provider-access-token"; private static final ObjectMapper json = new ObjectMapper(); private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) { - OAuth2User oauth2User = getOAuth2User(token, config.getCustom()); + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { + OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); } - private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) { + private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { if (!StringUtils.isEmpty(custom.getUsername()) && !StringUtils.isEmpty(custom.getPassword())) { restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword()); } + if (custom.isSendToken() && !StringUtils.isEmpty(providerAccessToken)) { + restTemplateBuilder = restTemplateBuilder.defaultHeader(PROVIDER_ACCESS_TOKEN, providerAccessToken); + } + RestTemplate restTemplate = restTemplateBuilder.build(); String request; try { 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 new file mode 100644 index 0000000000..dcca2b71a1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2020 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.security.auth.oauth2; + +import lombok.Data; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.dao.oauth2.OAuth2User; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; + +@Service(value = "githubOAuth2ClientMapper") +@Slf4j +public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + private static final String EMAIL_URL_KEY = "emailUrl"; + + private static final String AUTHORIZATION = "Authorization"; + + private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); + + @Autowired + private OAuth2Configuration oAuth2Configuration; + + @Override + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { + 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, config.isAllowUserCreation(), config.isActivateUser()); + } + + private synchronized String getEmail(String emailUrl, String oauth2Token) { + restTemplateBuilder = restTemplateBuilder.defaultHeader(AUTHORIZATION, "token " + oauth2Token); + + RestTemplate restTemplate = restTemplateBuilder.build(); + GithubEmailsResponse githubEmailsResponse; + try { + githubEmailsResponse = restTemplate.getForEntity(emailUrl, GithubEmailsResponse.class).getBody(); + if (githubEmailsResponse == null){ + throw new RuntimeException("Empty Github response!"); + } + } catch (Exception e) { + log.error("There was an error during connection to Github API", e); + throw new RuntimeException("Unable to login. Please contact your Administrator!"); + } + Optional emailOpt = githubEmailsResponse.stream() + .filter(GithubEmailResponse::isPrimary) + .map(GithubEmailResponse::getEmail) + .findAny(); + if (emailOpt.isPresent()){ + return emailOpt.get(); + } else { + log.error("Could not find primary email from {}.", githubEmailsResponse); + throw new RuntimeException("Unable to login. Please contact your Administrator!"); + } + } + private static class GithubEmailsResponse extends ArrayList {} + + @Data + @ToString + private static class GithubEmailResponse { + private String email; + private boolean verified; + private boolean primary; + private String visibility; + } +} 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 196bfe7b50..27b24043a5 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 @@ -16,9 +16,9 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.service.security.model.SecurityUser; public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config); + SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java index e1c5b694bb..2e764fe223 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.MapperType; @Component @Slf4j @@ -32,14 +33,20 @@ public class OAuth2ClientMapperProvider { @Qualifier("customOAuth2ClientMapper") private OAuth2ClientMapper customOAuth2ClientMapper; - public OAuth2ClientMapper getOAuth2ClientMapperByType(String oauth2ClientType) { - switch (oauth2ClientType) { - case "custom": + @Autowired + @Qualifier("githubOAuth2ClientMapper") + private OAuth2ClientMapper githubOAuth2ClientMapper; + + public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) { + switch (oauth2MapperType) { + case CUSTOM: return customOAuth2ClientMapper; - case "basic": + case BASIC: return basicOAuth2ClientMapper; + case GITHUB: + return githubOAuth2ClientMapper; default: - throw new RuntimeException("OAuth2ClientMapper with type " + oauth2ClientType + " is not supported!"); + throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!"); } } } 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 3375d046e2..5bb02b9a63 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 @@ -16,13 +16,14 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.oauth2.OAuth2Client; -import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtToken; @@ -34,25 +35,28 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.UUID; @Component(value = "oauth2AuthenticationSuccessHandler") -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenFactory tokenFactory; private final RefreshTokenRepository refreshTokenRepository; private final OAuth2ClientMapperProvider oauth2ClientMapperProvider; - private final OAuth2Configuration oauth2Configuration; + private final OAuth2Service oAuth2Service; + private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; @Autowired public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory, final RefreshTokenRepository refreshTokenRepository, final OAuth2ClientMapperProvider oauth2ClientMapperProvider, - final OAuth2Configuration oauth2Configuration) { + final OAuth2Service oAuth2Service, + final OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { this.tokenFactory = tokenFactory; this.refreshTokenRepository = refreshTokenRepository; this.oauth2ClientMapperProvider = oauth2ClientMapperProvider; - this.oauth2Configuration = oauth2Configuration; + this.oAuth2Service = oAuth2Service; + this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; } @Override @@ -64,9 +68,13 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS try { OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; - OAuth2Client oauth2Client = oauth2Configuration.getClientByRegistrationId(token.getAuthorizedClientRegistrationId()); - OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType()); - SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oauth2Client.getMapperConfig()); + OAuth2ClientRegistrationInfo clientRegistration = oAuth2Service.findClientRegistrationInfo(UUID.fromString(token.getAuthorizedClientRegistrationId())); + OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient( + token.getAuthorizedClientRegistrationId(), + token.getPrincipal().getName()); + OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType()); + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), + clientRegistration.getMapperConfig()); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); 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 96eff6efd0..08534dc2d4 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 @@ -32,6 +32,8 @@ public enum Resource { USER(EntityType.USER), WIDGETS_BUNDLE(EntityType.WIDGETS_BUNDLE), WIDGET_TYPE(EntityType.WIDGET_TYPE), + OAUTH2_CONFIGURATION_INFO(), + 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 766290298a..abed9d5bc7 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 @@ -19,14 +19,10 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.HashMap; -import java.util.Optional; - @Component(value="sysAdminPermissions") public class SysAdminPermissions extends AbstractPermissions { @@ -39,6 +35,8 @@ 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_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 3caa405214..4124292c95 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -19,13 +19,10 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.HashMap; - @Component(value="tenantAdminPermissions") public class TenantAdminPermissions extends AbstractPermissions { diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java index ed13ca603d..ad242c96e5 100644 --- a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java @@ -49,12 +49,27 @@ public class MiscUtils { } public static String constructBaseUrl(HttpServletRequest request) { - String scheme = request.getScheme(); + return String.format("%s://%s:%d", + getScheme(request), + getDomainName(request), + getPort(request)); + } + public static String getScheme(HttpServletRequest request){ + String scheme = request.getScheme(); String forwardedProto = request.getHeader("x-forwarded-proto"); if (forwardedProto != null) { scheme = forwardedProto; } + return scheme; + } + + public static String getDomainName(HttpServletRequest request){ + return request.getServerName(); + } + + public static int getPort(HttpServletRequest request){ + String forwardedProto = request.getHeader("x-forwarded-proto"); int serverPort = request.getServerPort(); if (request.getHeader("x-forwarded-port") != null) { @@ -72,11 +87,6 @@ public class MiscUtils { break; } } - - String baseUrl = String.format("%s://%s:%d", - scheme, - request.getServerName(), - serverPort); - return baseUrl; + return serverPort; } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 51363d3c83..d0ddeb611a 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -113,62 +113,10 @@ security: basic: enabled: "${SECURITY_BASIC_ENABLED:false}" oauth2: - # Enable/disable OAuth 2 login functionality - # For details please refer to https://thingsboard.io/docs/user-guide/oauth-2-support/ - enabled: "${SECURITY_OAUTH2_ENABLED:false}" # Redirect URL where access code from external user management system will be processed loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}" - # List of SSO clients - clients: - default: - # Label that going to be show on login button - 'Login with {loginButtonLabel}' - loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}" - # Icon that going to be show on login button. Material design icon ID (https://material.angularjs.org/latest/api/directive/mdIcon) - loginButtonIcon: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_ICON:}" - clientName: "${SECURITY_OAUTH2_DEFAULT_CLIENT_NAME:ClientName}" - clientId: "${SECURITY_OAUTH2_DEFAULT_CLIENT_ID:}" - clientSecret: "${SECURITY_OAUTH2_DEFAULT_CLIENT_SECRET:}" - accessTokenUri: "${SECURITY_OAUTH2_DEFAULT_ACCESS_TOKEN_URI:}" - authorizationUri: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_URI:}" - scope: "${SECURITY_OAUTH2_DEFAULT_SCOPE:}" - # Redirect URL that must be in sync with 'security.oauth2.loginProcessingUrl', but domain name added - redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}" - jwkSetUri: "${SECURITY_OAUTH2_DEFAULT_JWK_SET_URI:}" - # 'authorization_code', 'implicit', 'refresh_token' or 'client_credentials' - authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}" - clientAuthenticationMethod: "${SECURITY_OAUTH2_DEFAULT_CLIENT_AUTHENTICATION_METHOD:post}" # basic or post - userInfoUri: "${SECURITY_OAUTH2_DEFAULT_USER_INFO_URI:}" - userNameAttributeName: "${SECURITY_OAUTH2_DEFAULT_USER_NAME_ATTRIBUTE_NAME:email}" - mapperConfig: - # Allows to create user if it not exists - allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_ALLOW_USER_CREATION:true}" - # Allows user to setup ThingsBoard internal password and login over default Login window - activateUser: "${SECURITY_OAUTH2_DEFAULT_MAPPER_ACTIVATE_USER:false}" - # Mapper type of converter from external user into internal - 'basic' or 'custom' - type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}" - basic: - # Key from attributes of external user object to use as email - emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}" - firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}" - lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}" - # Strategy for generating Tenant from external user object - 'domain', 'email' or 'custom' - # 'domain' - name of the Tenant will be extracted as domain from the email of the user - # 'email' - name of the Tenant will email of the user - # 'custom' - please configure 'tenantNamePattern' for custom mapping - tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" - # %{attribute_key} as placeholder for attribute value of attributes of external user object - tenantNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_PATTERN:}" - # If this field is not empty, user will be created as a user under defined Customer - # %{attribute_key} as placeholder for attribute value of attributes of external user object - customerNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_PATTERN:}" - # If this field is not empty, user will be created with default defined Dashboard - defaultDashboardName: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_DEFAULT_DASHBOARD_NAME:}" - # If this field is set 'true' along with non-empty 'defaultDashboardName', user will start from the defined Dashboard in fullscreen mode - alwaysFullScreen: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_ALWAYS_FULL_SCREEN:false}" - custom: - url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}" - username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}" - password: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_PASSWORD:}" + githubMapper: + emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}" # Dashboard parameters dashboard: diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java new file mode 100644 index 0000000000..b3718a8d92 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2020 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.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; + +import java.util.List; + +public interface OAuth2ConfigTemplateService { + OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate); + + OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); + + List findAllClientRegistrationTemplates(); + + void deleteClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId); +} 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 d72b6ef98c..073bab823b 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 @@ -16,10 +16,20 @@ package org.thingsboard.server.dao.oauth2; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; import java.util.List; +import java.util.UUID; public interface OAuth2Service { + List getOAuth2Clients(String domainScheme, String domainName); - List getOAuth2Clients(); + void saveOAuth2Params(OAuth2ClientsParams oauth2Params); + + OAuth2ClientsParams findOAuth2Params(); + + OAuth2ClientRegistrationInfo findClientRegistrationInfo(UUID id); + + List findAllClientRegistrationInfos(); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java index ee263c2564..a0c8d46d72 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java @@ -24,7 +24,7 @@ public interface AdminSettingsService { AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId); AdminSettings findAdminSettingsByKey(TenantId tenantId, String key); - + AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings); - + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java index 30fd55d204..e87d9c7b2e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java @@ -20,16 +20,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.UUID; -public class OAuth2IntegrationId extends UUIDBased { - - private static final long serialVersionUID = 1L; +public class OAuth2ClientRegistrationId extends UUIDBased { @JsonCreator - public OAuth2IntegrationId(@JsonProperty("id") UUID id) { + public OAuth2ClientRegistrationId(@JsonProperty("id") UUID id) { super(id); } - public static OAuth2IntegrationId fromString(String oauth2IntegrationId) { - return new OAuth2IntegrationId(UUID.fromString(oauth2IntegrationId)); + public static OAuth2ClientRegistrationId fromString(String clientRegistrationId) { + return new OAuth2ClientRegistrationId(UUID.fromString(clientRegistrationId)); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java new file mode 100644 index 0000000000..87a2109909 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2ClientRegistrationInfoId extends UUIDBased { + + @JsonCreator + public OAuth2ClientRegistrationInfoId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2ClientRegistrationInfoId fromString(String clientRegistrationInfoId) { + return new OAuth2ClientRegistrationInfoId(UUID.fromString(clientRegistrationInfoId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationTemplateId.java new file mode 100644 index 0000000000..cc6ee3a795 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationTemplateId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2ClientRegistrationTemplateId extends UUIDBased { + + @JsonCreator + public OAuth2ClientRegistrationTemplateId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2ClientRegistrationTemplateId fromString(String clientRegistrationTemplateId) { + return new OAuth2ClientRegistrationTemplateId(UUID.fromString(clientRegistrationTemplateId)); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java similarity index 67% rename from dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java index 9676d55f5f..b3b1911523 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java @@ -13,27 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.oauth2; +package org.thingsboard.server.common.data.oauth2; -import lombok.Data; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.*; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; -@Data -public class OAuth2Client { +import java.util.List; - private String loginButtonLabel; - private String loginButtonIcon; - private String clientName; +@EqualsAndHashCode +@Data +@ToString(exclude = {"clientSecret"}) +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ClientRegistrationDto { + private OAuth2MapperConfig mapperConfig; private String clientId; private String clientSecret; - private String accessTokenUri; private String authorizationUri; - private String scope; - private String redirectUriTemplate; - private String jwkSetUri; - private String authorizationGrantType; - private String clientAuthenticationMethod; + private String accessTokenUri; + private List scope; private String userInfoUri; private String userNameAttributeName; - private OAuth2ClientMapperConfig mapperConfig; - + private String jwkSetUri; + private String clientAuthenticationMethod; + private String loginButtonLabel; + private String loginButtonIcon; + private JsonNode additionalInfo; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java new file mode 100644 index 0000000000..7eb5b34661 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 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.*; + +@EqualsAndHashCode +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DomainInfo { + private SchemeType scheme; + private String name; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java new file mode 100644 index 0000000000..9998b2bd35 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2020 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; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ExtendedOAuth2ClientRegistrationInfo extends OAuth2ClientRegistrationInfo { + + private String domainName; + private SchemeType domainScheme; + + public ExtendedOAuth2ClientRegistrationInfo() { + super(); + } + + public ExtendedOAuth2ClientRegistrationInfo(OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo, + SchemeType domainScheme, + String domainName) { + super(oAuth2ClientRegistrationInfo); + this.domainScheme = domainScheme; + this.domainName = domainName; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java new file mode 100644 index 0000000000..25faad187e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 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; + +public enum MapperType { + BASIC, CUSTOM, GITHUB; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java new file mode 100644 index 0000000000..3f9f1c6dc2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 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.*; + +@Builder(toBuilder = true) +@EqualsAndHashCode +@Data +@ToString +public class OAuth2BasicMapperConfig { + private final String emailAttributeKey; + private final String firstNameAttributeKey; + private final String lastNameAttributeKey; + private final TenantNameStrategyType tenantNameStrategy; + private final String tenantNamePattern; + private final String customerNamePattern; + private final String defaultDashboardName; + private final boolean alwaysFullScreen; +} 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 0ee5832e63..f15706a55e 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 @@ -17,27 +17,20 @@ package org.thingsboard.server.common.data.oauth2; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.id.OAuth2IntegrationId; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode @Data -public class OAuth2ClientInfo extends BaseData { +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2ClientInfo { private String name; private String icon; private String url; - public OAuth2ClientInfo() { - super(); - } - - public OAuth2ClientInfo(OAuth2IntegrationId id) { - super(id); - } - public OAuth2ClientInfo(OAuth2ClientInfo oauth2ClientInfo) { - super(oauth2ClientInfo); this.name = oauth2ClientInfo.getName(); this.icon = oauth2ClientInfo.getIcon(); this.url = oauth2ClientInfo.getUrl(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java new file mode 100644 index 0000000000..0ced05cfdb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2020 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.OAuth2ClientRegistrationId; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +@NoArgsConstructor +public class OAuth2ClientRegistration extends BaseData { + + private OAuth2ClientRegistrationInfoId clientRegistrationId; + private String domainName; + private SchemeType domainScheme; + + public OAuth2ClientRegistration(OAuth2ClientRegistration clientRegistration) { + super(clientRegistration); + this.clientRegistrationId = clientRegistration.clientRegistrationId; + this.domainName = clientRegistration.domainName; + this.domainScheme = clientRegistration.domainScheme; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java new file mode 100644 index 0000000000..177d677abf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2020 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.HasName; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString(exclude = {"clientSecret"}) +@NoArgsConstructor +public class OAuth2ClientRegistrationInfo extends SearchTextBasedWithAdditionalInfo implements HasName { + + private boolean enabled; + 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; + + public OAuth2ClientRegistrationInfo(OAuth2ClientRegistrationInfo clientRegistration) { + super(clientRegistration); + this.enabled = clientRegistration.enabled; + this.mapperConfig = clientRegistration.mapperConfig; + this.clientId = clientRegistration.clientId; + this.clientSecret = clientRegistration.clientSecret; + this.authorizationUri = clientRegistration.authorizationUri; + this.accessTokenUri = clientRegistration.accessTokenUri; + this.scope = clientRegistration.scope; + this.userInfoUri = clientRegistration.userInfoUri; + this.userNameAttributeName = clientRegistration.userNameAttributeName; + this.jwkSetUri = clientRegistration.jwkSetUri; + this.clientAuthenticationMethod = clientRegistration.clientAuthenticationMethod; + this.loginButtonLabel = clientRegistration.loginButtonLabel; + this.loginButtonIcon = clientRegistration.loginButtonIcon; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getName() { + return loginButtonLabel; + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java new file mode 100644 index 0000000000..897472f154 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2020 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.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +@NoArgsConstructor +public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo implements HasName { + + private String providerId; + private MapperType mapperType; + private OAuth2BasicMapperConfig basic; + private String authorizationUri; + private String accessTokenUri; + private List scope; + private String userInfoUri; + private String userNameAttributeName; + private String jwkSetUri; + private String clientAuthenticationMethod; + private String comment; + private String loginButtonIcon; + private String loginButtonLabel; + private String helpLink; + + public OAuth2ClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + super(clientRegistrationTemplate); + this.providerId = clientRegistrationTemplate.providerId; + this.mapperType = clientRegistrationTemplate.mapperType; + this.basic = clientRegistrationTemplate.basic; + this.authorizationUri = clientRegistrationTemplate.authorizationUri; + this.accessTokenUri = clientRegistrationTemplate.accessTokenUri; + this.scope = clientRegistrationTemplate.scope; + this.userInfoUri = clientRegistrationTemplate.userInfoUri; + this.userNameAttributeName = clientRegistrationTemplate.userNameAttributeName; + this.jwkSetUri = clientRegistrationTemplate.jwkSetUri; + this.clientAuthenticationMethod = clientRegistrationTemplate.clientAuthenticationMethod; + this.comment = clientRegistrationTemplate.comment; + this.loginButtonIcon = clientRegistrationTemplate.loginButtonIcon; + this.loginButtonLabel = clientRegistrationTemplate.loginButtonLabel; + this.helpLink = clientRegistrationTemplate.helpLink; + } + + @Override + public String getName() { + return providerId; + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java new file mode 100644 index 0000000000..d93401f5cc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2020 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.*; + +import java.util.List; +import java.util.Set; + +@EqualsAndHashCode +@Data +@ToString +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2ClientsDomainParams { + private Set domainInfos; + private Set clientRegistrations; +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java new file mode 100644 index 0000000000..ee20021aca --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 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.*; +import java.util.Set; + +@EqualsAndHashCode +@Data +@ToString +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2ClientsParams { + private boolean enabled; + private Set domainsParams; +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java new file mode 100644 index 0000000000..cead19c39c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 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.*; + +@Builder(toBuilder = true) +@EqualsAndHashCode +@Data +@ToString(exclude = {"password"}) +public class OAuth2CustomMapperConfig { + private final String url; + private final String username; + private final String password; + private final boolean sendToken; +} 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 new file mode 100644 index 0000000000..15b3067cda --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2020 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.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Builder(toBuilder = true) +@EqualsAndHashCode +@Data +@ToString +public class OAuth2MapperConfig { + private boolean allowUserCreation; + private boolean activateUser; + private MapperType type; + private OAuth2BasicMapperConfig basic; + private OAuth2CustomMapperConfig custom; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/SchemeType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/SchemeType.java new file mode 100644 index 0000000000..80d0362b7d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/SchemeType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 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; + +public enum SchemeType { + HTTP, HTTPS, MIXED; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/TenantNameStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/TenantNameStrategyType.java new file mode 100644 index 0000000000..91562a883a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/TenantNameStrategyType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2020 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; + +public enum TenantNameStrategyType { + DOMAIN, EMAIL, CUSTOM; +} 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 fb8a0194b6..b9b81d6cfe 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 @@ -389,6 +389,53 @@ public class ModelConstants { public static final String RULE_NODE_STATE_ENTITY_ID_PROPERTY = "entity_id"; public static final String RULE_NODE_STATE_DATA_PROPERTY = "state_data"; + /** + * OAuth2 client registration constants. + */ + public static final String OAUTH2_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info"; + public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration"; + public static final String OAUTH2_CLIENT_REGISTRATION_TO_DOMAIN_COLUMN_FAMILY_NAME = "oauth2_client_registration_to_domain"; + public static final String OAUTH2_CLIENT_REGISTRATION_TEMPLATE_COLUMN_FAMILY_NAME = "oauth2_client_registration_template"; + public static final String OAUTH2_ENABLED_PROPERTY = "enabled"; + public static final String OAUTH2_TEMPLATE_PROVIDER_ID_PROPERTY = "provider_id"; + public static final String OAUTH2_CLIENT_REGISTRATION_INFO_ID_PROPERTY = "client_registration_info_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_ID_PROPERTY = "client_id"; + public static final String OAUTH2_CLIENT_SECRET_PROPERTY = "client_secret"; + public static final String OAUTH2_AUTHORIZATION_URI_PROPERTY = "authorization_uri"; + public static final String OAUTH2_TOKEN_URI_PROPERTY = "token_uri"; + public static final String OAUTH2_REDIRECT_URI_TEMPLATE_PROPERTY = "redirect_uri_template"; + public static final String OAUTH2_SCOPE_PROPERTY = "scope"; + public static final String OAUTH2_USER_INFO_URI_PROPERTY = "user_info_uri"; + public static final String OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY = "user_name_attribute_name"; + public static final String OAUTH2_JWK_SET_URI_PROPERTY = "jwk_set_uri"; + public static final String OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY = "client_authentication_method"; + public static final String OAUTH2_LOGIN_BUTTON_LABEL_PROPERTY = "login_button_label"; + public static final String OAUTH2_LOGIN_BUTTON_ICON_PROPERTY = "login_button_icon"; + public static final String OAUTH2_ALLOW_USER_CREATION_PROPERTY = "allow_user_creation"; + public static final String OAUTH2_ACTIVATE_USER_PROPERTY = "activate_user"; + public static final String OAUTH2_MAPPER_TYPE_PROPERTY = "type"; + public static final String OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY = "basic_email_attribute_key"; + public static final String OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY = "basic_first_name_attribute_key"; + public static final String OAUTH2_LAST_NAME_ATTRIBUTE_KEY_PROPERTY = "basic_last_name_attribute_key"; + public static final String OAUTH2_TENANT_NAME_STRATEGY_PROPERTY = "basic_tenant_name_strategy"; + public static final String OAUTH2_TENANT_NAME_PATTERN_PROPERTY = "basic_tenant_name_pattern"; + public static final String OAUTH2_CUSTOMER_NAME_PATTERN_PROPERTY = "basic_customer_name_pattern"; + public static final String OAUTH2_DEFAULT_DASHBOARD_NAME_PROPERTY = "basic_default_dashboard_name"; + public static final String OAUTH2_ALWAYS_FULL_SCREEN_PROPERTY = "basic_always_full_screen"; + public static final String OAUTH2_MAPPER_URL_PROPERTY = "custom_url"; + public static final String OAUTH2_MAPPER_USERNAME_PROPERTY = "custom_username"; + public static final String OAUTH2_MAPPER_PASSWORD_PROPERTY = "custom_password"; + public static final String OAUTH2_MAPPER_SEND_TOKEN_PROPERTY = "custom_send_token"; + public static final String OAUTH2_TEMPLATE_COMMENT_PROPERTY = "comment"; + public static final String OAUTH2_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String OAUTH2_TEMPLATE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String OAUTH2_TEMPLATE_LOGIN_BUTTON_ICON_PROPERTY = OAUTH2_LOGIN_BUTTON_ICON_PROPERTY; + public static final String OAUTH2_TEMPLATE_LOGIN_BUTTON_LABEL_PROPERTY = OAUTH2_LOGIN_BUTTON_LABEL_PROPERTY; + public static final String OAUTH2_TEMPLATE_HELP_LINK_PROPERTY = "help_link"; + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java new file mode 100644 index 0000000000..6e9ed4822a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java @@ -0,0 +1,231 @@ +/** + * Copyright © 2016-2020 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 com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.*; +import java.util.Arrays; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractOAuth2ClientRegistrationInfoEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_ENABLED_PROPERTY) + private Boolean enabled; + @Column(name = ModelConstants.OAUTH2_CLIENT_ID_PROPERTY) + private String clientId; + @Column(name = ModelConstants.OAUTH2_CLIENT_SECRET_PROPERTY) + private String clientSecret; + @Column(name = ModelConstants.OAUTH2_AUTHORIZATION_URI_PROPERTY) + private String authorizationUri; + @Column(name = ModelConstants.OAUTH2_TOKEN_URI_PROPERTY) + private String tokenUri; + @Column(name = ModelConstants.OAUTH2_SCOPE_PROPERTY) + private String scope; + @Column(name = ModelConstants.OAUTH2_USER_INFO_URI_PROPERTY) + private String userInfoUri; + @Column(name = ModelConstants.OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY) + private String userNameAttributeName; + @Column(name = ModelConstants.OAUTH2_JWK_SET_URI_PROPERTY) + private String jwkSetUri; + @Column(name = ModelConstants.OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY) + private String clientAuthenticationMethod; + @Column(name = ModelConstants.OAUTH2_LOGIN_BUTTON_LABEL_PROPERTY) + private String loginButtonLabel; + @Column(name = ModelConstants.OAUTH2_LOGIN_BUTTON_ICON_PROPERTY) + private String loginButtonIcon; + @Column(name = ModelConstants.OAUTH2_ALLOW_USER_CREATION_PROPERTY) + private Boolean allowUserCreation; + @Column(name = ModelConstants.OAUTH2_ACTIVATE_USER_PROPERTY) + private Boolean activateUser; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_MAPPER_TYPE_PROPERTY) + private MapperType type; + @Column(name = ModelConstants.OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY) + private String emailAttributeKey; + @Column(name = ModelConstants.OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String firstNameAttributeKey; + @Column(name = ModelConstants.OAUTH2_LAST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String lastNameAttributeKey; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_STRATEGY_PROPERTY) + private TenantNameStrategyType tenantNameStrategy; + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_PATTERN_PROPERTY) + private String tenantNamePattern; + @Column(name = ModelConstants.OAUTH2_CUSTOMER_NAME_PATTERN_PROPERTY) + private String customerNamePattern; + @Column(name = ModelConstants.OAUTH2_DEFAULT_DASHBOARD_NAME_PROPERTY) + private String defaultDashboardName; + @Column(name = ModelConstants.OAUTH2_ALWAYS_FULL_SCREEN_PROPERTY) + private Boolean alwaysFullScreen; + @Column(name = ModelConstants.OAUTH2_MAPPER_URL_PROPERTY) + private String url; + @Column(name = ModelConstants.OAUTH2_MAPPER_USERNAME_PROPERTY) + private String username; + @Column(name = ModelConstants.OAUTH2_MAPPER_PASSWORD_PROPERTY) + private String password; + @Column(name = ModelConstants.OAUTH2_MAPPER_SEND_TOKEN_PROPERTY) + private Boolean sendToken; + + @Type(type = "json") + @Column(name = ModelConstants.OAUTH2_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public AbstractOAuth2ClientRegistrationInfoEntity() { + super(); + } + + public AbstractOAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfo clientRegistrationInfo) { + if (clientRegistrationInfo.getId() != null) { + this.setUuid(clientRegistrationInfo.getId().getId()); + } + this.createdTime = clientRegistrationInfo.getCreatedTime(); + this.enabled = clientRegistrationInfo.isEnabled(); + this.clientId = clientRegistrationInfo.getClientId(); + this.clientSecret = clientRegistrationInfo.getClientSecret(); + this.authorizationUri = clientRegistrationInfo.getAuthorizationUri(); + this.tokenUri = clientRegistrationInfo.getAccessTokenUri(); + this.scope = clientRegistrationInfo.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); + this.userInfoUri = clientRegistrationInfo.getUserInfoUri(); + this.userNameAttributeName = clientRegistrationInfo.getUserNameAttributeName(); + this.jwkSetUri = clientRegistrationInfo.getJwkSetUri(); + this.clientAuthenticationMethod = clientRegistrationInfo.getClientAuthenticationMethod(); + this.loginButtonLabel = clientRegistrationInfo.getLoginButtonLabel(); + this.loginButtonIcon = clientRegistrationInfo.getLoginButtonIcon(); + this.additionalInfo = clientRegistrationInfo.getAdditionalInfo(); + OAuth2MapperConfig mapperConfig = clientRegistrationInfo.getMapperConfig(); + if (mapperConfig != null) { + this.allowUserCreation = mapperConfig.isAllowUserCreation(); + this.activateUser = mapperConfig.isActivateUser(); + this.type = mapperConfig.getType(); + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig != null) { + this.emailAttributeKey = basicConfig.getEmailAttributeKey(); + this.firstNameAttributeKey = basicConfig.getFirstNameAttributeKey(); + this.lastNameAttributeKey = basicConfig.getLastNameAttributeKey(); + this.tenantNameStrategy = basicConfig.getTenantNameStrategy(); + this.tenantNamePattern = basicConfig.getTenantNamePattern(); + this.customerNamePattern = basicConfig.getCustomerNamePattern(); + this.defaultDashboardName = basicConfig.getDefaultDashboardName(); + this.alwaysFullScreen = basicConfig.isAlwaysFullScreen(); + } + OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom(); + if (customConfig != null) { + this.url = customConfig.getUrl(); + this.username = customConfig.getUsername(); + this.password = customConfig.getPassword(); + this.sendToken = customConfig.isSendToken(); + } + } + } + + public AbstractOAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfoEntity oAuth2ClientRegistrationInfoEntity) { + this.setId(oAuth2ClientRegistrationInfoEntity.getId()); + this.setCreatedTime(oAuth2ClientRegistrationInfoEntity.getCreatedTime()); + this.enabled = oAuth2ClientRegistrationInfoEntity.getEnabled(); + this.clientId = oAuth2ClientRegistrationInfoEntity.getClientId(); + this.clientSecret = oAuth2ClientRegistrationInfoEntity.getClientSecret(); + this.authorizationUri = oAuth2ClientRegistrationInfoEntity.getAuthorizationUri(); + this.tokenUri = oAuth2ClientRegistrationInfoEntity.getTokenUri(); + this.scope = oAuth2ClientRegistrationInfoEntity.getScope(); + this.userInfoUri = oAuth2ClientRegistrationInfoEntity.getUserInfoUri(); + this.userNameAttributeName = oAuth2ClientRegistrationInfoEntity.getUserNameAttributeName(); + this.jwkSetUri = oAuth2ClientRegistrationInfoEntity.getJwkSetUri(); + this.clientAuthenticationMethod = oAuth2ClientRegistrationInfoEntity.getClientAuthenticationMethod(); + this.loginButtonLabel = oAuth2ClientRegistrationInfoEntity.getLoginButtonLabel(); + this.loginButtonIcon = oAuth2ClientRegistrationInfoEntity.getLoginButtonIcon(); + this.additionalInfo = oAuth2ClientRegistrationInfoEntity.getAdditionalInfo(); + this.allowUserCreation = oAuth2ClientRegistrationInfoEntity.getAllowUserCreation(); + this.activateUser = oAuth2ClientRegistrationInfoEntity.getActivateUser(); + this.type = oAuth2ClientRegistrationInfoEntity.getType(); + this.emailAttributeKey = oAuth2ClientRegistrationInfoEntity.getEmailAttributeKey(); + this.firstNameAttributeKey = oAuth2ClientRegistrationInfoEntity.getFirstNameAttributeKey(); + this.lastNameAttributeKey = oAuth2ClientRegistrationInfoEntity.getLastNameAttributeKey(); + this.tenantNameStrategy = oAuth2ClientRegistrationInfoEntity.getTenantNameStrategy(); + this.tenantNamePattern = oAuth2ClientRegistrationInfoEntity.getTenantNamePattern(); + this.customerNamePattern = oAuth2ClientRegistrationInfoEntity.getCustomerNamePattern(); + this.defaultDashboardName = oAuth2ClientRegistrationInfoEntity.getDefaultDashboardName(); + this.alwaysFullScreen = oAuth2ClientRegistrationInfoEntity.getAlwaysFullScreen(); + this.url = oAuth2ClientRegistrationInfoEntity.getUrl(); + this.username = oAuth2ClientRegistrationInfoEntity.getUsername(); + this.password = oAuth2ClientRegistrationInfoEntity.getPassword(); + this.sendToken = oAuth2ClientRegistrationInfoEntity.getSendToken(); + } + + + protected OAuth2ClientRegistrationInfo toOAuth2ClientRegistrationInfo() { + OAuth2ClientRegistrationInfo clientRegistrationInfo = new OAuth2ClientRegistrationInfo(); + clientRegistrationInfo.setId(new OAuth2ClientRegistrationInfoId(id)); + clientRegistrationInfo.setEnabled(enabled); + clientRegistrationInfo.setCreatedTime(createdTime); + clientRegistrationInfo.setAdditionalInfo(additionalInfo); + clientRegistrationInfo.setMapperConfig( + OAuth2MapperConfig.builder() + .allowUserCreation(allowUserCreation) + .activateUser(activateUser) + .type(type) + .basic( + (type == MapperType.BASIC || type == MapperType.GITHUB) ? + OAuth2BasicMapperConfig.builder() + .emailAttributeKey(emailAttributeKey) + .firstNameAttributeKey(firstNameAttributeKey) + .lastNameAttributeKey(lastNameAttributeKey) + .tenantNameStrategy(tenantNameStrategy) + .tenantNamePattern(tenantNamePattern) + .customerNamePattern(customerNamePattern) + .defaultDashboardName(defaultDashboardName) + .alwaysFullScreen(alwaysFullScreen) + .build() + : null + ) + .custom( + type == MapperType.CUSTOM ? + OAuth2CustomMapperConfig.builder() + .url(url) + .username(username) + .password(password) + .sendToken(sendToken) + .build() + : null + ) + .build() + ); + clientRegistrationInfo.setClientId(clientId); + clientRegistrationInfo.setClientSecret(clientSecret); + clientRegistrationInfo.setAuthorizationUri(authorizationUri); + clientRegistrationInfo.setAccessTokenUri(tokenUri); + clientRegistrationInfo.setScope(Arrays.asList(scope.split(","))); + clientRegistrationInfo.setUserInfoUri(userInfoUri); + clientRegistrationInfo.setUserNameAttributeName(userNameAttributeName); + clientRegistrationInfo.setJwkSetUri(jwkSetUri); + clientRegistrationInfo.setClientAuthenticationMethod(clientAuthenticationMethod); + clientRegistrationInfo.setLoginButtonLabel(loginButtonLabel); + clientRegistrationInfo.setLoginButtonIcon(loginButtonIcon); + return clientRegistrationInfo; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ExtendedOAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ExtendedOAuth2ClientRegistrationInfoEntity.java new file mode 100644 index 0000000000..ddb6e264ac --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ExtendedOAuth2ClientRegistrationInfoEntity.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 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 lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.oauth2.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.SchemeType; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ExtendedOAuth2ClientRegistrationInfoEntity extends AbstractOAuth2ClientRegistrationInfoEntity { + + private String domainName; + private SchemeType domainScheme; + + public ExtendedOAuth2ClientRegistrationInfoEntity() { + super(); + } + + public ExtendedOAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfoEntity oAuth2ClientRegistrationInfoEntity, + String domainName, + SchemeType domainScheme) { + super(oAuth2ClientRegistrationInfoEntity); + this.domainName = domainName; + this.domainScheme = domainScheme; + } + + @Override + public ExtendedOAuth2ClientRegistrationInfo toData() { + return new ExtendedOAuth2ClientRegistrationInfo(super.toOAuth2ClientRegistrationInfo(), + domainScheme, + domainName); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java new file mode 100644 index 0000000000..466034e31c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 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 lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationId; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.*; +import java.util.Arrays; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME) +public class OAuth2ClientRegistrationEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_INFO_ID_PROPERTY, columnDefinition = "uuid") + private UUID clientRegistrationInfoId; + + @Column(name = ModelConstants.OAUTH2_DOMAIN_NAME_PROPERTY) + private String domainName; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_DOMAIN_SCHEME_PROPERTY) + private SchemeType domainScheme; + + public OAuth2ClientRegistrationEntity() { + super(); + } + + public OAuth2ClientRegistrationEntity(OAuth2ClientRegistration clientRegistration) { + if (clientRegistration.getId() != null) { + this.setUuid(clientRegistration.getId().getId()); + } + if (clientRegistration.getClientRegistrationId() != null){ + this.clientRegistrationInfoId = clientRegistration.getClientRegistrationId().getId(); + } + this.createdTime = clientRegistration.getCreatedTime(); + this.domainName = clientRegistration.getDomainName(); + this.domainScheme = clientRegistration.getDomainScheme(); + } + + @Override + public OAuth2ClientRegistration toData() { + OAuth2ClientRegistration clientRegistration = new OAuth2ClientRegistration(); + clientRegistration.setId(new OAuth2ClientRegistrationId(id)); + clientRegistration.setClientRegistrationId(new OAuth2ClientRegistrationInfoId(clientRegistrationInfoId)); + clientRegistration.setCreatedTime(createdTime); + clientRegistration.setDomainName(domainName); + clientRegistration.setDomainScheme(domainScheme); + return clientRegistration; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java new file mode 100644 index 0000000000..7fd68e2716 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2020 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 lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Entity; +import javax.persistence.Table; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME) +public class OAuth2ClientRegistrationInfoEntity extends AbstractOAuth2ClientRegistrationInfoEntity { + + public OAuth2ClientRegistrationInfoEntity() { + super(); + } + + public OAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfo clientRegistration) { + super(clientRegistration); + } + + public OAuth2ClientRegistrationInfoEntity(OAuth2ClientRegistrationInfoEntity oAuth2ClientRegistrationInfoEntity) { + super(oAuth2ClientRegistrationInfoEntity); + } + + @Override + public OAuth2ClientRegistrationInfo toData() { + return super.toOAuth2ClientRegistrationInfo(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java new file mode 100644 index 0000000000..e7973dcd5d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java @@ -0,0 +1,160 @@ +/** + * Copyright © 2016-2020 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 com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; + +import javax.persistence.*; +import java.util.Arrays; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.OAUTH2_CLIENT_REGISTRATION_TEMPLATE_COLUMN_FAMILY_NAME) +public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_TEMPLATE_PROVIDER_ID_PROPERTY) + private String providerId; + @Column(name = ModelConstants.OAUTH2_AUTHORIZATION_URI_PROPERTY) + private String authorizationUri; + @Column(name = ModelConstants.OAUTH2_TOKEN_URI_PROPERTY) + private String tokenUri; + @Column(name = ModelConstants.OAUTH2_SCOPE_PROPERTY) + private String scope; + @Column(name = ModelConstants.OAUTH2_USER_INFO_URI_PROPERTY) + private String userInfoUri; + @Column(name = ModelConstants.OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY) + private String userNameAttributeName; + @Column(name = ModelConstants.OAUTH2_JWK_SET_URI_PROPERTY) + private String jwkSetUri; + @Column(name = ModelConstants.OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY) + private String clientAuthenticationMethod; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_MAPPER_TYPE_PROPERTY) + private MapperType type; + @Column(name = ModelConstants.OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY) + private String emailAttributeKey; + @Column(name = ModelConstants.OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String firstNameAttributeKey; + @Column(name = ModelConstants.OAUTH2_LAST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String lastNameAttributeKey; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_STRATEGY_PROPERTY) + private TenantNameStrategyType tenantNameStrategy; + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_PATTERN_PROPERTY) + private String tenantNamePattern; + @Column(name = ModelConstants.OAUTH2_CUSTOMER_NAME_PATTERN_PROPERTY) + private String customerNamePattern; + @Column(name = ModelConstants.OAUTH2_DEFAULT_DASHBOARD_NAME_PROPERTY) + private String defaultDashboardName; + @Column(name = ModelConstants.OAUTH2_ALWAYS_FULL_SCREEN_PROPERTY) + private Boolean alwaysFullScreen; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_COMMENT_PROPERTY) + private String comment; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_LOGIN_BUTTON_ICON_PROPERTY) + private String loginButtonIcon; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_LOGIN_BUTTON_LABEL_PROPERTY) + private String loginButtonLabel; + @Column(name = ModelConstants.OAUTH2_TEMPLATE_HELP_LINK_PROPERTY) + private String helpLink; + + @Type(type = "json") + @Column(name = ModelConstants.OAUTH2_TEMPLATE_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public OAuth2ClientRegistrationTemplateEntity() { + } + + public OAuth2ClientRegistrationTemplateEntity(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + if (clientRegistrationTemplate.getId() != null) { + this.setUuid(clientRegistrationTemplate.getId().getId()); + } + this.createdTime = clientRegistrationTemplate.getCreatedTime(); + this.providerId = clientRegistrationTemplate.getProviderId(); + this.authorizationUri = clientRegistrationTemplate.getAuthorizationUri(); + this.tokenUri = clientRegistrationTemplate.getAccessTokenUri(); + this.scope = clientRegistrationTemplate.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); + this.userInfoUri = clientRegistrationTemplate.getUserInfoUri(); + this.userNameAttributeName = clientRegistrationTemplate.getUserNameAttributeName(); + this.jwkSetUri = clientRegistrationTemplate.getJwkSetUri(); + this.clientAuthenticationMethod = clientRegistrationTemplate.getClientAuthenticationMethod(); + this.comment = clientRegistrationTemplate.getComment(); + this.loginButtonIcon = clientRegistrationTemplate.getLoginButtonIcon(); + this.loginButtonLabel = clientRegistrationTemplate.getLoginButtonLabel(); + this.helpLink = clientRegistrationTemplate.getHelpLink(); + this.additionalInfo = clientRegistrationTemplate.getAdditionalInfo(); + this.type = clientRegistrationTemplate.getMapperType(); + OAuth2BasicMapperConfig basicConfig = clientRegistrationTemplate.getBasic(); + if (basicConfig != null) { + this.emailAttributeKey = basicConfig.getEmailAttributeKey(); + this.firstNameAttributeKey = basicConfig.getFirstNameAttributeKey(); + this.lastNameAttributeKey = basicConfig.getLastNameAttributeKey(); + this.tenantNameStrategy = basicConfig.getTenantNameStrategy(); + this.tenantNamePattern = basicConfig.getTenantNamePattern(); + this.customerNamePattern = basicConfig.getCustomerNamePattern(); + this.defaultDashboardName = basicConfig.getDefaultDashboardName(); + this.alwaysFullScreen = basicConfig.isAlwaysFullScreen(); + } + } + + @Override + public OAuth2ClientRegistrationTemplate toData() { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = new OAuth2ClientRegistrationTemplate(); + clientRegistrationTemplate.setId(new OAuth2ClientRegistrationTemplateId(id)); + clientRegistrationTemplate.setCreatedTime(createdTime); + clientRegistrationTemplate.setAdditionalInfo(additionalInfo); + + clientRegistrationTemplate.setMapperType(type); + clientRegistrationTemplate.setProviderId(providerId); + clientRegistrationTemplate.setBasic( + OAuth2BasicMapperConfig.builder() + .emailAttributeKey(emailAttributeKey) + .firstNameAttributeKey(firstNameAttributeKey) + .lastNameAttributeKey(lastNameAttributeKey) + .tenantNameStrategy(tenantNameStrategy) + .tenantNamePattern(tenantNamePattern) + .customerNamePattern(customerNamePattern) + .defaultDashboardName(defaultDashboardName) + .alwaysFullScreen(alwaysFullScreen) + .build() + ); + clientRegistrationTemplate.setAuthorizationUri(authorizationUri); + clientRegistrationTemplate.setAccessTokenUri(tokenUri); + clientRegistrationTemplate.setScope(Arrays.asList(scope.split(","))); + clientRegistrationTemplate.setUserInfoUri(userInfoUri); + clientRegistrationTemplate.setUserNameAttributeName(userNameAttributeName); + clientRegistrationTemplate.setJwkSetUri(jwkSetUri); + clientRegistrationTemplate.setClientAuthenticationMethod(clientAuthenticationMethod); + clientRegistrationTemplate.setComment(comment); + clientRegistrationTemplate.setLoginButtonIcon(loginButtonIcon); + clientRegistrationTemplate.setLoginButtonLabel(loginButtonLabel); + clientRegistrationTemplate.setHelpLink(helpLink); + return clientRegistrationTemplate; + } +} 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 new file mode 100644 index 0000000000..4c9d44a5bc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2020 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.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +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.OAuth2ClientRegistrationInfo; + +import java.util.UUID; + +@Component +public class HybridClientRegistrationRepository implements ClientRegistrationRepository { + private static final String defaultRedirectUriTemplate = "{baseUrl}/login/oauth2/code/{registrationId}"; + + @Autowired + private OAuth2Service oAuth2Service; + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo = oAuth2Service.findClientRegistrationInfo(UUID.fromString(registrationId)); + return oAuth2ClientRegistrationInfo == null ? + null : toSpringClientRegistration(oAuth2ClientRegistrationInfo); + } + + private ClientRegistration toSpringClientRegistration(OAuth2ClientRegistrationInfo localClientRegistration){ + String registrationId = localClientRegistration.getUuidId().toString(); + return ClientRegistration.withRegistrationId(registrationId) + .clientName(localClientRegistration.getName()) + .clientId(localClientRegistration.getClientId()) + .authorizationUri(localClientRegistration.getAuthorizationUri()) + .clientSecret(localClientRegistration.getClientSecret()) + .tokenUri(localClientRegistration.getAccessTokenUri()) + .scope(localClientRegistration.getScope()) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri(localClientRegistration.getUserInfoUri()) + .userNameAttributeName(localClientRegistration.getUserNameAttributeName()) + .jwkSetUri(localClientRegistration.getJwkSetUri()) + .clientAuthenticationMethod(new ClientAuthenticationMethod(localClientRegistration.getClientAuthenticationMethod())) + .redirectUriTemplate(defaultRedirectUriTemplate) + .build(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java deleted file mode 100644 index f2f23843f2..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright © 2016-2020 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 lombok.Data; - -@Data -public class OAuth2ClientMapperConfig { - - private boolean allowUserCreation; - private boolean activateUser; - private String type; - private BasicOAuth2ClientMapperConfig basic; - private CustomOAuth2ClientMapperConfig custom; - - @Data - public static class BasicOAuth2ClientMapperConfig { - private String emailAttributeKey; - private String firstNameAttributeKey; - private String lastNameAttributeKey; - private String tenantNameStrategy; - private String tenantNamePattern; - private String customerNamePattern; - private boolean alwaysFullScreen; - private String defaultDashboardName; - } - - @Data - public static class CustomOAuth2ClientMapperConfig { - private String url; - private String username; - private String password; - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java new file mode 100644 index 0000000000..d56ff34354 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 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.OAuth2ClientRegistration; +import org.thingsboard.server.dao.Dao; + +public interface OAuth2ClientRegistrationDao extends Dao { + void deleteAll(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java new file mode 100644 index 0000000000..a907aefe6b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 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.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.Set; + +public interface OAuth2ClientRegistrationInfoDao extends Dao { + List findAll(); + + List findAllExtended(); + + List findByDomainSchemesAndDomainName(List domainSchemes, String domainName); + + void deleteAll(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java new file mode 100644 index 0000000000..75fcbf1b10 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationTemplateDao.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2020 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.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.dao.Dao; + +import java.util.List; + +public interface OAuth2ClientRegistrationTemplateDao extends Dao { + List findAll(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java new file mode 100644 index 0000000000..4eaeb914c9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.List; + +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateString; + +@Slf4j +@Service +public class OAuth2ConfigTemplateServiceImpl extends AbstractEntityService implements OAuth2ConfigTemplateService { + public static final String INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID = "Incorrect clientRegistrationTemplateId "; + + @Autowired + private OAuth2ClientRegistrationTemplateDao clientRegistrationTemplateDao; + + @Override + public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + log.trace("Executing saveClientRegistrationTemplate [{}]", clientRegistrationTemplate); + clientRegistrationTemplateValidator.validate(clientRegistrationTemplate, o -> TenantId.SYS_TENANT_ID); + OAuth2ClientRegistrationTemplate savedClientRegistrationTemplate; + try { + savedClientRegistrationTemplate = clientRegistrationTemplateDao.save(TenantId.SYS_TENANT_ID, clientRegistrationTemplate); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("oauth2_template_provider_id_unq_key")) { + throw new DataValidationException("Client registration template with such providerId already exists!"); + } else { + throw t; + } + } + return savedClientRegistrationTemplate; + } + + @Override + public OAuth2ClientRegistrationTemplate findClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { + log.trace("Executing findClientRegistrationTemplateById [{}]", templateId); + validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); + return clientRegistrationTemplateDao.findById(TenantId.SYS_TENANT_ID, templateId.getId()); + } + + @Override + public List findAllClientRegistrationTemplates() { + log.trace("Executing findAllClientRegistrationTemplates"); + return clientRegistrationTemplateDao.findAll(); + } + + @Override + public void deleteClientRegistrationTemplateById(OAuth2ClientRegistrationTemplateId templateId) { + log.trace("Executing deleteClientRegistrationTemplateById [{}]", templateId); + validateId(templateId, INCORRECT_CLIENT_REGISTRATION_TEMPLATE_ID + templateId); + clientRegistrationTemplateDao.removeById(TenantId.SYS_TENANT_ID, templateId.getId()); + } + + private final DataValidator clientRegistrationTemplateValidator = + new DataValidator() { + + @Override + protected void validateCreate(TenantId tenantId, OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + } + + @Override + protected void validateUpdate(TenantId tenantId, OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + } + + @Override + protected void validateDataImpl(TenantId tenantId, OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + if (StringUtils.isEmpty(clientRegistrationTemplate.getProviderId())) { + throw new DataValidationException("Provider ID should be specified!"); + } + if (clientRegistrationTemplate.getBasic() == null) { + throw new DataValidationException("Basic mapper config should be specified!"); + } + } + }; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java index e54f9a5b3e..8bd620ac2d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java @@ -17,66 +17,15 @@ package org.thingsboard.server.dao.oauth2; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; @Configuration -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") @ConfigurationProperties(prefix = "security.oauth2") @Data -@Slf4j public class OAuth2Configuration { - - private boolean enabled; private String loginProcessingUrl; - private Map clients = new HashMap<>(); - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - List result = new ArrayList<>(); - for (Map.Entry entry : clients.entrySet()) { - OAuth2Client client = entry.getValue(); - ClientRegistration registration = ClientRegistration.withRegistrationId(entry.getKey()) - .clientId(client.getClientId()) - .authorizationUri(client.getAuthorizationUri()) - .clientSecret(client.getClientSecret()) - .tokenUri(client.getAccessTokenUri()) - .redirectUriTemplate(client.getRedirectUriTemplate()) - .scope(client.getScope().split(",")) - .clientName(client.getClientName()) - .authorizationGrantType(new AuthorizationGrantType(client.getAuthorizationGrantType())) - .userInfoUri(client.getUserInfoUri()) - .userNameAttributeName(client.getUserNameAttributeName()) - .jwkSetUri(client.getJwkSetUri()) - .clientAuthenticationMethod(new ClientAuthenticationMethod(client.getClientAuthenticationMethod())) - .build(); - result.add(registration); - } - return new InMemoryClientRegistrationRepository(result); - } - - public OAuth2Client getClientByRegistrationId(String registrationId) { - OAuth2Client result = null; - if (clients != null && !clients.isEmpty()) { - for (String key : clients.keySet()) { - if (key.equals(registrationId)) { - result = clients.get(key); - break; - } - } - } - return result; - } + private Map githubMapper; } 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 43e11244b0..9fda0e3c2f 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 @@ -18,33 +18,198 @@ package org.thingsboard.server.dao.oauth2; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.IncorrectParameterException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import javax.transaction.Transactional; +import java.util.*; +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 implements OAuth2Service { +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(required = false) - OAuth2Configuration oauth2Configuration; + @Autowired + private OAuth2ClientRegistrationInfoDao clientRegistrationInfoDao; + @Autowired + private OAuth2ClientRegistrationDao clientRegistrationDao; @Override - public List getOAuth2Clients() { - if (oauth2Configuration == null || !oauth2Configuration.isEnabled()) { - return Collections.emptyList(); + public List getOAuth2Clients(String domainSchemeStr, String domainName) { + log.trace("Executing getOAuth2Clients [{}://{}]", domainSchemeStr, domainName); + if (domainSchemeStr == null) { + throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); } - List result = new ArrayList<>(); - for (Map.Entry entry : oauth2Configuration.getClients().entrySet()) { - OAuth2ClientInfo client = new OAuth2ClientInfo(); - client.setName(entry.getValue().getLoginButtonLabel()); - client.setUrl(String.format("/oauth2/authorization/%s", entry.getKey())); - client.setIcon(entry.getValue().getLoginButtonIcon()); - result.add(client); + SchemeType domainScheme; + try { + domainScheme = SchemeType.valueOf(domainSchemeStr.toUpperCase()); + } catch (IllegalArgumentException e){ + throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); } - return result; + validateString(domainName, INCORRECT_DOMAIN_NAME + domainName); + return clientRegistrationInfoDao.findByDomainSchemesAndDomainName(Arrays.asList(domainScheme, SchemeType.MIXED), domainName).stream() + .filter(OAuth2ClientRegistrationInfo::isEnabled) + .map(OAuth2Utils::toClientInfo) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public void saveOAuth2Params(OAuth2ClientsParams oauth2Params) { + log.trace("Executing saveOAuth2Params [{}]", oauth2Params); + clientParamsValidator.accept(oauth2Params); + clientRegistrationDao.deleteAll(); + clientRegistrationInfoDao.deleteAll(); + oauth2Params.getDomainsParams().forEach(domainParams -> { + domainParams.getClientRegistrations().forEach(clientRegistrationDto -> { + OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo = OAuth2Utils.toClientRegistrationInfo(oauth2Params.isEnabled(), clientRegistrationDto); + OAuth2ClientRegistrationInfo savedClientRegistrationInfo = clientRegistrationInfoDao.save(TenantId.SYS_TENANT_ID, oAuth2ClientRegistrationInfo); + domainParams.getDomainInfos().forEach(domainInfo -> { + OAuth2ClientRegistration oAuth2ClientRegistration = OAuth2Utils.toClientRegistration(savedClientRegistrationInfo.getId(), + domainInfo.getScheme(), domainInfo.getName()); + clientRegistrationDao.save(TenantId.SYS_TENANT_ID, oAuth2ClientRegistration); + }); + }); + }); + } + + @Override + public OAuth2ClientsParams findOAuth2Params() { + log.trace("Executing findOAuth2Params"); + List extendedInfos = clientRegistrationInfoDao.findAllExtended(); + return OAuth2Utils.toOAuth2Params(extendedInfos); + } + + @Override + public OAuth2ClientRegistrationInfo findClientRegistrationInfo(UUID id) { + log.trace("Executing findClientRegistrationInfo [{}]", id); + validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); + return clientRegistrationInfoDao.findById(null, id); + } + + @Override + public List findAllClientRegistrationInfos() { + log.trace("Executing findAllClientRegistrationInfos"); + return clientRegistrationInfoDao.findAll(); } + + private final Consumer clientParamsValidator = oauth2Params -> { + if (oauth2Params == null + || oauth2Params.getDomainsParams() == null) { + throw new DataValidationException("Domain params should be specified!"); + } + for (OAuth2ClientsDomainParams domainParams : oauth2Params.getDomainsParams()) { + if (domainParams.getDomainInfos() == null + || domainParams.getDomainInfos().isEmpty()) { + throw new DataValidationException("List of domain configuration should be specified!"); + } + for (DomainInfo domainInfo : domainParams.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!"); + } + } + domainParams.getDomainInfos().stream() + .collect(Collectors.groupingBy(DomainInfo::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!"); + } + }); + if (domainParams.getClientRegistrations() == null || domainParams.getClientRegistrations().isEmpty()) { + throw new DataValidationException("Client registrations should be specified!"); + } + for (ClientRegistrationDto clientRegistration : domainParams.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 (StringUtils.isEmpty(clientRegistration.getScope())) { + throw new DataValidationException("Scope 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.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 new file mode 100644 index 0000000000..a96a0d36c8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2020 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.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.*; + +import java.util.*; + +public class OAuth2Utils { + public static final String OAUTH2_AUTHORIZATION_PATH_TEMPLATE = "/oauth2/authorization/%s"; + + public static OAuth2ClientInfo toClientInfo(OAuth2ClientRegistrationInfo clientRegistrationInfo) { + OAuth2ClientInfo client = new OAuth2ClientInfo(); + client.setName(clientRegistrationInfo.getLoginButtonLabel()); + client.setUrl(String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, clientRegistrationInfo.getUuidId().toString())); + client.setIcon(clientRegistrationInfo.getLoginButtonIcon()); + return client; + } + + public static OAuth2ClientsParams toOAuth2Params(List extendedOAuth2ClientRegistrationInfos) { + Map> domainsByInfoId = new HashMap<>(); + Map infoById = new HashMap<>(); + for (ExtendedOAuth2ClientRegistrationInfo extendedClientRegistrationInfo : extendedOAuth2ClientRegistrationInfos) { + String domainName = extendedClientRegistrationInfo.getDomainName(); + SchemeType domainScheme = extendedClientRegistrationInfo.getDomainScheme(); + domainsByInfoId.computeIfAbsent(extendedClientRegistrationInfo.getId(), key -> new HashSet<>()) + .add(new DomainInfo(domainScheme, domainName)); + infoById.put(extendedClientRegistrationInfo.getId(), extendedClientRegistrationInfo); + } + Map, OAuth2ClientsDomainParams> domainParamsMap = new HashMap<>(); + domainsByInfoId.forEach((clientRegistrationInfoId, domainInfos) -> { + domainParamsMap.computeIfAbsent(domainInfos, + key -> new OAuth2ClientsDomainParams(key, new HashSet<>()) + ) + .getClientRegistrations() + .add(toClientRegistrationDto(infoById.get(clientRegistrationInfoId))); + }); + boolean enabled = extendedOAuth2ClientRegistrationInfos.stream() + .map(OAuth2ClientRegistrationInfo::isEnabled) + .findFirst().orElse(false); + return new OAuth2ClientsParams(enabled, new HashSet<>(domainParamsMap.values())); + } + + public static ClientRegistrationDto toClientRegistrationDto(OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo) { + return ClientRegistrationDto.builder() + .mapperConfig(oAuth2ClientRegistrationInfo.getMapperConfig()) + .clientId(oAuth2ClientRegistrationInfo.getClientId()) + .clientSecret(oAuth2ClientRegistrationInfo.getClientSecret()) + .authorizationUri(oAuth2ClientRegistrationInfo.getAuthorizationUri()) + .accessTokenUri(oAuth2ClientRegistrationInfo.getAccessTokenUri()) + .scope(oAuth2ClientRegistrationInfo.getScope()) + .userInfoUri(oAuth2ClientRegistrationInfo.getUserInfoUri()) + .userNameAttributeName(oAuth2ClientRegistrationInfo.getUserNameAttributeName()) + .jwkSetUri(oAuth2ClientRegistrationInfo.getJwkSetUri()) + .clientAuthenticationMethod(oAuth2ClientRegistrationInfo.getClientAuthenticationMethod()) + .loginButtonLabel(oAuth2ClientRegistrationInfo.getLoginButtonLabel()) + .loginButtonIcon(oAuth2ClientRegistrationInfo.getLoginButtonIcon()) + .additionalInfo(oAuth2ClientRegistrationInfo.getAdditionalInfo()) + .build(); + } + + public static OAuth2ClientRegistrationInfo toClientRegistrationInfo(boolean enabled, ClientRegistrationDto clientRegistrationDto) { + OAuth2ClientRegistrationInfo clientRegistrationInfo = new OAuth2ClientRegistrationInfo(); + clientRegistrationInfo.setEnabled(enabled); + clientRegistrationInfo.setMapperConfig(clientRegistrationDto.getMapperConfig()); + clientRegistrationInfo.setClientId(clientRegistrationDto.getClientId()); + clientRegistrationInfo.setClientSecret(clientRegistrationDto.getClientSecret()); + clientRegistrationInfo.setAuthorizationUri(clientRegistrationDto.getAuthorizationUri()); + clientRegistrationInfo.setAccessTokenUri(clientRegistrationDto.getAccessTokenUri()); + clientRegistrationInfo.setScope(clientRegistrationDto.getScope()); + clientRegistrationInfo.setUserInfoUri(clientRegistrationDto.getUserInfoUri()); + clientRegistrationInfo.setUserNameAttributeName(clientRegistrationDto.getUserNameAttributeName()); + clientRegistrationInfo.setJwkSetUri(clientRegistrationDto.getJwkSetUri()); + clientRegistrationInfo.setClientAuthenticationMethod(clientRegistrationDto.getClientAuthenticationMethod()); + clientRegistrationInfo.setLoginButtonLabel(clientRegistrationDto.getLoginButtonLabel()); + clientRegistrationInfo.setLoginButtonIcon(clientRegistrationDto.getLoginButtonIcon()); + clientRegistrationInfo.setAdditionalInfo(clientRegistrationDto.getAdditionalInfo()); + return clientRegistrationInfo; + } + + public static OAuth2ClientRegistration toClientRegistration(OAuth2ClientRegistrationInfoId clientRegistrationInfoId, SchemeType domainScheme, String domainName) { + OAuth2ClientRegistration clientRegistration = new OAuth2ClientRegistration(); + clientRegistration.setClientRegistrationId(clientRegistrationInfoId); + clientRegistration.setDomainName(domainName); + clientRegistration.setDomainScheme(domainScheme); + return clientRegistration; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java new file mode 100644 index 0000000000..1fd901b4a3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistration; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2ClientRegistrationDao extends JpaAbstractDao implements OAuth2ClientRegistrationDao { + private final OAuth2ClientRegistrationRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ClientRegistrationEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public void deleteAll() { + repository.deleteAll(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java new file mode 100644 index 0000000000..ec8651bc3a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2020 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.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationInfoEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationInfoDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2ClientRegistrationInfoDao extends JpaAbstractDao implements OAuth2ClientRegistrationInfoDao { + private final OAuth2ClientRegistrationInfoRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ClientRegistrationInfoEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public List findAll() { + Iterable entities = repository.findAll(); + List result = new ArrayList<>(); + entities.forEach(entity -> { + result.add(DaoUtil.getData(entity)); + }); + return result; + } + + @Override + public List findAllExtended() { + return repository.findAllExtended().stream() + .map(DaoUtil::getData) + .collect(Collectors.toList()); + } + + @Override + public List findByDomainSchemesAndDomainName(List domainSchemes, String domainName) { + List entities = repository.findAllByDomainSchemesAndName(domainSchemes, domainName); + return entities.stream().map(DaoUtil::getData).collect(Collectors.toList()); + } + + @Override + public void deleteAll() { + repository.deleteAll(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java new file mode 100644 index 0000000000..70c3c03479 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationTemplateDao.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2020 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.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationTemplateDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2ClientRegistrationTemplateDao extends JpaAbstractDao implements OAuth2ClientRegistrationTemplateDao { + private final OAuth2ClientRegistrationTemplateRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ClientRegistrationTemplateEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public List findAll() { + Iterable entities = repository.findAll(); + List result = new ArrayList<>(); + entities.forEach(entity -> result.add(DaoUtil.getData(entity))); + return result; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java new file mode 100644 index 0000000000..96094bd2fe --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2020 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.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.model.sql.ExtendedOAuth2ClientRegistrationInfoEntity; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationInfoEntity; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2ClientRegistrationInfoRepository extends CrudRepository { + @Query("SELECT new OAuth2ClientRegistrationInfoEntity(cr_info) " + + "FROM OAuth2ClientRegistrationInfoEntity cr_info " + + "LEFT JOIN OAuth2ClientRegistrationEntity cr on cr_info.id = cr.clientRegistrationInfoId " + + "WHERE cr.domainName = :domainName " + + "AND cr.domainScheme IN (:domainSchemes)") + List findAllByDomainSchemesAndName(@Param("domainSchemes") List domainSchemes, + @Param("domainName") String domainName); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.ExtendedOAuth2ClientRegistrationInfoEntity(cr_info, cr.domainName, cr.domainScheme) " + + "FROM OAuth2ClientRegistrationInfoEntity cr_info " + + "LEFT JOIN OAuth2ClientRegistrationEntity cr on cr_info.id = cr.clientRegistrationInfoId ") + List findAllExtended(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java new file mode 100644 index 0000000000..438f73c7d1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 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.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationEntity; + +import java.util.UUID; + +public interface OAuth2ClientRegistrationRepository extends CrudRepository { +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java new file mode 100644 index 0000000000..8ffd380496 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationTemplateRepository.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 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.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationTemplateEntity; + +import java.util.UUID; + +public interface OAuth2ClientRegistrationTemplateRepository extends CrudRepository { +} diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 99fc915d24..74f9679b11 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -331,3 +331,73 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( key_id int GENERATED BY DEFAULT AS IDENTITY(start with 0 increment by 1) UNIQUE, CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); + + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + provider_id varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + comment varchar, + login_button_icon varchar(255), + login_button_label varchar(255), + help_link varchar(255), + CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) +); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index a4ff653aa6..6056ba8895 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -359,6 +359,75 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + +CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + provider_id varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + comment varchar, + login_button_icon varchar(255), + login_button_label varchar(255), + help_link varchar(255), + CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) +); + CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java new file mode 100644 index 0000000000..3c32af73a3 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2020 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.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +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.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; + +import java.util.Arrays; +import java.util.UUID; + +public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest { + + @Autowired + protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService; + + @Before + public void beforeRun() throws Exception { + Assert.assertTrue(oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().isEmpty()); + } + + @After + public void after() throws Exception { + oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().forEach(clientRegistrationTemplate -> { + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplate.getId()); + }); + + Assert.assertTrue(oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().isEmpty()); + } + + + @Test(expected = DataValidationException.class) + public void testSaveDuplicateProviderId() { + OAuth2ClientRegistrationTemplate first = validClientRegistrationTemplate("providerId"); + OAuth2ClientRegistrationTemplate second = validClientRegistrationTemplate("providerId"); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(first); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(second); + } + + @Test + public void testCreateNewTemplate() { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = validClientRegistrationTemplate(UUID.randomUUID().toString()); + OAuth2ClientRegistrationTemplate savedClientRegistrationTemplate = oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + + Assert.assertNotNull(savedClientRegistrationTemplate); + Assert.assertNotNull(savedClientRegistrationTemplate.getId()); + clientRegistrationTemplate.setId(savedClientRegistrationTemplate.getId()); + clientRegistrationTemplate.setCreatedTime(savedClientRegistrationTemplate.getCreatedTime()); + Assert.assertEquals(clientRegistrationTemplate, savedClientRegistrationTemplate); + } + + @Test + public void testFindTemplate() { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = validClientRegistrationTemplate(UUID.randomUUID().toString()); + OAuth2ClientRegistrationTemplate savedClientRegistrationTemplate = oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); + + OAuth2ClientRegistrationTemplate foundClientRegistrationTemplate = oAuth2ConfigTemplateService.findClientRegistrationTemplateById(savedClientRegistrationTemplate.getId()); + Assert.assertEquals(savedClientRegistrationTemplate, foundClientRegistrationTemplate); + } + + @Test + public void testFindAll() { + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + + Assert.assertEquals(2, oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().size()); + } + + @Test + public void testDeleteTemplate() { + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + OAuth2ClientRegistrationTemplate saved = oAuth2ConfigTemplateService.saveClientRegistrationTemplate(validClientRegistrationTemplate(UUID.randomUUID().toString())); + + Assert.assertEquals(3, oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().size()); + Assert.assertNotNull(oAuth2ConfigTemplateService.findClientRegistrationTemplateById(saved.getId())); + + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(saved.getId()); + + Assert.assertEquals(2, oAuth2ConfigTemplateService.findAllClientRegistrationTemplates().size()); + Assert.assertNull(oAuth2ConfigTemplateService.findClientRegistrationTemplateById(saved.getId())); + } + + private OAuth2ClientRegistrationTemplate validClientRegistrationTemplate(String providerId) { + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = new OAuth2ClientRegistrationTemplate(); + clientRegistrationTemplate.setProviderId(providerId); + clientRegistrationTemplate.setAdditionalInfo(mapper.createObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + clientRegistrationTemplate.setMapperType(MapperType.BASIC); + clientRegistrationTemplate.setBasic( + OAuth2BasicMapperConfig.builder() + .firstNameAttributeKey("firstName") + .lastNameAttributeKey("lastName") + .emailAttributeKey("email") + .tenantNamePattern("tenant") + .defaultDashboardName("Test") + .alwaysFullScreen(true) + .build() + ); + clientRegistrationTemplate.setAuthorizationUri("authorizationUri"); + clientRegistrationTemplate.setAccessTokenUri("tokenUri"); + clientRegistrationTemplate.setScope(Arrays.asList("scope1", "scope2")); + clientRegistrationTemplate.setUserInfoUri("userInfoUri"); + clientRegistrationTemplate.setUserNameAttributeName("userNameAttributeName"); + clientRegistrationTemplate.setJwkSetUri("jwkSetUri"); + clientRegistrationTemplate.setClientAuthenticationMethod("clientAuthenticationMethod"); + clientRegistrationTemplate.setComment("comment"); + clientRegistrationTemplate.setLoginButtonIcon("icon"); + clientRegistrationTemplate.setLoginButtonLabel("label"); + clientRegistrationTemplate.setHelpLink("helpLink"); + return clientRegistrationTemplate; + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java new file mode 100644 index 0000000000..b54505a3d7 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -0,0 +1,524 @@ +/** + * Copyright © 2016-2020 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.Sets; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.oauth2.OAuth2Service; + +import java.util.*; +import java.util.stream.Collectors; + +public class BaseOAuth2ServiceTest extends AbstractServiceTest { + private static final OAuth2ClientsParams EMPTY_PARAMS = new OAuth2ClientsParams(false, new HashSet<>()); + + @Autowired + protected OAuth2Service oAuth2Service; + + @Before + public void beforeRun() { + Assert.assertTrue(oAuth2Service.findAllClientRegistrationInfos().isEmpty()); + } + + @After + public void after() { + oAuth2Service.saveOAuth2Params(EMPTY_PARAMS); + Assert.assertTrue(oAuth2Service.findAllClientRegistrationInfos().isEmpty()); + Assert.assertTrue(oAuth2Service.findOAuth2Params().getDomainsParams().isEmpty()); + } + + @Test(expected = DataValidationException.class) + public void testSaveHttpAndMixedDomainsTogether() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + oAuth2Service.saveOAuth2Params(clientsParams); + } + + @Test(expected = DataValidationException.class) + public void testSaveHttpsAndMixedDomainsTogether() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + oAuth2Service.saveOAuth2Params(clientsParams); + } + + @Test + public void testCreateAndFindParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + // TODO ask if it's safe to check equality on AdditionalProperties + Assert.assertEquals(clientsParams, foundClientsParams); + } + + @Test + public void testDisableParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + clientsParams.setEnabled(true); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + clientsParams.setEnabled(false); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundDisabledClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertEquals(clientsParams, foundDisabledClientsParams); + } + + @Test + public void testClearDomainParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + oAuth2Service.saveOAuth2Params(EMPTY_PARAMS); + OAuth2ClientsParams foundAfterClearClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundAfterClearClientsParams); + Assert.assertEquals(EMPTY_PARAMS, foundAfterClearClientsParams); + } + + @Test + public void testUpdateClientsParams() { + OAuth2ClientsParams clientsParams = createDefaultClientsParams(); + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + OAuth2ClientsParams newClientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("test-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto() + )) + .build() + )); + oAuth2Service.saveOAuth2Params(newClientsParams); + OAuth2ClientsParams foundAfterUpdateClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundAfterUpdateClientsParams); + Assert.assertEquals(newClientsParams, foundAfterUpdateClientsParams); + } + + @Test + public void testGetOAuth2Clients() { + Set firstGroup = Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + ); + Set secondGroup = Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto() + ); + Set thirdGroup = Sets.newHashSet( + validClientRegistrationDto() + ); + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(firstGroup) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(secondGroup) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + )) + .clientRegistrations(thirdGroup) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + List firstGroupClientInfos = firstGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + List secondGroupClientInfos = secondGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + List thirdGroupClientInfos = thirdGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + + List nonExistentDomainClients = oAuth2Service.getOAuth2Clients("http", "non-existent-domain"); + Assert.assertTrue(nonExistentDomainClients.isEmpty()); + + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain"); + 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"); + Assert.assertTrue(firstDomainHttpsClients.isEmpty()); + + List fourthDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "fourth-domain"); + 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"); + 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"); + 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"); + 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() { + Set firstGroup = Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + ); + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(firstGroup) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); + Assert.assertNotNull(foundClientsParams); + Assert.assertEquals(clientsParams, foundClientsParams); + + List firstGroupClientInfos = firstGroup.stream() + .map(clientRegistrationDto -> new OAuth2ClientInfo( + clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .collect(Collectors.toList()); + + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain"); + 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"); + 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() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + + List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + Assert.assertEquals(5, secondDomainHttpClients.size()); + + clientsParams.setEnabled(false); + oAuth2Service.saveOAuth2Params(clientsParams); + + List secondDomainHttpDisabledClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + Assert.assertEquals(0, secondDomainHttpDisabledClients.size()); + } + + @Test + public void testFindAllClientRegistrationInfos() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto() + )) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + List foundClientRegistrationInfos = oAuth2Service.findAllClientRegistrationInfos(); + Assert.assertEquals(6, foundClientRegistrationInfos.size()); + clientsParams.getDomainsParams().stream() + .flatMap(domainParams -> domainParams.getClientRegistrations().stream()) + .forEach(clientRegistrationDto -> + Assert.assertTrue( + foundClientRegistrationInfos.stream() + .anyMatch(clientRegistrationInfo -> clientRegistrationInfo.getClientId().equals(clientRegistrationDto.getClientId())) + ) + ); + } + + @Test + public void testFindClientRegistrationById() { + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto() + )) + .build() + )); + + oAuth2Service.saveOAuth2Params(clientsParams); + List clientRegistrationInfos = oAuth2Service.findAllClientRegistrationInfos(); + clientRegistrationInfos.forEach(clientRegistrationInfo -> { + OAuth2ClientRegistrationInfo foundClientRegistrationInfo = oAuth2Service.findClientRegistrationInfo(clientRegistrationInfo.getUuidId()); + Assert.assertEquals(clientRegistrationInfo, foundClientRegistrationInfo); + }); + } + + private OAuth2ClientsParams createDefaultClientsParams() { + return new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build(), + OAuth2ClientsDomainParams.builder() + .domainInfos(Sets.newHashSet( + DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .clientRegistrations(Sets.newHashSet( + validClientRegistrationDto(), + validClientRegistrationDto() + )) + .build() + )); + } + + private ClientRegistrationDto validClientRegistrationDto() { + return ClientRegistrationDto.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())) + .userInfoUri(UUID.randomUUID().toString()) + .userNameAttributeName(UUID.randomUUID().toString()) + .jwkSetUri(UUID.randomUUID().toString()) + .clientAuthenticationMethod(UUID.randomUUID().toString()) + .loginButtonLabel(UUID.randomUUID().toString()) + .loginButtonIcon(UUID.randomUUID().toString()) + .additionalInfo(mapper.createObjectNode().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(); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ConfigTemplateServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ConfigTemplateServiceSqlTest.java new file mode 100644 index 0000000000..53fd2e37b4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ConfigTemplateServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 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.sql; + +import org.thingsboard.server.dao.service.BaseOAuth2ConfigTemplateServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class OAuth2ConfigTemplateServiceSqlTest extends BaseOAuth2ConfigTemplateServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ServiceSqlTest.java new file mode 100644 index 0000000000..64eba8f89a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/OAuth2ServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 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.sql; + +import org.thingsboard.server.dao.service.BaseOAuth2ServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class OAuth2ServiceSqlTest extends BaseOAuth2ServiceTest { +} diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index c8cc908125..160049a144 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -24,4 +24,7 @@ DROP TABLE IF EXISTS tenant_profile; DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; +DROP TABLE IF EXISTS oauth2_client_registration; +DROP TABLE IF EXISTS oauth2_client_registration_info; +DROP TABLE IF EXISTS oauth2_client_registration_template; DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 899a66f510..e277c39403 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -25,3 +25,6 @@ DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS tb_schema_settings; +DROP TABLE IF EXISTS oauth2_client_registration; +DROP TABLE IF EXISTS oauth2_client_registration_info; +DROP TABLE IF EXISTS oauth2_client_registration_template; diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index 4270a2a192..da7eaff876 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -25,3 +25,6 @@ DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; DROP TABLE IF EXISTS tb_schema_settings; +DROP TABLE IF EXISTS oauth2_client_registration; +DROP TABLE IF EXISTS oauth2_client_registration_info; +DROP TABLE IF EXISTS oauth2_client_registration_template; diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index f5e8c49028..32733ac139 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -18,7 +18,7 @@ import { Injectable, NgZone } from '@angular/core'; import { JwtHelperService } from '@auth0/angular-jwt'; import { HttpClient } from '@angular/common/http'; -import { forkJoin, Observable, of, throwError, ReplaySubject } from 'rxjs'; +import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { LoginRequest, LoginResponse, OAuth2Client, PublicLoginRequest } from '@shared/models/login.models'; diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts index f575f56906..9423392773 100644 --- a/ui-ngx/src/app/core/http/admin.service.ts +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -18,7 +18,14 @@ import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; -import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models'; +import { + AdminSettings, + ClientProviderTemplated, + MailServerSettings, + OAuth2Settings, + SecuritySettings, + UpdateMessage +} from '@shared/models/settings.models'; @Injectable({ providedIn: 'root' @@ -53,6 +60,19 @@ export class AdminService { defaultHttpOptionsFromConfig(config)); } + 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: OAuth2Settings, config?: RequestConfig): Observable { + return this.http.post('/api/oauth2/config', OAuth2Setting, + defaultHttpOptionsFromConfig(config)); + } + public checkUpdates(config?: RequestConfig): Observable { return this.http.get(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 05477f1ac1..e44c3c6ec2 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -108,7 +108,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '120px', + height: '160px', icon: 'settings', pages: [ { @@ -131,6 +131,13 @@ export class MenuService { type: 'link', path: '/settings/security-settings', icon: 'security' + }, + { + id: guid(), + name: 'admin.oauth2.oauth2', + type: 'link', + path: '/settings/oauth2', + icon: 'security' } ] } 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 47f1a5261b..af7dc98e19 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 @@ -22,6 +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'; const routes: Routes = [ { @@ -77,6 +78,19 @@ const routes: Routes = [ icon: 'security' } } + }, + { + path: 'oauth2', + component: OAuth2SettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.oauth2.oauth2', + breadcrumb: { + label: 'admin.oauth2.oauth2', + icon: 'security' + } + } } ] } 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 48bcf56fd5..73fef6a32f 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,13 +23,15 @@ 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'; @NgModule({ declarations: [ GeneralSettingsComponent, MailServerComponent, - SecuritySettingsComponent + SecuritySettingsComponent, + OAuth2SettingsComponent ], imports: [ CommonModule, 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 new file mode 100644 index 0000000000..30564e9306 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -0,0 +1,478 @@ + +

+ + +
+ admin.oauth2.oauth2 + +
+
+
+ + +
+ +
+
+ + {{ 'admin.oauth2.enable' | translate }} + +
+ +
+ + + + + + {{ domainListTittle(domain) }} + + + + + + + + +
+
+
+
+
+ + admin.oauth2.protocol + + + {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} + + + + + admin.domain-name + + + {{ 'admin.error-verification-url' | translate }} + + +
+ + {{ 'admin.domain-name-unique' | translate }} + +
+ +
+ + admin.oauth2.redirect-uri-template + + + + + + + + + +
+
+ +
+ +
+
+
+
+ +
+
+ + +
+ + + + {{ getProviderName(registration) }} + + + + + + + +
+
+ + admin.oauth2.login-provider + + + {{ provider }} + + + +
+
+ +
+ + admin.oauth2.client-id + + + {{ 'admin.oauth2.client-id-required' | translate }} + + + + + admin.oauth2.client-secret + + + {{ 'admin.oauth2.client-secret-required' | 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.user-info-uri-required' | translate }} + + + {{ '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 + + + {{ converterTypeExternalUser }} + + + + +
+ + admin.oauth2.email-attribute-key + + + {{ 'admin.oauth2.email-attribute-key-required' | translate }} + + + +
+ + admin.oauth2.first-name-attribute-key + + + + + admin.oauth2.last-name-attribute-key + + +
+ +
+ + admin.oauth2.tenant-name-strategy + + + {{ tenantNameStrategy }} + + + + + + admin.oauth2.tenant-name-pattern + + + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + + +
+ + + admin.oauth2.customer-name-pattern + + + +
+ + admin.oauth2.default-dashboard-name + + + + + {{ 'admin.oauth2.always-fullscreen' | translate}} + +
+
+ +
+ + admin.oauth2.url + + + {{ 'admin.oauth2.url-required' | translate }} + + + {{ 'admin.oauth2.url-pattern' | translate }} + + + +
+ + common.username + + + + + common.password + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
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 new file mode 100644 index 0000000000..0ad644f61b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + .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: 1em; + + .tb-highlight{ + margin: 0; + } + + .tb-hint{ + padding-bottom: 0; + } + } +} + +:host ::ng-deep{ + .registration-card{ + .custom-settings{ + .mat-expansion-panel-body{ + padding: 0 2px 1em; + } + .mat-tab-label{ + text-transform: none; + } + .mat-form-field-suffix .mat-icon-button .mat-icon{ + font-size: 18px; + } + } + } + .domains-list{ + margin-bottom: 1.5em; + .mat-form-field-suffix .mat-icon-button .mat-icon{ + font-size: 24px; + } + } +} 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 new file mode 100644 index 0000000000..11b39468e9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -0,0 +1,500 @@ +/// +/// Copyright © 2016-2020 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, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { + ClientAuthenticationMethod, + ClientProviderTemplated, + ClientRegistration, + DomainInfo, + DomainSchema, + domainSchemaTranslations, + DomainsParam, + MapperConfig, + MapperConfigBasic, + MapperConfigCustom, + MapperConfigType, + OAuth2Settings, + TenantNameStrategy +} from '@shared/models/settings.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AdminService } from '@core/http/admin.service'; +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 } from '@core/utils'; + +@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 { + + private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; + 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: FormGroup; + oauth2Settings: OAuth2Settings; + + clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); + converterTypesExternalUser = Object.keys(MapperConfigType); + tenantNameStrategies = Object.keys(TenantNameStrategy); + protocols = Object.keys(DomainSchema); + domainSchemaTranslations = domainSchemaTranslations; + + templateProvider = ['Custom']; + + constructor(protected store: Store, + private adminService: AdminService, + private fb: FormBuilder, + private dialogService: DialogService, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) { + super(store); + } + + ngOnInit(): void { + this.buildOAuth2SettingsForm(); + forkJoin([ + this.adminService.getOAuth2Template(), + this.adminService.getOAuth2Settings() + ]).subscribe( + ([templates, oauth2Settings]) => { + this.initTemplates(templates); + this.oauth2Settings = oauth2Settings; + this.initOAuth2Settings(this.oauth2Settings); + } + ); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } + + private initTemplates(templates: ClientProviderTemplated[]): void { + templates.map(provider => this.templates.set(provider.name, provider)); + this.templateProvider.push(...Array.from(this.templates.keys())); + this.templateProvider.sort(); + } + + get domainsParams(): FormArray { + return this.oauth2SettingsForm.get('domainsParams') as FormArray; + } + + private formBasicGroup(mapperConfigBasic?: MapperConfigBasic): FormGroup { + 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], + firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : ''], + lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : ''], + tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN], + tenantNamePattern: [tenantNamePattern, Validators.required], + customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null], + defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null], + alwaysFullScreen: [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): FormGroup { + return this.fb.group({ + url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, [Validators.required, Validators.pattern(this.URL_REGEXP)]], + username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null], + password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null] + }); + } + + private buildOAuth2SettingsForm(): void { + this.oauth2SettingsForm = this.fb.group({ + domainsParams: this.fb.array([]), + enabled: [false] + }); + } + + private initOAuth2Settings(oauth2Settings: OAuth2Settings): void { + if (oauth2Settings) { + this.oauth2SettingsForm.patchValue({enabled: oauth2Settings.enabled}, {emitEvent: false}); + oauth2Settings.domainsParams.forEach((domain) => { + this.domainsParams.push(this.buildDomainsForm(domain)); + }); + } + } + + private uniqueDomainValidator(control: FormGroup): { [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; + } + + public domainListTittle(control: AbstractControl): string { + const domainInfos = control.get('domainInfos').value as DomainInfo[]; + 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 buildDomainsForm(domainParams?: DomainsParam): FormGroup { + const formDomain = this.fb.group({ + domainInfos: this.fb.array([], Validators.required), + clientRegistrations: this.fb.array([], Validators.required) + }); + + if (domainParams) { + domainParams.domainInfos.forEach((domain) => { + this.clientDomainInfos(formDomain).push(this.buildDomainForm(domain)); + }); + domainParams.clientRegistrations.forEach((registration) => { + this.clientDomainProviders(formDomain).push(this.buildProviderForm(registration)); + }); + } else { + this.clientDomainProviders(formDomain).push(this.buildProviderForm()); + this.clientDomainInfos(formDomain).push(this.buildDomainForm()); + } + + return formDomain; + } + + private buildDomainForm(domainInfo?: DomainInfo): FormGroup { + const domain = this.fb.group({ + name: [domainInfo ? domainInfo.name : this.window.location.hostname, [ + Validators.required, + Validators.pattern('((?![:/]).)*$')]], + scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required] + }, {validators: this.uniqueDomainValidator}); + return domain; + } + + private buildProviderForm(registrationData?: ClientRegistration): FormGroup { + let additionalInfo = null; + if (registrationData?.additionalInfo) { + additionalInfo = JSON.parse(registrationData.additionalInfo); + if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { + additionalInfo.providerName = 'Custom'; + } + } + let defaultProviderName = 'Custom'; + if (this.templateProvider.indexOf('Google')) { + defaultProviderName = 'Google'; + } + + const clientRegistration = this.fb.group({ + id: this.fb.group({ + id: [registrationData?.id?.id ? registrationData.id.id : null], + entityType: [registrationData?.id?.entityType ? registrationData.id.entityType : null] + }), + additionalInfo: this.fb.group({ + providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required] + }), + loginButtonLabel: [registrationData?.loginButtonLabel ? registrationData.loginButtonLabel : null, Validators.required], + loginButtonIcon: [registrationData?.loginButtonIcon ? registrationData.loginButtonIcon : null], + clientId: [registrationData?.clientId ? registrationData.clientId : '', Validators.required], + clientSecret: [registrationData?.clientSecret ? registrationData.clientSecret : '', Validators.required], + accessTokenUri: [registrationData?.accessTokenUri ? registrationData.accessTokenUri : '', + [Validators.required, + Validators.pattern(this.URL_REGEXP)]], + authorizationUri: [registrationData?.authorizationUri ? registrationData.authorizationUri : '', + [Validators.required, + Validators.pattern(this.URL_REGEXP)]], + scope: this.fb.array(registrationData?.scope ? registrationData.scope : [], Validators.required), + jwkSetUri: [registrationData?.jwkSetUri ? registrationData.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], + userInfoUri: [registrationData?.userInfoUri ? registrationData.userInfoUri : '', + [Validators.required, + Validators.pattern(this.URL_REGEXP)]], + clientAuthenticationMethod: [ + registrationData?.clientAuthenticationMethod ? registrationData.clientAuthenticationMethod : ClientAuthenticationMethod.POST, + Validators.required], + userNameAttributeName: [ + registrationData?.userNameAttributeName ? registrationData.userNameAttributeName : 'email', Validators.required], + mapperConfig: this.fb.group({ + allowUserCreation: [registrationData?.mapperConfig?.allowUserCreation ? registrationData.mapperConfig.allowUserCreation : true], + activateUser: [registrationData?.mapperConfig?.activateUser ? registrationData.mapperConfig.activateUser : false], + type: [registrationData?.mapperConfig?.type ? registrationData.mapperConfig.type : MapperConfigType.BASIC, Validators.required] + } + ) + }); + + if (registrationData) { + this.changeMapperConfigType(clientRegistration, registrationData.mapperConfig.type, registrationData.mapperConfig); + } else { + this.changeMapperConfigType(clientRegistration, MapperConfigType.BASIC); + this.setProviderDefaultValue(defaultProviderName, clientRegistration); + } + + this.subscriptions.push(clientRegistration.get('mapperConfig.type').valueChanges.subscribe((value) => { + this.changeMapperConfigType(clientRegistration, value); + })); + + this.subscriptions.push(clientRegistration.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { + (clientRegistration.get('scope') as FormArray).clear(); + this.setProviderDefaultValue(provider, clientRegistration); + })); + + return clientRegistration; + } + + private setProviderDefaultValue(provider: string, clientRegistration: FormGroup) { + if (provider === 'Custom') { + const defaultSettings = {...this.defaultProvider, ...{id: clientRegistration.get('id').value}}; + clientRegistration.reset(defaultSettings, {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); + delete template.id; + delete template.additionalInfo; + template.clientId = ''; + template.clientSecret = ''; + template.scope.forEach(() => { + (clientRegistration.get('scope') as FormArray).push(this.fb.control('')); + }); + clientRegistration.get('accessTokenUri').disable(); + clientRegistration.get('authorizationUri').disable(); + clientRegistration.get('jwkSetUri').disable(); + clientRegistration.get('userInfoUri').disable(); + clientRegistration.patchValue(template, {emitEvent: false}); + } + } + + private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) { + const mapperConfig = control.get('mapperConfig') as FormGroup; + if (type === MapperConfigType.BASIC) { + mapperConfig.removeControl('custom'); + mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); + } else { + mapperConfig.removeControl('basic'); + mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); + } + } + + save(): void { + const setting = this.prepareFormValue(this.oauth2SettingsForm.getRawValue()); + this.adminService.saveOAuth2Settings(setting).subscribe( + (oauth2Settings) => { + this.oauth2Settings = oauth2Settings; + this.oauth2SettingsForm.markAsPristine(); + this.oauth2SettingsForm.markAsUntouched(); + } + ); + } + + private prepareFormValue(formValue: OAuth2Settings): OAuth2Settings{ + formValue.domainsParams.forEach((setting, index) => { + setting.clientRegistrations.forEach((registration) => { + registration.additionalInfo = JSON.stringify(registration.additionalInfo); + if (registration.id.id === null) { + delete registration.id; + } + }); + }); + return formValue; + } + + confirmForm(): FormGroup { + return this.oauth2SettingsForm; + } + + addScope(event: MatChipInputEvent, control: AbstractControl): void { + const input = event.input; + const value = event.value; + const controller = control.get('scope') as FormArray; + 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 FormArray; + controller.removeAt(i); + controller.markAsTouched(); + controller.markAsDirty(); + } + + addDomain(): void { + this.domainsParams.push(this.buildDomainsForm()); + } + + deleteDomain($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + + const domainName = this.domainListTittle(this.domainsParams.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.domainsParams.removeAt(index); + this.domainsParams.markAsTouched(); + this.domainsParams.markAsDirty(); + } + }); + } + + clientDomainProviders(control: AbstractControl): FormArray { + return control.get('clientRegistrations') as FormArray; + } + + clientDomainInfos(control: AbstractControl): FormArray { + return control.get('domainInfos') as FormArray; + } + + addProvider(control: AbstractControl): void { + this.clientDomainProviders(control).push(this.buildProviderForm()); + } + + deleteProvider($event: Event, control: AbstractControl, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + + const providerName = this.clientDomainProviders(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.clientDomainProviders(control).removeAt(index); + this.clientDomainProviders(control).markAsTouched(); + this.clientDomainProviders(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; + } + + 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.clientDomainInfos(control).push(this.buildDomainForm({ + name: '', + scheme: DomainSchema.HTTPS + })); + } + + removeDomain($event: Event, control: AbstractControl, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.clientDomainInfos(control).removeAt(index); + this.clientDomainInfos(control).markAsTouched(); + this.clientDomainInfos(control).markAsDirty(); + } + + redirectURI(control: AbstractControl, schema?: DomainSchema): string { + const domainInfo = control.value as DomainInfo; + 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}/login/oauth2/code/`; + } + return ''; + } + + redirectURIMixed(control: AbstractControl): string { + return this.redirectURI(control, DomainSchema.HTTP); + } + + trackByParams(index: number): number { + return index; + } +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index cff9b26c2b..71dcb16a37 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -60,6 +60,7 @@ export const HelpLinks = { linksMap: { outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings', securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', + oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/', ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node', ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node', @@ -123,7 +124,7 @@ export const HelpLinks = { widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest', widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc', widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', - widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static' + widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', } }; diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index f71ac1bb2e..f31dc5ebe8 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -14,6 +14,9 @@ /// limitations under the License. /// +import { EntityId } from '@shared/models/id/entity-id'; +import { TenantId } from '@shared/models/id/tenant-id'; + export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; export interface AdminSettings { @@ -60,3 +63,99 @@ export interface UpdateMessage { message: string; updateAvailable: boolean; } + +export interface OAuth2Settings { + enabled: boolean; + domainsParams: DomainsParam[]; +} + +export interface DomainsParam { + clientRegistrations: ClientRegistration[]; + domainInfos: DomainInfo[]; +} + +export interface DomainInfo { + name: string; + scheme: DomainSchema; +} + +export enum DomainSchema{ + HTTP = 'HTTP', + HTTPS = 'HTTPS', + MIXED = 'MIXED' +} + +export const domainSchemaTranslations = new Map( + [ + [DomainSchema.HTTP, 'admin.oauth2.domain-schema-http'], + [DomainSchema.HTTPS, 'admin.oauth2.domain-schema-https'], + [DomainSchema.MIXED, 'admin.oauth2.domain-schema-mixed'] + ] +); + +export enum MapperConfigType{ + BASIC = 'BASIC', + CUSTOM = 'CUSTOM' +} + +export enum TenantNameStrategy{ + DOMAIN = 'DOMAIN', + EMAIL = 'EMAIL', + CUSTOM = 'CUSTOM' +} + +export interface ClientProviderTemplated extends ClientRegistration{ + comment: string; + createdTime: number; + helpLink: string; + name: string; + providerId: string; + tenantId: TenantId; +} + +export interface ClientRegistration { + loginButtonLabel: string; + loginButtonIcon: string; + clientId: string; + clientSecret: string; + accessTokenUri: string; + authorizationUri: string; + scope: string[]; + jwkSetUri?: string; + userInfoUri: string; + clientAuthenticationMethod: ClientAuthenticationMethod; + userNameAttributeName: string; + mapperConfig: MapperConfig; + id?: EntityId; + additionalInfo: string; +} + +export enum ClientAuthenticationMethod { + BASIC = 'BASIC', + POST = 'POST' +} + +export interface MapperConfig { + allowUserCreation: boolean; + activateUser: boolean; + type: MapperConfigType; + basic?: MapperConfigBasic; + custom?: MapperConfigCustom; +} + +export interface MapperConfigBasic { + emailAttributeKey: string; + firstNameAttributeKey?: string; + lastNameAttributeKey?: string; + tenantNameStrategy: TenantNameStrategy; + tenantNamePattern?: string; + customerNamePattern?: string; + defaultDashboardName?: string; + alwaysFullScreen?: boolean; +} + +export interface MapperConfigCustom { + url: string; + username?: string; + password?: string; +} 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 c7f3dbfbd1..8f89fa8f39 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -120,8 +120,74 @@ "general-policy": "General policy", "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked", "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", - "user-lockout-notification-email": "In case user account lockout, send notification to email" - }, + "user-lockout-notification-email": "In case user account lockout, send notification to email", + "domain-name": "Domain name", + "domain-name-unique": "Domain name and protocol need to unique.", + "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", + "oauth2": { + "access-token-uri": "Access token URI", + "access-token-uri-required": "Access token URI is required.", + "activate-user": "Activate user", + "add-domain": "Add domain", + "delete-domain": "Delete domain", + "add-provider": "Add provider", + "delete-provider": "Delete provider", + "allow-user-creation": "Allow user creation", + "always-fullscreen": "Always fullscreen", + "authorization-uri": "Authorization URI", + "authorization-uri-required": "Authorization URI is required.", + "client-authentication-method": "Client authentication method", + "client-id": "Client ID", + "client-id-required": "Client ID is required.", + "client-secret": "Client secret", + "client-secret-required": "Client secret is required.", + "custom-setting": "Custom settings", + "customer-name-pattern": "Customer name pattern", + "default-dashboard-name": "Default dashboard name", + "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", + "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", + "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", + "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", + "email-attribute-key": "Email attribute key", + "email-attribute-key-required": "Email attribute key is required.", + "first-name-attribute-key": "First name attribute key", + "general": "General", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "Last name attribute key", + "login-button-icon": "Login button icon", + "login-button-label": "Provider label", + "login-button-label-1": "Login with $(Provider label)", + "login-button-label-required": "Label is required.", + "login-provider": "Login provider", + "mapper": "Mapper", + "new-domain": "New domain", + "oauth2": "OAuth2", + "redirect-uri-template": "Redirect URI template", + "copy-redirect-uri": "Copy redirect URI", + "registration-id": "Registration ID", + "registration-id-required": "Registration ID is required.", + "registration-id-unique": "Registration ID need to unique for the system.", + "scope": "Scope", + "scope-required": "Scope is required.", + "tenant-name-pattern": "Tenant name pattern", + "tenant-name-pattern-required": "Tenant name pattern is required.", + "tenant-name-strategy": "Tenant name strategy", + "type": "Mapper type", + "uri-pattern-error": "Invalid URI format.", + "url": "URL", + "url-pattern": "Invalid URL format.", + "url-required": "URL is required.", + "user-info-uri": "User info URI", + "user-info-uri-required": "User info URI is required.", + "user-name-attribute-name": "User name attribute key", + "user-name-attribute-name-required": "User name attribute key is required", + "protocol": "Protocol", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Enable OAuth2 settings" + } + }, "alarm": { "alarm": "Alarm", "alarms": "Alarms",