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

org.opendaylight.aaa.encrypt.impl.OSGiEncryptionServiceConfigurator Maven / Gradle / Ivy

There is a newer version: 0.20.3
Show newest version
/*
 * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 */
package org.opendaylight.aaa.encrypt.impl;

import static java.util.Objects.requireNonNull;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.RandomStringUtils;
import org.checkerframework.checker.lock.qual.GuardedBy;
import org.checkerframework.checker.lock.qual.Holding;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.mdsal.binding.api.DataBroker;
import org.opendaylight.mdsal.binding.api.DataListener;
import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
import org.opendaylight.mdsal.common.api.CommitInfo;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.odlparent.logging.markers.Markers;
import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev240202.AaaEncryptServiceConfig;
import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev240202.AaaEncryptServiceConfigBuilder;
import org.opendaylight.yangtools.concepts.Registration;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.ComponentException;
import org.osgi.service.component.ComponentFactory;
import org.osgi.service.component.ComponentInstance;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Intermediate component dealing with establishing initial configuration for {@link AAAEncryptionServiceImpl}. In
 * particular it deals with generating and persisting of encryption salt and encryption password.
 *
 * 

* We primarily listen to the configuration being present. Whenever the salt is missing or the password does not match * the required length, we generate them and persist them. This mode of operation means we potentially have a loop, i.e. * our touching the datastore will trigger again {@link #dataChangedTo(AaaEncryptServiceConfig)}, which will re-evaluate * the conditions and we try again. */ @Component(service = { }) public final class OSGiEncryptionServiceConfigurator implements DataListener { private static final Logger LOG = LoggerFactory.getLogger(OSGiEncryptionServiceConfigurator.class); private static final SecureRandom RANDOM = new SecureRandom(); private static final @NonNull AaaEncryptServiceConfig DEFAULT_CONFIG = new AaaEncryptServiceConfigBuilder() // Note: mirrors defaults from YANG file .setEncryptMethod("PBKDF2WithHmacSHA1") .setEncryptType("AES") .setEncryptIterationCount(32768) .setEncryptKeyLength(128) .setCipherTransforms("AES/GCM/NoPadding") .setPasswordLength(12) .setAuthTagLength(128) .build(); private final ComponentFactory factory; private final DataBroker dataBroker; @GuardedBy("this") private Registration reg; @GuardedBy("this") private ComponentInstance instance; @GuardedBy("this") private AaaEncryptServiceConfig current; @Activate public OSGiEncryptionServiceConfigurator(@Reference final DataBroker dataBroker, @Reference(target = "(component.factory=" + AAAEncryptionServiceImpl.FACTORY_NAME + ")") final ComponentFactory factory) { this.dataBroker = requireNonNull(dataBroker); this.factory = requireNonNull(factory); reg = dataBroker.registerDataListener( DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(AaaEncryptServiceConfig.class)), this); LOG.debug("AAA Encryption Service configurator started"); } @Deactivate public synchronized void deactivate() { reg.close(); reg = null; disableInstance(); LOG.debug("AAA Encryption Service configurator stopped"); } @Override public void dataChangedTo(final AaaEncryptServiceConfig data) { // Acquire the last reported configuration and check if it needs to have salt/password generated. if (data == null || needKey(data) || needSalt(data)) { // Generate salt/key as needed and persist it -- causing us to be re-invoked later. updateDatastore(data); } else { // Configuration is self-consistent, proceed to activate an instance based on it updateInstance(data); } } @VisibleForTesting static @NonNull AaaEncryptServiceConfig generateConfig(final @Nullable AaaEncryptServiceConfig datastoreConfig) { // Select template and decide which parts need to be updated final var template = datastoreConfig != null ? datastoreConfig : DEFAULT_CONFIG; final var builder = new AaaEncryptServiceConfigBuilder(template); if (needKey(template)) { LOG.debug("Set the Encryption Service salt"); builder.setEncryptKey(RandomStringUtils.random(template.requirePasswordLength(), true, true)); } if (needSalt(template)) { LOG.debug("Set the Encryption Service salt"); final var salt = new byte[16]; RANDOM.nextBytes(salt); builder.setEncryptSalt(Base64.getEncoder().encodeToString(salt)); } return builder.build(); } private void updateDatastore(final @Nullable AaaEncryptServiceConfig expected) { final var target = generateConfig(expected); // Careful update of the datastore: we are coming from DTCL thread, so inherently 'expected' may already be out // of date, either by user action, or our update from another node (in a cluster). We rely on transaction's // read&put atomicity to do the right thing here. final var iid = InstanceIdentifier.create(AaaEncryptServiceConfig.class); final var tx = dataBroker.newReadWriteTransaction(); final var readFuture = tx.read(LogicalDatastoreType.CONFIGURATION, iid); final AaaEncryptServiceConfig actual; try { actual = readFuture.get().orElse(null); } catch (InterruptedException | ExecutionException e) { // Read failed: all we can do now is to disable the service and hope for an external recovery action -- like // a restart of this component or a write to the datastore (which will trigger a retry). tx.cancel(); LOG.error("Failed to read configuration, disabling service", e); synchronized (this) { disableInstance(); } return; } if (!Objects.equals(actual, expected)) { // Yup, there has been a race -- log that fact and bail out tx.cancel(); LOG.debug(Markers.confidential(), "Skipping update on datastore mismatch: expected {} actual {}", expected, actual); return; } LOG.debug(Markers.confidential(), "Updating configuration to {}", target); tx.put(LogicalDatastoreType.CONFIGURATION, iid, target); Futures.addCallback(tx.commit(), new FutureCallback() { @Override public void onFailure(final Throwable throwable) { // Async update: we should get a new onDataTreeChanged() callback LOG.warn("Configuration update failed, attempting to continue", throwable); } @Override public void onSuccess(final CommitInfo result) { LOG.info("Configuration update succeeded"); } }, MoreExecutors.directExecutor()); } @Holding("this") private void disableInstance() { if (instance != null) { instance.dispose(); instance = null; current = null; LOG.info("Encryption Service disabled"); } } private synchronized void updateInstance(final AaaEncryptServiceConfig newConfig) { if (reg == null) { LOG.debug("Skipping instance update due to shutdown"); return; } if (newConfig.equals(current)) { LOG.debug("Skipping instance update due to equal configuration"); return; } disableInstance(); try { instance = factory.newInstance(FrameworkUtil.asDictionary( AAAEncryptionServiceImpl.props(new EncryptServiceConfigImpl(newConfig)))); } catch (ComponentException e) { LOG.error("Failed to start Encryption Service", e); return; } current = newConfig; LOG.info("Encryption Service enabled"); } private static boolean needKey(final AaaEncryptServiceConfig config) { final var key = config.getEncryptKey(); return key == null || key.length() != config.requirePasswordLength(); } private static boolean needSalt(final AaaEncryptServiceConfig config) { final var salt = config.getEncryptSalt(); return salt == null || salt.isEmpty(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy