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