Commit ac055397b01341838a14754efeec46f6d63a71a0

Authored by vzikratyi
1 parent e2791a28

Added GITHUB mapper type

... ... @@ -7,7 +7,10 @@
7 7 "userInfoUri": "https://api.github.com/user",
8 8 "clientAuthenticationMethod": "BASIC",
9 9 "userNameAttributeName": "login",
10   - "basic": {},
  10 + "basic": {
  11 + "lastNameAttributeKey": "name",
  12 + "tenantNameStrategy": "DOMAIN"
  13 + },
11 14 "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 <a href=\"https://docs.github.com/en/rest/reference/users#list-email-addresses-for-the-authenticated-user\">Github Documentation</a>",
12 15 "loginButtonIcon": "mdi:github",
13 16 "loginButtonLabel": "Github",
... ...
... ... @@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
67 67 user_name_attribute_name varchar(255),
68 68 jwk_set_uri varchar(255),
69 69 client_authentication_method varchar(255),
  70 + type varchar(31),
70 71 basic_email_attribute_key varchar(31),
71 72 basic_first_name_attribute_key varchar(31),
72 73 basic_last_name_attribute_key varchar(31),
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.apache.commons.lang3.text.StrSubstitutor;
  20 +import org.springframework.util.StringUtils;
  21 +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
  22 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  23 +
  24 +import java.util.Map;
  25 +
  26 +@Slf4j
  27 +public class BasicMapperUtils {
  28 + private static final String START_PLACEHOLDER_PREFIX = "%{";
  29 + private static final String END_PLACEHOLDER_PREFIX = "}";
  30 +
  31 + public static OAuth2User getOAuth2User(String email, Map<String, Object> attributes, OAuth2MapperConfig config) {
  32 + OAuth2User oauth2User = new OAuth2User();
  33 + oauth2User.setEmail(email);
  34 + oauth2User.setTenantName(getTenantName(email, attributes, config));
  35 + if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
  36 + String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
  37 + oauth2User.setLastName(lastName);
  38 + }
  39 + if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
  40 + String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
  41 + oauth2User.setFirstName(firstName);
  42 + }
  43 + if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) {
  44 + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
  45 + String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
  46 + oauth2User.setCustomerName(customerName);
  47 + }
  48 + oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen());
  49 + if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) {
  50 + oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName());
  51 + }
  52 + return oauth2User;
  53 + }
  54 +
  55 + public static String getTenantName(String email, Map<String, Object> attributes, OAuth2MapperConfig config) {
  56 + switch (config.getBasic().getTenantNameStrategy()) {
  57 + case EMAIL:
  58 + return email;
  59 + case DOMAIN:
  60 + return email.substring(email .indexOf("@") + 1);
  61 + case CUSTOM:
  62 + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
  63 + return sub.replace(config.getBasic().getTenantNamePattern());
  64 + default:
  65 + throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
  66 + }
  67 + }
  68 +
  69 + public static String getStringAttributeByKey(Map<String, Object> attributes, String key) {
  70 + String result = null;
  71 + try {
  72 + result = (String) attributes.get(key);
  73 + } catch (Exception e) {
  74 + log.warn("Can't convert attribute to String by key " + key);
  75 + }
  76 + return result;
  77 + }
  78 +}
... ...
... ... @@ -16,10 +16,8 @@
16 16 package org.thingsboard.server.service.security.auth.oauth2;
17 17
18 18 import lombok.extern.slf4j.Slf4j;
19   -import org.apache.commons.lang3.text.StrSubstitutor;
20 19 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
21 20 import org.springframework.stereotype.Service;
22   -import org.springframework.util.StringUtils;
23 21 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
24 22 import org.thingsboard.server.dao.oauth2.OAuth2User;
25 23 import org.thingsboard.server.service.security.model.SecurityUser;
... ... @@ -30,59 +28,12 @@ import java.util.Map;
30 28 @Slf4j
31 29 public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
32 30
33   - private static final String START_PLACEHOLDER_PREFIX = "%{";
34   - private static final String END_PLACEHOLDER_PREFIX = "}";
35   -
36 31 @Override
37 32 public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
38   - OAuth2User oauth2User = new OAuth2User();
39 33 Map<String, Object> attributes = token.getPrincipal().getAttributes();
40   - String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
41   - oauth2User.setEmail(email);
42   - oauth2User.setTenantName(getTenantName(attributes, config));
43   - if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
44   - String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
45   - oauth2User.setLastName(lastName);
46   - }
47   - if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
48   - String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
49   - oauth2User.setFirstName(firstName);
50   - }
51   - if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) {
52   - StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
53   - String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
54   - oauth2User.setCustomerName(customerName);
55   - }
56   - oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen());
57   - if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) {
58   - oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName());
59   - }
  34 + String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  35 + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
