From 06ebf05b65ed8292c0ea14fa4bba624c70c895d5 Mon Sep 17 00:00:00 2001 From: Jonathan Creasy Date: Fri, 18 Mar 2016 00:19:58 -0500 Subject: [PATCH] The TELNET api now supports basic and HMACSHA256 authentication. There is still some work to do with the HMAC implementation, it's kind of fake, the noonce and date are hardcoded for example. The connection will attempt to authenticate, and once authenticated the authentication handler is dropped from the pipeline and OpenTSDB behaves the same as it does when not using authentication. You can see below various authentication attempts and the corresponding OpenTSDB log output. ``` --------------------------------------------------------------- $ telnet localhost 4242 Connected to localhost. Escape character is '^]'. auth basic admin admin AUTHSUCESS. exit Connection closed by foreign host. 2016-03-18 00:05:32,510 DEBUG [OpenTSDB I/O Worker #4] AuthenticationChannelHandler: Setting up AuthenticationChannelHandler 2016-03-18 00:05:32,510 DEBUG [OpenTSDB I/O Worker #4] AuthenticationChannelHandler: Passing auth command to Authentication Plugin 2016-03-18 00:05:32,510 DEBUG [OpenTSDB I/O Worker #4] EmbeddedAuthenticationPlugin: Validating Credentials 2016-03-18 00:05:32,510 DEBUG [OpenTSDB I/O Worker #4] EmbeddedAuthenticationPlugin: Authentication Succeeded for: admin 2016-03-18 00:05:32,511 INFO [OpenTSDB I/O Worker #4] AuthenticationChannelHandler: Authentication Completed --------------------------------------------------------------- $ telnet localhost 4242 Connected to localhost. Escape character is '^]'. auth hmacsha256 admin digest=6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535&date=2016-03-18T04:10:27+00:00&noonce=4353 AUTHSUCESS. put test.foo 53434646745 43 tag=value put: unknown metric: No such name for 'metrics': 'test.foo' exit Connection closed by foreign host. 2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker #2] AuthenticationChannelHandler: Setting up AuthenticationChannelHandler 2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker #2] AuthenticationChannelHandler: Passing auth command to Authentication Plugin 2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker #2] EmbeddedAuthenticationPlugin: Validating Digest 2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker #2] EmbeddedAuthenticationPlugin: Authenticating admin 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535 2016-03-18 00:02:47,631 TRACE [OpenTSDB I/O Worker #2] EmbeddedAuthenticationPlugin: Generating HASH for admin admin2016-03-18T04:10:27+00:004353 2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker #2] EmbeddedAuthenticationPlugin: Generating HASH for admin 2016-03-18 00:02:47,632 DEBUG [OpenTSDB I/O Worker #2] EmbeddedAuthenticationPlugin: Calc: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535 2016-03-18 00:02:47,632 DEBUG [OpenTSDB I/O Worker #2] EmbeddedAuthenticationPlugin: Prov: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535 2016-03-18 00:02:47,632 DEBUG [OpenTSDB I/O Worker #2] EmbeddedAuthenticationPlugin: Authentication Succeeded for: admin 2016-03-18 00:02:47,632 INFO [OpenTSDB I/O Worker #2] AuthenticationChannelHandler: Authentication Completed 2016-03-18 00:02:52,719 DEBUG [OpenTSDB I/O Worker #2] PutDataPointRpc: put: unknown metric: No such name for 'metrics': 'test.foo' --------------------------------------------------------------- $ telnet localhost 4242 Connected to localhost. Escape character is '^]'. put test.foo 53434646745 43 tag=value AUTHFAIL put test.foo 53434646745 43 tag=value AUTHFAIL auth hmacsha256 admin digest=6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535&date=2016-03-18T04:10:27+00:00&noonce=4353 AUTHSUCESS. put test.foo 53434646745 43 tag=value put: unknown metric: No such name for 'metrics': 'test.foo' exit Connection closed by foreign host. 2016-03-18 00:02:56,102 DEBUG [OpenTSDB I/O Worker #3] AuthenticationChannelHandler: Setting up AuthenticationChannelHandler 2016-03-18 00:02:56,103 DEBUG [OpenTSDB I/O Worker #3] AuthenticationChannelHandler: Passing auth command to Authentication Plugin 2016-03-18 00:02:56,103 ERROR [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Invalid Authentication Command Length: 5 2016-03-18 00:02:57,798 DEBUG [OpenTSDB I/O Worker #3] AuthenticationChannelHandler: Passing auth command to Authentication Plugin 2016-03-18 00:02:57,798 ERROR [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Invalid Authentication Command Length: 5 2016-03-18 00:03:02,173 DEBUG [OpenTSDB I/O Worker #3] AuthenticationChannelHandler: Passing auth command to Authentication Plugin 2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Validating Digest 2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Authenticating admin 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535 2016-03-18 00:03:02,174 TRACE [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Generating HASH for admin admin2016-03-18T04:10:27+00:004353 2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Generating HASH for admin 2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Calc: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535 2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Prov: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535 2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker #3] EmbeddedAuthenticationPlugin: Authentication Succeeded for: admin 2016-03-18 00:03:02,174 INFO [OpenTSDB I/O Worker #3] AuthenticationChannelHandler: Authentication Completed 2016-03-18 00:03:12,038 DEBUG [OpenTSDB I/O Worker #3] PutDataPointRpc: put: unknown metric: No such name for 'metrics': 'test.foo' --------------------------------------------------------------- ``` Remaining Items: * Create handler for HTTP to pull same HMAC values from headers * Create HTTP API for modifying accessKey and Account objects - GET accessKey will generate new accessKey/accessSecretKey for an account (requires account root credentials) - DEL accessKey will delete the associated accessKey (will work with accessKey credentials or account root credentials) - PUT account will create a new account (requires admin credentials), returns root account accessKey/secretAccessKey - GET account will fetch account info, and list all accessKeys (but not accessSecretKeys) - DEL account will delete an account and all keys (requires admin credentials or account root credentials) * Modify the built-in authentication plugin to store credentials in HBase The API above will do nothing in OpenTSDB but will call the appropriate functions on the AuthenticationPlugin if configured. Will return a 405 Method Not Allowed if no plugin is configured. The built-in authentication plugin currently just uses the single user provided in the config, but I would like to expand it to store accounts and accessKey/accessSecretKey pairs in an HBase table. The admin credentials are in the config, the built in plugin has no notion of groups. --- src/auth/AuthenticationChannelHandler.java | 61 ++++++++++- src/auth/AuthenticationPlugin.java | 13 +-- src/auth/EmbeddedAuthenticationPlugin.java | 122 +++++++++++++++++++-- src/logback.xml | 5 +- src/opentsdb.conf | 6 + 5 files changed, 180 insertions(+), 27 deletions(-) diff --git a/src/auth/AuthenticationChannelHandler.java b/src/auth/AuthenticationChannelHandler.java index 67e0d08f14..c1a9d36904 100644 --- a/src/auth/AuthenticationChannelHandler.java +++ b/src/auth/AuthenticationChannelHandler.java @@ -16,20 +16,73 @@ import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Arrays; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelEvent; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; /** * @since 2.3 */ public class AuthenticationChannelHandler extends SimpleChannelUpstreamHandler { + private static final Logger LOG = LoggerFactory.getLogger(AuthenticationChannelHandler.class); + private TSDB tsdb = null; private AuthenticationPlugin authentication = null; + public AuthenticationChannelHandler(String type, TSDB tsdb) { + LOG.debug("Setting up AuthenticationChannelHandler"); this.authentication = tsdb.getAuth(); } + + @Override + public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { + if (e instanceof ChannelStateEvent) { + System.err.println(e); + } + super.handleUpstream(ctx, e); + } + + @Override + public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { + // Send greeting for a new connection. + e.getChannel().write("AUTHREQUIRED.\r\n"); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + @Override - public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { - if (authentication.authenticate(e)) { - ctx.getPipeline().remove(this); + public void messageReceived(ChannelHandlerContext ctx, MessageEvent msgevent) { + String response = "AUTHFAIL\r\n"; + try { + final Object message = msgevent.getMessage(); + if (message instanceof String[]) { + LOG.debug("Passing auth command to Authentication Plugin"); + if (authentication.authenticate((String[]) message)) { + LOG.info("Authentication Completed"); + response = "AUTHSUCESS.\r\n"; + ctx.getPipeline().remove(this); + } + } else { + LOG.debug("Unexpected message type " + + message.getClass() + ": " + message); + } + } catch (Exception e) { + LOG.debug("Unexpected exception caught" + + " while serving: " + e); + } finally { + ChannelFuture future = msgevent.getChannel().write(response); } } } diff --git a/src/auth/AuthenticationPlugin.java b/src/auth/AuthenticationPlugin.java index 9c9c45560d..73550fd403 100644 --- a/src/auth/AuthenticationPlugin.java +++ b/src/auth/AuthenticationPlugin.java @@ -13,13 +13,9 @@ package net.opentsdb.auth; import net.opentsdb.core.TSDB; -import net.opentsdb.meta.Annotation; -import net.opentsdb.meta.TSMeta; -import net.opentsdb.meta.UIDMeta; import net.opentsdb.stats.StatsCollector; - import com.stumbleupon.async.Deferred; -import org.jboss.netty.channel.MessageEvent; +import java.util.Map; /** * @since 2.3 @@ -64,8 +60,7 @@ public abstract class AuthenticationPlugin { * @param collector The collector used for emitting statistics */ public abstract void collectStats(final StatsCollector collector); - - public abstract Boolean authenticate(MessageEvent e); - public abstract Boolean authenticate(String digest); - public abstract Boolean authenticate(String username, String password); + public abstract Boolean authenticate(String[] command); + public abstract Boolean authenticate(String access_key, String access_secret_key); + public abstract Boolean authenticate(String access_key, Map fields); } diff --git a/src/auth/EmbeddedAuthenticationPlugin.java b/src/auth/EmbeddedAuthenticationPlugin.java index 03c1063f37..ebef8862f4 100644 --- a/src/auth/EmbeddedAuthenticationPlugin.java +++ b/src/auth/EmbeddedAuthenticationPlugin.java @@ -16,20 +16,78 @@ import com.stumbleupon.async.Deferred; import net.opentsdb.core.TSDB; import net.opentsdb.stats.StatsCollector; -import org.jboss.netty.channel.MessageEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + /** * @since 2.3 */ public class EmbeddedAuthenticationPlugin extends AuthenticationPlugin { private static final Logger LOG = LoggerFactory.getLogger(EmbeddedAuthenticationPlugin.class); private TSDB tsdb = null; + private Map authDB = new HashMap(); + + private static String algo = "HmacSHA256"; + + private static String hmacDigest(String access_key, String access_key_secret) { + LOG.trace("Generating HASH for " + access_key + " " + access_key_secret); + LOG.debug("Generating HASH for " + access_key); + String digest = null; + try { + SecretKeySpec key = new SecretKeySpec((access_key_secret).getBytes("UTF-8"), algo); + Mac mac = Mac.getInstance(algo); + mac.init(key); + + byte[] bytes = mac.doFinal(access_key.getBytes("ASCII")); + + StringBuffer hash = new StringBuffer(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + hash.append('0'); + } + hash.append(hex); + } + digest = hash.toString(); + } catch (UnsupportedEncodingException e) { + LOG.debug("UnsupportedEncodingException: " + e); + } catch (InvalidKeyException e) { + LOG.debug("InvalidKeyException: " + e); + } catch (NoSuchAlgorithmException e) { + LOG.debug("NoSuchAlgorithmException: " + e); + } + return digest; + } + + private static String createDigest(String accessKey, String accessKeySecret, Map fields) { + String digest = hmacDigest(accessKey, accessKeySecret + (String) fields.get("date") + (String) fields.get("noonce")); + LOG.trace("got hash: " + digest); + return digest; + } + + private Map createFields(final String input) { + final Map map = new HashMap(); + for (String pair : input.split("&")) { + String[] kv = pair.split("="); + map.put(kv[0], kv[1]); + } + return map; + } @Override public void initialize(TSDB tsdb) { LOG.debug("initialized authentication plugin"); + String correctUsername = tsdb.getConfig().getString("tsd.authentication.username"); + String correctSecret = tsdb.getConfig().getString("tsd.authentication.secret"); + authDB.put(correctUsername, correctSecret); this.tsdb = tsdb; } @@ -49,23 +107,63 @@ public void collectStats(StatsCollector collector) { } @Override - public Boolean authenticate(MessageEvent e) { - String username = tsdb.getConfig().getString("tsd.authentication.username"); - String secret = tsdb.getConfig().getString("tsd.authentication.secret"); - LOG.debug(e.getMessage().toString()); - LOG.debug(username + ":" + secret); - return authenticate(username, secret); + public Boolean authenticate(String[] command) { + Boolean ret = false; + // Command should be 'auth basic access_key access_secret' + if (command.length < 3 || command.length > 4) { + LOG.error("Invalid Authentication Command Length: " + Integer.toString(command.length)); + } else if (command[0].equals("auth")) { + if (command[1].equals(algo.trim().toLowerCase())) { + LOG.debug("Validating Digest"); + Map fields = createFields(command[3]); + ret = authenticate(command[2], fields); + } else if (command[1].equals("basic")) { + LOG.debug("Validating Credentials"); + ret = authenticate(command[2], command[3]); + } else { + LOG.error("Command not understood: " + command[0] + " " + command[1]); + } + } else { + LOG.error("Command is not auth: " + command[0]); + } + return ret; } @Override - public Boolean authenticate(String digest) { - return true; + public Boolean authenticate(String accessKey, Map fields) { + try { + String providedDigest = (String) fields.get("digest"); + LOG.debug("Authenticating " + accessKey + " " + providedDigest); + String date = (String) fields.get("date"); + String noonce = (String) fields.get("noonce"); + String secretKey = (String) authDB.get(accessKey); + String fullSecretKey = secretKey + date + noonce; + String calculatedDigest = hmacDigest(accessKey, fullSecretKey); + LOG.debug("Calc: " + calculatedDigest); + LOG.debug("Prov: " + providedDigest); + if (calculatedDigest.equals(providedDigest)) { + LOG.debug("Authentication Succeeded for: " + accessKey); + return true; + } else { + LOG.debug("Authentication Failed for: " + accessKey); + return false; + } + } catch (Exception e) { + LOG.error("Exception: " + e); + return false; + } } @Override public Boolean authenticate(String providedUsername, String providedSecret) { - String correctUsername = tsdb.getConfig().getString("tsd.authentication.username"); - String correctSecret = tsdb.getConfig().getString("tsd.authentication.secret"); - return (correctUsername == providedUsername && correctSecret == providedSecret); + String correctSecret = (String) authDB.get(providedUsername); + Boolean passwordMatched = correctSecret.equals(providedSecret); + if (passwordMatched) { + LOG.debug("Authentication Succeeded for: " + providedUsername); + return true; + } else { + LOG.debug("Authentication Failed for: " + providedUsername); + return false; + } } } diff --git a/src/logback.xml b/src/logback.xml index ff97a50889..c053292653 100644 --- a/src/logback.xml +++ b/src/logback.xml @@ -63,9 +63,10 @@ - + + - + diff --git a/src/opentsdb.conf b/src/opentsdb.conf index 11d2a911df..c5eaf9cb85 100644 --- a/src/opentsdb.conf +++ b/src/opentsdb.conf @@ -71,3 +71,9 @@ tsd.http.cachedir = # Compaction flush speed multiplier, default 2 # tsd.storage.compaction.flush_speed = 2 + +# --------- PLUGINS --------------------------------- +# Authentication Plugin +tsd.authentication.enable =true +tsd.authentication.plugin = net.opentsdb.auth.EmbeddedAuthenticationPlugin +tsd.startup.enable =false