From aad1ec967cc6b25fca19d91e12b432eb0ad06db0 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 5 Dec 2019 17:05:50 +0100 Subject: [PATCH] added initial admin account generation on startup --- .../seb/sebserver/gbl/model/user/UserMod.java | 12 +- .../webservice/AdminUserInitializer.java | 156 ++++++++++++++++++ .../sebserver/webservice/WebserviceInit.java | 44 +++-- .../client/ClientCredentialServiceImpl.java | 20 ++- .../dao/ActivatableEntityDAO.java | 9 +- .../webservice/servicelayer/dao/UserDAO.java | 2 +- .../servicelayer/dao/impl/UserDAOImpl.java | 2 +- .../weblayer/WebServiceSecurityConfig.java | 5 +- .../resources/config/application.properties | 6 +- 9 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/AdminUserInitializer.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java index 5b97f5fd..04da7a61 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java @@ -82,11 +82,11 @@ public final class UserMod implements UserAccount { @NotNull(message = "user:newPassword:notNull") @Size(min = 8, max = 255, message = "user:newPassword:size:{min}:{max}:${validatedValue}") @JsonProperty(PasswordChange.ATTR_NAME_NEW_PASSWORD) - private final String newPassword; + private final CharSequence newPassword; @NotNull(message = "user:confirmNewPassword:notNull") @JsonProperty(PasswordChange.ATTR_NAME_CONFIRM_NEW_PASSWORD) - private final String confirmNewPassword; + private final CharSequence confirmNewPassword; @JsonCreator public UserMod( @@ -94,8 +94,8 @@ public final class UserMod implements UserAccount { @JsonProperty(USER.ATTR_INSTITUTION_ID) final Long institutionId, @JsonProperty(USER.ATTR_NAME) final String name, @JsonProperty(USER.ATTR_USERNAME) final String username, - @JsonProperty(PasswordChange.ATTR_NAME_NEW_PASSWORD) final String newPassword, - @JsonProperty(PasswordChange.ATTR_NAME_CONFIRM_NEW_PASSWORD) final String confirmNewPassword, + @JsonProperty(PasswordChange.ATTR_NAME_NEW_PASSWORD) final CharSequence newPassword, + @JsonProperty(PasswordChange.ATTR_NAME_CONFIRM_NEW_PASSWORD) final CharSequence confirmNewPassword, @JsonProperty(USER.ATTR_EMAIL) final String email, @JsonProperty(USER.ATTR_LANGUAGE) final Locale language, @JsonProperty(USER.ATTR_TIMEZONE) final DateTimeZone timeZone, @@ -148,7 +148,7 @@ public final class UserMod implements UserAccount { return this.uuid; } - public String getNewPassword() { + public CharSequence getNewPassword() { return this.newPassword; } @@ -195,7 +195,7 @@ public final class UserMod implements UserAccount { return EnumSet.copyOf(roles); } - public String getRetypedNewPassword() { + public CharSequence getRetypedNewPassword() { return this.confirmNewPassword; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/AdminUserInitializer.java b/src/main/java/ch/ethz/seb/sebserver/webservice/AdminUserInitializer.java new file mode 100644 index 00000000..d715e4f1 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/AdminUserInitializer.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.WebSecurityConfig; +import ch.ethz.seb.sebserver.gbl.model.institution.Institution; +import ch.ethz.seb.sebserver.gbl.model.user.UserMod; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialServiceImpl; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; + +@Component +@WebServiceProfile +class AdminUserInitializer { + + private static final Logger log = LoggerFactory.getLogger(AdminUserInitializer.class); + + private final UserDAO userDAO; + private final InstitutionDAO institutionDAO; + private final PasswordEncoder passwordEncoder; + private final boolean initializeAdmin; + private final String adminName; + private final String orgName; + + public AdminUserInitializer( + final UserDAO userDAO, + final InstitutionDAO institutionDAO, + @Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder passwordEncoder, + @Value("${sebserver.init.adminaccount.gen-on-init:false}") final boolean initializeAdmin, + @Value("${sebserver.init.adminaccount.username:seb-server-admin}") final String adminName, + @Value("${sebserver.init.organisation.name:ETHZ}") final String orgName) { + + this.userDAO = userDAO; + this.institutionDAO = institutionDAO; + this.passwordEncoder = passwordEncoder; + this.initializeAdmin = initializeAdmin; + this.adminName = adminName; + this.orgName = orgName; + } + + void initAdminAccount() { + if (!this.initializeAdmin) { + log.debug("Create initial admin account is switched on off"); + return; + } + + log.debug("Create initial admin account is switched on. Check database if exists..."); + final Result byUsername = this.userDAO.sebServerUserByUsername(this.adminName); + if (byUsername.hasValue()) { + + log.debug("Initial admin account already exists. Check if the password must be reset..."); + + final SEBServerUser sebServerUser = byUsername.get(); + final String password = sebServerUser.getPassword(); + if (this.passwordEncoder.matches("admin", password)) { + + log.debug("Setting new generated password for already existing admin account"); + final CharSequence generateAdminPassword = this.generateAdminPassword(); + if (generateAdminPassword != null) { + this.userDAO.changePassword( + sebServerUser.getUserInfo().getModelId(), + generateAdminPassword); + this.writeAdminCredentials(this.adminName, generateAdminPassword); + } + } + } else { + final CharSequence generateAdminPassword = this.generateAdminPassword(); + if (generateAdminPassword != null) { + + Long institutionId = this.institutionDAO.allMatching(new FilterMap()) + .getOrElse(() -> Collections.emptyList()) + .stream() + .findFirst() + .filter(Institution::isActive) + .map(Institution::getInstitutionId) + .orElseGet(() -> -1L); + + if (institutionId < 0) { + + log.debug("Create new initial institution"); + institutionId = this.institutionDAO.createNew(new Institution( + null, + this.orgName, + null, + null, + null, + true)) + .map(inst -> this.institutionDAO.setActive(inst, true).getOrThrow()) + .map(Institution::getInstitutionId) + .getOrThrow(); + } + + this.userDAO.createNew(new UserMod( + this.adminName, + institutionId, + this.adminName, + this.adminName, + generateAdminPassword, + generateAdminPassword, + null, + null, + null, + new HashSet<>(Arrays.asList(UserRole.SEB_SERVER_ADMIN.name())))) + .flatMap(account -> this.userDAO.setActive(account, true)) + .map(account -> { + writeAdminCredentials(this.adminName, generateAdminPassword); + return account; + }) + .getOrThrow(); + } + } + + } + + private void writeAdminCredentials(final String name, final CharSequence pwd) { + WebserviceInit.INIT_LOGGER.info("---->"); + WebserviceInit.INIT_LOGGER.info("----> SEB Server initial admin-account; name: {}, pwd: {}", name, pwd); + WebserviceInit.INIT_LOGGER.info("---->"); + WebserviceInit.INIT_LOGGER.info( + "----> !!!! NOTE: Do not forget to login and reset the generated admin password immediately !!!!"); + WebserviceInit.INIT_LOGGER.info("---->"); + } + + private CharSequence generateAdminPassword() { + try { + return ClientCredentialServiceImpl.generateClientSecret(); + } catch (final UnsupportedEncodingException e) { + log.error("Unable to generate admin password: ", e); + return null; + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java index f9bf265e..12735621 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java @@ -15,7 +15,6 @@ import javax.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; @@ -32,33 +31,54 @@ public class WebserviceInit implements ApplicationListener ___ ___ ___ ___ "); + INIT_LOGGER.info("----> / __|| __|| _ ) / __| ___ _ _ __ __ ___ _ _ "); + INIT_LOGGER.info("----> \\__ \\| _| | _ \\ \\__ \\/ -_)| '_|\\ V // -_)| '_|"); + INIT_LOGGER.info("----> |___/|___||___/ |___/\\___||_| \\_/ \\___||_| "); + INIT_LOGGER.info("---->"); + INIT_LOGGER.info("----> SEB Server successfully started up!"); + INIT_LOGGER.info("---->"); + try { - log.info("----> config server address: {}", this.environment.getProperty("server.address")); - log.info("----> config server port: {}", this.environment.getProperty("server.port")); + INIT_LOGGER.info("----> config server address: {}", this.environment.getProperty("server.address")); + INIT_LOGGER.info("----> config server port: {}", this.environment.getProperty("server.port")); - log.info("----> local host address: {}", InetAddress.getLocalHost().getHostAddress()); - log.info("----> local host name: {}", InetAddress.getLocalHost().getHostName()); + INIT_LOGGER.info("----> local host address: {}", InetAddress.getLocalHost().getHostAddress()); + INIT_LOGGER.info("----> local host name: {}", InetAddress.getLocalHost().getHostName()); - log.info("----> remote host address: {}", InetAddress.getLoopbackAddress().getHostAddress()); - log.info("----> remote host name: {}", InetAddress.getLoopbackAddress().getHostName()); + INIT_LOGGER.info("----> remote host address: {}", InetAddress.getLoopbackAddress().getHostAddress()); + INIT_LOGGER.info("----> remote host name: {}", InetAddress.getLoopbackAddress().getHostName()); } catch (final UnknownHostException e) { log.error("Unknown Host: ", e); } - log.info("{}", this.webserviceInfo); + INIT_LOGGER.info("----> {}", this.webserviceInfo); // TODO integration of Flyway for database initialization and migration: https://flywaydb.org // see also https://flywaydb.org/getstarted/firststeps/api + // Create an initial admin account if requested and not already in the data-base + this.adminUserInitializer.initAdminAccount(); } @PreDestroy diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java index 0b9549d4..64ac6852 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentialServiceImpl.java @@ -9,6 +9,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.client; import java.io.UnsupportedEncodingException; +import java.nio.CharBuffer; import java.security.SecureRandom; import org.apache.commons.lang3.RandomStringUtils; @@ -172,16 +173,31 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?" .toCharArray(); - private CharSequence generateClientId() { + public final static CharSequence generateClientId() { return RandomStringUtils.random( 16, 0, possibleCharacters.length - 1, false, false, possibleCharacters, new SecureRandom()); } - private CharSequence generateClientSecret() throws UnsupportedEncodingException { + public final static CharSequence generateClientSecret() throws UnsupportedEncodingException { + // TODO fine a better way to generate a random char array instead of using RandomStringUtils.random which uses a String return RandomStringUtils.random( 64, 0, possibleCharacters.length - 1, false, false, possibleCharacters, new SecureRandom()); } + public final static void clearChars(final CharSequence sequence) { + if (sequence == null) { + return; + } + + if (sequence instanceof CharBuffer) { + ((CharBuffer) sequence).clear(); + return; + } + + throw new IllegalArgumentException( + "Cannot clear chars on CharSequence of type: " + sequence.getClass().getName()); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java index 2b6263db..8e2cde4d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java @@ -8,7 +8,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.Set; import ch.ethz.seb.sebserver.gbl.model.Entity; @@ -39,7 +41,12 @@ public interface ActivatableEntityDAO * @return The Collection of Results refer to the EntityKey instance or refer to an error if happened */ Result> setActive(Set all, boolean active); - /** Indicates if the activatable entity with specified model identifier is currently active + default Result setActive(final T entity, final boolean active) { + return setActive(new HashSet<>(Arrays.asList(entity.getEntityKey())), true) + .flatMap(result -> byModelId(result.iterator().next().modelId)); + } + + /** Indicates if the entity with specified model identifier is currently active * * @param modelId the model identifier of the entity * @return true if the entity is active, false otherwise */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java index 7808ddcb..815d0574 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java @@ -40,7 +40,7 @@ public interface UserDAO extends ActivatableEntityDAO, BulkAc * @param newPassword the new verified password that is encrypted and stored as the new password for the user * account * @return a Result of user account information. Or an exception result on error case */ - Result changePassword(String modelId, String newPassword); + Result changePassword(String modelId, CharSequence newPassword); /** Use this to get the SEBServerUser principal for a given username. * This should be used for internal authorization and consider only active user accounts diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDAOImpl.java index d414d259..d06aa578 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDAOImpl.java @@ -229,7 +229,7 @@ public class UserDAOImpl implements UserDAO { @Override @Transactional - public Result changePassword(final String modelId, final String newPassword) { + public Result changePassword(final String modelId, final CharSequence newPassword) { return recordByUUID(modelId) .map(record -> { final UserRecord newRecord = new UserRecord( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java index 86970fbd..003085e0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java @@ -46,6 +46,7 @@ import org.springframework.security.oauth2.provider.token.UserAuthenticationConv import org.springframework.security.web.AuthenticationEntryPoint; import ch.ethz.seb.sebserver.WebSecurityConfig; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.CachableJdbcTokenStore; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebClientDetailsService; @@ -284,8 +285,8 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { http.antMatcher(apiEndpoint + "/**") .authorizeRequests() .anyRequest() - .permitAll(); - // .hasAuthority(UserRole.SEB_SERVER_ADMIN.name()); + // .permitAll(); + .hasAuthority(UserRole.SEB_SERVER_ADMIN.name()); } } diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index 8f30f0f3..4bd019bb 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -1,10 +1,14 @@ spring.application.name=SEB Server spring.profiles.active=dev +file.encoding=UTF-8 spring.mandatory-file-encoding=UTF-8 spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true sebserver.version=0.5.1 beta -sebserver.supported.languages=en,de +sebserver.supported.languages=en +sebserver.init.organisation.name=ETHZ +sebserver.init.adminaccount.gen-on-init=true +sebserver.init.adminaccount.username=super-admin