Skip to content

Commit 259f4d9

Browse files
authored
Merge pull request #80 from robert-engel/feature/auth
authentication and authorization
2 parents f35afd0 + ddb41df commit 259f4d9

14 files changed

+410
-15
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright © 2017-2019 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package io.cdap.http;
17+
18+
import io.netty.handler.codec.http.HttpRequest;
19+
20+
/**
21+
* Basic interface for implementing an AuthHandler.
22+
*/
23+
public interface AuthHandler {
24+
/**
25+
* This method is responsible for deciding whether a request is authenticated.
26+
*
27+
* @param request the HttpRequest in question.
28+
* @return <code>true</code> if this request should be handled.
29+
* @see Secured
30+
*/
31+
public boolean isAuthenticated(HttpRequest request);
32+
33+
/**
34+
* This method is responsible for deciding whether a request meets the role requirement.
35+
*
36+
* @param request the HttpRequest in question.
37+
* @param roles the roles that are required for this request to be handled.
38+
* @return <code>true</code> if this request should be handled.
39+
* @see RequiredRoles
40+
*/
41+
public boolean hasRoles(HttpRequest request, String[] roles);
42+
43+
/**
44+
* Returns the value for the <code>WWW-Authenticate</code> header field that will be
45+
* set for requests which were rejected by {@link #isAuthenticated(HttpRequest)}
46+
* or {@link #hasRoles(HttpRequest, String[])}.
47+
* @return value of the <code>WWW-Authenticate</code> header field
48+
*/
49+
public String getWWWAuthenticateHeader();
50+
}

