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