blob: 7d4c604b12b6b7c747c517736c85ee9bb2fff011 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.tinkerpop.gremlin.server.authz;
import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
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.TraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy;
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.Settings.AuthorizationSettings;
import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
import java.util.*;
/**
* Authorizes a user per request, based on a list that grants access to {@link TraversalSource} instances for
* bytecode requests and to gremlin server's sandbox for string requests and lambdas. The {@link
* AuthorizationSettings}.config must have an authorizationAllowList entry that contains the name of a YAML file.
* This authorizer is for demonstration purposes only. It does not scale well in the number of users regarding
* memory usage and administrative burden.
*
* @author Marc de Lignie
*/
public class AllowListAuthorizer implements Authorizer {
public static final String SANDBOX = "sandbox";
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_SUBGRAPH = "the SubgraphStrategy";
public static final String REJECT_STRING = "User not authorized for string-based requests.";
public static final String KEY_AUTHORIZATION_ALLOWLIST = "authorizationAllowList";
// Collections derived from the list with allowed users for fast lookups
private final Map<String, List<String>> usernamesByTraversalSource = new HashMap<>();
private final Set<String> usernamesSandbox = new HashSet<>();
/**
* This method is called once upon system startup to initialize the {@code AllowListAuthorizer}.
*/
@Override
public void setup(final Map<String,Object> config) {
AllowList allowList;
final String file = (String) config.get(KEY_AUTHORIZATION_ALLOWLIST);
try {
allowList = AllowList.read(file);
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Failed to read list with allowed users from %s", file));
}
for (Map.Entry<String, List<String>> entry : allowList.grants.entrySet()) {
if (!entry.getKey().equals(SANDBOX)) {
usernamesByTraversalSource.put(entry.getKey(), new ArrayList<>());
}
for (final String group : entry.getValue()) {
if (allowList.groups.get(group) == null) {
throw new RuntimeException(String.format("Group '%s' not defined in file with allowed users.", group));
}
if (entry.getKey().equals(SANDBOX)) {
usernamesSandbox.addAll(allowList.groups.get(group));
} else {
usernamesByTraversalSource.get(entry.getKey()).addAll(allowList.groups.get(group));
}
}
}
}
/**
* 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. For a request to be authorized, the user must either
* have a grant for the requested {@link TraversalSource}, without using lambdas, mutating steps or OLAP, or have a
* sandbox grant.
*
* @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 {@link 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 Set<String> usernames = new HashSet<>();
for (final String resource: aliases.values()) {
usernames.addAll(usernamesByTraversalSource.get(resource));
}
final boolean userHasTraversalSourceGrant = usernames.contains(user.getName()) || usernames.contains(AuthenticatedUser.ANONYMOUS_USERNAME);
final boolean userHasSandboxGrant = usernamesSandbox.contains(user.getName()) || usernamesSandbox.contains(AuthenticatedUser.ANONYMOUS_USERNAME);
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());
// This element becomes obsolete after resolving TINKERPOP-2473 for allowing only a single instance of each traversal strategy.
final boolean touchesSubgraphStrategy = bytecode.toString().contains(SubgraphStrategy.class.getSimpleName());
final List<String> rejections = new ArrayList<>();
if (runsLambda) {
rejections.add(REJECT_LAMBDA);
}
if (touchesReadOnlyStrategy) {
rejections.add(REJECT_MUTATE);
}
if (touchesOLAPRestriction) {
rejections.add(REJECT_OLAP);
}
if (touchesSubgraphStrategy) {
rejections.add(REJECT_SUBGRAPH);
}
String rejectMessage = REJECT_BYTECODE;
if (rejections.size() > 0) {
rejectMessage += " using " + String.join(", ", rejections);
}
rejectMessage += ".";
if ( (!userHasTraversalSourceGrant || runsLambda || touchesOLAPRestriction || touchesReadOnlyStrategy || touchesSubgraphStrategy) && !userHasSandboxGrant) {
throw new AuthorizationException(String.format(rejectMessage, aliases.values()));
}
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 {@link org.apache.tinkerpop.gremlin.driver.Tokens}.ARGS_GREMLIN argument can contain an arbitrary succession of script statements.
*/
public void authorize(final AuthenticatedUser user, final RequestMessage msg) throws AuthorizationException {
if (!usernamesSandbox.contains(user.getName())) {
throw new AuthorizationException(REJECT_STRING);
}
}
}