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 com.google.common.collect.Iterables;
11 import java.io.IOException;
12 import java.net.InetSocketAddress;
13 import java.net.SocketAddress;
14 import java.security.GeneralSecurityException;
15 import java.security.PublicKey;
16 import java.util.Collection;
17 import java.util.concurrent.ConcurrentHashMap;
18 import java.util.concurrent.ConcurrentMap;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.opendaylight.mdsal.binding.api.DataBroker;
21 import org.opendaylight.mdsal.binding.api.DataObjectModification;
22 import org.opendaylight.mdsal.binding.api.DataObjectModification.ModificationType;
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.protocol.AuthorizedKeysDecoder;
28 import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorization;
29 import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorization.Builder;
30 import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorizationProvider;
31 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.NetconfCallhomeServer;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.credentials.Credentials;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.AllowedDevices;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.Global;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.Global.MountPointNamingStrategy;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.Device;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.device.transport.Ssh;
38 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;
39 import org.opendaylight.yangtools.concepts.ListenerRegistration;
40 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 public class CallHomeAuthProviderImpl implements CallHomeAuthorizationProvider, AutoCloseable {
45 private static final Logger LOG = LoggerFactory.getLogger(CallHomeAuthProviderImpl.class);
47 private final @NonNull GlobalConfig globalConfig = new GlobalConfig();
48 private final @NonNull DeviceConfig deviceConfig = new DeviceConfig();
49 private final @NonNull DeviceOp deviceOp = new DeviceOp();
50 private final ListenerRegistration<GlobalConfig> configReg;
51 private final ListenerRegistration<DeviceConfig> deviceReg;
52 private final ListenerRegistration<DeviceOp> deviceOpReg;
54 private final CallhomeStatusReporter statusReporter;
56 CallHomeAuthProviderImpl(final DataBroker broker) {
57 configReg = broker.registerDataTreeChangeListener(
58 DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION,
59 InstanceIdentifier.create(NetconfCallhomeServer.class).child(Global.class)),
62 final var allowedDeviceWildcard =
63 InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class);
65 deviceReg = broker.registerDataTreeChangeListener(
66 DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, allowedDeviceWildcard),
68 deviceOpReg = broker.registerDataTreeChangeListener(
69 DataTreeIdentifier.create(LogicalDatastoreType.OPERATIONAL, allowedDeviceWildcard),
71 statusReporter = new CallhomeStatusReporter(broker);
75 public CallHomeAuthorization provideAuth(final SocketAddress remoteAddress, final PublicKey serverKey) {
76 Device deviceSpecific = deviceConfig.get(serverKey);
78 Credentials deviceCred;
80 if (deviceSpecific != null) {
81 sessionName = deviceSpecific.getUniqueId();
82 if (deviceSpecific.getTransport() instanceof Ssh ssh) {
83 final SshClientParams clientParams = ssh.getSshClientParams();
84 deviceCred = clientParams.getCredentials();
86 deviceCred = deviceSpecific.getCredentials();
89 String syntheticId = fromRemoteAddress(remoteAddress);
90 if (globalConfig.allowedUnknownKeys()) {
91 sessionName = syntheticId;
93 statusReporter.asForceListedDevice(syntheticId, serverKey);
95 Device opDevice = deviceOp.get(serverKey);
96 if (opDevice == null) {
97 statusReporter.asUnlistedDevice(syntheticId, serverKey);
99 LOG.info("Repeating rejection of unlisted device with id of {}", opDevice.getUniqueId());
101 return CallHomeAuthorization.rejected();
105 final Credentials credentials = deviceCred != null ? deviceCred : globalConfig.getCredentials();
107 if (credentials == null) {
108 LOG.info("No credentials found for {}, rejecting.", remoteAddress);
109 return CallHomeAuthorization.rejected();
112 Builder authBuilder = CallHomeAuthorization.serverAccepted(sessionName, credentials.getUsername());
113 for (String password : credentials.getPasswords()) {
114 authBuilder.addPassword(password);
116 return authBuilder.build();
120 public void close() {
126 private String fromRemoteAddress(final SocketAddress remoteAddress) {
127 if (remoteAddress instanceof InetSocketAddress socketAddress) {
128 final var hostAddress = socketAddress.getAddress().getHostAddress();
129 return switch (globalConfig.getMountPointNamingStrategy()) {
130 case IPONLY -> hostAddress;
131 case IPPORT -> hostAddress + ":" + socketAddress.getPort();
134 return remoteAddress.toString();
137 private abstract static class AbstractDeviceListener implements DataTreeChangeListener<Device> {
140 public final void onDataTreeChanged(final Collection<DataTreeModification<Device>> mods) {
141 for (DataTreeModification<Device> dataTreeModification : mods) {
142 final DataObjectModification<Device> deviceMod = dataTreeModification.getRootNode();
143 final ModificationType modType = deviceMod.getModificationType();
146 deleteDevice(deviceMod.getDataBefore());
148 case SUBTREE_MODIFIED:
150 deleteDevice(deviceMod.getDataBefore());
151 writeDevice(deviceMod.getDataAfter());
154 throw new IllegalStateException("Unhandled modification type " + modType);
159 private void deleteDevice(final Device dataBefore) {
160 if (dataBefore != null) {
161 final String publicKey = getHostPublicKey(dataBefore);
162 if (publicKey != null) {
163 LOG.debug("Removing device {}", dataBefore.getUniqueId());
164 removeDevice(publicKey, dataBefore);
166 LOG.debug("Ignoring removal of device {}, no host key present", dataBefore.getUniqueId());
171 private void writeDevice(final Device dataAfter) {
172 final String publicKey = getHostPublicKey(dataAfter);
173 if (publicKey != null) {
174 LOG.debug("Adding device {}", dataAfter.getUniqueId());
175 addDevice(publicKey, dataAfter);
177 LOG.debug("Ignoring addition of device {}, no host key present", dataAfter.getUniqueId());
181 private static String getHostPublicKey(final Device device) {
182 if (device.getTransport() instanceof Ssh ssh) {
183 return ssh.getSshClientParams().getHostKey();
185 return device.getSshHostKey();
189 abstract void addDevice(String publicKey, Device device);
191 abstract void removeDevice(String publicKey, Device device);
194 private static class DeviceConfig extends AbstractDeviceListener {
195 private final ConcurrentMap<PublicKey, Device> byPublicKey = new ConcurrentHashMap<>();
196 private final AuthorizedKeysDecoder keyDecoder = new AuthorizedKeysDecoder();
198 Device get(final PublicKey key) {
199 return byPublicKey.get(key);
203 void addDevice(final String publicKey, final Device device) {
204 final PublicKey key = publicKey(publicKey, device);
206 byPublicKey.put(key, device);
211 void removeDevice(final String publicKey, final Device device) {
212 final PublicKey key = publicKey(publicKey, device);
214 byPublicKey.remove(key);
218 private PublicKey publicKey(final String hostKey, final Device device) {
220 return keyDecoder.decodePublicKey(hostKey);
221 } catch (GeneralSecurityException e) {
222 LOG.error("Unable to decode SSH key for {}. Ignoring update for this device", device.getUniqueId(), e);
228 private static class DeviceOp extends AbstractDeviceListener {
229 private final ConcurrentMap<String, Device> byPublicKey = new ConcurrentHashMap<>();
231 Device get(final PublicKey serverKey) {
234 skey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
235 } catch (IOException | IllegalArgumentException e) {
236 LOG.error("Unable to encode server key: {}", serverKey, e);
240 return byPublicKey.get(skey);
244 void removeDevice(final String publicKey, final Device device) {
245 byPublicKey.remove(publicKey);
249 void addDevice(final String publicKey, final Device device) {
250 byPublicKey.put(publicKey, device);
254 private static class GlobalConfig implements DataTreeChangeListener<Global> {
255 private volatile Global current = null;
258 public void onDataTreeChanged(final Collection<DataTreeModification<Global>> mods) {
259 if (!mods.isEmpty()) {
260 current = Iterables.getLast(mods).getRootNode().getDataAfter();
264 boolean allowedUnknownKeys() {
265 final Global local = current;
266 // Deal with null values.
267 return local != null && Boolean.TRUE.equals(local.getAcceptAllSshKeys());
270 Credentials getCredentials() {
271 final Global local = current;
272 return local != null ? local.getCredentials() : null;
275 @NonNull MountPointNamingStrategy getMountPointNamingStrategy() {
276 final Global local = current;
277 final MountPointNamingStrategy strat = local != null ? local.getMountPointNamingStrategy() : null;
278 return strat == null ? MountPointNamingStrategy.IPPORT : strat;