2 * Copyright (c) 2016 Brocade Communication Systems and others. All rights reserved.
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
8 package org.opendaylight.netconf.callhome.mount;
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;
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;
45 @Component(service = CallHomeSshAuthProvider.class, immediate = true)
47 public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvider, AutoCloseable {
48 private static final Logger LOG = LoggerFactory.getLogger(CallHomeMountSshAuthProvider.class);
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;
57 private final CallHomeMountStatusReporter statusReporter;
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)),
68 final var allowedDeviceWildcard =
69 InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class);
71 deviceReg = broker.registerDataTreeChangeListener(
72 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, allowedDeviceWildcard),
74 deviceOpReg = broker.registerDataTreeChangeListener(
75 DataTreeIdentifier.of(LogicalDatastoreType.OPERATIONAL, allowedDeviceWildcard),
78 this.statusReporter = statusReporter;
82 public CallHomeSshAuthSettings provideAuth(final SocketAddress remoteAddress, final PublicKey serverKey) {
83 Device deviceSpecific = deviceConfig.get(serverKey);
85 Credentials deviceCred;
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();
93 deviceCred = deviceSpecific.getCredentials();
96 String syntheticId = fromRemoteAddress(remoteAddress);
97 if (globalConfig.allowedUnknownKeys()) {
100 statusReporter.reportNewSshDevice(syntheticId, serverKey, Device.DeviceStatus.DISCONNECTED);
102 Device opDevice = deviceOp.get(serverKey);
103 if (opDevice == null) {
104 statusReporter.reportNewSshDevice(syntheticId, serverKey, Device.DeviceStatus.FAILEDNOTALLOWED);
106 LOG.info("Repeating rejection of unlisted device with id of {}", opDevice.getUniqueId());
112 final Credentials credentials = deviceCred != null ? deviceCred : globalConfig.getCredentials();
114 if (credentials == null) {
115 LOG.info("No credentials found for {}, rejecting.", id);
119 return new CallHomeSshAuthSettings.DefaultAuthSettings(id, credentials.getUsername(),
120 Set.copyOf(credentials.getPasswords()), null);
124 public void close() {
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();
138 return remoteAddress.toString();
141 private abstract static class AbstractDeviceListener implements DataTreeChangeListener<Device> {
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();
150 deleteDevice(deviceMod.dataBefore());
152 case SUBTREE_MODIFIED:
154 deleteDevice(deviceMod.dataBefore());
155 writeDevice(deviceMod.dataAfter());
158 throw new IllegalStateException("Unhandled modification type " + modType);
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);
170 LOG.debug("Ignoring removal of device {}, no host key present", dataBefore.getUniqueId());
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);
181 LOG.debug("Ignoring addition of device {}, no host key present", dataAfter.getUniqueId());
185 private static String getHostPublicKey(final Device device) {
186 if (device.getTransport() instanceof Ssh ssh) {
187 return ssh.getSshClientParams().getHostKey();
189 return device.getSshHostKey();
193 abstract void addDevice(String publicKey, Device device);
195 abstract void removeDevice(String publicKey, Device device);
198 private static final class DeviceConfig extends AbstractDeviceListener {
199 private final ConcurrentMap<PublicKey, Device> byPublicKey = new ConcurrentHashMap<>();
200 private final AuthorizedKeysDecoder keyDecoder = new AuthorizedKeysDecoder();
202 Device get(final PublicKey key) {
203 return byPublicKey.get(key);
207 void addDevice(final String publicKey, final Device device) {
208 final PublicKey key = publicKey(publicKey, device);
210 byPublicKey.put(key, device);
215 void removeDevice(final String publicKey, final Device device) {
216 final PublicKey key = publicKey(publicKey, device);
218 byPublicKey.remove(key);
222 private PublicKey publicKey(final String hostKey, final Device device) {
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);
232 private static final class DeviceOp extends AbstractDeviceListener {
233 private final ConcurrentMap<String, Device> byPublicKey = new ConcurrentHashMap<>();
235 Device get(final PublicKey serverKey) {
238 skey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
239 } catch (IOException | IllegalArgumentException e) {
240 LOG.error("Unable to encode server key: {}", serverKey, e);
244 return byPublicKey.get(skey);
248 void removeDevice(final String publicKey, final Device device) {
249 byPublicKey.remove(publicKey);
253 void addDevice(final String publicKey, final Device device) {
254 byPublicKey.put(publicKey, device);
258 private static final class GlobalConfig implements DataTreeChangeListener<Global> {
259 private volatile Global current = null;
262 public void onDataTreeChanged(final List<DataTreeModification<Global>> mods) {
263 if (!mods.isEmpty()) {
264 current = mods.get(mods.size() - 1).getRootNode().dataAfter();
268 boolean allowedUnknownKeys() {
269 final Global local = current;
270 // Deal with null values.
271 return local != null && Boolean.TRUE.equals(local.getAcceptAllSshKeys());
274 Credentials getCredentials() {
275 final Global local = current;
276 return local != null ? local.getCredentials() : null;
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;