diff --git a/community/server/src/main/java/org/neo4j/server/modules/AuthorizationModule.java b/community/server/src/main/java/org/neo4j/server/modules/AuthorizationModule.java index 5f501c7f2ab54..26f88a2779125 100644 --- a/community/server/src/main/java/org/neo4j/server/modules/AuthorizationModule.java +++ b/community/server/src/main/java/org/neo4j/server/modules/AuthorizationModule.java @@ -22,11 +22,14 @@ import java.util.function.Supplier; import java.util.regex.Pattern; +import javax.servlet.Filter; + import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.kernel.api.security.AuthManager; import org.neo4j.kernel.configuration.Config; import org.neo4j.logging.LogProvider; -import org.neo4j.server.rest.dbms.AuthorizationFilter; +import org.neo4j.server.rest.dbms.AuthorizationDisabledFilter; +import org.neo4j.server.rest.dbms.AuthorizationEnabledFilter; import org.neo4j.server.web.WebServer; public class AuthorizationModule implements ServerModule @@ -49,11 +52,18 @@ public AuthorizationModule( WebServer webServer, Supplier authManag @Override public void start() { + final Filter authorizationFilter; + if ( config.get( GraphDatabaseSettings.auth_enabled ) ) { - final AuthorizationFilter authorizationFilter = new AuthorizationFilter( authManagerSupplier, logProvider, uriWhitelist ); - webServer.addFilter( authorizationFilter, "/*" ); + authorizationFilter = new AuthorizationEnabledFilter( authManagerSupplier, logProvider, uriWhitelist ); } + else + { + authorizationFilter = new AuthorizationDisabledFilter(); + } + + webServer.addFilter( authorizationFilter, "/*" ); } @Override diff --git a/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationDisabledFilter.java b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationDisabledFilter.java new file mode 100644 index 0000000000000..12ec83dbd4446 --- /dev/null +++ b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationDisabledFilter.java @@ -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 . + */ +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 ); + } + } +} diff --git a/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationEnabledFilter.java b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationEnabledFilter.java new file mode 100644 index 0000000000000..a7833e519a9ff --- /dev/null +++ b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationEnabledFilter.java @@ -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 . + */ +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 authManagerSupplier; + private final Log log; + private final Pattern[] uriWhitelist; + + public AuthorizationEnabledFilter( Supplier 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 noHeader = + error( 401, + map( "errors", singletonList( map( + "code", Status.Security.Unauthorized.code().serialize(), + "message", "No authentication header supplied." ) ) ) ); + + private static final ThrowingConsumer badHeader = + error( 400, + map( "errors", singletonList( map( + "code", Status.Request.InvalidFormat.code().serialize(), + "message", "Invalid authentication header." ) ) ) ); + + private static final ThrowingConsumer invalidCredential = + error( 401, + map( "errors", singletonList( map( + "code", Status.Security.Unauthorized.code().serialize(), + "message", "Invalid username or password." ) ) ) ); + + private static final ThrowingConsumer 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 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 requestAuthentication( + HttpServletRequest req, ThrowingConsumer 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 ); + } + } +} diff --git a/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationFilter.java b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationFilter.java index 0ca01ab028884..74765be3f0b23 100644 --- a/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationFilter.java +++ b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizationFilter.java @@ -20,12 +20,8 @@ package org.neo4j.server.rest.dbms; import java.io.IOException; -import java.net.URI; import java.nio.charset.StandardCharsets; -import java.util.function.Supplier; -import java.util.regex.Pattern; import javax.servlet.Filter; -import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; @@ -33,108 +29,18 @@ 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.security.AuthManager; import org.neo4j.kernel.api.exceptions.Status; -import org.neo4j.logging.Log; -import org.neo4j.logging.LogProvider; import org.neo4j.server.rest.domain.JsonHelper; -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 AuthorizationFilter implements Filter +public abstract class AuthorizationFilter implements Filter { - private static final Pattern PASSWORD_CHANGE_WHITELIST = Pattern.compile( "/user/.*" ); - - private final Supplier authManagerSupplier; - private final Log log; - private final Pattern[] uriWhitelist; - - public AuthorizationFilter( Supplier authManager, LogProvider logProvider, Pattern... uriWhitelist ) - { - this.authManagerSupplier = authManager; - this.log = logProvider.getLog( getClass() ); - this.uriWhitelist = uriWhitelist; - } - - @Override - public void init( FilterConfig filterConfig ) throws ServletException - { - } - - @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 ) ) - { - 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]; - - AuthManager authManager = authManagerSupplier.get(); - switch ( authManager.authenticate( username, password ) ) - { - 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 ), 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 static ThrowingConsumer error( int code, Object body ) + protected static ThrowingConsumer error( int code, Object body ) { return (response) -> { @@ -144,31 +50,7 @@ private static ThrowingConsumer error( int cod }; } - private static final ThrowingConsumer noHeader = - error( 401, - map( "errors", singletonList( map( - "code", Status.Security.Unauthorized.code().serialize(), - "message", "No authentication header supplied." ) ) ) ); - - private static final ThrowingConsumer badHeader = - error( 400, - map( "errors", singletonList( map( - "code", Status.Request.InvalidFormat.code().serialize(), - "message", "Invalid authentication header." ) ) ) ); - - private static final ThrowingConsumer invalidCredential = - error( 401, - map( "errors", singletonList( map( - "code", Status.Security.Unauthorized.code().serialize(), - "message", "Invalid username or password." ) ) ) ); - - private static final ThrowingConsumer 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 unauthorizedAccess( final String message ) + protected static ThrowingConsumer unauthorizedAccess( final String message ) { return error( 403, map( "errors", singletonList( map( @@ -176,49 +58,9 @@ private static ThrowingConsumer unauthorizedAc "message", String.format("Unauthorized access violation: %s.", message ) ) ) ) ); } - private static ThrowingConsumer 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 requestAuthentication( - HttpServletRequest req, ThrowingConsumer 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 ) + @Override + public void init( FilterConfig filterConfig ) throws ServletException { - 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 ) ); } @Override @@ -226,39 +68,16 @@ public void destroy() { } - 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 ); - } - } - - private void validateRequestType( ServletRequest request ) throws ServletException + protected void validateRequestType( ServletRequest request ) throws ServletException { - if ( !( request instanceof HttpServletRequest ) ) + if ( !( request instanceof HttpServletRequest) ) { throw new ServletException( format( "Expected HttpServletRequest, received [%s]", request.getClass() .getCanonicalName() ) ); } } - private void validateResponseType( ServletResponse response ) throws ServletException + protected void validateResponseType( ServletResponse response ) throws ServletException { if ( !( response instanceof HttpServletResponse ) ) { diff --git a/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizedRequestWrapper.java b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizedRequestWrapper.java index 7048b6dd225cf..95a4d6ef108c8 100644 --- a/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizedRequestWrapper.java +++ b/community/server/src/main/java/org/neo4j/server/rest/dbms/AuthorizedRequestWrapper.java @@ -19,19 +19,50 @@ */ package org.neo4j.server.rest.dbms; +import com.sun.jersey.api.core.HttpContext; +import com.sun.jersey.api.core.HttpRequestContext; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.security.Principal; +import org.neo4j.kernel.api.security.AccessMode; + public class AuthorizedRequestWrapper extends HttpServletRequestWrapper { - private class DelegatingPrinciple implements Principal + public static final AccessMode getAccessModeFromHttpServletRequest( HttpServletRequest request ) + { + Principal principal = request.getUserPrincipal(); + return getAccessModeFromUserPrincipal( principal ); + } + + public static final AccessMode getAccessModeFromHttpContext( HttpContext httpContext ) + { + HttpRequestContext requestContext = httpContext.getRequest(); + Principal principal = requestContext.getUserPrincipal(); + return getAccessModeFromUserPrincipal( principal ); + } + + private static final AccessMode getAccessModeFromUserPrincipal( Principal principal ) + { + if ( principal instanceof DelegatingPrinciple ) + { + return ((DelegatingPrinciple) principal).getAccessMode(); + } + // If whitelisted uris can start transactions we cannot throw exception here + //throw new IllegalArgumentException( "Tried to get access mode on illegal user principal" ); + return AccessMode.Static.NONE; + } + + public class DelegatingPrinciple implements Principal { private String username; + private final AccessMode accessMode; - private DelegatingPrinciple( String username ) + private DelegatingPrinciple( String username, AccessMode accessMode ) { this.username = username; + this.accessMode = accessMode; } @Override @@ -40,6 +71,8 @@ public String getName() return username; } + public AccessMode getAccessMode() { return accessMode; } + @Override public boolean equals( Object o ) { @@ -74,11 +107,12 @@ public String toString() private final String authType; private final DelegatingPrinciple principle; - public AuthorizedRequestWrapper( final String authType, final String username, final HttpServletRequest request ) + public AuthorizedRequestWrapper( final String authType, final String username, final HttpServletRequest request, + AccessMode accessMode ) { super( request ); this.authType = authType; - this.principle = new DelegatingPrinciple( username ); + this.principle = new DelegatingPrinciple( username, accessMode ); } @Override diff --git a/community/server/src/main/java/org/neo4j/server/rest/transactional/TransactionalRequestDispatcher.java b/community/server/src/main/java/org/neo4j/server/rest/transactional/TransactionalRequestDispatcher.java index 2cf4a5757376f..5c68b9dbcca9c 100644 --- a/community/server/src/main/java/org/neo4j/server/rest/transactional/TransactionalRequestDispatcher.java +++ b/community/server/src/main/java/org/neo4j/server/rest/transactional/TransactionalRequestDispatcher.java @@ -26,6 +26,7 @@ import org.neo4j.kernel.api.security.AccessMode; import org.neo4j.kernel.api.KernelTransaction; import org.neo4j.server.database.Database; +import org.neo4j.server.rest.dbms.AuthorizedRequestWrapper; import org.neo4j.server.rest.repr.RepresentationWriteHandler; import org.neo4j.server.rest.web.BatchOperationService; import org.neo4j.server.rest.web.CypherService; @@ -51,7 +52,7 @@ public void dispatch( Object o, final HttpContext httpContext ) { RepresentationWriteHandler representationWriteHandler = DO_NOTHING; - AccessMode mode = AccessMode.Static.FULL; + AccessMode mode = AuthorizedRequestWrapper.getAccessModeFromHttpContext( httpContext ); if ( o instanceof RestfulGraphDatabase ) { diff --git a/community/server/src/main/java/org/neo4j/server/rest/web/TransactionalService.java b/community/server/src/main/java/org/neo4j/server/rest/web/TransactionalService.java index 1a12a3312f23b..87b5976020469 100644 --- a/community/server/src/main/java/org/neo4j/server/rest/web/TransactionalService.java +++ b/community/server/src/main/java/org/neo4j/server/rest/web/TransactionalService.java @@ -39,6 +39,7 @@ import javax.ws.rs.core.UriInfo; import org.neo4j.kernel.api.security.AccessMode; +import org.neo4j.server.rest.dbms.AuthorizedRequestWrapper; import org.neo4j.server.rest.transactional.ExecutionResultSerializer; import org.neo4j.server.rest.transactional.TransactionFacade; import org.neo4j.server.rest.transactional.TransactionHandle; @@ -77,7 +78,8 @@ public Response executeStatementsInNewTransaction( final InputStream input, @Con try { usage.get( features ).flag( http_tx_endpoint ); - TransactionHandle transactionHandle = facade.newTransactionHandle( uriScheme, false, AccessMode.Static.FULL ); + AccessMode accessMode = AuthorizedRequestWrapper.getAccessModeFromHttpServletRequest( request ); + TransactionHandle transactionHandle = facade.newTransactionHandle( uriScheme, false, accessMode ); return createdResponse( transactionHandle, executeStatements( input, transactionHandle, uriInfo.getBaseUri(), request ) ); } catch ( TransactionLifecycleException e ) @@ -134,7 +136,8 @@ public Response commitNewTransaction( final InputStream input, @Context final Ur final TransactionHandle transactionHandle; try { - transactionHandle = facade.newTransactionHandle( uriScheme, true, AccessMode.Static.FULL ); + AccessMode accessMode = AuthorizedRequestWrapper.getAccessModeFromHttpServletRequest( request ); + transactionHandle = facade.newTransactionHandle( uriScheme, true, accessMode ); } catch ( TransactionLifecycleException e ) { diff --git a/community/server/src/test/java/org/neo4j/server/rest/dbms/AuthorizationFilterTest.java b/community/server/src/test/java/org/neo4j/server/rest/dbms/AuthorizationFilterTest.java index c030335bcf524..3cc4213ed2eea 100644 --- a/community/server/src/test/java/org/neo4j/server/rest/dbms/AuthorizationFilterTest.java +++ b/community/server/src/test/java/org/neo4j/server/rest/dbms/AuthorizationFilterTest.java @@ -34,9 +34,11 @@ import org.junit.Before; import org.junit.Test; +import org.neo4j.kernel.api.security.AccessMode; +import org.neo4j.kernel.api.security.AuthSubject; +import org.neo4j.kernel.api.security.AuthenticationResult; import org.neo4j.logging.AssertableLogProvider; import org.neo4j.server.security.auth.BasicAuthManager; -import org.neo4j.kernel.api.security.AuthenticationResult; import static javax.servlet.http.HttpServletRequest.BASIC_AUTH; @@ -90,7 +92,7 @@ public void setWriteListener( WriteListener writeListener ) public void shouldAllowOptionsRequests() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider ); + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider ); when( servletRequest.getMethod() ).thenReturn( "OPTIONS" ); // When @@ -104,7 +106,7 @@ public void shouldAllowOptionsRequests() throws Exception public void shouldWhitelistMatchingUris() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider, + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider, Pattern.compile( "/" ), Pattern.compile( "/browser.*" ) ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/", "/browser/index.html" ); @@ -121,7 +123,8 @@ public void shouldWhitelistMatchingUris() throws Exception public void shouldRequireAuthorizationForNonWhitelistedUris() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider, Pattern.compile( "/" ), Pattern.compile( "/browser.*" ) ); + final AuthorizationEnabledFilter + filter = new AuthorizationEnabledFilter( () -> authManager, logProvider, Pattern.compile( "/" ), Pattern.compile( "/browser.*" ) ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/db/data" ); @@ -141,7 +144,7 @@ public void shouldRequireAuthorizationForNonWhitelistedUris() throws Exception public void shouldRequireValidAuthorizationHeader() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider ); + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/db/data" ); when( servletRequest.getHeader( HttpHeaders.AUTHORIZATION ) ).thenReturn( "NOT A VALID VALUE" ); @@ -161,13 +164,15 @@ public void shouldRequireValidAuthorizationHeader() throws Exception public void shouldNotAuthorizeInvalidCredentials() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider ); + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider ); String credentials = Base64.encodeBase64String( "foo:bar".getBytes( StandardCharsets.UTF_8 ) ); + AuthSubject authSubject = mock( AuthSubject.class ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/db/data" ); when( servletRequest.getHeader( HttpHeaders.AUTHORIZATION ) ).thenReturn( "BASIC " + credentials ); when( servletRequest.getRemoteAddr() ).thenReturn( "remote_ip_address" ); - when( authManager.authenticate( "foo", "bar" ) ).thenReturn( AuthenticationResult.FAILURE ); + when( authManager.login( "foo", "bar" ) ).thenReturn( authSubject ); + when( authSubject.getAuthenticationResult() ).thenReturn( AuthenticationResult.FAILURE ); // When filter.doFilter( servletRequest, servletResponse, filterChain ); @@ -175,7 +180,7 @@ public void shouldNotAuthorizeInvalidCredentials() throws Exception // Then verifyNoMoreInteractions( filterChain ); logProvider.assertExactly( - inLog( AuthorizationFilter.class ).warn( "Failed authentication attempt for '%s' from %s", "foo", "remote_ip_address" ) + inLog( AuthorizationEnabledFilter.class ).warn( "Failed authentication attempt for '%s' from %s", "foo", "remote_ip_address" ) ); verify( servletResponse ).setStatus( 401 ); verify( servletResponse ).addHeader( HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8" ); @@ -187,32 +192,37 @@ public void shouldNotAuthorizeInvalidCredentials() throws Exception public void shouldAuthorizeWhenPasswordChangeRequiredForWhitelistedPath() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider ); + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider ); String credentials = Base64.encodeBase64String( "foo:bar".getBytes( StandardCharsets.UTF_8 ) ); + AuthSubject authSubject = mock( AuthSubject.class ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/user/foo" ); when( servletRequest.getHeader( HttpHeaders.AUTHORIZATION ) ).thenReturn( "BASIC " + credentials ); - when( authManager.authenticate( "foo", "bar" ) ).thenReturn( AuthenticationResult.PASSWORD_CHANGE_REQUIRED ); + when( authManager.login( "foo", "bar" ) ).thenReturn( authSubject ); + when( authSubject.getAuthenticationResult() ).thenReturn( AuthenticationResult.PASSWORD_CHANGE_REQUIRED ); // When filter.doFilter( servletRequest, servletResponse, filterChain ); // Then - verify( filterChain ).doFilter( eq( new AuthorizedRequestWrapper( BASIC_AUTH, "foo", servletRequest ) ), same( servletResponse ) ); + verify( filterChain ).doFilter( eq( new AuthorizedRequestWrapper( BASIC_AUTH, "foo", servletRequest, + AccessMode.Static.FULL ) ), same( servletResponse ) ); } @Test public void shouldNotAuthorizeWhenPasswordChangeRequired() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider ); + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider ); String credentials = Base64.encodeBase64String( "foo:bar".getBytes( StandardCharsets.UTF_8 ) ); + AuthSubject authSubject = mock( AuthSubject.class ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/db/data" ); when( servletRequest.getRequestURL() ).thenReturn( new StringBuffer( "http://bar.baz:7474/db/data/" ) ); when( servletRequest.getRequestURI() ).thenReturn( "/db/data/" ); when( servletRequest.getHeader( HttpHeaders.AUTHORIZATION ) ).thenReturn( "BASIC " + credentials ); - when( authManager.authenticate( "foo", "bar" ) ).thenReturn( AuthenticationResult.PASSWORD_CHANGE_REQUIRED ); + when( authManager.login( "foo", "bar" ) ).thenReturn( authSubject ); + when( authSubject.getAuthenticationResult() ).thenReturn( AuthenticationResult.PASSWORD_CHANGE_REQUIRED ); // When filter.doFilter( servletRequest, servletResponse, filterChain ); @@ -230,12 +240,14 @@ public void shouldNotAuthorizeWhenPasswordChangeRequired() throws Exception public void shouldNotAuthorizeWhenTooManyAttemptsMade() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider ); + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider ); String credentials = Base64.encodeBase64String( "foo:bar".getBytes( StandardCharsets.UTF_8 ) ); + AuthSubject authSubject = mock( AuthSubject.class ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/db/data" ); when( servletRequest.getHeader( HttpHeaders.AUTHORIZATION ) ).thenReturn( "BASIC " + credentials ); - when( authManager.authenticate( "foo", "bar" ) ).thenReturn( AuthenticationResult.TOO_MANY_ATTEMPTS ); + when( authManager.login( "foo", "bar" ) ).thenReturn( authSubject ); + when( authSubject.getAuthenticationResult() ).thenReturn( AuthenticationResult.TOO_MANY_ATTEMPTS ); // When filter.doFilter( servletRequest, servletResponse, filterChain ); @@ -252,25 +264,29 @@ public void shouldNotAuthorizeWhenTooManyAttemptsMade() throws Exception public void shouldAuthorizeWhenValidCredentialsSupplied() throws Exception { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider ); + final AuthorizationEnabledFilter filter = new AuthorizationEnabledFilter( () -> authManager, logProvider ); String credentials = Base64.encodeBase64String( "foo:bar".getBytes( StandardCharsets.UTF_8 ) ); + AuthSubject authSubject = mock( AuthSubject.class ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/db/data" ); when( servletRequest.getHeader( HttpHeaders.AUTHORIZATION ) ).thenReturn( "BASIC " + credentials ); - when( authManager.authenticate( "foo", "bar" ) ).thenReturn( AuthenticationResult.SUCCESS ); + when( authManager.login( "foo", "bar" ) ).thenReturn( authSubject ); + when( authSubject.getAuthenticationResult() ).thenReturn( AuthenticationResult.SUCCESS ); // When filter.doFilter( servletRequest, servletResponse, filterChain ); // Then - verify( filterChain ).doFilter( eq( new AuthorizedRequestWrapper( BASIC_AUTH, "foo", servletRequest ) ), same( servletResponse ) ); + verify( filterChain ).doFilter( eq( new AuthorizedRequestWrapper( BASIC_AUTH, "foo", servletRequest, + AccessMode.Static.FULL ) ), same( servletResponse ) ); } @Test public void shouldIncludeCrippledAuthHeaderIfBrowserIsTheOneCalling() throws Throwable { // Given - final AuthorizationFilter filter = new AuthorizationFilter( () -> authManager, logProvider, Pattern.compile( "/" ), Pattern.compile( "/browser.*" ) ); + final AuthorizationEnabledFilter + filter = new AuthorizationEnabledFilter( () -> authManager, logProvider, Pattern.compile( "/" ), Pattern.compile( "/browser.*" ) ); when( servletRequest.getMethod() ).thenReturn( "GET" ); when( servletRequest.getContextPath() ).thenReturn( "/db/data" ); when( servletRequest.getHeader( "X-Ajax-Browser-Auth" )).thenReturn( "true" ); diff --git a/community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationDocIT.java b/community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationDocIT.java index 7c2e699a720cc..99aef1ee2d5f9 100644 --- a/community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationDocIT.java +++ b/community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationDocIT.java @@ -23,7 +23,6 @@ import org.codehaus.jackson.JsonNode; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -248,28 +247,26 @@ public void shouldReplyNicelyToTooManyFailedAuthAttempts() throws Exception assertThat( firstError.get( "message" ).asText(), equalTo( "Too many failed authentication requests. Please wait 5 seconds and try again." ) ); } - // TODO: Enable this test when we have authorization in place @Test - @Ignore public void shouldNotAllowDataAccessForUnauthorizedUser() throws Exception { // Given - startServerWithConfiguredUser(); // TODO: The user for this test should not have read access + startServer( true ); // The user should not have read access before changing the password // When HTTP.Response response = - HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) ).POST( + HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ).POST( server.baseUri().resolve( "authentication" ).toString(), - HTTP.RawPayload.quotedJson( "{'username':'neo4j', 'password':'secret'}" ) + HTTP.RawPayload.quotedJson( "{'username':'neo4j', 'password':'neo4j'}" ) ); // When & then - assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) ) + assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ) .POST( server.baseUri().resolve( "db/data/node" ).toString(), RawPayload.quotedJson( "{'name':'jake'}" ) ).status() ); - assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) ) + assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ) .GET( server.baseUri().resolve( "db/data/node/1234" ).toString() ).status() ); - assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) ) + assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ) .POST( server.baseUri().resolve( "db/data/transaction/commit" ).toString(), RawPayload.quotedJson( "{'statements':[{'statement':'MATCH (n) RETURN n'}]}" ) ).status() ); }