Skip to content

Commit 35e7243

Browse files
zacw7arhimondr
authored andcommitted
Add authorization support for Jetty Servlets
1 parent 5d86fba commit 35e7243

File tree

7 files changed

+505
-14
lines changed

7 files changed

+505
-14
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2010 Proofpoint, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of 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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.facebook.airlift.http.server;
17+
18+
import com.google.common.collect.ImmutableSet;
19+
20+
import javax.annotation.security.RolesAllowed;
21+
import javax.servlet.Servlet;
22+
import javax.servlet.ServletException;
23+
import javax.servlet.ServletRequest;
24+
import javax.servlet.ServletResponse;
25+
import javax.servlet.http.HttpServlet;
26+
import javax.servlet.http.HttpServletRequest;
27+
import javax.servlet.http.HttpServletResponse;
28+
29+
import java.io.IOException;
30+
import java.io.InputStream;
31+
import java.security.Principal;
32+
import java.util.Optional;
33+
import java.util.Set;
34+
35+
import static com.facebook.airlift.http.server.HttpServerConfig.AuthorizationPolicy;
36+
import static com.google.common.io.ByteStreams.copy;
37+
import static com.google.common.io.ByteStreams.nullOutputStream;
38+
import static java.lang.String.format;
39+
import static java.util.Objects.requireNonNull;
40+
41+
public class AuthorizationEnabledServlet
42+
extends HttpServlet
43+
{
44+
private final Servlet delegate;
45+
private final Authorizer authorizer;
46+
private final AuthorizationPolicy authorizationPolicy;
47+
private final Set<String> defaultAllowedRoles;
48+
private final Optional<Set<String>> allowedRoles;
49+
50+
public AuthorizationEnabledServlet(
51+
Servlet delegate,
52+
Authorizer authorizer,
53+
AuthorizationPolicy authorizationPolicy,
54+
Set<String> defaultAllowedRoles)
55+
{
56+
this.delegate = requireNonNull(delegate, "delegate is null");
57+
this.authorizer = requireNonNull(authorizer, "authorizer is null");
58+
this.authorizationPolicy = requireNonNull(authorizationPolicy, "authorizationPolicy is null");
59+
this.defaultAllowedRoles = requireNonNull(defaultAllowedRoles, "defaultAllowedRoles is null");
60+
this.allowedRoles = getRolesFromClassMetadata(delegate);
61+
}
62+
63+
@Override
64+
public void init()
65+
throws ServletException
66+
{
67+
super.init();
68+
delegate.init(this.getServletConfig());
69+
}
70+
71+
@Override
72+
public void service(ServletRequest req, ServletResponse res)
73+
throws ServletException, IOException
74+
{
75+
HttpServletRequest request = (HttpServletRequest) req;
76+
HttpServletResponse response = (HttpServletResponse) res;
77+
78+
Principal principal = request.getUserPrincipal();
79+
if (principal == null) {
80+
abortWithMessage(request, response, "Request principal is missing.");
81+
return;
82+
}
83+
84+
Optional<Set<String>> allowedRoles = this.allowedRoles;
85+
if (!allowedRoles.isPresent()) {
86+
switch (authorizationPolicy) {
87+
case ALLOW:
88+
delegate.service(req, res);
89+
return;
90+
case DENY:
91+
abortWithMessage(request, response, format("Principal %s is not allowed to access the resource. Reason: denied by default policy",
92+
principal.getName()));
93+
return;
94+
case DEFAULT_ROLES:
95+
allowedRoles = Optional.of(defaultAllowedRoles);
96+
break;
97+
default:
98+
}
99+
}
100+
101+
AuthorizationResult result = authorizer.authorize(principal, allowedRoles.get());
102+
if (!result.isAllowed()) {
103+
abortWithMessage(request, response, format("Principal %s is not allowed to access the resource. Reason: %s",
104+
principal.getName(),
105+
result.getReason()));
106+
return;
107+
}
108+
delegate.service(req, res);
109+
}
110+
111+
private static void abortWithMessage(HttpServletRequest request, HttpServletResponse response, String message)
112+
throws IOException
113+
{
114+
skipRequestBody(request);
115+
response.sendError(HttpServletResponse.SC_FORBIDDEN, format(message));
116+
}
117+
118+
private static void skipRequestBody(HttpServletRequest request)
119+
throws IOException
120+
{
121+
// If we send the challenge without consuming the body of the request,
122+
// the server will close the connection after sending the response.
123+
// The client may interpret this as a failed request and not resend the
124+
// request with the authentication header. We can avoid this behavior
125+
// in the client by reading and discarding the entire body of the
126+
// unauthenticated request before sending the response.
127+
try (InputStream inputStream = request.getInputStream()) {
128+
copy(inputStream, nullOutputStream());
129+
}
130+
}
131+
132+
private static Optional<Set<String>> getRolesFromClassMetadata(Servlet servlet)
133+
{
134+
if (servlet.getClass().isAnnotationPresent(RolesAllowed.class)) {
135+
return Optional.of(ImmutableSet.copyOf(servlet.getClass().getAnnotation(RolesAllowed.class).value()));
136+
}
137+
else {
138+
return Optional.empty();
139+
}
140+
}
141+
}

