Skip to content

Commit

Permalink
The TELNET api now supports basic and HMACSHA256 authentication. Ther…
Browse files Browse the repository at this point in the history
…e 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 OpenTSDB#4] AuthenticationChannelHandler: Setting up AuthenticationChannelHandler
2016-03-18 00:05:32,510 DEBUG [OpenTSDB I/O Worker OpenTSDB#4] AuthenticationChannelHandler: Passing auth command to Authentication Plugin
2016-03-18 00:05:32,510 DEBUG [OpenTSDB I/O Worker OpenTSDB#4] EmbeddedAuthenticationPlugin: Validating Credentials
2016-03-18 00:05:32,510 DEBUG [OpenTSDB I/O Worker OpenTSDB#4] EmbeddedAuthenticationPlugin: Authentication Succeeded for: admin
2016-03-18 00:05:32,511 INFO  [OpenTSDB I/O Worker OpenTSDB#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 tsuna#2] AuthenticationChannelHandler: Setting up AuthenticationChannelHandler
2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker tsuna#2] AuthenticationChannelHandler: Passing auth command to Authentication Plugin
2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker tsuna#2] EmbeddedAuthenticationPlugin: Validating Digest
2016-03-18 00:02:47,631 DEBUG [OpenTSDB I/O Worker tsuna#2] EmbeddedAuthenticationPlugin: Authenticating admin 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535
2016-03-18 00:02:47,631 TRACE [OpenTSDB I/O Worker tsuna#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 tsuna#2] EmbeddedAuthenticationPlugin: Generating HASH for admin
2016-03-18 00:02:47,632 DEBUG [OpenTSDB I/O Worker tsuna#2] EmbeddedAuthenticationPlugin: Calc: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535
2016-03-18 00:02:47,632 DEBUG [OpenTSDB I/O Worker tsuna#2] EmbeddedAuthenticationPlugin: Prov: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535
2016-03-18 00:02:47,632 DEBUG [OpenTSDB I/O Worker tsuna#2] EmbeddedAuthenticationPlugin: Authentication Succeeded for: admin
2016-03-18 00:02:47,632 INFO  [OpenTSDB I/O Worker tsuna#2] AuthenticationChannelHandler: Authentication Completed
2016-03-18 00:02:52,719 DEBUG [OpenTSDB I/O Worker tsuna#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 tsuna#3] AuthenticationChannelHandler: Setting up AuthenticationChannelHandler
2016-03-18 00:02:56,103 DEBUG [OpenTSDB I/O Worker tsuna#3] AuthenticationChannelHandler: Passing auth command to Authentication Plugin
2016-03-18 00:02:56,103 ERROR [OpenTSDB I/O Worker tsuna#3] EmbeddedAuthenticationPlugin: Invalid Authentication Command Length: 5
2016-03-18 00:02:57,798 DEBUG [OpenTSDB I/O Worker tsuna#3] AuthenticationChannelHandler: Passing auth command to Authentication Plugin
2016-03-18 00:02:57,798 ERROR [OpenTSDB I/O Worker tsuna#3] EmbeddedAuthenticationPlugin: Invalid Authentication Command Length: 5
2016-03-18 00:03:02,173 DEBUG [OpenTSDB I/O Worker tsuna#3] AuthenticationChannelHandler: Passing auth command to Authentication Plugin
2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker tsuna#3] EmbeddedAuthenticationPlugin: Validating Digest
2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker tsuna#3] EmbeddedAuthenticationPlugin: Authenticating admin 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535
2016-03-18 00:03:02,174 TRACE [OpenTSDB I/O Worker tsuna#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 tsuna#3] EmbeddedAuthenticationPlugin: Generating HASH for admin
2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker tsuna#3] EmbeddedAuthenticationPlugin: Calc: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535
2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker tsuna#3] EmbeddedAuthenticationPlugin: Prov: 6e833ce4ebdaa38b4e6f2473605a7d6d6709b6d63d2a38d0374237dee390c535
2016-03-18 00:03:02,174 DEBUG [OpenTSDB I/O Worker tsuna#3] EmbeddedAuthenticationPlugin: Authentication Succeeded for: admin
2016-03-18 00:03:02,174 INFO  [OpenTSDB I/O Worker tsuna#3] AuthenticationChannelHandler: Authentication Completed
2016-03-18 00:03:12,038 DEBUG [OpenTSDB I/O Worker tsuna#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.
  • Loading branch information
johann8384 committed Mar 18, 2016
1 parent 2c17fb0 commit 06ebf05
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 27 deletions.
61 changes: 57 additions & 4 deletions src/auth/AuthenticationChannelHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
13 changes: 4 additions & 9 deletions src/auth/AuthenticationPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
122 changes: 110 additions & 12 deletions src/auth/EmbeddedAuthenticationPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> createFields(final String input) {
final Map<String, String> map = new HashMap<String, String>();
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;
}

Expand All @@ -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;
}
}
}
5 changes: 3 additions & 2 deletions src/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@
<logger name="org.apache.zookeeper" level="INFO"/>
<logger name="org.hbase.async" level="INFO"/>
<logger name="com.stumbleupon.async" level="INFO"/>

<logger name="net.opentsdb.auth.EmbeddedAuthenticationPlugin" level="TRACE"/>

<!-- Fallthrough root logger and router -->
<root level="INFO">
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="CYCLIC"/>
<!-- Uncomment to log to file -->
Expand Down
6 changes: 6 additions & 0 deletions src/opentsdb.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 06ebf05

Please sign in to comment.