mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-09 07:28:05 -05:00
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:
@@ -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"
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/)
|
||||
|
||||
70
docs/en/resources/tools/postgres/postgres-list-roles.md
Normal file
70
docs/en/resources/tools/postgres/postgres-list-roles.md
Normal 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. |
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
228
internal/tools/postgres/postgreslistroles/postgreslistroles.go
Normal file
228
internal/tools/postgres/postgreslistroles/postgreslistroles.go
Normal 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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
174
tests/tool.go
174
tests/tool.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user