diff --git a/api/plugin_helpers.go b/api/plugin_helpers.go index 2b1b35c3b..6602a044b 100644 --- a/api/plugin_helpers.go +++ b/api/plugin_helpers.go @@ -63,6 +63,7 @@ var sudoPaths = map[string]*regexp.Regexp{ "/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`), "/sys/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/revoke-prefix/.+$`), "/sys/rotate": regexp.MustCompile(`^/sys/rotate$`), + "/sys/internal/inspect/router/{tag}": regexp.MustCompile(`^/sys/internal/inspect/router/.+$`), // enterprise-only paths "/sys/replication/dr/primary/secondary-token": regexp.MustCompile(`^/sys/replication/dr/primary/secondary-token$`), diff --git a/changelog/17789.txt b/changelog/17789.txt new file mode 100644 index 000000000..fd6f3b088 --- /dev/null +++ b/changelog/17789.txt @@ -0,0 +1,3 @@ +```release-note:improvement +sys/internal/inspect: Creates an endpoint to look to inspect internal subsystems. +``` \ No newline at end of file diff --git a/vault/inspectable.go b/vault/inspectable.go new file mode 100644 index 000000000..9e66c123e --- /dev/null +++ b/vault/inspectable.go @@ -0,0 +1,11 @@ +package vault + +type Inspectable interface { + // Returns a record view of a particular subsystem + GetRecords(tag string) ([]map[string]interface{}, error) +} + +type Deserializable interface { + // Converts a structure into a consummable map + Deserialize() map[string]interface{} +} diff --git a/vault/inspectable_test.go b/vault/inspectable_test.go new file mode 100644 index 000000000..e519cfb99 --- /dev/null +++ b/vault/inspectable_test.go @@ -0,0 +1,78 @@ +package vault + +import ( + "strings" + "testing" + + "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/sdk/logical" +) + +func TestInspectRouter(t *testing.T) { + // Verify that all the expected tables exist when we inspect the router + c, _, root := TestCoreUnsealed(t) + + rootCtx := namespace.RootContext(nil) + subTrees := map[string][]string{ + "routeEntry": {"root", "storage"}, + "mountEntry": {"uuid", "accessor"}, + } + subTreeFields := map[string][]string{ + "routeEntry": {"tainted", "storage_prefix", "accessor", "mount_namespace", "mount_path", "mount_type", "uuid"}, + "mountEntry": {"accessor", "mount_namespace", "mount_path", "mount_type", "uuid"}, + } + for subTreeType, subTreeArray := range subTrees { + for _, tag := range subTreeArray { + resp, err := c.HandleRequest(rootCtx, &logical.Request{ + ClientToken: root, + Operation: logical.ReadOperation, + Path: "sys/internal/inspect/router/" + tag, + }) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: resp: %#v\n err: %v", resp, err) + } + // Verify that data exists + data, ok := resp.Data[tag].([]map[string]interface{}) + if !ok { + t.Fatalf("Router data is malformed") + } + for _, entry := range data { + for _, field := range subTreeFields[subTreeType] { + if _, ok := entry[field]; !ok { + t.Fatalf("Field %s not found in %s", field, tag) + } + } + } + + } + } +} + +func TestInvalidInspectRouterPath(t *testing.T) { + // Verify that attempting to inspect an invalid tree in the router fails + core, _, rootToken := testCoreSystemBackend(t) + rootCtx := namespace.RootContext(nil) + _, err := core.HandleRequest(rootCtx, &logical.Request{ + ClientToken: rootToken, + Operation: logical.ReadOperation, + Path: "sys/internal/inspect/router/random", + }) + if !strings.Contains(err.Error(), logical.ErrUnsupportedPath.Error()) { + t.Fatal("expected unsupported path error") + } +} + +func TestInspectAPISudoProtect(t *testing.T) { + // Verify that the Inspect API path is sudo protected + core, _, rootToken := testCoreSystemBackend(t) + testMakeServiceTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"secret"}) + rootCtx := namespace.RootContext(nil) + _, err := core.HandleRequest(rootCtx, &logical.Request{ + ClientToken: "tokenid", + Operation: logical.ReadOperation, + Path: "sys/internal/inspect/router/root", + }) + if !strings.Contains(err.Error(), logical.ErrPermissionDenied.Error()) { + t.Fatal("expected permission denied error") + } +} diff --git a/vault/logical_system.go b/vault/logical_system.go index a6e6e4188..d67ce5f2f 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -113,6 +113,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { "leases/lookup/*", "storage/raft/snapshot-auto/config/*", "leases", + "internal/inspect/*", }, Unauthenticated: []string{ @@ -4274,6 +4275,20 @@ func (b *SystemBackend) pathInternalCountersEntities(ctx context.Context, req *l return resp, nil } +func (b *SystemBackend) pathInternalInspectRouter(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + tag := d.Get("tag").(string) + inspectableRouter, err := b.Core.router.GetRecords(tag) + if err != nil { + return nil, err + } + resp := &logical.Response{ + Data: map[string]interface{}{ + tag: inspectableRouter, + }, + } + return resp, nil +} + func (b *SystemBackend) pathInternalUIResultantACL(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { if req.ClientToken == "" { // 204 -- no ACL @@ -5686,6 +5701,14 @@ This path responds to the following HTTP methods. "Count of active entities in this Vault cluster.", "Count of active entities in this Vault cluster.", }, + "internal-inspect-router": { + "Information on the entries in each of the trees in the router. Inspectable trees are uuid, accessor, storage, and root.", + ` +This path responds to the following HTTP methods. + GET / + Returns a list of entries in specified table + `, + }, "host-info": { "Information about the host instance that this Vault server is running on.", `Information about the host instance that this Vault server is running on. diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index be774673f..fbeb9d541 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -1063,6 +1063,23 @@ func (b *SystemBackend) internalPaths() []*framework.Path { HelpSynopsis: strings.TrimSpace(sysHelp["internal-counters-entities"][0]), HelpDescription: strings.TrimSpace(sysHelp["internal-counters-entities"][1]), }, + { + Pattern: "internal/inspect/router/" + framework.GenericNameRegex("tag"), + Fields: map[string]*framework.FieldSchema{ + "tag": { + Type: framework.TypeString, + Description: "Name of subtree being observed", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: b.pathInternalInspectRouter, + Summary: "Expose the route entry and mount entry tables present in the router", + }, + }, + HelpSynopsis: strings.TrimSpace(sysHelp["internal-inspect-router"][0]), + HelpDescription: strings.TrimSpace(sysHelp["internal-inspect-router"][1]), + }, } } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index a13e1cdf1..a890e1c17 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -63,6 +63,7 @@ func TestSystemBackend_RootPaths(t *testing.T) { "leases/lookup/*", "storage/raft/snapshot-auto/config/*", "leases", + "internal/inspect/*", } b := testSystemBackend(t) diff --git a/vault/mount.go b/vault/mount.go index 5167ffa9c..b3b76396a 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -461,6 +461,16 @@ func (e *MountEntry) SyncCache() { } } +func (entry *MountEntry) Deserialize() map[string]interface{} { + return map[string]interface{}{ + "mount_path": entry.Path, + "mount_namespace": entry.Namespace().Path, + "uuid": entry.UUID, + "accessor": entry.Accessor, + "mount_type": entry.Type, + } +} + func (c *Core) decodeMountTable(ctx context.Context, raw []byte) (*MountTable, error) { // Decode into mount table mountTable := new(MountTable) diff --git a/vault/router.go b/vault/router.go index 96da9c5a1..211c054ef 100644 --- a/vault/router.go +++ b/vault/router.go @@ -94,6 +94,43 @@ func (r *Router) reset() { r.mountAccessorCache = radix.New() } +func (r *Router) GetRecords(tag string) ([]map[string]interface{}, error) { + r.l.RLock() + defer r.l.RUnlock() + var data []map[string]interface{} + var tree *radix.Tree + switch tag { + case "root": + tree = r.root + case "uuid": + tree = r.mountUUIDCache + case "accessor": + tree = r.mountAccessorCache + case "storage": + tree = r.storagePrefix + default: + return nil, logical.ErrUnsupportedPath + } + for _, v := range tree.ToMap() { + info := v.(Deserializable).Deserialize() + data = append(data, info) + } + return data, nil +} + +func (entry *routeEntry) Deserialize() map[string]interface{} { + entry.l.RLock() + defer entry.l.RUnlock() + ret := map[string]interface{}{ + "tainted": entry.tainted, + "storage_prefix": entry.storagePrefix, + } + for k, v := range entry.mountEntry.Deserialize() { + ret[k] = v + } + return ret +} + // ValidateMountByAccessor returns the mount type and ID for a given mount // accessor func (r *Router) ValidateMountByAccessor(accessor string) *ValidateMountResponse {