feat(tools/postgres): Add new postgres-list-roles tool (#2038)

## Description
Adds a postgresql custom list_roles tool, that lists all the
user-created roles in the instance. It provides details about each
role's attributes and memberships.


> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution
![Uploading Screenshot 2025-11-26 at 1.16.42 AM.png…]()

<img width="1065" height="145" alt="Screenshot 2025-11-26 at 12 59
56 AM"
src="https://github.com/user-attachments/assets/d90131b1-d369-4108-b4db-ee5dc9aafe38"
/>


## PR Checklist

> Thank you for opening a Pull Request! Before submitting your PR, there
are a
> few things you can do to make sure it goes smoothly:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #<1738>

Co-authored-by: Wenxin Du <117315983+duwenxin99@users.noreply.github.com>
This commit is contained in:
Srividya Reddy
2025-12-10 03:18:20 +05:30
committed by GitHub
parent 489117d747
commit bea9705450
17 changed files with 606 additions and 3 deletions

View File

@@ -191,6 +191,7 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistpgsettings"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistpublicationtables"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistquerystats"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistroles"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistschemas"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistsequences"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttables"

View File

@@ -1488,7 +1488,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"alloydb_postgres_database_tools": tools.ToolsetConfig{
Name: "alloydb_postgres_database_tools",
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats"},
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles"},
},
},
},
@@ -1518,7 +1518,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"cloud_sql_postgres_database_tools": tools.ToolsetConfig{
Name: "cloud_sql_postgres_database_tools",
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats"},
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles"},
},
},
},
@@ -1618,7 +1618,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"postgres_database_tools": tools.ToolsetConfig{
Name: "postgres_database_tools",
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats"},
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences", "long_running_transactions", "list_locks", "replication_stats", "list_query_stats", "get_column_cardinality", "list_publication_tables", "list_tablespaces", "list_pg_settings", "list_database_stats", "list_roles"},
},
},
},

View File

@@ -55,6 +55,7 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
* `list_pg_settings`: List configuration parameters for the PostgreSQL server.
* `list_database_stats`: Lists the key performance and activity statistics for
each database in the AlloyDB instance.
* `list_roles`: Lists all the user-created roles in PostgreSQL database.
## AlloyDB Postgres Admin
@@ -237,6 +238,7 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
* `list_pg_settings`: List configuration parameters for the PostgreSQL server.
* `list_database_stats`: Lists the key performance and activity statistics for
each database in the postgreSQL instance.
* `list_roles`: Lists all the user-created roles in PostgreSQL database.
## Cloud SQL for PostgreSQL Observability
@@ -547,6 +549,7 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
* `list_pg_settings`: List configuration parameters for the PostgreSQL server.
* `list_database_stats`: Lists the key performance and activity statistics for
each database in the PostgreSQL server.
* `list_roles`: Lists all the user-created roles in PostgreSQL database.
## Google Cloud Serverless for Apache Spark

View File

@@ -90,6 +90,9 @@ cluster][alloydb-free-trial].
Lists the key performance and activity statistics for each database in the AlloyDB
instance.
- [`postgres-list-roles`](../tools/postgres/postgres-list-roles.md)
Lists all the user-created roles in PostgreSQL database..
### Pre-built Configurations
- [AlloyDB using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_mcp/)

View File

