Merge changes I405a2317,Ifbaf2447,Ief534eaa
[aaa.git] / aaa-encrypt-service / impl / src / main / java / org / opendaylight / aaa / encrypt / impl / OSGiEncryptionServiceConfigurator.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.aaa.encrypt.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.collect.Iterables;
14 import com.google.common.util.concurrent.FutureCallback;
15 import com.google.common.util.concurrent.Futures;
16 import com.google.common.util.concurrent.MoreExecutors;
17 import java.security.SecureRandom;
18 import java.util.Base64;
19 import java.util.Collection;
20 import java.util.Objects;
21 import java.util.concurrent.ExecutionException;
22 import org.apache.commons.lang3.RandomStringUtils;
23 import org.checkerframework.checker.lock.qual.GuardedBy;
24 import org.checkerframework.checker.lock.qual.Holding;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.opendaylight.mdsal.binding.api.DataBroker;
28 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
29 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
30 import org.opendaylight.mdsal.binding.api.DataTreeModification;
31 import org.opendaylight.mdsal.common.api.CommitInfo;
32 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
33 import org.opendaylight.odlparent.logging.markers.Markers;
34 import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfig;
35 import org.opendaylight.yang.gen.v1.config.aaa.authn.encrypt.service.config.rev160915.AaaEncryptServiceConfigBuilder;
36 import org.opendaylight.yangtools.concepts.Registration;
37 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
38 import org.osgi.framework.FrameworkUtil;
39 import org.osgi.service.component.ComponentFactory;
40 import org.osgi.service.component.ComponentInstance;
41 import org.osgi.service.component.annotations.Activate;
42 import org.osgi.service.component.annotations.Component;
43 import org.osgi.service.component.annotations.Deactivate;
44 import org.osgi.service.component.annotations.Reference;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * Intermediate component dealing with establishing initial configuration for {@link AAAEncryptionServiceImpl}. In
50  * particular it deals with generating and persisting of encryption salt and encryption password.
51  *
52  * <p>
53  * We primarily listen to the configuration being present. Whenever the salt is missing or the password does not match
54  * the required length, we generate them and persist them. This mode of operation means we potentially have a loop, i.e.
55  * our touching the datastore will trigger again {@link #onDataTreeChanged(Collection)}, which will re-evaluate the
56  * conditions and we try again.
57  */
58 @Component(service = { })
59 public final class OSGiEncryptionServiceConfigurator implements DataTreeChangeListener<AaaEncryptServiceConfig> {
60     private static final Logger LOG = LoggerFactory.getLogger(OSGiEncryptionServiceConfigurator.class);
61     private static final SecureRandom RANDOM = new SecureRandom();
62     private static final @NonNull AaaEncryptServiceConfig DEFAULT_CONFIG = new AaaEncryptServiceConfigBuilder()
63         // Note: mirrors defaults from YANG file
64         .setEncryptMethod("PBKDF2WithHmacSHA1")
65         .setEncryptType("AES")
66         .setEncryptIterationCount(32768)
67         .setEncryptKeyLength(128)
68         .setCipherTransforms("AES/CBC/PKCS5Padding")
69         .setPasswordLength(12)
70         .build();
71
72     private final ComponentFactory<AAAEncryptionServiceImpl> factory;
73     private final DataBroker dataBroker;
74
75     @GuardedBy("this")
76     private Registration reg;
77     @GuardedBy("this")
78     private ComponentInstance<AAAEncryptionServiceImpl> instance;
79     @GuardedBy("this")
80     private AaaEncryptServiceConfig current;
81
82     @Activate
83     public OSGiEncryptionServiceConfigurator(@Reference final DataBroker dataBroker,
84             @Reference(target = "(component.factory=" + AAAEncryptionServiceImpl.FACTORY_NAME + ")")
85             final ComponentFactory<AAAEncryptionServiceImpl> factory) {
86         this.dataBroker = requireNonNull(dataBroker);
87         this.factory = requireNonNull(factory);
88         reg = dataBroker.registerDataTreeChangeListener(
89             DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION,
90                 InstanceIdentifier.create(AaaEncryptServiceConfig.class)),
91             this);
92         LOG.debug("AAA Encryption Service configurator started");
93     }
94
95     @Deactivate
96     public synchronized void deactivate() {
97         reg.close();
98         reg = null;
99         disableInstance();
100         LOG.debug("AAA Encryption Service configurator stopped");
101     }
102
103     @Override
104     public void onDataTreeChanged(final Collection<DataTreeModification<AaaEncryptServiceConfig>> changes) {
105         // Acquire the last reported configuration and check if it needs to have salt/password generated.
106         final var dsConfig = Iterables.getLast(changes).getRootNode().getDataAfter();
107         if (dsConfig == null || needKey(dsConfig) || needSalt(dsConfig)) {
108             // Generate salt/key as needed and persist it -- causing us to be re-invoked later.
109             updateDatastore(dsConfig);
110         } else {
111             // Configuration is self-consistent, proceed to activate an instance based on it
112             updateInstance(dsConfig);
113         }
114     }
115
116     @Override
117     public void onInitialData() {
118         updateDatastore(null);
119     }
120
121     @VisibleForTesting
122     static @NonNull AaaEncryptServiceConfig generateConfig(final @Nullable AaaEncryptServiceConfig datastoreConfig) {
123         // Select template and decide which parts need to be updated
124         final var template = datastoreConfig != null ? datastoreConfig : DEFAULT_CONFIG;
125         final var builder = new AaaEncryptServiceConfigBuilder(template);
126         if (needKey(template)) {
127             LOG.debug("Set the Encryption Service salt");
128             builder.setEncryptKey(RandomStringUtils.random(template.requirePasswordLength(), true, true));
129         }
130         if (needSalt(template)) {
131             LOG.debug("Set the Encryption Service salt");
132             final var salt = new byte[16];
133             RANDOM.nextBytes(salt);
134             builder.setEncryptSalt(Base64.getEncoder().encodeToString(salt));
135         }
136         return builder.build();
137     }
138
139     private void updateDatastore(final @Nullable AaaEncryptServiceConfig expected) {
140         final var target = generateConfig(expected);
141
142         // Careful update of the datastore: we are coming from DTCL thread, so inherently 'expected' may already be out
143         // of date, either by user action, or our update from another node (in a cluster). We rely on transaction's
144         // read&put atomicity to do the right thing here.
145         final var iid = InstanceIdentifier.create(AaaEncryptServiceConfig.class);
146         final var tx = dataBroker.newReadWriteTransaction();
147         final var readFuture = tx.read(LogicalDatastoreType.CONFIGURATION, iid);
148
149         final AaaEncryptServiceConfig actual;
150         try {
151             actual = readFuture.get().orElse(null);
152         } catch (InterruptedException | ExecutionException e) {
153             // Read failed: all we can do now is to disable the service and hope for an external recovery action -- like
154             // a restart of this component or a write to the datastore (which will trigger a retry).
155             tx.cancel();
156             LOG.error("Failed to read configuration, disabling service", e);
157             synchronized (this) {
158                 disableInstance();
159             }
160             return;
161         }
162
163         if (!Objects.equals(actual, expected)) {
164             // Yup, there has been a race -- log that fact and bail out
165             tx.cancel();
166             LOG.debug(Markers.confidential(), "Skipping update on datastore mismatch: expected {} actual {}",
167                 expected, actual);
168             return;
169         }
170
171         LOG.debug(Markers.confidential(), "Updating configuration to {}", target);
172         tx.put(LogicalDatastoreType.CONFIGURATION, iid, target);
173         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
174             @Override
175             public void onFailure(final Throwable throwable) {
176                 // Async update: we should get a new onDataTreeChanged() callback
177                 LOG.warn("Configuration update failed, attempting to continue", throwable);
178             }
179
180             @Override
181             public void onSuccess(final CommitInfo result) {
182                 LOG.info("Configuration update succeeded");
183             }
184         }, MoreExecutors.directExecutor());
185     }
186
187     @Holding("this")
188     private void disableInstance() {
189         if (instance != null) {
190             instance.dispose();
191             instance = null;
192             current = null;
193             LOG.info("Encryption Service disabled");
194         }
195     }
196
197     private synchronized void updateInstance(final AaaEncryptServiceConfig newConfig) {
198         if (reg == null) {
199             LOG.debug("Skipping instance update due to shutdown");
200             return;
201         }
202         if (newConfig.equals(current)) {
203             LOG.debug("Skipping instance update due to equal configuration");
204             return;
205         }
206
207         disableInstance();
208         instance = factory.newInstance(FrameworkUtil.asDictionary(
209             AAAEncryptionServiceImpl.props(new EncryptServiceConfigImpl(newConfig))));
210         current = newConfig;
211         LOG.info("Encryption Service enabled");
212     }
213
214     private static boolean needKey(final AaaEncryptServiceConfig config) {
215         final var key = config.getEncryptKey();
216         return key == null || key.length() != config.requirePasswordLength();
217     }
218
219     private static boolean needSalt(final AaaEncryptServiceConfig config) {
220         final var salt = config.getEncryptSalt();
221         return salt == null || salt.isEmpty();
222     }
223 }