http-server/src/main/java/com/facebook/airlift/http/server/HttpServer.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383

8484
import static com.facebook.airlift.http.utils.jetty.ConcurrentScheduler.createConcurrentScheduler;
8585
import static com.google.common.base.MoreObjects.firstNonNull;
86+
import static com.google.common.base.Preconditions.checkArgument;
8687
import static com.google.common.base.Preconditions.checkState;
8788
import static java.lang.Math.toIntExact;
8889
import static java.lang.String.format;
@@ -118,7 +119,8 @@ public HttpServer(HttpServerInfo httpServerInfo,
118119
LoginService loginService,
119120
TraceTokenManager tokenManager,
120121
RequestStats stats,
121-
EventClient eventClient)
122+
EventClient eventClient,
123+
Authorizer authorizer)
122124
throws IOException
123125
{
124126
requireNonNull(httpServerInfo, "httpServerInfo is null");
@@ -368,7 +370,7 @@ public HttpServer(HttpServerInfo httpServerInfo,
368370
handlers.addHandler(gzipHandler);
369371
}
370372

371-
handlers.addHandler(createServletContext(defaultServlet, servlets, parameters, filters, tokenManager, loginService, "http", "https"));
373+
handlers.addHandler(createServletContext(config, defaultServlet, servlets, parameters, filters, tokenManager, loginService, authorizer, "http", "https"));
372374

373375
if (config.isRequestStatsEnabled()) {
374376
RequestLogHandler statsRecorder = new RequestLogHandler();
@@ -382,7 +384,7 @@ public HttpServer(HttpServerInfo httpServerInfo,
382384

383385
HandlerList rootHandlers = new HandlerList();
384386
if (theAdminServlet != null && config.isAdminEnabled()) {
385-
rootHandlers.addHandler(createServletContext(theAdminServlet, ImmutableMap.of(), adminParameters, adminFilters, tokenManager, loginService, "admin"));
387+
rootHandlers.addHandler(createServletContext(config, theAdminServlet, ImmutableMap.of(), adminParameters, adminFilters, tokenManager, loginService, authorizer, "admin"));
386388
}
387389
rootHandlers.addHandler(statsHandler);
388390
server.setHandler(rootHandlers);
@@ -394,12 +396,14 @@ public HttpServer(HttpServerInfo httpServerInfo,
394396
}
395397

396398
private static ServletContextHandler createServletContext(
399+
HttpServerConfig config,
397400
Servlet defaultServlet,
398401
Map<String, Servlet> servlets,
399402
Map<String, String> parameters,
400403
Set<Filter> filters,
401404
TraceTokenManager tokenManager,
402405
LoginService loginService,
406+
Authorizer authorizer,
403407
String... connectorNames)
404408
{
405409
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
@@ -427,9 +431,22 @@ private static ServletContextHandler createServletContext(
427431
context.addServlet(servletHolder, "/*");
428432

429433
for (Map.Entry<String, Servlet> servlet : servlets.entrySet()) {
430-
ServletHolder holder = new ServletHolder(servlet.getValue());
431-
holder.setInitParameters(ImmutableMap.copyOf(parameters));
432-
context.addServlet(holder, servlet.getKey());
434+
if (config.isAuthorizationEnabled()) {
435+
checkArgument(authorizer != null, "when authorization is enabled, authorizer implementation must be provided");
436+
AuthorizationEnabledServlet authorizationEnabledServlet = new AuthorizationEnabledServlet(
437+
servlet.getValue(),
438+
authorizer,
439+
config.getDefaultAuthorizationPolicy(),
440+
config.getDefaultAllowedRoles());
441+
ServletHolder holder = new ServletHolder(authorizationEnabledServlet);
442+
holder.setInitParameters(ImmutableMap.copyOf(parameters));
443+
context.addServlet(holder, servlet.getKey());
444+
}
445+
else {
446+
ServletHolder holder = new ServletHolder(servlet.getValue());
447+
holder.setInitParameters(ImmutableMap.copyOf(parameters));
448+
context.addServlet(holder, servlet.getKey());
449+
}
433450
}
434451

435452
// Starting with Jetty 9 there is no way to specify connectors directly, but

http-server/src/main/java/com/facebook/airlift/http/server/HttpServerProvider.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class HttpServerProvider
5959
private final Set<Filter> adminFilters;
6060
private TraceTokenManager traceTokenManager;
6161
private final EventClient eventClient;
62+
private Authorizer authorizer;
6263

6364
@Inject
6465
public HttpServerProvider(HttpServerInfo httpServerInfo,
@@ -131,6 +132,12 @@ public void setTokenManager(@Nullable TraceTokenManager tokenManager)
131132
this.traceTokenManager = tokenManager;
132133
}
133134

135+
@Inject(optional = true)
136+
public void setAuthorizer(@Nullable Authorizer authorizer)
137+
{
138+
this.authorizer = authorizer;
139+
}
140+
134141
@Override
135142
public HttpServer get()
136143
{
@@ -150,7 +157,8 @@ public HttpServer get()
150157
loginService,
151158
traceTokenManager,
152159
stats,
153-
eventClient);
160+
eventClient,
161+
authorizer);
154162
httpServer.start();
155163
return httpServer;
156164
}

http-server/src/main/java/com/facebook/airlift/http/server/testing/TestingHttpServer.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.facebook.airlift.http.server.testing;
1717

1818
import com.facebook.airlift.event.client.NullEventClient;
19+
import com.facebook.airlift.http.server.Authorizer;
1920
import com.facebook.airlift.http.server.HttpServer;
2021
import com.facebook.airlift.http.server.HttpServerBinder.HttpResourceBinding;
2122
import com.facebook.airlift.http.server.HttpServerConfig;
@@ -25,14 +26,15 @@
2526
import com.facebook.airlift.node.NodeInfo;
2627
import com.facebook.airlift.tracetoken.TraceTokenManager;
2728
import com.google.common.collect.ImmutableSet;
29+
import com.google.inject.Inject;
2830

29-
import javax.inject.Inject;
3031
import javax.servlet.Filter;
3132
import javax.servlet.Servlet;
3233

3334
import java.io.IOException;
3435
import java.net.URI;
3536
import java.util.Map;
37+
import java.util.Optional;
3638
import java.util.Set;
3739

3840
public class TestingHttpServer
@@ -46,7 +48,8 @@ public TestingHttpServer(
4648
HttpServerConfig config,
4749
@TheServlet Servlet servlet,
4850
@TheServlet Map<String, Servlet> servlets,
49-
@TheServlet Map<String, String> initParameters)
51+
@TheServlet Map<String, String> initParameters,
52+
Optional<Authorizer> authorizer)
5053
throws IOException
5154
{
5255
this(httpServerInfo,
@@ -56,7 +59,8 @@ public TestingHttpServer(
5659
servlets,
5760
initParameters,
5861
ImmutableSet.of(),
59-
ImmutableSet.of());
62+
ImmutableSet.of(),
63+
authorizer);
6064
}
6165

6266
@Inject
@@ -68,7 +72,8 @@ public TestingHttpServer(
6872
@TheServlet Map<String, Servlet> servlets,
6973
@TheServlet Map<String, String> initParameters,
7074
@TheServlet Set<Filter> filters,
71-
@TheServlet Set<HttpResourceBinding> resources)
75+
@TheServlet Set<HttpResourceBinding> resources,
76+
Optional<Authorizer> authorizer)
7277
throws IOException
7378
{
7479
super(httpServerInfo,
@@ -86,7 +91,8 @@ public TestingHttpServer(
8691
null,
8792
new TraceTokenManager(),
8893
new RequestStats(),
89-
new NullEventClient());
94+
new NullEventClient(),
95+
authorizer.orElse(null));
9096
this.httpServerInfo = httpServerInfo;
9197
}
9298

http-server/src/main/java/com/facebook/airlift/http/server/testing/TestingHttpServerModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.facebook.airlift.discovery.client.AnnouncementHttpServerInfo;
1919
import com.facebook.airlift.http.server.AuthenticationFilter;
2020
import com.facebook.airlift.http.server.Authenticator;
21+
import com.facebook.airlift.http.server.Authorizer;
2122
import com.facebook.airlift.http.server.HttpServer;
2223
import com.facebook.airlift.http.server.HttpServerConfig;
2324
import com.facebook.airlift.http.server.HttpServerInfo;
@@ -40,6 +41,7 @@
4041
import static com.facebook.airlift.http.server.HttpServerBinder.HttpResourceBinding;
4142
import static com.google.inject.multibindings.MapBinder.newMapBinder;
4243
import static com.google.inject.multibindings.Multibinder.newSetBinder;
44+
import static com.google.inject.multibindings.OptionalBinder.newOptionalBinder;
4345

4446
public class TestingHttpServerModule
4547
implements Module
@@ -79,11 +81,13 @@ public void configure(Binder binder)
7981
newSetBinder(binder, Filter.class, TheServlet.class).addBinding()
8082
.to(AuthenticationFilter.class).in(Scopes.SINGLETON);
8183
newSetBinder(binder, Authenticator.class);
84+
newOptionalBinder(binder, Authorizer.class);
8285
}
8386

8487
@Provides
8588
List<Authenticator> getAuthenticatorList(Set<Authenticator> authenticators)
8689
{
8790
return ImmutableList.copyOf(authenticators);
91+
newOptionalBinder(binder, Authorizer.class);
8892
}
8993
}

0 commit comments

Comments
 (0)