60 36
61 37 return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
62 38 }
63   -
64   - private String getTenantName(Map<String, Object> attributes, OAuth2MapperConfig config) {
65   - switch (config.getBasic().getTenantNameStrategy()) {
66   - case EMAIL:
67   - return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
68   - case DOMAIN:
69   - String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
70   - return email.substring(email .indexOf("@") + 1);
71   - case CUSTOM:
72   - StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
73   - return sub.replace(config.getBasic().getTenantNamePattern());
74   - default:
75   - throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
76   - }
77   - }
78   -
79   - private String getStringAttributeByKey(Map<String, Object> attributes, String key) {
80   - String result = null;
81   - try {
82   - result = (String) attributes.get(key);
83   - } catch (Exception e) {
84   - log.warn("Can't convert attribute to String by key " + key);
85   - }
86   - return result;
87   - }
88 39 }
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.Data;
  19 +import lombok.ToString;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +import org.springframework.beans.factory.annotation.Autowired;
  22 +import org.springframework.boot.web.client.RestTemplateBuilder;
  23 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  24 +import org.springframework.stereotype.Service;
  25 +import org.springframework.web.client.RestTemplate;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
  27 +import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
  28 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  29 +import org.thingsboard.server.service.security.model.SecurityUser;
  30 +
  31 +import java.util.ArrayList;
  32 +import java.util.Map;
  33 +import java.util.Optional;
  34 +
  35 +@Service(value = "githubOAuth2ClientMapper")
  36 +@Slf4j
  37 +public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
  38 + private static final String EMAIL_URL_KEY = "emailUrl";
  39 +
  40 + private static final String AUTHORIZATION = "Authorization";
  41 +
  42 + private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
  43 +
  44 + @Autowired
  45 + private OAuth2Configuration oAuth2Configuration;
  46 +
  47 + @Override
  48 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
  49 + Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
  50 + String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
  51 + Map<String, Object> attributes = token.getPrincipal().getAttributes();
  52 + OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
  53 + return getOrCreateSecurityUserFromOAuth2User(oAuth2User, config.isAllowUserCreation(), config.isActivateUser());
  54 + }
  55 +
  56 + private synchronized String getEmail(String emailUrl, String oauth2Token) {
  57 + restTemplateBuilder = restTemplateBuilder.defaultHeader(AUTHORIZATION, "token " + oauth2Token);
  58 +
  59 + RestTemplate restTemplate = restTemplateBuilder.build();
  60 + GithubEmailsResponse githubEmailsResponse;
  61 + try {
  62 + githubEmailsResponse = restTemplate.getForEntity(emailUrl, GithubEmailsResponse.class).getBody();
  63 + if (githubEmailsResponse == null){
  64 + throw new RuntimeException("Empty Github response!");
  65 + }
  66 + } catch (Exception e) {
  67 + log.error("There was an error during connection to Github API", e);
  68 + throw new RuntimeException("Unable to login. Please contact your Administrator!");
  69 + }
  70 + Optional<String> emailOpt = githubEmailsResponse.stream()
  71 + .filter(GithubEmailResponse::isPrimary)
  72 + .map(GithubEmailResponse::getEmail)
  73 + .findAny();
  74 + if (emailOpt.isPresent()){
  75 + return emailOpt.get();
  76 + } else {
  77 + log.error("Could not find primary email from {}.", githubEmailsResponse);
  78 + throw new RuntimeException("Unable to login. Please contact your Administrator!");
  79 + }
  80 + }
  81 + private static class GithubEmailsResponse extends ArrayList<GithubEmailResponse> {}
  82 +
  83 + @Data
  84 + @ToString
  85 + private static class GithubEmailResponse {
  86 + private String email;
  87 + private boolean verified;
  88 + private boolean primary;
  89 + private String visibility;
  90 + }
  91 +}
