Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Add access mode to the user principal of the authorized request wrapper - Start transactions with the access mode of this user principal acquired through the http request / context. - Add a pass-through authorization filter when auth is disabled that adds an authorized request wrapper with full access mode.
- Loading branch information
1 parent
2c59a9d
commit 921bd8c
Showing
9 changed files
with
391 additions
and
227 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationDisabledFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* Copyright (c) 2002-2016 "Neo Technology," | ||
* Network Engine for Objects in Lund AB [http://neotechnology.com] | ||
* | ||
* This file is part of Neo4j. | ||
* | ||
* Neo4j is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
package org.neo4j.server.rest.dbms; | ||
|
||
import java.io.IOException; | ||
import javax.servlet.FilterChain; | ||
import javax.servlet.ServletException; | ||
import javax.servlet.ServletRequest; | ||
import javax.servlet.ServletResponse; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
|
||
import org.neo4j.graphdb.security.AuthorizationViolationException; | ||
import org.neo4j.kernel.api.security.AccessMode; | ||
|
||
import static javax.servlet.http.HttpServletRequest.BASIC_AUTH; | ||
|
||
public class AuthorizationDisabledFilter extends AuthorizationFilter | ||
{ | ||
@Override | ||
public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException | ||
{ | ||
validateRequestType( servletRequest ); | ||
validateResponseType( servletResponse ); | ||
|
||
final HttpServletRequest request = (HttpServletRequest) servletRequest; | ||
final HttpServletResponse response = (HttpServletResponse) servletResponse; | ||
|
||
try | ||
{ | ||
filterChain.doFilter( new AuthorizedRequestWrapper( BASIC_AUTH, "neo4j", request, AccessMode.Static.FULL ), servletResponse ); | ||
} | ||
catch ( AuthorizationViolationException e ) | ||
{ | ||
unauthorizedAccess( e.getMessage() ).accept( response ); | ||
} | ||
} | ||
} |
229 changes: 229 additions & 0 deletions
229
community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationEnabledFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
/* | ||
* Copyright (c) 2002-2016 "Neo Technology," | ||
* Network Engine for Objects in Lund AB [http://neotechnology.com] | ||
* | ||
* This file is part of Neo4j. | ||
* | ||
* Neo4j is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
package org.neo4j.server.rest.dbms; | ||
|
||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.util.function.Supplier; | ||
import java.util.regex.Pattern; | ||
import javax.servlet.FilterChain; | ||
import javax.servlet.ServletException; | ||
import javax.servlet.ServletRequest; | ||
import javax.servlet.ServletResponse; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
import javax.ws.rs.core.HttpHeaders; | ||
import javax.ws.rs.core.UriBuilder; | ||
|
||
import org.neo4j.function.ThrowingConsumer; | ||
import org.neo4j.graphdb.security.AuthorizationViolationException; | ||
import org.neo4j.kernel.api.exceptions.Status; | ||
import org.neo4j.kernel.api.security.AuthManager; | ||
import org.neo4j.kernel.api.security.AuthSubject; | ||
import org.neo4j.logging.Log; | ||
import org.neo4j.logging.LogProvider; | ||
import org.neo4j.server.web.XForwardUtil; | ||
|
||
import static java.lang.String.format; | ||
import static java.util.Collections.singletonList; | ||
import static javax.servlet.http.HttpServletRequest.BASIC_AUTH; | ||
import static org.neo4j.helpers.collection.MapUtil.map; | ||
import static org.neo4j.server.web.XForwardUtil.X_FORWARD_HOST_HEADER_KEY; | ||
import static org.neo4j.server.web.XForwardUtil.X_FORWARD_PROTO_HEADER_KEY; | ||
|
||
public class AuthorizationEnabledFilter extends AuthorizationFilter | ||
{ | ||
private static final Pattern PASSWORD_CHANGE_WHITELIST = Pattern.compile( "/user/.*" ); | ||
|
||
private final Supplier<AuthManager> authManagerSupplier; | ||
private final Log log; | ||
private final Pattern[] uriWhitelist; | ||
|
||
public AuthorizationEnabledFilter( Supplier<AuthManager> authManager, LogProvider logProvider, Pattern... uriWhitelist ) | ||
{ | ||
this.authManagerSupplier = authManager; | ||
this.log = logProvider.getLog( getClass() ); | ||
this.uriWhitelist = uriWhitelist; | ||
} | ||
|
||
@Override | ||
public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain ) throws IOException, ServletException | ||
{ | ||
validateRequestType( servletRequest ); | ||
validateResponseType( servletResponse ); | ||
|
||
final HttpServletRequest request = (HttpServletRequest) servletRequest; | ||
final HttpServletResponse response = (HttpServletResponse) servletResponse; | ||
|
||
final String path = request.getContextPath() + ( request.getPathInfo() == null ? "" : request.getPathInfo() ); | ||
|
||
if ( request.getMethod().equals( "OPTIONS" ) || whitelisted( path ) ) | ||
{ | ||
// NOTE: If starting transactions with access mode on whitelisted uris should be possible we need to | ||
// wrap servletRequest in an AuthorizedRequestWarpper here | ||
filterChain.doFilter( servletRequest, servletResponse ); | ||
return; | ||
} | ||
|
||
final String header = request.getHeader( HttpHeaders.AUTHORIZATION ); | ||
if ( header == null ) | ||
{ | ||
requestAuthentication( request, noHeader ).accept( response ); | ||
return; | ||
} | ||
|
||
final String[] usernameAndPassword = extractCredential( header ); | ||
if ( usernameAndPassword == null ) | ||
{ | ||
badHeader.accept( response ); | ||
return; | ||
} | ||
|
||
final String username = usernameAndPassword[0]; | ||
final String password = usernameAndPassword[1]; | ||
|
||
AuthSubject authSubject = authenticate( username, password ); | ||
switch ( authSubject.getAuthenticationResult() ) | ||
{ | ||
case PASSWORD_CHANGE_REQUIRED: | ||
if ( !PASSWORD_CHANGE_WHITELIST.matcher( path ).matches() ) | ||
{ | ||
passwordChangeRequired( username, baseURL( request ) ).accept( response ); | ||
return; | ||
} | ||
// fall through | ||
case SUCCESS: | ||
try | ||
{ | ||
filterChain.doFilter( new AuthorizedRequestWrapper( BASIC_AUTH, username, request, authSubject ), servletResponse ); | ||
} | ||
catch ( AuthorizationViolationException e ) | ||
{ | ||
unauthorizedAccess( e.getMessage() ).accept( response ); | ||
} | ||
return; | ||
case TOO_MANY_ATTEMPTS: | ||
tooManyAttempts.accept( response ); | ||
return; | ||
default: | ||
log.warn( "Failed authentication attempt for '%s' from %s", username, request.getRemoteAddr() ); | ||
requestAuthentication( request, invalidCredential ).accept( response ); | ||
} | ||
} | ||
|
||
private AuthSubject authenticate( String username, String password ) | ||
{ | ||
AuthManager authManager = authManagerSupplier.get(); | ||
AuthSubject authSubject = authManager.login( username, password ); | ||
return authSubject; | ||
} | ||
|
||
private static final ThrowingConsumer<HttpServletResponse, IOException> noHeader = | ||
error( 401, | ||
map( "errors", singletonList( map( | ||
"code", Status.Security.Unauthorized.code().serialize(), | ||
"message", "No authentication header supplied." ) ) ) ); | ||
|
||
private static final ThrowingConsumer<HttpServletResponse, IOException> badHeader = | ||
error( 400, | ||
map( "errors", singletonList( map( | ||
"code", Status.Request.InvalidFormat.code().serialize(), | ||
"message", "Invalid authentication header." ) ) ) ); | ||
|
||
private static final ThrowingConsumer<HttpServletResponse, IOException> invalidCredential = | ||
error( 401, | ||
map( "errors", singletonList( map( | ||
"code", Status.Security.Unauthorized.code().serialize(), | ||
"message", "Invalid username or password." ) ) ) ); | ||
|
||
private static final ThrowingConsumer<HttpServletResponse, IOException> tooManyAttempts = | ||
error( 429, | ||
map( "errors", singletonList( map( | ||
"code", Status.Security.AuthenticationRateLimit.code().serialize(), | ||
"message", "Too many failed authentication requests. Please wait 5 seconds and try again." ) ) ) ); | ||
|
||
private static ThrowingConsumer<HttpServletResponse, IOException> passwordChangeRequired( final String username, final String baseURL ) | ||
{ | ||
URI path = UriBuilder.fromUri( baseURL ).path( format( "/user/%s/password", username ) ).build(); | ||
return error( 403, | ||
map( "errors", singletonList( map( | ||
"code", Status.Security.Forbidden.code().serialize(), | ||
"message", "User is required to change their password." ) ), "password_change", path.toString() ) ); | ||
} | ||
|
||
/** | ||
* In order to avoid browsers popping up an auth box when using the Neo4j Browser, it sends us a special header. | ||
* When we get that special header, we send a crippled authentication challenge back that the browser does not | ||
* understand, which lets the Neo4j Browser handle auth on its own. | ||
* | ||
* Otherwise, we send a regular basic auth challenge. This method adds the appropriate header depending on the | ||
* inbound request. | ||
*/ | ||
private static ThrowingConsumer<HttpServletResponse, IOException> requestAuthentication( | ||
HttpServletRequest req, ThrowingConsumer<HttpServletResponse, IOException> responseGen ) | ||
{ | ||
if( "true".equals( req.getHeader( "X-Ajax-Browser-Auth" ) ) ) | ||
{ | ||
return (res) -> { | ||
responseGen.accept( res ); | ||
res.addHeader( HttpHeaders.WWW_AUTHENTICATE, "None" ); | ||
}; | ||
} else { | ||
return (res) -> { | ||
responseGen.accept( res ); | ||
res.addHeader( HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Neo4j\"" ); | ||
}; | ||
} | ||
} | ||
|
||
private String baseURL( HttpServletRequest request ) | ||
{ | ||
StringBuffer url = request.getRequestURL(); | ||
String baseURL = url.substring( 0, url.length() - request.getRequestURI().length() ) + "/"; | ||
|
||
return XForwardUtil.externalUri( | ||
baseURL, | ||
request.getHeader( X_FORWARD_HOST_HEADER_KEY ), | ||
request.getHeader( X_FORWARD_PROTO_HEADER_KEY ) ); | ||
} | ||
|
||
private boolean whitelisted( String path ) | ||
{ | ||
for ( Pattern pattern : uriWhitelist ) | ||
{ | ||
if ( pattern.matcher( path ).matches() ) | ||
{ | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private String[] extractCredential( String header ) | ||
{ | ||
if ( header == null ) | ||
{ | ||
return null; | ||
} else | ||
{ | ||
return AuthorizationHeaders.decode( header ); | ||
} | ||
} | ||
} |
Oops, something went wrong.