Skip to content

Commit

Permalink
[PLAT-13486]Provide admin user with replication role privileges to cr…
Browse files Browse the repository at this point in the history
…eate replication slots

Summary:
Use case: To enhance `createRestrictedUser` in YBA to accept a list of `DbRoleAttributes` and apply using an `ALTER ROLE <restricted_user> WITH <roleAttribute>` statement when creating the user.

This diff adds a payload parameter `dbRoleAttributes`, which accepts a list of `RoleAttribute` objects and the user is granted these roles during both the APIs: `create_restricted_db_credentials` & `create_db_credentials`

The supported roleAttributes for YSQL are: `SUPERUSER`, `NOSUPERUSER`, `CREATEDB`, `NOCREATEDB`, `CREATEROLE`, `NOCREATEROLE`, `INHERIT`, `NOINHERIT`, `LOGIN`, `NOLOGIN`, `REPLICATION`, `NOREPLICATION`, `BYPASSRLS`, `NOBYPASSRLS`
ref: https://www.postgresql.org/docs/current/sql-createrole.html

Test Plan:
Manual Testing
Endpoint:
```
POST    /customers/:cUUID/universes/:uniUUID/create_restricted_db_credentials
```
and
```
POST    /customers/:cUUID/universes/:uniUUID/create_db_credentials
```
  - Payload:
```
{
  "ycqlAdminUsername": "",
  "ycqlAdminPassword": "",
  "ysqlAdminUsername": "yugabyte",
  "ysqlAdminPassword": "",
  "dbName": "",
  "username": "test",
  "password": "Test@123",
  "dbRoleAttributes": []
}

```
User `test` is created but no permissions are granted for the same.

  - Payload:
```
{
  "ycqlAdminUsername": "",
  "ycqlAdminPassword": "",
  "ysqlAdminUsername": "yugabyte",
  "ysqlAdminPassword": "",
  "dbName": "",
  "username": "test",
  "password": "Test@123",
  "dbRoleAttributes": [
    {"name": "REPLICATION"},
    {"name": "CREATEROLE"}
  ]
}
```
User `test` is granted `REPLICATION` & `CREATEROLE` permissions

 - Payload:
```
{
  "ycqlAdminUsername": "",
  "ycqlAdminPassword": "",
  "ysqlAdminUsername": "yugabyte",
  "ysqlAdminPassword": "",
  "dbName": "",
  "username": "test",
  "password": "Test@123",
  "dbRoleAttributes": [
    {"name": "REPLICATION"},
    {"name": "test"}
  ]
}
```
response:
```
{
    "success": false,
    "error": {
        "dbRoleAttributes[1].name": [
            "Invalid value"
        ]
    }
}
```

Reviewers: #yba-api-review, sneelakantan, svarshney

Reviewed By: #yba-api-review, sneelakantan

Subscribers: yugaware

Differential Revision: https://phorge.dev.yugabyte.com/D34523
  • Loading branch information
rohita committed May 6, 2024
1 parent 0af5906 commit 749d700
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.yugabyte.yw.forms.DatabaseSecurityFormData;
import com.yugabyte.yw.forms.DatabaseUserDropFormData;
import com.yugabyte.yw.forms.DatabaseUserFormData;
import com.yugabyte.yw.forms.DatabaseUserFormData.RoleAttribute;
import com.yugabyte.yw.forms.RunQueryFormData;
import com.yugabyte.yw.models.Customer;
import com.yugabyte.yw.models.Universe;
Expand Down Expand Up @@ -304,6 +305,15 @@ public void createRestrictedUser(Universe universe, DatabaseUserFormData data) {
"GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO \"%1$s\"; ", data.username))
.append(DEL_PG_ROLES_CMD_1);

// Construct ALTER ROLE statements based on the roleAttributes specified
if (data.dbRoleAttributes != null) {
for (RoleAttribute roleAttribute : data.dbRoleAttributes) {
createUserWithPrivileges.append(
String.format(
"ALTER ROLE \"%s\" %s;", data.username, roleAttribute.getName().toString()));
}
}

try {
runUserDbCommands(createUserWithPrivileges.toString(), data.dbName, universe);
LOG.info("Created restricted user and deleted dependencies");
Expand Down Expand Up @@ -407,6 +417,16 @@ public void createUser(Universe universe, DatabaseUserFormData data) {
"GRANT \"%s\" TO \"%s\" WITH ADMIN OPTION", DB_ADMIN_ROLE_NAME, data.username);
allQueries.append(String.format("%s; ", query));
}

// Construct ALTER ROLE statements based on the roleAttributes specified
if (data.dbRoleAttributes != null) {
for (RoleAttribute roleAttribute : data.dbRoleAttributes) {
allQueries.append(
String.format(
"ALTER ROLE \"%s\" %s;", data.username, roleAttribute.getName().toString()));
}
}

query = "SELECT pg_stat_statements_reset();";
allQueries.append(query);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@

import com.yugabyte.yw.common.PlatformServiceException;
import com.yugabyte.yw.common.Util;
import com.yugabyte.yw.models.common.YbaApi;
import io.swagger.annotations.ApiModelProperty;
import java.util.List;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import play.data.validation.Constraints;

