Compare commits

...
Author SHA1 Message Date
Bill Oley 7694d0afe7 Update dependencies 2019-04-01 17:01:00 -04:00
Bill Oley dba494b06d Bump pom versions to 1.2.1-SNAPSHOT 2019-02-25 15:41:31 -05:00
Bill Oley 5826884c3c Tag version 1.2.0 2019-02-25 15:11:57 -05:00
Bill Oley f94c731bbc Remove unused modeule directory introduced in error 2019-02-25 15:11:12 -05:00
Bill Oley 4e042bcb94 Allow all http methods in grafana auth proxy, disable cookies in HttpPools 2019-02-22 16:17:36 -05:00
Bill Oley ab7f499134 closes #180 Allow a configurable default visibility for metrics 2019-02-21 15:33:25 -05:00
Bill Oley c098f9f3e4 re #179 Create an authentication app for grafana that uses Timely's authentication framework and grafana's auth-proxy
re #179 Create authentication proxy for grafana
2019-02-21 13:28:12 -05:00
Bill Oley 94cfcbc0c4 Allow authentication provider to return roles, check users against requiredRoles and requiredAuths for access
index on master: 674e1b0 Improve synchronization in AuthenticationService

Check validated users against requiredRoles and requiredAuths

Conflicts:
	server/conf/security.xml
	server/src/main/java/timely/auth/AuthenticationService.java

More work
2019-02-15 11:24:07 -05:00
Bill Oley 674e1b0ab5 Improve synchronization in AuthenticationService 2019-02-08 16:53:35 -05:00
Bill Oley 4eedb84547 Add synchronization to AuthenticationService to prevent simultaneous authentications of the same user by multiple threads 2019-02-07 17:17:08 -05:00
Bill Oley bef86eac37 Fix websocket proxying with a client certificate, add ability to configure client cert in python websocket client 2019-02-07 17:16:00 -05:00
Bill Oley 27bb16e403 re #178 Allow proxying of http and ws requests
More work

