Skip to content

authentication and authorization #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/main/java/io/cdap/http/AuthHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright © 2017-2019 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package io.cdap.http;

import io.netty.handler.codec.http.HttpRequest;

/**
* Basic interface for implementing an AuthHandler.
*/
public interface AuthHandler {
/**
* This method is responsible for deciding whether a request is authenticated.
*
* @param request the HttpRequest in question.
* @return <code>true</code> if this request should be handled.
* @see Secured
*/
public boolean isAuthenticated(HttpRequest request);

/**
* This method is responsible for deciding whether a request meets the role requirement.
*
* @param request the HttpRequest in question.
* @param roles the roles that are required for this request to be handled.
* @return <code>true</code> if this request should be handled.
* @see RequiredRoles
*/
public boolean hasRoles(HttpRequest request, String[] roles);

/**
* Returns the value for the <code>WWW-Authenticate</code> header field that will be
* set for requests which were rejected by {@link #isAuthenticated(HttpRequest)}
* or {@link #hasRoles(HttpRequest, String[])}.
* @return value of the <code>WWW-Authenticate</code> header field
*/
public String getWWWAuthenticateHeader();
}
22 changes: 19 additions & 3 deletions src/main/java/io/cdap/http/NettyHttpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ private enum State {
* @param pipelineModifier Function used to modify the pipeline.
* @param sslHandlerFactory Object used to share SSL certificate details
* @param exceptionHandler Handles exceptions from calling handler methods
* @param authHandler Handles authentication and authorization
*/
private NettyHttpService(String serviceName,
InetSocketAddress bindAddress, int bossThreadPoolSize, int workerThreadPoolSize,
Expand All @@ -122,7 +123,8 @@ private NettyHttpService(String serviceName,
Iterable<? extends HttpHandler> httpHandlers,
Iterable<? extends HandlerHook> handlerHooks, int httpChunkLimit,
ChannelPipelineModifier pipelineModifier,
SSLHandlerFactory sslHandlerFactory, ExceptionHandler exceptionHandler) {
SSLHandlerFactory sslHandlerFactory, ExceptionHandler exceptionHandler,
AuthHandler authHandler) {
this.serviceName = serviceName;
this.bindAddress = bindAddress;
this.bossThreadPoolSize = bossThreadPoolSize;
Expand All @@ -132,7 +134,8 @@ private NettyHttpService(String serviceName,
this.channelConfigs = new HashMap<>(channelConfigs);
this.childChannelConfigs = new HashMap<>(childChannelConfigs);
this.rejectedExecutionHandler = rejectedExecutionHandler;
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter, exceptionHandler);
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter, exceptionHandler,
authHandler);
this.handlerContext = new BasicHandlerContext(this.resourceHandler);
this.httpChunkLimit = httpChunkLimit;
this.pipelineModifier = pipelineModifier;
Expand Down Expand Up @@ -438,6 +441,7 @@ public static class Builder {
private SSLHandlerFactory sslHandlerFactory;
private ChannelPipelineModifier pipelineModifier;
private ExceptionHandler exceptionHandler;
private AuthHandler authHandler;

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

/**
* Add the {@link AuthHandler} that performs the checks for methods annotated by
* {@link Secured} and {@link RequiredRoles}.
*
* @param authHandler the handler to add
* @return instance of {@code Builder}.
*/
public Builder setAuthHandler(AuthHandler authHandler) {
this.authHandler = authHandler;
return this;
}

/**
* Sets the {@link ChannelPipelineModifier} to use for modifying {@link ChannelPipeline} on new {@link Channel}
* registration.
Expand Down Expand Up @@ -693,7 +709,7 @@ public NettyHttpService build() {
return new NettyHttpService(serviceName, bindAddress, bossThreadPoolSize, workerThreadPoolSize,
execThreadPoolSize, execThreadKeepAliveSecs, channelConfigs, childChannelConfigs,
rejectedExecutionHandler, urlRewriter, handlers, handlerHooks, httpChunkLimit,
pipelineModifier, sslHandlerFactory, exceptionHandler);
pipelineModifier, sslHandlerFactory, exceptionHandler, authHandler);
}
}
}
35 changes: 35 additions & 0 deletions src/main/java/io/cdap/http/RequiredRoles.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright © 2017-2019 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package io.cdap.http;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Indicates that the annotated method should only be called if the requesting
* {@link io.netty.handler.codec.http.HttpRequest} was deemed authorized by the registered's
* {@link AuthHandler#hasRoles(io.netty.handler.codec.http.HttpRequest, String[])} method.
*/
@Documented
@Retention(RUNTIME)
@Target(METHOD)
public @interface RequiredRoles {
String[] value();
}
34 changes: 34 additions & 0 deletions src/main/java/io/cdap/http/Secured.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright © 2017-2019 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package io.cdap.http;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates that the annotated method should only be called if the requesting
* {@link io.netty.handler.codec.http.HttpRequest} was deemed authenticated by the
* registered's {@link AuthHandler#isAuthenticated(io.netty.handler.codec.http.HttpRequest)} method.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Secured {
}
48 changes: 48 additions & 0 deletions src/main/java/io/cdap/http/internal/AuthenticationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright © 2017-2019 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package io.cdap.http.internal;

import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;

import java.nio.charset.StandardCharsets;

/**
* Exception that gets thrown when a request tries to access a secured resource
* and {@link io.cdap.http.AuthHandler#isAuthenticated(HttpRequest)} doesn't accept it.
*/
public class AuthenticationException extends HandlerException {

public AuthenticationException(String message) {
super(HttpResponseStatus.UNAUTHORIZED, message);
}

@Override
public HttpResponse createFailureResponse() {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED,
Unpooled.copiedBuffer("UNAUTHORIZED", StandardCharsets.UTF_8));
response.headers().add("WWW-Authenticate", getMessage());
HttpUtil.setContentLength(response, response.content().readableBytes());
return response;
}
}
47 changes: 47 additions & 0 deletions src/main/java/io/cdap/http/internal/AuthorizationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright © 2017-2019 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package io.cdap.http.internal;

import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;

import java.nio.charset.StandardCharsets;

/**
* Exception that gets thrown when a request tries to access a resource that requires roles
* and {@link io.cdap.http.AuthHandler#hasRoles(HttpRequest, String[])} doesn't accept it.
*/
public class AuthorizationException extends HandlerException {

public AuthorizationException() {
super(HttpResponseStatus.UNAUTHORIZED, "Request doesn't satisfy role requirement.");
}

@Override
public HttpResponse createFailureResponse() {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN,
Unpooled.copiedBuffer("FORBIDDEN", StandardCharsets.UTF_8));
HttpUtil.setContentLength(response, response.content().readableBytes());
return response;
}
}
10 changes: 7 additions & 3 deletions src/main/java/io/cdap/http/internal/HandlerException.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@
/**
*Creating Http Response for Exception messages.
*/
final class HandlerException extends Exception {
public class HandlerException extends Exception {

private final HttpResponseStatus failureStatus;
private final String message;

HandlerException(HttpResponseStatus failureStatus, String message) {
public HandlerException(HttpResponseStatus failureStatus, String message) {
super(message);
this.failureStatus = failureStatus;
this.message = message;
Expand All @@ -46,10 +46,14 @@ final class HandlerException extends Exception {
this.message = message;
}

HttpResponse createFailureResponse() {
public HttpResponse createFailureResponse() {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, failureStatus,
Unpooled.copiedBuffer(message, StandardCharsets.UTF_8));
HttpUtil.setContentLength(response, response.content().readableBytes());
return response;
}

public String getMessage() {
return message;
}
}
22 changes: 21 additions & 1 deletion src/main/java/io/cdap/http/internal/HttpMethodInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,22 @@ class HttpMethodInfo {
private final Object[] args;
private final boolean isStreaming;
private final ExceptionHandler exceptionHandler;
private final boolean isSecured;
private final String[] requiredRoles;

private HttpRequest request;
private BodyConsumer bodyConsumer;

HttpMethodInfo(Method method, HttpHandler handler,
HttpResponder responder, Object[] args, ExceptionHandler exceptionHandler) {
HttpResponder responder, Object[] args, ExceptionHandler exceptionHandler,
boolean isSecured, String[] requiredRoles) {
this.method = method;
this.handler = handler;
this.isStreaming = BodyConsumer.class.isAssignableFrom(method.getReturnType());
this.responder = responder;
this.exceptionHandler = exceptionHandler;
this.isSecured = isSecured;
this.requiredRoles = requiredRoles;

// The actual arguments list to invoke handler method
this.args = new Object[args.length + 2];
Expand Down Expand Up @@ -189,4 +194,19 @@ void sendError(HttpResponseStatus status, Throwable ex) {
boolean isStreaming() {
return isStreaming;
}

/**
* Returns true if the method was annotated by {@link Secured}.
*/
public boolean isSecured() {
return isSecured;
}

/**
* Returns the roles that required to execute this method as specified by {@link RequiredRoles}
* or <code>null</code> if none were specified.
*/
public String[] getRequiredRoles() {
return requiredRoles;
}
}
Loading