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