... ...
... ... @@ -33,12 +33,18 @@ public class OAuth2ClientMapperProvider {
33 33 @Qualifier("customOAuth2ClientMapper")
34 34 private OAuth2ClientMapper customOAuth2ClientMapper;
35 35
  36 + @Autowired
  37 + @Qualifier("githubOAuth2ClientMapper")
  38 + private OAuth2ClientMapper githubOAuth2ClientMapper;
  39 +
36 40 public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) {
37 41 switch (oauth2MapperType) {
38 42 case CUSTOM:
39 43 return customOAuth2ClientMapper;
40 44 case BASIC:
41 45 return basicOAuth2ClientMapper;
  46 + case GITHUB:
  47 + return githubOAuth2ClientMapper;
42 48 default:
43 49 throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!");
44 50 }
... ...
... ... @@ -115,6 +115,8 @@ security:
115 115 oauth2:
116 116 # Redirect URL where access code from external user management system will be processed
117 117 loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
  118 + githubMapper:
  119 + emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
118 120
119 121 # Dashboard parameters
120 122 dashboard:
... ...
... ... @@ -16,5 +16,5 @@
16 16 package org.thingsboard.server.common.data.oauth2;
17 17
18 18 public enum MapperType {
19   - BASIC, CUSTOM;
  19 + BASIC, CUSTOM, GITHUB;
20 20 }
... ...
... ... @@ -34,6 +34,7 @@ import java.util.List;
34 34 public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo<OAuth2ClientRegistrationTemplateId> implements HasName {
35 35
36 36 private String providerId;
  37 + private MapperType mapperType;
37 38 private OAuth2BasicMapperConfig basic;
38 39 private String authorizationUri;
39 40 private String accessTokenUri;
... ... @@ -50,6 +51,7 @@ public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditio
50 51 public OAuth2ClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) {
51 52 super(clientRegistrationTemplate);
52 53 this.providerId = clientRegistrationTemplate.providerId;
  54 + this.mapperType = clientRegistrationTemplate.mapperType;
53 55 this.basic = clientRegistrationTemplate.basic;
54 56 this.authorizationUri = clientRegistrationTemplate.authorizationUri;
55 57 this.accessTokenUri = clientRegistrationTemplate.accessTokenUri;
... ...
... ... @@ -190,7 +190,7 @@ public abstract class AbstractOAuth2ClientRegistrationInfoEntity<T extends OAuth
190 190 .activateUser(activateUser)
191 191 .type(type)
192 192 .basic(
193   - type == MapperType.BASIC ?
  193 + (type == MapperType.BASIC || type == MapperType.GITHUB) ?
194 194 OAuth2BasicMapperConfig.builder()
195 195 .emailAttributeKey(emailAttributeKey)
196 196 .firstNameAttributeKey(firstNameAttributeKey)
... ...
... ... @@ -55,6 +55,9 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
55 55 private String jwkSetUri;
56 56 @Column(name = ModelConstants.OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY)
57 57 private String clientAuthenticationMethod;
  58 + @Enumerated(EnumType.STRING)
  59 + @Column(name = ModelConstants.OAUTH2_MAPPER_TYPE_PROPERTY)
  60 + private MapperType type;
58 61 @Column(name = ModelConstants.OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY)
59 62 private String emailAttributeKey;
60 63 @Column(name = ModelConstants.OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY)
... ... @@ -106,6 +109,7 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
106 109 this.loginButtonLabel = clientRegistrationTemplate.getLoginButtonLabel();
107 110 this.helpLink = clientRegistrationTemplate.getHelpLink();
108 111 this.additionalInfo = clientRegistrationTemplate.getAdditionalInfo();
  112 + this.type = clientRegistrationTemplate.getMapperType();
109 113 OAuth2BasicMapperConfig basicConfig = clientRegistrationTemplate.getBasic();
110 114 if (basicConfig != null) {
111 115 this.emailAttributeKey = basicConfig.getEmailAttributeKey();
... ... @@ -126,6 +130,7 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
126 130 clientRegistrationTemplate.setCreatedTime(createdTime);
127 131 clientRegistrationTemplate.setAdditionalInfo(additionalInfo);
128 132
  133 + clientRegistrationTemplate.setMapperType(type);
129 134 clientRegistrationTemplate.setProviderId(providerId);
130 135 clientRegistrationTemplate.setBasic(
131 136 OAuth2BasicMapperConfig.builder()
... ...
... ... @@ -20,10 +20,12 @@ import lombok.extern.slf4j.Slf4j;
20 20 import org.springframework.boot.context.properties.ConfigurationProperties;
21 21 import org.springframework.context.annotation.Configuration;
22 22
  23 +import java.util.Map;
  24 +
23 25 @Configuration
24 26 @ConfigurationProperties(prefix = "security.oauth2")
25 27 @Data
26   -@Slf4j
27 28 public class OAuth2Configuration {
28 29 private String loginProcessingUrl;
  30 + private Map<String, String> githubMapper;
29 31 }
... ...
... ... @@ -184,6 +184,22 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
184 184 throw new DataValidationException("Tenant name pattern should be specified!");
185 185 }
186 186 }
  187 + if (mapperConfig.getType() == MapperType.GITHUB) {
  188 + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic();
  189 + if (basicConfig == null) {
  190 + throw new DataValidationException("Basic config should be specified!");
  191 + }
  192 + if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) {
  193 + throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!");
  194 + }
  195 + if (basicConfig.getTenantNameStrategy() == null) {
  196 + throw new DataValidationException("Tenant name strategy should be specified!");
  197 + }
  198 + if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM
  199 + && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) {
  200 + throw new DataValidationException("Tenant name pattern should be specified!");
  201 + }
  202 + }
187 203 if (mapperConfig.getType() == MapperType.CUSTOM) {
188 204 OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom();
189 205 if (customConfig == null) {
... ...
... ... @@ -386,6 +386,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
386 386 user_name_attribute_name varchar(255),
387 387 jwk_set_uri varchar(255),
388 388 client_authentication_method varchar(255),
  389 + type varchar(31),
389 390 basic_email_attribute_key varchar(31),
390 391 basic_first_name_attribute_key varchar(31),
391 392 basic_last_name_attribute_key varchar(31),
... ...
... ... @@ -412,6 +412,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
412 412 user_name_attribute_name varchar(255),
413 413 jwk_set_uri varchar(255),
414 414 client_authentication_method varchar(255),
  415 + type varchar(31),
415 416 basic_email_attribute_key varchar(31),
416 417 basic_first_name_attribute_key varchar(31),
417 418 basic_last_name_attribute_key varchar(31),
... ...
... ... @@ -21,6 +21,7 @@ import org.junit.Before;
21 21 import org.junit.Test;
22 22 import org.springframework.beans.factory.annotation.Autowired;
23 23 import org.thingsboard.server.common.data.id.TenantId;
  24 +import org.thingsboard.server.common.data.oauth2.MapperType;
24 25 import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig;
25 26 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
26 27 import org.thingsboard.server.dao.exception.DataValidationException;
... ... @@ -105,6 +106,7 @@ public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
105 106 OAuth2ClientRegistrationTemplate clientRegistrationTemplate = new OAuth2ClientRegistrationTemplate();
106 107 clientRegistrationTemplate.setProviderId(providerId);
107 108 clientRegistrationTemplate.setAdditionalInfo(mapper.createObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString()));
  109 + clientRegistrationTemplate.setMapperType(MapperType.BASIC);
108 110 clientRegistrationTemplate.setBasic(
109 111 OAuth2BasicMapperConfig.builder()
110 112 .firstNameAttributeKey("firstName")
... ...