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