Compare commits

..
Author SHA1 Message Date
Bill Oley 142f5c8dfb More work 2019-02-04 15:39:03 -05:00
Bill Oley 46a424bcce Work in progress 2019-02-01 17:08:43 -05:00
Bill Oley a95d2d7ea1 Bump pom versions to 1.1.1-SNAPSHOT 2019-01-16 13:55:47 -05:00
Bill Oley f751175190 Update version of collectd 2019-01-16 13:53:43 -05:00
Bill Oley 648199f330 Revert "Bump pom versions to 1.1.1-SNAPSHOT"
This reverts commit fcfc3f7bc0.
2019-01-16 13:53:26 -05:00
Bill Oley fcfc3f7bc0 Bump pom versions to 1.1.1-SNAPSHOT 2019-01-15 15:48:43 -05:00
Bill Oley 200bc39902 Tag version 1.1.0
Grafana app changes
2019-01-15 15:47:14 -05:00
Bill Oley f854c9e394 Add build profile for python client package 2019-01-15 15:08:30 -05:00
Bill Oley 73ddae1f8d Correct typo 2019-01-15 14:15:20 -05:00
Bill Oley cb7184503e Allow Websocket requests to take advantage of cached metrics 2019-01-11 16:12:26 -05:00
Christopher Tubbs 3a357b50ef Update jackson-databind dependency
(Also, remove redundant test dependency on timely-common.)
2019-01-10 16:07:25 -05:00
Bill Oley 689aee0678 Fix deadlock in tracking nonCachedMetrics across processes 2019-01-08 12:06:41 -05:00
Bill Oley 32f6b9d4c9 Include the bin/testing directory in assembly 2019-01-04 11:14:11 -05:00
Bill Oley 512f318387 Add distributed tracking of nonCachedMetrics between the timely servers and the balancers
work in progress

