Bump upstreams
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / CallHomeMountSshAuthProvider.java
1 /*
2  * Copyright (c) 2016 Brocade Communication Systems 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.netconf.callhome.mount;
9
10 import java.io.IOException;
11 import java.net.InetSocketAddress;
12 import java.net.SocketAddress;
13 import java.security.GeneralSecurityException;
14 import java.security.PublicKey;
15 import java.util.List;
16 import java.util.Set;
17 import java.util.concurrent.ConcurrentHashMap;
18 import java.util.concurrent.ConcurrentMap;
19 import javax.inject.Inject;
20 import javax.inject.Singleton;
21 import org.eclipse.jdt.annotation.NonNull;
22 import org.opendaylight.mdsal.binding.api.DataBroker;
23 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
24 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
25 import org.opendaylight.mdsal.binding.api.DataTreeModification;
26 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
27 import org.opendaylight.netconf.callhome.server.ssh.CallHomeSshAuthProvider;
28 import org.opendaylight.netconf.callhome.server.ssh.CallHomeSshAuthSettings;
29 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.NetconfCallhomeServer;
30 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.credentials.Credentials;
31 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.AllowedDevices;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.Global;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.Global.MountPointNamingStrategy;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.Device;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.device.transport.Ssh;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParams;
37 import org.opendaylight.yangtools.concepts.Registration;
38 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
39 import org.osgi.service.component.annotations.Activate;
40 import org.osgi.service.component.annotations.Component;
41 import org.osgi.service.component.annotations.Reference;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 @Component(service = CallHomeSshAuthProvider.class, immediate = true)
46 @Singleton
47 public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvider, AutoCloseable {
48     private static final Logger LOG = LoggerFactory.getLogger(CallHomeMountSshAuthProvider.class);
49
50     private final GlobalConfig globalConfig = new GlobalConfig();
51     private final DeviceConfig deviceConfig = new DeviceConfig();
52     private final DeviceOp deviceOp = new DeviceOp();
53     private final Registration configReg;
54     private final Registration deviceReg;
55     private final Registration deviceOpReg;
56
57     private final CallHomeMountStatusReporter statusReporter;
58
59     @Activate
60     @Inject
61     public CallHomeMountSshAuthProvider(final @Reference DataBroker broker,
62             final @Reference CallHomeMountStatusReporter statusReporter) {
63         configReg = broker.registerDataTreeChangeListener(
64             DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION,
65                 InstanceIdentifier.create(NetconfCallhomeServer.class).child(Global.class)),
66             globalConfig);
67
68         final var allowedDeviceWildcard =
69             InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class);
70
71         deviceReg = broker.registerDataTreeChangeListener(
72             DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, allowedDeviceWildcard),
73             deviceConfig);
74         deviceOpReg = broker.registerDataTreeChangeListener(
75             DataTreeIdentifier.of(LogicalDatastoreType.OPERATIONAL, allowedDeviceWildcard),
76             deviceOp);
77
78         this.statusReporter = statusReporter;
79     }
80
81     @Override
82     public CallHomeSshAuthSettings provideAuth(final SocketAddress remoteAddress, final PublicKey serverKey) {
83         Device deviceSpecific = deviceConfig.get(serverKey);
84         String id;
85         Credentials deviceCred;
86
87         if (deviceSpecific != null) {
88             id = deviceSpecific.getUniqueId();
89             if (deviceSpecific.getTransport() instanceof Ssh ssh) {
90                 final SshClientParams clientParams = ssh.getSshClientParams();
91                 deviceCred = clientParams.getCredentials();
92             } else {
93                 deviceCred = deviceSpecific.getCredentials();
94             }
95         } else {
96             String syntheticId = fromRemoteAddress(remoteAddress);
97             if (globalConfig.allowedUnknownKeys()) {
98                 id = syntheticId;
99                 deviceCred = null;
100                 statusReporter.reportNewSshDevice(syntheticId, serverKey, Device.DeviceStatus.DISCONNECTED);
101             } else {
102                 Device opDevice = deviceOp.get(serverKey);
103                 if (opDevice == null) {
104                     statusReporter.reportNewSshDevice(syntheticId, serverKey, Device.DeviceStatus.FAILEDNOTALLOWED);
105                 } else {
106                     LOG.info("Repeating rejection of unlisted device with id of {}", opDevice.getUniqueId());
107                 }
108                 return null;
109             }
110         }
111
112         final Credentials credentials = deviceCred != null ? deviceCred : globalConfig.getCredentials();
113
114         if (credentials == null) {
115             LOG.info("No credentials found for {}, rejecting.", id);
116             return null;
117         }
118
119         return new CallHomeSshAuthSettings.DefaultAuthSettings(id, credentials.getUsername(),
120             Set.copyOf(credentials.getPasswords()), null);
121     }
122
123     @Override
124     public void close() {
125         configReg.close();
126         deviceReg.close();
127         deviceOpReg.close();
128     }
129
130     private String fromRemoteAddress(final SocketAddress remoteAddress) {
131         if (remoteAddress instanceof InetSocketAddress socketAddress) {
132             final var hostAddress = socketAddress.getAddress().getHostAddress();
133             return switch (globalConfig.getMountPointNamingStrategy()) {
134                 case IPONLY -> hostAddress;
135                 case IPPORT -> hostAddress + ":" + socketAddress.getPort();
136             };
137         }
138         return remoteAddress.toString();
139     }
140
141     private abstract static class AbstractDeviceListener implements DataTreeChangeListener<Device> {
142
143         @Override
144         public final void onDataTreeChanged(final List<DataTreeModification<Device>> mods) {
145             for (var dataTreeModification : mods) {
146                 final var deviceMod = dataTreeModification.getRootNode();
147                 final var modType = deviceMod.modificationType();
148                 switch (modType) {
149                     case DELETE:
150                         deleteDevice(deviceMod.dataBefore());
151                         break;
152                     case SUBTREE_MODIFIED:
153                     case WRITE:
154                         deleteDevice(deviceMod.dataBefore());
155                         writeDevice(deviceMod.dataAfter());
156                         break;
157                     default:
158                         throw new IllegalStateException("Unhandled modification type " + modType);
159                 }
160             }
161         }
162
163         private void deleteDevice(final Device dataBefore) {
164             if (dataBefore != null) {
165                 final String publicKey = getHostPublicKey(dataBefore);
166                 if (publicKey != null) {
167                     LOG.debug("Removing device {}", dataBefore.getUniqueId());
168                     removeDevice(publicKey, dataBefore);
169                 } else {
170                     LOG.debug("Ignoring removal of device {}, no host key present", dataBefore.getUniqueId());
171                 }
172             }
173         }
174
175         private void writeDevice(final Device dataAfter) {
176             final String publicKey = getHostPublicKey(dataAfter);
177             if (publicKey != null) {
178                 LOG.debug("Adding device {}", dataAfter.getUniqueId());
179                 addDevice(publicKey, dataAfter);
180             } else {
181                 LOG.debug("Ignoring addition of device {}, no host key present", dataAfter.getUniqueId());
182             }
183         }
184
185         private static String getHostPublicKey(final Device device) {
186             if (device.getTransport() instanceof Ssh ssh) {
187                 return ssh.getSshClientParams().getHostKey();
188             } else {
189                 return device.getSshHostKey();
190             }
191         }
192
193         abstract void addDevice(String publicKey, Device device);
194
195         abstract void removeDevice(String publicKey, Device device);
196     }
197
198     private static final class DeviceConfig extends AbstractDeviceListener {
199         private final ConcurrentMap<PublicKey, Device> byPublicKey = new ConcurrentHashMap<>();
200         private final AuthorizedKeysDecoder keyDecoder = new AuthorizedKeysDecoder();
201
202         Device get(final PublicKey key) {
203             return byPublicKey.get(key);
204         }
205
206         @Override
207         void addDevice(final String publicKey, final Device device) {
208             final PublicKey key = publicKey(publicKey, device);
209             if (key != null) {
210                 byPublicKey.put(key, device);
211             }
212         }
213
214         @Override
215         void removeDevice(final String publicKey, final Device device) {
216             final PublicKey key = publicKey(publicKey, device);
217             if (key != null) {
218                 byPublicKey.remove(key);
219             }
220         }
221
222         private PublicKey publicKey(final String hostKey, final Device device) {
223             try {
224                 return keyDecoder.decodePublicKey(hostKey);
225             } catch (GeneralSecurityException e) {
226                 LOG.error("Unable to decode SSH key for {}. Ignoring update for this device", device.getUniqueId(), e);
227                 return null;
228             }
229         }
230     }
231
232     private static final class DeviceOp extends AbstractDeviceListener {
233         private final ConcurrentMap<String, Device> byPublicKey = new ConcurrentHashMap<>();
234
235         Device get(final PublicKey serverKey) {
236             final String skey;
237             try {
238                 skey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
239             } catch (IOException | IllegalArgumentException e) {
240                 LOG.error("Unable to encode server key: {}", serverKey, e);
241                 return null;
242             }
243
244             return byPublicKey.get(skey);
245         }
246
247         @Override
248         void removeDevice(final String publicKey, final Device device) {
249             byPublicKey.remove(publicKey);
250         }
251
252         @Override
253         void addDevice(final String publicKey, final Device device) {
254             byPublicKey.put(publicKey, device);
255         }
256     }
257
258     private static final class GlobalConfig implements DataTreeChangeListener<Global> {
259         private volatile Global current = null;
260
261         @Override
262         public void onDataTreeChanged(final List<DataTreeModification<Global>> mods) {
263             if (!mods.isEmpty()) {
264                 current = mods.get(mods.size() - 1).getRootNode().dataAfter();
265             }
266         }
267
268         boolean allowedUnknownKeys() {
269             final Global local = current;
270             // Deal with null values.
271             return local != null && Boolean.TRUE.equals(local.getAcceptAllSshKeys());
272         }
273
274         Credentials getCredentials() {
275             final Global local = current;
276             return local != null ? local.getCredentials() : null;
277         }
278
279         @NonNull MountPointNamingStrategy getMountPointNamingStrategy() {
280             final Global local = current;
281             final MountPointNamingStrategy strat = local != null ? local.getMountPointNamingStrategy() : null;
282             return strat == null ? MountPointNamingStrategy.IPPORT : strat;
283         }
284     }
285 }