io.gravitee.rest.api.service.impl.UserServiceImpl Maven / Gradle / Ivy
/**
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* 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 io.gravitee.rest.api.service.impl;
import static io.gravitee.repository.management.model.Audit.AuditProperties.USER;
import static io.gravitee.rest.api.model.permissions.RolePermissionAction.UPDATE;
import static io.gravitee.rest.api.service.common.JWTHelper.ACTION.*;
import static io.gravitee.rest.api.service.common.JWTHelper.DefaultValues.DEFAULT_JWT_EMAIL_REGISTRATION_EXPIRE_AFTER;
import static io.gravitee.rest.api.service.common.JWTHelper.DefaultValues.DEFAULT_JWT_ISSUER;
import static io.gravitee.rest.api.service.notification.NotificationParamsBuilder.*;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.isBlank;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.ReadContext;
import io.gravitee.common.data.domain.MetadataPage;
import io.gravitee.common.data.domain.Page;
import io.gravitee.common.util.Maps;
import io.gravitee.el.TemplateEngine;
import io.gravitee.el.spel.function.JsonPathFunction;
import io.gravitee.repository.exceptions.TechnicalException;
import io.gravitee.repository.management.api.UserRepository;
import io.gravitee.repository.management.api.search.UserCriteria;
import io.gravitee.repository.management.api.search.builder.PageableBuilder;
import io.gravitee.repository.management.model.User;
import io.gravitee.repository.management.model.UserStatus;
import io.gravitee.rest.api.model.*;
import io.gravitee.rest.api.model.application.ApplicationSettings;
import io.gravitee.rest.api.model.application.SimpleApplicationSettings;
import io.gravitee.rest.api.model.audit.AuditEntity;
import io.gravitee.rest.api.model.audit.AuditQuery;
import io.gravitee.rest.api.model.common.Pageable;
import io.gravitee.rest.api.model.configuration.identity.GroupMappingEntity;
import io.gravitee.rest.api.model.configuration.identity.RoleMappingEntity;
import io.gravitee.rest.api.model.configuration.identity.SocialIdentityProviderEntity;
import io.gravitee.rest.api.model.parameters.Key;
import io.gravitee.rest.api.model.parameters.ParameterReferenceType;
import io.gravitee.rest.api.model.permissions.RolePermission;
import io.gravitee.rest.api.model.permissions.RolePermissionAction;
import io.gravitee.rest.api.model.permissions.RoleScope;
import io.gravitee.rest.api.model.permissions.SystemRole;
import io.gravitee.rest.api.service.*;
import io.gravitee.rest.api.service.builder.EmailNotificationBuilder;
import io.gravitee.rest.api.service.common.GraviteeContext;
import io.gravitee.rest.api.service.common.JWTHelper.ACTION;
import io.gravitee.rest.api.service.common.JWTHelper.Claims;
import io.gravitee.rest.api.service.common.RandomString;
import io.gravitee.rest.api.service.configuration.identity.IdentityProviderService;
import io.gravitee.rest.api.service.exceptions.*;
import io.gravitee.rest.api.service.impl.search.SearchResult;
import io.gravitee.rest.api.service.notification.NotificationParamsBuilder;
import io.gravitee.rest.api.service.notification.PortalHook;
import io.gravitee.rest.api.service.sanitizer.UrlSanitizerUtils;
import io.gravitee.rest.api.service.search.SearchEngineService;
import io.gravitee.rest.api.service.search.query.Query;
import io.gravitee.rest.api.service.search.query.QueryBuilder;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @author David BRASSELY (david.brassely at graviteesource.com)
* @author Nicolas GERAUD (nicolas.geraud at graviteesource.com)
* @author Azize Elamrani (azize.elamrani at graviteesource.com)
* @author GraviteeSource Team
*/
@Component
public class UserServiceImpl extends AbstractService implements UserService, InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
/** A default source used for user registration.*/
private static final String IDP_SOURCE_GRAVITEE = "gravitee";
private static final String TEMPLATE_ENGINE_PROFILE_ATTRIBUTE = "profile";
// Dirty hack: only used to force class loading
static {
try {
LOGGER.trace(
"Loading class to initialize properly JsonPath Cache provider: {}",
Class.forName(JsonPathFunction.class.getName())
);
} catch (ClassNotFoundException ignored) {
LOGGER.trace("Loading class to initialize properly JsonPath Cache provider : fail");
}
}
@Autowired
private UserRepository userRepository;
@Autowired
private ConfigurableEnvironment environment;
@Autowired
private EmailService emailService;
@Autowired
private ApplicationService applicationService;
@Autowired
private RoleService roleService;
@Autowired
private MembershipService membershipService;
@Autowired
private PermissionService permissionService;
@Autowired
private AuditService auditService;
@Autowired
private NotifierService notifierService;
@Autowired
private ApiService apiService;
@Autowired
private ParameterService parameterService;
@Autowired
private SearchEngineService searchEngineService;
@Autowired
private InvitationService invitationService;
@Autowired
private PortalNotificationService portalNotificationService;
@Autowired
private PortalNotificationConfigService portalNotificationConfigService;
@Autowired
private GenericNotificationConfigService genericNotificationConfigService;
@Autowired
private GroupService groupService;
@Autowired
private OrganizationService organizationService;
@Autowired
private NewsletterService newsletterService;
@Autowired
private PasswordValidator passwordValidator;
@Autowired
private TokenService tokenService;
@Autowired
private EnvironmentService environmentService;
@Autowired
private IdentityProviderService identityProviderService;
@Autowired
private UserMetadataService userMetadataService;
@Value("${user.login.defaultApplication:true}")
private boolean defaultApplicationForFirstConnection;
@Value("${user.anonymize-on-delete.enabled:false}")
private boolean anonymizeOnDelete;
private List portalWhitelist;
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Override
public void afterPropertiesSet() {
int i = 0;
portalWhitelist = new ArrayList<>();
String whitelistUrl;
while ((whitelistUrl = environment.getProperty("portal.whitelist[" + i + "]")) != null) {
portalWhitelist.add(whitelistUrl);
i++;
}
}
@Override
public UserEntity connect(String userId) {
try {
LOGGER.debug("Connection of {}", userId);
Optional checkUser = userRepository.findById(userId);
if (!checkUser.isPresent()) {
throw new UserNotFoundException(userId);
}
User user = checkUser.get();
User previousUser = new User(user);
// First connection: create default application for user & notify
if (user.getLastConnectionAt() == null && user.getFirstConnectionAt() == null) {
notifierService.trigger(PortalHook.USER_FIRST_LOGIN, new NotificationParamsBuilder().user(convert(user, false)).build());
user.setFirstConnectionAt(new Date());
if (defaultApplicationForFirstConnection) {
LOGGER.debug("Create a default application for {}", userId);
NewApplicationEntity defaultApp = new NewApplicationEntity();
defaultApp.setName("Default application");
defaultApp.setDescription("My default application");
// To preserve backward compatibility, ensure that we have at least default settings for simple application type
ApplicationSettings settings = new ApplicationSettings();
SimpleApplicationSettings simpleAppSettings = new SimpleApplicationSettings();
settings.setApp(simpleAppSettings);
defaultApp.setSettings(settings);
try {
applicationService.create(defaultApp, userId, true);
} catch (IllegalStateException ex) {
//do not fail to create a user even if we are not able to create its default app
}
}
}
// Set date fields
user.setLastConnectionAt(new Date());
if (user.getFirstConnectionAt() == null) {
user.setFirstConnectionAt(user.getLastConnectionAt());
}
user.setUpdatedAt(user.getLastConnectionAt());
user.setLoginCount(user.getLoginCount() + 1);
User updatedUser = userRepository.update(user);
auditService.createOrganizationAuditLog(
Collections.singletonMap(USER, userId),
User.AuditEvent.USER_CONNECTED,
user.getUpdatedAt(),
previousUser,
user
);
final UserEntity userEntity = convert(updatedUser, true);
searchEngineService.index(userEntity, false);
return userEntity;
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to connect {}", userId, ex);
throw new TechnicalManagementException("An error occurs while trying to connect " + userId, ex);
}
}
@Override
public UserEntity findById(String id, boolean defaultValue) {
return GraviteeContext
.getCurrentUsers()
.computeIfAbsent(
id,
k -> {
try {
LOGGER.debug("Find user by ID: {}", k);
Optional optionalUser = userRepository.findById(k);
if (optionalUser.isPresent()) {
return convert(optionalUser.get(), false, userMetadataService.findAllByUserId(k));
}
if (defaultValue) {
UserEntity unknownUser = new UserEntity();
unknownUser.setId(k);
unknownUser.setFirstname("Unknown user");
return unknownUser;
}
//should never happen
throw new UserNotFoundException(k);
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to find user using its ID {}", k, ex);
throw new TechnicalManagementException("An error occurs while trying to find user using its ID " + k, ex);
}
}
);
}
@Override
public Optional findByEmail(String email) {
try {
LOGGER.debug("Find user by Email: {}", email);
Optional optionalUser = userRepository.findByEmail(email, GraviteeContext.getCurrentOrganization());
return optionalUser.map(user -> convert(optionalUser.get(), false));
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to find user using its email", ex);
throw new TechnicalManagementException("An error occurs while trying to find user using its email", ex);
}
}
@Override
public UserEntity findByIdWithRoles(String id) {
try {
LOGGER.debug("Find user by ID: {}", id);
Optional optionalUser = userRepository.findById(id);
if (optionalUser.isPresent()) {
UserEntity userEntity = convert(optionalUser.get(), true, userMetadataService.findAllByUserId(id));
populateUserFlags(Collections.singletonList(userEntity));
return userEntity;
}
//should never happen
throw new UserNotFoundException(id);
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to find user using its ID {}", id, ex);
throw new TechnicalManagementException("An error occurs while trying to find user using its ID " + id, ex);
}
}
@Override
public UserEntity findBySource(String source, String sourceId, boolean loadRoles) {
try {
LOGGER.debug("Find user by source[{}] user[{}]", source, sourceId);
Optional optionalUser = userRepository.findBySource(source, sourceId, GraviteeContext.getCurrentOrganization());
if (optionalUser.isPresent()) {
return convert(optionalUser.get(), loadRoles);
}
throw new UserNotFoundException(sourceId);
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to find user using source[{}], user[{}]", source, sourceId, ex);
throw new TechnicalManagementException("An error occurs while trying to find user using source " + source + ':' + sourceId, ex);
}
}
@Override
public Set findByIds(List ids) {
return this.findByIds(ids, true);
}
@Override
public Set findByIds(List ids, boolean withUserMetadata) {
try {
LOGGER.debug("Find users by ID: {}", ids);
Set users = userRepository.findByIds(ids);
if (!users.isEmpty()) {
return users
.stream()
.map(
u ->
this.convert(
u,
false,
withUserMetadata ? userMetadataService.findAllByUserId(u.getId()) : Collections.emptyList()
)
)
.collect(toSet());
}
Optional idsAsString = ids.stream().reduce((a, b) -> a + '/' + b);
if (idsAsString.isPresent()) {
throw new UserNotFoundException(idsAsString.get());
} else {
throw new UserNotFoundException("?");
}
} catch (TechnicalException ex) {
Optional idsAsString = ids.stream().reduce((a, b) -> a + '/' + b);
LOGGER.error("An error occurs while trying to find users using their ID {}", idsAsString, ex);
throw new TechnicalManagementException("An error occurs while trying to find users using their ID " + idsAsString, ex);
}
}
private void checkUserRegistrationEnabled(GraviteeContext.ReferenceContext currentContext) {
boolean userCreationEnabled;
if (currentContext.getReferenceType().equals(GraviteeContext.ReferenceContextType.ORGANIZATION)) {
userCreationEnabled =
parameterService.findAsBoolean(
Key.CONSOLE_USERCREATION_ENABLED,
currentContext.getReferenceId(),
ParameterReferenceType.ORGANIZATION
);
} else {
userCreationEnabled =
parameterService.findAsBoolean(
Key.PORTAL_USERCREATION_ENABLED,
currentContext.getReferenceId(),
ParameterReferenceType.ENVIRONMENT
);
}
if (!userCreationEnabled) {
throw new UserRegistrationUnavailableException();
}
}
/**
* Allows to complete the creation of a user which is pre-created.
* @param registerUserEntity a valid token and a password
* @return the user
*/
@Override
public UserEntity finalizeRegistration(final RegisterUserEntity registerUserEntity) {
try {
DecodedJWT jwt = getDecodedJWT(registerUserEntity.getToken());
final String action = jwt.getClaim(Claims.ACTION).asString();
if (RESET_PASSWORD.name().equals(action)) {
throw new UserStateConflictException("Reset password forbidden on this resource");
}
if (USER_REGISTRATION.name().equals(action)) {
checkUserRegistrationEnabled(GraviteeContext.getCurrentContext());
} else if (GROUP_INVITATION.name().equals(action)) {
// check invitations
final String email = jwt.getClaim(Claims.EMAIL).asString();
final List invitations = invitationService.findAll();
final List userInvitations = invitations
.stream()
.filter(invitation -> invitation.getEmail().equals(email))
.collect(toList());
if (userInvitations.isEmpty()) {
throw new IllegalStateException("Invitation has been canceled");
}
}
// check password here to avoid user creation if password is invalid
if (registerUserEntity.getPassword() != null) {
if (!passwordValidator.validate(registerUserEntity.getPassword())) {
throw new PasswordFormatInvalidException();
}
}
final Object subject = jwt.getSubject();
User user;
if (subject == null) {
final NewExternalUserEntity externalUser = new NewExternalUserEntity();
final String email = jwt.getClaim(Claims.EMAIL).asString();
externalUser.setSource(IDP_SOURCE_GRAVITEE);
externalUser.setSourceId(email);
externalUser.setFirstname(registerUserEntity.getFirstname());
externalUser.setLastname(registerUserEntity.getLastname());
externalUser.setEmail(email);
user = convert(create(externalUser, true));
user.setOrganizationId(GraviteeContext.getCurrentOrganization());
} else {
final String username = subject.toString();
LOGGER.debug("Create an internal user {}", username);
Optional checkUser = userRepository.findById(username);
user = checkUser.orElseThrow(() -> new UserNotFoundException(username));
if (StringUtils.isNotBlank(user.getPassword())) {
throw new UserAlreadyFinalizedException(GraviteeContext.getCurrentOrganization());
}
}
if (GROUP_INVITATION.name().equals(action)) {
// check invitations
final String email = user.getEmail();
final String userId = user.getId();
final List invitations = invitationService.findAll();
invitations
.stream()
.filter(invitation -> invitation.getEmail().equals(email))
.forEach(
invitation -> {
invitationService.addMember(
invitation.getReferenceType().name(),
invitation.getReferenceId(),
userId,
invitation.getApiRole(),
invitation.getApplicationRole()
);
invitationService.delete(invitation.getId(), invitation.getReferenceId());
}
);
}
// Set date fields
user.setUpdatedAt(new Date());
// Encrypt password if internal user
encryptPassword(user, registerUserEntity.getPassword());
user = userRepository.update(user);
auditService.createOrganizationAuditLog(
Collections.singletonMap(USER, user.getId()),
User.AuditEvent.USER_CREATED,
user.getUpdatedAt(),
null,
user
);
// Do not send back the password
user.setPassword(null);
final UserEntity userEntity = convert(user, true);
searchEngineService.index(userEntity, false);
return userEntity;
} catch (AbstractManagementException ex) {
throw ex;
} catch (Exception ex) {
LOGGER.error("An error occurs while trying to create an internal user with the token {}", registerUserEntity.getToken(), ex);
throw new TechnicalManagementException(ex.getMessage(), ex);
}
}
@Override
public UserEntity finalizeResetPassword(ResetPasswordUserEntity registerUserEntity) {
try {
DecodedJWT jwt = getDecodedJWT(registerUserEntity.getToken());
final String action = jwt.getClaim(Claims.ACTION).asString();
if (!RESET_PASSWORD.name().equals(action)) {
throw new UserStateConflictException("Invalid action on reset password resource");
}
final Object subject = jwt.getSubject();
User user;
if (subject == null) {
throw new UserNotFoundException("Subject missing from JWT token");
} else {
final String username = subject.toString();
LOGGER.debug("Find user {} to update password", username);
Optional checkUser = userRepository.findById(username);
user = checkUser.orElseThrow(() -> new UserNotFoundException(username));
}
// Set date fields
user.setUpdatedAt(new Date());
// Encrypt password if internal user
encryptPassword(user, registerUserEntity.getPassword());
user = userRepository.update(user);
auditService.createOrganizationAuditLog(
Collections.singletonMap(USER, user.getId()),
User.AuditEvent.PASSWORD_CHANGED,
user.getUpdatedAt(),
null,
null
);
// Do not send back the password
user.setPassword(null);
return convert(user, true);
} catch (AbstractManagementException ex) {
throw ex;
} catch (Exception ex) {
LOGGER.error(
"An error occurs while trying to change password of an internal user with the token {}",
registerUserEntity.getToken(),
ex
);
throw new TechnicalManagementException(ex.getMessage(), ex);
}
}
private void encryptPassword(User user, String password) {
if (password != null) {
if (passwordValidator.validate(password)) {
user.setPassword(passwordEncoder.encode(password));
} else {
throw new PasswordFormatInvalidException();
}
}
}
private DecodedJWT getDecodedJWT(String token) {
final String jwtSecret = environment.getProperty("jwt.secret");
if (jwtSecret == null || jwtSecret.isEmpty()) {
throw new IllegalStateException("JWT secret is mandatory");
}
Algorithm algorithm = Algorithm.HMAC256(jwtSecret);
JWTVerifier verifier = JWT.require(algorithm).withIssuer(environment.getProperty("jwt.issuer", DEFAULT_JWT_ISSUER)).build();
return verifier.verify(token);
}
@Override
public PictureEntity getPicture(String id) {
UserEntity user = findById(id);
if (user.getPicture() != null) {
String picture = user.getPicture();
if (picture.matches("^(http|https)://.*$")) {
return new UrlPictureEntity(picture);
} else {
try {
InlinePictureEntity imageEntity = new InlinePictureEntity();
String[] parts = picture.split(";", 2);
imageEntity.setType(parts[0].split(":")[1]);
String base64Content = picture.split(",", 2)[1];
imageEntity.setContent(DatatypeConverter.parseBase64Binary(base64Content));
return imageEntity;
} catch (Exception ex) {
LOGGER.warn("Unable to get user picture for id[{}]", id);
}
}
}
// Return empty image
InlinePictureEntity imageEntity = new InlinePictureEntity();
imageEntity.setType("image/png");
return imageEntity;
}
/**
* Allows to create a user.
* @param newExternalUserEntity
* @return
*/
@Override
public UserEntity create(NewExternalUserEntity newExternalUserEntity, boolean addDefaultRole) {
return create(newExternalUserEntity, addDefaultRole, true);
}
private UserEntity create(NewExternalUserEntity newExternalUserEntity, boolean addDefaultRole, boolean autoRegistrationEnabled) {
try {
/*
TODO: getCurrentEnvironnemenet and call database to fetch the corresponding organization OR add parameters in methods
Because, this method is called by portal and console. And in portal, we don't have a "current organization" in path.
*/
String organizationId = GraviteeContext.getCurrentOrganization();
// First we check that organization exist
this.organizationService.findById(organizationId);
LOGGER.debug("Create an external user {}", newExternalUserEntity);
Optional checkUser = userRepository.findBySource(
newExternalUserEntity.getSource(),
newExternalUserEntity.getSourceId(),
organizationId
);
if (checkUser.isPresent()) {
throw new UserAlreadyExistsException(
newExternalUserEntity.getSource(),
newExternalUserEntity.getSourceId(),
organizationId
);
}
User user = convert(newExternalUserEntity);
user.setId(RandomString.generate());
user.setOrganizationId(organizationId);
user.setStatus(autoRegistrationEnabled ? UserStatus.ACTIVE : UserStatus.PENDING);
// Set date fields
user.setCreatedAt(new Date());
user.setUpdatedAt(user.getCreatedAt());
User createdUser = userRepository.create(user);
auditService.createOrganizationAuditLog(
Collections.singletonMap(USER, user.getId()),
User.AuditEvent.USER_CREATED,
user.getCreatedAt(),
null,
user
);
List metadata = new ArrayList<>();
if (newExternalUserEntity.getCustomFields() != null) {
for (Map.Entry entry : newExternalUserEntity.getCustomFields().entrySet()) {
NewUserMetadataEntity metadataEntity = new NewUserMetadataEntity();
metadataEntity.setName(entry.getKey());
metadataEntity.setUserId(createdUser.getId());
metadataEntity.setFormat(MetadataFormat.STRING);
metadataEntity.setValue(String.valueOf(entry.getValue()));
metadata.add(userMetadataService.create(metadataEntity));
}
}
if (addDefaultRole) {
addDefaultMembership(createdUser);
}
final UserEntity userEntity = convert(createdUser, true, metadata);
searchEngineService.index(userEntity, false);
return userEntity;
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to create an external user {}", newExternalUserEntity, ex);
throw new TechnicalManagementException("An error occurs while trying to create an external user" + newExternalUserEntity, ex);
}
}
private void addDefaultMembership(User user) {
RoleScope[] scopes = { RoleScope.ORGANIZATION, RoleScope.ENVIRONMENT };
List defaultRoleByScopes = roleService.findDefaultRoleByScopes(scopes);
if (defaultRoleByScopes == null || defaultRoleByScopes.isEmpty()) {
throw new DefaultRoleNotFoundException(scopes);
}
for (RoleEntity defaultRoleByScope : defaultRoleByScopes) {
switch (defaultRoleByScope.getScope()) {
case ORGANIZATION:
membershipService.addRoleToMemberOnReference(
new MembershipService.MembershipReference(
MembershipReferenceType.ORGANIZATION,
GraviteeContext.getCurrentOrganization()
),
new MembershipService.MembershipMember(user.getId(), null, MembershipMemberType.USER),
new MembershipService.MembershipRole(RoleScope.ORGANIZATION, defaultRoleByScope.getName())
);
break;
case ENVIRONMENT:
membershipService.addRoleToMemberOnReference(
new MembershipService.MembershipReference(
MembershipReferenceType.ENVIRONMENT,
GraviteeContext.getCurrentEnvironmentOrDefault()
),
new MembershipService.MembershipMember(user.getId(), null, MembershipMemberType.USER),
new MembershipService.MembershipRole(RoleScope.ENVIRONMENT, defaultRoleByScope.getName())
);
break;
default:
break;
}
}
}
@Override
public UserEntity register(final NewExternalUserEntity newExternalUserEntity) {
return register(newExternalUserEntity, null);
}
@Override
public UserEntity register(final NewExternalUserEntity newExternalUserEntity, final String confirmationPageUrl) {
final GraviteeContext.ReferenceContext currentContext = GraviteeContext.getCurrentContext();
UrlSanitizerUtils.checkAllowed(confirmationPageUrl, portalWhitelist, true);
checkUserRegistrationEnabled(currentContext);
boolean autoRegistrationEnabled = isAutoRegistrationEnabled(currentContext);
return createAndSendEmail(newExternalUserEntity, USER_REGISTRATION, confirmationPageUrl, autoRegistrationEnabled);
}
private boolean isAutoRegistrationEnabled(GraviteeContext.ReferenceContext currentContext) {
if (currentContext.getReferenceType().equals(GraviteeContext.ReferenceContextType.ORGANIZATION)) {
return parameterService.findAsBoolean(
Key.CONSOLE_USERCREATION_AUTOMATICVALIDATION_ENABLED,
currentContext.getReferenceId(),
ParameterReferenceType.ORGANIZATION
);
}
return parameterService.findAsBoolean(
Key.PORTAL_USERCREATION_AUTOMATICVALIDATION_ENABLED,
currentContext.getReferenceId(),
ParameterReferenceType.ENVIRONMENT
);
}
@Override
public UserEntity create(final NewExternalUserEntity newExternalUserEntity) {
return createAndSendEmail(newExternalUserEntity, USER_CREATION, null, true);
}
/**
* Allows to create an user and send an email notification to finalize its creation.
*/
private UserEntity createAndSendEmail(
final NewExternalUserEntity newExternalUserEntity,
final ACTION action,
final String confirmationPageUrl,
final boolean autoRegistrationEnabled
) {
if (!EmailValidator.isValid(newExternalUserEntity.getEmail())) {
throw new EmailFormatInvalidException(newExternalUserEntity.getEmail());
}
String organizationId = GraviteeContext.getCurrentOrganization();
if (isBlank(newExternalUserEntity.getSource())) {
newExternalUserEntity.setSource(IDP_SOURCE_GRAVITEE);
} else {
if (!IDP_SOURCE_GRAVITEE.equals(newExternalUserEntity.getSource())) {
// check if IDP exists
identityProviderService.findById(newExternalUserEntity.getSource());
}
}
if (isBlank(newExternalUserEntity.getSourceId())) {
newExternalUserEntity.setSourceId(newExternalUserEntity.getEmail());
}
final Optional optionalUser;
try {
optionalUser =
userRepository.findBySource(newExternalUserEntity.getSource(), newExternalUserEntity.getSourceId(), organizationId);
if (optionalUser.isPresent()) {
throw new UserAlreadyExistsException(
newExternalUserEntity.getSource(),
newExternalUserEntity.getSourceId(),
organizationId
);
}
} catch (final TechnicalException e) {
LOGGER.error(
"An error occurs while trying to create user {} / {}",
newExternalUserEntity.getSource(),
newExternalUserEntity.getSourceId(),
e
);
throw new TechnicalManagementException(e.getMessage(), e);
}
final UserEntity userEntity = create(newExternalUserEntity, true, autoRegistrationEnabled);
if (IDP_SOURCE_GRAVITEE.equals(newExternalUserEntity.getSource())) {
final Map params = getTokenRegistrationParams(userEntity, REGISTRATION_PATH, action, confirmationPageUrl);
emailService.sendAsyncEmailNotification(
new EmailNotificationBuilder()
.to(userEntity.getEmail())
.template(EmailNotificationBuilder.EmailTemplate.TEMPLATES_FOR_ACTION_USER_REGISTRATION)
.params(params)
.param("registrationAction", USER_REGISTRATION.equals(action) ? "registration" : "creation")
.build(),
GraviteeContext.getCurrentContext()
);
if (autoRegistrationEnabled) {
notifierService.trigger(
ACTION.USER_REGISTRATION.equals(action) ? PortalHook.USER_REGISTERED : PortalHook.USER_CREATED,
params
);
} else {
notifierService.trigger(PortalHook.USER_REGISTRATION_REQUEST, params);
}
}
if (newExternalUserEntity.getNewsletter() != null && newExternalUserEntity.getNewsletter()) {
newsletterService.subscribe(newExternalUserEntity.getEmail());
}
return userEntity;
}
@Override
public UserEntity processRegistration(String userId, boolean accepted) {
UserEntity userToProcess = findById(userId);
UserEntity processedUser = this.changeUserStatus(userId, accepted ? UserStatus.ACTIVE : UserStatus.REJECTED);
final Map params = new NotificationParamsBuilder().user(processedUser).build();
emailService.sendAsyncEmailNotification(
new EmailNotificationBuilder()
.to(userToProcess.getEmail())
.template(EmailNotificationBuilder.EmailTemplate.TEMPLATES_FOR_ACTION_USER_REGISTRATION_REQUEST_PROCESSED)
.params(params)
.param("registrationStatus", accepted ? "accepted" : "rejected")
.build(),
GraviteeContext.getCurrentContext()
);
auditService.createEnvironmentAuditLog(
Collections.singletonMap(USER, processedUser.getId()),
accepted ? User.AuditEvent.USER_CONFIRMED : User.AuditEvent.USER_REJECTED,
processedUser.getUpdatedAt(),
userToProcess,
processedUser
);
return processedUser;
}
private UserEntity changeUserStatus(String userId, UserStatus newStatus) {
try {
Optional optionalUser = this.userRepository.findById(userId);
if (optionalUser.isPresent()) {
final User user = optionalUser.get();
user.setStatus(newStatus);
user.setUpdatedAt(new Date());
if (newStatus == UserStatus.REJECTED) {
//so a new registration can be requested with the same email
user.setSourceId(newStatus.name().toLowerCase() + "-" + user.getSourceId());
}
return convert(this.userRepository.update(user), true);
}
throw new UserNotFoundException(userId);
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to validate user registration {}", userId, ex);
throw new TechnicalManagementException("An error occurs while trying to create an external user" + userId, ex);
}
}
@Override
public Map getTokenRegistrationParams(final UserEntity userEntity, final String managementUri, final ACTION action) {
return getTokenRegistrationParams(userEntity, managementUri, action, null);
}
@Override
public Map getTokenRegistrationParams(
final UserEntity userEntity,
final String managementUri,
final ACTION action,
final String targetPageUrl
) {
// generate a JWT to store user's information and for security purpose
final String jwtSecret = environment.getProperty("jwt.secret");
if (jwtSecret == null || jwtSecret.isEmpty()) {
throw new IllegalStateException("JWT secret is mandatory");
}
Algorithm algorithm = Algorithm.HMAC256(environment.getProperty("jwt.secret"));
Date issueAt = new Date();
Instant expireAt = issueAt
.toInstant()
.plus(
Duration.ofSeconds(
environment.getProperty("user.creation.token.expire-after", Integer.class, DEFAULT_JWT_EMAIL_REGISTRATION_EXPIRE_AFTER)
)
);
final String token = JWT
.create()
.withIssuer(environment.getProperty("jwt.issuer", DEFAULT_JWT_ISSUER))
.withIssuedAt(issueAt)
.withExpiresAt(Date.from(expireAt))
.withSubject(userEntity.getId())
.withClaim(Claims.EMAIL, userEntity.getEmail())
.withClaim(Claims.FIRSTNAME, userEntity.getFirstname())
.withClaim(Claims.LASTNAME, userEntity.getLastname())
.withClaim(Claims.ACTION, action.name())
.sign(algorithm);
String managementURL = parameterService.find(Key.MANAGEMENT_URL, ParameterReferenceType.ORGANIZATION);
String userURL = "";
if (!StringUtils.isEmpty(managementURL)) {
if (managementURL.endsWith("/")) {
managementURL = managementURL.substring(0, managementURL.length() - 1);
}
userURL = managementURL + "/#!/settings/users/" + userEntity.getId();
}
String registrationUrl = "";
if (targetPageUrl != null && !targetPageUrl.isEmpty()) {
registrationUrl += targetPageUrl;
if (!targetPageUrl.endsWith("/")) {
registrationUrl += "/";
}
registrationUrl += token;
} else if (!StringUtils.isEmpty(managementURL)) {
registrationUrl = managementURL + managementUri + token;
} else {
// This value is used as a fallback when no Management URL has been configured by the platform admin.
registrationUrl = DEFAULT_MANAGEMENT_URL + managementUri + token;
LOGGER.warn(
"An email will be sent with a default '" +
managementUri.substring(4, managementUri.indexOf('/', 4)) +
"' link. You may want to change this default configuration of the 'Management URL' in the Settings."
);
}
// send a confirm email with the token
return new NotificationParamsBuilder().user(userEntity).token(token).registrationUrl(registrationUrl).userUrl(userURL).build();
}
@Override
public UserEntity update(String id, UpdateUserEntity updateUserEntity) {
return this.update(id, updateUserEntity, updateUserEntity.getEmail());
}
@Override
public UserEntity update(String id, UpdateUserEntity updateUserEntity, String newsletterEmail) {
try {
LOGGER.debug("Updating {}", updateUserEntity);
Optional checkUser = userRepository.findById(id);
if (!checkUser.isPresent()) {
throw new UserNotFoundException(id);
}
User user = checkUser.get();
User previousUser = new User(user);
// Set date fields
user.setUpdatedAt(new Date());
// Set variant fields
if (updateUserEntity.getPicture() != null) {
user.setPicture(updateUserEntity.getPicture());
}
if (updateUserEntity.getFirstname() != null) {
user.setFirstname(updateUserEntity.getFirstname());
}
if (updateUserEntity.getLastname() != null) {
user.setLastname(updateUserEntity.getLastname());
}
if (updateUserEntity.getEmail() != null && !updateUserEntity.getEmail().equals(user.getEmail())) {
if (isInternalUser(user)) {
// sourceId can be updated only for user registered into the Gravitee Repository
// in that case, check if the email is available before update sourceId
final Optional optionalUser = userRepository.findBySource(
user.getSource(),
updateUserEntity.getEmail(),
user.getOrganizationId()
);
if (optionalUser.isPresent()) {
throw new UserAlreadyExistsException(user.getSource(), updateUserEntity.getEmail(), user.getOrganizationId());
}
user.setSourceId(updateUserEntity.getEmail());
}
user.setEmail(updateUserEntity.getEmail());
}
if (updateUserEntity.getStatus() != null) {
user.setStatus(UserStatus.valueOf(updateUserEntity.getStatus()));
}
if (updateUserEntity.isNewsletter() != null) {
user.setNewsletterSubscribed(updateUserEntity.isNewsletter());
if (updateUserEntity.isNewsletter() && newsletterEmail != null) {
newsletterService.subscribe(newsletterEmail);
}
}
User updatedUser = userRepository.update(user);
auditService.createOrganizationAuditLog(
Collections.singletonMap(USER, user.getId()),
User.AuditEvent.USER_UPDATED,
user.getUpdatedAt(),
previousUser,
user
);
List updatedMetadata = new ArrayList<>();
if (updateUserEntity.getCustomFields() != null && !updateUserEntity.getCustomFields().isEmpty()) {
List metadata = userMetadataService.findAllByUserId(user.getId());
for (Map.Entry entry : updateUserEntity.getCustomFields().entrySet()) {
Optional existingMeta = metadata
.stream()
.filter(meta -> meta.getKey().equals(entry.getKey()))
.findFirst();
if (existingMeta.isPresent()) {
UserMetadataEntity meta = existingMeta.get();
UpdateUserMetadataEntity metadataEntity = new UpdateUserMetadataEntity();
metadataEntity.setName(meta.getName());
metadataEntity.setKey(meta.getKey());
metadataEntity.setValue(String.valueOf(entry.getValue()));
metadataEntity.setUserId(meta.getUserId());
metadataEntity.setFormat(meta.getFormat());
updatedMetadata.add(userMetadataService.update(metadataEntity));
} else {
// some additional fields may have been added after the user registration
NewUserMetadataEntity metadataEntity = new NewUserMetadataEntity();
metadataEntity.setName(entry.getKey());
metadataEntity.setValue(String.valueOf(entry.getValue()));
metadataEntity.setUserId(user.getId());
metadataEntity.setFormat(MetadataFormat.STRING);
updatedMetadata.add(userMetadataService.create(metadataEntity));
}
}
}
return convert(updatedUser, true, updatedMetadata);
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to update {}", updateUserEntity, ex);
throw new TechnicalManagementException("An error occurs while trying update " + updateUserEntity, ex);
}
}
@Override
public Page search(String query, Pageable pageable) {
LOGGER.debug("search users");
if (query == null || query.isEmpty()) {
return search(
new UserCriteria.Builder().statuses(UserStatus.ACTIVE, UserStatus.PENDING, UserStatus.REJECTED).build(),
pageable
);
}
// UserDocumentTransformation remove domain from email address for security reasons
// remove it during search phase to provide results
String sanitizedQuery = query.indexOf('@') > 0 ? query.substring(0, query.indexOf('@')) : query;
Query userQuery = QueryBuilder.create(UserEntity.class).setQuery(sanitizedQuery).setPage(pageable).build();
SearchResult results = searchEngineService.search(userQuery);
if (results.hasResults()) {
List users = new ArrayList<>((findByIds(results.getDocuments())));
populateUserFlags(users);
return new Page<>(users, pageable.getPageNumber(), pageable.getPageSize(), results.getHits());
}
return new Page<>(Collections.emptyList(), 1, 0, 0);
}
private void populateUserFlags(final List users) {
RoleEntity apiPORole = roleService.findPrimaryOwnerRoleByOrganization(GraviteeContext.getCurrentOrganization(), RoleScope.API);
RoleEntity applicationPORole = roleService.findPrimaryOwnerRoleByOrganization(
GraviteeContext.getCurrentOrganization(),
RoleScope.APPLICATION
);
users.forEach(
user -> {
final boolean apiPO = !membershipService
.getMembershipsByMemberAndReferenceAndRole(
MembershipMemberType.USER,
user.getId(),
MembershipReferenceType.API,
apiPORole.getId()
)
.isEmpty();
final boolean appPO = !membershipService
.getMembershipsByMemberAndReferenceAndRole(
MembershipMemberType.USER,
user.getId(),
MembershipReferenceType.APPLICATION,
applicationPORole.getId()
)
.isEmpty();
user.setPrimaryOwner(apiPO || appPO);
user.setNbActiveTokens(tokenService.findByUser(user.getId()).size());
}
);
}
@Override
public Page search(UserCriteria criteria, Pageable pageable) {
try {
LOGGER.debug("search users");
UserCriteria.Builder builder = new UserCriteria.Builder()
.organizationId(GraviteeContext.getCurrentOrganization())
.statuses(criteria.getStatuses());
if (criteria.hasNoStatus()) {
builder.noStatus();
}
UserCriteria newCriteria = builder.build();
Page users = userRepository.search(
newCriteria,
new PageableBuilder().pageNumber(pageable.getPageNumber() - 1).pageSize(pageable.getPageSize()).build()
);
List entities = users.getContent().stream().map(u -> convert(u, false)).collect(toList());
populateUserFlags(entities);
return new Page<>(entities, users.getPageNumber() + 1, (int) users.getPageElements(), users.getTotalElements());
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to search users", ex);
throw new TechnicalManagementException("An error occurs while trying to search users", ex);
}
}
@Override
public void delete(String id) {
try {
// If the users is PO of apps or apis, throw an exception
long apiCount = apiService
.findByUser(id, null, false)
.stream()
.filter(entity -> entity.getPrimaryOwner().getId().equals(id))
.count();
long applicationCount = applicationService
.findByUser(id)
.stream()
.filter(app -> app.getPrimaryOwner() != null)
.filter(app -> app.getPrimaryOwner().getId().equals(id))
.count();
if (apiCount > 0 || applicationCount > 0) {
throw new StillPrimaryOwnerException(apiCount, applicationCount);
}
Optional optionalUser = userRepository.findById(id);
if (!optionalUser.isPresent()) {
throw new UserNotFoundException(id);
}
membershipService.removeMemberMemberships(MembershipMemberType.USER, id);
User user = optionalUser.get();
//remove notifications
portalNotificationService.deleteAll(user.getId());
portalNotificationConfigService.deleteByUser(user.getId());
genericNotificationConfigService.deleteByUser(user);
//remove tokens
tokenService.revokeByUser(user.getId());
// change user datas
user.setSourceId("deleted-" + user.getSourceId());
user.setStatus(UserStatus.ARCHIVED);
user.setUpdatedAt(new Date());
if (anonymizeOnDelete) {
User anonym = new User();
anonym.setId(user.getId());
anonym.setCreatedAt(user.getCreatedAt());
anonym.setUpdatedAt(user.getUpdatedAt());
anonym.setStatus(user.getStatus());
anonym.setSource(user.getSource());
anonym.setLastConnectionAt(user.getLastConnectionAt());
anonym.setSourceId("deleted-" + user.getId());
anonym.setFirstname("Unknown");
anonym.setLastname("");
anonym.setLoginCount(user.getLoginCount());
user = anonym;
}
userRepository.update(user);
final UserEntity userEntity = convert(optionalUser.get(), false);
searchEngineService.delete(userEntity, false);
} catch (TechnicalException ex) {
LOGGER.error("An error occurs while trying to delete user", ex);
throw new TechnicalManagementException("An error occurs while trying to delete user", ex);
}
}
@Override
public void resetPassword(final String id) {
this.resetPassword(id, null);
}
@Override
public UserEntity resetPasswordFromSourceId(String sourceId, String resetPageUrl) {
if (sourceId.startsWith("deleted")) {
throw new UserNotActiveException(sourceId);
}
UrlSanitizerUtils.checkAllowed(resetPageUrl, portalWhitelist, true);
UserEntity foundUser = this.findBySource(IDP_SOURCE_GRAVITEE, sourceId, false);
if ("ACTIVE".equals(foundUser.getStatus())) {
this.resetPassword(foundUser.getId(), resetPageUrl);
return foundUser;
} else {
throw new UserNotActiveException(foundUser.getSourceId());
}
}
private boolean isInternalUser(User user) {
return IDP_SOURCE_GRAVITEE.equals(user.getSource());
}
private void resetPassword(final String id, final String resetPageUrl) {
try {
LOGGER.debug("Resetting password of user id {}", id);
Optional optionalUser = userRepository.findById(id);
if (!optionalUser.isPresent()) {
throw new UserNotFoundException(id);
}
final User user = optionalUser.get();
if (!isInternalUser(user)) {
throw new UserNotInternallyManagedException(id);
}
// do not update password to null anymore to avoid DoS attack on a user account.
// use the audit events to throttle the number of resetPassword for a given userid
// see: https://github.com/gravitee-io/issues/issues/4410
// do not perform this check if the request comes from an authenticated user (ie. admin or someone with right permission)
if (!isAuthenticated() || !canResetPassword()) {
AuditQuery query = new AuditQuery();
query.setEvents(Arrays.asList(User.AuditEvent.PASSWORD_RESET.name()));
query.setFrom(Instant.now().minus(1, ChronoUnit.HOURS).toEpochMilli());
query.setPage(1);
query.setSize(100);
MetadataPage events = auditService.search(query);
if (events != null) {
if (events.getContent().size() == 100) {
LOGGER.warn("More than 100 reset password received in less than 1 hour", user.getId());
}
Optional optReset = events
.getContent()
.stream()
.filter(evt -> user.getId().equals(evt.getProperties().get(USER.name())))
.findFirst();
if (optReset.isPresent()) {
LOGGER.warn("Multiple reset password received for user '{}' in less than 1 hour", user.getId());
throw new PasswordAlreadyResetException();
}
}
}
final Map params = getTokenRegistrationParams(
convert(user, false),
RESET_PASSWORD_PATH,
RESET_PASSWORD,
resetPageUrl
);
notifierService.trigger(PortalHook.PASSWORD_RESET, params);
auditService.createOrganizationAuditLog(
Collections.singletonMap(USER, user.getId()),
User.AuditEvent.PASSWORD_RESET,
new Date(),
null,
null
);
emailService.sendAsyncEmailNotification(
new EmailNotificationBuilder()
.to(user.getEmail())
.template(EmailNotificationBuilder.EmailTemplate.TEMPLATES_FOR_ACTION_USER_PASSWORD_RESET)
.params(params)
.build(),
GraviteeContext.getCurrentContext()
);
} catch (TechnicalException ex) {
final String message = "An error occurs while trying to reset password for user " + id;
LOGGER.error(message, ex);
throw new TechnicalManagementException(message, ex);
}
}
protected boolean canResetPassword() {
if (isAdmin()) {
return true;
}
return permissionService.hasPermission(
RolePermission.ORGANIZATION_USERS,
GraviteeContext.getCurrentOrganization(),
new RolePermissionAction[] { UPDATE }
);
}
private User convert(NewExternalUserEntity newExternalUserEntity) {
if (newExternalUserEntity == null) {
return null;
}
User user = new User();
user.setEmail(newExternalUserEntity.getEmail());
user.setFirstname(newExternalUserEntity.getFirstname());
user.setLastname(newExternalUserEntity.getLastname());
user.setSource(newExternalUserEntity.getSource());
user.setSourceId(newExternalUserEntity.getSourceId());
user.setStatus(UserStatus.ACTIVE);
user.setPicture(newExternalUserEntity.getPicture());
user.setNewsletterSubscribed(newExternalUserEntity.getNewsletter());
return user;
}
private User convert(UserEntity userEntity) {
if (userEntity == null) {
return null;
}
User user = new User();
user.setId(userEntity.getId());
user.setEmail(userEntity.getEmail());
user.setFirstname(userEntity.getFirstname());
user.setLastname(userEntity.getLastname());
user.setSource(userEntity.getSource());
user.setSourceId(userEntity.getSourceId());
if (userEntity.getStatus() != null) {
user.setStatus(UserStatus.valueOf(userEntity.getStatus()));
}
return user;
}
private UserEntity convert(User user, boolean loadRoles) {
return convert(user, loadRoles, Collections.emptyList());
}
private UserEntity convert(User user, boolean loadRoles, List customUserFields) {
if (user == null) {
return null;
}
UserEntity userEntity = new UserEntity();
final String userId = user.getId();
userEntity.setId(userId);
userEntity.setSource(user.getSource());
userEntity.setSourceId(user.getSourceId());
userEntity.setEmail(user.getEmail());
userEntity.setFirstname(user.getFirstname());
userEntity.setLastname(user.getLastname());
userEntity.setPassword(user.getPassword());
userEntity.setCreatedAt(user.getCreatedAt());
userEntity.setUpdatedAt(user.getUpdatedAt());
userEntity.setLastConnectionAt(user.getLastConnectionAt());
userEntity.setFirstConnectionAt(user.getFirstConnectionAt());
userEntity.setPicture(user.getPicture());
if (user.getStatus() != null) {
userEntity.setStatus(user.getStatus().name());
}
if (loadRoles) {
Set roles = new HashSet<>();
Set roleEntities = membershipService.getRoles(
MembershipReferenceType.ORGANIZATION,
GraviteeContext.getCurrentOrganization(),
MembershipMemberType.USER,
userId
);
if (!roleEntities.isEmpty()) {
roleEntities.forEach(roleEntity -> roles.add(convert(roleEntity)));
}
this.environmentService.findByOrganization(GraviteeContext.getCurrentOrganization())
.stream()
.flatMap(
env ->
membershipService
.getRoles(MembershipReferenceType.ENVIRONMENT, env.getId(), MembershipMemberType.USER, userId)
.stream()
)
.filter(Objects::nonNull)
.forEach(roleEntity -> roles.add(convert(roleEntity)));
userEntity.setRoles(roles);
Map> envRolesMap = new HashMap<>();
this.environmentService.findByOrganization(GraviteeContext.getCurrentOrganization())
.forEach(
env -> {
Set envRoles = new HashSet<>();
Set envRoleEntities = membershipService.getRoles(
MembershipReferenceType.ENVIRONMENT,
env.getId(),
MembershipMemberType.USER,
userId
);
if (!envRoleEntities.isEmpty()) {
envRoleEntities.forEach(roleEntity -> envRoles.add(convert(roleEntity)));
}
envRolesMap.put(env.getId(), envRoles);
}
);
userEntity.setEnvRoles(envRolesMap);
}
userEntity.setLoginCount(user.getLoginCount());
userEntity.setNewsletterSubscribed(user.getNewsletterSubscribed());
if (customUserFields != null && !customUserFields.isEmpty()) {
Maps.MapBuilder builder = Maps.builder();
for (UserMetadataEntity meta : customUserFields) {
builder.put(meta.getKey(), meta.getValue());
}
userEntity.setCustomFields(builder.build());
}
return userEntity;
}
private UserRoleEntity convert(RoleEntity roleEntity) {
if (roleEntity == null) {
return null;
}
UserRoleEntity userRoleEntity = new UserRoleEntity();
userRoleEntity.setId(roleEntity.getId());
userRoleEntity.setScope(roleEntity.getScope());
userRoleEntity.setName(roleEntity.getName());
userRoleEntity.setPermissions(roleEntity.getPermissions());
return userRoleEntity;
}
@Override
public UserEntity createOrUpdateUserFromSocialIdentityProvider(SocialIdentityProviderEntity socialProvider, String userInfo) {
HashMap attrs = getUserProfileAttrs(socialProvider.getUserProfileMapping(), userInfo);
String email = attrs.get(SocialIdentityProviderEntity.UserProfile.EMAIL);
if (email == null && socialProvider.isEmailRequired()) {
throw new EmailRequiredException(attrs.get(SocialIdentityProviderEntity.UserProfile.ID));
}
// Compute group and role mappings
// This is done BEFORE updating or creating the user account to ensure this one is properly created with correct
// information (ie. mappings)
Set userGroups = computeUserGroupsFromProfile(email, socialProvider.getGroupMappings(), userInfo);
Set userRoles = computeUserRolesFromProfile(email, socialProvider.getRoleMappings(), userInfo);
UserEntity user = null;
boolean created = false;
try {
user = refreshExistingUser(socialProvider, attrs, email);
} catch (UserNotFoundException unfe) {
created = true;
user = createNewExternalUser(socialProvider, userInfo, attrs, email);
}
// Memberships must be refresh only when it is a user creation context or mappings should be synced during
// later authentication
List groupMemberships = refreshUserGroups(user.getId(), socialProvider.getId(), userGroups);
List envRoleMemberships = refreshUserRoles(
user.getId(),
socialProvider.getId(),
userRoles,
RoleScope.ENVIRONMENT
);
List orgRoleMemberships = refreshUserRoles(
user.getId(),
socialProvider.getId(),
userRoles,
RoleScope.ORGANIZATION
);
if (created || socialProvider.isSyncMappings()) {
refreshUserMemberships(user.getId(), socialProvider.getId(), groupMemberships, MembershipReferenceType.GROUP);
refreshUserMemberships(user.getId(), socialProvider.getId(), envRoleMemberships, MembershipReferenceType.ENVIRONMENT);
refreshUserMemberships(user.getId(), socialProvider.getId(), orgRoleMemberships, MembershipReferenceType.ORGANIZATION);
}
return user;
}
private HashMap getUserProfileAttrs(Map userProfileMapping, String userInfo) {
TemplateEngine templateEngine = TemplateEngine.templateEngine();
templateEngine.getTemplateContext().setVariable(TEMPLATE_ENGINE_PROFILE_ATTRIBUTE, userInfo);
ReadContext userInfoPath = JsonPath.parse(userInfo);
HashMap map = new HashMap<>(userProfileMapping.size());
for (Map.Entry entry : userProfileMapping.entrySet()) {
String field = entry.getKey();
String mapping = entry.getValue();
if (mapping != null) {
try {
if (mapping.contains("{#")) {
map.put(field, templateEngine.getValue(mapping, String.class));
} else {
map.put(field, userInfoPath.read(mapping, String.class));
}
} catch (Exception e) {
LOGGER.error("Using mapping: \"{}\", no fields are located in {}", mapping, userInfo);
}
}
}
return map;
}
private UserEntity createNewExternalUser(
final SocialIdentityProviderEntity socialProvider,
final String userInfo,
HashMap attrs,
String email
) {
final NewExternalUserEntity newUser = new NewExternalUserEntity();
newUser.setEmail(email);
newUser.setSource(socialProvider.getId());
if (attrs.get(SocialIdentityProviderEntity.UserProfile.ID) != null) {
newUser.setSourceId(attrs.get(SocialIdentityProviderEntity.UserProfile.ID));
}
if (attrs.get(SocialIdentityProviderEntity.UserProfile.LASTNAME) != null) {
newUser.setLastname(attrs.get(SocialIdentityProviderEntity.UserProfile.LASTNAME));
}
if (attrs.get(SocialIdentityProviderEntity.UserProfile.FIRSTNAME) != null) {
newUser.setFirstname(attrs.get(SocialIdentityProviderEntity.UserProfile.FIRSTNAME));
}
if (attrs.get(SocialIdentityProviderEntity.UserProfile.PICTURE) != null) {
newUser.setPicture(attrs.get(SocialIdentityProviderEntity.UserProfile.PICTURE));
}
return this.create(newUser, false);
}
private UserEntity refreshExistingUser(final SocialIdentityProviderEntity socialProvider, HashMap attrs, String email) {
String userId;
UserEntity registeredUser =
this.findBySource(socialProvider.getId(), attrs.get(SocialIdentityProviderEntity.UserProfile.ID), false);
userId = registeredUser.getId();
// User refresh
UpdateUserEntity user = new UpdateUserEntity();
if (attrs.get(SocialIdentityProviderEntity.UserProfile.LASTNAME) != null) {
user.setLastname(attrs.get(SocialIdentityProviderEntity.UserProfile.LASTNAME));
}
if (attrs.get(SocialIdentityProviderEntity.UserProfile.FIRSTNAME) != null) {
user.setFirstname(attrs.get(SocialIdentityProviderEntity.UserProfile.FIRSTNAME));
}
if (attrs.get(SocialIdentityProviderEntity.UserProfile.PICTURE) != null) {
user.setPicture(attrs.get(SocialIdentityProviderEntity.UserProfile.PICTURE));
}
user.setEmail(email);
return this.update(userId, user);
}
private void addRolesToUser(
String userId,
Collection rolesToAdd,
MembershipReferenceType referenceType,
String referenceId
) {
// add roles to user
for (RoleEntity roleEntity : rolesToAdd) {
MembershipService.MembershipReference ref = null;
if (referenceType != null && referenceId != null) {
ref = new MembershipService.MembershipReference(referenceType, referenceId);
} else if (roleEntity.getScope() == RoleScope.ORGANIZATION) {
ref =
new MembershipService.MembershipReference(
MembershipReferenceType.ORGANIZATION,
GraviteeContext.getCurrentOrganization()
);
} else {
ref =
new MembershipService.MembershipReference(MembershipReferenceType.ENVIRONMENT, GraviteeContext.getCurrentEnvironment());
}
membershipService.addRoleToMemberOnReference(
ref,
new MembershipService.MembershipMember(userId, null, MembershipMemberType.USER),
new MembershipService.MembershipRole(RoleScope.valueOf(roleEntity.getScope().name()), roleEntity.getName())
);
}
}
private void trace(String userId, boolean match, String condition) {
if (LOGGER.isDebugEnabled()) {
if (match) {
LOGGER.debug("the expression {} match {} on user's info ", condition, userId);
} else {
LOGGER.debug("the expression {} didn't match {} on user's info ", condition, userId);
}
}
}
/**
* Calculate the list of groups to associate to a user according to its OIDC profile (ie. UserInfo)
*
* @param userId
* @param mappings
* @param userInfo
* @return
*/
private Set computeUserGroupsFromProfile(String userId, List mappings, String userInfo) {
if (mappings == null || mappings.isEmpty()) {
return Collections.emptySet();
}
Set groups = new HashSet<>();
for (GroupMappingEntity mapping : mappings) {
TemplateEngine templateEngine = TemplateEngine.templateEngine();
templateEngine.getTemplateContext().setVariable(TEMPLATE_ENGINE_PROFILE_ATTRIBUTE, userInfo);
boolean match = templateEngine.getValue(mapping.getCondition(), boolean.class);
trace(userId, match, mapping.getCondition());
// Get groups
if (match) {
for (String groupName : mapping.getGroups()) {
try {
groups.add(groupService.findById(groupName));
} catch (GroupNotFoundException gnfe) {
LOGGER.error("Unable to create user, missing group in repository : {}", groupName);
}
}
}
}
return groups;
}
/**
* Calculate the list of roles to associate to a user according to its OIDC profile (ie. UserInfo)
*
* @param userId
* @param mappings
* @param userInfo
* @return
*/
private Set computeUserRolesFromProfile(String userId, List mappings, String userInfo) {
if (mappings == null || mappings.isEmpty()) {
// provide default roles in this case otherwise user will not have roles if the RoleMapping isn't provided and if the
// option to refresh user profile on each connection is enabled
return roleService.findDefaultRoleByScopes(RoleScope.ORGANIZATION, RoleScope.ENVIRONMENT).stream().collect(toSet());
}
Set roles = new HashSet<>();
for (RoleMappingEntity mapping : mappings) {
TemplateEngine templateEngine = TemplateEngine.templateEngine();
templateEngine.getTemplateContext().setVariable(TEMPLATE_ENGINE_PROFILE_ATTRIBUTE, userInfo);
boolean match = templateEngine.getValue(mapping.getCondition(), boolean.class);
trace(userId, match, mapping.getCondition());
// Get roles
if (match) {
if (mapping.getEnvironments() != null) {
try {
mapping
.getEnvironments()
.forEach(env -> roleService.findByScopeAndName(RoleScope.ENVIRONMENT, env).ifPresent(roles::add));
} catch (RoleNotFoundException rnfe) {
LOGGER.error("Unable to create user, missing role in repository : {}", mapping.getEnvironments());
}
}
if (mapping.getOrganizations() != null) {
try {
mapping
.getOrganizations()
.forEach(org -> roleService.findByScopeAndName(RoleScope.ORGANIZATION, org).ifPresent(roles::add));
} catch (RoleNotFoundException rnfe) {
LOGGER.error("Unable to create user, missing role in repository : {}", mapping.getOrganizations());
}
}
}
}
return roles;
}
private List refreshUserGroups(
String userId,
String identityProviderId,
Collection userGroups
) {
List memberships = new ArrayList<>();
// Get the default group roles from system
List roleEntities = roleService.findDefaultRoleByScopes(RoleScope.API, RoleScope.APPLICATION);
// Add groups to user
for (GroupEntity groupEntity : userGroups) {
for (RoleEntity roleEntity : roleEntities) {
String defaultRole = roleEntity.getName();
// If defined, get the override default role at the group level
if (groupEntity.getRoles() != null) {
String groupDefaultRole = groupEntity.getRoles().get(RoleScope.valueOf(roleEntity.getScope().name()));
if (groupDefaultRole != null) {
defaultRole = groupDefaultRole;
}
}
MembershipService.Membership membership = new MembershipService.Membership(
new MembershipService.MembershipReference(MembershipReferenceType.GROUP, groupEntity.getId()),
new MembershipService.MembershipMember(userId, null, MembershipMemberType.USER),
new MembershipService.MembershipRole(roleEntity.getScope(), defaultRole)
);
membership.setSource(identityProviderId);
memberships.add(membership);
}
}
return memberships;
}
private List refreshUserRoles(
String userId,
String identityProviderId,
Collection userRoles,
RoleScope scope
) {
return userRoles
.stream()
.filter(role -> role.getScope().equals(scope))
.map(
roleEntity -> {
MembershipService.Membership membership = new MembershipService.Membership(
new MembershipService.MembershipReference(
RoleScope.ENVIRONMENT == roleEntity.getScope()
? MembershipReferenceType.ENVIRONMENT
: MembershipReferenceType.ORGANIZATION,
RoleScope.ENVIRONMENT == roleEntity.getScope()
? GraviteeContext.getCurrentEnvironmentOrDefault()
: GraviteeContext.getCurrentOrganization()
),
new MembershipService.MembershipMember(userId, null, MembershipMemberType.USER),
new MembershipService.MembershipRole(RoleScope.valueOf(roleEntity.getScope().name()), roleEntity.getName())
);
membership.setSource(identityProviderId);
return membership;
}
)
.collect(toList());
}
/**
* Refresh user memberships.
*
* @param userId User identifier.
* @param identityProviderId The identity provider used to authenticate the user.
* @param memberships List of memberships to associate to the user
* @param types The types of user memberships to manage
*/
private void refreshUserMemberships(
String userId,
String identityProviderId,
List memberships,
MembershipReferenceType... types
) {
// Get existing memberships for a given type
List userMemberships = new ArrayList<>();
for (MembershipReferenceType type : types) {
userMemberships.addAll(membershipService.findUserMembershipBySource(type, userId, identityProviderId));
}
// Delete existing memberships
userMemberships.forEach(
membership -> {
membershipService.deleteReferenceMember(
MembershipReferenceType.valueOf(membership.getType()),
membership.getReference(),
MembershipMemberType.USER,
userId
);
}
);
Map>>> groupedRoles = new HashMap<>();
memberships.forEach(
membership ->
groupedRoles
.computeIfAbsent(membership.getReference(), ignore -> new HashMap<>())
.computeIfAbsent(membership.getMember(), ignore -> new HashMap<>())
.computeIfAbsent(membership.getSource(), ignore -> new ArrayList<>())
.add(membership.getRole())
);
// Create updated memberships
groupedRoles.forEach(
(reference, memberMapping) ->
memberMapping.forEach(
(member, sourceMapping) ->
sourceMapping.forEach(
(source, roles) -> membershipService.updateRolesToMemberOnReferenceBySource(reference, member, roles, source)
)
)
);
}
@Override
public void updateUserRoles(String userId, MembershipReferenceType referenceType, String referenceId, List roleIds) {
// check if user exist
this.findById(userId);
MemberEntity userMember = membershipService.getUserMember(referenceType, referenceId, userId);
if (userMember != null) {
userMember
.getRoles()
.forEach(
role -> {
if (!roleIds.contains(role.getId())) {
membershipService.removeRole(referenceType, referenceId, MembershipMemberType.USER, userId, role.getId());
} else {
roleIds.remove(role.getId());
}
}
);
}
if (!roleIds.isEmpty()) {
this.addRolesToUser(
userId,
roleIds
.stream()
.map(roleService::findById)
.filter(role -> role.getScope().equals(RoleScope.valueOf(referenceType.name())))
.collect(toSet()),
referenceType,
referenceId
);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy