diff --git a/pkg/tools/auth/introspect_self.go b/pkg/tools/auth/introspect_self.go new file mode 100644 index 0000000..77d737b --- /dev/null +++ b/pkg/tools/auth/introspect_self.go @@ -0,0 +1,90 @@ +// Copyright IBM Corp. 2025 +// SPDX-License-Identifier: MPL-2.0 + +package auth + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/vault-mcp-server/pkg/client" + "github.com/hashicorp/vault-mcp-server/pkg/utils" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + log "github.com/sirupsen/logrus" +) + +// IntrospectSelf creates a tool that returns caller token details and self entity data together. +func IntrospectSelf(logger *log.Logger) server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool("introspect_self", + mcp.WithDescription("Introspect current Vault identity by combining auth/token/lookup-self and identity/entity/id/:entity_id (when present). Useful for policy/template-aware access analysis."), + mcp.WithToolAnnotation( + mcp.ToolAnnotation{ + IdempotentHint: utils.ToBoolPtr(true), + ReadOnlyHint: utils.ToBoolPtr(true), + }, + ), + mcp.WithString("namespace", + mcp.DefaultString(""), + mcp.Description("Namespace path (for example 'admin/'). Defaults to current namespace.")), + ), + Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return introspectSelfHandler(ctx, req, logger) + }, + } +} + +func introspectSelfHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) { + args, ok := req.Params.Arguments.(map[string]interface{}) + if !ok { + return mcp.NewToolResultError("Missing or invalid arguments format"), nil + } + + namespace, _ := args["namespace"].(string) + + vault, err := client.GetVaultClientFromContext(ctx, logger) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to get Vault client: %v", err)), nil + } + + nsClient := vault + if namespace != "" { + nsClient = vault.WithNamespace(namespace) + } + + lookupSecret, err := nsClient.Logical().Read("auth/token/lookup-self") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to lookup self token: %v", err)), nil + } + if lookupSecret == nil || lookupSecret.Data == nil { + return mcp.NewToolResultError("No token lookup data returned"), nil + } + + entityID, _ := lookupSecret.Data["entity_id"].(string) + result := map[string]interface{}{ + "namespace": namespace, + "token": lookupSecret.Data, + "entity_id": entityID, + } + + if entityID != "" { + entityPath := fmt.Sprintf("identity/entity/id/%s", entityID) + entitySecret, entityErr := nsClient.Logical().Read(entityPath) + if entityErr != nil { + result["entity_error"] = entityErr.Error() + } else if entitySecret == nil || entitySecret.Data == nil { + result["entity_error"] = "entity not found" + } else { + result["entity"] = entitySecret.Data + } + } + + jsonResult, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonResult)), nil +} diff --git a/pkg/tools/auth/lookup_self.go b/pkg/tools/auth/lookup_self.go new file mode 100644 index 0000000..1de7084 --- /dev/null +++ b/pkg/tools/auth/lookup_self.go @@ -0,0 +1,76 @@ +// Copyright IBM Corp. 2025 +// SPDX-License-Identifier: MPL-2.0 + +package auth + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/vault-mcp-server/pkg/client" + "github.com/hashicorp/vault-mcp-server/pkg/utils" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + log "github.com/sirupsen/logrus" +) + +// LookupSelf creates a tool for reading details about the caller token. +func LookupSelf(logger *log.Logger) server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool("lookup_self", + mcp.WithDescription("Look up details about the current Vault token (auth/token/lookup-self), including policies, entity_id, display_name, TTL, and metadata."), + mcp.WithToolAnnotation( + mcp.ToolAnnotation{ + IdempotentHint: utils.ToBoolPtr(true), + ReadOnlyHint: utils.ToBoolPtr(true), + }, + ), + mcp.WithString("namespace", + mcp.DefaultString(""), + mcp.Description("Namespace path (for example 'admin/'). Defaults to current namespace.")), + ), + Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return lookupSelfHandler(ctx, req, logger) + }, + } +} + +func lookupSelfHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) { + args, ok := req.Params.Arguments.(map[string]interface{}) + if !ok { + return mcp.NewToolResultError("Missing or invalid arguments format"), nil + } + + namespace, _ := args["namespace"].(string) + + vault, err := client.GetVaultClientFromContext(ctx, logger) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to get Vault client: %v", err)), nil + } + + nsClient := vault + if namespace != "" { + nsClient = vault.WithNamespace(namespace) + } + + secret, err := nsClient.Logical().Read("auth/token/lookup-self") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to lookup self token: %v", err)), nil + } + if secret == nil || secret.Data == nil { + return mcp.NewToolResultError("No token lookup data returned"), nil + } + + result := map[string]interface{}{ + "namespace": namespace, + "data": secret.Data, + } + + jsonResult, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonResult)), nil +} diff --git a/pkg/tools/identity/read_entity_self.go b/pkg/tools/identity/read_entity_self.go new file mode 100644 index 0000000..c7ea305 --- /dev/null +++ b/pkg/tools/identity/read_entity_self.go @@ -0,0 +1,91 @@ +// Copyright IBM Corp. 2025 +// SPDX-License-Identifier: MPL-2.0 + +package identity + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/vault-mcp-server/pkg/client" + "github.com/hashicorp/vault-mcp-server/pkg/utils" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + log "github.com/sirupsen/logrus" +) + +// ReadEntitySelf creates a tool for reading the caller's identity entity. +func ReadEntitySelf(logger *log.Logger) server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool("read_entity_self", + mcp.WithDescription("Read the Vault identity entity associated with the current token by resolving entity_id from auth/token/lookup-self. Includes entity metadata and aliases."), + mcp.WithToolAnnotation( + mcp.ToolAnnotation{ + IdempotentHint: utils.ToBoolPtr(true), + ReadOnlyHint: utils.ToBoolPtr(true), + }, + ), + mcp.WithString("namespace", + mcp.DefaultString(""), + mcp.Description("Namespace path (for example 'admin/'). Defaults to current namespace.")), + ), + Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return readEntitySelfHandler(ctx, req, logger) + }, + } +} + +func readEntitySelfHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) { + args, ok := req.Params.Arguments.(map[string]interface{}) + if !ok { + return mcp.NewToolResultError("Missing or invalid arguments format"), nil + } + + namespace, _ := args["namespace"].(string) + + vault, err := client.GetVaultClientFromContext(ctx, logger) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to get Vault client: %v", err)), nil + } + + nsClient := vault + if namespace != "" { + nsClient = vault.WithNamespace(namespace) + } + + lookupSecret, err := nsClient.Logical().Read("auth/token/lookup-self") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to lookup self token: %v", err)), nil + } + if lookupSecret == nil || lookupSecret.Data == nil { + return mcp.NewToolResultError("No token lookup data returned"), nil + } + + entityID, _ := lookupSecret.Data["entity_id"].(string) + if entityID == "" { + return mcp.NewToolResultError("Current token has no entity_id"), nil + } + + entityPath := fmt.Sprintf("identity/entity/id/%s", entityID) + entitySecret, err := nsClient.Logical().Read(entityPath) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to read entity for self token: %v", err)), nil + } + if entitySecret == nil || entitySecret.Data == nil { + return mcp.NewToolResultError("Self entity not found"), nil + } + + result := map[string]interface{}{ + "namespace": namespace, + "entity_id": entityID, + "entity": entitySecret.Data, + } + + jsonResult, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonResult)), nil +} diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index 0a92838..fd84b4f 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -4,6 +4,8 @@ package tools import ( + "github.com/hashicorp/vault-mcp-server/pkg/tools/auth" + "github.com/hashicorp/vault-mcp-server/pkg/tools/identity" "github.com/hashicorp/vault-mcp-server/pkg/tools/kv" "github.com/hashicorp/vault-mcp-server/pkg/tools/pki" "github.com/hashicorp/vault-mcp-server/pkg/tools/sys" @@ -23,6 +25,15 @@ func InitTools(hcServer *server.MCPServer, logger *log.Logger) { deleteMountTool := sys.DeleteMount(logger) hcServer.AddTool(deleteMountTool.Tool, deleteMountTool.Handler) + lookupSelfTool := auth.LookupSelf(logger) + hcServer.AddTool(lookupSelfTool.Tool, lookupSelfTool.Handler) + + introspectSelfTool := auth.IntrospectSelf(logger) + hcServer.AddTool(introspectSelfTool.Tool, introspectSelfTool.Handler) + + readEntitySelfTool := identity.ReadEntitySelf(logger) + hcServer.AddTool(readEntitySelfTool.Tool, readEntitySelfTool.Handler) + // Tools for KV secrets management listSecretsTool := kv.ListSecrets(logger) hcServer.AddTool(listSecretsTool.Tool, listSecretsTool.Handler)