All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.license.LicenseService Maven / Gradle / Ivy

There is a newer version: 8.13.2
Show newest version
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */
package org.elasticsearch.license;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.AckedClusterStateUpdateTask;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.component.Lifecycle;
import org.elasticsearch.common.logging.LoggerMessageFormat;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.time.DateFormatter;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.discovery.DiscoveryModule;
import org.elasticsearch.env.Environment;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.protocol.xpack.XPackInfoResponse;
import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.scheduler.SchedulerEngine;

import java.time.Clock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Service responsible for managing {@link LicensesMetadata}.
 * 

* On the master node, the service handles updating the cluster state when a new license is registered. * It also listens on all nodes for cluster state updates, and updates {@link XPackLicenseState} when * the license changes are detected in the cluster state. */ public class LicenseService extends AbstractLifecycleComponent implements ClusterStateListener, SchedulerEngine.Listener { private static final Logger logger = LogManager.getLogger(LicenseService.class); public static final Setting SELF_GENERATED_LICENSE_TYPE = new Setting<>("xpack.license.self_generated.type", (s) -> License.LicenseType.BASIC.getTypeName(), (s) -> { final License.LicenseType type = License.LicenseType.parse(s); return SelfGeneratedLicense.validateSelfGeneratedType(type); }, Setting.Property.NodeScope); static final List ALLOWABLE_UPLOAD_TYPES = getAllowableUploadTypes(); public static final Setting> ALLOWED_LICENSE_TYPES_SETTING = Setting.listSetting("xpack.license.upload.types", Collections.unmodifiableList(ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.toList())), License.LicenseType::parse, LicenseService::validateUploadTypesSetting, Setting.Property.NodeScope); // pkg private for tests static final TimeValue NON_BASIC_SELF_GENERATED_LICENSE_DURATION = TimeValue.timeValueHours(30 * 24); static final Set VALID_TRIAL_TYPES = Collections.unmodifiableSet(Sets.newHashSet( License.LicenseType.GOLD, License.LicenseType.PLATINUM, License.LicenseType.ENTERPRISE, License.LicenseType.TRIAL)); /** * Period before the license expires when warning starts being added to the response header */ static final TimeValue LICENSE_EXPIRATION_WARNING_PERIOD = days(7); public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS = XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS; private final Settings settings; private final ClusterService clusterService; /** * The xpack feature state to update when license changes are made. */ private final XPackLicenseState licenseState; /** * Currently active license */ private final AtomicReference currentLicense = new AtomicReference<>(); private SchedulerEngine scheduler; private final Clock clock; /** * File watcher for operation mode changes */ private final OperationModeFileWatcher operationModeFileWatcher; /** * Callbacks to notify relative to license expiry */ private List expirationCallbacks = new ArrayList<>(); /** * Which license types are permitted to be uploaded to the cluster * @see #ALLOWED_LICENSE_TYPES_SETTING */ private final List allowedLicenseTypes; /** * Max number of nodes licensed by generated trial license */ static final int SELF_GENERATED_LICENSE_MAX_NODES = 1000; static final int SELF_GENERATED_LICENSE_MAX_RESOURCE_UNITS = SELF_GENERATED_LICENSE_MAX_NODES; public static final String LICENSE_JOB = "licenseJob"; public static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy"); private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " + "please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:"; public LicenseService(Settings settings, ClusterService clusterService, Clock clock, Environment env, ResourceWatcherService resourceWatcherService, XPackLicenseState licenseState) { this.settings = settings; this.clusterService = clusterService; this.clock = clock; this.scheduler = new SchedulerEngine(settings, clock); this.licenseState = licenseState; this.allowedLicenseTypes = ALLOWED_LICENSE_TYPES_SETTING.get(settings); this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService, XPackPlugin.resolveConfigFile(env, "license_mode"), logger, () -> updateLicenseState(getLicensesMetadata())); this.scheduler.register(this); populateExpirationCallbacks(); } private void logExpirationWarning(long expirationMillis, boolean expired) { logger.warn("{}", buildExpirationMessage(expirationMillis, expired)); } static CharSequence buildExpirationMessage(long expirationMillis, boolean expired) { String expiredMsg = expired ? "expired" : "will expire"; String general = LoggerMessageFormat.format(null, "License [{}] on [{}].\n" + "# If you have a new license, please update it. Otherwise, please reach out to\n" + "# your support contact.\n" + "# ", expiredMsg, DATE_FORMATTER.formatMillis(expirationMillis)); if (expired) { general = general.toUpperCase(Locale.ROOT); } StringBuilder builder = new StringBuilder(general); builder.append(System.lineSeparator()); if (expired) { builder.append("# COMMERCIAL PLUGINS OPERATING WITH REDUCED FUNCTIONALITY"); } else { builder.append("# Commercial plugins operate with reduced functionality on license expiration:"); } XPackLicenseState.EXPIRATION_MESSAGES.forEach((feature, messages) -> { if (messages.length > 0) { builder.append(System.lineSeparator()); builder.append("# - "); builder.append(feature); for (String message : messages) { builder.append(System.lineSeparator()); builder.append("# - "); builder.append(message); } } }); return builder; } private void populateExpirationCallbacks() { expirationCallbacks.add(new ExpirationCallback.Pre(days(7), days(25), days(1)) { @Override public void on(License license) { logExpirationWarning(license.expiryDate(), false); } }); expirationCallbacks.add(new ExpirationCallback.Post(days(0), null, TimeValue.timeValueMinutes(10)) { @Override public void on(License license) { logExpirationWarning(license.expiryDate(), true); } }); } /** * Registers new license in the cluster * Master only operation. Installs a new license on the master provided it is VALID */ public void registerLicense(final PutLicenseRequest request, final ActionListener listener) { final License newLicense = request.license(); final long now = clock.millis(); if (LicenseVerifier.verifyLicense(newLicense) == false || newLicense.issueDate() > now || newLicense.startDate() > now) { listener.onResponse(new PutLicenseResponse(true, LicensesStatus.INVALID)); return; } final License.LicenseType licenseType; try { licenseType = License.LicenseType.resolve(newLicense); } catch (Exception e) { listener.onFailure(e); return; } if (licenseType == License.LicenseType.BASIC) { listener.onFailure(new IllegalArgumentException("Registering basic licenses is not allowed.")); } else if (isAllowedLicenseType(licenseType) == false) { listener.onFailure(new IllegalArgumentException( "Registering [" + licenseType.getTypeName() + "] licenses is not allowed on this cluster")); } else if (newLicense.expiryDate() < now) { listener.onResponse(new PutLicenseResponse(true, LicensesStatus.EXPIRED)); } else { if (request.acknowledged() == false) { // TODO: ack messages should be generated on the master, since another node's cluster state may be behind... final License currentLicense = getLicense(); if (currentLicense != null) { Map acknowledgeMessages = getAckMessages(newLicense, currentLicense); if (acknowledgeMessages.isEmpty() == false) { // needs acknowledgement listener.onResponse(new PutLicenseResponse(false, LicensesStatus.VALID, ACKNOWLEDGEMENT_HEADER, acknowledgeMessages)); return; } } } // This check would be incorrect if "basic" licenses were allowed here // because the defaults there mean that security can be "off", even if the setting is "on" // BUT basic licenses are explicitly excluded earlier in this method, so we don't need to worry if (XPackSettings.SECURITY_ENABLED.get(settings)) { // TODO we should really validate that all nodes have xpack installed and are consistently configured but this // should happen on a different level and not in this code if (XPackLicenseState.isTransportTlsRequired(newLicense, settings) && XPackSettings.TRANSPORT_SSL_ENABLED.get(settings) == false && isProductionMode(settings, clusterService.localNode())) { // security is on but TLS is not configured we gonna fail the entire request and throw an exception throw new IllegalStateException("Cannot install a [" + newLicense.operationMode() + "] license unless TLS is configured or security is disabled"); } else if (XPackSettings.FIPS_MODE_ENABLED.get(settings) && false == XPackLicenseState.isFipsAllowedForOperationMode(newLicense.operationMode())) { throw new IllegalStateException("Cannot install a [" + newLicense.operationMode() + "] license unless FIPS mode is disabled"); } } clusterService.submitStateUpdateTask("register license [" + newLicense.uid() + "]", new AckedClusterStateUpdateTask(request, listener) { @Override protected PutLicenseResponse newResponse(boolean acknowledged) { return new PutLicenseResponse(acknowledged, LicensesStatus.VALID); } @Override public ClusterState execute(ClusterState currentState) throws Exception { XPackPlugin.checkReadyForXPackCustomMetadata(currentState); final Version oldestNodeVersion = currentState.nodes().getSmallestNonClientNodeVersion(); if (licenseIsCompatible(newLicense, oldestNodeVersion) == false) { throw new IllegalStateException("The provided license is not compatible with node version [" + oldestNodeVersion + "]"); } Metadata currentMetadata = currentState.metadata(); LicensesMetadata licensesMetadata = currentMetadata.custom(LicensesMetadata.TYPE); Version trialVersion = null; if (licensesMetadata != null) { trialVersion = licensesMetadata.getMostRecentTrialVersion(); } Metadata.Builder mdBuilder = Metadata.builder(currentMetadata); mdBuilder.putCustom(LicensesMetadata.TYPE, new LicensesMetadata(newLicense, trialVersion)); return ClusterState.builder(currentState).metadata(mdBuilder).build(); } }); } } private static boolean licenseIsCompatible(License license, Version version) { final int maxVersion = LicenseUtils.getMaxLicenseVersion(version); return license.version() <= maxVersion; } private boolean isAllowedLicenseType(License.LicenseType type) { logger.debug("Checking license [{}] against allowed license types: {}", type, allowedLicenseTypes); return allowedLicenseTypes.contains(type); } public static Map getAckMessages(License newLicense, License currentLicense) { Map acknowledgeMessages = new HashMap<>(); if (License.isAutoGeneratedLicense(currentLicense.signature()) == false // current license is not auto-generated && currentLicense.issueDate() > newLicense.issueDate()) { // and has a later issue date acknowledgeMessages.put("license", new String[] { "The new license is older than the currently installed license. " + "Are you sure you want to override the current license?" }); } XPackLicenseState.ACKNOWLEDGMENT_MESSAGES.forEach((feature, ackMessages) -> { String[] messages = ackMessages.apply(currentLicense.operationMode(), newLicense.operationMode()); if (messages.length > 0) { acknowledgeMessages.put(feature, messages); } }); return acknowledgeMessages; } private static TimeValue days(int days) { return TimeValue.timeValueHours(days * 24); } @Override public void triggered(SchedulerEngine.Event event) { final LicensesMetadata licensesMetadata = getLicensesMetadata(); if (licensesMetadata != null) { final License license = licensesMetadata.getLicense(); if (event.getJobName().equals(LICENSE_JOB)) { updateLicenseState(license, licensesMetadata.getMostRecentTrialVersion()); } else if (event.getJobName().startsWith(ExpirationCallback.EXPIRATION_JOB_PREFIX)) { expirationCallbacks.stream() .filter(expirationCallback -> expirationCallback.getId().equals(event.getJobName())) .forEach(expirationCallback -> expirationCallback.on(license)); } } } /** * Remove license from the cluster state metadata */ public void removeLicense(final DeleteLicenseRequest request, final ActionListener listener) { final PostStartBasicRequest startBasicRequest = new PostStartBasicRequest().acknowledge(true); clusterService.submitStateUpdateTask("delete license", new StartBasicClusterTask(logger, clusterService.getClusterName().value(), clock, startBasicRequest, listener)); } public License getLicense() { final License license = getLicense(clusterService.state().metadata()); return license == LicensesMetadata.LICENSE_TOMBSTONE ? null : license; } private LicensesMetadata getLicensesMetadata() { return this.clusterService.state().metadata().custom(LicensesMetadata.TYPE); } void startTrialLicense(PostStartTrialRequest request, final ActionListener listener) { License.LicenseType requestedType = License.LicenseType.parse(request.getType()); if (VALID_TRIAL_TYPES.contains(requestedType) == false) { throw new IllegalArgumentException("Cannot start trial of type [" + requestedType.getTypeName() + "]. Valid trial types are [" + VALID_TRIAL_TYPES.stream().map(License.LicenseType::getTypeName).sorted().collect(Collectors.joining(",")) + "]"); } StartTrialClusterTask task = new StartTrialClusterTask(logger, clusterService.getClusterName().value(), clock, request, listener); clusterService.submitStateUpdateTask("started trial license", task); } void startBasicLicense(PostStartBasicRequest request, final ActionListener listener) { StartBasicClusterTask task = new StartBasicClusterTask(logger, clusterService.getClusterName().value(), clock, request, listener); clusterService.submitStateUpdateTask("start basic license", task); } /** * Master-only operation to generate a one-time global self generated license. * The self generated license is only generated and stored if the current cluster state metadata * has no existing license. If the cluster currently has a basic license that has an expiration date, * a new basic license with no expiration date is generated. */ private void registerOrUpdateSelfGeneratedLicense() { clusterService.submitStateUpdateTask("maybe generate license for cluster", new StartupSelfGeneratedLicenseTask(settings, clock, clusterService)); } @Override protected void doStart() throws ElasticsearchException { clusterService.addListener(this); scheduler.start(Collections.emptyList()); logger.debug("initializing license state"); if (clusterService.lifecycleState() == Lifecycle.State.STARTED) { final ClusterState clusterState = clusterService.state(); if (clusterState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK) == false && clusterState.nodes().getMasterNode() != null && XPackPlugin.isReadyForXPackCustomMetadata(clusterState)) { final LicensesMetadata currentMetadata = clusterState.metadata().custom(LicensesMetadata.TYPE); boolean noLicense = currentMetadata == null || currentMetadata.getLicense() == null; if (clusterState.getNodes().isLocalNodeElectedMaster() && (noLicense || LicenseUtils.licenseNeedsExtended(currentMetadata.getLicense()))) { // triggers a cluster changed event eventually notifying the current licensee registerOrUpdateSelfGeneratedLicense(); } } } } @Override protected void doStop() throws ElasticsearchException { clusterService.removeListener(this); scheduler.stop(); // clear current license currentLicense.set(null); } @Override protected void doClose() throws ElasticsearchException { } /** * When there is no global block on {@link org.elasticsearch.gateway.GatewayService#STATE_NOT_RECOVERED_BLOCK} * notify licensees and issue auto-generated license if no license has been installed/issued yet. */ @Override public void clusterChanged(ClusterChangedEvent event) { final ClusterState previousClusterState = event.previousState(); final ClusterState currentClusterState = event.state(); if (currentClusterState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK) == false) { if (XPackPlugin.isReadyForXPackCustomMetadata(currentClusterState) == false) { logger.debug("cannot add license to cluster as the following nodes might not understand the license metadata: {}", () -> XPackPlugin.nodesNotReadyForXPackCustomMetadata(currentClusterState)); return; } final LicensesMetadata prevLicensesMetadata = previousClusterState.getMetadata().custom(LicensesMetadata.TYPE); final LicensesMetadata currentLicensesMetadata = currentClusterState.getMetadata().custom(LicensesMetadata.TYPE); if (logger.isDebugEnabled()) { logger.debug("previous [{}]", prevLicensesMetadata); logger.debug("current [{}]", currentLicensesMetadata); } // notify all interested plugins if (previousClusterState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK) || prevLicensesMetadata == null) { if (currentLicensesMetadata != null) { onUpdate(currentLicensesMetadata); } } else if (prevLicensesMetadata.equals(currentLicensesMetadata) == false) { onUpdate(currentLicensesMetadata); } License currentLicense = null; boolean noLicenseInPrevMetadata = prevLicensesMetadata == null || prevLicensesMetadata.getLicense() == null; if (noLicenseInPrevMetadata == false) { currentLicense = prevLicensesMetadata.getLicense(); } boolean noLicenseInCurrentMetadata = (currentLicensesMetadata == null || currentLicensesMetadata.getLicense() == null); if (noLicenseInCurrentMetadata == false) { currentLicense = currentLicensesMetadata.getLicense(); } boolean noLicense = noLicenseInPrevMetadata && noLicenseInCurrentMetadata; // auto-generate license if no licenses ever existed or if the current license is basic and // needs extended or if the license signature needs to be updated. this will trigger a subsequent cluster changed event if (currentClusterState.getNodes().isLocalNodeElectedMaster() && (noLicense || LicenseUtils.licenseNeedsExtended(currentLicense) || LicenseUtils.signatureNeedsUpdate(currentLicense, currentClusterState.nodes()))) { registerOrUpdateSelfGeneratedLicense(); } } else if (logger.isDebugEnabled()) { logger.debug("skipped license notifications reason: [{}]", GatewayService.STATE_NOT_RECOVERED_BLOCK); } } private void updateLicenseState(LicensesMetadata licensesMetadata) { if (licensesMetadata != null) { updateLicenseState(getLicense(licensesMetadata), licensesMetadata.getMostRecentTrialVersion()); } } protected void updateLicenseState(final License license, Version mostRecentTrialVersion) { if (license == LicensesMetadata.LICENSE_TOMBSTONE) { // implies license has been explicitly deleted licenseState.update(License.OperationMode.MISSING, false, license.expiryDate(), mostRecentTrialVersion); return; } if (license != null) { long time = clock.millis(); final boolean active; if (license.expiryDate() == BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) { active = true; } else { active = time >= license.issueDate() && time < license.expiryDate(); } licenseState.update(license.operationMode(), active, license.expiryDate(), mostRecentTrialVersion); if (active) { logger.debug("license [{}] - valid", license.uid()); } else { logger.warn("license [{}] - expired", license.uid()); } } } /** * Notifies registered licensees of license state change and/or new active license * based on the license in currentLicensesMetadata. * Additionally schedules license expiry notifications and event callbacks * relative to the current license's expiry */ private void onUpdate(final LicensesMetadata currentLicensesMetadata) { final License license = getLicense(currentLicensesMetadata); // license can be null if the trial license is yet to be auto-generated // in this case, it is a no-op if (license != null) { final License previousLicense = currentLicense.get(); if (license.equals(previousLicense) == false) { currentLicense.set(license); license.setOperationModeFileWatcher(operationModeFileWatcher); scheduler.add(new SchedulerEngine.Job(LICENSE_JOB, nextLicenseCheck(license))); for (ExpirationCallback expirationCallback : expirationCallbacks) { scheduler.add(new SchedulerEngine.Job(expirationCallback.getId(), (startTime, now) -> expirationCallback.nextScheduledTimeForExpiry(license.expiryDate(), startTime, now))); } if (previousLicense != null) { // remove operationModeFileWatcher to gc the old license object previousLicense.removeOperationModeFileWatcher(); } logger.info("license [{}] mode [{}] - valid", license.uid(), license.operationMode().name().toLowerCase(Locale.ROOT)); } updateLicenseState(license, currentLicensesMetadata.getMostRecentTrialVersion()); } } // pkg private for tests static SchedulerEngine.Schedule nextLicenseCheck(License license) { return (startTime, time) -> { if (time < license.issueDate()) { // when we encounter a license with a future issue date // which can happen with autogenerated license, // we want to schedule a notification on the license issue date // so the license is notificed once it is valid // see https://github.com/elastic/x-plugins/issues/983 return license.issueDate(); } else if (time < license.expiryDate()) { return license.expiryDate(); } return -1; // license is expired, no need to check again }; } public static License getLicense(final Metadata metadata) { final LicensesMetadata licensesMetadata = metadata.custom(LicensesMetadata.TYPE); return getLicense(licensesMetadata); } static License getLicense(final LicensesMetadata metadata) { if (metadata != null) { License license = metadata.getLicense(); if (license == LicensesMetadata.LICENSE_TOMBSTONE) { return license; } else if (license != null) { boolean autoGeneratedLicense = License.isAutoGeneratedLicense(license.signature()); if ((autoGeneratedLicense && SelfGeneratedLicense.verify(license)) || (!autoGeneratedLicense && LicenseVerifier.verifyLicense(license))) { return license; } } } return null; } private static boolean isProductionMode(Settings settings, DiscoveryNode localNode) { final boolean singleNodeDisco = "single-node".equals(DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings)); return singleNodeDisco == false && isBoundToLoopback(localNode) == false; } private static boolean isBoundToLoopback(DiscoveryNode localNode) { return localNode.getAddress().address().getAddress().isLoopbackAddress(); } private static List getAllowableUploadTypes() { return Collections.unmodifiableList(Stream.of(License.LicenseType.values()) .filter(t -> t != License.LicenseType.BASIC) .collect(Collectors.toList())); } private static void validateUploadTypesSetting(List value) { if (ALLOWABLE_UPLOAD_TYPES.containsAll(value) == false) { throw new IllegalArgumentException("Invalid value [" + value.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) + "] for " + ALLOWED_LICENSE_TYPES_SETTING.getKey() + ", allowed values are [" + ALLOWABLE_UPLOAD_TYPES.stream().map(License.LicenseType::getTypeName).collect(Collectors.joining(",")) + "]"); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy