Skip to content

Commit

Permalink
[YSQL] yugabyte#869 Add Role-based Authorization
Browse files Browse the repository at this point in the history
Summary:
This diff adds support for postgres roles, object ownership, and permissions, which makes up the majority of RBAC. The two remaining steps are to add support for customizing config files (e.g. `pg_hba.conf`), and to add row-level security.

For the time being, roles can be used in the database to their full extent, however our default `pg_hba.conf` allows any connection from any host to connect as any role, without having to provide a password. To make roles practical, one must manually change the `pg_hba.conf` on all nodes to enforce more strict authentication methods (e.g. see the hba supplied in `TestPgAuthorization`). This can be done by either editing the default generated hba created by initdb, or by passing the relative file path to a custom hba file using the `pgsql_hba_conf_file` gflag in tests.

Test Plan:
Enabled the `roleattributes`, `rolenames`, `password`, `privileges`, and `init_privs` postgres regression tests with some minor modifications (and some unsupported features commented out). Added `TestPgRegressAuthorization.java` to test these pg regress tests.

Added `TestPgAuthorization` with java tests, covering areas which were not tested sufficiently by the above regress tests (especially multi-node tests, and tests related to login/connection).

Added `ClusterCleaner` interface, along with several implementations, as a faster and simpler way to clean up postgres between tests.

Added `ConnectionBuilder` to simplify connection creation with many parameters.

Reviewers: mihnea, neha

Reviewed By: neha

Subscribers: yql

Differential Revision: https://phabricator.dev.yugabyte.com/D6776
  • Loading branch information
srhickma committed Aug 6, 2019
1 parent 9c5616e commit e89d75b
Show file tree
Hide file tree
Showing 55 changed files with 9,681 additions and 320 deletions.
381 changes: 239 additions & 142 deletions java/yb-pgsql/src/test/java/org/yb/pgsql/BasePgSQLTest.java

Large diffs are not rendered by default.