src/main/java/io/cdap/http/NettyHttpService.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ private enum State {
112112
* @param pipelineModifier Function used to modify the pipeline.
113113
* @param sslHandlerFactory Object used to share SSL certificate details
114114
* @param exceptionHandler Handles exceptions from calling handler methods
115+
* @param authHandler Handles authentication and authorization
115116
*/
116117
private NettyHttpService(String serviceName,
117118
InetSocketAddress bindAddress, int bossThreadPoolSize, int workerThreadPoolSize,
@@ -122,7 +123,8 @@ private NettyHttpService(String serviceName,
122123
Iterable<? extends HttpHandler> httpHandlers,
123124
Iterable<? extends HandlerHook> handlerHooks, int httpChunkLimit,
124125
ChannelPipelineModifier pipelineModifier,
125-
SSLHandlerFactory sslHandlerFactory, ExceptionHandler exceptionHandler) {
126+
SSLHandlerFactory sslHandlerFactory, ExceptionHandler exceptionHandler,
127+
AuthHandler authHandler) {
126128
this.serviceName = serviceName;
127129
this.bindAddress = bindAddress;
128130
this.bossThreadPoolSize = bossThreadPoolSize;
@@ -132,7 +134,8 @@ private NettyHttpService(String serviceName,
132134
this.channelConfigs = new HashMap<>(channelConfigs);
133135
this.childChannelConfigs = new HashMap<>(childChannelConfigs);
134136
this.rejectedExecutionHandler = rejectedExecutionHandler;
135-
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter, exceptionHandler);
137+
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter, exceptionHandler,
138+
authHandler);
136139
this.handlerContext = new BasicHandlerContext(this.resourceHandler);
137140
this.httpChunkLimit = httpChunkLimit;
138141
this.pipelineModifier = pipelineModifier;
@@ -438,6 +441,7 @@ public static class Builder {
438441
private SSLHandlerFactory sslHandlerFactory;
439442
private ChannelPipelineModifier pipelineModifier;
440443
private ExceptionHandler exceptionHandler;
444+
private AuthHandler authHandler;
441445

442446
// Protected constructor to prevent instantiating Builder instance directly.
443447
protected Builder(String serviceName) {
@@ -456,6 +460,18 @@ protected Builder(String serviceName) {
456460
exceptionHandler = new ExceptionHandler();
457461
}
458462

463+
/**
464+
* Add the {@link AuthHandler} that performs the checks for methods annotated by
465+
* {@link Secured} and {@link RequiredRoles}.
466+
*
467+
* @param authHandler the handler to add
468+
* @return instance of {@code Builder}.
469+
*/
470+
public Builder setAuthHandler(AuthHandler authHandler) {
471+
this.authHandler = authHandler;
472+
return this;
473+
}
474+
459475
/**
460476
* Sets the {@link ChannelPipelineModifier} to use for modifying {@link ChannelPipeline} on new {@link Channel}
461477
* registration.
@@ -693,7 +709,7 @@ public NettyHttpService build() {
693709
return new NettyHttpService(serviceName, bindAddress, bossThreadPoolSize, workerThreadPoolSize,
694710
execThreadPoolSize, execThreadKeepAliveSecs, channelConfigs, childChannelConfigs,
695711
rejectedExecutionHandler, urlRewriter, handlers, handlerHooks, httpChunkLimit,
696-
pipelineModifier, sslHandlerFactory, exceptionHandler);
712+
pipelineModifier, sslHandlerFactory, exceptionHandler, authHandler);
697713
}
698714
}
699715
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright © 2017-2019 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.http;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.Target;
22+
import static java.lang.annotation.ElementType.METHOD;
23+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
24+
25+
/**
26+
* Indicates that the annotated method should only be called if the requesting
27+
* {@link io.netty.handler.codec.http.HttpRequest} was deemed authorized by the registered's
28+
* {@link AuthHandler#hasRoles(io.netty.handler.codec.http.HttpRequest, String[])} method.
29+
*/
30+
@Documented
31+
@Retention(RUNTIME)
32+
@Target(METHOD)
33+
public @interface RequiredRoles {
34+
String[] value();
35+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright © 2017-2019 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.http;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Indicates that the annotated method should only be called if the requesting
27+
* {@link io.netty.handler.codec.http.HttpRequest} was deemed authenticated by the
28+
* registered's {@link AuthHandler#isAuthenticated(io.netty.handler.codec.http.HttpRequest)} method.
29+
*/
30+
@Target({ElementType.METHOD})
31+
@Retention(RetentionPolicy.RUNTIME)
32+
@Documented
33+
public @interface Secured {
34+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright © 2017-2019 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.http.internal;
18+
19+
import io.netty.buffer.Unpooled;
20+
import io.netty.handler.codec.http.DefaultFullHttpResponse;
21+
import io.netty.handler.codec.http.FullHttpResponse;
22+
import io.netty.handler.codec.http.HttpRequest;
23+
import io.netty.handler.codec.http.HttpResponse;
24+
import io.netty.handler.codec.http.HttpResponseStatus;
25+
import io.netty.handler.codec.http.HttpUtil;
26+
import io.netty.handler.codec.http.HttpVersion;
27+
28+
import java.nio.charset.StandardCharsets;
29+
30+
/**
31+
* Exception that gets thrown when a request tries to access a secured resource
32+
* and {@link io.cdap.http.AuthHandler#isAuthenticated(HttpRequest)} doesn't accept it.
33+
*/
34+
public class AuthenticationException extends HandlerException {
35+
36+
public AuthenticationException(String message) {
37+
super(HttpResponseStatus.UNAUTHORIZED, message);
38+
}
39+
40+
@Override
41+
public HttpResponse createFailureResponse() {
42+
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED,
43+
Unpooled.copiedBuffer("UNAUTHORIZED", StandardCharsets.UTF_8));
44+
response.headers().add("WWW-Authenticate", getMessage());
45+
HttpUtil.setContentLength(response, response.content().readableBytes());
46+
return response;
47+
}
48+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright © 2017-2019 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package io.cdap.http.internal;
18+
19+
import io.netty.buffer.Unpooled;
20+
import io.netty.handler.codec.http.DefaultFullHttpResponse;
21+
import io.netty.handler.codec.http.FullHttpResponse;
22+
import io.netty.handler.codec.http.HttpRequest;
23+
import io.netty.handler.codec.http.HttpResponse;
24+
import io.netty.handler.codec.http.HttpResponseStatus;
25+
import io.netty.handler.codec.http.HttpUtil;
26+
import io.netty.handler.codec.http.HttpVersion;
27+
28+
import java.nio.charset.StandardCharsets;
29+
30+
/**
31+
* Exception that gets thrown when a request tries to access a resource that requires roles
32+
* and {@link io.cdap.http.AuthHandler#hasRoles(HttpRequest, String[])} doesn't accept it.
33+
*/
34+
public class AuthorizationException extends HandlerException {
35+
36+
public AuthorizationException() {
37+
super(HttpResponseStatus.UNAUTHORIZED, "Request doesn't satisfy role requirement.");
38+
}
39+
40+
@Override
41+
public HttpResponse createFailureResponse() {
42+
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN,
43+
Unpooled.copiedBuffer("FORBIDDEN", StandardCharsets.UTF_8));
44+
HttpUtil.setContentLength(response, response.content().readableBytes());
45+
return response;
46+
}
47+
}

src/main/java/io/cdap/http/internal/HandlerException.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@
2929
/**
3030
*Creating Http Response for Exception messages.
3131
*/
32-
final class HandlerException extends Exception {
32+
public class HandlerException extends Exception {
3333

3434
private final HttpResponseStatus failureStatus;
3535
private final String message;
3636

37-
HandlerException(HttpResponseStatus failureStatus, String message) {
37+
public HandlerException(HttpResponseStatus failureStatus, String message) {
3838
super(message);
3939
this.failureStatus = failureStatus;
4040
this.message = message;
@@ -46,10 +46,14 @@ final class HandlerException extends Exception {
4646
this.message = message;
4747
}
4848

49-
HttpResponse createFailureResponse() {
49+
public HttpResponse createFailureResponse() {
5050
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, failureStatus,
5151
Unpooled.copiedBuffer(message, StandardCharsets.UTF_8));
5252
HttpUtil.setContentLength(response, response.content().readableBytes());
5353
return response;
5454
}
55+
56+
public String getMessage() {
57+
return message;
58+
}
5559
}

src/main/java/io/cdap/http/internal/HttpMethodInfo.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,22 @@ class HttpMethodInfo {
5050
private final Object[] args;
5151
private final boolean isStreaming;
5252
private final ExceptionHandler exceptionHandler;
53+
private final boolean isSecured;
54+
private final String[] requiredRoles;
5355

5456
private HttpRequest request;
5557
private BodyConsumer bodyConsumer;
5658

5759
HttpMethodInfo(Method method, HttpHandler handler,
58-
HttpResponder responder, Object[] args, ExceptionHandler exceptionHandler) {
60+
HttpResponder responder, Object[] args, ExceptionHandler exceptionHandler,
61+
boolean isSecured, String[] requiredRoles) {
5962
this.method = method;
6063
this.handler = handler;
6164
this.isStreaming = BodyConsumer.class.isAssignableFrom(method.getReturnType());
6265
this.responder = responder;
6366
this.exceptionHandler = exceptionHandler;
67+
this.isSecured = isSecured;
68+
this.requiredRoles = requiredRoles;
6469

6570
// The actual arguments list to invoke handler method
6671
this.args = new Object[args.length + 2];
@@ -189,4 +194,19 @@ void sendError(HttpResponseStatus status, Throwable ex) {
189194
boolean isStreaming() {
190195
return isStreaming;
191196
}
197+
198+
/**
199+
* Returns true if the method was annotated by {@link Secured}.
200+
*/
201+
public boolean isSecured() {
202+
return isSecured;
203+
}
204+
205+
/**
206+
* Returns the roles that required to execute this method as specified by {@link RequiredRoles}
207+
* or <code>null</code> if none were specified.
208+
*/
209+
public String[] getRequiredRoles() {
210+
return requiredRoles;
211+
}
192212
}

0 commit comments

Comments
 (0)