Skip to content
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

Gh-2991: Improve User Authorisation in GafferPop #3202

Merged
merged 8 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions library/tinkerpop/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
<artifactId>gremlin-core</artifactId>
<version>${tinkerpop.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tinkerpop</groupId>
<artifactId>gremlin-server</artifactId>
<version>${tinkerpop.version}</version>
</dependency>
GCHQDeveloper314 marked this conversation as resolved.
Show resolved Hide resolved

<!-- Runtime dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -702,9 +702,6 @@ public <T> T execute(final OperationChain<T> opChain) {
} catch (final Exception e) {
LOGGER.error("Operation chain failed: {}", e.getMessage());
throw new RuntimeException("GafferPop operation failed: " + e.getMessage(), e);
} finally {
// Reset the variables to default after running operation as they may have been updated in the query
setDefaultVariables(variables);
}
}

Expand Down Expand Up @@ -941,7 +938,7 @@ private IncludeIncomingOutgoingType getInOutType(final Direction direction) {
*
* @param variables The variables
*/
private void setDefaultVariables(final GafferPopGraphVariables variables) {
public void setDefaultVariables(final GafferPopGraphVariables variables) {
variables.set(GafferPopGraphVariables.OP_OPTIONS, Collections.unmodifiableMap(opOptions));
variables.set(GafferPopGraphVariables.USER_ID, defaultUser.getUserId());
variables.set(GafferPopGraphVariables.DATA_AUTHS, configuration().getStringArray(DATA_AUTHS));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.slf4j.LoggerFactory;

import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraph;
import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraphVariables;

import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -65,9 +66,13 @@ public GafferPopGraphStep(final GraphStep<S, E> originalGraphStep) {
// Save reference to the graph
GafferPopGraph graph = (GafferPopGraph) originalGraphStep.getTraversal().getGraph().get();

// Restore variables to defaults before parsing options
graph.setDefaultVariables((GafferPopGraphVariables) graph.variables());

// Find any options on the traversal
Optional<OptionsStrategy> optionsStrategy = originalGraphStep.getTraversal().getStrategies().getStrategy(OptionsStrategy.class);
if (optionsStrategy.isPresent()) {
LOGGER.debug("Found options on requested traversal");
optionsStrategy.get().getOptions().forEach((k, v) -> {
if (graph.variables().asMap().containsKey(k)) {
graph.variables().set(k, v);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2024 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.gchq.gaffer.tinkerpop.server.auth;

import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException;
import org.apache.tinkerpop.gremlin.server.auth.Authenticator;

import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
* An example authenticator class for GafferPop, this should not
* be used in production as it allows all user and password combinations.
* The class is intended as a template for an deployment specific class
* that hooks into a proper authorisation mechanism such as LDAP etc.
*/
public class ExampleGafferPopAuthenticator implements Authenticator {

@Override
public boolean requireAuthentication() {
return true;
}

@Override
public void setup(final Map<String, Object> config) {
// Nothing to do
}

Check warning on line 45 in library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java

View check run for this annotation

Codecov / codecov/patch

library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java#L45

Added line #L45 was not covered by tests

@Override
public SaslNegotiator newSaslNegotiator(final InetAddress remoteAddress) {
return new PlainTextSaslAuthenticator();
}


@Override
public AuthenticatedUser authenticate(final Map<String, String> credentials) throws AuthenticationException {
// Get the username from the credentials set by the SASL negotiator
final String username = credentials.get("username");
return new AuthenticatedUser(username);
}

/**
* Very simple SASL authenticator that will just extract username and password
* from plain text.
*/
private class PlainTextSaslAuthenticator implements Authenticator.SaslNegotiator {
private static final byte NUL = 0;
private boolean complete = false;
private String username;
private String password;

@Override
public byte[] evaluateResponse(final byte[] clientResponse) throws AuthenticationException {
decodeCredentials(clientResponse);
complete = true;
return new byte[0];
}

@Override
public boolean isComplete() {
return complete;
}

@Override
public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException {
if (!complete) {
throw new AuthenticationException("SASL negotiation not complete");

Check warning on line 85 in library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java

View check run for this annotation

Codecov / codecov/patch

library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java#L85

Added line #L85 was not covered by tests
}
final Map<String, String> credentials = new HashMap<>();
credentials.put("username", username);
credentials.put("password", password);
rj77259 marked this conversation as resolved.
Show resolved Hide resolved
return authenticate(credentials);
}

/**
* SASL PLAIN mechanism specifies that credentials are encoded in a
* sequence of UTF-8 bytes, delimited by 0 (US-ASCII NUL).
* The form is :
*
* <pre>
* authzIdNULauthnIdNULpasswordNUL
* </pre>
*
* @param bytes encoded credentials string sent by the client
* @throws AuthenticationException If issue decoding
*/
private void decodeCredentials(final byte[] bytes) throws AuthenticationException {
byte[] user = null;
byte[] pass = null;
int end = bytes.length;
// Loop over the byte array to extract the user and password
for (int i = bytes.length - 1; i >= 0; i--) {
if (bytes[i] != NUL) {
continue;
}

if (pass == null) {
pass = Arrays.copyOfRange(bytes, i + 1, end);
} else if (user == null) {
user = Arrays.copyOfRange(bytes, i + 1, end);
}
end = i;
}

if (user == null) {
throw new AuthenticationException("Authentication ID must not be null");

Check warning on line 124 in library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java

View check run for this annotation

Codecov / codecov/patch

library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java#L124

Added line #L124 was not covered by tests
}
if (pass == null) {
throw new AuthenticationException("Password must not be null");

Check warning on line 127 in library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java

View check run for this annotation

Codecov / codecov/patch

library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/ExampleGafferPopAuthenticator.java#L127

Added line #L127 was not covered by tests
}

username = new String(user, StandardCharsets.UTF_8);
password = new String(pass, StandardCharsets.UTF_8);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright 2024 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.gchq.gaffer.tinkerpop.server.auth;

import org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.verification.VertexProgramRestrictionStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode.Instruction;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReadOnlyStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.util.BytecodeHelper;
import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
import org.apache.tinkerpop.gremlin.server.authz.AuthorizationException;
import org.apache.tinkerpop.gremlin.server.authz.Authorizer;
import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraphVariables;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
* The {@link Authorizer} for GafferPop, responsible for checking a query from a
* gremlin server user is valid and ensuring the current authorised user ID is
* passed on to the GafferPop graph. This should be used along side a instance
* specific Authenticator to provide user management for gremlin server connections.
*/
public class GafferPopAuthoriser implements Authorizer {
private static final Logger LOGGER = LoggerFactory.getLogger(GafferPopAuthoriser.class);
public static final String REJECT_BYTECODE = "User not authorized for bytecode requests on %s";
public static final String REJECT_LAMBDA = "lambdas";
public static final String REJECT_MUTATE = "the ReadOnlyStrategy";
public static final String REJECT_OLAP = "the VertexProgramRestrictionStrategy";
public static final String REJECT_STRING = "User not authorized for string-based requests.";

/**
* This method is called once upon system startup to initialize the
* {@code GafferAuthoriser}.
*/
@Override
public void setup(final Map<String, Object> config) {
// Nothing to setup
}

/**
* Checks whether a user is authorized to have a gremlin bytecode request from a client answered and raises an
* {@link AuthorizationException} if this is not the case. If authorised will modify the bytecode to inject the
* users details via a 'with()' step so the Gaffer graph can run with the correct user.
*
* @param user {@link AuthenticatedUser} that needs authorization.
* @param bytecode The gremlin {@link Bytecode} request to authorize the user for.
* @param aliases A {@link Map} with a single key/value pair that maps the name of the TraversalSource in the
* {@link Bytecode} request to name of one configured in Gremlin Server.
* @return The original or modified {@link Bytecode} to be used for further processing.
*/
@Override
public Bytecode authorize(final AuthenticatedUser user, final Bytecode bytecode, final Map<String, String> aliases) throws AuthorizationException {
final boolean runsLambda = BytecodeHelper.getLambdaLanguage(bytecode).isPresent();
final boolean touchesReadOnlyStrategy = bytecode.toString().contains(ReadOnlyStrategy.class.getSimpleName());
final boolean touchesOLAPRestriction = bytecode.toString().contains(VertexProgramRestrictionStrategy.class.getSimpleName());

final List<String> rejections = new ArrayList<>();
// Reject use of Lambdas
if (runsLambda) {
rejections.add(REJECT_LAMBDA);
}
// Reject any modification steps to the graph via gremlin
if (touchesReadOnlyStrategy) {
rejections.add(REJECT_MUTATE);
}
// Reject use of OLAP operations
if (touchesOLAPRestriction) {
rejections.add(REJECT_OLAP);
}

// Formulate a rejection message
String rejectMessage = REJECT_BYTECODE;
if (!rejections.isEmpty()) {
rejectMessage += " using " + String.join(", ", rejections);
throw new AuthorizationException(String.format(rejectMessage, aliases.values()));
}

// Prevent overriding the user ID in a 'with()' block as we will set it based on the authenticated user
for (final Instruction i : bytecode.getStepInstructions()) {
LOGGER.debug("Found query operator: {} with args: {}", i.getOperator(), i.getArguments());
if (i.getOperator().equals("with") && Arrays.asList(i.getArguments()).contains(GafferPopGraphVariables.USER_ID)) {
throw new AuthorizationException("Can't override current user ID from within a query");
}
}

// Add the user ID to the query
bytecode.addSource("with", GafferPopGraphVariables.USER_ID, user.getName());

return bytecode;
}

/**
* Checks whether a user is authorized to have a script request from a gremlin
* client answered and raises an {@link AuthorizationException} if this is not
* the case.
*
* @param user {@link AuthenticatedUser} that needs authorization.
* @param msg {@link RequestMessage} in which the ARGS_GREMLIN
* argument can contain an arbitrary succession of script
* statements.
*/
@Override
public void authorize(final AuthenticatedUser user, final RequestMessage msg) throws AuthorizationException {
// Not supported in GafferPop
throw new AuthorizationException(REJECT_STRING);

Check warning on line 126 in library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/GafferPopAuthoriser.java

View check run for this annotation

Codecov / codecov/patch

library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/server/auth/GafferPopAuthoriser.java#L126

Added line #L126 was not covered by tests
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraphVariables;
import uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTestUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

Expand All @@ -45,9 +44,7 @@ class GafferPopGraphStepStrategyTest {
@Test
void shouldUpdateGraphVariablesOnGremlinWithStep() {
// Given
final GafferPopGraph graph = Mockito.spy(GafferPopGraph.open(GafferPopTestUtil.TEST_CONFIGURATION_1, getGafferGraph()));
// Need to stub the actual execution as it will reset the variables back to defaults after execution
Mockito.doReturn(new ArrayList<>()).when(graph).execute(Mockito.any());
final GafferPopGraph graph = GafferPopGraph.open(GafferPopTestUtil.TEST_CONFIGURATION_1, getGafferGraph());
final GafferPopGraphVariables graphVariables = (GafferPopGraphVariables) graph.variables();
final String testUserId = "testUserId";
final String testDataAuths = "auth1,auth2";
Expand Down