Commit a1a10a63 by James Cropcho

Merge pull request #61 from todvora/master

Variety works with restricted user permissions + tests
parents 558352d0 6a366b35
......@@ -16,7 +16,7 @@
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.12.3</version>
<version>2.12.4</version>
</dependency>
<dependency>
......
package com.github.variety;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject;
import com.mongodb.MongoCredential;
import com.mongodb.util.JSON;
/**
* MongoDB access credentials for admin/root with unrestricted access and for user with read only database test
*/
public enum Credentials {
ADMIN("admin", "variety_test_admin", "admin", "['userAdminAnyDatabase', 'readWriteAnyDatabase', 'dbAdminAnyDatabase']"),
USER("test", "variety_test_user", "test", "['read']");
/**
* Name of database, where auth objects are stored.
*/
public static final String AUTH_DATABASE_NAME = "admin";
private final String authDatabase;
private final String username;
private final String password;
private final String rolesJson;
Credentials(final String authDatabase, final String username, final String password, final String rolesJson) {
this.authDatabase = authDatabase;
this.username = username;
this.password = password;
this.rolesJson = rolesJson;
}
public String getAuthDatabase() {
return authDatabase;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
/**
* @return Auth credentials for MongoDB Java driver.
*/
public MongoCredential getMongoCredential() {
return MongoCredential.createMongoCRCredential(getUsername(), getAuthDatabase(), getPassword().toCharArray());
}
/**
* Convert username, password and roles to MongoDB document for user creation
* @return json document to be passed to createUser (mongodb version >=2.6.x) / addUser function(mongodb 2.4.x)
*/
public DBObject getUserDocument() {
return new BasicDBObjectBuilder()
.add("user", username)
.add("pwd", password)
.add("roles", JSON.parse(this.rolesJson))
.get();
}
}
package com.github.variety;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
public class MongoShell {
private final boolean quiet;
private final Credentials credentials;
private final String eval;
private final String database;
private final String script;
public MongoShell(final String database, final Credentials credentials, final String eval, final String script, final boolean quiet) {
this.quiet = quiet;
this.credentials = credentials;
this.eval = eval;
this.database = database;
this.script = script;
}
public String execute() throws IOException, InterruptedException {
final List<String> commands = new ArrayList<>();
commands.add("mongo");
if (database != null && !database.isEmpty()) {
commands.add(this.database);
}
if (quiet) {
commands.add("--quiet");
}
if (credentials != null) {
commands.add("--username");
commands.add(credentials.getUsername());
commands.add("--password");
commands.add(credentials.getPassword());
commands.add("--authenticationDatabase");
commands.add(credentials.getAuthDatabase());
}
if (eval != null && !eval.isEmpty()) {
commands.add("--eval");
commands.add(eval);
}
if (script != null && !script.isEmpty()) {
commands.add(script);
}
final String[] cmdarray = commands.toArray(new String[commands.size()]);
final Process child = Runtime.getRuntime().exec(cmdarray);
final int returnCode = child.waitFor();
final String stdOut = readStream(child.getInputStream());
if (returnCode != 0) {
throw new RuntimeException("Failed to execute MongoDB shell with arguments: " + Arrays.toString(cmdarray) + ".\n" + stdOut);
}
return stdOut;
}
/**
* Converts input stream to String containing lines separated by \n
*/
private String readStream(final InputStream stream) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
final StringJoiner builder = new StringJoiner("\n");
reader.lines().forEach(builder::add);
return builder.toString();
}
}
......@@ -6,17 +6,12 @@ import com.github.variety.validator.ResultsValidator;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
/**
......@@ -42,6 +37,8 @@ public class Variety {
private final String inputCollection;
private final MongoClient mongoClient;
private final Credentials credentials;
private Integer limit;
private Integer maxDepth;
private String query;
......@@ -52,14 +49,24 @@ public class Variety {
/**
* Create variety wrapper with defined connection do analysed database and collection
* @param database name of database, that will be analysed
* @param database name of database, that will be analysed
* @param collection name of collection, that will be analysed
* @throws UnknownHostException Thrown when fails connection do default host and port of MongoDB
*/
public Variety(final String database, final String collection) throws UnknownHostException {
this.inputDatabase = database;
this.inputCollection = collection;
this.mongoClient = new MongoClient();
this(database, collection, null);
}
public Variety(final String inputDatabase, final String inputCollection, final Credentials credentials) throws UnknownHostException {
this.inputDatabase = inputDatabase;
this.inputCollection = inputCollection;
this.credentials = credentials;
if (credentials == null) {
this.mongoClient = new MongoClient();
} else {
this.mongoClient = new MongoClient(new ServerAddress("localhost"), Arrays.asList(credentials.getMongoCredential()));
}
}
/**
......@@ -138,28 +145,11 @@ public class Variety {
* Executes mongo shell with configured variety options and variety.js script in path.
* @return Stdout of variety.js
*/
private String runAnalysis() throws IOException, InterruptedException {
final List<String> commands = new ArrayList<>();
commands.add("mongo");
commands.add(this.inputDatabase);
if(quiet) {
commands.add("--quiet");
}
commands.add("--eval");
commands.add(buildParams());
commands.add(getVarietyPath());
final String[] cmdarray = commands.toArray(new String[commands.size()]);
final Process child = Runtime.getRuntime().exec(cmdarray);
final int returnCode = child.waitFor();
final String stdOut = readStream(child.getInputStream());
if(returnCode != 0) {
throw new RuntimeException("Failed to execute variety.js with arguments: " + Arrays.toString(cmdarray) + ".\n" + stdOut);
}
System.out.println(stdOut);
return stdOut;
public String runAnalysis() throws IOException, InterruptedException {
final MongoShell mongoShell = new MongoShell(inputDatabase, credentials, buildParams(), getVarietyPath(), quiet);
final String result = mongoShell.execute();
System.out.println(result);
return result;
}
public ResultsValidator runJsonAnalysis() throws IOException, InterruptedException {
......@@ -214,13 +204,5 @@ public class Variety {
return Paths.get(this.getClass().getResource("/").getFile()).getParent().getParent().getParent().resolve("variety.js").toString();
}
/**
* Converts input stream to String containing lines separated by \n
*/
private String readStream(final InputStream stream) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
final StringJoiner builder = new StringJoiner("\n");
reader.lines().forEach(builder::add);
return builder.toString();
}
}
package com.github.variety.test;
import com.github.variety.Credentials;
import com.github.variety.MongoShell;
import com.github.variety.Variety;
import com.github.variety.validator.ResultsValidator;
import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.Arrays;
/**
* Tests, if variety can return results for user with read only access to analyzed database (without permission to list
* all other dbs / collections, without permission to persist results).
*/
public class LimitedAccessTest {
private Variety variety;
private MongoClient adminConnection;
@Before
public void setUp() throws Exception {
// create admin user (expects empty users table => no auth used for this connection)
createUser(null, Credentials.ADMIN);
// create limited user
createUser(Credentials.ADMIN, Credentials.USER);
// connect with admin credentials
adminConnection = new MongoClient(new ServerAddress("localhost"), Arrays.asList(Credentials.ADMIN.getMongoCredential()));
// create sample collection (logged admin user, writes to test DB)
adminConnection.getDB("test").getCollection("users").insert(SampleData.getDocuments());
// initialize variety with limited user credentials, connects to test/users collection
variety = new Variety("test", "users", Credentials.USER);
}
private void createUser(final Credentials loginCredentials, final Credentials userToCreate) throws IOException, InterruptedException {
final MongoShell shell = new MongoShell(userToCreate.getAuthDatabase(), loginCredentials, "db.addUser(" + userToCreate.getUserDocument() + ")", null, false);
System.out.println(shell.execute());
}
@After
public void tearDown() throws Exception {
adminConnection.getDB("test").getCollection("users").drop();
// remove both users from admin (auth) database. Caution, order is important - first delete user, then admin
System.out.println(new MongoShell("test", Credentials.ADMIN, "db.removeUser('" + Credentials.USER.getUsername() + "')", null, false).execute());
System.out.println(new MongoShell("admin", Credentials.ADMIN, "db.removeUser('" + Credentials.ADMIN.getUsername() + "')", null, false).execute());
}
/**
* Validate correct results read from JSON standard output, limited user connection provided
*/
@Test
public void verifyBasicResultsJson() throws Exception {
validate(variety.runJsonAnalysis());
}
@Test
public void verifyBasicResultsAscii() throws Exception {
final String stdout = variety.withPersistResults(false).withQuiet(true).runAnalysis();
Assert.assertEquals(SampleData.EXPECTED_DATA_ASCII_TABLE, stdout);
}
@Test
public void testNotFoundDatabaseForAdmin() throws Exception {
final Variety adminVariety = new Variety("foo", "users", Credentials.ADMIN);
try {
adminVariety.runAnalysis();
Assert.fail("Should throw exception");
} catch (final Exception e) {
System.out.println(e);
final String messageVersion24 = "The collection specified (users) in the database specified (foo) does not exist or is empty";
final String messageVersion26 = "The database specified (foo) does not exist";
Assert.assertTrue(e.getMessage().contains(messageVersion24) || e.getMessage().contains(messageVersion26));
}
}
@Test
public void testNotFoundCollectionForAdmin() throws Exception {
final Variety adminVariety = new Variety("test", "bar", Credentials.ADMIN);
try {
adminVariety.runAnalysis();
Assert.fail("Should throw exception");
} catch (final Exception e) {
Assert.assertTrue(e.getMessage().contains("The collection specified (bar) in the database specified (test) does not exist or is empty."));
Assert.assertTrue(e.getMessage().contains("Possible collection options for database specified:"));
}
}
@Test
public void testNotFoundCollectionForUser() throws Exception {
final Variety adminVariety = new Variety("test", "bar", Credentials.USER);
try {
adminVariety.runAnalysis();
Assert.fail("Should throw exception");
} catch (final Exception e) {
Assert.assertTrue(e.getMessage().contains("The collection specified (bar) in the database specified (test) does not exist or is empty."));
Assert.assertTrue(e.getMessage().contains("Possible collection options for database specified:"));
}
}
@Test
public void testNotFoundDbForUser() throws Exception {
final Variety adminVariety = new Variety("foo", "users", Credentials.USER);
try {
adminVariety.runAnalysis();
Assert.fail("Should throw exception");
} catch (final Exception e) {
Assert.assertTrue("Exception should contain info about not authorized access, full message is: '" + e.getMessage() + "'", e.getMessage().contains("not authorized"));
}
}
private void validate(final ResultsValidator analysis) {
analysis.validate("_id", 5, 100, "ObjectId");
analysis.validate("name", 5, 100, "String");
analysis.validate("bio", 3, 60, "String");
analysis.validate("pets", 2, 40, "String", "Array");
analysis.validate("someBinData", 1, 20, "BinData-old");
analysis.validate("someWeirdLegacyKey", 1, 20, "String");
}
}
......@@ -25,24 +25,26 @@ if (typeof db_name === 'string') {
db = db.getMongo().getDB( db_name );
}
var knownDatabases = db.adminCommand('listDatabases').databases;
if(typeof knownDatabases !== 'undefined') { // not authorized user receives error response (json) without databases key
knownDatabases.forEach(function(d){
if(db.getSisterDB(d.name).getCollectionNames().length > 0) {
dbs.push(d.name);
}
if(db.getSisterDB(d.name).getCollectionNames().length === 0) {
emptyDbs.push(d.name);
}
});
db.adminCommand('listDatabases').databases.forEach(function(d){
if(db.getSisterDB(d.name).getCollectionNames().length > 0) {
dbs.push(d.name);
}
if(db.getSisterDB(d.name).getCollectionNames().length === 0) {
emptyDbs.push(d.name);
if (emptyDbs.indexOf(db.getName()) !== -1) {
throw 'The database specified ('+ db +') is empty.\n'+
'Possible database options are: ' + dbs.join(', ') + '.';
}
});
if (emptyDbs.indexOf(db.getName()) !== -1) {
throw 'The database specified ('+ db +') is empty.\n'+
'Possible database options are: ' + dbs.join(', ') + '.';
}
if (dbs.indexOf(db.getName()) === -1) {
throw 'The database specified ('+ db +') does not exist.\n'+
'Possible database options are: ' + dbs.join(', ') + '.';
if (dbs.indexOf(db.getName()) === -1) {
throw 'The database specified ('+ db +') does not exist.\n'+
'Possible database options are: ' + dbs.join(', ') + '.';
}
}
var collNames = db.getCollectionNames().join(', ');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment