added initial admin account generation on startup

This commit is contained in:
anhefti 2019-12-05 17:05:50 +01:00
parent 3d4b705f8f
commit aad1ec967c
9 changed files with 230 additions and 26 deletions

View file

@ -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;
}

View file

@ -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<SEBServerUser> 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;
}
}
}

View file

@ -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<ApplicationReadyEvent
private static final Logger log = LoggerFactory.getLogger(WebserviceInit.class);
@Autowired
private Environment environment;
@Autowired
private WebserviceInfo webserviceInfo;
static final Logger INIT_LOGGER = LoggerFactory.getLogger("SEB SERVER INIT");
private final Environment environment;
private final WebserviceInfo webserviceInfo;
private final AdminUserInitializer adminUserInitializer;
protected WebserviceInit(
final Environment environment,
final WebserviceInfo webserviceInfo,
final AdminUserInitializer adminUserInitializer) {
this.environment = environment;
this.webserviceInfo = webserviceInfo;
this.adminUserInitializer = adminUserInitializer;
}
@Override
public void onApplicationEvent(final ApplicationReadyEvent event) {
log.info("Initialize SEB-Server Web-Service Component");
INIT_LOGGER.info("----> ___ ___ ___ ___ ");
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

View file

@ -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());
}
}

View file

@ -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<T extends Entity, M extends ModelIdAware>
* @return The Collection of Results refer to the EntityKey instance or refer to an error if happened */
Result<Collection<EntityKey>> setActive(Set<EntityKey> all, boolean active);
/** Indicates if the activatable entity with specified model identifier is currently active
default Result<T> 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 */

View file

@ -40,7 +40,7 @@ public interface UserDAO extends ActivatableEntityDAO<UserInfo, UserMod>, 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<UserInfo> changePassword(String modelId, String newPassword);
Result<UserInfo> 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

View file

@ -229,7 +229,7 @@ public class UserDAOImpl implements UserDAO {
@Override
@Transactional
public Result<UserInfo> changePassword(final String modelId, final String newPassword) {
public Result<UserInfo> changePassword(final String modelId, final CharSequence newPassword) {
return recordByUUID(modelId)
.map(record -> {
final UserRecord newRecord = new UserRecord(

View file

@ -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());
}
}

View file

@ -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