Add distributed tracking of noncachedMetrics
2019-01-04 11:04:24 -05:00
93 changed files with 2538 additions and 549 deletions

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>timely-analytics</artifactId>
<!--
@@ -66,7 +66,7 @@ public class SubscriptionSource extends RichSourceFunction<MetricResponse> imple
LOG.info("Opening summarization job.");
super.open(parameters);
client = new WebSocketSubscriptionClient(jp.getTimelyHostname(), jp.getTimelyHttpsPort(), jp.getTimelyWssPort(),
jp.isDoLogin(), jp.getTimelyUsername(), jp.getTimelyPassword(), jp.getKeyStoreFile(),
jp.isClientAuth(), jp.isDoLogin(), jp.getTimelyUsername(), jp.getTimelyPassword(), jp.getKeyStoreFile(),
jp.getKeyStoreType(), jp.getKeyStorePass(), jp.getTrustStoreFile(), jp.getTrustStoreType(),
jp.getTrustStorePass(), jp.isHostVerificationEnabled(), jp.getBufferSize());
}
@@ -96,7 +96,7 @@ public class SubscriptionSource extends RichSourceFunction<MetricResponse> imple
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(new MessageHandler.Whole<String>() {
session.addMessageHandler(String.class, new MessageHandler.Whole<String>() {
private final SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd-HH");
private long lastWatermarkTime = 0L;
@@ -18,6 +18,7 @@ public class SummarizationJobParameters extends GlobalJobParameters implements S
private final int timelyTcpPort;
private final int timelyHttpsPort;
private final int timelyWssPort;
private final boolean clientAuth;
private final boolean doLogin;
private final String timelyUsername;
private final String timelyPassword;
@@ -40,6 +41,7 @@ public class SummarizationJobParameters extends GlobalJobParameters implements S
timelyTcpPort = params.getInt("timelyTcpPort", 4241);
timelyHttpsPort = params.getInt("timelyHttpsPort", 4242);
timelyWssPort = params.getInt("timelyWssPort", 4243);
clientAuth = params.getBoolean("clientAuth", false);
doLogin = params.getBoolean("doLogin", false);
timelyUsername = params.get("timelyUsername", null);
timelyPassword = params.get("timelyPassword", null);
@@ -69,6 +71,7 @@ public class SummarizationJobParameters extends GlobalJobParameters implements S
timelyTcpPort = conf.getInteger("timelyTcpPort", 4241);
timelyHttpsPort = conf.getInteger("timelyHttpsPort", 4242);
timelyWssPort = conf.getInteger("timelyWssPort", 4243);
clientAuth = conf.getBoolean("clientAuth", false);
doLogin = conf.getBoolean("doLogin", false);
timelyUsername = conf.getString("timelyUsername", null);
timelyPassword = conf.getString("timelyPassword", null);
@@ -112,6 +115,10 @@ public class SummarizationJobParameters extends GlobalJobParameters implements S
return timelyWssPort;
}
public boolean isClientAuth() {
return clientAuth;
}
public boolean isDoLogin() {
return doLogin;
}
+1 -1
View File
@@ -16,7 +16,7 @@
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
<beans:property name="users">
<util:map key-type="java.lang.String">
<beans:entry key="example.com">
<beans:entry key="CN=example.com">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>
+4 -3
View File
@@ -8,7 +8,8 @@ timely-balancer:
assignmentFile: /timely/balancer/assignments.csv
security:
session-max-age: 86400
allow-anonymous-access: true
allow-anonymous-http-access: true
allow-anonymous-ws-access: true
serverSsl:
certificate-file:
key-file:
@@ -31,7 +32,7 @@ timely-balancer:
nonCachedMetrics:
server:
ip: 127.0.0.1
tcp-port: 4242
tcpPort: 4242
numTcpPools: 1
tcpBufferSize: 100
tcpClientPool:
@@ -51,7 +52,7 @@ timely-balancer:
jmxEnabled: true
testOnBorrow: false
testOnReturn: false
udp-port: 4245
udpPort: 4245
udpClientPool:
minIdlePerKey: 0
maxIdlePerKey: 40
+1 -6
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>timely-balancer</artifactId>
<dependencies>
@@ -201,11 +201,6 @@
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>timely</groupId>
<artifactId>timely-common</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
+1 -1
View File
@@ -33,7 +33,7 @@
<directory>${project.basedir}/bin</directory>
<outputDirectory>bin</outputDirectory>
<includes>
<include>*</include>
<include>**/*</include>
</includes>
</fileSet>
<fileSet>
@@ -66,6 +66,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import timely.auth.AuthCache;
import timely.balancer.configuration.BalancerConfiguration;
import timely.balancer.configuration.ClientSsl;
import timely.balancer.configuration.SpringBootstrap;
@@ -91,7 +92,7 @@ import timely.netty.tcp.TcpDecoder;
import timely.netty.tcp.TcpVersionHandler;
import timely.netty.udp.UdpDecoder;
import timely.netty.udp.UdpPacketToByteBuf;
import timely.netty.websocket.WebSocketHttpCookieHandler;
import timely.netty.websocket.WebSocketFullRequestHandler;
import timely.netty.websocket.WebSocketRequestDecoder;
import timely.netty.websocket.subscription.WSTimelyExceptionHandler;
@@ -362,6 +363,7 @@ public class Balancer {
curatorFramework.start();
ensureZkPaths(curatorFramework, zkPaths);
AuthCache.setSessionMaxAge(balancerConfig.getSecurity());
final boolean useEpoll = useEpoll();
Class<? extends ServerSocketChannel> channelClass;
Class<? extends Channel> datagramChannelClass;
@@ -559,7 +561,7 @@ public class Balancer {
ch.pipeline().addLast("ssl", sslCtx.newHandler(ch.alloc()));
ch.pipeline().addLast("httpServer", new HttpServerCodec());
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(8192));
ch.pipeline().addLast("sessionExtractor", new WebSocketHttpCookieHandler(balancerConfig.getSecurity()));
ch.pipeline().addLast("sessionExtractor", new WebSocketFullRequestHandler());
ch.pipeline().addLast("idle-handler",
new IdleStateHandler(balancerConfig.getWebsocket().getTimeout(), 0, 0));
@@ -8,6 +8,7 @@ public class ClientSsl {
private String trustStoreFile;
private String trustStoreType;
private String trustStorePassword;
private boolean twoWaySsl;
public String getKeyFile() {
return keyFile;
@@ -55,6 +56,9 @@ public class ClientSsl {
public void setTrustStorePassword(String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}
public boolean isTwoWaySsl() {
return twoWaySsl;
}
}
@@ -7,20 +7,24 @@ import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import timely.balancer.configuration.BalancerConfiguration;
import timely.balancer.connection.TimelyBalancedHost;
public class HttpClientFactory implements KeyedPooledObjectFactory<TimelyBalancedHost, CloseableHttpClient> {
private final SSLContext sslContext;
private boolean twoWaySsl;
public HttpClientFactory(SSLContext sslContext) {
public HttpClientFactory(BalancerConfiguration balancerConfig, SSLContext sslContext) {
this.sslContext = sslContext;
this.twoWaySsl = balancerConfig.getSecurity().getClientSsl().isTwoWaySsl();
}
@Override
public PooledObject<CloseableHttpClient> makeObject(TimelyBalancedHost k) throws Exception {
BasicCookieStore cookieJar = new BasicCookieStore();
return new DefaultPooledObject<>(timely.client.http.HttpClient.get(this.sslContext, cookieJar, false));
return new DefaultPooledObject<>(
timely.client.http.HttpClient.get(this.sslContext, cookieJar, false, twoWaySsl));
}
@Override
@@ -13,7 +13,7 @@ public class HttpClientPool extends GenericKeyedObjectPool<TimelyBalancedHost, C
private static final Logger LOG = LoggerFactory.getLogger(HttpClientPool.class);
public HttpClientPool(BalancerConfiguration config, SSLContext sslContext) {
super(new HttpClientFactory(sslContext), config.getHttp().getHttpClientPool());
public HttpClientPool(BalancerConfiguration balancerConfig, SSLContext sslContext) {
super(new HttpClientFactory(balancerConfig, sslContext), balancerConfig.getHttp().getHttpClientPool());
}
}
@@ -13,12 +13,14 @@ public class WsClientFactory implements KeyedPooledObjectFactory<TimelyBalancedH
private final SSLContext sslContext;
private final boolean doLogin;
private final boolean twoWaySsl;
private BalancerConfiguration balancerConfig;
public WsClientFactory(BalancerConfiguration balancerConfig, SSLContext sslContext) {
this.sslContext = sslContext;
this.doLogin = balancerConfig.isLoginRequired();
this.balancerConfig = balancerConfig;
this.twoWaySsl = balancerConfig.getSecurity().getClientSsl().isTwoWaySsl();
}
@Override
@@ -26,7 +28,7 @@ public class WsClientFactory implements KeyedPooledObjectFactory<TimelyBalancedH
int bufferSize = balancerConfig.getWebsocket().getSubscriptionBatchSize() * 500;
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslContext, k.getHost(), k.getHttpPort(),
k.getWsPort(), doLogin, "", "", false, bufferSize);
k.getWsPort(), twoWaySsl, doLogin, "", "", false, bufferSize);
return new DefaultPooledObject<>(client);
}
@@ -12,6 +12,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Multimap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
@@ -33,15 +34,19 @@ import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StreamUtils;
import timely.api.request.AuthenticatedRequest;
import timely.api.request.MetricRequest;
import timely.api.request.timeseries.HttpRequest;
import timely.api.request.timeseries.QueryRequest;
import timely.api.response.TimelyException;
import timely.auth.util.HttpHeaderUtils;
import timely.auth.util.ProxiedEntityUtils;
import timely.balancer.configuration.BalancerConfiguration;
import timely.balancer.connection.TimelyBalancedHost;
import timely.balancer.connection.http.HttpClientPool;
import timely.balancer.resolver.MetricResolver;
import timely.netty.http.TimelyHttpHandler;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class HttpRelayHandler extends SimpleChannelInboundHandler<HttpRequest> implements TimelyHttpHandler {
@@ -89,10 +94,19 @@ public class HttpRelayHandler extends SimpleChannelInboundHandler<HttpRequest> i
k = metricResolver.getHostPortKey(metric);
client = httpClientPool.borrowObject(k);
List<Map.Entry<String, String>> headerList = request.headers().entries();
List<Header> relayedHeaderList = new ArrayList<>();
Multimap<String, String> headers = HttpHeaderUtils.toMultimap(request.headers());
if (msg instanceof AuthenticatedRequest) {
AuthenticatedRequest authenticatedRequest = (AuthenticatedRequest) msg;
TimelyAuthenticationToken token = authenticatedRequest.getToken();
if (token != null) {
if (token.getClientCert() != null) {
ProxiedEntityUtils.addProxyHeaders(headers, token.getClientCert());
}
}
}
for (Map.Entry<String, String> h : headerList) {
List<Header> relayedHeaderList = new ArrayList<>();
for (Map.Entry<String, String> h : headers.entries()) {
if (!h.getKey().equals(CONTENT_LENGTH)) {
relayedHeaderList.add(new BasicHeader(h.getKey(), h.getValue()));
}
@@ -143,7 +157,6 @@ public class HttpRelayHandler extends SimpleChannelInboundHandler<HttpRequest> i
this.sendHttpError(ctx, new TimelyException(HttpResponseStatus.METHOD_NOT_ALLOWED.code(),
"Method not allowed", "Method not allowed"));
} else {
// buf = JsonUtil.getObjectMapper().writeValueAsBytes(msg);
ByteArrayOutputStream baos = new MyByteArrayOutputStream();
StreamUtils.copy(relayedResponse.getEntity().getContent(), baos);
@@ -153,8 +166,6 @@ public class HttpRelayHandler extends SimpleChannelInboundHandler<HttpRequest> i
for (Header h : relayedResponse.getAllHeaders()) {
response.headers().add(h.getName(), h.getValue());
}
// response.headers().set(HttpHeaders.Names.CONTENT_TYPE,
// Constants.JSON_TYPE);
response.headers().set(CONTENT_LENGTH, response.content().readableBytes());
sendResponse(ctx, response);
}
@@ -1,12 +1,15 @@
package timely.balancer.netty.ws;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import com.google.common.collect.Multimap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
@@ -15,25 +18,46 @@ import io.netty.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.api.response.TimelyException;
import timely.auth.util.ProxiedEntityUtils;
import timely.client.websocket.ClientHandler;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class WsClientHandler extends ClientHandler {
private static final Logger LOG = LoggerFactory.getLogger(WsClientHandler.class);
private final ChannelHandlerContext ctx;
private final TimelyAuthenticationToken token;
private ScheduledFuture<?> ping;
public WsClientHandler(ChannelHandlerContext ctx, int pingRate) {
public WsClientHandler(ChannelHandlerContext ctx, TimelyAuthenticationToken token, int pingRate) {
this.ctx = ctx;
this.token = token;
this.ping = this.ctx.executor().scheduleAtFixedRate(() -> {
LOG.trace("Sending ping on channel {}", ctx.channel());
ctx.writeAndFlush(new PingWebSocketFrame());
}, pingRate, pingRate, TimeUnit.SECONDS);
}
@Override
public void beforeRequest(Map<String, List<String>> headers) {
Multimap<String, String> requestHeaders = token.getHttpHeaders();
if (token.getClientCert() != null) {
ProxiedEntityUtils.addProxyHeaders(requestHeaders, token.getClientCert());
}
for (String s : requestHeaders.keySet()) {
List<String> valueList = new ArrayList<>();
valueList.addAll(requestHeaders.get(s));
if (headers.containsKey(s)) {
// add pre-existing values
valueList.addAll(headers.get(s));
}
headers.put(s, valueList);
}
}
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler((MessageHandler.Whole<String>) message -> {
session.addMessageHandler(String.class, message -> {
ctx.writeAndFlush(new TextWebSocketFrame(message));
LOG.debug("Message received on Websocket session {}: {}", session.getId(), message);
});
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.google.common.collect.Multimap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
@@ -24,6 +25,7 @@ import timely.api.request.subscription.CreateSubscription;
import timely.api.request.subscription.RemoveSubscription;
import timely.api.request.subscription.SubscriptionRequest;
import timely.api.response.TimelyException;
import timely.auth.util.ProxiedEntityUtils;
import timely.balancer.configuration.BalancerConfiguration;
import timely.balancer.connection.TimelyBalancedHost;
import timely.balancer.connection.ws.WsClientPool;
@@ -64,6 +66,10 @@ public class WsRelayHandler extends SimpleChannelInboundHandler<SubscriptionRequ
String subscriptionId;
try {
String metric = null;
Multimap<String, String> headers = msg.getRequestHeaders();
ProxiedEntityUtils.addProxyHeaders(headers, msg.getToken().getClientCert());
if (msg instanceof CreateSubscription) {
CreateSubscription create = (CreateSubscription) msg;
final String currentSubscriptionId = create.getSubscriptionId();
@@ -118,7 +124,8 @@ public class WsRelayHandler extends SimpleChannelInboundHandler<SubscriptionRequ
if (hostClientHolder == null) {
k = metricResolver.getHostPortKey(metric);
client = wsClientPool.borrowObject(k);
client.open(new WsClientHandler(origContext, (balancerConfig.getWebsocket().getTimeout() / 2)));
client.open(new WsClientHandler(origContext, add.getToken(),
(balancerConfig.getWebsocket().getTimeout() / 2)));
metricToClientMap.put(metric, new WsClientHolder(k, client));
} else {
client = hostClientHolder.getClient();
@@ -72,7 +72,7 @@ public class BalancedMetricResolver implements MetricResolver {
private Random r = new Random();
final private HealthChecker healthChecker;
private Timer timer = new Timer("RebalanceTimer", true);
private Timer arrivalRateTimer = new Timer("AriivalRateTimerResolver", true);
private Timer arrivalRateTimer = new Timer("ArrivalRateTimerResolver", true);
private int roundRobinCounter = 0;
private Set<String> nonCachedMetrics = new HashSet<>();
private DistributedAtomicValue nonCachedMetricsIP;
@@ -105,17 +105,17 @@ public class BalancedMetricResolver implements MetricResolver {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception {
LOG.info("Handling assignments event {}. assignmentsLastUpdatedInHdfs:{}", event.getType().toString(),
LOG.debug("Handling assignments event {}. assignmentsLastUpdatedInHdfs:{}", event.getType().toString(),
new Date(assignmentsLastUpdatedInHdfs.get().postValue()));
if (event.getType().equals(TreeCacheEvent.Type.NODE_UPDATED)) {
long lastLocalUpdate = assignmentsLastUpdatedLocal.get();
long lastHdfsUpdate = assignmentsLastUpdatedInHdfs.get().postValue();
if (lastHdfsUpdate > lastLocalUpdate) {
LOG.info("Reading assignments from hdfs lastHdfsUpdate ({}) > lastLocalUpdate ({})",
LOG.debug("Reading assignments from hdfs lastHdfsUpdate ({}) > lastLocalUpdate ({})",
new Date(lastHdfsUpdate), new Date(lastLocalUpdate));
readAssignmentsFromHdfs();
} else {
LOG.info("Not reading assignments from hdfs lastHdfsUpdate ({}) <= lastLocalUpdate ({})",
LOG.debug("Not reading assignments from hdfs lastHdfsUpdate ({}) <= lastLocalUpdate ({})",
new Date(lastHdfsUpdate), new Date(lastLocalUpdate));
}
}
@@ -138,7 +138,7 @@ public class BalancedMetricResolver implements MetricResolver {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception {
if (event.getType().equals(TreeCacheEvent.Type.NODE_UPDATED)) {
LOG.info("Handling nonCachedMetricsIP event {}", event.getType().toString());
LOG.debug("Handling nonCachedMetricsIP event {}", event.getType().toString());
readNonCachedMetricsIP();
}
}
@@ -185,7 +185,7 @@ public class BalancedMetricResolver implements MetricResolver {
long lastLocalUpdate = assignmentsLastUpdatedLocal.get();
long lastHdfsUpdate = assignmentsLastUpdatedInHdfs.get().postValue();
if (lastLocalUpdate > lastHdfsUpdate) {
LOG.info("Writing assignments to hdfs lastLocalUpdate ({}) > lastHdfsUpdate ({})",
LOG.debug("Writing assignments to hdfs lastLocalUpdate ({}) > lastHdfsUpdate ({})",
new Date(lastLocalUpdate), new Date(lastHdfsUpdate));
writeAssigmentsToHdfs();
}
@@ -198,53 +198,44 @@ public class BalancedMetricResolver implements MetricResolver {
}
private void readNonCachedMetricsIP() {
try {
nonCachedMetricsLocalLock.readLock().lock();
Set<String> currentNonCachedMetricsDistributed;
try {
nonCachedMetricsIPRWLock.readLock().acquire();
byte[] currentNonCachedMetricsDistributedBytes = nonCachedMetricsIP.get().postValue();
if (currentNonCachedMetricsDistributedBytes == null) {
currentNonCachedMetricsDistributed = new TreeSet<>();
} else {
try {
currentNonCachedMetricsDistributed = SerializationUtils
.deserialize(currentNonCachedMetricsDistributedBytes);
} catch (Exception e) {
LOG.error(e.getMessage());
currentNonCachedMetricsDistributed = new TreeSet<>();
}
}
} finally {
nonCachedMetricsIPRWLock.readLock().release();
}
if (nonCachedMetrics.containsAll(currentNonCachedMetricsDistributed)) {
LOG.info("local nonCachedMetrics already contains {}", currentNonCachedMetricsDistributed);
} else {
nonCachedMetricsLocalLock.readLock().unlock();
nonCachedMetricsLocalLock.writeLock().lock();
currentNonCachedMetricsDistributed.removeAll(nonCachedMetrics);
LOG.info("Adding {} to local nonCachedMetrics", currentNonCachedMetricsDistributed);
nonCachedMetrics.addAll(currentNonCachedMetricsDistributed);
balancerLock.writeLock().lock();
Set<String> currentNonCachedMetricsDistributed = new TreeSet<>();
try {
nonCachedMetricsIPRWLock.readLock().acquire();
byte[] currentNonCachedMetricsDistributedBytes = nonCachedMetricsIP.get().postValue();
if (currentNonCachedMetricsDistributedBytes != null) {
try {
for (String m : currentNonCachedMetricsDistributed) {
metricToHostMap.remove(m);
}
} finally {
balancerLock.writeLock().unlock();
currentNonCachedMetricsDistributed = SerializationUtils
.deserialize(currentNonCachedMetricsDistributedBytes);
} catch (Exception e) {
LOG.error(e.getMessage());
currentNonCachedMetricsDistributed = new TreeSet<>();
}
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
} finally {
if (nonCachedMetricsLocalLock.isWriteLockedByCurrentThread()) {
nonCachedMetricsLocalLock.writeLock().unlock();
} else {
nonCachedMetricsLocalLock.readLock().unlock();
try {
nonCachedMetricsIPRWLock.readLock().release();
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
nonCachedMetricsLocalLock.writeLock().lock();
try {
if (nonCachedMetrics.containsAll(currentNonCachedMetricsDistributed)) {
LOG.debug("local nonCachedMetrics already contains {}", currentNonCachedMetricsDistributed);
} else {
currentNonCachedMetricsDistributed.removeAll(nonCachedMetrics);
LOG.debug("Adding {} to local nonCachedMetrics", currentNonCachedMetricsDistributed);
nonCachedMetrics.addAll(currentNonCachedMetricsDistributed);
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
} finally {
nonCachedMetricsLocalLock.writeLock().unlock();
}
}
private void startLeaderLatch(CuratorFramework curatorFramework) {
@@ -514,21 +505,21 @@ public class BalancedMetricResolver implements MetricResolver {
}
if (highHosts.isEmpty() && lowHosts.isEmpty()) {
LOG.info("Host's arrival rates are within {} of the average", controlBandPercentage);
LOG.debug("Host's arrival rates are within {} of the average", controlBandPercentage);
} else if (!highHosts.isEmpty() && !lowHosts.isEmpty()) {
LOG.info("begin rebalancing {}", BalanceType.HIGH_LOW);
LOG.debug("begin rebalancing {}", BalanceType.HIGH_LOW);
numReassigned = rebalance(highHosts, lowHosts, calculatedRates, averageArrivalRate,
BalanceType.HIGH_LOW);
LOG.info("end rebalancing {} - reassigned {}", BalanceType.HIGH_LOW, numReassigned);
LOG.debug("end rebalancing {} - reassigned {}", BalanceType.HIGH_LOW, numReassigned);
} else if (lowHosts.isEmpty()) {
LOG.info("begin rebalancing {}", BalanceType.HIGH_AVG);
LOG.debug("begin rebalancing {}", BalanceType.HIGH_AVG);
numReassigned = rebalance(highHosts, avgHosts, calculatedRates, averageArrivalRate,
BalanceType.HIGH_AVG);
LOG.info("end rebalancing {} - reassigned {}", BalanceType.HIGH_AVG, numReassigned);
LOG.debug("end rebalancing {} - reassigned {}", BalanceType.HIGH_AVG, numReassigned);
} else {
LOG.info("begin rebalancing {}", BalanceType.AVG_LOW);
LOG.debug("begin rebalancing {}", BalanceType.AVG_LOW);
numReassigned = rebalance(avgHosts, lowHosts, calculatedRates, averageArrivalRate, BalanceType.AVG_LOW);
LOG.info("end rebalancing {} - reassigned {}", BalanceType.AVG_LOW, numReassigned);
LOG.debug("end rebalancing {} - reassigned {}", BalanceType.AVG_LOW, numReassigned);
}
}
return numReassigned;
@@ -642,21 +633,13 @@ public class BalancedMetricResolver implements MetricResolver {
private void addNonCachedMetrics(Collection<String> nonCachedMetricsUpdate) {
if (!nonCachedMetricsUpdate.isEmpty()) {
try {
LOG.info("Adding {} to local nonCachedMetrics", nonCachedMetricsUpdate);
LOG.debug("Adding {} to local nonCachedMetrics", nonCachedMetricsUpdate);
nonCachedMetricsLocalLock.writeLock().lock();
try {
nonCachedMetrics.addAll(nonCachedMetricsUpdate);
} finally {
nonCachedMetricsLocalLock.writeLock().unlock();
}
balancerLock.writeLock().lock();
try {
for (String m : nonCachedMetricsUpdate) {
metricToHostMap.remove(m);
}
} finally {
balancerLock.writeLock().unlock();
}
try {
nonCachedMetricsIPRWLock.writeLock().acquire();
@@ -669,10 +652,10 @@ public class BalancedMetricResolver implements MetricResolver {
.deserialize(currentNonCachedMetricsDistributedBytes);
}
if (currentNonCachedMetricsIP.containsAll(nonCachedMetricsUpdate)) {
LOG.info("nonCachedMetricsIP already contains {}", nonCachedMetricsUpdate);
LOG.debug("nonCachedMetricsIP already contains {}", nonCachedMetricsUpdate);
} else {
nonCachedMetricsUpdate.removeAll(currentNonCachedMetricsIP);
LOG.info("Adding {} to nonCachedMetricsIP", nonCachedMetricsUpdate);
LOG.debug("Adding {} to nonCachedMetricsIP", nonCachedMetricsUpdate);
TreeSet<String> updateSet = new TreeSet<>();
updateSet.addAll(currentNonCachedMetricsIP);
updateSet.addAll(nonCachedMetricsUpdate);
@@ -737,7 +720,7 @@ public class BalancedMetricResolver implements MetricResolver {
try {
for (String r : nonCachedMetrics) {
if (metricName.matches(r)) {
LOG.info("Adding {} to list of non-cached metrics", metricName);
LOG.debug("Adding {} to list of non-cached metrics", metricName);
addNonCachedMetrics(Collections.singleton(metricName));
return false;
}
@@ -786,7 +769,7 @@ public class BalancedMetricResolver implements MetricResolver {
} else if (!tbh.isUp()) {
TimelyBalancedHost oldTbh = tbh;
tbh = getLeastUsedHost();
LOG.info("rebalancing from host that is down: reassigning metric {} from server {}:{} to {}:{}",
LOG.debug("rebalancing from host that is down: reassigning metric {} from server {}:{} to {}:{}",
metric, oldTbh.getHost(), oldTbh.getTcpPort(), tbh.getHost(), tbh.getTcpPort());
if (!balancerLock.isWriteLockedByCurrentThread()) {
balancerLock.readLock().unlock();
@@ -999,7 +982,7 @@ public class BalancedMetricResolver implements MetricResolver {
if (!assignmentsLastUpdatedInHdfs.get().succeeded()) {
assignmentsLastUpdatedInHdfs.forceSet(now);
}
LOG.info("Wrote {} assignments to hdfs lastHdfsUpdate = lastLocalUpdate ({})",
LOG.debug("Wrote {} assignments to hdfs lastHdfsUpdate = lastLocalUpdate ({})",
metricToHostMap.size(), new Date(assignmentsLastUpdatedLocal.get()));
}
} finally {
+41 -2
View File
@@ -4,12 +4,12 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>timely-client</artifactId>
<properties>
<!-- PEP 440 compliant version gets substituted into src/main/python/template/setup.py -->
<python.client.version>0.0.7.dev0</python.client.version>
<python.client.version>1.1.1.dev0</python.client.version>
</properties>
<dependencies>
<dependency>
@@ -107,4 +107,43 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>python</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>python-dist</id>
<goals>
<goal>exec</goal>
</goals>
<phase>package</phase>
<configuration>
<executable>python</executable>
<workingDirectory>${project.basedir}/src/main/python</workingDirectory>
<commandlineArgs>setup.py sdist --dist-dir ${project.build.directory}</commandlineArgs>
</configuration>
</execution>
<execution>
<id>python-clean</id>
<goals>
<goal>exec</goal>
</goals>
<phase>package</phase>
<configuration>
<executable>rm</executable>
<workingDirectory>${project.basedir}/src/main/python</workingDirectory>
<commandlineArgs>-rf timely.egg-info setup.py</commandlineArgs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
@@ -1,12 +1,13 @@
package timely.client.http;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
@@ -40,14 +41,25 @@ public class HttpClient {
}
public static CloseableHttpClient get(SSLContext ssl, CookieStore cookieStore, boolean hostVerificationEnabled) {
RequestConfig defaultRequestConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build();
return get(ssl, cookieStore, hostVerificationEnabled, false);
}
HttpClientBuilder builder = HttpClients.custom().setSSLContext(ssl).setDefaultCookieStore(cookieStore)
.setDefaultRequestConfig(defaultRequestConfig);
public static CloseableHttpClient get(SSLContext ssl, CookieStore cookieStore, boolean hostVerificationEnabled,
boolean clientAuth) {
RequestConfig defaultRequestConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build();
HostnameVerifier hostnameVerifier;
if (hostVerificationEnabled) {
builder.setSSLHostnameVerifier(new DefaultHostnameVerifier());
hostnameVerifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
} else {
builder.setSSLHostnameVerifier(new NoopHostnameVerifier());
hostnameVerifier = new NoopHostnameVerifier();
}
HttpClientBuilder builder = HttpClients.custom().setSSLContext(ssl).setDefaultCookieStore(cookieStore)
.setDefaultRequestConfig(defaultRequestConfig).setSSLHostnameVerifier(hostnameVerifier);
if (clientAuth) {
SSLConnectionSocketFactory sslCF = new SSLConnectionSocketFactory(ssl,
new String[] { "TLSv1.1", "TLSv1.2" }, null, hostnameVerifier);
builder.setSSLSocketFactory(sslCF);
}
return builder.build();
}
@@ -1,5 +1,8 @@
package timely.client.websocket;
import java.util.List;
import java.util.Map;
import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
@@ -35,4 +38,6 @@ public abstract class ClientHandler extends Endpoint {
LOG.error("Error occurred on Websocket session" + session.getId(), error);
}
public void beforeRequest(Map<String, List<String>> headers) {
}
}
@@ -14,13 +14,15 @@ import org.apache.http.cookie.Cookie;
public class TimelyEndpointConfig implements ClientEndpointConfig {
private String sessionCookie = null;
private final ClientHandler clientEndpoint;
private static final String FORMAT = "%s=%s;";
public TimelyEndpointConfig(Cookie sessionCookie) {
public TimelyEndpointConfig(ClientHandler clientEndpoint, Cookie sessionCookie) {
if (null != sessionCookie) {
this.sessionCookie = String.format(FORMAT, sessionCookie.getName(), sessionCookie.getValue());
}
this.clientEndpoint = clientEndpoint;
}
@Override
@@ -58,6 +60,7 @@ public class TimelyEndpointConfig implements ClientEndpointConfig {
if (null != sessionCookie) {
headers.put("Cookie", Collections.singletonList(sessionCookie));
}
TimelyEndpointConfig.this.clientEndpoint.beforeRequest(headers);
}
};
}
@@ -39,6 +39,7 @@ public class WebSocketClient implements AutoCloseable {
private final String timelyHostname;
private final int timelyHttpsPort;
private final int timelyWssPort;
private final boolean clientAuth;
private final boolean doLogin;
private final String timelyUsername;
private final String timelyPassword;
@@ -51,13 +52,14 @@ public class WebSocketClient implements AutoCloseable {
protected volatile boolean closed = true;
protected WebSocketClient(SSLContext ssl, String timelyHostname, int timelyHttpsPort, int timelyWssPort,
boolean doLogin, String timelyUsername, String timelyPassword, boolean hostVerificationEnabled,
int bufferSize) {
boolean clientAuth, boolean doLogin, String timelyUsername, String timelyPassword,
boolean hostVerificationEnabled, int bufferSize) {
this.ssl = ssl;
this.timelyHostname = timelyHostname;
this.timelyHttpsPort = timelyHttpsPort;
this.timelyWssPort = timelyWssPort;
this.doLogin = doLogin;
this.clientAuth = clientAuth;
this.timelyUsername = timelyUsername;
this.timelyPassword = timelyPassword;
this.hostVerificationEnabled = hostVerificationEnabled;
@@ -67,28 +69,28 @@ public class WebSocketClient implements AutoCloseable {
Preconditions.checkNotNull(timelyHttpsPort, "%s must be supplied", "Timely HTTPS port");
Preconditions.checkNotNull(timelyWssPort, "%s must be supplied", "Timely WSS port");
if (doLogin && ((StringUtils.isEmpty(timelyUsername) && !StringUtils.isEmpty(timelyPassword)
|| (!StringUtils.isEmpty(timelyUsername) && StringUtils.isEmpty(timelyPassword))))) {
throw new IllegalArgumentException("Both Timely username and password must be empty or non-empty");
if (doLogin && !clientAuth) {
if ((StringUtils.isEmpty(timelyUsername) && !StringUtils.isEmpty(timelyPassword)
|| (!StringUtils.isEmpty(timelyUsername) && StringUtils.isEmpty(timelyPassword)))) {
throw new IllegalArgumentException("Both Timely username and password must be empty or non-empty");
}
}
}
protected WebSocketClient(String timelyHostname, int timelyHttpsPort, int timelyWssPort, boolean doLogin,
String timelyUsername, String timelyPassword, String keyStoreFile, String keyStoreType, String keyStorePass,
String trustStoreFile, String trustStoreType, String trustStorePass, boolean hostVerificationEnabled,
int bufferSize) {
protected WebSocketClient(String timelyHostname, int timelyHttpsPort, int timelyWssPort, boolean clientAuth,
boolean doLogin, String timelyUsername, String timelyPassword, String keyStoreFile, String keyStoreType,
String keyStorePass, String trustStoreFile, String trustStoreType, String trustStorePass,
boolean hostVerificationEnabled, int bufferSize) {
this(HttpClient.getSSLContext(trustStoreFile, trustStoreType, trustStorePass, keyStoreFile, keyStoreType,
keyStorePass), timelyHostname, timelyHttpsPort, timelyWssPort, doLogin, timelyUsername, timelyPassword,
hostVerificationEnabled, bufferSize);
keyStorePass), timelyHostname, timelyHttpsPort, timelyWssPort, clientAuth, doLogin, timelyUsername,
timelyPassword, hostVerificationEnabled, bufferSize);
}
public void open(ClientHandler clientEndpoint) throws IOException, DeploymentException, URISyntaxException {
Cookie sessionCookie = null;
if (doLogin) {
BasicCookieStore cookieJar = new BasicCookieStore();
try (CloseableHttpClient client = HttpClient.get(ssl, cookieJar, hostVerificationEnabled)) {
try (CloseableHttpClient client = HttpClient.get(ssl, cookieJar, hostVerificationEnabled, clientAuth)) {
String target = "https://" + timelyHostname + ":" + timelyHttpsPort + "/login";
@@ -140,8 +142,8 @@ public class WebSocketClient implements AutoCloseable {
webSocketClient.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngine);
webSocketClient.getProperties().put(ClientProperties.INCOMING_BUFFER_SIZE, bufferSize);
String wssPath = "wss://" + timelyHostname + ":" + timelyWssPort + "/websocket";
session = webSocketClient.connectToServer(clientEndpoint, new TimelyEndpointConfig(sessionCookie),
new URI(wssPath));
session = webSocketClient.connectToServer(clientEndpoint,
new TimelyEndpointConfig(clientEndpoint, sessionCookie), new URI(wssPath));
final ByteBuffer pingData = ByteBuffer.allocate(0);
webSocketClient.getScheduledExecutorService().scheduleAtFixedRate(() -> {
@@ -27,21 +27,21 @@ public class WebSocketSubscriptionClient extends WebSocketClient {
private final String subscriptionId;
public WebSocketSubscriptionClient(SSLContext ssl, String timelyHostname, int timelyHttpsPort, int timelyWssPort,
boolean doLogin, String timelyUsername, String timelyPassword, boolean hostVerificationEnabled,
int bufferSize) {
super(ssl, timelyHostname, timelyHttpsPort, timelyWssPort, doLogin, timelyUsername, timelyPassword,
boolean clientAuth, boolean doLogin, String timelyUsername, String timelyPassword,
boolean hostVerificationEnabled, int bufferSize) {
super(ssl, timelyHostname, timelyHttpsPort, timelyWssPort, clientAuth, doLogin, timelyUsername, timelyPassword,
hostVerificationEnabled, bufferSize);
subscriptionId = UUID.randomUUID().toString();
LOG.trace("Created WebSocketClient with subscriptionId {}", this.subscriptionId);
}
public WebSocketSubscriptionClient(String timelyHostname, int timelyHttpsPort, int timelyWssPort, boolean doLogin,
String timelyUsername, String timelyPassword, String keyStoreFile, String keyStoreType, String keyStorePass,
String trustStoreFile, String trustStoreType, String trustStorePass, boolean hostVerificationEnabled,
int bufferSize) {
super(timelyHostname, timelyHttpsPort, timelyWssPort, doLogin, timelyUsername, timelyPassword, keyStoreFile,
keyStoreType, keyStorePass, trustStoreFile, trustStoreType, trustStorePass, hostVerificationEnabled,
bufferSize);
public WebSocketSubscriptionClient(String timelyHostname, int timelyHttpsPort, int timelyWssPort,
boolean clientAuth, boolean doLogin, String timelyUsername, String timelyPassword, String keyStoreFile,
String keyStoreType, String keyStorePass, String trustStoreFile, String trustStoreType,
String trustStorePass, boolean hostVerificationEnabled, int bufferSize) {
super(timelyHostname, timelyHttpsPort, timelyWssPort, clientAuth, doLogin, timelyUsername, timelyPassword,
keyStoreFile, keyStoreType, keyStorePass, trustStoreFile, trustStoreType, trustStorePass,
hostVerificationEnabled, bufferSize);
subscriptionId = UUID.randomUUID().toString();
LOG.trace("Created WebSocketClient with subscriptionId {}", this.subscriptionId);
}
@@ -2,7 +2,6 @@ import time
from datetime import datetime
from datetime import timedelta
from datetime import tzinfo
from pandas.tslib import Timedelta
import pandas
class TimeDateError(ValueError):
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>collectd-nsq-plugin</artifactId>
<dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>collectd-plugin-parent</artifactId>
<dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>collectd-timely-plugin</artifactId>
<dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>timely-common</artifactId>
<dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>timely-app</artifactId>
<packaging>pom</packaging>
+2 -2
View File
@@ -23,8 +23,8 @@
{"name": "Project site", "url": "https://github.com/NationalSecurityAgency/timely"},
{"name": "License & Terms", "url": "https://raw.githubusercontent.com/NationalSecurityAgency/timely/master/LICENSE"}
],
"version": "0.0.6-SNAPSHOT",
"updated": "2017-08-11"
"version": "1.1.0",
"updated": "2019-01-15"
},
"includes": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "timely-app",
"private": true,
"version": "0.0.2",
"version": "1.1.0",
"description": "",
"main": "index.js",
"scripts": {
+2 -2
View File
@@ -23,8 +23,8 @@
{"name": "Project site", "url": "https://github.com/NationalSecurityAgency/timely"},
{"name": "License & Terms", "url": "https://raw.githubusercontent.com/NationalSecurityAgency/timely/master/LICENSE"}
],
"version": "0.0.6-SNAPSHOT",
"updated": "2017-08-11"
"version": "1.1.0",
"updated": "2019-01-15"
},
"includes": [
+9 -4
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Timely</name>
<description>A secure and scalable time series database based on Apache Accumulo</description>
@@ -40,10 +40,10 @@
<os.detection.classifierWithLikes>fedora</os.detection.classifierWithLikes>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<version.accumulo>1.7.4</version.accumulo>
<version.accumulo>1.9.2</version.accumulo>
<version.buildnumber-maven>1.4</version.buildnumber-maven>
<version.caffeine>2.3.3</version.caffeine>
<version.collectd>5.5.1</version.collectd>
<version.collectd>5.7.1</version.collectd>
<version.commons-collections4>4.1</version.commons-collections4>
<version.commons-configuration>1.10</version.commons-configuration>
<version.commons-io>2.6</version.commons-io>
@@ -59,7 +59,7 @@
<version.hadoop>2.7.3</version.hadoop>
<version.httpcomponents>4.5.2</version.httpcomponents>
<version.impsort>1.2.0</version.impsort>
<version.jackson>2.9.7</version.jackson>
<version.jackson>2.9.8</version.jackson>
<version.jacoco>0.7.6.201602180812</version.jacoco>
<version.javacsv>2.0</version.javacsv>
<version.javassist>3.21.0-GA</version.javassist>
@@ -102,6 +102,11 @@
<artifactId>jackson-databind</artifactId>
<version>${version.jackson}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-guava</artifactId>
<version>${version.jackson}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
+1 -1
View File
@@ -16,7 +16,7 @@
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
<beans:property name="users">
<util:map key-type="java.lang.String">
<beans:entry key="example.com">
<beans:entry key="CN=example.com">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>
+2 -1
View File
@@ -18,7 +18,8 @@ timely:
max-downsample-memory: 10000000
security:
session-max-age: 86400
allow-anonymous-access: true
allow-anonymous-http-access: true
allow-anonymous-ws-access: true
ssl:
certificate-file: /timely-server-src/server/pki/timely.csr
key-file: /timely-server-src/server/pki/timely-pkcs8.key
+2 -1
View File
@@ -18,7 +18,8 @@ timely:
max-downsample-memory: 10000000
security:
session-max-age: 86400
allow-anonymous-access: true
allow-anonymous-http-access: true
allow-anonymous-ws-access: true
ssl:
certificate-file:
key-file:
+2 -1
View File
@@ -22,7 +22,8 @@ timely:
max-downsample-memory: 10000000
security:
session-max-age: 86400
allow-anonymous-access: true
allow-anonymous-http-access: true
allow-anonymous-ws-access: true
serverSsl:
certificate-file:
key-file:
+5 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>timely-server</artifactId>
<dependencies>
@@ -20,6 +20,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-guava</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
+1 -1
View File
@@ -33,7 +33,7 @@
<directory>${project.basedir}/bin</directory>
<outputDirectory>bin</outputDirectory>
<includes>
<include>*</include>
<include>**/*</include>
</includes>
</fileSet>
<fileSet>
+5 -4
View File
@@ -101,7 +101,7 @@ import timely.netty.udp.UdpDecoder;
import timely.netty.udp.UdpPacketToByteBuf;
import timely.netty.websocket.WSMetricPutHandler;
import timely.netty.websocket.WSVersionRequestHandler;
import timely.netty.websocket.WebSocketHttpCookieHandler;
import timely.netty.websocket.WebSocketFullRequestHandler;
import timely.netty.websocket.WebSocketRequestDecoder;
import timely.netty.websocket.subscription.WSAddSubscriptionRequestHandler;
import timely.netty.websocket.subscription.WSCloseSubscriptionRequestHandler;
@@ -430,7 +430,7 @@ public class Server {
// Initialize the MetaCache
MetaCacheFactory.getCache(config);
// initialize the auth cache
AuthCache.setSessionMaxAge(config);
AuthCache.setSessionMaxAge(config.getSecurity());
// Initialize the VisibilityCache
VisibilityCache.init(config);
final boolean useEpoll = useEpoll();
@@ -664,7 +664,7 @@ public class Server {
ch.pipeline().addLast("ssl", sslCtx.newHandler(ch.alloc()));
ch.pipeline().addLast("httpServer", new HttpServerCodec());
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(8192));
ch.pipeline().addLast("sessionExtractor", new WebSocketHttpCookieHandler(config.getSecurity()));
ch.pipeline().addLast("sessionExtractor", new WebSocketFullRequestHandler());
ch.pipeline().addLast("idle-handler", new IdleStateHandler(conf.getWebsocket().getTimeout(), 0, 0));
ch.pipeline().addLast("ws-protocol",
new WebSocketServerProtocolHandler(WS_PATH, null, true, 65536, true));
@@ -676,7 +676,8 @@ public class Server {
ch.pipeline().addLast("suggest", new WSSuggestRequestHandler(dataStore));
ch.pipeline().addLast("version", new WSVersionRequestHandler());
ch.pipeline().addLast("put", new WSMetricPutHandler(dataStore));
ch.pipeline().addLast("create", new WSCreateSubscriptionRequestHandler(dataStore, config));
ch.pipeline().addLast("create",
new WSCreateSubscriptionRequestHandler(dataStore, dataStoreCache, config));
ch.pipeline().addLast("add", new WSAddSubscriptionRequestHandler());
ch.pipeline().addLast("remove", new WSRemoveSubscriptionRequestHandler());
ch.pipeline().addLast("close", new WSCloseSubscriptionRequestHandler());
@@ -1,18 +1,22 @@
package timely.api.request;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import timely.auth.TimelyUser;
import timely.auth.util.HttpHeaderUtils;
import timely.netty.http.auth.TimelyAuthenticationToken;
/**
* Base class for requests that require authentication
*/
@JsonIgnoreProperties({ "userName", "requestHeader" })
public class AuthenticatedRequest implements Request {
private String sessionId = null;
private Map<String, String> requestHeaders = new HashMap<>();
private TimelyAuthenticationToken token = null;
private Multimap<String, String> requestHeaders = HashMultimap.create();
public String getSessionId() {
return sessionId;
@@ -22,12 +26,17 @@ public class AuthenticatedRequest implements Request {
this.sessionId = sessionId;
}
public void addHeaders(List<Entry<String, String>> headers) {
headers.forEach(l -> requestHeaders.put(l.getKey(), l.getValue()));
public void addHeaders(Multimap<String, String> headers) {
requestHeaders.putAll(headers);
}
public Map<String, String> getRequestHeaders() {
return Collections.unmodifiableMap(requestHeaders);
public Multimap<String, String> getRequestHeaders() {
return requestHeaders;
}
@JsonIgnore
public String getRequestHeader(String name) {
return HttpHeaderUtils.getSingleHeader(requestHeaders, name, false);
}
@Override
@@ -35,17 +44,32 @@ public class AuthenticatedRequest implements Request {
StringBuilder buf = new StringBuilder();
buf.append("{");
buf.append("Session ID: ").append(sessionId);
buf.append("Token: ").append(token.getTimelyPrincipal().getName());
buf.append(", Request Headers: ").append(requestHeaders.toString());
buf.append("}");
return buf.toString();
}
@Override
public void validate() {
Request.super.validate();
if (null == sessionId) {
throw new IllegalArgumentException("SessionID is null, must log in first");
}
public void setToken(TimelyAuthenticationToken token) {
this.token = token;
}
public TimelyAuthenticationToken getToken() {
return token;
}
@JsonIgnore
public String getUserName() {
String name;
try {
TimelyUser user = token.getTimelyPrincipal().getPrimaryUser();
name = user.getCommonName();
if (name == null) {
name = user.getName();
}
} catch (Exception e) {
name = "UNKNOWN";
}
return name;
}
}
+46 -34
View File
@@ -1,25 +1,27 @@
package timely.auth;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.api.request.AuthenticatedRequest;
import timely.api.request.Request;
import timely.api.response.TimelyException;
import timely.configuration.Configuration;
import timely.api.request.AuthenticatedWebSocketRequest;
import timely.auth.util.AuthorizationsUtil;
import timely.configuration.Security;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class AuthCache {
private static Cache<String, Authentication> CACHE = null;
private static Logger LOG = LoggerFactory.getLogger(AuthCache.class);
private static Cache<String, TimelyPrincipal> CACHE = null;
private static int sessionMaxAge = -1;
@@ -30,14 +32,14 @@ public class AuthCache {
sessionMaxAge = -1;
}
public static void setSessionMaxAge(Configuration config) {
public static void setSessionMaxAge(Security security) {
if (-1 != sessionMaxAge) {
throw new IllegalStateException("Cache session max age already configured.");
}
sessionMaxAge = config.getSecurity().getSessionMaxAge();
sessionMaxAge = security.getSessionMaxAge();
}
public static Cache<String, Authentication> getCache() {
public static Cache<String, TimelyPrincipal> getCache() {
if (-1 == sessionMaxAge) {
throw new IllegalStateException("Cache session max age not configured.");
}
@@ -47,35 +49,45 @@ public class AuthCache {
return CACHE;
}
public static Authorizations getAuthorizations(String sessionId) {
if (!StringUtils.isEmpty(sessionId)) {
Authentication auth = CACHE.asMap().get(sessionId);
if (null != auth) {
Collection<? extends GrantedAuthority> authorities = CACHE.asMap().get(sessionId).getAuthorities();
String[] auths = new String[authorities.size()];
final AtomicInteger i = new AtomicInteger(0);
authorities.forEach(a -> auths[i.getAndIncrement()] = a.getAuthority());
return new Authorizations(auths);
protected static Collection<Authorizations> getAuthorizations(String entityName) {
if (!StringUtils.isEmpty(entityName)) {
List<Authorizations> authorizationsList = new ArrayList<>();
TimelyPrincipal principal = CACHE.asMap().get(entityName);
if (principal != null) {
for (Collection<String> authCollection : principal.getAuthorizations()) {
authorizationsList.add(AuthorizationsUtil.toAuthorizations(authCollection));
}
LOG.debug("Authorizations for user {} {}", entityName, principal.getAuthorizationsString());
return authorizationsList;
} else {
return null;
return Collections.singletonList(Authorizations.EMPTY);
}
} else {
throw new IllegalArgumentException("session id cannot be null");
throw new IllegalArgumentException("entityName can not be null");
}
}
public static void enforceAccess(Security security, Request r) throws Exception {
if (!security.isAllowAnonymousAccess() && (r instanceof AuthenticatedRequest)) {
AuthenticatedRequest ar = (AuthenticatedRequest) r;
if (StringUtils.isEmpty(ar.getSessionId())) {
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
"Anonymous access is disabled, log in first");
}
if (!AuthCache.getCache().asMap().containsKey(ar.getSessionId())) {
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
"Unknown session id was submitted, log in again");
}
public static Collection<Authorizations> getAuthorizations(AuthenticatedRequest request, Security security) {
Collection<Authorizations> auths;
boolean anonAccessAllowed;
if (request instanceof AuthenticatedWebSocketRequest) {
anonAccessAllowed = security.isAllowAnonymousWsAccess();
} else {
anonAccessAllowed = security.isAllowAnonymousHttpAccess();
}
TimelyAuthenticationToken token = request.getToken();
String sessionId = request.getSessionId();
if (StringUtils.isBlank(sessionId) && token.getClientCert() == null) {
if (anonAccessAllowed) {
auths = Collections.singletonList(Authorizations.EMPTY);
} else {
throw new IllegalArgumentException("User must provide either a sessionId or a client certificate");
}
} else if (StringUtils.isNotBlank(sessionId)) {
auths = getAuthorizations(sessionId);
} else {
auths = getAuthorizations(token.getTimelyPrincipal().getName());
}
return auths;
}
}
@@ -1,16 +1,31 @@
package timely.auth;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceConfigurationError;
import com.google.common.collect.Multimap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.ssl.SslHandler;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
import timely.api.request.AuthenticatedRequest;
import timely.api.response.TimelyException;
import timely.auth.util.AuthenticationUtils;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class AuthenticationService {
private static Logger LOG = LoggerFactory.getLogger(AuthenticationService.class);
private static ApplicationContext springContext = null;
private static AuthenticationManager authManager = null;
private static SubjectDnX509PrincipalExtractor x509 = null;
@@ -26,12 +41,98 @@ public class AuthenticationService {
}
}
public static AuthenticationManager getAuthenticationManager() {
private static AuthenticationManager getAuthenticationManager() {
return authManager;
}
public static String extractDN(X509Certificate clientCert) throws Exception {
return (String) x509.extractPrincipal(clientCert);
public static TimelyAuthenticationToken authenticate(Authentication authentication) {
return authenticate(authentication, null);
}
public static TimelyAuthenticationToken authenticate(Authentication authentication, X509Certificate clientCert) {
Authentication token = getAuthenticationManager().authenticate(authentication);
return AuthenticationUtils.getTimelyAuthenticationToken(token, clientCert);
}
public static void enforceAccess(AuthenticatedRequest request) throws Exception {
String sessionId = request.getSessionId();
X509Certificate clientCert = request.getToken().getClientCert();
if (StringUtils.isBlank(sessionId) && clientCert == null) {
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
"Anonymous access is disabled. User must either send a client certificate or log in");
} else if (StringUtils.isNotBlank(sessionId)) {
if (!AuthCache.getCache().asMap().containsKey(sessionId)) {
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "User must log in",
"Unknown session id was submitted, log in again");
}
} else {
try {
TimelyAuthenticationToken token = request.getToken();
TimelyPrincipal requestPrincipal = token.getTimelyPrincipal();
if (!AuthCache.getCache().asMap().containsKey(requestPrincipal.getName())) {
List<TimelyUser> authenticatedTimelyUsers = new ArrayList<>();
for (TimelyUser user : requestPrincipal.getProxiedUsers()) {
SubjectIssuerDNPair p = user.getDn();
TimelyAuthenticationToken entityToken = getAuthenticationToken(clientCert, p.subjectDN(),
p.issuerDN());
TimelyPrincipal entityPrincipal = AuthCache.getCache().asMap()
.get(entityToken.getTimelyPrincipal().getName());
if (entityPrincipal == null) {
TimelyAuthenticationToken t = authenticate(entityToken, clientCert);
entityPrincipal = t.getTimelyPrincipal();
AuthCache.getCache().asMap().put(entityPrincipal.getName(), entityPrincipal);
}
authenticatedTimelyUsers.addAll(entityPrincipal.getProxiedUsers());
}
TimelyPrincipal principal = new TimelyPrincipal(authenticatedTimelyUsers);
AuthCache.getCache().asMap().put(principal.getName(), principal);
LOG.debug("Authenticated user {} for request {} with authorizations {}", principal.getName(),
request.toString(), principal.getAuthorizationsString());
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Access denied", e.getMessage());
}
}
}
public static X509Certificate getClientCertificate(ChannelHandlerContext ctx) {
X509Certificate clientCert = null;
if (ctx != null) {
SslHandler sslHandler = (SslHandler) ctx.channel().pipeline().get("ssl");
if (null != sslHandler) {
try {
clientCert = (X509Certificate) sslHandler.engine().getSession().getPeerCertificates()[0];
} catch (Exception e) {
}
} else {
throw new IllegalStateException("The expected SSL handler is not in the pipeline.");
}
}
return clientCert;
}
public static TimelyAuthenticationToken getAuthenticationToken(X509Certificate clientCert,
Multimap<String, String> headers) {
TimelyAuthenticationToken authenticationToken = null;
try {
authenticationToken = new TimelyAuthenticationToken(clientCert.getSubjectDN().getName(), clientCert,
headers);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
return authenticationToken;
}
public static TimelyAuthenticationToken getAuthenticationToken(X509Certificate clientCert, String subjectDn,
String issuerDn) {
TimelyAuthenticationToken authenticationToken = null;
try {
authenticationToken = new TimelyAuthenticationToken(subjectDn, issuerDn, clientCert);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
return authenticationToken;
}
}
@@ -0,0 +1,93 @@
package timely.auth;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.accumulo.core.security.Authorizations;
/**
* @see AuthorizationsMinimizer#minimize(Collection)
*/
public class AuthorizationsMinimizer {
/**
* Given a collection of Accumulo {@link Authorizations} objects, this method
* attempts to compute a "minimum" set that can be used for a multi-entity scan
* of data from Accumulo. The idea is that there is a call chain with multiple
* entities (e.g., a middleware broker acting on behalf of a GUI that is in turn
* acting on behalf of a user). In order to ensure that only data visible to all
* of the entities is returned, we must add a visibility filter for each entity
* in the chain to ensure that each key/value pair returned by Accumulo can be
* seen by all of the entities in the chain. Since we may be scanning a very
* large number of keys, it would be ideal if we could reduce the number of
* visibility filters to a small set if that can be done safely.
* <p>
* The crux of the problem, and the reason we need multiple filters in the first
* place (as opposed to just intersecting the authorizations for all all
* entities and using that set), is the fact that Accumulo visibility
* expressions may include disjunctions. Consider the following visibility
* expression.
* <p>
* {@code (A & B & ORG1) | (A & C & ORG2)}
* <p>
* If the incoming user has the Accumulo authorizations A, B, and ORG1 and the
* user is coming through a server with the Accumulo authorizations A, C, and
* ORG2, then the server is allowed to see data protected with this expression
* and the user is also allowed, so the data should be returned with those two
* calling entities. If we simply computed an intersection of the authorizations
* that set would be A. That set of authorizations does not match the visibility
* expression and data that should be returned would not be.
* <p>
* Given that we must apply multiple visibility filters in order to properly
* return data for a calling entity chain, it is desirable to minimize the
* number of visibility filters used, if possible. If a set of authorizations is
* a superset of any other set of authorizations, then it does not need to be
* tested. As long as we are not changing any of the authorizations sets, we
* know we will never allow any data to be returned that would otherwise not be.
* By removing authorizations sets, we might only prevent the return of data
* that should otherwise be returned. Consider a given set of authorizations
* that is a subset of another. If the subset passes a visibility expression,
* then the superset will too since it contains at least the same
* authorizations, so there is no need to test against it. If the subset does
* not pass, then the data is not visible to the chain anyway, so there is no
* point in testing the superset.
*
* @param authorizations
* the list of Authorizations for each entity in the call chain
* @return a minimized set of Authorizations that allows visibility of exactly
* the same data as {@code authorizations}
*/
public static Collection<Authorizations> minimize(Collection<Authorizations> authorizations) {
if (authorizations.size() > 1) {
// Convert collection of Authorizations into a collection of String sets (the
// individual authorizations).
// Since we are adding to a LinkedHashSet, this will de-dupe any duplicate
// authorization sets.
final LinkedHashSet<Set<String>> allAuths = authorizations.stream().map(
a -> a.getAuthorizations().stream().map(String::new).collect(Collectors.toCollection(HashSet::new)))
.collect(Collectors.toCollection(LinkedHashSet::new));
// Go through the authorizations sets and remove any that are supersets of any
// other.
for (Iterator<Set<String>> it = allAuths.iterator(); it.hasNext(); /* empty */) {
Set<String> currentSet = it.next();
if (allAuths.stream().filter(a -> a != currentSet && a.size() <= currentSet.size())
.anyMatch(currentSet::containsAll))
it.remove();
}
// If we removed any sets of authorizations, then we need to convert the reduced
// set from
// TreeSet<String> objects back into Authorizations objects.
if (allAuths.size() < authorizations.size()) {
authorizations = allAuths.stream().map(a -> new Authorizations(a.toArray(new String[0])))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
}
return authorizations;
}
}
@@ -0,0 +1,76 @@
package timely.auth;
import java.io.IOException;
import java.util.Map;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.iterators.IteratorEnvironment;
import org.apache.accumulo.core.iterators.OptionDescriber;
import org.apache.accumulo.core.iterators.SortedKeyValueIterator;
import org.apache.accumulo.core.iterators.WrappingIterator;
import org.apache.accumulo.core.iterators.system.VisibilityFilter;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.log4j.Logger;
public class ConfigurableVisibilityFilter extends WrappingIterator implements OptionDescriber {
public static final String AUTHORIZATIONS_OPT = "authorizations";
private SortedKeyValueIterator<Key, Value> delegate;
private static final Logger log = Logger.getLogger(ConfigurableVisibilityFilter.class);
public ConfigurableVisibilityFilter() {
}
public ConfigurableVisibilityFilter(SortedKeyValueIterator<Key, Value> iterator, Authorizations authorizations,
byte[] defaultVisibility) {
setSource(iterator);
}
public ConfigurableVisibilityFilter(ConfigurableVisibilityFilter other, IteratorEnvironment env) {
this.delegate = other.delegate.deepCopy(env);
this.setSource(delegate);
}
@Override
public void init(SortedKeyValueIterator<Key, Value> source, Map<String, String> options, IteratorEnvironment env)
throws IOException {
Authorizations auths = Authorizations.EMPTY;
if (options.containsKey(AUTHORIZATIONS_OPT))
auths = new Authorizations(options.get(AUTHORIZATIONS_OPT).split(","));
log.debug("Using authorizations: " + auths);
delegate = VisibilityFilter.wrap(source, auths, new byte[0]);
super.init(delegate, options, env);
}
@Override
public SortedKeyValueIterator<Key, Value> deepCopy(IteratorEnvironment env) {
return new ConfigurableVisibilityFilter(this, env);
}
@Override
public IteratorOptions describeOptions() {
IteratorOptions opts = new IteratorOptions(getClass().getSimpleName(),
"Filters keys based to return only those whose visibility tests positive against the supplied authorizations",
null, null);
opts.addNamedOption(AUTHORIZATIONS_OPT, "Comma delimited list of scan authorizations");
return opts;
}
@Override
public boolean validateOptions(Map<String, String> options) {
boolean valid = false;
String auths = options.get(AUTHORIZATIONS_OPT);
if (auths != null) {
try {
new Authorizations(auths.split(","));
valid = true;
} catch (Exception e) {
// ignore
}
}
return valid;
}
}
@@ -0,0 +1,257 @@
package timely.auth;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.Lists;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.IteratorSetting.Column;
import org.apache.accumulo.core.client.ScannerBase;
import org.apache.accumulo.core.client.impl.ScannerOptions;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.data.thrift.IterInfo;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.hadoop.io.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A simple wrapper around a {@link ScannerBase} that overrides the methods that
* configure iterators.
*
* @see ScannerOptionsHelper
*/
public class ScannerBaseDelegate implements ScannerBase {
private static final Logger log = LoggerFactory.getLogger(ScannerBaseDelegate.class);
private static final String SYSTEM_ITERATOR_NAME_PREFIX = "sys_";
protected final ScannerBase delegate;
public ScannerBaseDelegate(ScannerBase delegate) {
this.delegate = delegate;
}
@Override
public void addScanIterator(IteratorSetting cfg) {
if (cfg.getName().startsWith(SYSTEM_ITERATOR_NAME_PREFIX)) {
throw new IllegalArgumentException(
"Non-system iterators' names cannot start with " + SYSTEM_ITERATOR_NAME_PREFIX);
} else {
delegate.addScanIterator(cfg);
}
}
/**
* Adds a "system" scan iterator. The iterator name is automatically prefixed
* with {@link #SYSTEM_ITERATOR_NAME_PREFIX}. A "system" scan iterator can only
* be modified or removed by calling
* {@link #updateSystemScanIteratorOption(String, String, String)},
* {@link #removeSystemScanIterator(String)}, or
* {@link #clearSystemScanIterators()}. Updates the iterator configuration for
* {@code iteratorName}. The iterator name is automatically prefixed with
* {@link #SYSTEM_ITERATOR_NAME_PREFIX}.
*
* @param cfg
* the configuration of the iterator to add
*/
public void addSystemScanIterator(IteratorSetting cfg) {
if (!cfg.getName().startsWith(SYSTEM_ITERATOR_NAME_PREFIX)) {
cfg.setName(SYSTEM_ITERATOR_NAME_PREFIX + cfg.getName());
}
delegate.addScanIterator(cfg);
}
@Override
public void removeScanIterator(String iteratorName) {
if (iteratorName.startsWith(SYSTEM_ITERATOR_NAME_PREFIX)) {
throw new IllegalArgumentException("DATAWAVE system iterator " + iteratorName + " cannot be removed");
} else {
delegate.removeScanIterator(iteratorName);
}
}
/**
* Removes a "system" scan iterator. The iterator name is automatically prefixed
* with {@link #SYSTEM_ITERATOR_NAME_PREFIX}.
*
* @param iteratorName
* the name of the system iterator to remove
*/
public void removeSystemScanIterator(String iteratorName) {
if (!iteratorName.startsWith(SYSTEM_ITERATOR_NAME_PREFIX)) {
iteratorName = SYSTEM_ITERATOR_NAME_PREFIX + iteratorName;
}
delegate.removeScanIterator(iteratorName);
}
@Override
public void updateScanIteratorOption(String iteratorName, String key, String value) {
if (iteratorName.startsWith(SYSTEM_ITERATOR_NAME_PREFIX)) {
throw new IllegalArgumentException("DATAWAVE system iterator " + iteratorName + " cannot be updated");
} else {
delegate.updateScanIteratorOption(iteratorName, key, value);
}
}
/**
* Updates the iterator configuration for {@code iteratorName}. The iterator
* name is automatically prefixed with {@link #SYSTEM_ITERATOR_NAME_PREFIX}.
*
* @param iteratorName
* the name of the system iterator to modify
* @param key
* the name of the iterator option to modify
* @param value
* the new value for the iterator option named in {@code key}
*/
public void updateSystemScanIteratorOption(String iteratorName, String key, String value) {
if (!iteratorName.startsWith(SYSTEM_ITERATOR_NAME_PREFIX)) {
iteratorName = SYSTEM_ITERATOR_NAME_PREFIX + iteratorName;
}
delegate.updateScanIteratorOption(iteratorName, key, value);
}
@Override
public void fetchColumnFamily(Text col) {
delegate.fetchColumnFamily(col);
}
@Override
public void fetchColumn(Text colFam, Text colQual) {
delegate.fetchColumn(colFam, colQual);
}
@Override
public void fetchColumn(Column column) {
delegate.fetchColumn(column);
}
@Override
public void clearColumns() {
delegate.clearColumns();
}
@Override
public void clearScanIterators() {
if (delegate instanceof ScannerOptions) {
ScannerOptionsHelper opts = new ScannerOptionsHelper((ScannerOptions) delegate);
for (IteratorSetting iteratorSetting : opts.getIterators()) {
if (!iteratorSetting.getName().startsWith(SYSTEM_ITERATOR_NAME_PREFIX)) {
delegate.removeScanIterator(iteratorSetting.getName());
}
}
} else {
throw new UnsupportedOperationException(
"Cannot clear scan iterators on a non-ScannerOptions class! (" + delegate.getClass() + ")");
}
}
/**
* Clears all iterators (including system iterators).
*/
public void clearSystemScanIterators() {
delegate.clearScanIterators();
}
@Override
public Iterator<Map.Entry<Key, Value>> iterator() {
return delegate.iterator();
}
@Override
public void setTimeout(long timeOut, TimeUnit timeUnit) {
delegate.setTimeout(timeOut, timeUnit);
}
@Override
public long getTimeout(TimeUnit timeUnit) {
return delegate.getTimeout(timeUnit);
}
@Override
public void close() {
delegate.close();
}
@Override
public Authorizations getAuthorizations() {
return delegate.getAuthorizations();
}
@Override
public void setSamplerConfiguration(SamplerConfiguration samplerConfiguration) {
delegate.setSamplerConfiguration(samplerConfiguration);
}
@Override
public SamplerConfiguration getSamplerConfiguration() {
return delegate.getSamplerConfiguration();
}
@Override
public void clearSamplerConfiguration() {
delegate.clearSamplerConfiguration();
}
@Override
public void setBatchTimeout(long l, TimeUnit timeUnit) {
delegate.setBatchTimeout(l, timeUnit);
}
@Override
public long getBatchTimeout(TimeUnit timeUnit) {
return delegate.getBatchTimeout(timeUnit);
}
@Override
public void setClassLoaderContext(String s) {
delegate.setClassLoaderContext(s);
}
@Override
public void clearClassLoaderContext() {
delegate.clearClassLoaderContext();
}
@Override
public String getClassLoaderContext() {
return delegate.getClassLoaderContext();
}
public void setContext(String context) {
delegate.setClassLoaderContext(context);
}
public void clearContext() {
delegate.clearClassLoaderContext();
}
public String getContext() {
return delegate.getClassLoaderContext();
}
private static class ScannerOptionsHelper extends ScannerOptions {
public ScannerOptionsHelper(ScannerOptions other) {
super(other);
}
public Collection<IteratorSetting> getIterators() {
Collection<IteratorSetting> settings = Lists.newArrayList();
for (IterInfo iter : serverSideIteratorList) {
IteratorSetting setting = new IteratorSetting(iter.getPriority(), iter.getIterName(),
iter.getClassName());
setting.addOptions(serverSideIteratorOptions.get(iter.getIterName()));
settings.add(setting);
}
return settings;
}
}
}
@@ -0,0 +1,80 @@
package timely.auth;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import timely.auth.util.ProxiedEntityUtils;
/**
* A simple pair containing a subject and (optional) issuer DN. The supplied DN
* values are normalized into a lower-case form with the CN portion first.
*/
public class SubjectIssuerDNPair implements Serializable {
private final String subjectDN;
private final String issuerDN;
public static SubjectIssuerDNPair of(String subjectDN) {
return new SubjectIssuerDNPair(subjectDN, null);
}
@JsonCreator
public static SubjectIssuerDNPair of(@JsonProperty("subjectDN") String subjectDN,
@JsonProperty("issuerDN") String issuerDN) {
return new SubjectIssuerDNPair(subjectDN, issuerDN);
}
public static SubjectIssuerDNPair parse(String dn) {
String[] dns = ProxiedEntityUtils.splitProxiedSubjectIssuerDNs(dn);
if (dns.length != 2)
throw new IllegalArgumentException(dn + " must contain a single subject and issuer DN");
return new SubjectIssuerDNPair(dns[0], dns[1]);
}
protected SubjectIssuerDNPair(String subjectDN, String issuerDN) {
this.subjectDN = ProxiedEntityUtils.normalizeDN(subjectDN);
if (issuerDN != null) {
this.issuerDN = ProxiedEntityUtils.normalizeDN(issuerDN);
} else {
this.issuerDN = null;
}
}
@JsonGetter
public String subjectDN() {
return subjectDN;
}
@JsonGetter
public String issuerDN() {
return issuerDN;
}
@Override
public String toString() {
return issuerDN == null ? subjectDN + "<>" : ProxiedEntityUtils.buildProxiedDN(subjectDN, issuerDN);
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
SubjectIssuerDNPair that = (SubjectIssuerDNPair) o;
if (!subjectDN.equals(that.subjectDN))
return false;
return issuerDN != null ? issuerDN.equals(that.issuerDN) : that.issuerDN == null;
}
@Override
public int hashCode() {
int result = subjectDN.hashCode();
result = 31 * result + (issuerDN != null ? issuerDN.hashCode() : 0);
return result;
}
}
@@ -0,0 +1,150 @@
package timely.auth;
import static timely.auth.TimelyUser.ANONYMOUS_USER;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import timely.auth.util.DnUtils;
/**
* A {@link Principal} that represents a set of proxied {@link TimelyUser}s. For
* example, this proxied user could represent a GUI server acting on behalf of a
* user. The GUI server user represents the entity that made the call to us and
* the other proxied user would be the actual end user.
*/
@XmlRootElement
@XmlType(factoryMethod = "anonymousPrincipal", propOrder = { "name", "proxiedUsers", "creationTime" })
@XmlAccessorType(XmlAccessType.NONE)
public class TimelyPrincipal implements Principal, Serializable {
private final String username;
private final TimelyUser primaryUser;
@XmlElement
private final Set<TimelyUser> proxiedUsers = new LinkedHashSet<>();
@XmlElement
private final long creationTime;
/**
* This constructor should not be used. It is here to allow JAX-B mapping and
* CDI proxying of this class.
*/
public TimelyPrincipal() {
this(ANONYMOUS_USER);
}
public TimelyPrincipal(TimelyUser timelyUser) {
this.creationTime = System.currentTimeMillis();
this.primaryUser = timelyUser;
this.proxiedUsers.add(timelyUser);
this.username = timelyUser.getName();
}
public TimelyPrincipal(Collection<TimelyUser> proxiedUsers) {
this(proxiedUsers, System.currentTimeMillis());
}
public TimelyPrincipal(Collection<TimelyUser> proxiedUsers, long creationTime) {
this.creationTime = creationTime;
TimelyUser first = proxiedUsers.stream().findFirst().orElse(null);
this.primaryUser = proxiedUsers.stream().filter(u -> u.getUserType() == TimelyUser.UserType.USER).findFirst()
.orElse(first);
this.proxiedUsers.add(first);
this.proxiedUsers
.addAll(proxiedUsers.stream().filter(u -> !u.equals(primaryUser)).collect(Collectors.toList()));
this.username = this.proxiedUsers.stream().map(TimelyUser::getName).collect(Collectors.joining(" -> "));
}
public Set<TimelyUser> getProxiedUsers() {
return Collections.unmodifiableSet(proxiedUsers);
}
public TimelyUser getPrimaryUser() {
return primaryUser;
}
public Collection<? extends Collection<String>> getAuthorizations() {
return Collections
.unmodifiableCollection(proxiedUsers.stream().map(TimelyUser::getAuths).collect(Collectors.toList()));
}
public String[] getDNs() {
return proxiedUsers.stream().map(TimelyUser::getDn).map(SubjectIssuerDNPair::subjectDN).toArray(String[]::new);
}
public long getCreationTime() {
return creationTime;
}
@Override
@XmlElement
public String getName() {
return username;
}
public String getShortName() {
return DnUtils.getShortName(getPrimaryUser().getName());
}
public SubjectIssuerDNPair getUserDN() {
return getPrimaryUser().getDn();
}
public Set<String> getProxyServers() {
// @formatter:off
Set<String> proxyServers = getProxiedUsers().stream()
.filter(u -> u.getUserType() == TimelyUser.UserType.SERVER)
.map(TimelyUser::getDn)
.map(SubjectIssuerDNPair::subjectDN)
.collect(Collectors.toSet());
// @formatter:on
return proxyServers.isEmpty() ? null : proxyServers;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
TimelyPrincipal that = (TimelyPrincipal) o;
if (!username.equals(that.username))
return false;
return proxiedUsers.equals(that.proxiedUsers);
}
@Override
public int hashCode() {
int result = username.hashCode();
result = 31 * result + proxiedUsers.hashCode();
return result;
}
@Override
public String toString() {
return "TimelyPrincipal{" + "name='" + username + "'" + ", proxiedUsers=" + proxiedUsers + "}";
}
public static TimelyPrincipal anonymousPrincipal() {
return new TimelyPrincipal(ANONYMOUS_USER);
}
public String getAuthorizationsString() {
return proxiedUsers.stream().map(TimelyUser::getAuths).map(a -> a.toString())
.collect(Collectors.joining(" -> ", "[", "]"));
}
}
@@ -0,0 +1,139 @@
package timely.auth;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import timely.auth.util.ProxiedEntityUtils;
/**
* A user of a TIMELY service. Typically, one or more of these users (a chain
* where a user called an intermediate service which in turn called us) is
* represented with a TimelyPrincipal.
*/
public class TimelyUser implements Serializable {
public enum UserType {
USER, SERVER
}
public static final TimelyUser ANONYMOUS_USER = new TimelyUser(SubjectIssuerDNPair.of("ANONYMOUS"), UserType.USER,
null, null, null, -1L);
private final String name;
private final String commonName;
private final SubjectIssuerDNPair dn;
private final UserType userType;
private final Collection<String> auths;
private final Collection<String> unmodifiableAuths;
private final Collection<String> roles;
private final Collection<String> unmodifiableRoles;
private final Multimap<String, String> roleToAuthMapping;
private final long creationTime;
private final long expirationTime;
/*
* For testing
*/
public TimelyUser(String subjectDn, Collection<String> auths) {
this(SubjectIssuerDNPair.of(subjectDn), UserType.USER, auths, null, null, System.currentTimeMillis(), -1);
}
public TimelyUser(SubjectIssuerDNPair dn, UserType userType, Collection<String> auths, Collection<String> roles,
Multimap<String, String> roleToAuthMapping, long creationTime) {
this(dn, userType, auths, roles, roleToAuthMapping, creationTime, -1L);
}
@JsonCreator
public TimelyUser(@JsonProperty(value = "dn", required = true) SubjectIssuerDNPair dn,
@JsonProperty(value = "userType", required = true) UserType userType,
@JsonProperty("auths") Collection<String> auths, @JsonProperty("roles") Collection<String> roles,
@JsonProperty("roleToAuthMapping") Multimap<String, String> roleToAuthMapping,
@JsonProperty(value = "creationTime", defaultValue = "-1L") long creationTime,
@JsonProperty(value = "expirationTime", defaultValue = "-1L") long expirationTime) {
this.name = dn.toString();
this.commonName = ProxiedEntityUtils.getCommonName(dn.subjectDN());
this.dn = dn;
this.userType = userType;
this.auths = auths == null ? Collections.emptyList() : new LinkedHashSet<>(auths);
this.unmodifiableAuths = Collections.unmodifiableCollection(this.auths);
this.roles = roles == null ? Collections.emptyList() : new LinkedHashSet<>(roles);
this.unmodifiableRoles = Collections.unmodifiableCollection(this.roles);
this.roleToAuthMapping = roleToAuthMapping == null ? LinkedHashMultimap.create()
: Multimaps.unmodifiableMultimap(LinkedHashMultimap.create(roleToAuthMapping));
this.creationTime = creationTime;
this.expirationTime = expirationTime;
}
public String getName() {
return name;
}
@JsonIgnore
public String getCommonName() {
return commonName;
}
public SubjectIssuerDNPair getDn() {
return dn;
}
public UserType getUserType() {
return userType;
}
public Collection<String> getAuths() {
return unmodifiableAuths;
}
public Collection<String> getRoles() {
return unmodifiableRoles;
}
public Multimap<String, String> getRoleToAuthMapping() {
return roleToAuthMapping;
}
public long getCreationTime() {
return creationTime;
}
public long getExpirationTime() {
return expirationTime;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
TimelyUser that = (TimelyUser) o;
return creationTime == that.creationTime && dn.equals(that.dn) && userType == that.userType
&& auths.equals(that.auths) && roles.equals(that.roles);
}
@Override
public int hashCode() {
int result = dn.hashCode();
result = 31 * result + userType.hashCode();
result = 31 * result + auths.hashCode();
result = 31 * result + roles.hashCode();
result = 31 * result + (int) (creationTime ^ (creationTime >>> 32));
return result;
}
@Override
public String toString() {
return "TimelyUser{" + "name='" + getName() + "'" + ", userType=" + getUserType() + ", auths=" + getAuths()
+ ", roles=" + getRoles() + ", creationTime=" + getCreationTime() + "}";
}
}
@@ -0,0 +1,48 @@
package timely.auth.util;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.stream.Collectors;
import com.google.common.collect.HashMultimap;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import timely.auth.SubjectIssuerDNPair;
import timely.auth.TimelyPrincipal;
import timely.auth.TimelyUser;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class AuthenticationUtils {
public static Collection<String> getAuthCollection(Authentication authentication) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return authorities.stream().map(a -> a.getAuthority()).collect(Collectors.toList());
}
public static TimelyAuthenticationToken getTimelyAuthenticationToken(Authentication authentication,
X509Certificate clientCert) {
TimelyAuthenticationToken timelyAuthenticationToken = null;
if (authentication instanceof TimelyAuthenticationToken) {
return (TimelyAuthenticationToken) authentication;
} else {
if (clientCert == null) {
timelyAuthenticationToken = new TimelyAuthenticationToken(authentication.getName(), null,
HashMultimap.create());
TimelyUser timelyUser = new TimelyUser(SubjectIssuerDNPair.of(authentication.getName()),
TimelyUser.UserType.USER, AuthenticationUtils.getAuthCollection(authentication), null, null,
System.currentTimeMillis());
timelyAuthenticationToken.setTimelyPrincipal(new TimelyPrincipal(timelyUser));
} else {
timelyAuthenticationToken = new TimelyAuthenticationToken(authentication.getName(), clientCert,
HashMultimap.create());
TimelyUser.UserType userType = DnUtils.isServerDN(clientCert.getSubjectDN().getName())
? TimelyUser.UserType.SERVER
: TimelyUser.UserType.USER;
TimelyUser timelyUser = new TimelyUser(SubjectIssuerDNPair.of(authentication.getName()), userType,
AuthenticationUtils.getAuthCollection(authentication), null, null, System.currentTimeMillis());
timelyAuthenticationToken.setTimelyPrincipal(new TimelyPrincipal(timelyUser));
}
}
return timelyAuthenticationToken;
}
}
@@ -0,0 +1,61 @@
package timely.auth.util;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.accumulo.core.security.Authorizations;
import timely.auth.AuthorizationsMinimizer;
public class AuthorizationsUtil {
public static Authorizations union(Iterable<byte[]> authorizations1, Iterable<byte[]> authorizations2) {
LinkedList<byte[]> aggregatedAuthorizations = Lists.newLinkedList();
addTo(aggregatedAuthorizations, authorizations1);
addTo(aggregatedAuthorizations, authorizations2);
return new Authorizations(aggregatedAuthorizations);
}
private static void addTo(LinkedList<byte[]> aggregatedAuthorizations, Iterable<byte[]> authsToAdd) {
for (byte[] auth : authsToAdd) {
aggregatedAuthorizations.add(auth);
}
}
public static Authorizations toAuthorizations(Collection<String> auths) {
return new Authorizations(auths.stream().map(String::trim).map(s -> s.getBytes(Charset.forName("UTF-8")))
.collect(Collectors.toList()));
}
public static List<String> splitAuths(String requestedAuths) {
return Arrays.asList(Iterables.toArray(Splitter.on(',').omitEmptyStrings().trimResults().split(requestedAuths),
String.class));
}
public static Set<Authorizations> buildAuthorizations(Collection<? extends Collection<String>> userAuths) {
if (null == userAuths) {
return Collections.singleton(new Authorizations());
}
HashSet<Authorizations> auths = Sets.newHashSet();
for (Collection<String> userAuth : userAuths) {
auths.add(new Authorizations(userAuth.toArray(new String[userAuth.size()])));
}
return auths;
}
public static Collection<Authorizations> minimize(Collection<Authorizations> authorizations) {
return AuthorizationsMinimizer.minimize(authorizations);
}
}
@@ -0,0 +1,203 @@
package timely.auth.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DnUtils {
/** Config for injecting NPE OU identifiers */
public static final String PROPS_RESOURCE = "dnutils.properties";
private static final Properties PROPS = new Properties();
public static final String SUBJECT_DN_PATTERN_PROPERTY = "subject.dn.pattern";
private static final Pattern SUBJECT_DN_PATTERN;
private static final Logger log = LoggerFactory.getLogger(DnUtils.class);
static {
InputStream in = null;
try {
in = DnUtils.class.getClassLoader().getResourceAsStream(PROPS_RESOURCE);
PROPS.load(in);
} catch (Throwable t) {
log.error(PROPS_RESOURCE + " could not be loaded!", t);
throw new RuntimeException(t);
} finally {
if (null != in) {
try {
in.close();
} catch (IOException e) {
log.warn("Failed to close input stream", e);
}
}
}
String subjectDnPattern = System.getProperty(SUBJECT_DN_PATTERN_PROPERTY,
PROPS.getProperty(SUBJECT_DN_PATTERN_PROPERTY));
try {
if (null == subjectDnPattern || subjectDnPattern.isEmpty()) {
throw new IllegalStateException(SUBJECT_DN_PATTERN_PROPERTY + " property value cannot be null");
}
SUBJECT_DN_PATTERN = Pattern.compile(subjectDnPattern, Pattern.CASE_INSENSITIVE);
} catch (Throwable t) {
log.error(SUBJECT_DN_PATTERN_PROPERTY + " = '" + subjectDnPattern + "' could not be compiled", t);
throw new RuntimeException(t);
}
}
public static String[] splitProxiedDNs(String proxiedDNs, boolean allowDups) {
return ProxiedEntityUtils.splitProxiedDNs(proxiedDNs, allowDups);
}
public static String[] splitProxiedSubjectIssuerDNs(String proxiedDNs) {
return ProxiedEntityUtils.splitProxiedSubjectIssuerDNs(proxiedDNs);
}
public static String buildProxiedDN(String... dns) {
return ProxiedEntityUtils.buildProxiedDN(dns);
}
public static Collection<String> buildNormalizedDNList(String subjectDN, String issuerDN, String proxiedSubjectDNs,
String proxiedIssuerDNs) {
subjectDN = normalizeDN(subjectDN);
issuerDN = normalizeDN(issuerDN);
List<String> dnList = new ArrayList<>();
HashSet<String> subjects = new HashSet<>();
String subject = subjectDN.replaceAll("(?<!\\\\)([<>])", "\\\\$1");
dnList.add(subject);
subjects.add(subject);
dnList.add(issuerDN.replaceAll("(?<!\\\\)([<>])", "\\\\$1"));
if (proxiedSubjectDNs != null) {
if (proxiedIssuerDNs == null)
throw new IllegalArgumentException(
"If proxied subject DNs are supplied, then issuer DNs must be supplied as well.");
String[] subjectDNarray = splitProxiedDNs(proxiedSubjectDNs, true);
String[] issuerDNarray = splitProxiedDNs(proxiedIssuerDNs, true);
if (subjectDNarray.length != issuerDNarray.length)
throw new IllegalArgumentException(
"Subject and issuer DN lists do not have the same number of entries: "
+ Arrays.toString(subjectDNarray) + " vs " + Arrays.toString(issuerDNarray));
for (int i = 0; i < subjectDNarray.length; ++i) {
subjectDNarray[i] = normalizeDN(subjectDNarray[i]);
if (!subjects.contains(subjectDNarray[i])) {
issuerDNarray[i] = normalizeDN(issuerDNarray[i]);
subjects.add(subjectDNarray[i]);
dnList.add(subjectDNarray[i]);
dnList.add(issuerDNarray[i]);
if (issuerDNarray[i].equalsIgnoreCase(subjectDNarray[i]))
throw new IllegalArgumentException(
"Subject DN " + issuerDNarray[i] + " was passed as an issuer DN.");
if (SUBJECT_DN_PATTERN.matcher(issuerDNarray[i]).find())
throw new IllegalArgumentException(
"It appears that a subject DN (" + issuerDNarray[i] + ") was passed as an issuer DN.");
}
}
}
return dnList;
}
public static String buildNormalizedProxyDN(String subjectDN, String issuerDN, String proxiedSubjectDNs,
String proxiedIssuerDNs) {
StringBuilder sb = new StringBuilder();
for (String escapedDN : buildNormalizedDNList(subjectDN, issuerDN, proxiedSubjectDNs, proxiedIssuerDNs)) {
if (sb.length() == 0)
sb.append(escapedDN);
else
sb.append('<').append(escapedDN).append('>');
}
return sb.toString();
}
public static String getCommonName(String dn) {
return ProxiedEntityUtils.getCommonName(dn);
}
public static String[] getOrganizationalUnits(String dn) {
return ProxiedEntityUtils.getOrganizationalUnits(dn);
}
public static String getShortName(String dn) {
return ProxiedEntityUtils.getShortName(dn);
}
public static boolean isServerDN(String dn) {
return NpeUtils.isNPE(dn);
}
public static String getUserDN(String[] dns) {
return getUserDN(dns, false);
}
public static String getUserDN(String[] dns, boolean issuerDNs) {
if (issuerDNs && (dns.length % 2) != 0)
throw new IllegalArgumentException("DNs array is not a subject/issuer DN list: " + Arrays.toString(dns));
for (int i = 0; i < dns.length; i += (issuerDNs) ? 2 : 1) {
String dn = dns[i];
if (!isServerDN(dn))
return dn;
}
return null;
}
public static String[] getComponents(String dn, String componentName) {
return ProxiedEntityUtils.getComponents(dn, componentName);
}
public static String normalizeDN(String userName) {
return ProxiedEntityUtils.normalizeDN(userName);
}
/**
* Encapsulates the known/valid NPE OU list, and whatever else is needed for NPE
* handling. Static inner-class, so that injection of the configured OUs is only
* performed on-demand/if needed
*/
public static class NpeUtils {
/** Property containing a comma-delimited list of OUs */
public static final String NPE_OU_PROPERTY = "npe.ou.entries";
/** Parsed NPE OU identifiers */
static final List<String> NPE_OU_LIST;
private NpeUtils() {
}
static {
List<String> npeOUs = new ArrayList<>();
String ouString = System.getProperty(NPE_OU_PROPERTY, PROPS.getProperty(NPE_OU_PROPERTY));
if (null == ouString || ouString.isEmpty()) {
throw new IllegalStateException("No '" + NPE_OU_PROPERTY + "' value has been configured");
}
// Normalize and load...
String[] ouArray = ouString.split(",");
for (String ou : ouArray) {
npeOUs.add(ou.trim().toUpperCase());
}
NPE_OU_LIST = Collections.unmodifiableList(npeOUs);
}
static boolean isNPE(String dn) {
String[] ouList = ProxiedEntityUtils.getOrganizationalUnits(dn);
for (String ou : ouList) {
if (NPE_OU_LIST.contains(ou.toUpperCase())) {
return true;
}
}
return false;
}
}
}
@@ -0,0 +1,34 @@
package timely.auth.util;
import java.util.Collection;
import java.util.Map;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import io.netty.handler.codec.http.HttpHeaders;
public class HttpHeaderUtils {
public static Multimap<String, String> toMultimap(HttpHeaders headers) {
Multimap<String, String> headerMultimap = HashMultimap.create();
if (headers != null) {
for (Map.Entry<String, String> e : headers.entries()) {
headerMultimap.put(e.getKey(), e.getValue());
}
}
return headerMultimap;
}
public static String getSingleHeader(Multimap<String, String> headers, String headerName, boolean enforceOneValue)
throws IllegalArgumentException {
String value = null;
Collection<String> values = (headers == null) ? null : headers.get(headerName);
if (values != null && !values.isEmpty()) {
if (values.size() > 1 && enforceOneValue) {
throw new IllegalArgumentException(headerName + " was specified multiple times, which is not allowed");
}
value = values.stream().findFirst().orElse(null);
}
return value;
}
}
@@ -0,0 +1,188 @@
package timely.auth.util;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.regex.Pattern;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import com.google.common.collect.Multimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class ProxiedEntityUtils {
private static final Logger log = LoggerFactory.getLogger(ProxiedEntityUtils.class);
public static String[] splitProxiedDNs(String proxiedDNs, boolean allowDups) {
String[] dns;
if (proxiedDNs.indexOf('<') < 0) {
dns = new String[] { proxiedDNs };
} else {
Collection<String> dnCollection = allowDups ? new ArrayList<>() : new LinkedHashSet<>();
String[] pieces = proxiedDNs.split("(?<!\\\\)<|(?<!\\\\)>");
for (String piece : pieces) {
if (piece.trim().length() > 0)
dnCollection.add(piece);
}
dns = dnCollection.toArray(new String[0]);
}
return dns;
}
public static String[] splitProxiedSubjectIssuerDNs(String proxiedDNs) {
String[] dns;
if (proxiedDNs.indexOf('<') < 0) {
dns = new String[] { proxiedDNs };
} else {
HashSet<String> subjects = new HashSet<>();
List<String> dnList = new ArrayList<>();
String[] pieces = proxiedDNs.split("(?<!\\\\)<|(?<!\\\\)>");
ArrayList<String> trimmedPieces = new ArrayList<>(pieces.length);
for (String piece : pieces) {
piece = piece.trim();
if (piece.length() > 0)
trimmedPieces.add(piece);
}
if ((trimmedPieces.size() % 2) != 0)
throw new IllegalArgumentException("Invalid proxied DNs list does not have entries in pairs.");
for (int i = 0; i < trimmedPieces.size(); i += 2) {
String subject = trimmedPieces.get(i);
String issuer = trimmedPieces.get(i + 1);
if (subject.length() > 0 && !subjects.contains(subject)) {
subjects.add(subject);
dnList.add(subject);
dnList.add(issuer);
}
}
dns = dnList.toArray(new String[0]);
}
return dns;
}
public static String buildProxiedDN(String... dns) {
StringBuilder sb = new StringBuilder();
for (String dn : dns) {
String escapedDN = dn.replaceAll("(?<!\\\\)(<|>)", "\\\\$1");
if (sb.length() == 0)
sb.append(escapedDN);
else
sb.append('<').append(escapedDN).append('>');
}
return sb.toString();
}
public static String getCommonName(String dn) {
String[] comps = getComponents(dn, "CN");
return comps.length >= 1 ? comps[0] : null;
}
public static String[] getOrganizationalUnits(String dn) {
return getComponents(dn, "OU");
}
public static String getShortName(String dn) {
String cn = getCommonName(dn);
if (cn == null)
cn = dn;
String sid = cn;
int idx = cn.lastIndexOf(' ');
if (idx >= 0)
sid = cn.substring(idx + 1);
return sid;
}
public static String[] getComponents(String dn, String componentName) {
componentName = componentName.toUpperCase();
ArrayList<String> components = new ArrayList<>();
try {
LdapName name = new LdapName(dn);
for (Rdn rdn : name.getRdns()) {
if (componentName.equals(rdn.getType().toUpperCase())) {
components.add(String.valueOf(rdn.getValue()));
}
}
} catch (InvalidNameException e) {
// ignore -- invalid name, so can't find components
}
return components.toArray(new String[0]);
}
/**
* Attempts to normalize a DN by taking it and reversing the components if it
* doesn't start with CN. Some systems requires the DN components be in a
* specific order, or that order reversed. We cannot arbitrarily reorder the
* components however, e.g., sorting them.
*/
public static String normalizeDN(String userName) {
String normalizedUserName = userName.trim().toLowerCase();
try {
if (!normalizedUserName.startsWith("cn") || Pattern.compile(",[^ ]").matcher(normalizedUserName).find()) {
LdapName name = new LdapName(userName);
StringBuilder sb = new StringBuilder();
ArrayList<Rdn> rdns = new ArrayList<>(name.getRdns());
if (rdns.size() > 0 && !rdns.get(0).toString().toLowerCase().startsWith("cn"))
Collections.reverse(rdns);
for (Rdn rdn : rdns) {
if (sb.length() > 0)
sb.append(", ");
sb.append(rdn.toString());
}
normalizedUserName = sb.toString().toLowerCase();
}
} catch (InvalidNameException e) {
// ignore -- might be a sid rather than a DN
}
log.trace("Normalized [" + userName + "] into [" + normalizedUserName + "]");
return normalizedUserName;
}
public static void addProxyHeaders(Multimap<String, String> headers, X509Certificate clientCert) {
if (clientCert != null) {
List<String> proxiedEntitiesList = new ArrayList<>();
if (headers.containsKey(TimelyAuthenticationToken.PROXIED_ENTITIES_HEADER)) {
String proxiesEntities = HttpHeaderUtils.getSingleHeader(headers,
TimelyAuthenticationToken.PROXIED_ENTITIES_HEADER, false);
headers.removeAll(TimelyAuthenticationToken.PROXIED_ENTITIES_HEADER);
String[] preExistingProxiedEntities = ProxiedEntityUtils.splitProxiedDNs(proxiesEntities, false);
if (preExistingProxiedEntities.length > 0) {
proxiedEntitiesList.addAll(Arrays.asList(preExistingProxiedEntities));
}
}
String subjectDN = DnUtils.normalizeDN(clientCert.getSubjectDN().getName());
proxiedEntitiesList.add(subjectDN);
String[] newProxiedEntities = new String[proxiedEntitiesList.size()];
proxiedEntitiesList.toArray(newProxiedEntities);
headers.put(TimelyAuthenticationToken.PROXIED_ENTITIES_HEADER,
ProxiedEntityUtils.buildProxiedDN(newProxiedEntities));
List<String> proxiedIssuersList = new ArrayList<>();
if (headers.containsKey(TimelyAuthenticationToken.PROXIED_ISSUERS_HEADER)) {
String proxiesIssuers = HttpHeaderUtils.getSingleHeader(headers,
TimelyAuthenticationToken.PROXIED_ISSUERS_HEADER, false);
headers.removeAll(TimelyAuthenticationToken.PROXIED_ISSUERS_HEADER);
String[] preExistingProxiedIssuers = ProxiedEntityUtils.splitProxiedDNs(proxiesIssuers, false);
if (preExistingProxiedIssuers.length > 0) {
proxiedEntitiesList.addAll(Arrays.asList(preExistingProxiedIssuers));
}
}
String issuerDN = DnUtils.normalizeDN(clientCert.getIssuerDN().getName());
proxiedIssuersList.add(issuerDN);
String[] newProxiedIssuers = new String[proxiedIssuersList.size()];
proxiedIssuersList.toArray(newProxiedIssuers);
headers.put(TimelyAuthenticationToken.PROXIED_ISSUERS_HEADER,
ProxiedEntityUtils.buildProxiedDN(newProxiedIssuers));
}
}
}
@@ -0,0 +1,81 @@
package timely.auth.util;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.client.BatchDeleter;
import org.apache.accumulo.core.client.BatchScanner;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.ScannerBase;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.security.Authorizations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.auth.AuthorizationsMinimizer;
import timely.auth.ConfigurableVisibilityFilter;
import timely.auth.ScannerBaseDelegate;
public class ScannerHelper {
private static final Logger logger = LoggerFactory.getLogger(ScannerHelper.class);
public static Scanner createScanner(Connector connector, String tableName,
Collection<Authorizations> authorizations) throws TableNotFoundException {
if (authorizations == null || authorizations.isEmpty())
throw new IllegalArgumentException("Authorizations must not be empty.");
Iterator<Authorizations> iter = AuthorizationsMinimizer.minimize(authorizations).iterator();
Scanner scanner = connector.createScanner(tableName, iter.next());
addVisibilityFilters(iter, scanner);
return scanner;
}
public static BatchScanner createBatchScanner(Connector connector, String tableName,
Collection<Authorizations> authorizations, int numQueryThreads) throws TableNotFoundException {
if (authorizations == null || authorizations.isEmpty())
throw new IllegalArgumentException("Authorizations must not be empty.");
Iterator<Authorizations> iter = AuthorizationsMinimizer.minimize(authorizations).iterator();
BatchScanner batchScanner = connector.createBatchScanner(tableName, iter.next(), numQueryThreads);
addVisibilityFilters(iter, batchScanner);
return batchScanner;
}
public static BatchDeleter createBatchDeleter(Connector connector, String tableName,
Collection<Authorizations> authorizations, int numQueryThreads, long maxMemory, long maxLatency,
int maxWriteThreads) throws TableNotFoundException {
if (authorizations == null || authorizations.isEmpty())
throw new IllegalArgumentException("Authorizations must not be empty.");
Iterator<Authorizations> iter = AuthorizationsMinimizer.minimize(authorizations).iterator();
BatchWriterConfig bwCfg = new BatchWriterConfig().setMaxLatency(maxLatency, TimeUnit.MILLISECONDS)
.setMaxMemory(maxMemory).setMaxWriteThreads(maxWriteThreads);
BatchDeleter batchDeleter = connector.createBatchDeleter(tableName, iter.next(), numQueryThreads, bwCfg);
addVisibilityFilters(iter, batchDeleter);
return batchDeleter;
}
protected static void addVisibilityFilters(Iterator<Authorizations> iter, ScannerBase scanner) {
for (int priority = 10; iter.hasNext(); priority++) {
IteratorSetting cfg = new IteratorSetting(priority, ConfigurableVisibilityFilter.class);
cfg.setName("visibilityFilter" + priority);
cfg.addOption(ConfigurableVisibilityFilter.AUTHORIZATIONS_OPT, iter.next().toString());
// Set the visibility filter as a "system" iterator, which means that normal
// modify, remove, clear operations performed
// on the scanner will not modify/remove/clear this iterator. This way, if a
// query logic attempts to reconfigure the
// scanner's iterators, then this iterator will remain intact.
if (scanner instanceof ScannerBaseDelegate) {
((ScannerBaseDelegate) scanner).addSystemScanIterator(cfg);
} else {
logger.warn("Adding system visibility filter to non-wrapped scanner {}.", scanner.getClass(),
new Exception());
scanner.addScanIterator(cfg);
}
}
}
}
@@ -6,18 +6,27 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty;
public class Security {
private boolean allowAnonymousAccess = false;
private boolean allowAnonymousHttpAccess = false;
private boolean allowAnonymousWsAccess = false;
private int sessionMaxAge = 86400;
@Valid
@NestedConfigurationProperty
private ServerSsl serverSsl = new ServerSsl();
public boolean isAllowAnonymousAccess() {
return allowAnonymousAccess;
public boolean isAllowAnonymousHttpAccess() {
return allowAnonymousHttpAccess;
}
public void setAllowAnonymousAccess(boolean allowAnonymousAccess) {
this.allowAnonymousAccess = allowAnonymousAccess;
public void setAllowAnonymousHttpAccess(boolean allowAnonymousHttpAccess) {
this.allowAnonymousHttpAccess = allowAnonymousHttpAccess;
}
public boolean isAllowAnonymousWsAccess() {
return allowAnonymousWsAccess;
}
public void setAllowAnonymousWsAccess(boolean allowAnonymousWsAccess) {
this.allowAnonymousWsAccess = allowAnonymousWsAccess;
}
public int getSessionMaxAge() {
@@ -1,8 +1,11 @@
package timely.netty.http;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import com.google.common.collect.Multimap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
@@ -12,6 +15,7 @@ import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.api.annotation.AnnotationResolver;
@@ -22,16 +26,20 @@ import timely.api.request.timeseries.HttpRequest;
import timely.api.response.StrictTransportResponse;
import timely.api.response.TimelyException;
import timely.auth.AuthCache;
import timely.auth.AuthenticationService;
import timely.auth.SubjectIssuerDNPair;
import timely.auth.TimelyPrincipal;
import timely.auth.util.HttpHeaderUtils;
import timely.configuration.Http;
import timely.configuration.Security;
import timely.netty.Constants;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class HttpRequestDecoder extends MessageToMessageDecoder<FullHttpRequest> implements TimelyHttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(HttpRequestDecoder.class);
private static final String LOG_RECEIVED_REQUEST = "Received HTTP request {}";
private static final String LOG_PARSED_REQUEST = "Parsed request {}";
private static final String NO_AUTHORIZATIONS = "";
private final Security security;
private final String nonSecureRedirectAddress;
@@ -42,9 +50,11 @@ public class HttpRequestDecoder extends MessageToMessageDecoder<FullHttpRequest>
this.nonSecureRedirectAddress = http.getRedirectPath();
}
public static String getSessionId(FullHttpRequest msg, boolean anonymousAccessAllowed) {
public static String getSessionId(FullHttpRequest msg) {
Multimap<String, String> headers = HttpHeaderUtils.toMultimap(msg.headers());
Collection<String> cookies = headers.get(Names.COOKIE);
final StringBuilder buf = new StringBuilder();
msg.headers().getAll(Names.COOKIE).forEach(h -> {
cookies.forEach(h -> {
ServerCookieDecoder.STRICT.decode(h).forEach(c -> {
if (c.name().equals(Constants.COOKIE_NAME)) {
if (buf.length() == 0) {
@@ -53,13 +63,11 @@ public class HttpRequestDecoder extends MessageToMessageDecoder<FullHttpRequest>
}
});
});
String sessionId = buf.toString();
if (sessionId.length() == 0 && anonymousAccessAllowed) {
sessionId = NO_AUTHORIZATIONS;
} else if (sessionId.length() == 0) {
sessionId = null;
if (buf.length() == 0) {
return null;
} else {
return buf.toString();
}
return sessionId;
}
@Override
@@ -74,7 +82,7 @@ public class HttpRequestDecoder extends MessageToMessageDecoder<FullHttpRequest>
return;
}
final String sessionId = getSessionId(msg, this.security.isAllowAnonymousAccess());
final String sessionId = getSessionId(msg);
LOG.trace("SessionID: " + sessionId);
HttpRequest request;
@@ -97,9 +105,28 @@ public class HttpRequestDecoder extends MessageToMessageDecoder<FullHttpRequest>
LOG.warn("Unhandled HTTP request type {}", msg.getMethod());
throw e;
}
if (request instanceof AuthenticatedRequest && sessionId != null) {
((AuthenticatedRequest) request).setSessionId(sessionId);
((AuthenticatedRequest) request).addHeaders(msg.headers().entries());
if (request instanceof AuthenticatedRequest) {
Multimap<String, String> headers = HttpHeaderUtils.toMultimap(msg.headers());
((AuthenticatedRequest) request).addHeaders(headers);
X509Certificate clientCert = AuthenticationService.getClientCertificate(ctx);
TimelyAuthenticationToken token;
if (StringUtils.isNotBlank(sessionId)) {
TimelyPrincipal principal;
((AuthenticatedRequest) request).setSessionId(sessionId);
if (AuthCache.getCache().asMap().containsKey(sessionId)) {
principal = AuthCache.getCache().asMap().get(sessionId);
} else {
principal = TimelyPrincipal.anonymousPrincipal();
}
SubjectIssuerDNPair dn = principal.getPrimaryUser().getDn();
token = AuthenticationService.getAuthenticationToken(null, dn.subjectDN(), dn.issuerDN());
} else if (clientCert != null) {
token = AuthenticationService.getAuthenticationToken(clientCert, headers);
} else {
SubjectIssuerDNPair dn = TimelyPrincipal.anonymousPrincipal().getPrimaryUser().getDn();
token = AuthenticationService.getAuthenticationToken(null, dn.subjectDN(), dn.issuerDN());
}
((AuthenticatedRequest) request).setToken(token);
}
request.setHttpRequest(msg.copy());
LOG.trace(LOG_PARSED_REQUEST, request);
@@ -113,7 +140,15 @@ public class HttpRequestDecoder extends MessageToMessageDecoder<FullHttpRequest>
return;
}
try {
AuthCache.enforceAccess(security, request);
if (request instanceof AuthenticatedRequest) {
try {
AuthenticationService.enforceAccess((AuthenticatedRequest) request);
} catch (TimelyException e) {
if (!security.isAllowAnonymousHttpAccess()) {
throw e;
}
}
}
} catch (Exception e) {
out.clear();
throw e;
@@ -1,27 +1,36 @@
package timely.netty.http.auth;
import io.netty.channel.ChannelHandlerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import timely.api.request.auth.BasicAuthLoginRequest;
import timely.auth.AuthenticationService;
import timely.auth.TimelyPrincipal;
import timely.configuration.Http;
import timely.configuration.Security;
public class BasicAuthLoginRequestHandler extends TimelyLoginRequestHandler<BasicAuthLoginRequest> {
private static final Logger LOG = LoggerFactory.getLogger(BasicAuthLoginRequestHandler.class);
public BasicAuthLoginRequestHandler(Security security, Http http) {
super(security, http);
}
@Override
protected Authentication authenticate(ChannelHandlerContext ctx, BasicAuthLoginRequest msg)
protected TimelyPrincipal authenticate(ChannelHandlerContext ctx, BasicAuthLoginRequest msg)
throws AuthenticationException {
// Perform the login process using username/password
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(msg.getUsername(),
msg.getPassword());
return AuthenticationService.getAuthenticationManager().authenticate(token);
TimelyAuthenticationToken timelyToken = AuthenticationService.authenticate(token);
TimelyPrincipal principal = timelyToken.getTimelyPrincipal();
LOG.debug("Authenticated user {} with authorizations {}", principal.getPrimaryUser().getDn().subjectDN(),
principal.getAuthorizationsString());
return principal;
}
}
@@ -0,0 +1,114 @@
package timely.netty.http.auth;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import timely.auth.SubjectIssuerDNPair;
import timely.auth.TimelyPrincipal;
import timely.auth.TimelyUser;
import timely.auth.util.DnUtils;
import timely.auth.util.HttpHeaderUtils;
public class TimelyAuthenticationToken extends PreAuthenticatedAuthenticationToken {
private static final Logger LOG = LoggerFactory.getLogger(TimelyAuthenticationToken.class);
public static final String PROXIED_ENTITIES_HEADER = "X-ProxiedEntitiesChain";
public static final String PROXIED_ISSUERS_HEADER = "X-ProxiedIssuersChain";
private Multimap<String, String> httpHeaders = null;
private List<SubjectIssuerDNPair> entities = new ArrayList<>();
private String completeProxyName;
private X509Certificate clientCert;
private TimelyPrincipal timelyPrincipal;
public TimelyAuthenticationToken(String subjectDN, X509Certificate clientCert,
Multimap<String, String> httpHeaders) {
super(subjectDN, clientCert);
if (httpHeaders == null) {
this.httpHeaders = HashMultimap.create();
} else {
this.httpHeaders = httpHeaders;
}
this.clientCert = clientCert;
String issuerDn = clientCert == null ? null : clientCert.getSubjectDN().getName();
String proxiedEntities;
String proxiedIssuers;
try {
proxiedEntities = HttpHeaderUtils.getSingleHeader(httpHeaders, PROXIED_ENTITIES_HEADER, true);
proxiedIssuers = HttpHeaderUtils.getSingleHeader(httpHeaders, PROXIED_ISSUERS_HEADER, true);
} catch (IllegalArgumentException e) {
LOG.error(e.getMessage(), e);
throw e;
}
if (proxiedEntities != null && proxiedIssuers == null) {
LOG.error(PROXIED_ENTITIES_HEADER + " supplied, but missing " + PROXIED_ISSUERS_HEADER);
throw new IllegalArgumentException(
PROXIED_ENTITIES_HEADER + " supplied, but missing " + PROXIED_ISSUERS_HEADER);
}
List<SubjectIssuerDNPair> entities = extractEntities(subjectDN, issuerDn, proxiedEntities, proxiedIssuers);
long now = System.currentTimeMillis();
List<TimelyUser> timelyUsers = new ArrayList<>();
for (SubjectIssuerDNPair ent : entities) {
TimelyUser.UserType userType = DnUtils.isServerDN(ent.subjectDN()) ? TimelyUser.UserType.SERVER
: TimelyUser.UserType.USER;
timelyUsers.add(new TimelyUser(ent, userType, null, null, null, now));
}
timelyPrincipal = new TimelyPrincipal(timelyUsers);
LOG.trace("Created TimelyAuthenticationToken: {}", timelyPrincipal.getName());
}
public TimelyAuthenticationToken(String subjectDn, String issuerDn, X509Certificate clientCert) {
super(subjectDn, clientCert);
this.clientCert = clientCert;
TimelyUser.UserType userType = DnUtils.isServerDN(subjectDn) ? TimelyUser.UserType.SERVER
: TimelyUser.UserType.USER;
TimelyUser timelyUser = new TimelyUser(SubjectIssuerDNPair.of(subjectDn, issuerDn), userType, null, null, null,
System.currentTimeMillis());
timelyPrincipal = new TimelyPrincipal(timelyUser);
LOG.trace("Created TimelyAuthenticationToken: {}", timelyPrincipal.getName());
}
private List<SubjectIssuerDNPair> extractEntities(String subjectDn, String issuerDn, String proxiedEntities,
String proxiedIssuers) {
List<SubjectIssuerDNPair> entities = new ArrayList<>();
entities.add(SubjectIssuerDNPair.of(subjectDn, issuerDn));
if (proxiedEntities != null) {
String[] subjects = DnUtils.splitProxiedDNs(proxiedEntities, true);
if (proxiedIssuers == null) {
throw new IllegalArgumentException("Proxied issuers must be supplied if proxied subjects are supplied");
}
String[] issuers = DnUtils.splitProxiedDNs(proxiedIssuers, true);
if (subjects.length != issuers.length) {
throw new IllegalArgumentException("Proxied subjects and issuers don't match up. Subjects="
+ proxiedEntities + " , Issuers=" + proxiedIssuers);
}
for (int i = 0; i < subjects.length; ++i) {
entities.add(SubjectIssuerDNPair.of(subjects[i], issuers[i]));
}
}
return entities;
}
public Multimap<String, String> getHttpHeaders() {
return httpHeaders;
}
public void setTimelyPrincipal(TimelyPrincipal timelyPrincipal) {
this.timelyPrincipal = timelyPrincipal;
}
public TimelyPrincipal getTimelyPrincipal() {
return timelyPrincipal;
}
public X509Certificate getClientCert() {
return clientCert;
}
}
@@ -15,8 +15,8 @@ import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import timely.auth.AuthCache;
import timely.auth.TimelyPrincipal;
import timely.configuration.Http;
import timely.configuration.Security;
import timely.netty.Constants;
@@ -38,14 +38,16 @@ public abstract class TimelyLoginRequestHandler<T> extends SimpleChannelInboundH
protected final void channelRead0(ChannelHandlerContext ctx, T loginRequest) throws Exception {
try {
LOG.trace("Authenticating {}", loginRequest);
Authentication auth = authenticate(ctx, loginRequest);
LOG.trace("Authenticated {}", auth);
String sessionId = URLEncoder.encode(UUID.randomUUID().toString(), StandardCharsets.UTF_8.name());
AuthCache.getCache().put(sessionId, auth);
TimelyPrincipal principal = authenticate(ctx, loginRequest);
String sessionId = UUID.randomUUID().toString();
AuthCache.getCache().put(sessionId, principal);
LOG.debug("Authenticated new sessionId {} for user {}", sessionId,
principal.getPrimaryUser().getDn().subjectDN());
String sessionIdEncoded = URLEncoder.encode(sessionId, StandardCharsets.UTF_8.name());
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.headers().set(Names.CONTENT_TYPE, Constants.JSON_TYPE);
response.headers().set(Names.CONTENT_LENGTH, response.content().readableBytes());
DefaultCookie cookie = new DefaultCookie(Constants.COOKIE_NAME, sessionId);
DefaultCookie cookie = new DefaultCookie(Constants.COOKIE_NAME, sessionIdEncoded);
cookie.setDomain(domain);
cookie.setMaxAge(maxAge);
cookie.setPath("/");
@@ -64,5 +66,5 @@ public abstract class TimelyLoginRequestHandler<T> extends SimpleChannelInboundH
}
protected abstract Authentication authenticate(ChannelHandlerContext ctx, T loginRequest) throws Exception;
protected abstract TimelyPrincipal authenticate(ChannelHandlerContext ctx, T loginRequest) throws Exception;
}
@@ -1,21 +0,0 @@
package timely.netty.http.auth;
import java.security.cert.X509Certificate;
import io.netty.handler.codec.http.HttpHeaders;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
public class TimelyPreAuthenticatedAuthenticationToken extends PreAuthenticatedAuthenticationToken {
private HttpHeaders httpHeaders = null;
public TimelyPreAuthenticatedAuthenticationToken(String subjectDN, X509Certificate clientCert,
HttpHeaders httpHeaders) {
super(subjectDN, clientCert);
this.httpHeaders = httpHeaders;
}
public HttpHeaders getHttpHeaders() {
return httpHeaders;
}
}
@@ -3,35 +3,48 @@ package timely.netty.http.auth;
import java.security.cert.X509Certificate;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.ssl.SslHandler;
import org.springframework.security.core.Authentication;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.api.request.auth.X509LoginRequest;
import timely.api.response.TimelyException;
import timely.auth.AuthenticationService;
import timely.auth.TimelyPrincipal;
import timely.configuration.Http;
import timely.configuration.Security;
public class X509LoginRequestHandler extends TimelyLoginRequestHandler<X509LoginRequest> {
private static Logger LOG = LoggerFactory.getLogger(X509LoginRequestHandler.class);
public X509LoginRequestHandler(Security security, Http http) {
super(security, http);
}
@Override
protected Authentication authenticate(ChannelHandlerContext ctx, X509LoginRequest loginRequest) throws Exception {
protected TimelyPrincipal authenticate(ChannelHandlerContext ctx, X509LoginRequest loginRequest) throws Exception {
// If we are operating in 2 way SSL, then get the subjectDN from the
// client certificate and perform the login process.
SslHandler sslHandler = (SslHandler) ctx.channel().pipeline().get("ssl");
if (null != sslHandler) {
X509Certificate clientCert = (X509Certificate) sslHandler.engine().getSession().getPeerCertificates()[0];
String subjectDN = AuthenticationService.extractDN(clientCert);
HttpHeaders httpHeaders = loginRequest.getHttpRequest().headers();
TimelyPreAuthenticatedAuthenticationToken token = new TimelyPreAuthenticatedAuthenticationToken(subjectDN,
clientCert, httpHeaders);
return AuthenticationService.getAuthenticationManager().authenticate(token);
} else {
throw new IllegalStateException("The expected SSL handler is not in the pipeline.");
TimelyPrincipal principal = null;
try {
X509Certificate clientCert = AuthenticationService.getClientCertificate(ctx);
if (clientCert == null) {
throw new IllegalArgumentException("No client certificate found");
} else {
// login endpoint is for direct certificate only, not for proxying
TimelyAuthenticationToken token = AuthenticationService.getAuthenticationToken(clientCert,
clientCert.getSubjectDN().getName(), clientCert.getIssuerDN().getName());
TimelyAuthenticationToken t = AuthenticationService.authenticate(token, clientCert);
principal = t.getTimelyPrincipal();
LOG.debug("Authenticated user {} with authorizations {}",
principal.getPrimaryUser().getDn().subjectDN(), principal.getAuthorizationsString());
}
} catch (Exception e) {
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Error performing login", e.getMessage(),
e);
}
return principal;
}
}
@@ -20,7 +20,7 @@ public class HttpMetricsRequestHandler extends SimpleChannelInboundHandler<Metri
@Override
protected void channelRead0(ChannelHandlerContext ctx, MetricsRequest msg) throws Exception {
MetricsResponse r = new MetricsResponse(conf);
String acceptHeader = msg.getRequestHeaders().get(Names.ACCEPT);
String acceptHeader = msg.getRequestHeader(Names.ACCEPT);
sendResponse(ctx, r.toHttpResponse(acceptHeader));
}
@@ -0,0 +1,51 @@
package timely.netty.websocket;
import java.security.cert.X509Certificate;
import java.util.List;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.FullHttpRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.auth.AuthenticationService;
import timely.auth.util.HttpHeaderUtils;
import timely.netty.http.HttpRequestDecoder;
import timely.subscription.SubscriptionRegistry;
public class WebSocketFullRequestHandler extends MessageToMessageCodec<FullHttpRequest, FullHttpRequest> {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketFullRequestHandler.class);
public WebSocketFullRequestHandler() {
super();
}
@Override
protected void decode(ChannelHandlerContext ctx, FullHttpRequest msg, List<Object> out) throws Exception {
msg.retain();
out.add(msg);
// If the session cookie exists, set its value on the ctx.
final String sessionId = HttpRequestDecoder.getSessionId(msg);
if (sessionId != null) {
ctx.channel().attr(SubscriptionRegistry.SESSION_ID_ATTR).set(sessionId);
LOG.trace("Found sessionId in WebSocket channel, setting sessionId {} on context", sessionId);
}
if (msg.headers() != null) {
ctx.channel().attr(WebSocketRequestDecoder.HTTP_HEADERS_ATTR)
.set(HttpHeaderUtils.toMultimap(msg.headers()));
}
X509Certificate clientCert = AuthenticationService.getClientCertificate(ctx);
if (clientCert != null) {
ctx.channel().attr(WebSocketRequestDecoder.CLIENT_CERT_ATTR).set(clientCert);
}
}
@Override
protected void encode(ChannelHandlerContext ctx, FullHttpRequest msg, List<Object> out) throws Exception {
msg.retain();
out.add(msg);
}
}
@@ -1,40 +0,0 @@
package timely.netty.websocket;
import java.util.List;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.FullHttpRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.configuration.Security;
import timely.netty.http.HttpRequestDecoder;
import timely.subscription.SubscriptionRegistry;
public class WebSocketHttpCookieHandler extends MessageToMessageCodec<FullHttpRequest, FullHttpRequest> {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketHttpCookieHandler.class);
private boolean anonymousAccessAllowed;
public WebSocketHttpCookieHandler(Security security) {
super();
this.anonymousAccessAllowed = security.isAllowAnonymousAccess();
}
@Override
protected void decode(ChannelHandlerContext ctx, FullHttpRequest msg, List<Object> out) throws Exception {
msg.retain();
out.add(msg);
// If the session cookie exists, set its value on the ctx.
final String sessionId = HttpRequestDecoder.getSessionId(msg, this.anonymousAccessAllowed);
ctx.channel().attr(SubscriptionRegistry.SESSION_ID_ATTR).set(sessionId);
LOG.info("Found session id in WebSocket channel, setting sessionId {} on context", sessionId);
}
@Override
protected void encode(ChannelHandlerContext ctx, FullHttpRequest msg, List<Object> out) throws Exception {
msg.retain();
out.add(msg);
}
}
@@ -1,15 +1,19 @@
package timely.netty.websocket;
import java.security.cert.X509Certificate;
import java.util.List;
import com.google.common.collect.Multimap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.ssl.SslCompletionEvent;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,12 +21,19 @@ import timely.api.request.AuthenticatedRequest;
import timely.api.request.WebSocketRequest;
import timely.api.response.TimelyException;
import timely.auth.AuthCache;
import timely.auth.AuthenticationService;
import timely.auth.SubjectIssuerDNPair;
import timely.auth.TimelyPrincipal;
import timely.configuration.Security;
import timely.netty.http.auth.TimelyAuthenticationToken;
import timely.subscription.SubscriptionRegistry;
import timely.util.JsonUtil;
public class WebSocketRequestDecoder extends MessageToMessageDecoder<WebSocketFrame> {
public static final AttributeKey<Multimap<String, String>> HTTP_HEADERS_ATTR = AttributeKey.newInstance("headers");
public static final AttributeKey<X509Certificate> CLIENT_CERT_ATTR = AttributeKey.newInstance("clientCert");
private static final Logger LOG = LoggerFactory.getLogger(WebSocketRequestDecoder.class);
private Security security;
@@ -41,12 +52,32 @@ public class WebSocketRequestDecoder extends MessageToMessageDecoder<WebSocketFr
LOG.trace("Received WS request {}", content);
String sessionId = ctx.channel().attr(SubscriptionRegistry.SESSION_ID_ATTR).get();
if (request instanceof AuthenticatedRequest && !StringUtils.isEmpty(sessionId)) {
LOG.info("Found session id in WebSocket channel, setting sessionId {} on request", sessionId);
AuthenticatedRequest ar = (AuthenticatedRequest) request;
ar.setSessionId(sessionId);
}
Multimap<String, String> headers = ctx.channel().attr(HTTP_HEADERS_ATTR).get();
X509Certificate clientCert = ctx.channel().attr(CLIENT_CERT_ATTR).get();
if (request instanceof AuthenticatedRequest) {
if (headers != null) {
((AuthenticatedRequest) request).addHeaders(headers);
}
TimelyAuthenticationToken token;
if (StringUtils.isNotBlank(sessionId)) {
TimelyPrincipal principal;
((AuthenticatedRequest) request).setSessionId(sessionId);
if (AuthCache.getCache().asMap().containsKey(sessionId)) {
principal = AuthCache.getCache().asMap().get(sessionId);
} else {
principal = TimelyPrincipal.anonymousPrincipal();
}
SubjectIssuerDNPair dn = principal.getPrimaryUser().getDn();
token = AuthenticationService.getAuthenticationToken(null, dn.subjectDN(), dn.issuerDN());
} else if (clientCert != null) {
token = AuthenticationService.getAuthenticationToken(clientCert, headers);
} else {
SubjectIssuerDNPair dn = TimelyPrincipal.anonymousPrincipal().getPrimaryUser().getDn();
token = AuthenticationService.getAuthenticationToken(null, dn.subjectDN(), dn.issuerDN());
}
((AuthenticatedRequest) request).setToken(token);
}
try {
request.validate();
} catch (IllegalArgumentException e) {
@@ -55,8 +86,16 @@ public class WebSocketRequestDecoder extends MessageToMessageDecoder<WebSocketFr
return;
}
try {
AuthCache.enforceAccess(security, request);
} catch (TimelyException e) {
if (request instanceof AuthenticatedRequest) {
try {
AuthenticationService.enforceAccess((AuthenticatedRequest) request);
} catch (TimelyException e) {
if (!security.isAllowAnonymousWsAccess()) {
throw e;
}
}
}
} catch (Exception e) {
out.clear();
LOG.error("Error during access enforcment: " + e.getMessage());
ctx.writeAndFlush(new CloseWebSocketFrame(1008, e.getMessage()));
@@ -99,10 +138,11 @@ public class WebSocketRequestDecoder extends MessageToMessageDecoder<WebSocketFr
}
}
} else if (evt instanceof SslCompletionEvent) {
SslCompletionEvent ssl = (SslCompletionEvent) evt;
if (!ssl.isSuccess()) {
LOG.error("SSL error: {}", ssl.getClass().getSimpleName(), ssl.cause());
}
LOG.debug("{}", ((SslCompletionEvent) evt).getClass().getSimpleName());
} else if (evt instanceof WebSocketServerProtocolHandler.ServerHandshakeStateEvent) {
// The handshake completed succesfully and the channel was upgraded to
// websockets
LOG.trace("SSL handshake completed successfully, upgraded channel to websockets");
} else {
LOG.warn("Received unhandled user event {}", evt);
}
@@ -40,7 +40,7 @@ public class WSAddSubscriptionRequestHandler extends SimpleChannelInboundHandler
if (add.getDelayTime().isPresent()) {
delayTime = add.getDelayTime().get();
}
s.addMetric(metric, tags, startTime, endTime, delayTime);
s.addMetric(add, metric, tags, startTime, endTime, delayTime);
} else {
LOG.error("Unknown subscription id, create subscription first");
ctx.writeAndFlush(new CloseWebSocketFrame(1003, "Unknown subscription id, create subscription first"));
@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import timely.api.request.subscription.CreateSubscription;
import timely.configuration.Configuration;
import timely.store.DataStore;
import timely.store.cache.DataStoreCache;
import timely.subscription.Subscription;
import timely.subscription.SubscriptionRegistry;
@@ -16,10 +17,12 @@ public class WSCreateSubscriptionRequestHandler extends SimpleChannelInboundHand
private static final Logger LOG = LoggerFactory.getLogger(WSCreateSubscriptionRequestHandler.class);
private final DataStore store;
private final DataStoreCache cache;
private final Configuration conf;
public WSCreateSubscriptionRequestHandler(DataStore store, Configuration conf) {
public WSCreateSubscriptionRequestHandler(DataStore store, DataStoreCache cache, Configuration conf) {
this.store = store;
this.cache = cache;
this.conf = conf;
}
@@ -27,11 +30,11 @@ public class WSCreateSubscriptionRequestHandler extends SimpleChannelInboundHand
protected void channelRead0(ChannelHandlerContext ctx, CreateSubscription create) throws Exception {
final String subscriptionId = create.getSubscriptionId();
SubscriptionRegistry.get().put(subscriptionId,
new Subscription(subscriptionId, create.getSessionId(), store, ctx, this.conf));
new Subscription(subscriptionId, create.getSessionId(), store, cache, ctx, this.conf));
// Store the session id as an attribute on the context.
ctx.channel().attr(SubscriptionRegistry.SUBSCRIPTION_ID_ATTR).set(subscriptionId);
LOG.info("Created subscription {} on channel {}", subscriptionId, ctx);
LOG.info("[{}] Created subscription on channel {}", subscriptionId, ctx);
ctx.channel().closeFuture().addListener(new ChannelFutureListener() {
@@ -39,7 +42,7 @@ public class WSCreateSubscriptionRequestHandler extends SimpleChannelInboundHand
public void operationComplete(ChannelFuture future) throws Exception {
Subscription s = SubscriptionRegistry.get().remove(subscriptionId);
if (null != s) {
LOG.info("Channel closed, closing subscriptions for subscriptionId: " + subscriptionId);
LOG.info("[{}] Channel closed, closing subscriptions", subscriptionId);
s.close();
}
}
@@ -7,6 +7,7 @@ import java.util.Set;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.data.Range;
import timely.api.request.AuthenticatedRequest;
import timely.api.request.timeseries.QueryRequest;
import timely.api.request.timeseries.SearchLookupRequest;
import timely.api.request.timeseries.SuggestRequest;
@@ -36,8 +37,8 @@ public interface DataStore {
Set<Tag> getColumnFamilies(String metric, Map<String, String> tags) throws TableNotFoundException;
Scanner createScannerForMetric(String sessionId, String metric, Map<String, String> tags, long startTime,
long endTime, int lag, int scannerBatchSize, int scannerReadAhead) throws TimelyException;
Scanner createScannerForMetric(AuthenticatedRequest request, String metric, Map<String, String> tags,
int scannerBatchSize, int scannerReadAhead) throws TimelyException;
void setCache(DataStoreCache cache);
@@ -52,7 +52,6 @@ import org.apache.accumulo.core.iterators.user.RegExFilter;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.accumulo.core.util.Pair;
import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.io.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -71,8 +70,10 @@ import timely.api.response.timeseries.SearchLookupResponse;
import timely.api.response.timeseries.SearchLookupResponse.Result;
import timely.api.response.timeseries.SuggestResponse;
import timely.auth.AuthCache;
import timely.auth.util.ScannerHelper;
import timely.configuration.Accumulo;
import timely.configuration.Configuration;
import timely.configuration.Security;
import timely.model.Metric;
import timely.model.Tag;
import timely.sample.Aggregation;
@@ -118,14 +119,14 @@ public class DataStoreImpl implements DataStore {
private final String metricsTable;
private final String metaTable;
private final InternalMetrics internalMetrics;
private final Timer internalMetricsTimer = new Timer("InternalMatricsTimer", true);
private final Timer internalMetricsTimer = new Timer("InternalMetricsTimer", true);
private final int scannerThreads;
private final long maxDownsampleMemory;
private final BatchWriterConfig bwConfig;
private final List<BatchWriter> writers = new ArrayList<>();
private final ThreadLocal<BatchWriter> metaWriter = new ThreadLocal<>();
private final ThreadLocal<BatchWriter> batchWriter = new ThreadLocal<>();
private boolean anonAccessAllowed = false;
private final Security security;
private final Map<String, String> ageOff;
private final long defaultAgeOffMilliSec;
private DataStoreCache cache = null;
@@ -147,7 +148,7 @@ public class DataStoreImpl implements DataStore {
bwConfig.setMaxWriteThreads(accumuloConf.getWrite().getThreads());
scannerThreads = accumuloConf.getScan().getThreads();
maxDownsampleMemory = accumuloConf.getScan().getMaxDownsampleMemory();
anonAccessAllowed = conf.getSecurity().isAllowAnonymousAccess();
security = conf.getSecurity();
metricsTable = conf.getMetricsTable();
if (metricsTable.contains(".")) {
@@ -505,8 +506,7 @@ public class DataStoreImpl implements DataStore {
@Override
public List<QueryResponse> query(QueryRequest msg) throws TimelyException {
List<QueryResponse> result = new ArrayList<>();
LOG.debug("Query request {}", msg);
LOG.debug("Query for [{}] [{}]", msg.getUserName(), msg);
long requestedStartTs = msg.getStart();
long requestedEndTs = msg.getEnd();
StringBuilder metricList = new StringBuilder();
@@ -549,8 +549,9 @@ public class DataStoreImpl implements DataStore {
if (percentServedFromCache > 100) {
percentServedFromCache = 100;
}
LOG.debug("Cache query time:{} duration (min):{} metrics:{} results:{} percentFromCache:{}",
(System.currentTimeMillis() - now),
LOG.debug(
"Cache query for [{}] time:{} duration (min):{} metrics:{} results:{} percentFromCache:{}",
msg.getUserName(), (System.currentTimeMillis() - now),
((requestedEndTs - requestedStartTs) / (1000 * 60)), metricList.toString(), z,
percentServedFromCache);
allSeries.putAll(cachedMetrics);
@@ -581,7 +582,8 @@ public class DataStoreImpl implements DataStore {
if (endOfLastPeriod > startOfFirstPeriod) {
BatchScanner scanner = null;
try {
scanner = connector.createBatchScanner(metricsTable, getSessionAuthorizations(msg),
Collection<Authorizations> authorizations = getSessionAuthorizations(msg);
scanner = ScannerHelper.createBatchScanner(connector, metricsTable, authorizations,
scannerThreads);
List<String> tagOrder = prioritizeTags(query.getMetric(), query.getTags());
Map<String, String> orderedTags = orderTags(tagOrder, query.getTags());
@@ -658,8 +660,9 @@ public class DataStoreImpl implements DataStore {
result.add(convertToQueryResponse(query, entry.getKey(), entry.getValue(), tsDivisor));
}
}
LOG.debug("Query time:{} duration:{} metrics:{} results:{}", (System.currentTimeMillis() - now),
((requestedEndTs - requestedStartTs) / (1000 * 60)), metricList.toString(), numResults);
LOG.debug("Query for [{}] time:{} duration:{} metrics:{} results:{}", msg.getUserName(),
(System.currentTimeMillis() - now), ((requestedEndTs - requestedStartTs) / (1000 * 60)),
metricList.toString(), numResults);
internalMetrics.addQueryResponse(result.size(), (System.currentTimeMillis() - now));
return result;
} catch (ClassNotFoundException | IOException | TableNotFoundException ex) {
@@ -945,32 +948,8 @@ public class DataStoreImpl implements DataStore {
return Aggregator.getAggregator(query.getAggregator());
}
private Authorizations getSessionAuthorizations(AuthenticatedRequest request) {
return getSessionAuthorizations(request.getSessionId());
}
private Authorizations getSessionAuthorizations(String sessionId) {
if (anonAccessAllowed) {
if (StringUtils.isEmpty(sessionId)) {
return Authorizations.EMPTY;
} else {
Authorizations auths = AuthCache.getAuthorizations(sessionId);
if (null == auths) {
auths = Authorizations.EMPTY;
}
return auths;
}
} else {
if (StringUtils.isEmpty(sessionId)) {
throw new IllegalArgumentException("session id cannot be null");
} else {
Authorizations auths = AuthCache.getAuthorizations(sessionId);
if (null == auths) {
throw new IllegalStateException("No auths found for sessionId: " + sessionId);
}
return auths;
}
}
private Collection<Authorizations> getSessionAuthorizations(AuthenticatedRequest request) {
return AuthCache.getAuthorizations(request, security);
}
public long getAgeOffForMetric(String metricName) {
@@ -982,27 +961,15 @@ public class DataStoreImpl implements DataStore {
}
}
public Scanner createScannerForMetric(String sessionId, String metric, Map<String, String> tags, long startTime,
long endTime, int lag, int scannerBatchSize, int scannerReadAhead) throws TimelyException {
public Scanner createScannerForMetric(AuthenticatedRequest request, String metric, Map<String, String> tags,
int scannerBatchSize, int scannerReadAhead) throws TimelyException {
try {
Authorizations auths = null;
try {
auths = getSessionAuthorizations(sessionId);
} catch (NullPointerException npe) {
// Session id being used for metric scanner, but session Id does
// not exist in Auth Cache. Use Empty auths if anonymous access
// allowed
if (anonAccessAllowed) {
auths = Authorizations.EMPTY;
} else {
throw npe;
}
}
LOG.debug("Creating metric scanner for session: {} with auths: {}", sessionId, auths);
Scanner s = connector.createScanner(this.metricsTable, auths);
if (null == metric) {
throw new IllegalArgumentException("metric name must be specified");
}
Collection<Authorizations> auths = getSessionAuthorizations(request);
LOG.debug("Creating metric scanner for session: {} with auths: {}", request.getSessionId(), auths);
Scanner s = ScannerHelper.createScanner(connector, this.metricsTable, auths);
if (tags == null) {
tags = new LinkedHashMap<>();
}
+55 -35
View File
@@ -25,7 +25,6 @@ import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.iterators.SortedKeyValueIterator;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.atomic.DistributedAtomicValue;
import org.apache.curator.framework.recipes.cache.TreeCache;
@@ -36,13 +35,17 @@ import org.apache.curator.retry.RetryForever;
import org.apache.zookeeper.CreateMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.adapter.accumulo.MetricAdapter;
import timely.api.request.AuthenticatedRequest;
import timely.api.request.timeseries.QueryRequest;
import timely.api.response.CacheResponse;
import timely.api.response.MetricResponse;
import timely.api.response.TimelyException;
import timely.api.response.timeseries.QueryResponse;
import timely.auth.AuthCache;
import timely.auth.util.AuthorizationsUtil;
import timely.configuration.Configuration;
import timely.configuration.Security;
import timely.model.Metric;
import timely.model.Tag;
import timely.sample.Aggregation;
@@ -69,7 +72,7 @@ public class DataStoreCache {
private DistributedAtomicValue nonCachedMetricsIP = null;
private InterProcessReadWriteLock nonCachedMetricsIPRWLock;
private long maxUniqueTagSets;
private boolean anonAccessAllowed;
private final Security security;
private Map<String, Long> minimumAgeOff;
private Map<String, String> minimumAgeOffForIterator;
private int flushBatch = 0;
@@ -86,7 +89,7 @@ public class DataStoreCache {
LOG.info("Reading initial values from nonCachedMetricsIP");
readNonCachedMetricsIP();
maxUniqueTagSets = conf.getCache().getMaxUniqueTagSets();
anonAccessAllowed = conf.getSecurity().isAllowAnonymousAccess();
security = conf.getSecurity();
Map<String, Integer> cacheAgeOff = conf.getCache().getMetricAgeOffHours();
Map<String, Integer> accumuloAgeOff = conf.getMetricAgeOffDays();
LOG.info("cacheAgeOff:{} accumuloAgeOff:{}", cacheAgeOff, accumuloAgeOff);
@@ -559,7 +562,6 @@ public class DataStoreCache {
return false;
}
}
return true;
}
@@ -601,7 +603,8 @@ public class DataStoreCache {
long start = System.currentTimeMillis();
try {
SortedKeyValueIterator<org.apache.accumulo.core.data.Key, org.apache.accumulo.core.data.Value> itr = null;
itr = setupIterator(msg, query, getSessionAuthorizations(msg), getAgeOffForMetric(query.getMetric()));
Collection<Authorizations> authorizations = getSessionAuthorizations(msg);
itr = setupIterator(msg, query, authorizations, getAgeOffForMetric(query.getMetric()));
Map<Set<Tag>, Set<Tag>> matchingTagCache = new HashMap<>();
while (itr.hasTop()) {
Map<Set<Tag>, Aggregation> samples = AggregationIterator.decodeValue(itr.getTopValue());
@@ -632,8 +635,46 @@ public class DataStoreCache {
}
}
public List<MetricResponse> getMetricsFromCache(AuthenticatedRequest request, String metric,
Map<String, String> tags, long begin, long end) {
List<MetricResponse> metricResponses = new ArrayList<>();
QueryRequest.SubQuery subQuery = new QueryRequest.SubQuery();
subQuery.setMetric(metric);
if (tags != null) {
for (Map.Entry<String, String> t : tags.entrySet()) {
subQuery.addTag(t.getKey(), t.getValue());
}
}
Collection<Authorizations> authorizations = getSessionAuthorizations(request);
Collection<Authorizations> minimizedAuths = AuthorizationsUtil.minimize(authorizations);
Collection<VisibilityFilter> visibilityFilters = new ArrayList<>();
for (Authorizations a : minimizedAuths) {
((ArrayList<VisibilityFilter>) visibilityFilters).add(new VisibilityFilter(a));
}
DataStoreCacheIterator itr = new DataStoreCacheIterator(this, visibilityFilters, subQuery, begin, end);
try {
itr.seek(new Range(subQuery.getMetric()), null, true);
while (itr.hasTop()) {
Key k = itr.getTopKey();
Value v = itr.getTopValue();
Metric m = MetricAdapter.parse(k, v);
MetricResponse mr = new MetricResponse();
mr.setMetric(metric);
mr.setTags(m.getTags());
mr.setTimestamp(m.getValue().getTimestamp());
mr.setValue(m.getValue().getMeasure());
metricResponses.add(mr);
itr.next();
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
return metricResponses;
}
protected SortedKeyValueIterator<Key, Value> setupIterator(QueryRequest query, QueryRequest.SubQuery subQuery,
Authorizations authorizations, long ageOffForMetric) throws TimelyException {
Collection<Authorizations> authorizations, long ageOffForMetric) throws TimelyException {
SortedKeyValueIterator<org.apache.accumulo.core.data.Key, org.apache.accumulo.core.data.Value> itr = null;
@@ -652,9 +693,12 @@ public class DataStoreCache {
try {
// create DataStoreCacheIterator which is the base iterator of the stack
VisibilityFilter visFilter = new VisibilityFilter(authorizations);
itr = new DataStoreCacheIterator(this, visFilter, subQuery, startOfFirstPeriod, endOfLastPeriod);
Collection<Authorizations> minimizedAuths = AuthorizationsUtil.minimize(authorizations);
Collection<VisibilityFilter> visibilityFilters = new ArrayList<>();
for (Authorizations a : minimizedAuths) {
((ArrayList<VisibilityFilter>) visibilityFilters).add(new VisibilityFilter(a));
}
itr = new DataStoreCacheIterator(this, visibilityFilters, subQuery, startOfFirstPeriod, endOfLastPeriod);
// create RateIterator if necessary
if (subQuery.isRate()) {
@@ -703,32 +747,8 @@ public class DataStoreCache {
return itr;
}
private Authorizations getSessionAuthorizations(AuthenticatedRequest request) {
return getSessionAuthorizations(request.getSessionId());
}
private Authorizations getSessionAuthorizations(String sessionId) {
if (anonAccessAllowed) {
if (StringUtils.isEmpty(sessionId)) {
return Authorizations.EMPTY;
} else {
Authorizations auths = AuthCache.getAuthorizations(sessionId);
if (null == auths) {
auths = Authorizations.EMPTY;
}
return auths;
}
} else {
if (StringUtils.isEmpty(sessionId)) {
throw new IllegalArgumentException("session id cannot be null");
} else {
Authorizations auths = AuthCache.getAuthorizations(sessionId);
if (null == auths) {
throw new IllegalStateException("No auths found for sessionId: " + sessionId);
}
return auths;
}
}
private Collection<Authorizations> getSessionAuthorizations(AuthenticatedRequest request) {
return AuthCache.getAuthorizations(request, security);
}
private QueryResponse convertToQueryResponse(QueryRequest.SubQuery query, Set<Tag> tags,
@@ -27,7 +27,7 @@ public class DataStoreCacheIterator implements SortedKeyValueIterator<Key, Value
private static final Logger LOG = LoggerFactory.getLogger(DataStoreCacheIterator.class);
private DataStoreCache store;
private VisibilityFilter visibilityFilter;
private Collection<VisibilityFilter> visibilityFilters;
private QueryRequest.SubQuery query;
private long startTs;
private long endTs;
@@ -38,11 +38,11 @@ public class DataStoreCacheIterator implements SortedKeyValueIterator<Key, Value
private Queue<KeyValue> kvQueue = new LinkedList<>();
private Set<TaggedMetric> matchingTaggedMetrics = new HashSet<>();
public DataStoreCacheIterator(DataStoreCache store, VisibilityFilter visibilityFilter, QueryRequest.SubQuery query,
long startTs, long endTs) {
public DataStoreCacheIterator(DataStoreCache store, Collection<VisibilityFilter> visibilityFilters,
QueryRequest.SubQuery query, long startTs, long endTs) {
this.store = store;
this.visibilityFilter = visibilityFilter;
this.visibilityFilters = visibilityFilters;
this.query = query;
this.startTs = startTs;
this.endTs = endTs;
@@ -72,9 +72,20 @@ public class DataStoreCacheIterator implements SortedKeyValueIterator<Key, Value
if (matchingTaggedMetrics.contains(currentTaggedMetric)) {
addCurrent = true;
} else {
if (currentTaggedMetric.matches(requestedTags) && entry.getKey().isVisible(visibilityFilter)) {
addCurrent = true;
matchingTaggedMetrics.add(currentTaggedMetric);
if (currentTaggedMetric.matches(requestedTags)) {
boolean isVisible = false;
for (VisibilityFilter v : visibilityFilters) {
if (entry.getKey().isVisible(v)) {
isVisible = true;
} else {
isVisible = false;
break;
}
}
if (isVisible) {
addCurrent = true;
matchingTaggedMetrics.add(currentTaggedMetric);
}
}
}
@@ -3,6 +3,7 @@ package timely.subscription;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -22,15 +23,18 @@ import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.adapter.accumulo.MetricAdapter;
import timely.api.request.AuthenticatedRequest;
import timely.api.response.MetricResponse;
import timely.api.response.MetricResponses;
import timely.api.response.TimelyException;
import timely.model.Metric;
import timely.model.Tag;
import timely.store.DataStore;
import timely.store.cache.DataStoreCache;
import timely.util.JsonUtil;
public class MetricScanner extends Thread implements UncaughtExceptionHandler {
@@ -41,81 +45,147 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
private final Scanner scanner;
private Iterator<Entry<Key, Value>> iter = null;
private final ChannelHandlerContext ctx;
private final AuthenticatedRequest request;
private volatile boolean closed = false;
private final long delay;
private final String name;
private String scannerInfo;
private final int lag;
private final String subscriptionId;
private final String metric;
private long startTime;
private long beginTime;
private long endTime;
private final Subscription subscription;
private MetricResponses responses = new MetricResponses();
private final ScheduledFuture<?> flusher;
private final int subscriptionBatchSize;
private MetricResponses responses = new MetricResponses();
private ScheduledFuture<?> flusher = null;
private List<Range> ranges = null;
private Iterator<Range> rangeItr = null;
private DataStore store = null;
private DataStoreCache cache = null;
private Set<Tag> colFamValues = null;
private boolean completedResponseSent = false;
private boolean done = false;
public MetricScanner(Subscription sub, String subscriptionId, String sessionId, DataStore store, String metric,
Map<String, String> tags, long startTime, long endTime, long delay, int lag, ChannelHandlerContext ctx,
int scannerBatchSize, int flushIntervalSeconds, int scannerReadAhead, int subscriptionBatchSize)
throws TimelyException {
public MetricScanner(Subscription sub, String subscriptionId, String sessionId, DataStore store,
DataStoreCache cache, String metric, Map<String, String> tags, long beginTime, long endTime, long delay,
int lag, AuthenticatedRequest request, ChannelHandlerContext ctx, int scannerBatchSize,
int flushIntervalSeconds, int scannerReadAhead, int subscriptionBatchSize) throws TimelyException {
this.setDaemon(true);
this.setUncaughtExceptionHandler(this);
this.store = store;
this.cache = cache;
this.subscription = sub;
this.ctx = ctx;
this.request = request;
this.lag = lag;
this.metric = metric;
this.startTime = startTime;
this.beginTime = beginTime;
this.endTime = endTime;
this.scanner = this.store.createScannerForMetric(sessionId, metric, tags, startTime, endTime, lag,
scannerBatchSize, scannerReadAhead);
if (0 == startTime) {
this.startTime = (System.currentTimeMillis() - this.store.getAgeOffForMetric(metric) - 1000);
LOG.debug("Overriding zero start time to {} due to age off configuration", startTime);
}
long endTimeStamp = (endTime == 0) ? (System.currentTimeMillis() - (lag * 1000)) : endTime;
try {
this.colFamValues = this.store.getColumnFamilies(metric, tags);
ranges = this.store.getQueryRanges(metric, this.startTime, endTimeStamp, colFamValues);
rangeItr = ranges.iterator();
if (rangeItr.hasNext()) {
Range r = rangeItr.next();
this.scanner.setRange(r);
}
} catch (TableNotFoundException e) {
throw new TimelyException(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), "Error in MetricScanner",
e.getMessage(), e);
}
this.iter = scanner.iterator();
this.delay = delay;
this.subscriptionId = subscriptionId;
this.subscriptionBatchSize = subscriptionBatchSize;
ToStringBuilder buf = new ToStringBuilder(this);
ToStringBuilder buf = new ToStringBuilder(this, ToStringStyle.JSON_STYLE);
buf.append("sessionId", sessionId);
buf.append("subscriptionId", subscriptionId);
buf.append("metric", metric);
buf.append("startTime", startTime);
buf.append("startTime", beginTime);
buf.append("endTime", endTime);
buf.append("delayTime", delay);
if (null != tags) {
buf.append("tags", tags.toString());
}
name = buf.toString();
this.setName("Metric Scanner " + name);
this.flusher = this.ctx.executor().scheduleAtFixedRate(() -> {
flush();
}, flushIntervalSeconds, flushIntervalSeconds, TimeUnit.SECONDS);
LOG.trace("Created MetricScanner: {}", name);
scannerInfo = buf.toString();
LOG.info("[{}] Setting up MetricScanner: {}", subscriptionId, scannerInfo);
if (0 == beginTime) {
this.beginTime = (System.currentTimeMillis() - this.store.getAgeOffForMetric(metric) - 1000);
LOG.debug("[{}] Overriding zero start time to {} due to age off configuration", subscriptionId, beginTime);
}
long endTimeStamp = (endTime == 0) ? (System.currentTimeMillis() - (lag * 1000)) : endTime;
try {
this.colFamValues = this.store.getColumnFamilies(metric, tags);
this.ranges = new ArrayList<>();
if (this.cache == null) {
LOG.debug("[{}] Cache not enabled, adding complete range", subscriptionId);
ranges.addAll(this.store.getQueryRanges(metric, this.beginTime, endTimeStamp, colFamValues));
} else {
long oldestTsForMetric = this.cache.getOldestTimestamp(metric);
// metric is cached on this server and requested range ends somewhere in the
// cache
if (oldestTsForMetric < Long.MAX_VALUE && endTimeStamp > oldestTsForMetric) {
List<MetricResponse> metricsFromCache;
if (this.beginTime < oldestTsForMetric) {
// add range this.beginTime to oldestTsForMetric - 1 because this range is not
// cached
ranges.addAll(
this.store.getQueryRanges(metric, this.beginTime, oldestTsForMetric - 1, colFamValues));
metricsFromCache = cache.getMetricsFromCache(request, metric, tags, oldestTsForMetric,
endTimeStamp);
LOG.debug("[{}] MetricScanner request partially fulfilled from cache: {} metrics",
subscriptionId, metricsFromCache.size());
} else {
metricsFromCache = cache.getMetricsFromCache(request, metric, tags, this.beginTime,
endTimeStamp);
LOG.debug("[{}] Websocket request completely fulfilled from cache: {} metrics [{}]",
subscriptionId, metricsFromCache.size());
}
if (metricsFromCache.isEmpty()) {
// no metrics from cache, clear ranges and add whole range
ranges.clear();
ranges.addAll(this.store.getQueryRanges(metric, this.beginTime, endTimeStamp, colFamValues));
LOG.debug("[{}] MetricScanner request got no metrics from cache, re-adding complete range",
subscriptionId);
} else {
for (MetricResponse r : metricsFromCache) {
responses.addResponse(r);
}
flush();
}
} else {
LOG.debug("[{}] MetricScanner request range not cached, adding complete range", subscriptionId);
ranges.addAll(this.store.getQueryRanges(metric, this.beginTime, endTimeStamp, colFamValues));
}
}
if (ranges.isEmpty() && endTime == 0) {
// request fulfilled from cache up until current, but user asked for continuous
// data
// must add a small range otherwise we will be scanning all data regardless of
// metric, tag
ranges.addAll(this.store.getQueryRanges(metric, endTimeStamp, endTimeStamp + 1, colFamValues));
}
if (ranges.isEmpty()) {
this.scanner = null;
this.done = true;
} else {
this.scanner = this.store.createScannerForMetric(this.request, metric, tags, scannerBatchSize,
scannerReadAhead);
rangeItr = ranges.iterator();
if (rangeItr.hasNext()) {
Range r = rangeItr.next();
this.scanner.setRange(r);
}
}
} catch (TableNotFoundException e) {
throw new TimelyException(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(),
"[" + subscriptionId + "] Error in MetricScanner", e.getMessage(), e);
}
if (!done) {
this.iter = scanner.iterator();
this.setName("Scan:" + subscriptionId);
this.flusher = this.ctx.executor().scheduleAtFixedRate(() -> {
flush();
}, flushIntervalSeconds, flushIntervalSeconds, TimeUnit.SECONDS);
}
}
public synchronized void flush() {
LOG.info("Flush called");
LOG.debug("[{}] Flush called", subscriptionId);
synchronized (responses) {
if (responses.size() > 0) {
try {
@@ -123,7 +193,7 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
this.ctx.writeAndFlush(new TextWebSocketFrame(json));
responses.clear();
} catch (JsonProcessingException e) {
LOG.error("Error serializing metrics: " + responses, e);
LOG.error("[" + subscriptionId + "] Error serializing metrics: " + responses, e);
}
}
}
@@ -133,7 +203,6 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
public void run() {
Metric m = null;
try {
boolean done = false;
while (!done && !closed) {
if (this.iter.hasNext()) {
@@ -145,7 +214,8 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
}
this.responses.addResponse(MetricResponse.fromMetric(m, this.subscriptionId));
} catch (Exception e1) {
LOG.error("Error {} parsing metric at key: {}", e1.getMessage(), e.getKey().toString());
LOG.error("[{}] Error {} parsing metric at key: {}", subscriptionId, e1.getMessage(),
e.getKey().toString());
}
} else if (rangeItr.hasNext()) {
// set next range on the scanner
@@ -158,9 +228,10 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
long endTimeStamp = (System.currentTimeMillis() - (lag * 1000));
this.scanner.close();
if (null == m) {
LOG.debug("No results found, waiting {}ms to retry with new end time {}.", delay, endTimeStamp);
LOG.debug("[{}] No results found, waiting {}ms to retry with new end time {}. [{}]",
subscriptionId, delay, endTimeStamp);
sleepUninterruptibly(delay, TimeUnit.MILLISECONDS);
ranges = this.store.getQueryRanges(metric, startTime, endTimeStamp, colFamValues);
ranges = this.store.getQueryRanges(metric, beginTime, endTimeStamp, colFamValues);
rangeItr = ranges.iterator();
if (rangeItr.hasNext()) {
Range r = rangeItr.next();
@@ -169,8 +240,9 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
}
} else {
// Reset the starting range to the last key returned
LOG.debug("Exhausted scanner, last metric returned was {}", m);
LOG.debug("Waiting {}ms to retry with new end time {}.", delay, endTimeStamp);
LOG.debug("[{}] Exhausted scanner, last metric returned was {} [{}]", subscriptionId, m);
LOG.debug("[{}] Waiting {}ms to retry with new end time {}. [{}]", subscriptionId, delay,
endTimeStamp);
sleepUninterruptibly(delay, TimeUnit.MILLISECONDS);
ranges = this.store.getQueryRanges(metric, m.getValue().getTimestamp() + 1, endTimeStamp,
colFamValues);
@@ -187,22 +259,24 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
done = true;
}
if (done) {
LOG.debug("Exhausted scanner for subscription {}", this.subscriptionId);
LOG.debug("[{}] Exhausted scanner", subscriptionId);
sendCompletedResponse();
}
}
} catch (Throwable e) {
LOG.error("Error in metric scanner, closing.", e);
LOG.error("[" + subscriptionId + "] Error in metric scanner, closing.", e);
} finally {
close();
this.scanner.close();
if (this.scanner != null) {
this.scanner.close();
}
subscription.scannerComplete(metric);
}
}
synchronized private void sendCompletedResponse() {
if (!completedResponseSent) {
LOG.debug("Sending completed response for subscription {}", this.subscriptionId);
LOG.info("[{}] Sending completed response", subscriptionId);
final MetricResponse completedResponse = new MetricResponse();
completedResponse.setSubscriptionId(this.subscriptionId);
completedResponse.setComplete(true);
@@ -214,16 +288,18 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
public void close() {
if (!closed) {
LOG.info("Marking metric scanner closed: {}", name);
LOG.debug("[{}] Marking metric scanner closed", subscriptionId);
sendCompletedResponse();
this.flusher.cancel(false);
if (this.flusher != null) {
this.flusher.cancel(false);
}
this.closed = true;
}
}
@Override
public void uncaughtException(Thread t, Throwable e) {
LOG.error("Error during metric scanner " + name, e);
LOG.error("[" + subscriptionId + "] Error during metric scanner", e);
this.close();
}
}
@@ -10,9 +10,11 @@ import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.api.request.AuthenticatedRequest;
import timely.api.response.TimelyException;
import timely.configuration.Configuration;
import timely.store.DataStore;
import timely.store.cache.DataStoreCache;
public class Subscription {
@@ -21,6 +23,7 @@ public class Subscription {
private final String sessionId;
private final DataStore store;
private final DataStoreCache cache;
private final ChannelHandlerContext ctx;
private final ScheduledFuture<?> ping;
private final int lag;
@@ -31,11 +34,12 @@ public class Subscription {
private final String subscriptionId;
private LinkedList<String> deadMetricScannerQueue = new LinkedList<>();
public Subscription(String subscriptionId, String sessionId, DataStore store, ChannelHandlerContext ctx,
Configuration conf) {
public Subscription(String subscriptionId, String sessionId, DataStore store, DataStoreCache cache,
ChannelHandlerContext ctx, Configuration conf) {
this.subscriptionId = subscriptionId;
this.sessionId = sessionId;
this.store = store;
this.cache = cache;
this.ctx = ctx;
this.lag = conf.getWebsocket().getSubscriptionLag();
this.scannerBatchSize = conf.getWebsocket().getScannerBatchSize();
@@ -45,17 +49,17 @@ public class Subscription {
// send a websocket ping at half the timeout interval.
int rate = conf.getWebsocket().getTimeout() / 2;
this.ping = this.ctx.executor().scheduleAtFixedRate(() -> {
LOG.trace("Sending ping on channel {}", ctx.channel());
LOG.trace("[{}] Sending ping on channel {}", subscriptionId, ctx.channel());
ctx.writeAndFlush(new PingWebSocketFrame());
cleanupCompletedMetrics();
}, rate, rate, TimeUnit.SECONDS);
}
public void addMetric(String metric, Map<String, String> tags, long startTime, long endTime, long delay)
throws TimelyException {
LOG.debug("Adding metric scanner for subscription {}", this.subscriptionId);
MetricScanner m = new MetricScanner(this, this.subscriptionId, this.sessionId, store, metric, tags, startTime,
endTime, delay, lag, ctx, scannerBatchSize, flushIntervalSeconds, scannerReadAhead,
public void addMetric(AuthenticatedRequest request, String metric, Map<String, String> tags, long startTime,
long endTime, long delay) throws TimelyException {
LOG.debug("[{}] Adding metric scanner", this.subscriptionId);
MetricScanner m = new MetricScanner(this, this.subscriptionId, this.sessionId, store, cache, metric, tags,
startTime, endTime, delay, lag, request, ctx, scannerBatchSize, flushIntervalSeconds, scannerReadAhead,
subscriptionBatchSize);
METRICS.put(metric, m);
m.start();
@@ -80,20 +84,20 @@ public class Subscription {
try {
m.join();
} catch (InterruptedException e) {
LOG.error("Error closing MetricScanner", e);
LOG.error("[" + this.subscriptionId + "] Error closing MetricScanner", e);
}
}
}
public void close() {
LOG.info("Closing subscriptions for {}", this.subscriptionId);
LOG.info("[{}] Closing subscriptions", this.subscriptionId);
this.ping.cancel(false);
METRICS.forEach((k, v) -> {
v.close();
try {
v.join();
} catch (InterruptedException e) {
LOG.error("Error closing MetricScanner", e);
LOG.error("[" + this.subscriptionId + "] Error closing MetricScanner", e);
}
});
METRICS.clear();
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.SubtypeResolver;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,18 +19,11 @@ public class JsonUtil {
static {
mapper.registerModule(new Jdk8Module());
mapper.registerModule(new GuavaModule());
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
// mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.configure(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS, true);
// Register the subtypes dynamically based on the WebSocket annotation
// instead of having
// to register them statically on this class using:
// @JsonSubTypes({ @Type(name = "create", value =
// CreateSubscription.class),
// @Type(name = "add", value = AddSubscription.class),
// @Type(name = "remove", value = RemoveSubscription.class),
// @Type(name = "close", value = CloseSubscription.class) })
SubtypeResolver resolver = mapper.getSubtypeResolver();
AnnotationResolver.getWebSocketClasses().forEach(c -> {
String name = c.getAnnotation(WebSocket.class).operation();
@@ -0,0 +1,2 @@
npe.ou.entries=iamnotaperson,npe,stillnotaperson
subject.dn.pattern=(?:^|,)\\s*OU\\s*My Department\\s*(?:,|$)
@@ -2,6 +2,7 @@ package timely.auth;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.UUID;
import org.apache.accumulo.core.security.Authorizations;
@@ -11,8 +12,8 @@ import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import timely.configuration.Configuration;
import timely.netty.http.auth.TimelyAuthenticationToken;
import timely.test.TestConfiguration;
public class AuthCacheTest {
@@ -29,10 +30,10 @@ public class AuthCacheTest {
@Before
public void setup() throws Exception {
AuthCache.setSessionMaxAge(config);
AuthCache.setSessionMaxAge(config.getSecurity());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", "test1");
Authentication auth = AuthenticationService.getAuthenticationManager().authenticate(token);
AuthCache.getCache().put(cookie, auth);
TimelyAuthenticationToken auth = AuthenticationService.authenticate(token);
AuthCache.getCache().put(cookie, auth.getTimelyPrincipal());
}
@After
@@ -47,7 +48,9 @@ public class AuthCacheTest {
@Test
public void testGetAuths() throws Exception {
Authorizations a = AuthCache.getAuthorizations(cookie);
Collection<Authorizations> auths = AuthCache.getAuthorizations(cookie);
Assert.assertEquals(1, auths.size());
Authorizations a = auths.iterator().next();
Assert.assertEquals("A,B,C", a.toString());
}
@@ -16,13 +16,13 @@ public class AuthenticationServiceTest {
@Test(expected = BadCredentialsException.class)
public void testBasicAuthenticationFailure() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", "test2");
AuthenticationService.getAuthenticationManager().authenticate(token);
AuthenticationService.authenticate(token);
}
@Test
public void testBasicAuthenticationLogin() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", "test1");
Authentication auth = AuthenticationService.getAuthenticationManager().authenticate(token);
Authentication auth = AuthenticationService.authenticate(token);
Collection<? extends GrantedAuthority> authorizations = auth.getAuthorities();
authorizations.forEach(a -> {
Assert.assertTrue(
@@ -32,9 +32,9 @@ public class AuthenticationServiceTest {
@Test
public void testX509AuthenticationLogin() {
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken("example.com",
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken("CN=example.com",
"doesn't matter what I put here");
Authentication auth = AuthenticationService.getAuthenticationManager().authenticate(token);
Authentication auth = AuthenticationService.authenticate(token);
Collection<? extends GrantedAuthority> authorizations = auth.getAuthorities();
authorizations.forEach(a -> {
Assert.assertTrue(
@@ -44,9 +44,9 @@ public class AuthenticationServiceTest {
@Test(expected = UsernameNotFoundException.class)
public void testX509AuthenticationLoginFailed() {
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken("bad.example.com",
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken("CN=bad.example.com",
"doesn't matter what I put here");
Authentication auth = AuthenticationService.getAuthenticationManager().authenticate(token);
Authentication auth = AuthenticationService.authenticate(token);
Collection<? extends GrantedAuthority> authorizations = auth.getAuthorities();
authorizations.forEach(a -> {
Assert.assertTrue(
@@ -31,11 +31,14 @@ import timely.api.request.timeseries.QueryRequest.RateOption;
import timely.api.request.timeseries.QueryRequest.SubQuery;
import timely.api.request.timeseries.SearchLookupRequest;
import timely.api.request.timeseries.SuggestRequest;
import timely.api.response.TimelyException;
import timely.auth.AuthCache;
import timely.auth.AuthenticationService;
import timely.configuration.Configuration;
import timely.model.Metric;
import timely.model.Tag;
import timely.netty.Constants;
import timely.netty.http.auth.TimelyAuthenticationToken;
import timely.test.TestConfiguration;
import timely.util.JsonUtil;
@@ -66,10 +69,12 @@ public class HttpRequestDecoderTest {
public static void before() throws Exception {
config = TestConfiguration.createMinimalConfigurationForTest();
anonConfig = TestConfiguration.createMinimalConfigurationForTest();
anonConfig.getSecurity().setAllowAnonymousAccess(true);
anonConfig.getSecurity().setAllowAnonymousHttpAccess(true);
cookie = URLEncoder.encode(UUID.randomUUID().toString(), StandardCharsets.UTF_8.name());
AuthCache.setSessionMaxAge(config);
AuthCache.getCache().put(cookie, new UsernamePasswordAuthenticationToken("test", "test1"));
AuthCache.setSessionMaxAge(config.getSecurity());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", "test1");
TimelyAuthenticationToken auth = AuthenticationService.authenticate(token);
AuthCache.getCache().put(cookie, auth.getTimelyPrincipal());
}
@AfterClass
@@ -96,7 +101,7 @@ public class HttpRequestDecoderTest {
Assert.assertEquals(request, results.get(0));
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = TimelyException.class)
public void testAggregatorsURINoSession() throws Exception {
decoder = new TestHttpQueryDecoder(config);
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
@@ -127,7 +132,7 @@ public class HttpRequestDecoderTest {
Assert.assertEquals(AggregatorsRequest.class, results.iterator().next().getClass());
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = TimelyException.class)
public void testAggregatorsPostNoSession() throws Exception {
decoder = new TestHttpQueryDecoder(config);
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST,
@@ -375,7 +380,7 @@ public class HttpRequestDecoderTest {
suggest.validate();
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = TimelyException.class)
public void testSuggestURIWithValidTypeNoSession() throws Exception {
decoder = new TestHttpQueryDecoder(config);
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
@@ -510,7 +515,7 @@ public class HttpRequestDecoderTest {
suggest.validate();
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = TimelyException.class)
public void testQueryWithNoSession() throws Exception {
decoder = new TestHttpQueryDecoder(config);
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
@@ -47,7 +47,7 @@ public class BasicAuthLoginRequestHandlerTest {
@BeforeClass
public static void before() throws Exception {
AuthCache.setSessionMaxAge(TestConfiguration.createMinimalConfigurationForTest());
AuthCache.setSessionMaxAge(TestConfiguration.createMinimalConfigurationForTest().getSecurity());
}
@AfterClass
@@ -28,6 +28,9 @@ import timely.api.request.timeseries.QueryRequest;
import timely.api.request.timeseries.SearchLookupRequest;
import timely.api.request.timeseries.SuggestRequest;
import timely.auth.AuthCache;
import timely.auth.TimelyPrincipal;
import timely.auth.TimelyUser;
import timely.auth.util.AuthenticationUtils;
import timely.configuration.Configuration;
import timely.model.Metric;
import timely.subscription.SubscriptionRegistry;
@@ -60,10 +63,12 @@ public class WebSocketRequestDecoderTest {
public static void before() throws Exception {
config = TestConfiguration.createMinimalConfigurationForTest();
anonConfig = TestConfiguration.createMinimalConfigurationForTest();
anonConfig.getSecurity().setAllowAnonymousAccess(true);
anonConfig.getSecurity().setAllowAnonymousWsAccess(true);
cookie = URLEncoder.encode(UUID.randomUUID().toString(), StandardCharsets.UTF_8.name());
AuthCache.setSessionMaxAge(config);
AuthCache.getCache().put(cookie, new UsernamePasswordAuthenticationToken("test", "test1"));
AuthCache.setSessionMaxAge(config.getSecurity());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", "test1");
AuthCache.getCache().put(cookie,
new TimelyPrincipal(new TimelyUser("ANONYMOUS", AuthenticationUtils.getAuthCollection(token))));
}
@AfterClass
@@ -35,7 +35,7 @@ public class TestDataStoreCacheIterator {
@BeforeClass
public static void setup() {
configuration = new Configuration();
configuration.getSecurity().setAllowAnonymousAccess(true);
configuration.getSecurity().setAllowAnonymousHttpAccess(true);
HashMap<String, Integer> ageoffs = new HashMap<>();
ageoffs.put(DataStoreCache.DEFAULT_AGEOFF_KEY, Integer.MAX_VALUE);
configuration.getCache().setMetricAgeOffHours(ageoffs);
@@ -120,7 +120,8 @@ public class TestDataStoreCacheIterator {
long firstTimestamp = -1;
long lastTimestamp = -1;
int numSamples = 0;
itr = mmStore.setupIterator(query, subQuery, new Authorizations(), Long.MAX_VALUE);
itr = mmStore.setupIterator(query, subQuery, Collections.singletonList(Authorizations.EMPTY),
Long.MAX_VALUE);
while (itr.hasTop()) {
itr.next();
Map<Set<Tag>, Aggregation> aggregations = AggregationIterator.decodeValue(itr.getTopValue());
@@ -171,7 +172,8 @@ public class TestDataStoreCacheIterator {
long firstTimestamp = -1;
long lastTimestamp = -1;
int numSamples = 0;
itr = mmStore.setupIterator(query, subQuery, new Authorizations(), Long.MAX_VALUE);
itr = mmStore.setupIterator(query, subQuery, Collections.singletonList(Authorizations.EMPTY),
Long.MAX_VALUE);
while (itr.hasTop()) {
itr.next();
Map<Set<Tag>, Aggregation> aggregations = AggregationIterator.decodeValue(itr.getTopValue());
@@ -18,7 +18,7 @@ public class TestGorillaStore {
@BeforeClass
public static void setup() {
configuration = new Configuration();
configuration.getSecurity().setAllowAnonymousAccess(true);
configuration.getSecurity().setAllowAnonymousHttpAccess(true);
}
@Test
@@ -21,7 +21,7 @@ public class TestMemoryDataStore {
@BeforeClass
public static void setup() {
configuration = new Configuration();
configuration.getSecurity().setAllowAnonymousAccess(true);
configuration.getSecurity().setAllowAnonymousHttpAccess(true);
configuration.getCache().setDefaultAgeOffHours(24);
}
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.conf.AccumuloConfiguration;
import org.apache.accumulo.core.data.ByteSequence;
import org.apache.accumulo.core.data.Key;
@@ -63,6 +64,20 @@ public class IteratorTestBase {
throw new UnsupportedOperationException();
}
@Override
public IteratorEnvironment cloneWithSamplingEnabled() {
throw new UnsupportedOperationException();
}
@Override
public SamplerConfiguration getSamplerConfiguration() {
throw new UnsupportedOperationException();
}
@Override
public boolean isSamplingEnabled() {
return false;
}
}
protected static class CombinerIteratorEnvironment extends DefaultIteratorEnvironment {
@@ -42,7 +42,7 @@ public class OneWaySSLBase extends QueryBase {
config.getSecurity().getServerSsl().setKeyFile(serverCert.privateKey().getAbsolutePath());
config.getSecurity().getServerSsl().setUseOpenssl(false);
config.getSecurity().getServerSsl().setUseGeneratedKeypair(false);
config.getSecurity().setAllowAnonymousAccess(true);
config.getSecurity().setAllowAnonymousHttpAccess(true);
}
@Before
@@ -52,7 +52,7 @@ public class TwoWaySSLFailureIT extends QueryBase {
static {
try {
// This fqdn does not match what is in security.xml
serverCert = new SelfSignedCertificate("bad.example.com");
serverCert = new SelfSignedCertificate("CN=bad.example.com");
clientTrustStoreFile = serverCert.certificate().getAbsoluteFile();
} catch (Exception e) {
throw new RuntimeException("Error creating self signed certificate", e);
@@ -80,7 +80,7 @@ public class TwoWaySSLFailureIT extends QueryBase {
config.getSecurity().getServerSsl().setTrustStoreFile(serverCert.certificate().getAbsolutePath());
config.getSecurity().getServerSsl().setUseOpenssl(false);
config.getSecurity().getServerSsl().setUseGeneratedKeypair(false);
config.getSecurity().setAllowAnonymousAccess(false);
config.getSecurity().setAllowAnonymousHttpAccess(false);
}
protected HttpsURLConnection getUrlConnection(URL url) throws Exception {
@@ -96,7 +96,7 @@ public class TwoWaySSLIT extends QueryBase {
config.getSecurity().getServerSsl().setTrustStoreFile(serverCert.certificate().getAbsolutePath());
config.getSecurity().getServerSsl().setUseOpenssl(false);
config.getSecurity().getServerSsl().setUseGeneratedKeypair(false);
config.getSecurity().setAllowAnonymousAccess(false);
config.getSecurity().setAllowAnonymousHttpAccess(false);
}
protected HttpsURLConnection getUrlConnection(URL url) throws Exception {
@@ -78,7 +78,7 @@ public class TwoWaySSLOpenSSLIT extends QueryBase {
config.getSecurity().getServerSsl().setTrustStoreFile(serverCert.certificate().getAbsolutePath());
config.getSecurity().getServerSsl().setUseOpenssl(false);
config.getSecurity().getServerSsl().setUseGeneratedKeypair(false);
config.getSecurity().setAllowAnonymousAccess(false);
config.getSecurity().setAllowAnonymousHttpAccess(false);
}
@Before
@@ -93,7 +93,7 @@ public class WebSocketClientIT extends OneWaySSLBase {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(new MessageHandler.Whole<String>() {
session.addMessageHandler(String.class, new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
@@ -147,28 +147,33 @@ public class WebSocketClientIT extends OneWaySSLBase {
@Test
public void testClientAnonymousAccess() throws Exception {
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslCtx, "localhost", 54322, 54323, false,
null, null, false, 65536);
testWorkflow(client);
false, null, null, false, 65536);
try {
conf.getSecurity().setAllowAnonymousWsAccess(true);
testWorkflow(client);
} finally {
conf.getSecurity().setAllowAnonymousWsAccess(false);
}
}
@Test
public void testClientBasicAuthAccess() throws Exception {
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslCtx, "localhost", 54322, 54323, true,
"test", "test1", false, 65536);
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslCtx, "localhost", 54322, 54323, false,
true, "test", "test1", false, 65536);
testWorkflow(client);
}
@Test(expected = HttpResponseException.class)
public void testClientBasicAuthAccessFailure() throws Exception {
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslCtx, "localhost", 54322, 54323, true,
"test", "test2", false, 65536);
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslCtx, "localhost", 54322, 54323, false,
true, "test", "test2", false, 65536);
testWorkflow(client);
}
@Test(expected = IllegalArgumentException.class)
public void testClientBasicAuthParameterMismatch() throws Exception {
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslCtx, "localhost", 54322, 54323, true,
"test", null, false, 65536);
WebSocketSubscriptionClient client = new WebSocketSubscriptionClient(sslCtx, "localhost", 54322, 54323, false,
true, "test", null, false, 65536);
testWorkflow(client);
}
@@ -517,7 +517,7 @@ public class HttpApiIT extends OneWaySSLBase {
AtomicInteger rack1Count = new AtomicInteger(0);
AtomicInteger rack2Count = new AtomicInteger(0);
for(QueryResponse response: responses){
assertTrue("Found incorrect metric", "sys.cpu.idle".equals(response.getMetric()));
assertEquals("Found incorrect metric", "sys.cpu.idle", response.getMetric());
response.getTags().forEach((tagk, tagv) -> {
switch (tagk){
case "rack":
@@ -50,7 +50,6 @@ import org.junit.experimental.categories.Category;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import timely.Server;
import timely.api.request.VersionRequest;
import timely.api.request.subscription.AddSubscription;
@@ -68,9 +67,11 @@ import timely.api.response.timeseries.SearchLookupResponse.Result;
import timely.api.response.timeseries.SuggestResponse;
import timely.auth.AuthCache;
import timely.auth.AuthenticationService;
import timely.auth.TimelyPrincipal;
import timely.model.Metric;
import timely.model.Tag;
import timely.netty.Constants;
import timely.netty.http.auth.TimelyAuthenticationToken;
import timely.test.IntegrationTest;
import timely.test.TestConfiguration;
import timely.test.integration.OneWaySSLBase;
@@ -195,7 +196,7 @@ public class WebSocketIT extends OneWaySSLBase {
con.securityOperations().changeUserAuthorizations("root", new Authorizations("A", "B", "C", "D", "E", "F"));
this.sessionId = UUID.randomUUID().toString();
AuthCache.getCache().put(sessionId, token);
AuthCache.getCache().put(sessionId, TimelyPrincipal.anonymousPrincipal());
group = new NioEventLoopGroup();
SslContext ssl = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
@@ -504,8 +505,9 @@ public class WebSocketIT extends OneWaySSLBase {
@Test
public void testSubscriptionWorkflowWithEndTimeAndVisibilities() throws Exception {
Authentication auth = AuthenticationService.getAuthenticationManager().authenticate(token);
AuthCache.getCache().put(sessionId, auth);
TimelyAuthenticationToken auth = AuthenticationService.authenticate(token);
TimelyPrincipal principal = auth.getTimelyPrincipal();
AuthCache.getCache().put(sessionId, principal);
try {
final String subscriptionId = "1234";
@@ -583,7 +585,7 @@ public class WebSocketIT extends OneWaySSLBase {
close.setSubscriptionId(subscriptionId);
ch.writeAndFlush(new TextWebSocketFrame(JsonUtil.getObjectMapper().writeValueAsString(close)));
} finally {
AuthCache.getCache().asMap().remove(sessionId, auth);
AuthCache.getCache().asMap().remove(sessionId, principal);
ch.close().sync();
s.shutdown();
group.shutdownGracefully();
@@ -0,0 +1,2 @@
npe.ou.entries=iamnotaperson,npe,stillnotaperson
subject.dn.pattern=(?:^|,)\\s*OU\\s*My Department\\s*(?:,|$)
+1 -1
View File
@@ -16,7 +16,7 @@
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
<beans:property name="users">
<util:map key-type="java.lang.String">
<beans:entry key="example.com">
<beans:entry key="CN=example.com">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>