/* * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.glowroot.ui; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.net.InetAddress; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.net.MediaType; import io.netty.handler.ssl.SslContextBuilder; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.glowroot.common.config.CentralStorageConfig; import org.glowroot.common.config.CentralWebConfig; import org.glowroot.common.config.EmbeddedStorageConfig; import org.glowroot.common.config.EmbeddedWebConfig; import org.glowroot.common.config.ImmutableCentralStorageConfig; import org.glowroot.common.config.ImmutableCentralWebConfig; import org.glowroot.common.config.ImmutableEmbeddedStorageConfig; import org.glowroot.common.config.ImmutableEmbeddedWebConfig; import org.glowroot.common.config.ImmutableLdapConfig; import org.glowroot.common.config.ImmutableSmtpConfig; import org.glowroot.common.config.ImmutableUserConfig; import org.glowroot.common.config.LdapConfig; import org.glowroot.common.config.RoleConfig; import org.glowroot.common.config.SmtpConfig; import org.glowroot.common.config.UserConfig; import org.glowroot.common.live.LiveAggregateRepository; import org.glowroot.common.repo.ConfigRepository; import org.glowroot.common.repo.ConfigRepository.OptimisticLockException; import org.glowroot.common.repo.RepoAdmin; import org.glowroot.common.repo.util.AlertingService; import org.glowroot.common.repo.util.Encryption; import org.glowroot.common.repo.util.LazySecretKey.SymmetricEncryptionKeyMissingException; import org.glowroot.common.repo.util.MailService; import org.glowroot.common.util.ObjectMappers; import org.glowroot.ui.CommonHandler.CommonResponse; import org.glowroot.ui.HttpServer.PortChangeFailedException; import org.glowroot.ui.HttpSessionManager.Authentication; import org.glowroot.ui.LdapAuthentication.AuthenticationException; import static com.google.common.base.Preconditions.checkNotNull; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpResponseStatus.PRECONDITION_FAILED; @JsonService class AdminJsonService { private static final Logger logger = LoggerFactory.getLogger(ConfigJsonService.class); private static final ObjectMapper mapper = ObjectMappers.create(); private final boolean central; private final File certificateDir; private final ConfigRepository configRepository; private final RepoAdmin repoAdmin; private final LiveAggregateRepository liveAggregateRepository; private final MailService mailService; // null when running in servlet container private volatile @MonotonicNonNull HttpServer httpServer; AdminJsonService(boolean central, File certificateDir, ConfigRepository configRepository, RepoAdmin repoAdmin, LiveAggregateRepository liveAggregateRepository, MailService mailService) { this.central = central; this.certificateDir = certificateDir; this.configRepository = configRepository; this.repoAdmin = repoAdmin; this.liveAggregateRepository = liveAggregateRepository; this.mailService = mailService; } void setHttpServer(HttpServer httpServer) { this.httpServer = httpServer; } // all users have permission to change their own password @POST(path = "/backend/change-password", permission = "") String changePassword(@BindRequest ChangePassword changePassword, @BindAuthentication Authentication authentication) throws Exception { if (authentication.anonymous()) { throw new JsonServiceException(BAD_REQUEST, "cannot change anonymous password"); } UserConfig userConfig = configRepository .getUserConfigCaseInsensitive(authentication.caseAmbiguousUsername()); checkNotNull(userConfig, "user no longer exists"); if (!PasswordHash.validatePassword(changePassword.currentPassword(), userConfig.passwordHash())) { return "{\"currentPasswordIncorrect\":true}"; } ImmutableUserConfig updatedUserConfig = ImmutableUserConfig.builder().copyFrom(userConfig) .passwordHash(PasswordHash.createHash(changePassword.newPassword())) .build(); configRepository.updateUserConfig(updatedUserConfig, userConfig.version()); return ""; } @GET(path = "/backend/admin/web", permission = "admin:view:web") String getWebConfig() throws Exception { if (central) { return getCentralWebConfig(); } else { return getEmbeddedWebConfig(false); } } @GET(path = "/backend/admin/storage", permission = "admin:view:storage") String getStorageConfig() throws Exception { if (central) { CentralStorageConfig config = configRepository.getCentralStorageConfig(); return mapper.writeValueAsString(CentralStorageConfigDto.create(config)); } else { EmbeddedStorageConfig config = configRepository.getEmbeddedStorageConfig(); return mapper.writeValueAsString(EmbeddedStorageConfigDto.create(config)); } } @GET(path = "/backend/admin/smtp", permission = "admin:view:smtp") String getSmtpConfig() throws Exception { SmtpConfig config = configRepository.getSmtpConfig(); String localServerName = InetAddress.getLocalHost().getHostName(); return mapper.writeValueAsString(ImmutableSmtpConfigResponse.builder() .config(SmtpConfigDto.create(config)) .localServerName(localServerName) .build()); } @GET(path = "/backend/admin/ldap", permission = "admin:view:ldap") String getLdapConfig() throws Exception { List<String> allGlowrootRoles = Lists.newArrayList(); for (RoleConfig roleConfig : configRepository.getRoleConfigs()) { allGlowrootRoles.add(roleConfig.name()); } allGlowrootRoles = Ordering.natural().sortedCopy(allGlowrootRoles); return mapper.writeValueAsString(ImmutableLdapConfigResponse.builder() .config(LdapConfigDto.create(configRepository.getLdapConfig())) .allGlowrootRoles(allGlowrootRoles) .build()); } @POST(path = "/backend/admin/web", permission = "admin:edit:web") Object updateWebConfig(@BindRequest String content) throws Exception { if (central) { CentralWebConfigDto configDto = mapper.readValue(content, ImmutableCentralWebConfigDto.class); CentralWebConfig config = configDto.convert(); try { configRepository.updateCentralWebConfig(config, configDto.version()); } catch (OptimisticLockException e) { throw new JsonServiceException(PRECONDITION_FAILED, e); } return getCentralWebConfig(); } else { checkNotNull(httpServer); EmbeddedWebConfigDto configDto = mapper.readValue(content, ImmutableEmbeddedWebConfigDto.class); EmbeddedWebConfig config = configDto.convert(); if (config.https() && !httpServer.getHttps()) { // validate certificate and private key exist and are valid File certificateFile = new File(certificateDir, "certificate.pem"); if (!certificateFile.exists()) { return "{\"httpsRequiredFilesDoNotExist\":true}"; } File privateKeyFile = new File(certificateDir, "private.pem"); if (!privateKeyFile.exists()) { return "{\"httpsRequiredFilesDoNotExist\":true}"; } try { SslContextBuilder.forServer(certificateFile, privateKeyFile); } catch (Exception e) { logger.debug(e.getMessage(), e); StringWriter sw = new StringWriter(); JsonGenerator jg = mapper.getFactory().createGenerator(sw); jg.writeStartObject(); jg.writeStringField("httpsValidationError", e.getMessage()); jg.writeEndObject(); jg.close(); return sw.toString(); } } try { configRepository.updateEmbeddedWebConfig(config, configDto.version()); } catch (OptimisticLockException e) { throw new JsonServiceException(PRECONDITION_FAILED, e); } return onSuccessfulEmbeddedWebUpdate(config); } } @POST(path = "/backend/admin/storage", permission = "admin:edit:storage") String updateStorageConfig(@BindRequest String content) throws Exception { if (central) { CentralStorageConfigDto configDto = mapper.readValue(content, ImmutableCentralStorageConfigDto.class); try { configRepository.updateCentralStorageConfig(configDto.convert(), configDto.version()); } catch (OptimisticLockException e) { throw new JsonServiceException(PRECONDITION_FAILED, e); } } else { EmbeddedStorageConfigDto configDto = mapper.readValue(content, ImmutableEmbeddedStorageConfigDto.class); try { configRepository.updateEmbeddedStorageConfig(configDto.convert(), configDto.version()); } catch (OptimisticLockException e) { throw new JsonServiceException(PRECONDITION_FAILED, e); } repoAdmin.resizeIfNeeded(); } return getStorageConfig(); } @POST(path = "/backend/admin/smtp", permission = "admin:edit:smtp") String updateSmtpConfig(@BindRequest SmtpConfigDto configDto) throws Exception { try { configRepository.updateSmtpConfig(configDto.convert(configRepository), configDto.version()); } catch (SymmetricEncryptionKeyMissingException e) { return "{\"symmetricEncryptionKeyMissing\":true}"; } catch (OptimisticLockException e) { throw new JsonServiceException(PRECONDITION_FAILED, e); } return getSmtpConfig(); } @POST(path = "/backend/admin/ldap", permission = "admin:edit:ldap") String updateLdapConfig(@BindRequest LdapConfigDto configDto) throws Exception { try { configRepository.updateLdapConfig(configDto.convert(configRepository), configDto.version()); } catch (SymmetricEncryptionKeyMissingException e) { return "{\"symmetricEncryptionKeyMissing\":true}"; } catch (OptimisticLockException e) { throw new JsonServiceException(PRECONDITION_FAILED, e); } return getLdapConfig(); } @POST(path = "/backend/admin/send-test-email", permission = "admin:edit:smtp") String sendTestEmail(@BindRequest SmtpConfigDto configDto) throws IOException { // capturing newPlainPassword separately so that newPassword doesn't go through // encryption/decryption which has possibility of throwing // org.glowroot.common.repo.util.LazySecretKey.SymmetricEncryptionKeyMissingException SmtpConfigDto configDtoWithoutNewPassword; String passwordOverride; String newPassword = configDto.newPassword(); if (newPassword.isEmpty()) { configDtoWithoutNewPassword = configDto; passwordOverride = null; } else { configDtoWithoutNewPassword = ImmutableSmtpConfigDto.builder() .copyFrom(configDto) .newPassword("") .build(); passwordOverride = newPassword; } String testEmailRecipient = configDtoWithoutNewPassword.testEmailRecipient(); checkNotNull(testEmailRecipient); List<String> emailAddresses = Splitter.on(',').trimResults().splitToList(testEmailRecipient); try { AlertingService.sendEmail(emailAddresses, "Test email from Glowroot", "", configDtoWithoutNewPassword.convert(configRepository), passwordOverride, configRepository.getLazySecretKey(), mailService); } catch (Exception e) { logger.debug(e.getMessage(), e); return createErrorResponse(e.getMessage()); } return "{}"; } @POST(path = "/backend/admin/test-ldap-connection", permission = "admin:edit:ldap") String testLdapConnection(@BindRequest LdapConfigDto configDto) throws Exception { // capturing newPlainPassword separately so that newPassword doesn't go through // encryption/decryption which has possibility of throwing // org.glowroot.common.repo.util.LazySecretKey.SymmetricEncryptionKeyMissingException LdapConfigDto configDtoWithoutNewPassword; String passwordOverride; String newPassword = configDto.newPassword(); if (newPassword.isEmpty()) { configDtoWithoutNewPassword = configDto; passwordOverride = null; } else { configDtoWithoutNewPassword = ImmutableLdapConfigDto.builder() .copyFrom(configDto) .newPassword("") .build(); passwordOverride = newPassword; } LdapConfig config = configDtoWithoutNewPassword.convert(configRepository); String authTestUsername = checkNotNull(configDtoWithoutNewPassword.authTestUsername()); String authTestPassword = checkNotNull(configDtoWithoutNewPassword.authTestPassword()); Set<String> ldapGroupDns; try { ldapGroupDns = LdapAuthentication.authenticateAndGetLdapGroupDns(authTestUsername, authTestPassword, config, passwordOverride, configRepository.getLazySecretKey()); } catch (AuthenticationException e) { logger.debug(e.getMessage(), e); return createErrorResponse(e.getMessage()); } Set<String> glowrootRoles = LdapAuthentication.getGlowrootRoles(ldapGroupDns, config); StringWriter sw = new StringWriter(); JsonGenerator jg = mapper.getFactory().createGenerator(sw); jg.writeStartObject(); jg.writeObjectField("ldapGroupDns", ldapGroupDns); jg.writeObjectField("glowrootRoles", glowrootRoles); jg.writeEndObject(); jg.close(); return sw.toString(); } @POST(path = "/backend/admin/delete-all-stored-data", permission = "admin:edit:storage") void deleteAllData() throws Exception { repoAdmin.deleteAllData(); liveAggregateRepository.clearInMemoryAggregate(); } @POST(path = "/backend/admin/defrag-data", permission = "admin:edit:storage") void defragData() throws Exception { repoAdmin.defrag(); } @RequiresNonNull("httpServer") private CommonResponse onSuccessfulEmbeddedWebUpdate(EmbeddedWebConfig config) throws Exception { boolean closeCurrentChannelAfterPortChange = false; boolean portChangeFailed = false; if (config.port() != checkNotNull(httpServer.getPort())) { try { httpServer.changePort(config.port()); closeCurrentChannelAfterPortChange = true; } catch (PortChangeFailedException e) { logger.error(e.getMessage(), e); portChangeFailed = true; } } if (config.https() != httpServer.getHttps() && !portChangeFailed) { // only change protocol if port change did not fail, otherwise confusing to display // message that port change failed while at the same time redirecting user to HTTP/S httpServer.changeProtocol(config.https()); closeCurrentChannelAfterPortChange = true; } String responseText = getEmbeddedWebConfig(portChangeFailed); CommonResponse response = new CommonResponse(OK, MediaType.JSON_UTF_8, responseText); if (closeCurrentChannelAfterPortChange) { response.setCloseConnectionAfterPortChange(); } return response; } private String getEmbeddedWebConfig(boolean portChangeFailed) throws Exception { EmbeddedWebConfig config = configRepository.getEmbeddedWebConfig(); ImmutableEmbeddedWebConfigResponse.Builder builder = ImmutableEmbeddedWebConfigResponse.builder() .config(EmbeddedWebConfigDto.create(config)) .certificateDir(certificateDir.getAbsolutePath()) .portChangeFailed(portChangeFailed); if (httpServer == null) { builder.activePort(config.port()) .activeBindAddress(config.bindAddress()) .activeHttps(config.https()); } else { builder.activePort(checkNotNull(httpServer.getPort())) .activeBindAddress(httpServer.getBindAddress()) .activeHttps(httpServer.getHttps()); } return mapper.writeValueAsString(builder.build()); } private String getCentralWebConfig() throws Exception { return mapper.writeValueAsString(ImmutableCentralWebConfigResponse.builder() .config(CentralWebConfigDto.create(configRepository.getCentralWebConfig())) .build()); } private static String createErrorResponse(@Nullable String message) throws IOException { StringWriter sw = new StringWriter(); JsonGenerator jg = mapper.getFactory().createGenerator(sw); jg.writeStartObject(); jg.writeBooleanField("error", true); jg.writeStringField("message", message); jg.writeEndObject(); jg.close(); return sw.toString(); } @Value.Immutable interface ChangePassword { String currentPassword(); String newPassword(); } @Value.Immutable interface EmbeddedWebConfigResponse { EmbeddedWebConfigDto config(); int activePort(); String activeBindAddress(); boolean activeHttps(); String certificateDir(); boolean portChangeFailed(); } @Value.Immutable interface CentralWebConfigResponse { CentralWebConfigDto config(); } @Value.Immutable interface SmtpConfigResponse { SmtpConfigDto config(); String localServerName(); } @Value.Immutable interface LdapConfigResponse { LdapConfigDto config(); List<String> allGlowrootRoles(); } @Value.Immutable abstract static class EmbeddedWebConfigDto { abstract int port(); abstract String bindAddress(); abstract boolean https(); abstract String contextPath(); abstract int sessionTimeoutMinutes(); abstract String sessionCookieName(); abstract String version(); private EmbeddedWebConfig convert() throws Exception { return ImmutableEmbeddedWebConfig.builder() .port(port()) .bindAddress(bindAddress()) .https(https()) .contextPath(contextPath()) .sessionTimeoutMinutes(sessionTimeoutMinutes()) .sessionCookieName(sessionCookieName()) .build(); } private static EmbeddedWebConfigDto create(EmbeddedWebConfig config) { return ImmutableEmbeddedWebConfigDto.builder() .port(config.port()) .bindAddress(config.bindAddress()) .https(config.https()) .contextPath(config.contextPath()) .sessionTimeoutMinutes(config.sessionTimeoutMinutes()) .sessionCookieName(config.sessionCookieName()) .version(config.version()) .build(); } } @Value.Immutable abstract static class CentralWebConfigDto { abstract int sessionTimeoutMinutes(); abstract String sessionCookieName(); abstract String version(); private CentralWebConfig convert() throws Exception { return ImmutableCentralWebConfig.builder() .sessionTimeoutMinutes(sessionTimeoutMinutes()) .sessionCookieName(sessionCookieName()) .build(); } private static CentralWebConfigDto create(CentralWebConfig config) { return ImmutableCentralWebConfigDto.builder() .sessionTimeoutMinutes(config.sessionTimeoutMinutes()) .sessionCookieName(config.sessionCookieName()) .version(config.version()) .build(); } } @Value.Immutable abstract static class EmbeddedStorageConfigDto { abstract ImmutableList<Integer> rollupExpirationHours(); abstract int traceExpirationHours(); abstract int fullQueryTextExpirationHours(); abstract ImmutableList<Integer> rollupCappedDatabaseSizesMb(); abstract int traceCappedDatabaseSizeMb(); abstract String version(); private EmbeddedStorageConfig convert() { return ImmutableEmbeddedStorageConfig.builder() .rollupExpirationHours(rollupExpirationHours()) .traceExpirationHours(traceExpirationHours()) .fullQueryTextExpirationHours(fullQueryTextExpirationHours()) .rollupCappedDatabaseSizesMb(rollupCappedDatabaseSizesMb()) .traceCappedDatabaseSizeMb(traceCappedDatabaseSizeMb()) .build(); } private static EmbeddedStorageConfigDto create(EmbeddedStorageConfig config) { return ImmutableEmbeddedStorageConfigDto.builder() .addAllRollupExpirationHours(config.rollupExpirationHours()) .traceExpirationHours(config.traceExpirationHours()) .fullQueryTextExpirationHours(config.fullQueryTextExpirationHours()) .addAllRollupCappedDatabaseSizesMb(config.rollupCappedDatabaseSizesMb()) .traceCappedDatabaseSizeMb(config.traceCappedDatabaseSizeMb()) .version(config.version()) .build(); } } @Value.Immutable abstract static class CentralStorageConfigDto { abstract ImmutableList<Integer> rollupExpirationHours(); abstract int traceExpirationHours(); abstract int fullQueryTextExpirationHours(); abstract String version(); private CentralStorageConfig convert() { return ImmutableCentralStorageConfig.builder() .rollupExpirationHours(rollupExpirationHours()) .traceExpirationHours(traceExpirationHours()) .fullQueryTextExpirationHours(fullQueryTextExpirationHours()) .build(); } private static CentralStorageConfigDto create(CentralStorageConfig config) { return ImmutableCentralStorageConfigDto.builder() .addAllRollupExpirationHours(config.rollupExpirationHours()) .traceExpirationHours(config.traceExpirationHours()) .fullQueryTextExpirationHours(config.fullQueryTextExpirationHours()) .version(config.version()) .build(); } } @Value.Immutable abstract static class SmtpConfigDto { abstract String host(); abstract @Nullable Integer port(); abstract boolean ssl(); abstract String username(); abstract boolean passwordExists(); @Value.Default String newPassword() { // only used in request return ""; } abstract Map<String, String> additionalProperties(); abstract String fromEmailAddress(); abstract String fromDisplayName(); abstract @Nullable String testEmailRecipient(); // only used in request abstract String version(); private SmtpConfig convert(ConfigRepository configRepository) throws Exception { ImmutableSmtpConfig.Builder builder = ImmutableSmtpConfig.builder() .host(host()) .port(port()) .ssl(ssl()) .username(username()) .putAllAdditionalProperties(additionalProperties()) .fromEmailAddress(fromEmailAddress()) .fromDisplayName(fromDisplayName()); if (!passwordExists()) { // clear password builder.password(""); } else if (passwordExists() && !newPassword().isEmpty()) { // change password String newPassword = Encryption.encrypt(newPassword(), configRepository.getLazySecretKey()); builder.password(newPassword); } else { // keep existing password builder.password(configRepository.getSmtpConfig().password()); } return builder.build(); } private static SmtpConfigDto create(SmtpConfig config) { return ImmutableSmtpConfigDto.builder() .host(config.host()) .port(config.port()) .ssl(config.ssl()) .username(config.username()) .passwordExists(!config.password().isEmpty()) .putAllAdditionalProperties(config.additionalProperties()) .fromEmailAddress(config.fromEmailAddress()) .fromDisplayName(config.fromDisplayName()) .version(config.version()) .build(); } } @Value.Immutable abstract static class LdapConfigDto { abstract String host(); abstract @Nullable Integer port(); abstract boolean ssl(); abstract String username(); abstract boolean passwordExists(); @Value.Default String newPassword() { // only used in request return ""; } abstract String userBaseDn(); abstract String userSearchFilter(); abstract String groupBaseDn(); abstract String groupSearchFilter(); abstract Map<String, List<String>> roleMappings(); abstract @Nullable String authTestUsername(); // only used in request abstract @Nullable String authTestPassword(); // only used in request abstract String version(); private LdapConfig convert(ConfigRepository configRepository) throws Exception { ImmutableLdapConfig.Builder builder = ImmutableLdapConfig.builder() .host(host()) .port(port()) .ssl(ssl()) .username(username()) .userBaseDn(userBaseDn()) .userSearchFilter(userSearchFilter()) .groupBaseDn(groupBaseDn()) .groupSearchFilter(groupSearchFilter()) .roleMappings(roleMappings()); if (!passwordExists()) { // clear password builder.password(""); } else if (passwordExists() && !newPassword().isEmpty()) { // change password String newPassword = Encryption.encrypt(newPassword(), configRepository.getLazySecretKey()); builder.password(newPassword); } else { // keep existing password builder.password(configRepository.getLdapConfig().password()); } return builder.build(); } private static LdapConfigDto create(LdapConfig config) { return ImmutableLdapConfigDto.builder() .host(config.host()) .port(config.port()) .ssl(config.ssl()) .username(config.username()) .passwordExists(!config.password().isEmpty()) .userBaseDn(config.userBaseDn()) .userSearchFilter(config.userSearchFilter()) .groupBaseDn(config.groupBaseDn()) .groupSearchFilter(config.groupSearchFilter()) .roleMappings(config.roleMappings()) .version(config.version()) .build(); } } }