Correct errors
2019-02-05 11:41:35 -05:00
115 changed files with 4187 additions and 545 deletions

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.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());
}
@@ -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
@@ -1,4 +1,4 @@
#!/bin/bash -vx
#!/bin/bash
if [[ `uname` == "Darwin" ]]; then
THIS_SCRIPT=`python -c 'import os,sys; print os.path.realpath(sys.argv[1])' $0`
+39 -17
View File
@@ -11,21 +11,33 @@
<beans:bean id="x509PrincipalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor">
<beans:property name="subjectDnRegex" value="CN=(.*?)(?:,|$)"/>
</beans:bean>
</beans:bean>
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
<beans:property name="users">
<util:map key-type="java.lang.String">
<beans:entry key="example.com">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>
<beans:value>F</beans:value>
</util:list>
</beans:entry>
</util:map>
</beans:property>
</beans:bean>
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
<beans:property name="users">
<util:list>
<beans:bean class="timely.auth.TimelyUser">
<beans:constructor-arg name="subjectDn">
<beans:value>CN=example.com</beans:value>
</beans:constructor-arg>
<beans:constructor-arg name="auths">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>
<beans:value>F</beans:value>
</util:list>
</beans:constructor-arg>
<beans:constructor-arg name="roles">
<util:list>
<beans:value>G</beans:value>
<beans:value>H</beans:value>
<beans:value>I</beans:value>
</util:list>
</beans:constructor-arg>
</beans:bean>
</util:list>
</beans:property>
</beans:bean>
<beans:bean id="x509AuthorizationProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<beans:property name="preAuthenticatedUserDetailsService" ref="timelyUserDetailsService" />
@@ -34,9 +46,12 @@
<user-service id="usernamePasswordService">
<user name="test" password="test1" authorities="A,B,C" />
</user-service>
<beans:bean id="UserNamePasswordAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:bean id="noOpPasswordEncoder" class="org.springframework.security.crypto.password.NoOpPasswordEncoder"/>
<beans:bean id="UserNamePasswordAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="usernamePasswordService"/>
<beans:property name="passwordEncoder" ref="noOpPasswordEncoder"/>
</beans:bean>
<authentication-manager alias="authenticationManager">
@@ -45,5 +60,12 @@
<!-- Basic authorization provider, validates provided username and password -->
<authentication-provider ref="UserNamePasswordAuthenticationProvider" />
</authentication-manager>
<util:list id="requiredRoles" value-type="java.lang.String">
<beans:value></beans:value>
</util:list>
<util:list id="requiredAuths" value-type="java.lang.String">
<beans:value></beans:value>
</util:list>
</beans:beans>
+2 -1
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:
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.1-SNAPSHOT</version>
</parent>
<artifactId>timely-balancer</artifactId>
<dependencies>
@@ -64,8 +64,10 @@ import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.WebApplicationType;
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 +93,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;
@@ -142,7 +144,7 @@ public class Balancer {
}
protected static ConfigurableApplicationContext initializeConfiguration(String[] args) {
return new SpringApplicationBuilder(SpringBootstrap.class).web(false).run(args);
return new SpringApplicationBuilder(SpringBootstrap.class).web(WebApplicationType.NONE).run(args);
}
private void shutdownHook() {
@@ -362,6 +364,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;
@@ -427,7 +430,8 @@ public class Balancer {
httpServer.group(httpBossGroup, httpWorkerGroup);
httpServer.channel(channelClass);
httpServer.handler(new LoggingHandler());
HttpClientPool httpClientPool = new HttpClientPool(balancerConfig, clientSSLContext);
HttpClientPool httpClientPool = new HttpClientPool(balancerConfig.getSecurity(), balancerConfig.getHttp(),
clientSSLContext);
httpServer.childHandler(setupHttpChannel(balancerConfig, sslCtx, metricResolver, httpClientPool));
httpServer.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
httpServer.option(ChannelOption.SO_BACKLOG, 128);
@@ -507,15 +511,14 @@ public class Balancer {
ch.pipeline().addLast("decoder", new HttpRequestDecoder());
ch.pipeline().addLast("compressor", new HttpContentCompressor());
ch.pipeline().addLast("decompressor", new HttpContentDecompressor());
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(8192));
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("chunker", new ChunkedWriteHandler());
ch.pipeline().addLast("queryDecoder", new timely.netty.http.HttpRequestDecoder(
balancerConfig.getSecurity(), balancerConfig.getHttp()));
ch.pipeline().addLast("fileServer", new HttpStaticFileServerHandler());
ch.pipeline().addLast("login",
new X509LoginRequestHandler(balancerConfig.getSecurity(), balancerConfig.getHttp()));
ch.pipeline().addLast("httpRelay",
new HttpRelayHandler(balancerConfig, metricResolver, httpClientPool));
ch.pipeline().addLast("httpRelay", new HttpRelayHandler(metricResolver, httpClientPool));
ch.pipeline().addLast("error", new TimelyExceptionHandler());
}
};
@@ -529,7 +532,7 @@ public class Balancer {
ch.pipeline().addLast("logger", new LoggingHandler());
ch.pipeline().addLast("packetDecoder", new UdpPacketToByteBuf());
ch.pipeline().addLast("buffer", new MetricsBufferDecoder());
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(8192, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(65536, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("putDecoder", new UdpDecoder());
ch.pipeline().addLast("udpRelayHandler", new UdpRelayHandler(metricResolver, udpClientPool));
}
@@ -542,7 +545,7 @@ public class Balancer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("buffer", new MetricsBufferDecoder());
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(8192, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(65536, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("putDecoder", new TcpDecoder());
ch.pipeline().addLast("tcpRelayHandler", new TcpRelayHandler(metricResolver, tcpClientPools));
ch.pipeline().addLast("versionHandler", new TcpVersionHandler());
@@ -558,8 +561,8 @@ public class Balancer {
protected void initChannel(SocketChannel ch) throws Exception {
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("aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("sessionExtractor", new WebSocketFullRequestHandler());
ch.pipeline().addLast("idle-handler",
new IdleStateHandler(balancerConfig.getWebsocket().getTimeout(), 0, 0));
@@ -5,8 +5,10 @@ import javax.validation.Valid;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import timely.configuration.Cache;
@Validated
@Component
@ConfigurationProperties(prefix = "timely-balancer")
public class BalancerConfiguration {
@@ -8,6 +8,8 @@ public class ClientSsl {
private String trustStoreFile;
private String trustStoreType;
private String trustStorePassword;
private boolean useClientCert = true;
private boolean hostVerificationEnabled = true;
public String getKeyFile() {
return keyFile;
@@ -55,6 +57,21 @@ public class ClientSsl {
public void setTrustStorePassword(String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}
public void setUseClientCert(boolean useClientCert) {
this.useClientCert = useClientCert;
}
public boolean isUseClientCert() {
return useClientCert;
}
public void setHostVerificationEnabled(boolean hostVerificationEnabled) {
this.hostVerificationEnabled = hostVerificationEnabled;
}
public boolean isHostVerificationEnabled() {
return hostVerificationEnabled;
}
}
@@ -5,22 +5,25 @@ import javax.net.ssl.SSLContext;
import org.apache.commons.pool2.KeyedPooledObjectFactory;
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.BalancerSecurity;
import timely.balancer.connection.TimelyBalancedHost;
public class HttpClientFactory implements KeyedPooledObjectFactory<TimelyBalancedHost, CloseableHttpClient> {
private final SSLContext sslContext;
private final BalancerSecurity security;
public HttpClientFactory(SSLContext sslContext) {
public HttpClientFactory(BalancerSecurity security, SSLContext sslContext) {
this.security = security;
this.sslContext = sslContext;
}
@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));
// disable cookie management because we are sharing connections
return new DefaultPooledObject<>(timely.client.http.HttpClient.get(this.sslContext, null,
security.getClientSsl().isHostVerificationEnabled(), security.getClientSsl().isUseClientCert()));
}
@Override
@@ -6,14 +6,15 @@ import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import timely.balancer.configuration.BalancerConfiguration;
import timely.balancer.configuration.BalancerHttp;
import timely.balancer.configuration.BalancerSecurity;
import timely.balancer.connection.TimelyBalancedHost;
public class HttpClientPool extends GenericKeyedObjectPool<TimelyBalancedHost, CloseableHttpClient> {
private static final Logger LOG = LoggerFactory.getLogger(HttpClientPool.class);
public HttpClientPool(BalancerConfiguration config, SSLContext sslContext) {
super(new HttpClientFactory(sslContext), config.getHttp().getHttpClientPool());
public HttpClientPool(BalancerSecurity balancerSecurity, BalancerHttp http, SSLContext sslContext) {
super(new HttpClientFactory(balancerSecurity, sslContext), http.getHttpClientPool());
}
}
@@ -12,13 +12,11 @@ import timely.client.websocket.subscription.WebSocketSubscriptionClient;
public class WsClientFactory implements KeyedPooledObjectFactory<TimelyBalancedHost, WebSocketSubscriptionClient> {
private final SSLContext sslContext;
private final boolean doLogin;
private BalancerConfiguration balancerConfig;
public WsClientFactory(BalancerConfiguration balancerConfig, SSLContext sslContext) {
this.sslContext = sslContext;
this.doLogin = balancerConfig.isLoginRequired();
this.balancerConfig = balancerConfig;
this.sslContext = sslContext;
}
@Override
@@ -26,7 +24,9 @@ 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(), balancerConfig.getSecurity().getClientSsl().isUseClientCert(),
balancerConfig.isLoginRequired(), "", "",
balancerConfig.getSecurity().getClientSsl().isHostVerificationEnabled(), bufferSize);
return new DefaultPooledObject<>(client);
}
@@ -3,7 +3,6 @@ package timely.balancer.netty.http;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -12,6 +11,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,26 +33,26 @@ 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.balancer.configuration.BalancerConfiguration;
import timely.auth.util.HttpHeaderUtils;
import timely.auth.util.ProxiedEntityUtils;
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 {
private static final Logger LOG = LoggerFactory.getLogger(HttpRelayHandler.class);
private final BalancerConfiguration balancerConf;
private final HttpClientPool httpClientPool;
private MetricResolver metricResolver;
public HttpRelayHandler(BalancerConfiguration balancerConf, MetricResolver metricResolver,
HttpClientPool httpClientPool) {
this.balancerConf = balancerConf;
public HttpRelayHandler(MetricResolver metricResolver, HttpClientPool httpClientPool) {
this.metricResolver = metricResolver;
this.httpClientPool = httpClientPool;
}
@@ -86,13 +86,21 @@ public class HttpRelayHandler extends SimpleChannelInboundHandler<HttpRequest> i
} else {
k = metricResolver.getHostPortKey(null);
}
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 +151,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 +160,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);
}
@@ -197,13 +202,4 @@ public class HttpRelayHandler extends SimpleChannelInboundHandler<HttpRequest> i
}
return s;
}
public static void main(String[] args) {
String s = "https://127.0.0.1:4243/api/query?start=1356998400000&end=1528398304000&m=sum:rate{false,100,0}:sys.thermal.gauge{class=aws}";
URI uri = URI.create(encodeURI(s));
System.out.println(uri.toString());
}
}
@@ -1,11 +1,16 @@
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.Session;
import com.google.common.collect.HashMultimap;
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;
@@ -14,22 +19,45 @@ 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) {
if (token.getClientCert() != null) {
Multimap<String, String> proxyRequestHeaders = HashMultimap.create();
ProxiedEntityUtils.addProxyHeaders(proxyRequestHeaders, token.getClientCert());
for (String s : proxyRequestHeaders.keySet()) {
headers.put(s, new ArrayList(proxyRequestHeaders.get(s)));
}
}
Multimap<String, String> originalRequestHeaders = token.getHttpHeaders();
for (String s : originalRequestHeaders.keySet()) {
if (!headers.containsKey(s)) {
// add pre-existing values if key does not exist in proxyRequest
headers.put(s, new ArrayList(originalRequestHeaders.get(s)));
}
}
}
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(String.class, 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();
@@ -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();
}
@@ -225,10 +225,10 @@ public class BalancedMetricResolver implements MetricResolver {
nonCachedMetricsLocalLock.writeLock().lock();
try {
if (nonCachedMetrics.containsAll(currentNonCachedMetricsDistributed)) {
LOG.info("local nonCachedMetrics already contains {}", currentNonCachedMetricsDistributed);
LOG.debug("local nonCachedMetrics already contains {}", currentNonCachedMetricsDistributed);
} else {
currentNonCachedMetricsDistributed.removeAll(nonCachedMetrics);
LOG.info("Adding {} to local nonCachedMetrics", currentNonCachedMetricsDistributed);
LOG.debug("Adding {} to local nonCachedMetrics", currentNonCachedMetricsDistributed);
nonCachedMetrics.addAll(currentNonCachedMetricsDistributed);
}
} catch (Exception e) {
@@ -505,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;
@@ -633,7 +633,7 @@ 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);
@@ -652,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);
@@ -720,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;
}
@@ -769,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();
@@ -982,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 {
+2 -2
View File
@@ -4,12 +4,12 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.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>1.1.1.dev0</python.client.version>
<python.client.version>1.2.1.dev0</python.client.version>
</properties>
<dependencies>
<dependency>
@@ -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,31 @@ 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) {
HostnameVerifier hostnameVerifier;
if (hostVerificationEnabled) {
builder.setSSLHostnameVerifier(new DefaultHostnameVerifier());
hostnameVerifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
} else {
builder.setSSLHostnameVerifier(new NoopHostnameVerifier());
hostnameVerifier = new NoopHostnameVerifier();
}
RequestConfig defaultRequestConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build();
HttpClientBuilder builder;
if (cookieStore == null) {
builder = HttpClients.custom().setSSLContext(ssl).disableCookieManagement()
.setDefaultRequestConfig(defaultRequestConfig).setSSLHostnameVerifier(hostnameVerifier);
} else {
builder = HttpClients.custom().setSSLContext(ssl).setDefaultCookieStore(cookieStore)
.setDefaultRequestConfig(defaultRequestConfig).setSSLHostnameVerifier(hostnameVerifier);
}
if (clientAuth) {
SSLConnectionSocketFactory sslCSF = new SSLConnectionSocketFactory(ssl,
new String[] { "TLSv1.1", "TLSv1.2" }, null, hostnameVerifier);
builder.setSSLSocketFactory(sslCSF);
}
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);
}
};
}
@@ -40,6 +40,7 @@ public class WebSocketClient implements AutoCloseable {
private final int timelyHttpsPort;
private final int timelyWssPort;
private final boolean doLogin;
private final boolean clientAuth;
private final String timelyUsername;
private final String timelyPassword;
private final boolean hostVerificationEnabled;
@@ -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);
}
@@ -23,7 +23,7 @@ def pivot(df, metric, groupByColumn=None):
return dataFrame
def resample(df, sample, how='mean'):
def resample(df, sample, how='mean', interpolate=True, fill=None):
dataFrame = pandas.DataFrame(df, copy=True)
if dataFrame is not None:
@@ -16,14 +16,15 @@ utc = UTC()
class TimelyWebSocketClient(WebSocketClient):
def __init__(self, hostport, metric, tags, startTime, endTime, connect_timeout=WebSocketClient.DEFAULT_CONNECT_TIMEOUT,
request_timeout=WebSocketClient.DEFAULT_REQUEST_TIMEOUT):
request_timeout=WebSocketClient.DEFAULT_REQUEST_TIMEOUT, client_cert=None, client_key=None, ca_certs=None, validate_cert=False):
self.metric = metric
self.tags = tags
self.startTime = startTime
self.endTime = endTime
self.subscriptionId = random.randint(1000000000, 9999999999)
WebSocketClient.__init__(self, hostport, '/websocket', {'metric' : metric}, connect_timeout, request_timeout)
WebSocketClient.__init__(self, hostport, '/websocket', {'metric' : metric}, connect_timeout, request_timeout,
client_cert=client_cert, client_key=client_key, ca_certs=ca_certs, validate_cert=validate_cert)
def _on_message(self, msg):
# implement in subclass
@@ -72,7 +73,8 @@ class TimelyMetric(TimelyWebSocketClient):
client = None
endtime = None
def __init__(self, hostport, metric, tags, beginStr, endStr, period, sample):
def __init__(self, hostport, metric, tags, beginStr, endStr, period, sample,
client_cert=None, client_key=None, ca_certs=None, validate_cert=False):
self.series = pandas.Series()
self.metric = metric
self.tags = tags
@@ -82,7 +84,8 @@ class TimelyMetric(TimelyWebSocketClient):
self.data = []
self.timeDateRange = TimeDateRange(beginStr, endStr, period)
TimelyWebSocketClient.__init__(self, hostport, metric, tags, self.timeDateRange.getBeginMs(), self.timeDateRange.getEndMs())
TimelyWebSocketClient.__init__(self, hostport, metric, tags, self.timeDateRange.getBeginMs(), self.timeDateRange.getEndMs(),
client_cert=client_cert, client_key=client_key, ca_certs=ca_certs, validate_cert=validate_cert)
def debugOn(self):
self.debug = True
@@ -15,10 +15,14 @@ class WebSocketClient():
APPLICATION_JSON = 'application/json'
def __init__(self, hostport, path, params, connect_timeout=DEFAULT_CONNECT_TIMEOUT,
request_timeout=DEFAULT_REQUEST_TIMEOUT):
request_timeout=DEFAULT_REQUEST_TIMEOUT, client_cert=None, client_key=None, ca_certs=None, validate_cert=False):
self.connect_timeout = connect_timeout
self.request_timeout = request_timeout
self.client_cert=client_cert
self.client_key=client_key
self.ca_certs=ca_certs
self.validate_cert=validate_cert
self.url = 'wss://' + hostport
if (path is not None and len(path) > 0):
if (path.startswith('/')):
@@ -39,7 +43,8 @@ class WebSocketClient():
"""Connect to the server.
:param str url: server URL.
"""
request = websocket.httpclient.HTTPRequest(self.url, connect_timeout=self.connect_timeout, request_timeout=self.request_timeout, validate_cert=False)
request = websocket.httpclient.HTTPRequest(self.url, connect_timeout=self.connect_timeout, request_timeout=self.request_timeout, validate_cert=self.validate_cert,
client_cert=self.client_cert, client_key=self.client_key, ca_certs=self.ca_certs)
websocket.websocket_connect(request, io_loop=ioloop, callback=self._connect_callback)
def send(self, data):
@@ -5,7 +5,9 @@ import os
from timely.TimelyAnalyticConfiguration import TimelyAnalyticConfiguration
hostport = "127.0.0.1:5244"
timelyMetric = timely.TimelyMetric.TimelyMetric(hostport, 'timely.keys.metric.inserted', None, None, None, '24 hours', None).fetch()
client_cert = None
client_key = None
timelyMetric = timely.TimelyMetric.TimelyMetric(hostport, 'timely.keys.metric.inserted', None, None, None, '24 hours', None, client_cert=client_cert, client_key=client_key).fetch()
print(timelyMetric.getDataFrame())
exit()
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.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.1.1-SNAPSHOT</version>
<version>1.2.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.1.1-SNAPSHOT</version>
<version>1.2.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.1.1-SNAPSHOT</version>
<version>1.2.1-SNAPSHOT</version>
</parent>
<artifactId>timely-common</artifactId>
<dependencies>
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
if [[ `uname` == "Darwin" ]]; then
THIS_SCRIPT=`python -c 'import os,sys; print os.path.realpath(sys.argv[1])' $0`
TCNATIVE_SUFFIX="jnilib"
else
THIS_SCRIPT=`readlink -f $0`
TCNATIVE_SUFFIX="so"
fi
THIS_DIR="${THIS_SCRIPT%/*}"
NATIVE_DIR="${THIS_DIR}/META-INF/native"
BASE_DIR=${THIS_DIR}/..
TMP_DIR="${BASE_DIR}/tmp"
CONF_DIR="${BASE_DIR}/conf"
LIB_DIR="${BASE_DIR}/lib"
NUM_SERVER_THREADS=4
if [[ -e ${TMP_DIR} ]]; then
rm -rf ${TMP_DIR}
fi
mkdir ${TMP_DIR}
if [[ -e ${NATIVE_DIR} ]]; then
rm -rf ${NATIVE_DIR}
fi
mkdir -p ${NATIVE_DIR}
pushd ${BASE_DIR}/bin
$JAVA_HOME/bin/jar xf ${LIB_DIR}/netty-tcnative*.jar META-INF/native/libnetty_tcnative.${TCNATIVE_SUFFIX}
$JAVA_HOME/bin/jar xf ${LIB_DIR}/netty-transport-native-epoll*.jar META-INF/native/libnetty_transport_native_epoll_x86_64.${TCNATIVE_SUFFIX}
popd
export CLASSPATH="${CONF_DIR}:${LIB_DIR}/*:${HADOOP_CONF_DIR}"
JVM_ARGS="-Xmx1G -Xms1G -Dio.netty.eventLoopThreads=${NUM_SERVER_THREADS}"
JVM_ARGS="${JVM_ARGS} -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector"
JVM_ARGS="${JVM_ARGS} -Djava.library.path=${NATIVE_DIR}"
JVM_ARGS="${JVM_ARGS} -Djava.library.path=${NATIVE_DIR}/libnetty-${TCNATIVE_SUFFIX}"
#JVM_ARGS="${JVM_ARGS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
echo "$JAVA_HOME/bin/java ${JVM_ARGS} timely.grafana.auth.GrafanaAuth --spring.config.name=grafana-auth"
$JAVA_HOME/bin/java ${JVM_ARGS} timely.grafana.auth.GrafanaAuth --spring.config.name=grafana-auth
+53
View File
@@ -0,0 +1,53 @@
grafana-auth:
grafana:
host: localhost
port: 3001
security:
session-max-age: 86400
allow-anonymous-http-access: true
allow-anonymous-ws-access: true
serverSsl:
certificate-file:
key-file:
#key-password:
use-generated-keypair: true
trust-store-file:
use-openssl: true
#use-ciphers:
clientSsl:
keyFile:
keyType: pkcs12
#keyPassword:
trustStoreFile:
trustStoreType: jks
#trustStorePassword:
http:
host: localhost
ip: 127.0.0.1
port: 3000
redirect-path: /secure-me
strict-transport-max-age: 608400
cors:
allow-any-origin: false
allow-null-origin: false
allow-credentials: true
allowed-origins:
allowed-methods: DELETE,GET,HEAD,OPTIONS,PUT,POST
allowed-headers: content-type
httpClientPool:
minIdlePerKey: 0
maxIdlePerKey: 40
maxTotalPerKey: 40
maxTotal: -1
lifo: true
fairness: false
maxWaitMillis: -1
minEvictableIdleTimeMillis: 1800000
evictorShutdownTimeoutMillis: 10000
softMinEvictableIdleTimeMillis: -1
numTestsPerEvictionRun: 3
timeBetweenEvictionRunsMillis: -1
blockWhenExhausted: true
jmxEnabled: true
testOnBorrow: false
testOnReturn: false
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" monitorInterval="60">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="error" includeLocation="false">
<AppenderRef ref="Console" level="error" />
</Root>
</Loggers>
</Configuration>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" monitorInterval="60">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_PATTERN">%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n%wEx</Property>
</Properties>
<Appenders>
<RollingRandomAccessFile name="Rolling" fileName="/srv/logs/timely/grafana-auth.log"
filePattern="/srv/logs/timely/$${date:yyyy-MM}/grafana-auth-%d{MM-dd-yyyy}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="1024 MB" />
</Policies>
<DefaultRolloverStrategy max="10" />
</RollingRandomAccessFile>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
</Appenders>
<Loggers>
<Root level="trace" includeLocation="false">
<AppenderRef ref="Console" level="warn" />
<AppenderRef ref="Rolling" level="trace" />
</Root>
<Logger name="timely" additivity="false">
<AppenderRef ref="Console" level="debug" />
</Logger>
</Loggers>
</Configuration>
+71
View File
@@ -0,0 +1,71 @@
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<beans:bean id="x509PrincipalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor">
<beans:property name="subjectDnRegex" value="CN=(.*?)(?:,|$)"/>
</beans:bean>
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
<beans:property name="users">
<util:list>
<beans:bean class="timely.auth.TimelyUser">
<beans:constructor-arg name="subjectDn">
<beans:value>CN=example.com</beans:value>
</beans:constructor-arg>
<beans:constructor-arg name="auths">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>
<beans:value>F</beans:value>
</util:list>
</beans:constructor-arg>
<beans:constructor-arg name="roles">
<util:list>
<beans:value>G</beans:value>
<beans:value>H</beans:value>
<beans:value>I</beans:value>
</util:list>
</beans:constructor-arg>
</beans:bean>
</util:list>
</beans:property>
</beans:bean>
<beans:bean id="x509AuthorizationProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<beans:property name="preAuthenticatedUserDetailsService" ref="timelyUserDetailsService" />
</beans:bean>
<user-service id="usernamePasswordService">
<user name="test" password="test1" authorities="A,B,C" />
</user-service>
<beans:bean id="noOpPasswordEncoder" class="org.springframework.security.crypto.password.NoOpPasswordEncoder"/>
<beans:bean id="UserNamePasswordAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="usernamePasswordService"/>
<beans:property name="passwordEncoder" ref="noOpPasswordEncoder"/>
</beans:bean>
<authentication-manager alias="authenticationManager">
<!-- X509 PreAuthenticated authorization provider -->
<authentication-provider ref="x509AuthorizationProvider"/>
<!-- Basic authorization provider, validates provided username and password -->
<authentication-provider ref="UserNamePasswordAuthenticationProvider" />
</authentication-manager>
<util:list id="requiredRoles" value-type="java.lang.String">
<beans:value></beans:value>
</util:list>
<util:list id="requiredAuths" value-type="java.lang.String">
<beans:value></beans:value>
</util:list>
</beans:beans>
+331
View File
@@ -0,0 +1,331 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.2.1-SNAPSHOT</version>
</parent>
<artifactId>grafana-auth</artifactId>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.google.flatbuffers</groupId>
<artifactId>flatbuffers-java</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<dependency>
<groupId>io.github.lukehutch</groupId>
<artifactId>fast-classpath-scanner</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.javacsv</groupId>
<artifactId>javacsv</artifactId>
</dependency>
<dependency>
<groupId>org.apache.accumulo</groupId>
<artifactId>accumulo-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.accumulo</groupId>
<artifactId>accumulo-minicluster</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-x-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<exclusions>
<exclusion>
<groupId>jdk.tools</groupId>
<artifactId>jdk.tools</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-minicluster</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>timely</groupId>
<artifactId>timely-balancer</artifactId>
</dependency>
<dependency>
<groupId>timely</groupId>
<artifactId>timely-client</artifactId>
</dependency>
<dependency>
<groupId>timely</groupId>
<artifactId>timely-common</artifactId>
</dependency>
<dependency>
<groupId>timely</groupId>
<artifactId>timely-server</artifactId>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative</artifactId>
<version>${version.netty-tcnative}</version>
<classifier>${os.detected.classifier}</classifier>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assemble</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptors>
<descriptor>src/main/dist/assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<goals>
<goal>copy-dependencies</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
<excludeArtifactIds>junit</excludeArtifactIds>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
<execution>
<id>unpack-native</id>
<goals>
<goal>unpack</goal>
</goals>
<phase>process-test-classes</phase>
<configuration>
<artifactItems>
<artifactItem>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative</artifactId>
<version>${version.netty-tcnative}</version>
<classifier>${os.detected.classifier}</classifier>
<type>jar</type>
<overWrite>false</overWrite>
<outputDirectory>${project.build.directory}/native</outputDirectory>
<includes>**/*.so</includes>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<goals>
<goal>add-source</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<sources>
<source>src/main/generated-java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<extensions>
<!-- Use os-maven-plugin to initialize the "os.detected" properties -->
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
</extension>
</extensions>
</build>
<profiles>
<profile>
<id>native-epoll-linux</id>
<activation>
<os>
<family>!mac</family>
</os>
</activation>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>${version.netty}</version>
<classifier>linux-x86_64</classifier>
</dependency>
</dependencies>
</profile>
<profile>
<id>native-epoll</id>
<activation>
<os>
<family>mac</family>
</os>
</activation>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>${version.netty}</version>
</dependency>
</dependencies>
</profile>
</profiles>
</project>
+55
View File
@@ -0,0 +1,55 @@
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>dist</id>
<formats>
<format>tar.gz</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory></outputDirectory>
<includes>
<include>README*</include>
<include>LICENSE*</include>
<include>NOTICE*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>lib</outputDirectory>
<includes>
<include>timely*.jar</include>
<include>grafana*.jar</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}/lib</directory>
<outputDirectory>lib</outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/bin</directory>
<outputDirectory>bin</outputDirectory>
<includes>
<include>**/*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/conf</directory>
<outputDirectory>conf</outputDirectory>
<includes>
<include>*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/src/main/resources/webapp</directory>
<outputDirectory>bin/webapp</outputDirectory>
<includes>
<include>*</include>
</includes>
</fileSet>
</fileSets>
</assembly>
@@ -0,0 +1,321 @@
package timely.grafana.auth;
import java.io.File;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.OpenSslServerContext;
import io.netty.handler.ssl.OpenSslServerSessionContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.internal.SystemPropertyUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import timely.auth.AuthCache;
import timely.balancer.configuration.ClientSsl;
import timely.balancer.connection.http.HttpClientPool;
import timely.client.http.HttpClient;
import timely.configuration.ServerSsl;
import timely.grafana.auth.configuration.GrafanaAuthConfiguration;
import timely.grafana.auth.configuration.SpringBootstrap;
import timely.grafana.auth.netty.http.GrafanaRelayHandler;
import timely.grafana.auth.netty.http.GrafanaRequestDecoder;
import timely.netty.http.HttpStaticFileServerHandler;
import timely.netty.http.NonSslRedirectHandler;
import timely.netty.http.TimelyExceptionHandler;
import timely.netty.http.auth.X509LoginRequestHandler;
public class GrafanaAuth {
private static final Logger LOG = LoggerFactory.getLogger(GrafanaAuth.class);
private static final int EPOLL_MIN_MAJOR_VERSION = 2;
private static final int EPOLL_MIN_MINOR_VERSION = 6;
private static final int EPOLL_MIN_PATCH_VERSION = 32;
private static final String OS_NAME = "os.name";
private static final String OS_VERSION = "os.version";
protected static final CountDownLatch LATCH = new CountDownLatch(1);
static ConfigurableApplicationContext applicationContext;
private final GrafanaAuthConfiguration grafanaAuthConfig;
private EventLoopGroup httpWorkerGroup = null;
private EventLoopGroup httpBossGroup = null;
protected Channel httpChannelHandle = null;
protected volatile boolean shutdown = false;
public GrafanaAuth(GrafanaAuthConfiguration grafanaAuthConfig) throws Exception {
this.grafanaAuthConfig = grafanaAuthConfig;
}
protected static ConfigurableApplicationContext initializeConfiguration(String[] args) {
return new SpringApplicationBuilder(SpringBootstrap.class).web(WebApplicationType.NONE).run(args);
}
private void shutdownHook() {
final Runnable shutdownRunner = () -> {
if (!shutdown) {
shutdown();
}
};
final Thread hook = new Thread(shutdownRunner, "shutdown-hook-thread");
Runtime.getRuntime().addShutdownHook(hook);
}
public void shutdown() {
List<ChannelFuture> channelFutures = new ArrayList<>();
if (httpChannelHandle != null) {
LOG.info("Closing httpChannelHandle");
channelFutures.add(httpChannelHandle.close());
}
// wait for the channels to shutdown
channelFutures.forEach(f -> {
try {
f.get();
} catch (final Exception e) {
LOG.error("Error while shutting down channel: {}", f.channel().config());
LOG.error("{}", e.getMessage());
e.printStackTrace();
}
});
Integer quietPeriod = grafanaAuthConfig.getShutdownQuietPeriod();
List<Future<?>> groupFutures = new ArrayList<>();
if (httpBossGroup != null) {
LOG.info("Shutting down httpBossGroup");
groupFutures.add(httpBossGroup.shutdownGracefully(quietPeriod, 10, TimeUnit.SECONDS));
}
if (httpWorkerGroup != null) {
LOG.info("Shutting down httpWorkerGroup");
groupFutures.add(httpWorkerGroup.shutdownGracefully(quietPeriod, 10, TimeUnit.SECONDS));
}
groupFutures.parallelStream().forEach(f -> {
try {
f.get();
} catch (final Exception e) {
LOG.error("Error while shutting down group: {}", f.toString());
LOG.error("{}", e.getMessage());
e.printStackTrace();
}
});
if (applicationContext != null) {
LOG.info("Closing applicationContext");
applicationContext.close();
}
this.shutdown = true;
LOG.info("Server shut down.");
}
private static boolean useEpoll() {
// Should we just return true if this is Linux and if we get an error
// during Epoll
// setup handle it there?
final String os = SystemPropertyUtil.get(OS_NAME).toLowerCase().trim();
final String[] version = SystemPropertyUtil.get(OS_VERSION).toLowerCase().trim().split("\\.");
if (os.startsWith("linux") && version.length >= 3) {
final int major = Integer.parseInt(version[0]);
if (major > EPOLL_MIN_MAJOR_VERSION) {
return true;
} else if (major == EPOLL_MIN_MAJOR_VERSION) {
final int minor = Integer.parseInt(version[1]);
if (minor > EPOLL_MIN_MINOR_VERSION) {
return true;
} else if (minor == EPOLL_MIN_MINOR_VERSION) {
final int patch = Integer.parseInt(version[2].substring(0, 2));
return patch >= EPOLL_MIN_PATCH_VERSION;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}
protected SslContext createSSLContext(GrafanaAuthConfiguration config) throws Exception {
ServerSsl sslCfg = config.getSecurity().getServerSsl();
Boolean generate = sslCfg.isUseGeneratedKeypair();
SslContextBuilder ssl;
if (generate) {
LOG.warn("Using generated self signed server certificate");
Date begin = new Date();
Date end = new Date(begin.getTime() + 86400000);
SelfSignedCertificate ssc = new SelfSignedCertificate("localhost", begin, end);
ssl = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey());
} else {
String cert = sslCfg.getCertificateFile();
String key = sslCfg.getKeyFile();
String keyPass = sslCfg.getKeyPassword();
if (null == cert || null == key) {
throw new IllegalArgumentException("Check your SSL properties, something is wrong.");
}
ssl = SslContextBuilder.forServer(new File(cert), new File(key), keyPass);
}
ssl.ciphers(sslCfg.getUseCiphers());
// Can't set to REQUIRE because the CORS pre-flight requests will fail.
ssl.clientAuth(ClientAuth.OPTIONAL);
Boolean useOpenSSL = sslCfg.isUseOpenssl();
if (useOpenSSL) {
ssl.sslProvider(SslProvider.OPENSSL);
} else {
ssl.sslProvider(SslProvider.JDK);
}
String trustStore = sslCfg.getTrustStoreFile();
if (null != trustStore) {
if (!trustStore.isEmpty()) {
ssl.trustManager(new File(trustStore));
}
}
return ssl.build();
}
protected SSLContext createSSLClientContext(GrafanaAuthConfiguration config) throws Exception {
ClientSsl ssl = config.getSecurity().getClientSsl();
return HttpClient.getSSLContext(ssl.getTrustStoreFile(), ssl.getTrustStoreType(), ssl.getTrustStorePassword(),
ssl.getKeyFile(), ssl.getKeyType(), ssl.getKeyPassword());
}
public void run() throws Exception {
AuthCache.setSessionMaxAge(grafanaAuthConfig.getSecurity());
final boolean useEpoll = useEpoll();
Class<? extends ServerSocketChannel> channelClass;
if (useEpoll) {
httpWorkerGroup = new EpollEventLoopGroup();
httpBossGroup = new EpollEventLoopGroup();
channelClass = EpollServerSocketChannel.class;
} else {
httpWorkerGroup = new NioEventLoopGroup();
httpBossGroup = new NioEventLoopGroup();
channelClass = NioServerSocketChannel.class;
}
LOG.info("Using channel class {}", channelClass.getSimpleName());
final int httpPort = grafanaAuthConfig.getHttp().getPort();
final String httpIp = grafanaAuthConfig.getHttp().getIp();
SslContext sslCtx = createSSLContext(grafanaAuthConfig);
if (sslCtx instanceof OpenSslServerContext) {
OpenSslServerContext openssl = (OpenSslServerContext) sslCtx;
String application = "GrafanaAuth_" + httpPort;
OpenSslServerSessionContext opensslCtx = openssl.sessionContext();
opensslCtx.setSessionCacheEnabled(true);
opensslCtx.setSessionCacheSize(128);
opensslCtx.setSessionIdContext(application.getBytes(StandardCharsets.UTF_8));
opensslCtx.setSessionTimeout(grafanaAuthConfig.getSecurity().getSessionMaxAge());
}
SSLContext clientSSLContext = createSSLClientContext(grafanaAuthConfig);
final ServerBootstrap httpServer = new ServerBootstrap();
httpServer.group(httpBossGroup, httpWorkerGroup);
httpServer.channel(channelClass);
httpServer.handler(new LoggingHandler());
HttpClientPool httpClientPool = new HttpClientPool(grafanaAuthConfig.getSecurity(), grafanaAuthConfig.getHttp(),
clientSSLContext);
httpServer.childHandler(setupHttpChannel(grafanaAuthConfig, sslCtx, httpClientPool));
httpServer.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
httpServer.option(ChannelOption.SO_BACKLOG, 128);
httpServer.option(ChannelOption.SO_KEEPALIVE, true);
httpChannelHandle = httpServer.bind(httpIp, httpPort).sync().channel();
final String httpAddress = ((InetSocketAddress) httpChannelHandle.localAddress()).getAddress().getHostAddress();
shutdownHook();
LOG.info("Server started. Listening on {}:{} for HTTP traffic", httpAddress, httpPort);
}
public static void main(String[] args) throws Exception {
GrafanaAuth.applicationContext = GrafanaAuth.initializeConfiguration(args);
GrafanaAuthConfiguration balancerConf = applicationContext.getBean(GrafanaAuthConfiguration.class);
GrafanaAuth b = new GrafanaAuth(balancerConf);
try {
b.run();
LATCH.await();
} catch (final InterruptedException e) {
LOG.info("Server shutting down.");
} catch (Exception e) {
LOG.error("Error running server.", e);
} finally {
try {
b.shutdown();
} catch (Exception e) {
System.exit(1);
}
}
}
protected ChannelHandler setupHttpChannel(GrafanaAuthConfiguration config, SslContext sslCtx,
HttpClientPool httpClientPool) {
return new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("ssl", new NonSslRedirectHandler(config.getHttp(), sslCtx));
ch.pipeline().addLast("encoder", new HttpResponseEncoder());
ch.pipeline().addLast("decoder", new HttpRequestDecoder());
ch.pipeline().addLast("compressor", new HttpContentCompressor());
ch.pipeline().addLast("decompressor", new HttpContentDecompressor());
// high maximum contentLength so that grafana snapshots can be delivered
// might not be necessary if inbound chunking (while proxying) is handled
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(2097152));
ch.pipeline().addLast("chunker", new ChunkedWriteHandler());
ch.pipeline().addLast("grafanaDecoder",
new GrafanaRequestDecoder(config.getSecurity(), config.getHttp()));
ch.pipeline().addLast("fileServer", new HttpStaticFileServerHandler());
ch.pipeline().addLast("login", new X509LoginRequestHandler(config.getSecurity(), config.getHttp()));
ch.pipeline().addLast("httpRelay", new GrafanaRelayHandler(config, httpClientPool));
ch.pipeline().addLast("error", new TimelyExceptionHandler());
}
};
}
}
@@ -0,0 +1,32 @@
package timely.grafana.auth.configuration;
public class Grafana {
private String host = "localhost";
private Integer port = 3000;
private String protocol = "http";
public void setHost(String host) {
this.host = host;
}
public String getHost() {
return host;
}
public void setPort(Integer port) {
this.port = port;
}
public Integer getPort() {
return port;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getProtocol() {
return protocol;
}
}
@@ -0,0 +1,56 @@
package timely.grafana.auth.configuration;
import javax.validation.Valid;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import timely.balancer.configuration.BalancerHttp;
import timely.balancer.configuration.BalancerSecurity;
@Validated
@Component
@ConfigurationProperties(prefix = "grafana-auth")
public class GrafanaAuthConfiguration {
@Valid
@NestedConfigurationProperty
private BalancerHttp http = new BalancerHttp();
@Valid
@NestedConfigurationProperty
private BalancerSecurity security = new BalancerSecurity();
@Valid
@NestedConfigurationProperty
private Grafana grafana = new Grafana();
private Integer shutdownQuietPeriod = 5;
public BalancerSecurity getSecurity() {
return security;
}
public BalancerHttp getHttp() {
return http;
}
public Grafana getGrafana() {
return grafana;
}
/**
* Time to wait (in seconds) for connections to finish and to make sure no new
* connections happen before shutting down Netty event loop groups.
*
* @return
*/
public int getShutdownQuietPeriod() {
return this.shutdownQuietPeriod;
}
public void setShutdownQuietPeriod(Integer quietPeriod) {
this.shutdownQuietPeriod = quietPeriod;
}
}
@@ -0,0 +1,7 @@
package timely.grafana.auth.configuration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackageClasses = { SpringBootstrap.class })
public class SpringBootstrap {
}
@@ -0,0 +1,234 @@
package timely.grafana.auth.netty.http;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
import java.io.ByteArrayOutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
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;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.ReferenceCountUtil;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StreamUtils;
import timely.api.request.timeseries.HttpRequest;
import timely.api.response.TimelyException;
import timely.auth.TimelyPrincipal;
import timely.auth.TimelyUser;
import timely.auth.util.DnUtils;
import timely.auth.util.HttpHeaderUtils;
import timely.balancer.connection.TimelyBalancedHost;
import timely.balancer.connection.http.HttpClientPool;
import timely.grafana.auth.configuration.GrafanaAuthConfiguration;
import timely.grafana.request.GrafanaRequest;
import timely.netty.http.TimelyHttpHandler;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class GrafanaRelayHandler extends SimpleChannelInboundHandler<HttpRequest> implements TimelyHttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(GrafanaRelayHandler.class);
private final HttpClientPool httpClientPool;
private GrafanaAuthConfiguration config;
public GrafanaRelayHandler(GrafanaAuthConfiguration config, HttpClientPool httpClientPool) {
this.config = config;
this.httpClientPool = httpClientPool;
}
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
CloseableHttpClient client = null;
CloseableHttpResponse relayedResponse = null;
FullHttpRequest request = null;
GrafanaRequest grafanaRequest;
TimelyBalancedHost k = null;
String relayURI = null;
try {
try {
request = msg.getHttpRequest();
String originalURI = request.getUri();
originalURI = encodeURI(originalURI);
if (msg instanceof GrafanaRequest) {
grafanaRequest = (GrafanaRequest) msg;
} else {
throw new UnsupportedOperationException("Request not supported");
}
TimelyAuthenticationToken token = grafanaRequest.getToken();
if (token == null) {
throw new IllegalStateException("No token on grafana request");
}
String protocol = config.getGrafana().getProtocol();
String host = config.getGrafana().getHost();
Integer port = config.getGrafana().getPort();
k = TimelyBalancedHost.of(host, -1, port, -1, -1);
client = httpClientPool.borrowObject(k);
Multimap<String, String> headers = HttpHeaderUtils.toMultimap(request.headers());
// If incoming user sets these headers, do not relay them
headers.removeAll("X-WEBAUTH-USER");
headers.removeAll("X-WEBAUTH-NAME");
TimelyPrincipal principal = token.getTimelyPrincipal();
TimelyUser user = principal.getPrimaryUser();
String subjectDn = user.getDn().subjectDN();
String userName = DnUtils.getCommonName(subjectDn);
String loginName = DnUtils.getShortName(subjectDn);
headers.put("X-WEBAUTH-USER", loginName);
headers.put("X-WEBAUTH-NAME", userName);
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()));
}
}
Header[] headerArray = new Header[relayedHeaderList.size()];
relayedHeaderList.toArray(headerArray);
relayURI = protocol + "://" + host + ":" + port + originalURI;
if (request.getMethod().equals(HttpMethod.HEAD)) {
HttpHead relayedRequest = new HttpHead(relayURI);
if (headerArray.length > 0) {
relayedRequest.setHeaders(headerArray);
}
relayedResponse = client.execute(relayedRequest);
} else if (request.getMethod().equals(HttpMethod.GET)) {
HttpGet relayedRequest = new HttpGet(relayURI);
if (headerArray.length > 0) {
relayedRequest.setHeaders(headerArray);
}
relayedResponse = client.execute(relayedRequest);
} else if (request.getMethod().equals(HttpMethod.POST)) {
HttpPost relayedRequest = new HttpPost(relayURI);
String content = request.content().toString(StandardCharsets.UTF_8);
StringEntity entity = new StringEntity(content);
relayedRequest.setEntity(entity);
if (headerArray.length > 0) {
relayedRequest.setHeaders(headerArray);
}
relayedResponse = client.execute(relayedRequest);
} else if (request.getMethod().equals(HttpMethod.PUT)) {
HttpPut relayedRequest = new HttpPut(relayURI);
String content = request.content().toString(StandardCharsets.UTF_8);
StringEntity entity = new StringEntity(content);
relayedRequest.setEntity(entity);
if (headerArray.length > 0) {
relayedRequest.setHeaders(headerArray);
}
relayedResponse = client.execute(relayedRequest);
} else if (request.getMethod().equals(HttpMethod.DELETE)) {
HttpDelete relayedRequest = new HttpDelete(relayURI);
if (headerArray.length > 0) {
relayedRequest.setHeaders(headerArray);
}
relayedResponse = client.execute(relayedRequest);
} else if (request.getMethod().equals(HttpMethod.OPTIONS)) {
HttpOptions relayedRequest = new HttpOptions(relayURI);
if (headerArray.length > 0) {
relayedRequest.setHeaders(headerArray);
}
relayedResponse = client.execute(relayedRequest);
}
} catch (Exception e) {
String message = e.getMessage();
if (message == null) {
LOG.error("", e);
} else {
LOG.error(message, e);
}
this.sendHttpError(ctx, new TimelyException(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(),
e.getMessage(), e.getLocalizedMessage(), e));
return;
} finally {
if (request != null && request instanceof AutoCloseable) {
ReferenceCountUtil.release(request);
}
}
FullHttpResponse response = null;
if (relayedResponse == null) {
this.sendHttpError(ctx, new TimelyException(HttpResponseStatus.METHOD_NOT_ALLOWED.code(),
"Method not allowed", "Method not allowed"));
} else {
LOG.debug("RequestURL:{} ResponseCode:{}", relayURI, relayedResponse.getStatusLine().getStatusCode());
HttpEntity httpEntity = relayedResponse.getEntity();
if (httpEntity == null) {
response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(relayedResponse.getStatusLine().getStatusCode()));
} else {
ByteArrayOutputStream baos = new MyByteArrayOutputStream();
StreamUtils.copy(httpEntity.getContent(), baos);
response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(relayedResponse.getStatusLine().getStatusCode()),
Unpooled.copiedBuffer(baos.toByteArray()));
response.headers().set(CONTENT_LENGTH, response.content().readableBytes());
}
for (Header h : relayedResponse.getAllHeaders()) {
response.headers().add(h.getName(), h.getValue());
}
sendResponse(ctx, response);
}
} finally {
if (relayedResponse != null) {
relayedResponse.close();
}
if (client != null && k != null) {
httpClientPool.returnObject(k, client);
} else {
LOG.error("NOT RETURNING CONNECTION! " + msg.getHttpRequest().getUri());
}
}
}
static public class MyByteArrayOutputStream extends ByteArrayOutputStream {
public MyByteArrayOutputStream() {
}
public MyByteArrayOutputStream(int size) {
super(size);
}
public int getCount() {
return count;
}
public byte[] getBuf() {
return buf;
}
}
static public String encodeURI(String s) {
try {
s = s.replaceAll("\\{", URLEncoder.encode("{", "UTF-8"));
s = s.replaceAll("\\}", URLEncoder.encode("}", "UTF-8"));
} catch (Exception e) {
}
return s;
}
}
@@ -0,0 +1,133 @@
package timely.grafana.auth.netty.http;
import java.security.cert.X509Certificate;
import java.util.Collection;
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.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders.Names;
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.request.AuthenticatedRequest;
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.grafana.request.GrafanaRequest;
import timely.netty.Constants;
import timely.netty.http.TimelyHttpHandler;
import timely.netty.http.auth.TimelyAuthenticationToken;
public class GrafanaRequestDecoder extends MessageToMessageDecoder<FullHttpRequest> implements TimelyHttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(GrafanaRequestDecoder.class);
private static final String LOG_RECEIVED_REQUEST = "Received HTTP request {}";
private static final String LOG_PARSED_REQUEST = "Parsed request {}";
private final Security security;
private final String nonSecureRedirectAddress;
public GrafanaRequestDecoder(Security security, Http http) {
this.security = security;
this.nonSecureRedirectAddress = http.getRedirectPath();
}
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();
cookies.forEach(h -> {
ServerCookieDecoder.STRICT.decode(h).forEach(c -> {
if (c.name().equals(Constants.COOKIE_NAME)) {
if (buf.length() == 0) {
buf.append(c.value());
}
}
});
});
if (buf.length() == 0) {
return null;
} else {
return buf.toString();
}
}
@Override
protected void decode(ChannelHandlerContext ctx, FullHttpRequest msg, List<Object> out) throws Exception {
LOG.trace(LOG_RECEIVED_REQUEST, msg);
final String uri = msg.getUri();
final QueryStringDecoder decoder = new QueryStringDecoder(uri);
if (decoder.path().equals(nonSecureRedirectAddress)) {
out.add(new StrictTransportResponse());
return;
}
final String sessionId = getSessionId(msg);
LOG.trace("SessionID: " + sessionId);
HttpRequest request;
try {
request = new GrafanaRequest();
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.containsKey(sessionId)) {
principal = AuthCache.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);
request.validate();
out.add(request);
} catch (UnsupportedOperationException | NullPointerException e) {
// Return the original http request to route to the static file
// server
msg.retain();
out.add(msg);
return;
}
try {
try {
AuthenticationService.enforceAccess((AuthenticatedRequest) request);
} catch (TimelyException e) {
if (!security.isAllowAnonymousHttpAccess()) {
throw e;
}
}
} catch (Exception e) {
out.clear();
throw e;
}
}
}
@@ -0,0 +1,32 @@
package timely.grafana.request;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;
import timely.api.annotation.Http;
import timely.api.request.AuthenticatedRequest;
import timely.api.request.HttpGetRequest;
import timely.api.request.HttpPostRequest;
@Http(path = "/")
public class GrafanaRequest extends AuthenticatedRequest implements HttpGetRequest, HttpPostRequest {
private FullHttpRequest httpRequest = null;
@Override
public HttpGetRequest parseQueryParameters(QueryStringDecoder decoder) throws Exception {
return new GrafanaRequest();
}
@Override
public HttpPostRequest parseBody(String content) throws Exception {
return new GrafanaRequest();
}
public void setHttpRequest(FullHttpRequest httpRequest) {
this.httpRequest = httpRequest;
}
public FullHttpRequest getHttpRequest() {
return httpRequest;
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<Class name="timely.grafana.auth.netty.http.GrafanaRelayHandler$MyByteArrayOutputStream" />
<Method name="getBuf" />
<Bug pattern="EI_EXPOSE_REP" />
</Match>
</FindBugsFilter>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.1-SNAPSHOT</version>
</parent>
<artifactId>timely-app</artifactId>
<packaging>pom</packaging>
+19 -8
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>timely</groupId>
<artifactId>timely</artifactId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Timely</name>
<description>A secure and scalable time series database based on Apache Accumulo</description>
@@ -20,6 +20,7 @@
<module>grafana</module>
<module>analytics</module>
<module>balancer</module>
<module>grafana-auth</module>
</modules>
<scm>
<connection>scm:git:https://github.com/NationalSecurityAgency/timely.git</connection>
@@ -40,7 +41,7 @@
<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.7.1</version.collectd>
@@ -49,13 +50,13 @@
<version.commons-io>2.6</version.commons-io>
<version.commons-lang3>3.5</version.commons-lang3>
<version.commons-pool2>2.6.0</version.commons-pool2>
<version.curator>2.11.1</version.curator>
<version.curator>4.1.0</version.curator>
<version.disruptor>3.3.6</version.disruptor>
<version.eclipse-lifecycle>1.0.0</version.eclipse-lifecycle>
<version.fast-classpath-scanner>2.0.5</version.fast-classpath-scanner>
<version.flink>1.1.4</version.flink>
<version.google-flatbuffers>1.10.0</version.google-flatbuffers>
<version.guava>19.0</version.guava>
<version.guava>27.1-jre</version.guava>
<version.hadoop>2.7.3</version.hadoop>
<version.httpcomponents>4.5.2</version.httpcomponents>
<version.impsort>1.2.0</version.impsort>
@@ -76,13 +77,13 @@
<version.maven-rpm>2.1.4</version.maven-rpm>
<version.maven-shade>2.4.2</version.maven-shade>
<version.maven-surefire>2.19.1</version.maven-surefire>
<version.netty>4.0.56.Final</version.netty>
<version.netty-tcnative>2.0.7.Final</version.netty-tcnative>
<version.netty>4.1.33.Final</version.netty>
<version.netty-tcnative>2.0.20.Final</version.netty-tcnative>
<version.os-maven>1.5.0.Final</version.os-maven>
<version.powermock>1.6.4</version.powermock>
<version.slf4j>1.7.21</version.slf4j>
<version.slf4j>1.7.25</version.slf4j>
<version.spotbugs-maven>3.1.7</version.spotbugs-maven>
<version.spring-boot>1.4.1.RELEASE</version.spring-boot>
<version.spring-boot>2.1.3.RELEASE</version.spring-boot>
<version.tyrus>1.13</version.tyrus>
</properties>
<dependencyManagement>
@@ -102,6 +103,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>
@@ -304,6 +310,11 @@
<artifactId>collectd-plugin-parent</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>timely</groupId>
<artifactId>timely-balancer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>timely</groupId>
<artifactId>timely-client</artifactId>
+34 -12
View File
@@ -15,15 +15,27 @@
<beans:bean id="timelyUserDetailsService" class="timely.auth.UserDetailsService">
<beans:property name="users">
<util:map key-type="java.lang.String">
<beans:entry key="example.com">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>
<beans:value>F</beans:value>
</util:list>
</beans:entry>
</util:map>
<util:list>
<beans:bean class="timely.auth.TimelyUser">
<beans:constructor-arg name="subjectDn">
<beans:value>CN=example.com</beans:value>
</beans:constructor-arg>
<beans:constructor-arg name="auths">
<util:list>
<beans:value>D</beans:value>
<beans:value>E</beans:value>
<beans:value>F</beans:value>
</util:list>
</beans:constructor-arg>
<beans:constructor-arg name="roles">
<util:list>
<beans:value>G</beans:value>
<beans:value>H</beans:value>
<beans:value>I</beans:value>
</util:list>
</beans:constructor-arg>
</beans:bean>
</util:list>
</beans:property>
</beans:bean>
@@ -34,9 +46,12 @@
<user-service id="usernamePasswordService">
<user name="test" password="test1" authorities="A,B,C" />
</user-service>
<beans:bean id="UserNamePasswordAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:bean id="noOpPasswordEncoder" class="org.springframework.security.crypto.password.NoOpPasswordEncoder"/>
<beans:bean id="UserNamePasswordAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="usernamePasswordService"/>
<beans:property name="passwordEncoder" ref="noOpPasswordEncoder"/>
</beans:bean>
<authentication-manager alias="authenticationManager">
@@ -45,5 +60,12 @@
<!-- Basic authorization provider, validates provided username and password -->
<authentication-provider ref="UserNamePasswordAuthenticationProvider" />
</authentication-manager>
<util:list id="requiredRoles" value-type="java.lang.String">
<beans:value></beans:value>
</util:list>
<util:list id="requiredAuths" value-type="java.lang.String">
<beans:value></beans:value>
</util:list>
</beans:beans>
+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:
+3 -1
View File
@@ -8,6 +8,7 @@ timely:
age-off-hours:
default: 26
metrics-report-ignored-tags:
defaultVisibility:
accumulo:
instance-name: accumulo
zookeepers: localhost:2181
@@ -22,7 +23,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.1.1-SNAPSHOT</version>
<version>1.2.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>
+9 -8
View File
@@ -70,6 +70,7 @@ import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import timely.api.response.TimelyException;
@@ -101,7 +102,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;
@@ -250,7 +251,7 @@ public class Server {
}
protected static Configuration initializeConfiguration(String[] args) {
applicationContext = new SpringApplicationBuilder(SpringBootstrap.class).web(false).run(args);
applicationContext = new SpringApplicationBuilder(SpringBootstrap.class).web(WebApplicationType.NONE).run(args);
return applicationContext.getBean(Configuration.class);
}
@@ -430,7 +431,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();
@@ -587,7 +588,7 @@ public class Server {
ch.pipeline().addLast("decoder", new HttpRequestDecoder());
ch.pipeline().addLast("compressor", new HttpContentCompressor());
ch.pipeline().addLast("decompressor", new HttpContentDecompressor());
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(8192));
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("chunker", new ChunkedWriteHandler());
final Cors corsCfg = config.getHttp().getCors();
final CorsConfig.Builder ccb;
@@ -635,7 +636,7 @@ public class Server {
ch.pipeline().addLast("logger", new LoggingHandler());
ch.pipeline().addLast("packetDecoder", new UdpPacketToByteBuf());
ch.pipeline().addLast("buffer", new MetricsBufferDecoder());
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(8192, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(65536, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("putDecoder", new UdpDecoder());
ch.pipeline().addLast("putHandler", new TcpPutHandler(dataStore));
}
@@ -648,7 +649,7 @@ public class Server {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("buffer", new MetricsBufferDecoder());
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(8192, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("frame", new DelimiterBasedFrameDecoder(65536, true, Delimiters.lineDelimiter()));
ch.pipeline().addLast("putDecoder", new TcpDecoder());
ch.pipeline().addLast("putHandler", new TcpPutHandler(dataStore));
ch.pipeline().addLast("versionHandler", new TcpVersionHandler());
@@ -663,8 +664,8 @@ public class Server {
protected void initChannel(SocketChannel ch) throws Exception {
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("aggregator", new HttpObjectAggregator(65536));
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));
@@ -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
* Base class for requests that require auth
*/
@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 == null ? null : 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;
}
}
+68 -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 final 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() {
private static Cache<String, TimelyPrincipal> getCache() {
if (-1 == sessionMaxAge) {
throw new IllegalStateException("Cache session max age not configured.");
}
@@ -47,35 +49,67 @@ 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);
public static boolean containsKey(String key) {
return getCache().getIfPresent(key) != null;
}
public static TimelyPrincipal get(String key) {
return getCache().getIfPresent(key);
}
public static void put(String key, TimelyPrincipal principal) {
getCache().put(key, principal);
}
public static void remove(String key) {
getCache().invalidate(key);
}
public static void clear() {
if (CACHE != null) {
CACHE.invalidateAll();
}
}
protected static Collection<Authorizations> getAuthorizations(String entityName) {
if (!StringUtils.isEmpty(entityName)) {
List<Authorizations> authorizationsList = new ArrayList<>();
TimelyPrincipal principal = AuthCache.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,37 +1,262 @@
package timely.auth;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.ServiceConfigurationError;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
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.DnUtils;
import timely.netty.http.auth.TimelyAuthenticationToken;
import timely.netty.http.auth.TimelyUserDetails;
public class AuthenticationService {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationService.class);
private static ApplicationContext springContext = null;
private static AuthenticationManager authManager = null;
private static SubjectDnX509PrincipalExtractor x509 = null;
private static ConcurrentHashMap<String, Object> fetchingEntity = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, Object> fetchingPrincipal = new ConcurrentHashMap<>();
private static Collection<String> requiredRoles;
private static Collection<String> requiredAuths;
static {
try {
springContext = new ClassPathXmlApplicationContext("security.xml");
authManager = (AuthenticationManager) springContext.getBean("authenticationManager");
x509 = (SubjectDnX509PrincipalExtractor) springContext.getBean("x509PrincipalExtractor");
authManager = springContext.getBean("authenticationManager", AuthenticationManager.class);
x509 = springContext.getBean("x509PrincipalExtractor", SubjectDnX509PrincipalExtractor.class);
requiredRoles = springContext.getBean("requiredRoles", List.class);
requiredRoles.removeIf(String::isEmpty);
requiredAuths = springContext.getBean("requiredAuths", List.class);
requiredAuths.removeIf(String::isEmpty);
} catch (BeansException e) {
throw new ServiceConfigurationError("Error setting up Authentication objects: " + e.getMessage(),
e.getRootCause());
}
}
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 TimelyPrincipal authenticate(Authentication authentication, String cacheName) {
return authenticate(authentication, SubjectIssuerDNPair.of(cacheName), cacheName);
}
public static TimelyPrincipal authenticate(Authentication authentication, SubjectIssuerDNPair pair, String entity) {
// only one thread should call the authenticationManager for a given entity
TimelyPrincipal principal = AuthCache.get(entity);
// once an entity is in the AuthCache, we can bypass all of this
if (principal == null) {
Object newObj = new Object();
Object o = fetchingEntity.getOrDefault(entity, newObj);
if (o == null) {
o = newObj;
}
try {
synchronized (o) {
// if multiple threads are waiting on this synchronized, then the 2nd and all
// others will get the principal here
principal = AuthCache.get(entity);
if (principal == null) {
Authentication token = getAuthenticationManager().authenticate(authentication);
principal = getTimelyPrincipal(token, pair);
LOG.trace("Got entity principal for {} from AuthenticationManager", entity);
AuthCache.put(entity, principal);
} else {
LOG.trace("Got entity principal for {} from AuthCache on 2nd attempt", entity);
}
}
} finally {
fetchingEntity.remove(entity);
}
} else {
LOG.trace("Got entity principal for {} from AuthCache on 1st attempt", entity);
}
return principal;
}
public static void enforceAccess(AuthenticatedRequest request) throws Exception {
String sessionId = request.getSessionId();
X509Certificate clientCert = request.getToken().getClientCert();
TimelyPrincipal verifiedPrincipal = null;
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)) {
verifiedPrincipal = AuthCache.get(sessionId);
if (verifiedPrincipal == null) {
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();
String entity = requestPrincipal.getName();
verifiedPrincipal = AuthCache.get(entity);
if (verifiedPrincipal == null) {
Object newObj = new Object();
Object o = fetchingPrincipal.getOrDefault(entity, newObj);
if (o == null) {
o = newObj;
}
try {
synchronized (o) {
// check again if in the AuthCache
verifiedPrincipal = AuthCache.get(entity);
if (verifiedPrincipal == null) {
// first thread here, so authenticate all entities in requestPrincipal
List<TimelyUser> authenticatedTimelyUsers = new ArrayList<>();
for (TimelyUser user : requestPrincipal.getProxiedUsers()) {
SubjectIssuerDNPair p = user.getDn();
TimelyAuthenticationToken entityToken = getAuthenticationToken(clientCert,
p.subjectDN(), p.issuerDN());
TimelyPrincipal entityPrincipal = authenticate(entityToken, p,
entityToken.getTimelyPrincipal().getName());
authenticatedTimelyUsers.addAll(entityPrincipal.getProxiedUsers());
}
verifiedPrincipal = new TimelyPrincipal(authenticatedTimelyUsers);
AuthCache.put(verifiedPrincipal.getName(), verifiedPrincipal);
LOG.debug("Authenticated user {} for request {} with authorizations {}",
verifiedPrincipal.getName(), request.toString(),
verifiedPrincipal.getAuthorizationsString());
} else {
LOG.trace("Verified principal {} in AuthCache on 2nd attempt", entity);
}
}
} finally {
fetchingPrincipal.remove(entity);
}
} else {
LOG.trace("Verified principal {} in AuthCache on 1st attempt", entity);
}
} catch (RuntimeException e) {
LOG.error(e.getMessage(), e);
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Access denied", e.getMessage());
}
}
try {
checkAllowedAccess(verifiedPrincipal.getPrimaryUser());
} catch (TimelyException e) {
// validation based on roles and/or auths failed
// remove combined principal and primary user from cache
// i.e. don't cache a failure
AuthCache.remove(verifiedPrincipal.getName());
AuthCache.remove(verifiedPrincipal.getPrimaryUser().getName());
throw e;
}
}
private static void checkAllowedAccess(TimelyUser primaryUser) throws TimelyException {
Set<String> missingRoles = new TreeSet<>();
Set<String> missingAuths = new TreeSet<>();
if (requiredRoles != null) {
if (!primaryUser.getRoles().containsAll(requiredRoles)) {
missingRoles.addAll(requiredRoles);
missingRoles.removeAll(primaryUser.getRoles());
}
}
if (requiredAuths != null) {
if (!primaryUser.getAuths().containsAll(requiredAuths)) {
missingAuths.addAll(requiredRoles);
missingAuths.removeAll(primaryUser.getAuths());
}
}
if (!missingRoles.isEmpty() || !missingAuths.isEmpty()) {
String message = "";
if (!missingRoles.isEmpty() && !missingAuths.isEmpty()) {
message = "User:" + primaryUser.getName() + " is missing role(s):" + missingRoles + " and auth(s):"
+ missingAuths;
} else if (!missingRoles.isEmpty()) {
message = "User:" + primaryUser.getName() + " is missing role(s):" + missingRoles;
} else if (missingAuths.isEmpty()) {
message = "User:" + primaryUser.getName() + " is missing auths(s):" + missingAuths;
}
LOG.debug(message);
throw new TimelyException(HttpResponseStatus.UNAUTHORIZED.code(), "Access denied", message);
}
}
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;
}
private static TimelyPrincipal getTimelyPrincipal(Authentication authentication,
SubjectIssuerDNPair subjectIssuerDNPair) {
String subjectDn = subjectIssuerDNPair.subjectDN();
TimelyUser.UserType userType = DnUtils.isServerDN(subjectDn) ? TimelyUser.UserType.SERVER
: TimelyUser.UserType.USER;
Collection<String> auths = authentication.getAuthorities().stream().map(a -> a.getAuthority())
.collect(Collectors.toList());
Object o = authentication.getPrincipal();
Collection<String> roles = null;
if (o instanceof TimelyUserDetails) {
roles = ((TimelyUserDetails) o).getRoles();
}
TimelyUser timelyUser = new TimelyUser(subjectIssuerDNPair, userType, auths, roles, null,
System.currentTimeMillis());
return new TimelyPrincipal(Arrays.asList(timelyUser));
}
}
@@ -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(primaryUser);
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,156 @@
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.DnUtils;
/**
* 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 Spring configuration
*/
public TimelyUser(String subjectDn, Collection<String> auths, Collection<String> roles) {
this.name = subjectDn;
this.commonName = DnUtils.getCommonName(subjectDn);
this.dn = SubjectIssuerDNPair.of(subjectDn);
this.userType = DnUtils.isServerDN(subjectDn) ? UserType.SERVER : UserType.USER;
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 = null;
this.creationTime = System.currentTimeMillis();
this.expirationTime = -1L;
}
/*
* For testing
*/
public TimelyUser(String subjectDn, Collection<String> auths) {
this(SubjectIssuerDNPair.of(subjectDn), UserType.USER, auths, null, null, System.currentTimeMillis(), -1L);
}
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 = DnUtils.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() + "}";
}
}
@@ -3,7 +3,6 @@ package timely.auth;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -11,18 +10,20 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import timely.netty.http.auth.TimelyUserDetails;
public class UserDetailsService implements
org.springframework.security.core.userdetails.AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
private HashMap<String, List<String>> users;
private HashMap<String, TimelyUser> users;
public Map<String, List<String>> getUsers() {
public Map<String, TimelyUser> getUsers() {
return users;
}
public void setUsers(LinkedHashMap<String, List<String>> users) {
this.users = new HashMap<>(users);
public void setUsers(List<TimelyUser> users) {
this.users = new HashMap<>();
users.forEach(u -> this.users.put(u.getName(), u));
}
@Override
@@ -30,25 +31,32 @@ public class UserDetailsService implements
throws UsernameNotFoundException {
// Determine if the user is allowed to access the system, if not throw
// UsernameNotFoundException
String username = token.getName().toString();
String username = token.getName();
if (!users.containsKey(username)) {
throw new UsernameNotFoundException(username + " not configured.");
}
// If allowed, populate the user details object with the authorities for
// the user.
return new UserDetails() {
return new TimelyUserDetails() {
private static final long serialVersionUID = 1L;
@Override
public Collection<SimpleGrantedAuthority> getAuthorities() {
final Collection<SimpleGrantedAuthority> auths = new ArrayList<>();
users.get(username).forEach(a -> {
users.get(username).getAuths().forEach(a -> {
auths.add(new SimpleGrantedAuthority(a));
});
return auths;
}
@Override
public Collection<String> getRoles() {
final Collection<String> roles = new ArrayList<>();
roles.addAll(users.get(username).getRoles());
return roles;
}
@Override
public String getPassword() {
return "";
@@ -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,212 @@
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 io.netty.handler.codec.http.QueryStringDecoder;
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;
}
}
public static void main(String[] args) {
QueryStringDecoder decoder = new QueryStringDecoder("https://my.server:3000");
System.out.println(decoder.path());
}
}
@@ -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 proxiedEntities = HttpHeaderUtils.getSingleHeader(headers,
TimelyAuthenticationToken.PROXIED_ENTITIES_HEADER, false);
headers.removeAll(TimelyAuthenticationToken.PROXIED_ENTITIES_HEADER);
String[] preExistingProxiedEntities = ProxiedEntityUtils.splitProxiedDNs(proxiedEntities, 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 proxiedIssuers = HttpHeaderUtils.getSingleHeader(headers,
TimelyAuthenticationToken.PROXIED_ISSUERS_HEADER, false);
headers.removeAll(TimelyAuthenticationToken.PROXIED_ISSUERS_HEADER);
String[] preExistingProxiedIssuers = ProxiedEntityUtils.splitProxiedDNs(proxiedIssuers, 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);
}
}
}
}
@@ -9,7 +9,9 @@ import javax.validation.Valid;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
@Validated
@Component
@ConfigurationProperties(prefix = "timely")
public class Configuration {
@@ -19,6 +21,7 @@ public class Configuration {
private HashMap<String, Integer> metricAgeOffDays = new HashMap<>();
private List<String> metricsReportIgnoredTags = new ArrayList<>();
private String instance = null;
private String defaultVisibility = null;
@Valid
@NestedConfigurationProperty
@@ -71,6 +74,14 @@ public class Configuration {
return instance;
}
public String getDefaultVisibility() {
return defaultVisibility;
}
public void setDefaultVisibility(String defaultVisibility) {
this.defaultVisibility = defaultVisibility;
}
public HashMap<String, Integer> getMetricAgeOffDays() {
return metricAgeOffDays;
}
@@ -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.containsKey(sessionId)) {
principal = AuthCache.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,35 @@
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, String sessionId)
throws AuthenticationException {
// Perform the login process using username/password
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(msg.getUsername(),
msg.getPassword());
return AuthenticationService.getAuthenticationManager().authenticate(token);
TimelyPrincipal principal = AuthenticationService.authenticate(token, sessionId);
LOG.debug("Authenticated user {} with authorizations {}", principal.getPrimaryUser().getDn().subjectDN(),
principal.getAuthorizationsString());
return principal;
}
}
@@ -0,0 +1,112 @@
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 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.getIssuerDN().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,7 @@ 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 +37,15 @@ 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);
String sessionId = UUID.randomUUID().toString();
TimelyPrincipal principal = authenticate(ctx, loginRequest, sessionId);
LOG.trace("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 +64,6 @@ 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, String entity)
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;
}
}
@@ -0,0 +1,10 @@
package timely.netty.http.auth;
import java.util.Collection;
import org.springframework.security.core.userdetails.UserDetails;
public interface TimelyUserDetails extends UserDetails {
Collection<String> getRoles();
}
@@ -3,35 +3,50 @@ 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.SubjectIssuerDNPair;
import timely.auth.TimelyPrincipal;
import timely.configuration.Http;
import timely.configuration.Security;
public class X509LoginRequestHandler extends TimelyLoginRequestHandler<X509LoginRequest> {
private static final 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, String sessionId)
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());
SubjectIssuerDNPair pair = SubjectIssuerDNPair.of(clientCert.getSubjectDN().getName(),
clientCert.getIssuerDN().getName());
principal = AuthenticationService.authenticate(token, pair, sessionId);
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,7 +1,9 @@
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;
@@ -11,6 +13,7 @@ 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;
@@ -18,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;
@@ -40,14 +50,32 @@ public class WebSocketRequestDecoder extends MessageToMessageDecoder<WebSocketFr
String content = frame.text();
WebSocketRequest request = JsonUtil.getObjectMapper().readValue(content, WebSocketRequest.class);
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.containsKey(sessionId)) {
principal = AuthCache.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) {
@@ -56,8 +84,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()));
@@ -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"));
@@ -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, 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);
@@ -2,6 +2,7 @@ package timely.store;
import static org.apache.accumulo.core.conf.AccumuloConfiguration.getMemoryInBytes;
import static org.apache.accumulo.core.conf.AccumuloConfiguration.getTimeInMillis;
import static timely.adapter.accumulo.MetricAdapter.VISIBILITY_TAG;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -18,6 +19,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.Timer;
@@ -50,6 +52,8 @@ import org.apache.accumulo.core.data.*;
import org.apache.accumulo.core.iterators.IteratorUtil.IteratorScope;
import org.apache.accumulo.core.iterators.user.RegExFilter;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.accumulo.core.security.VisibilityEvaluator;
import org.apache.accumulo.core.util.Pair;
import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.lang3.StringUtils;
@@ -71,8 +75,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,17 +124,18 @@ 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;
private final String defaultVisibility;
public DataStoreImpl(Configuration conf, int numWriteThreads) throws TimelyException {
@@ -147,7 +154,32 @@ 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();
defaultVisibility = conf.getDefaultVisibility();
if (StringUtils.isNotBlank(defaultVisibility)) {
ColumnVisibility vis;
try {
// validate visibility
vis = new ColumnVisibility(defaultVisibility);
LOG.info("Using defaultVisibility: {}", vis.toString());
} catch (RuntimeException e) {
LOG.error("Error validating defaultVisibility " + defaultVisibility + " " + e.getMessage(), e);
throw e;
}
try {
String whoami = connector.whoami();
Authorizations auths = connector.securityOperations().getUserAuthorizations(whoami);
VisibilityEvaluator eval = new VisibilityEvaluator(auths);
if (!eval.evaluate(vis)) {
throw new IllegalArgumentException("Accumulo user " + whoami + " with authorization "
+ auths.toString() + " can not see data with defaultVisibility " + vis.toString());
}
} catch (RuntimeException e) {
LOG.error(e.getMessage(), e);
throw e;
}
}
metricsTable = conf.getMetricsTable();
if (metricsTable.contains(".")) {
@@ -278,6 +310,17 @@ public class DataStoreImpl implements DataStore {
public void store(Metric metric, boolean cacheEnabled) {
LOG.trace("Received Store Request for: {}", metric);
// if default visibility is configured and current metric does not contain a
// visibility
// add it here, so that the cache and accumulo stores both have the correct
// visibility
if (StringUtils.isNotBlank(defaultVisibility)) {
Optional<Tag> visTag = metric.getTags().stream().filter(t -> t.getKey().equals(VISIBILITY_TAG)).findFirst();
if (!visTag.isPresent()) {
metric.addTag(new Tag(VISIBILITY_TAG, defaultVisibility));
}
}
if (cache != null && cacheEnabled) {
cache.store(metric);
}
@@ -505,8 +548,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 +591,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 +624,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 +702,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 +990,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 +1003,15 @@ public class DataStoreImpl implements DataStore {
}
}
public Scanner createScannerForMetric(String sessionId, String metric, Map<String, String> tags,
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 [{}] with auths: {}", request.getUserName(), auths);
Scanner s = ScannerHelper.createScanner(connector, this.metricsTable, auths);
if (tags == null) {
tags = new LinkedHashMap<>();
}
+24 -38
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;
@@ -44,7 +43,9 @@ 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;
@@ -71,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;
@@ -88,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);
@@ -602,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());
@@ -633,8 +635,8 @@ public class DataStoreCache {
}
}
public List<MetricResponse> getMetricsFromCache(String metric, Map<String, String> tags, long begin, long end,
String sessionId) {
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();
@@ -644,8 +646,13 @@ public class DataStoreCache {
subQuery.addTag(t.getKey(), t.getValue());
}
}
VisibilityFilter visFilter = new VisibilityFilter(getSessionAuthorizations(sessionId));
DataStoreCacheIterator itr = new DataStoreCacheIterator(this, visFilter, subQuery, begin, end);
Collection<Authorizations> authorizations = getSessionAuthorizations(request);
Collection<Authorizations> minimizedAuths = AuthorizationsUtil.minimize(authorizations);
Collection<VisibilityFilter> visibilityFilters = new ArrayList<>();
for (Authorizations a : minimizedAuths) {
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()) {
@@ -667,7 +674,7 @@ public class DataStoreCache {
}
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;
@@ -686,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()) {
@@ -737,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);
}
}
}
@@ -27,6 +27,7 @@ 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;
@@ -44,6 +45,7 @@ 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 String scannerInfo;
@@ -66,14 +68,15 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
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, ChannelHandlerContext ctx, int scannerBatchSize, int flushIntervalSeconds, int scannerReadAhead,
int subscriptionBatchSize) throws TimelyException {
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.beginTime = beginTime;
@@ -118,13 +121,13 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
// cached
ranges.addAll(
this.store.getQueryRanges(metric, this.beginTime, oldestTsForMetric - 1, colFamValues));
metricsFromCache = cache.getMetricsFromCache(metric, tags, oldestTsForMetric, endTimeStamp,
sessionId);
metricsFromCache = cache.getMetricsFromCache(this.request, metric, tags, oldestTsForMetric,
endTimeStamp);
LOG.debug("[{}] MetricScanner request partially fulfilled from cache: {} metrics",
subscriptionId, metricsFromCache.size());
} else {
metricsFromCache = cache.getMetricsFromCache(metric, tags, this.beginTime, endTimeStamp,
sessionId);
metricsFromCache = cache.getMetricsFromCache(this.request, metric, tags, this.beginTime,
endTimeStamp);
LOG.debug("[{}] Websocket request completely fulfilled from cache: {} metrics [{}]",
subscriptionId, metricsFromCache.size());
}
@@ -159,7 +162,7 @@ public class MetricScanner extends Thread implements UncaughtExceptionHandler {
this.scanner = null;
this.done = true;
} else {
this.scanner = this.store.createScannerForMetric(sessionId, metric, tags, scannerBatchSize,
this.scanner = this.store.createScannerForMetric(this.request, metric, tags, scannerBatchSize,
scannerReadAhead);
rangeItr = ranges.iterator();
if (rangeItr.hasNext()) {
@@ -10,6 +10,7 @@ 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;
@@ -54,11 +55,11 @@ public class Subscription {
}, rate, rate, TimeUnit.SECONDS);
}
public void addMetric(String metric, Map<String, String> tags, long startTime, long endTime, long delay)
throws TimelyException {
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, ctx, scannerBatchSize, flushIntervalSeconds, scannerReadAhead,
startTime, endTime, delay, lag, request, ctx, scannerBatchSize, flushIntervalSeconds, scannerReadAhead,
subscriptionBatchSize);
METRICS.put(metric, m);
m.start();
@@ -14,6 +14,7 @@ import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.commons.configuration.BaseConfiguration;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import timely.api.model.Meta;
@@ -26,7 +27,7 @@ public class GetMetricTableSplitPoints {
public static void main(String[] args) throws Exception {
try (ConfigurableApplicationContext ctx = new SpringApplicationBuilder(SpringBootstrap.class)
.bannerMode(Mode.OFF).web(false).run(args)) {
.bannerMode(Mode.OFF).web(WebApplicationType.NONE).run(args)) {
Configuration conf = ctx.getBean(Configuration.class);
final BaseConfiguration apacheConf = new BaseConfiguration();
@@ -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*=\\s*My Department\\s*(?:,|$)
@@ -4,16 +4,22 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import timely.configuration.Configuration;
import timely.configuration.SpringBootstrap;
public class ConfigurationTest {
private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
private AnnotationConfigApplicationContext context;
@Before
public void createContext() {
context = new AnnotationConfigApplicationContext();
}
@After
public void closeContext() {
@@ -24,7 +30,7 @@ public class ConfigurationTest {
public void testMinimalConfiguration() throws Exception {
context.register(SpringBootstrap.class);
// @formatter:off
EnvironmentTestUtils.addEnvironment(this.context,
TestPropertyValues.of(
"timely.server.ip:127.0.0.1",
"timely.server.tcp-port:54321",
"timely.server.udp-port:54325",
@@ -38,7 +44,7 @@ public class ConfigurationTest {
"timely.accumulo.password:secret",
"timely.http.host:localhost",
"timely.security.serverSsl.use-generated-keypair:true",
"timely.metric-age-off-days[default]:7");
"timely.metric-age-off-days[default]:7").applyTo(this.context);
// @formatter:on
context.refresh();
Configuration config = this.context.getBean(Configuration.class);
@@ -62,7 +68,7 @@ public class ConfigurationTest {
public void testMissingSSLProperty() throws Exception {
context.register(SpringBootstrap.class);
// @formatter:off
EnvironmentTestUtils.addEnvironment(this.context,
TestPropertyValues.of(
"timely.server.ip:127.0.0.1",
"timely.server.tcp-port:54321",
"timely.server.udp-port:54325",
@@ -74,7 +80,7 @@ public class ConfigurationTest {
"timely.accumulo.instance-name:test",
"timely.accumulo.username:root",
"timely.accumulo.password:secret",
"timely.http.host:localhost");
"timely.http.host:localhost").applyTo(this.context);
// @formatter:on
context.refresh();
}
@@ -83,7 +89,7 @@ public class ConfigurationTest {
public void testSSLProperty() throws Exception {
context.register(SpringBootstrap.class);
// @formatter:off
EnvironmentTestUtils.addEnvironment(this.context,
TestPropertyValues.of(
"timely.server.ip:127.0.0.1",
"timely.server.tcp-port:54321",
"timely.server.udp-port:54325",
@@ -97,7 +103,7 @@ public class ConfigurationTest {
"timely.accumulo.password:secret",
"timely.http.host:localhost",
"timely.security.serverSsl.certificate-file:/tmp/foo",
"timely.security.serverSsl.key-file:/tmp/bar");
"timely.security.serverSsl.key-file:/tmp/bar").applyTo(this.context);
// @formatter:on
context.refresh();
}
@@ -2,16 +2,17 @@ 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;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
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.test.TestConfiguration;
@@ -27,12 +28,16 @@ public class AuthCacheTest {
cookie = URLEncoder.encode(UUID.randomUUID().toString(), StandardCharsets.UTF_8.name());
}
@AfterClass
public static void after() {
AuthCache.resetSessionMaxAge();
}
@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);
AuthenticationService.authenticate(token, cookie);
}
@After
@@ -47,7 +52,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());
}
@@ -2,55 +2,76 @@ package timely.auth;
import java.util.Collection;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import timely.configuration.Configuration;
import timely.test.TestConfiguration;
public class AuthenticationServiceTest {
private static Configuration config;
@BeforeClass
public static void before() {
config = TestConfiguration.createMinimalConfigurationForTest();
config.getSecurity().setSessionMaxAge(Integer.MAX_VALUE);
AuthCache.resetSessionMaxAge();
AuthCache.setSessionMaxAge(config.getSecurity());
}
@Before
public void setup() {
AuthCache.clear();
}
@AfterClass
public static void after() {
AuthCache.resetSessionMaxAge();
}
@Test(expected = BadCredentialsException.class)
public void testBasicAuthenticationFailure() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", "test2");
AuthenticationService.getAuthenticationManager().authenticate(token);
AuthenticationService.authenticate(token, "test");
}
@Test
public void testBasicAuthenticationLogin() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test", "test1");
Authentication auth = AuthenticationService.getAuthenticationManager().authenticate(token);
Collection<? extends GrantedAuthority> authorizations = auth.getAuthorities();
TimelyPrincipal principal = AuthenticationService.authenticate(token, "test");
Collection<? extends Collection<String>> authorizations = principal.getAuthorizations();
authorizations.forEach(a -> {
Assert.assertTrue(
a.getAuthority().equals("A") || a.getAuthority().equals("B") || a.getAuthority().equals("C"));
Assert.assertTrue(a.contains("A") || a.contains("B") || a.contains("C"));
});
}
@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);
Collection<? extends GrantedAuthority> authorizations = auth.getAuthorities();
TimelyPrincipal principal = AuthenticationService.authenticate(token, "test");
Collection<? extends Collection<String>> authorizations = principal.getAuthorizations();
authorizations.forEach(a -> {
Assert.assertTrue(
a.getAuthority().equals("D") || a.getAuthority().equals("E") || a.getAuthority().equals("F"));
Assert.assertTrue(a.contains("D") || a.contains("E") || a.contains("F"));
});
}
@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);
Collection<? extends GrantedAuthority> authorizations = auth.getAuthorities();
TimelyPrincipal principal = AuthenticationService.authenticate(token, "test");
Collection<? extends Collection<String>> authorizations = principal.getAuthorizations();
authorizations.forEach(a -> {
Assert.assertTrue(
a.getAuthority().equals("D") || a.getAuthority().equals("E") || a.getAuthority().equals("F"));
Assert.assertTrue(a.contains("D") || a.contains("E") || a.contains("F"));
});
}
@@ -0,0 +1,45 @@
package timely.auth;
import java.util.Arrays;
import java.util.ServiceConfigurationError;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class UserDetailsServiceTest {
private static ApplicationContext springContext = null;
private static UserDetailsService userDetailsService = null;
@BeforeClass
public static void setupClass() {
try {
springContext = new ClassPathXmlApplicationContext("security.xml");
userDetailsService = (UserDetailsService) springContext.getBean("timelyUserDetailsService");
} catch (BeansException e) {
throw new ServiceConfigurationError("Error initializing from spring: " + e.getMessage(), e.getRootCause());
}
}
@Test
public void testAuths() {
TimelyUser u = userDetailsService.getUsers().get("CN=example.com");
Assert.assertNotNull("TimelyUser not found", u);
Assert.assertEquals("3 auths expected", 3, u.getAuths().size());
Assert.assertTrue("Unexpected auths found " + u.getAuths(),
u.getAuths().containsAll(Arrays.asList("D", "E", "F")));
}
@Test
public void testRoles() {
TimelyUser u = userDetailsService.getUsers().get("CN=example.com");
Assert.assertNotNull("TimelyUser not found", u);
Assert.assertEquals("3 roles expected", 3, u.getRoles().size());
Assert.assertTrue("Unexpected roles found " + u.getRoles(),
u.getRoles().containsAll(Arrays.asList("G", "H", "I")));
}
}
@@ -31,7 +31,9 @@ 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;
@@ -66,10 +68,11 @@ 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");
AuthenticationService.authenticate(token, cookie);
}
@AfterClass
@@ -96,7 +99,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 +130,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 +378,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 +513,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
@@ -57,6 +57,7 @@ public class BasicAuthLoginRequestHandlerTest {
@Before
public void setup() throws Exception {
AuthCache.clear();
results.clear();
}
@@ -28,6 +28,8 @@ 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.configuration.Configuration;
import timely.model.Metric;
import timely.subscription.SubscriptionRegistry;
@@ -60,10 +62,11 @@ 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.put(cookie, new TimelyPrincipal(TimelyUser.ANONYMOUS_USER));
}
@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());
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more