@@ -86,6 +86,9 @@ to a database by following these instructions][csql-pg-quickstart].
Lists the key performance and activity statistics for each database in the postgreSQL
instance.
- [`postgres-list-roles`](../tools/postgres/postgres-list-roles.md)
Lists all the user-created roles in PostgreSQL database..
### Pre-built Configurations
- [Cloud SQL for Postgres using

View File

@@ -81,6 +81,9 @@ reputation for reliability, feature robustness, and performance.
Lists the key performance and activity statistics for each database in the postgreSQL
server.
- [`postgres-list-roles`](../tools/postgres/postgres-list-roles.md)
Lists all the user-created roles in PostgreSQL database..
### Pre-built Configurations
- [PostgreSQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/postgres_mcp/)

View File

@@ -0,0 +1,70 @@
---
title: "postgres-list-roles"
type: docs
weight: 1
description: >
The "postgres-list-roles" tool lists user-created roles in a Postgres database.
aliases:
- /resources/tools/postgres-list-roles
---
## About
The `postgres-list-roles` tool lists all the user-created roles in the instance, excluding system roles (like `cloudsql%` or `pg_%`). It provides details about each role's attributes and memberships. It's compatible with
any of the following sources:
- [alloydb-postgres](../../sources/alloydb-pg.md)
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
- [postgres](../../sources/postgres.md)
`postgres-list-roles` lists detailed information as JSON for each role. The tool
takes the following input parameters:
- `role_name` (optional): A text to filter results by role name. Default: `""`
- `limit` (optional): The maximum number of roles to return. Default: `50`
## Example
```yaml
tools:
list_indexes:
kind: postgres-list-roles
source: postgres-source
description: |
Lists all the user-created roles in the instance . It returns the role name,
Object ID, the maximum number of concurrent connections the role can make,
along with boolean indicators for: superuser status, privilege inheritance
from member roles, ability to create roles, ability to create databases,
ability to log in, replication privilege, and the ability to bypass
row-level security, the password expiration timestamp, a list of direct
members belonging to this role, and a list of other roles/groups that this
role is a member of.
```
The response is a json array with the following elements:
```json
{
"role_name": "Name of the role",
"oid": "Object ID of the role",
"connection_limit": "Maximum concurrent connections allowed (-1 for no limit)",
"is_superuser": "Boolean, true if the role is a superuser",
"inherits_privileges": "Boolean, true if the role inherits privileges of roles it is a member of",
"can_create_roles": "Boolean, true if the role can create other roles",
"can_create_db": "Boolean, true if the role can create databases",
"can_login": "Boolean, true if the role can log in",
"is_replication_role": "Boolean, true if this is a replication role",
"bypass_rls": "Boolean, true if the role bypasses row-level security policies",
"valid_until": "Timestamp until the password is valid (null if forever)",
"direct_members": ["Array of role names that are direct members of this role"],
"member_of": ["Array of role names that this role is a member of"]
}
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:------------:|------------------------------------------------------|
| kind | string | true | Must be "postgres-list-roles". |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | false | Description of the tool that is passed to the agent. |

View File

@@ -216,6 +216,10 @@ tools:
kind: postgres-list-database-stats
source: alloydb-pg-source
list_roles:
kind: postgres-list-roles
source: alloydb-pg-source
toolsets:
alloydb_postgres_database_tools:
- execute_sql
@@ -244,3 +248,4 @@ toolsets:
- list_tablespaces
- list_pg_settings
- list_database_stats
- list_roles

View File

@@ -218,6 +218,10 @@ tools:
kind: postgres-list-database-stats
source: cloudsql-pg-source
list_roles:
kind: postgres-list-roles
source: cloudsql-pg-source
toolsets:
cloud_sql_postgres_database_tools:
- execute_sql
@@ -246,3 +250,4 @@ toolsets:
- list_tablespaces
- list_pg_settings
- list_database_stats
- list_roles

View File

@@ -217,6 +217,10 @@ tools:
kind: postgres-list-database-stats
source: postgresql-source
list_roles:
kind: postgres-list-roles
source: postgresql-source
toolsets:
postgres_database_tools:
- execute_sql
@@ -245,3 +249,4 @@ toolsets:
- list_tablespaces
- list_pg_settings
- list_database_stats
- list_roles

View File

@@ -0,0 +1,228 @@
// Copyright 2025 Google LLC
//
// 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 postgreslistroles
import (
"context"
"fmt"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/sources/alloydbpg"
"github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
"github.com/googleapis/genai-toolbox/internal/sources/postgres"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
"github.com/jackc/pgx/v5/pgxpool"
)
const kind string = "postgres-list-roles"
const listRolesStatement = `
WITH RoleDetails AS (
SELECT
r.rolname AS role_name,
r.oid AS oid,
r.rolconnlimit AS connection_limit,
r.rolsuper AS is_superuser,
r.rolinherit AS inherits_privileges,
r.rolcreaterole AS can_create_roles,
r.rolcreatedb AS can_create_db,
r.rolcanlogin AS can_login,
r.rolreplication AS is_replication_role,
r.rolbypassrls AS bypass_rls,
r.rolvaliduntil AS valid_until,
-- List of roles that belong to this role (Direct Members)
ARRAY(
SELECT m_r.rolname
FROM pg_auth_members pam
JOIN pg_roles m_r ON pam.member = m_r.oid
WHERE pam.roleid = r.oid
) AS direct_members,
-- List of roles that this role belongs to (Member Of)
ARRAY(
SELECT g_r.rolname
FROM pg_auth_members pam
JOIN pg_roles g_r ON pam.roleid = g_r.oid
WHERE pam.member = r.oid
) AS member_of
FROM pg_roles r
-- Exclude system and internal roles
WHERE r.rolname NOT LIKE 'cloudsql%'
AND r.rolname NOT LIKE 'alloydb_%'
AND r.rolname NOT LIKE 'pg_%'
)
SELECT *
FROM RoleDetails
WHERE
($1::text IS NULL OR role_name LIKE '%' || $1 || '%')
ORDER BY role_name
LIMIT COALESCE($2::int, 50);
`
func init() {
if !tools.Register(kind, newConfig) {
panic(fmt.Sprintf("tool kind %q already registered", kind))
}
}
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
actual := Config{Name: name}
if err := decoder.DecodeContext(ctx, &actual); err != nil {
return nil, err
}
return actual, nil
}
type compatibleSource interface {
PostgresPool() *pgxpool.Pool
}
// validate compatible sources are still compatible
var _ compatibleSource = &alloydbpg.Source{}
var _ compatibleSource = &cloudsqlpg.Source{}
var _ compatibleSource = &postgres.Source{}
var compatibleSources = [...]string{alloydbpg.SourceKind, cloudsqlpg.SourceKind, postgres.SourceKind}
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description"`
AuthRequired []string `yaml:"authRequired"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigKind() string {
return kind
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
// verify source exists
rawS, ok := srcs[cfg.Source]
if !ok {
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
}
// verify the source is compatible
s, ok := rawS.(compatibleSource)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
allParameters := parameters.Parameters{
parameters.NewStringParameterWithDefault("role_name", "", "Optional: a text to filter results by role name. The input is used within a LIKE clause."),
parameters.NewIntParameterWithDefault("limit", 50, "Optional: The maximum number of rows to return. Default is 10"),
}
description := cfg.Description
if description == "" {
description = "Lists all the user-created roles in the instance . It returns the role name, Object ID, the maximum number of concurrent connections the role can make, along with boolean indicators for: superuser status, privilege inheritance from member roles, ability to create roles, ability to create databases, ability to log in, replication privilege, and the ability to bypass row-level security, the password expiration timestamp, a list of direct members belonging to this role, and a list of other roles/groups that this role is a member of."
}
mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters, nil)
// finish tool setup
return Tool{
Config: cfg,
allParams: allParameters,
pool: s.PostgresPool(),
manifest: tools.Manifest{
Description: description,
Parameters: allParameters.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Config
allParams parameters.Parameters `yaml:"allParams"`
pool *pgxpool.Pool
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) ToConfig() tools.ToolConfig {
return t.Config
}
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
paramsMap := params.AsMap()
newParams, err := parameters.GetParams(t.allParams, paramsMap)
if err != nil {
return nil, fmt.Errorf("unable to extract standard params %w", err)
}
sliceParams := newParams.AsSlice()
results, err := t.pool.Query(ctx, listRolesStatement, sliceParams...)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
}
defer results.Close()
fields := results.FieldDescriptions()
var out []map[string]any
for results.Next() {
values, err := results.Values()
if err != nil {
return nil, fmt.Errorf("unable to parse row: %w", err)
}
rowMap := make(map[string]any)
for i, field := range fields {
rowMap[string(field.Name)] = values[i]
}
out = append(out, rowMap)
}
// this will catch actual query execution errors
if err := results.Err(); err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
}
return out, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (parameters.ParamValues, error) {
return parameters.ParseParams(t.allParams, data, claims)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) bool {
return false
}
func (t Tool) GetAuthTokenHeaderName() string {
return "Authorization"
}

View File

@@ -0,0 +1,95 @@
// Copyright 2025 Google LLC
//
// 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 postgreslistroles_test
import (
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistroles"
)
func TestParseFromYamlPostgresListRoles(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
tcs := []struct {
desc string
in string
want server.ToolConfigs
}{
{
desc: "basic example",
in: `
tools:
example_tool:
kind: postgres-list-roles
source: my-postgres-instance
description: some description
authRequired:
- my-google-auth-service
- other-auth-service
`,
want: server.ToolConfigs{
"example_tool": postgreslistroles.Config{
Name: "example_tool",
Kind: "postgres-list-roles",
Source: "my-postgres-instance",
Description: "some description",
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
},
},
},
{
desc: "basic example",
in: `
tools:
example_tool:
kind: postgres-list-roles
source: my-postgres-instance
description: some description
`,
want: server.ToolConfigs{
"example_tool": postgreslistroles.Config{
Name: "example_tool",
Kind: "postgres-list-roles",
Source: "my-postgres-instance",
Description: "some description",
AuthRequired: []string{},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got := struct {
Tools server.ToolConfigs `yaml:"tools"`
}{}
// Parse contents
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
if err != nil {
t.Fatalf("unable to unmarshal: %s", err)
}
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
t.Fatalf("incorrect parse: diff %v", diff)
}
})
}
}

View File

@@ -199,6 +199,7 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
tests.RunPostgresListTableSpacesTest(t)
tests.RunPostgresListPgSettingsTest(t, ctx, pool)
tests.RunPostgresListDatabaseStatsTest(t, ctx, pool)
tests.RunPostgresListRolesTest(t, ctx, pool)
}
// Test connection with different IP type

View File

@@ -183,6 +183,7 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
tests.RunPostgresListTableSpacesTest(t)
tests.RunPostgresListPgSettingsTest(t, ctx, pool)
tests.RunPostgresListDatabaseStatsTest(t, ctx, pool)
tests.RunPostgresListRolesTest(t, ctx, pool)
}
// Test connection with different IP type

View File

@@ -211,6 +211,7 @@ func AddPostgresPrebuiltConfig(t *testing.T, config map[string]any) map[string]a
PostgresListTablespacesToolKind = "postgres-list-tablespaces"
PostgresListPGSettingsToolKind = "postgres-list-pg-settings"
PostgresListDatabaseStatsToolKind = "postgres-list-database-stats"
PostgresListRolesToolKind = "postgres-list-roles"
)
tools, ok := config["tools"].(map[string]any)
@@ -298,6 +299,10 @@ func AddPostgresPrebuiltConfig(t *testing.T, config map[string]any) map[string]a
"source": "my-instance",
}
tools["list_roles"] = map[string]any{
"kind": PostgresListRolesToolKind,
"source": "my-instance",
}
config["tools"] = tools
return config
}

View File

@@ -162,4 +162,5 @@ func TestPostgres(t *testing.T) {
tests.RunPostgresListTableSpacesTest(t)
tests.RunPostgresListPgSettingsTest(t, ctx, pool)
tests.RunPostgresListDatabaseStatsTest(t, ctx, pool)
tests.RunPostgresListRolesTest(t, ctx, pool)
}

View File

@@ -2539,6 +2539,180 @@ func setUpDatabase(t *testing.T, ctx context.Context, pool *pgxpool.Pool, dbName
}
}
func setupPostgresRoles(t *testing.T, ctx context.Context, pool *pgxpool.Pool) (string, string, string, func(t *testing.T)) {
t.Helper()
suffix := strings.ReplaceAll(uuid.New().String(), "-", "")
adminUser := "test_role_admin_" + suffix
superUser := "test_role_super_" + suffix
normalUser := "test_role_normal_" + suffix
createAdminStmt := fmt.Sprintf("CREATE ROLE %s NOLOGIN;", adminUser)
if _, err := pool.Exec(ctx, createAdminStmt); err != nil {
t.Fatalf("unable to create role %s: %v", adminUser, err)
}
createSuperUserStmt := fmt.Sprintf("CREATE ROLE %s LOGIN CREATEDB;", superUser)
if _, err := pool.Exec(ctx, createSuperUserStmt); err != nil {
t.Fatalf("unable to create role %s: %v", superUser, err)
}
createNormalUserStmt := fmt.Sprintf("CREATE ROLE %s LOGIN;", normalUser)
if _, err := pool.Exec(ctx, createNormalUserStmt); err != nil {
t.Fatalf("unable to create role %s: %v", normalUser, err)
}
// Establish Relationships (Admin -> Superuser -> Normal)
if _, err := pool.Exec(ctx, fmt.Sprintf("GRANT %s TO %s;", adminUser, superUser)); err != nil {
t.Fatalf("unable to grant %s to %s: %v", adminUser, superUser, err)
}
if _, err := pool.Exec(ctx, fmt.Sprintf("GRANT %s TO %s;", superUser, normalUser)); err != nil {
t.Fatalf("unable to grant %s to %s: %v", superUser, normalUser, err)
}
return adminUser, superUser, normalUser, func(t *testing.T) {
t.Helper()
_, _ = pool.Exec(ctx, fmt.Sprintf("DROP ROLE IF EXISTS %s;", normalUser))
_, _ = pool.Exec(ctx, fmt.Sprintf("DROP ROLE IF EXISTS %s;", superUser))
_, _ = pool.Exec(ctx, fmt.Sprintf("DROP ROLE IF EXISTS %s;", adminUser))
}
}
func RunPostgresListRolesTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
adminUser, superUser, normalUser, cleanup := setupPostgresRoles(t, ctx, pool)
defer cleanup(t)
wantAdmin := map[string]any{
"role_name": adminUser,
"connection_limit": float64(-1),
"is_superuser": false,
"inherits_privileges": true,
"can_create_roles": false,
"can_create_db": false,
"can_login": false,
"is_replication_role": false,
"bypass_rls": false,
"direct_members": []any{superUser},
"member_of": []any{},
}
wantSuperUser := map[string]any{
"role_name": superUser,
"connection_limit": float64(-1),
"is_superuser": false,
"inherits_privileges": true,
"can_create_roles": false,
"can_create_db": true,
"can_login": true,
"is_replication_role": false,
"bypass_rls": false,
"direct_members": []any{normalUser},
"member_of": []any{adminUser},
}
wantNormalUser := map[string]any{
"role_name": normalUser,
"connection_limit": float64(-1),
"is_superuser": false,
"inherits_privileges": true,
"can_create_roles": false,
"can_create_db": false,
"can_login": true,
"is_replication_role": false,
"bypass_rls": false,
"direct_members": []any{},
"member_of": []any{superUser},
}
invokeTcs := []struct {
name string
requestBody io.Reader
wantStatusCode int
want []map[string]any
}{
{
name: "list_roles with filter for created roles",
requestBody: bytes.NewBufferString(`{"role_name": "test_role_"}`),
wantStatusCode: http.StatusOK,
want: []map[string]any{wantAdmin, wantNormalUser, wantSuperUser},
},
{
name: "list_roles filter specific role",
requestBody: bytes.NewBufferString(fmt.Sprintf(`{"role_name": "%s"}`, superUser)),
wantStatusCode: http.StatusOK,
want: []map[string]any{wantSuperUser},
},
{
name: "list_roles non-existent role",
requestBody: bytes.NewBufferString(`{"role_name": "non_existent_role_xyz"}`),
wantStatusCode: http.StatusOK,
want: nil,
},
}
for _, tc := range invokeTcs {
t.Run(tc.name, func(t *testing.T) {
const api = "http://127.0.0.1:5000/api/tool/list_roles/invoke"
resp, respBody := RunRequest(t, http.MethodPost, api, tc.requestBody, nil)
if resp.StatusCode != tc.wantStatusCode {
t.Fatalf("wrong status code: got %d, want %d, body: %s", resp.StatusCode, tc.wantStatusCode, string(respBody))
}
if tc.wantStatusCode != http.StatusOK {
return
}
var bodyWrapper struct {
Result json.RawMessage `json:"result"`
}
if err := json.Unmarshal(respBody, &bodyWrapper); err != nil {
t.Fatalf("error decoding response wrapper: %v", err)
}
var resultString string
if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
resultString = string(bodyWrapper.Result)
}
var got []map[string]any
if err := json.Unmarshal([]byte(resultString), &got); err != nil {
t.Fatalf("failed to unmarshal nested result string: %v, resultString: %s", err, resultString)
}
gotMap := make(map[string]map[string]any)
for _, role := range got {
// Remove fields that change every run
delete(role, "oid")
delete(role, "valid_until")
if name, ok := role["role_name"].(string); ok {
gotMap[name] = role
}
}
// Check that every role in 'want' exists in 'got' and matches
for _, wantRole := range tc.want {
roleName, _ := wantRole["role_name"].(string)
gotRole, exists := gotMap[roleName]
if !exists {
t.Errorf("Expected role %q was not found in the response", roleName)
continue
}
if diff := cmp.Diff(wantRole, gotRole); diff != "" {
t.Errorf("Role %q mismatch (-want +got):\n%s", roleName, diff)
}
}
// Verify that if want is nil/empty, got is also empty
if len(tc.want) == 0 && len(got) != 0 {
t.Errorf("Expected empty result, but got %d roles", len(got))
}
})
}
}
// RunMySQLListTablesTest run tests against the mysql-list-tables tool
func RunMySQLListTablesTest(t *testing.T, databaseName, tableNameParam, tableNameAuth, expectedOwner string) {
var ownerWant any