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