Browse Source

Added GITHUB mapper type

pull/3557/head
vzikratyi 6 years ago
parent
commit
ac055397b0
  1. 5
      application/src/main/data/json/system/oauth2_config_templates/github_config.json
  2. 1
      application/src/main/data/upgrade/3.1.1/schema_update_before.sql
  3. 78
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java
  4. 53
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  5. 91
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
  7. 2
      application/src/main/resources/thingsboard.yml
  8. 2
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java
  9. 2
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java
  10. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java
  11. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java
  12. 4
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java
  13. 16
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java
  14. 1
      dao/src/main/resources/sql/schema-entities-hsql.sql
  15. 1
      dao/src/main/resources/sql/schema-entities.sql
  16. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java

5
application/src/main/data/json/system/oauth2_config_templates/github_config.json

@ -7,7 +7,10 @@
"userInfoUri": "https://api.github.com/user",
"clientAuthenticationMethod": "BASIC",
"userNameAttributeName": "login",
"basic": {},
"basic": {
"lastNameAttributeKey": "name",
"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 <a href=\"https://docs.github.com/en/rest/reference/users#list-email-addresses-for-the-authenticated-user\">Github Documentation</a>",
"loginButtonIcon": "mdi:github",
"loginButtonLabel": "Github",

1
application/src/main/data/upgrade/3.1.1/schema_update_before.sql

@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
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),

78
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<String, Object> 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<String, Object> 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<String, Object> 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;
}
}

53
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java

@ -16,10 +16,8 @@
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.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -30,59 +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 = "}";
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
OAuth2User oauth2User = new OAuth2User();
Map<String, Object> 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<String, Object> attributes, OAuth2MapperConfig config) {
switch (config.getBasic().getTenantNameStrategy()) {
case EMAIL:
return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
case DOMAIN:
String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
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!");
}
}
private String getStringAttributeByKey(Map<String, Object> 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;
}
}

91
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<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
Map<String, Object> 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<String> 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<GithubEmailResponse> {}
@Data
@ToString
private static class GithubEmailResponse {
private String email;
private boolean verified;
private boolean primary;
private String visibility;
}
}

6
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java

@ -33,12 +33,18 @@ public class OAuth2ClientMapperProvider {
@Qualifier("customOAuth2ClientMapper")
private OAuth2ClientMapper customOAuth2ClientMapper;
@Autowired
@Qualifier("githubOAuth2ClientMapper")
private OAuth2ClientMapper githubOAuth2ClientMapper;
public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) {
switch (oauth2MapperType) {
case CUSTOM:
return customOAuth2ClientMapper;
case BASIC:
return basicOAuth2ClientMapper;
case GITHUB:
return githubOAuth2ClientMapper;
default:
throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!");
}

2
application/src/main/resources/thingsboard.yml

@ -115,6 +115,8 @@ security:
oauth2:
# Redirect URL where access code from external user management system will be processed
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
githubMapper:
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
# Dashboard parameters
dashboard:

2
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.data.oauth2;
public enum MapperType {
BASIC, CUSTOM;
BASIC, CUSTOM, GITHUB;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java

@ -34,6 +34,7 @@ import java.util.List;
public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo<OAuth2ClientRegistrationTemplateId> implements HasName {
private String providerId;
private MapperType mapperType;
private OAuth2BasicMapperConfig basic;
private String authorizationUri;
private String accessTokenUri;
@ -50,6 +51,7 @@ public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditio
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;

2
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java

@ -190,7 +190,7 @@ public abstract class AbstractOAuth2ClientRegistrationInfoEntity<T extends OAuth
.activateUser(activateUser)
.type(type)
.basic(
type == MapperType.BASIC ?
(type == MapperType.BASIC || type == MapperType.GITHUB) ?
OAuth2BasicMapperConfig.builder()
.emailAttributeKey(emailAttributeKey)
.firstNameAttributeKey(firstNameAttributeKey)

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java

@ -55,6 +55,9 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
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)
@ -106,6 +109,7 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
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();
@ -126,6 +130,7 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity<OAuth2
clientRegistrationTemplate.setCreatedTime(createdTime);
clientRegistrationTemplate.setAdditionalInfo(additionalInfo);
clientRegistrationTemplate.setMapperType(type);
clientRegistrationTemplate.setProviderId(providerId);
clientRegistrationTemplate.setBasic(
OAuth2BasicMapperConfig.builder()

4
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java

@ -20,10 +20,12 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "security.oauth2")
@Data
@Slf4j
public class OAuth2Configuration {
private String loginProcessingUrl;
private Map<String, String> githubMapper;
}

16
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java

@ -184,6 +184,22 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
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) {

1
dao/src/main/resources/sql/schema-entities-hsql.sql

@ -386,6 +386,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
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),

1
dao/src/main/resources/sql/schema-entities.sql

@ -412,6 +412,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
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),

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java

@ -21,6 +21,7 @@ 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;
@ -105,6 +106,7 @@ public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
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")

Loading…
Cancel
Save