3,271 changes: 3,271 additions & 0 deletions java/yb-pgsql/src/test/java/org/yb/pgsql/TestPgAuthorization.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,10 @@ public void testNoDDLRetry() throws Exception {

@Test
public void testVersionMismatchWithoutRetry() throws Exception {
try (Statement statement1 = createConnection(0).createStatement();
Statement statement2 = createConnection(1).createStatement()) {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
statement1.execute("CREATE TABLE test_table(id int, PRIMARY KEY (id))");
statement1.execute("INSERT INTO test_table(id) VALUES (1), (2), (3)");

Expand Down Expand Up @@ -208,8 +210,10 @@ public void testVersionMismatchWithoutRetry() throws Exception {

@Test
public void testVersionMismatchWithFailedRetry() throws Exception {
try (Statement statement1 = createConnection(0).createStatement();
Statement statement2 = createConnection(1).createStatement()) {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
// Create table from connection 1.
statement1.execute("CREATE TABLE test_table(id int)");

Expand Down Expand Up @@ -259,8 +263,10 @@ public void testVersionMismatchWithFailedRetry() throws Exception {

@Ignore // TODO enable after #1502
public void testUndetectedSelectVersionMismatch() throws Exception {
try (Statement statement1 = createConnection(0).createStatement();
Statement statement2 = createConnection(1).createStatement()) {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
// Create table from connection 1.
statement1.execute("CREATE TABLE test_table(id int, PRIMARY KEY (id))");

Expand All @@ -280,8 +286,10 @@ public void testUndetectedSelectVersionMismatch() throws Exception {

@Test
public void testConsistentNonRetryableTransactions() throws Exception {
try (Statement statement1 = createConnection(0).createStatement();
Statement statement2 = createConnection(1).createStatement()) {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
// Create table from connection 1.
statement1.execute("CREATE TABLE test_table(id int, PRIMARY KEY (id))");

Expand All @@ -307,8 +315,10 @@ public void testConsistentNonRetryableTransactions() throws Exception {

@Test
public void testConsistentPreparedStatements() throws Exception {
try (Statement statement1 = createConnection(0).createStatement();
Statement statement2 = createConnection(1).createStatement()) {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
// Create table from connection 1.
statement1.execute("CREATE TABLE test_table(id int, PRIMARY KEY (id))");

Expand Down Expand Up @@ -349,8 +359,10 @@ public void testConsistentPreparedStatements() throws Exception {

@Test
public void testConsistentExplain() throws Exception {
try (Statement statement1 = createConnection(0).createStatement();
Statement statement2 = createConnection(1).createStatement()) {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
// Create table with unique column from connection 1.
statement1.execute("CREATE TABLE test_table(id int, u int)");
statement1.execute("ALTER TABLE test_table ADD CONSTRAINT unq UNIQUE (u)");
Expand Down Expand Up @@ -382,6 +394,51 @@ public void testConsistentExplain() throws Exception {
}
}

@Test
public void testConsistentGUCWrites() throws Exception {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
statement1.execute("CREATE ROLE some_role");

// Update roles cache on connection 2.
statement2.execute("SET ROLE some_role");
statement2.execute("RESET ROLE");

statement1.execute("DROP ROLE some_role");

waitForTServerHeartbeat();

// Connection 2 refreshes its cache before setting the guc var.
runInvalidQuery(statement2, "SET ROLE some_role", "role \"some_role\" does not exist");
}
}

@Test
public void testInvalidationCallbacksWhenInsertingIntoList() throws Exception {
try (Connection connection1 = newConnectionBuilder().setTServer(0).connect();
Connection connection2 = newConnectionBuilder().setTServer(1).connect();
Statement statement1 = connection1.createStatement();
Statement statement2 = connection2.createStatement()) {
statement1.execute("CREATE ROLE some_role CREATEROLE");

statement2.execute("SET SESSION AUTHORIZATION some_role");

// Populate membership roles cache from connection 2.
statement2.execute("CREATE ROLE inaccessible");
runInvalidQuery(statement2, "SET ROLE inaccessible", "permission denied");

// Invalidate membership roles cache from connection 1.
statement1.execute("CREATE ROLE some_group ROLE some_role");

waitForTServerHeartbeat();

// Connection 2 observes the new membership roles list.
statement2.execute("SET ROLE some_group");
}
}

private interface ThrowingRunnable {
void run() throws Throwable;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) YugaByte, Inc.
//
// 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 org.yb.pgsql;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.yb.util.YBTestRunnerNonTsanOnly;

/**
* Runs the pg_regress authorization-related tests on YB code.
*/
@RunWith(value = YBTestRunnerNonTsanOnly.class)
public class TestPgRegressAuthorization extends BasePgSQLTest {
@Override
public int getTestMethodTimeoutSec() {
return 1800;
}

@Test
public void testPgRegressAuthorization() throws Exception {
runPgRegressTest("yb_pg_auth_serial_schedule");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) YugaByte, Inc.
//
// 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 org.yb.pgsql.cleaners;

import java.sql.Connection;

/**
* Interface representing an object which can perform some post-test cleanup
* on a postgres cluster.
*/
public interface ClusterCleaner {
void clean(Connection connection) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) YugaByte, Inc.
//
// 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 org.yb.pgsql.cleaners;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

/**
* Closes all registered postgres connections, except the connection passed
* to {@link ConnectionCleaner#clean(Connection)}.
*/
public class ConnectionCleaner implements ClusterCleaner {
private static final Logger LOG = LoggerFactory.getLogger(ConnectionCleaner.class);

private static List<Connection> connectionsToClose = new ArrayList<>();

public static void register(Connection connection) {
connectionsToClose.add(connection);
}

@Override
public void clean(Connection rootConnection) throws Exception {
LOG.info("Cleaning-up postgres connections");

if (rootConnection != null) {
try (Statement statement = rootConnection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery(
"SELECT client_hostname, client_port, state, query, pid FROM pg_stat_activity")) {
while (resultSet.next()) {
int backendPid = resultSet.getInt(5);
LOG.info(String.format(
"Found connection: hostname=%s, port=%s, state=%s, query=%s, backend_pid=%s",
resultSet.getString(1), resultSet.getInt(2),
resultSet.getString(3), resultSet.getString(4), backendPid));
}
}
} catch (SQLException e) {
LOG.info("Exception when trying to list PostgreSQL connections", e);
}

LOG.info("Closing connections.");
for (Connection connection : connectionsToClose) {
// Keep the main connection alive between tests.
if (connection == rootConnection) continue;

try {
if (connection == null) {
LOG.error("connectionsToClose contains a null connection!");
} else {
connection.close();
}
} catch (SQLException ex) {
LOG.error("Exception while trying to close connection");
throw ex;
}
}
} else {
LOG.info("Connection is already null, nothing to close");
}
LOG.info("Finished closing connection.");

connectionsToClose.clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) YugaByte, Inc.
//
// 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 org.yb.pgsql.cleaners;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

/**
* Removes all databases excluding `postgres`, `template1`, and `template2`.
* Any lower-priority cleaners should only clean objects in one of the remaining
* three databases, or cluster-wide objects (e.g. roles).
* The passed connection must be open in one of the three databases listed above.
*/
public class DatabaseCleaner implements ClusterCleaner {
private static final Logger LOG = LoggerFactory.getLogger(DatabaseCleaner.class);

@Override
public void clean(Connection connection) throws Exception {
LOG.info("Cleaning-up non-standard postgres databases");

try (Statement statement = connection.createStatement()) {
statement.execute("RESET SESSION AUTHORIZATION");

ResultSet resultSet = statement.executeQuery(
"SELECT datname FROM pg_database" +
" WHERE datname <> 'template0'" +
" AND datname <> 'template1'" +
" AND datname <> 'postgres'");

List<String> databases = new ArrayList<>();
while (resultSet.next()) {
databases.add(resultSet.getString(1));
}

for (String database : databases) {
statement.execute("DROP DATABASE " + database);
}
}
}
}
59 changes: 59 additions & 0 deletions java/yb-pgsql/src/test/java/org/yb/pgsql/cleaners/RoleCleaner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) YugaByte, Inc.
//
// 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 org.yb.pgsql.cleaners;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yb.pgsql.BasePgSQLTest;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

/**
* Removes all non-standard roles (except the test role), as well as their permissions and
* owned objects.
* NOTE: This will fail if any roles own objects in databases other than `postgres`,
* so {@link DatabaseCleaner} must be run first.
*/
public class RoleCleaner implements ClusterCleaner {
private static final Logger LOG = LoggerFactory.getLogger(RoleCleaner.class);

@Override
public void clean(Connection connection) throws Exception {
LOG.info("Cleaning-up postgres roles and permissions");

try (Statement statement = connection.createStatement()) {
statement.execute("RESET SESSION AUTHORIZATION");

ResultSet resultSet = statement.executeQuery(
"SELECT rolname FROM pg_roles" +
" WHERE rolname <> 'postgres'" +
" AND rolname <> '" + BasePgSQLTest.TEST_PG_USER + "'" +
" AND rolname NOT LIKE 'pg_%'");

List<String> roles = new ArrayList<>();
while (resultSet.next()) {
roles.add(resultSet.getString(1));
}

for (String role : roles) {
statement.execute("DROP OWNED BY " + role + " CASCADE");
statement.execute("DROP ROLE " + role);
}
}
}
}
Loading

0 comments on commit e89d75b

Please sign in to comment.