-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
Add support for pluggable Custom Presto Authenticators #24111
Add support for pluggable Custom Presto Authenticators #24111
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @imsayari404, could you please add a test? Will the JwtAuthenticator and PasswordAuthenticator be refactored, as suggested in #24053 (review), in this PR?
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
public class PrestoAuthenticator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be an interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PrestoAuthenticator
class does not need to be an interface because it serves as a concrete adapter between the Airlift Authenticator
interface and the pluggable design provided by PrestoAuthenticator
and PrestoAuthenticatorFactory
SPI. Its role is to delegate authentication to the PrestoAuthenticatorManager
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pramodsatya I understand why you might be confused. We have an interface called PrestoAuthenticator in the presto-spi package with the same name.
@tdcmeehan Currently, we've followed the same naming convention as was done for the PasswordAuthenticator SPI. This could lead to confusion since we have an SPI with the same name as the PasswordAuthenticator class in the presto-main package. Do you think we should refactor the class names in presto-main to eliminate the confusion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. How about CustomPrestoAuthenticator
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the doc! A couple of rephrasing suggestions for conciseness.
Also - because this is a new page - you must add
develop/presto-authenticator
to https://github.com/prestodb/presto/blob/master/presto-docs/src/main/sphinx/develop.rst
so the new page will show up on the Developer Guide index page https://prestodb.io/docs/current/develop.html .
5a9b144
to
caefe42
Compare
@pramodsatya There has been a slight change since my last comment on this issue. After discussions with @tdcmeehan, we introduced a new and more generic SPI that is independent of the current Authenticators. This SPI can be used to implement any type of custom authenticator, without being limited to just a custom implementation of JWT or Password Authenticators. We decided to keep the current implementation of Password and JWT authenticators unchanged to avoid introducing a breaking change. |
@tdcmeehan can you please have a look whenever you get a chance? Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revisions! The page content looks good to me.
However, because this is a new page in the Presto documentation, you must add
develop/presto-authenticator
to https://github.com/prestodb/presto/blob/master/presto-docs/src/main/sphinx/develop.rst
so the new page will show up on the Developer Guide index page in the Presto documentation.
throws AuthenticationException | ||
{ | ||
try { | ||
return authenticatorManager.getAuthenticator().createAuthenticatedPrincipal(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HttpServletRequest
is a little broad because it also returns the input stream of the actual request. In theory, someone could call that in a plugin. I think that should be a loud failure if that happens. Can you implement a subclass of HttpServletRequest
, which wraps an inner HttpServletRequest
, but throws if you attempt to use any method which modifies the request, or reads something which doesn't support repeatable reads (such as getInputStream
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've implemented the changes as suggested. I created a subclass of HttpServletRequest
that wraps an inner HttpServletRequest
and throws an exception if any method is called that modifies the request or accesses non-repeatable data like getInputStream. Let me know if there's anything else you'd like me to adjust.
28a51c9
to
1ad5092
Compare
@Override | ||
public String getPathTranslated() | ||
{ | ||
return super.getPathTranslated(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not necessary right? Just omit this and it will use the parent method.
import java.util.Enumeration; | ||
import java.util.Map; | ||
|
||
public class RestrictedHttpServletRequest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's call it
public class RestrictedHttpServletRequest | |
public class ReadOnlyHttpServletRequest |
throws AuthenticationException | ||
{ | ||
try { | ||
//wrapped the original HttpServletRequest in a RestrictedHttpServletRequest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for this comment, it is self evident
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I'll remove the unnecessary comment and rename the file to CustomPrestoAuthenticator
as suggested.
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
public class PrestoAuthenticator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. How about CustomPrestoAuthenticator
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please squash commits.
public String getQueryString() | ||
{ | ||
return super.getQueryString(); | ||
} | ||
|
||
@Override | ||
public String getRemoteUser() | ||
{ | ||
return super.getRemoteUser(); | ||
} | ||
|
||
@Override | ||
public boolean isUserInRole(String role) | ||
{ | ||
return super.isUserInRole(role); | ||
} | ||
|
||
@Override | ||
public Principal getUserPrincipal() | ||
{ | ||
return super.getUserPrincipal(); | ||
} | ||
|
||
@Override | ||
public String getRequestedSessionId() | ||
{ | ||
return super.getRequestedSessionId(); | ||
} | ||
|
||
@Override | ||
public String getRequestURI() | ||
{ | ||
return super.getRequestURI(); | ||
} | ||
|
||
@Override | ||
public StringBuffer getRequestURL() | ||
{ | ||
return super.getRequestURL(); | ||
} | ||
|
||
@Override | ||
public String getServletPath() | ||
{ | ||
return super.getServletPath(); | ||
} | ||
|
||
@Override | ||
public HttpSession getSession(boolean create) | ||
{ | ||
return super.getSession(create); | ||
} | ||
|
||
@Override | ||
public HttpSession getSession() | ||
{ | ||
return super.getSession(); | ||
} | ||
|
||
@Override | ||
public String changeSessionId() | ||
{ | ||
return super.changeSessionId(); | ||
} | ||
|
||
@Override | ||
public boolean isRequestedSessionIdValid() | ||
{ | ||
return super.isRequestedSessionIdValid(); | ||
} | ||
|
||
@Override | ||
public boolean isRequestedSessionIdFromCookie() | ||
{ | ||
return super.isRequestedSessionIdFromCookie(); | ||
} | ||
|
||
@Override | ||
public boolean isRequestedSessionIdFromURL() | ||
{ | ||
return super.isRequestedSessionIdFromURL(); | ||
} | ||
|
||
@Override | ||
public boolean isRequestedSessionIdFromUrl() | ||
{ | ||
return super.isRequestedSessionIdFromUrl(); | ||
} | ||
|
||
@Override | ||
public Map<String, String[]> getParameterMap() | ||
{ | ||
return super.getParameterMap(); | ||
} | ||
|
||
@Override | ||
public String[] getParameterValues(String name) | ||
{ | ||
return super.getParameterValues(name); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Override | |
public String getAuthType() | |
{ | |
return super.getAuthType(); | |
} | |
@Override | |
public Cookie[] getCookies() | |
{ | |
return super.getCookies(); | |
} | |
@Override | |
public long getDateHeader(String name) | |
{ | |
return super.getDateHeader(name); | |
} | |
@Override | |
public String getHeader(String name) | |
{ | |
return super.getHeader(name); | |
} | |
@Override | |
public Enumeration<String> getHeaders(String name) | |
{ | |
return super.getHeaders(name); | |
} | |
@Override | |
public Enumeration<String> getHeaderNames() | |
{ | |
return super.getHeaderNames(); | |
} | |
@Override | |
public int getIntHeader(String name) | |
{ | |
return super.getIntHeader(name); | |
} | |
@Override | |
public String getMethod() | |
{ | |
return super.getMethod(); | |
} | |
@Override | |
public String getPathInfo() | |
{ | |
return super.getPathInfo(); | |
} | |
@Override | |
public String getContextPath() | |
{ | |
return super.getContextPath(); | |
} | |
@Override | |
public String getQueryString() | |
{ | |
return super.getQueryString(); | |
} | |
@Override | |
public String getRemoteUser() | |
{ | |
return super.getRemoteUser(); | |
} | |
@Override | |
public boolean isUserInRole(String role) | |
{ | |
return super.isUserInRole(role); | |
} | |
@Override | |
public Principal getUserPrincipal() | |
{ | |
return super.getUserPrincipal(); | |
} | |
@Override | |
public String getRequestedSessionId() | |
{ | |
return super.getRequestedSessionId(); | |
} | |
@Override | |
public String getRequestURI() | |
{ | |
return super.getRequestURI(); | |
} | |
@Override | |
public StringBuffer getRequestURL() | |
{ | |
return super.getRequestURL(); | |
} | |
@Override | |
public String getServletPath() | |
{ | |
return super.getServletPath(); | |
} | |
@Override | |
public HttpSession getSession(boolean create) | |
{ | |
return super.getSession(create); | |
} | |
@Override | |
public HttpSession getSession() | |
{ | |
return super.getSession(); | |
} | |
@Override | |
public String changeSessionId() | |
{ | |
return super.changeSessionId(); | |
} | |
@Override | |
public boolean isRequestedSessionIdValid() | |
{ | |
return super.isRequestedSessionIdValid(); | |
} | |
@Override | |
public boolean isRequestedSessionIdFromCookie() | |
{ | |
return super.isRequestedSessionIdFromCookie(); | |
} | |
@Override | |
public boolean isRequestedSessionIdFromURL() | |
{ | |
return super.isRequestedSessionIdFromURL(); | |
} | |
@Override | |
public boolean isRequestedSessionIdFromUrl() | |
{ | |
return super.isRequestedSessionIdFromUrl(); | |
} | |
@Override | |
public Map<String, String[]> getParameterMap() | |
{ | |
return super.getParameterMap(); | |
} | |
@Override | |
public String[] getParameterValues(String name) | |
{ | |
return super.getParameterValues(name); | |
} |
Just remove these, they will all use the parent's implementation if they are not specified.
c175051
to
44d8042
Compare
ReadOnlyHttpServletRequest restrictedRequest = new ReadOnlyHttpServletRequest(request); | ||
return authenticatorManager.getAuthenticator().createAuthenticatedPrincipal(restrictedRequest); | ||
} | ||
catch (RuntimeException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this exception more specific?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just let it throw.
ReadOnlyHttpServletRequest restrictedRequest = new ReadOnlyHttpServletRequest(request); | ||
return authenticatorManager.getAuthenticator().createAuthenticatedPrincipal(restrictedRequest); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ReadOnlyHttpServletRequest restrictedRequest = new ReadOnlyHttpServletRequest(request); | |
return authenticatorManager.getAuthenticator().createAuthenticatedPrincipal(restrictedRequest); | |
authenticatorManager.getAuthenticator().createAuthenticatedPrincipal(new ReadOnlyHttpServletRequest(request)); |
public void setRequired() | ||
{ | ||
required.set(true); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of this flag, let's just pass in the SecurityConfig
as a parameter to this class and @Inject
it. And just check in the constructor if CUSTOM
is one of the authentication types. Then we can make it immutable, private final boolean
.
Finally, let's name it something else, like customAuthenticatorRequested
.
presto-spi/pom.xml
Outdated
@@ -53,6 +53,11 @@ | |||
<artifactId>jol-core</artifactId> | |||
</dependency> | |||
|
|||
<dependency> | |||
<groupId>javax.servlet</groupId> | |||
<artifactId>javax.servlet-api</artifactId> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this could be problematic. I don't think it's a great idea to add the servlet API to the SPI. Instead, wondering if we can do something similar, like just authenticating the headers. That way, instead of the API surface being a ServletRequest
, it could just be a map representing the headers.
presto-lark-sheets/pom.xml
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can undo this change since we are removing the dependency from HttpServletRequest in the new SPI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can undo this change since we are removing the dependency from HttpServletRequest in the new SPI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@imsayari404 thanks for the changes, I have taken another pass and added a few comments.
@@ -147,6 +151,8 @@ public PluginManager( | |||
QueryPreparerProviderManager queryPreparerProviderManager, | |||
AccessControlManager accessControlManager, | |||
PasswordAuthenticatorManager passwordAuthenticatorManager, | |||
SecurityConfig securityConfig, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this is not required, please remove this.
} | ||
|
||
// Utility method to extract headers from HttpServletRequest | ||
private Map<String, String> getHeadersMap(HttpServletRequest request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we can move this method after all the public methods
// Utility method to extract headers from HttpServletRequest | ||
private Map<String, String> getHeadersMap(HttpServletRequest request) | ||
{ | ||
Map<String, String> headers = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some headers can be sent by clients as several headers each with a different value. So, we should update this to Map<String, List<String>>
Enumeration<String> headerNames = request.getHeaderNames(); | ||
while (headerNames.hasMoreElements()) { | ||
String headerName = headerNames.nextElement(); | ||
String headerValue = request.getHeader(headerName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use request.getHeaders(headerName)
to get all the values.
return authenticatorManager.getAuthenticator().createAuthenticatedPrincipal(headers); | ||
} | ||
catch (AccessDeniedException e) { | ||
throw e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's wrap this in AuthenticationException
throw e; | |
throw new AuthenticationException(e.getMessage()); |
return (httpRequest) -> { | ||
// TEST_HEADER will have value of the form PART1:PART2 | ||
String[] header = httpRequest.get(TEST_HEADER).split(":"); | ||
|
||
if (header[0].equals(this.validHeaderValue)) { | ||
return new BasicPrincipal(header[1]); | ||
} | ||
|
||
throw new AccessDeniedException("Authentication Failed!"); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After making the headers map in SPI to Map<String, List<String>>
, we can refactor this.
return (httpRequest) -> { | |
// TEST_HEADER will have value of the form PART1:PART2 | |
String[] header = httpRequest.get(TEST_HEADER).split(":"); | |
if (header[0].equals(this.validHeaderValue)) { | |
return new BasicPrincipal(header[1]); | |
} | |
throw new AccessDeniedException("Authentication Failed!"); | |
}; | |
return (headers) -> { | |
// TEST_HEADER will have value of the form PART1:PART2 | |
String[] header = headers.get(TEST_HEADER).get(0).split(":"); | |
if (header[0].equals(this.validHeaderValue)) { | |
return new BasicPrincipal(header[1]); | |
} | |
throw new AccessDeniedException("Authentication Failed!"); | |
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please remove this class since this is unused
* @return the authenticated Principal | ||
* @throws AccessDeniedException if not allowed | ||
*/ | ||
Principal createAuthenticatedPrincipal(Map<String, String> headers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change the headers map to Map<String, List<String>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove this class as this is unused
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@imsayari404 thanks, mostly LGTM now, just a few minor comments.
presto-lark-sheets/pom.xml
Outdated
@@ -75,6 +75,7 @@ | |||
</dependency> | |||
|
|||
<!-- Presto SPI --> | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: empty line can be removed
Map<String, List<String>> headers = new HashMap<>(); | ||
Enumeration<String> headerNames = request.getHeaderNames(); | ||
while (headerNames.hasMoreElements()) { | ||
String headerName = headerNames.nextElement(); | ||
Enumeration<String> headerValues = request.getHeaders(headerName); | ||
List<String> valuesList = new ArrayList<>(); | ||
while (headerValues.hasMoreElements()) { | ||
valuesList.add(headerValues.nextElement()); | ||
} | ||
headers.put(headerName, valuesList); | ||
} | ||
return headers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can refactor this method and use Java Stream API
Map<String, List<String>> headers = new HashMap<>(); | |
Enumeration<String> headerNames = request.getHeaderNames(); | |
while (headerNames.hasMoreElements()) { | |
String headerName = headerNames.nextElement(); | |
Enumeration<String> headerValues = request.getHeaders(headerName); | |
List<String> valuesList = new ArrayList<>(); | |
while (headerValues.hasMoreElements()) { | |
valuesList.add(headerValues.nextElement()); | |
} | |
headers.put(headerName, valuesList); | |
} | |
return headers; | |
return Collections.list(request.getHeaderNames()) | |
.stream() | |
.collect(Collectors.toMap( | |
headerName -> headerName, | |
headerName -> Collections.list(request.getHeaders(headerName)) | |
)); |
|
||
private Map<String, List<String>> getHeadersMap(HttpServletRequest request) | ||
{ | ||
Map<String, List<String>> headers = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the previous comment, please refactor this as well.
19c5bca
to
2c9848c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@imsayari404 thanks, changes LGTM, can we squash the commits into relevant commits?
b1590a7
to
0220514
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! (docs)
Pulled updated branch, new local doc build, looks good. Thanks!
Hi @tdcmeehan , |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some nits, otherwise LGTM
ReadOnlyHttpServletRequest restrictedRequest = new ReadOnlyHttpServletRequest(request); | ||
return authenticatorManager.getAuthenticator().createAuthenticatedPrincipal(restrictedRequest); | ||
} | ||
catch (RuntimeException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just let it throw.
{ | ||
return Collections.list(request.getHeaderNames()) | ||
.stream() | ||
.collect(Collectors.toMap( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.collect(Collectors.toMap( | |
.collect(toImmutableMap( |
// Utility method to extract headers from HttpServletRequest | ||
private Map<String, List<String>> getHeadersMap(HttpServletRequest request) | ||
{ | ||
return Collections.list(request.getHeaderNames()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return Collections.list(request.getHeaderNames()) | |
return list(request.getHeaderNames()) |
.stream() | ||
.collect(Collectors.toMap( | ||
headerName -> headerName, | ||
headerName -> Collections.list(request.getHeaders(headerName)))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
headerName -> Collections.list(request.getHeaders(headerName)))); | |
headerName -> list(request.getHeaders(headerName)))); |
c873a49
0220514
to
c873a49
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor change, otherwise LGTM
public Principal authenticate(HttpServletRequest request) | ||
throws AuthenticationException | ||
{ | ||
// Extracting headers into a Map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need to catch AccessDeniedException here and throw it as AuthenticationException, we cannot remove that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the catch clause for RuntimeException but we need to catch AccessDeniedException.
c873a49
to
415ef55
Compare
Co-authored-by: Namya Sehgal <[email protected]>
Add TestingPrestoAuthenticatorFactory Co-authored-by: Sayari Mukherjee <[email protected]>
415ef55
to
f6fe09f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@imsayari404 Thanks for the PR, LGTM!
@tdcmeehan This should be good for another review. Thank you. |
Description
A pluggable authenticator in Presto allows the authentication process to be customized based on specific use cases, such as integrating with different identity providers or token validation strategies.
Motivation and Context
Fixes #24052
Test Plan
Added Unit Tests
Contributor checklist
Release Notes
Please follow release notes guidelines and fill in the release notes below.