@Data
public class DatabaseUserFormData {

public String ycqlAdminUsername;
Expand All @@ -22,6 +27,34 @@ public class DatabaseUserFormData {

@Constraints.Required() public String password;

@ApiModelProperty(required = false, value = "YbaApi Internal.")
@YbaApi(visibility = YbaApi.YbaApiVisibility.INTERNAL, sinceYBAVersion = "2.23.0")
public List<RoleAttribute> dbRoleAttributes;

@Data
public static class RoleAttribute {
private RoleAttributeName name;

// Refer to the list of roleAttributes here:
// https://www.postgresql.org/docs/current/sql-createrole.html
public static enum RoleAttributeName {
SUPERUSER,
NOSUPERUSER,
CREATEDB,
NOCREATEDB,
CREATEROLE,
NOCREATEROLE,
INHERIT,
NOINHERIT,
LOGIN,
NOLOGIN,
REPLICATION,
NOREPLICATION,
BYPASSRLS,
NOBYPASSRLS
}
}

// TODO(Shashank): Move this to use Validatable
public void validation() {
if (username == null || password == null) {
Expand Down
10 changes: 10 additions & 0 deletions managed/src/main/resources/swagger-strict.json
Original file line number Diff line number Diff line change
Expand Up @@ -10778,6 +10778,16 @@
},
"type" : "object"
},
"RoleAttribute" : {
"properties" : {
"name" : {
"enum" : [ "SUPERUSER", "NOSUPERUSER", "CREATEDB", "NOCREATEDB", "CREATEROLE", "NOCREATEROLE", "INHERIT", "NOINHERIT", "LOGIN", "NOLOGIN", "REPLICATION", "NOREPLICATION", "BYPASSRLS", "NOBYPASSRLS" ],
"type" : "string"
}
},
"required" : [ "name" ],
"type" : "object"
},
"RoleBinding" : {
"properties" : {
"createTime" : {
Expand Down
10 changes: 10 additions & 0 deletions managed/src/main/resources/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -10882,6 +10882,16 @@
},
"type" : "object"
},
"RoleAttribute" : {
"properties" : {
"name" : {
"enum" : [ "SUPERUSER", "NOSUPERUSER", "CREATEDB", "NOCREATEDB", "CREATEROLE", "NOCREATEROLE", "INHERIT", "NOINHERIT", "LOGIN", "NOLOGIN", "REPLICATION", "NOREPLICATION", "BYPASSRLS", "NOBYPASSRLS" ],
"type" : "string"
}
},
"required" : [ "name" ],
"type" : "object"
},
"RoleBinding" : {
"properties" : {
"createTime" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@

import static com.yugabyte.yw.common.AssertHelper.assertAuditEntry;
import static com.yugabyte.yw.common.AssertHelper.assertBadRequest;
import static com.yugabyte.yw.common.AssertHelper.assertErrorNodeValue;
import static com.yugabyte.yw.common.AssertHelper.assertErrorResponse;
import static com.yugabyte.yw.common.AssertHelper.assertOk;
import static com.yugabyte.yw.common.AssertHelper.assertPlatformException;
import static com.yugabyte.yw.common.ModelFactory.createUniverse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import static play.test.Helpers.contentAsString;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import com.yugabyte.yw.common.ConfigHelper;
Expand Down Expand Up @@ -336,6 +340,59 @@ public void testConfigureYCQL() {
assertOk(result);
}

@Test
public void testInvalidDbRoleAttribute() {
Universe u = createUniverse(customer.getId());
when(mockRuntimeConfig.getBoolean("yb.cloud.enabled")).thenReturn(true);

ObjectNode bodyJson =
Json.newObject()
.put("ysqlAdminUsername", "yugabyte")
.put("username", "test")
.put("password", "Test@123")
.put("dbName", "test");

ObjectNode replication = Json.newObject();
replication.put("name", "REPLICATION");

ObjectNode test = Json.newObject();
test.put("name", "test");

ArrayNode dbRoleAttributesList = Json.newArray();
dbRoleAttributesList.add(replication);
dbRoleAttributesList.add(test);

bodyJson.set("dbRoleAttributes", dbRoleAttributesList);

String url1 =
"/api/customers/"
+ customer.getUuid()
+ "/universes/"
+ u.getUniverseUUID()
+ "/create_restricted_db_credentials";

Result result =
assertPlatformException(
() -> doRequestWithAuthTokenAndBody("POST", url1, authToken, bodyJson));
Mockito.verifyNoMoreInteractions(mockYsqlQueryExecutor);
JsonNode resultJson = Json.parse(contentAsString(result));
assertErrorNodeValue(resultJson, "dbRoleAttributes[1].name", "Invalid value");
assertAuditEntry(0, customer.getUuid());

String url2 =
"/api/customers/"
+ customer.getUuid()
+ "/universes/"
+ u.getUniverseUUID()
+ "/create_db_credentials";
result =
assertPlatformException(
() -> doRequestWithAuthTokenAndBody("POST", url2, authToken, bodyJson));
Mockito.verifyNoMoreInteractions(mockYsqlQueryExecutor, mockYcqlQueryExecutor);
assertErrorNodeValue(resultJson, "dbRoleAttributes[1].name", "Invalid value");
assertAuditEntry(0, customer.getUuid());
}

private void updateUniverseAPIDetails(
Universe universe,
boolean enableYSQL,
Expand Down

0 comments on commit 749d700

Please sign in to comment.