Introspection API Implementation for Router Struct (#17789)
* OSS Commit from ENT for Introspection API * Add changelog
This commit is contained in:
parent
419ba9159c
commit
2d3775a93b
|
@ -63,6 +63,7 @@ var sudoPaths = map[string]*regexp.Regexp{
|
||||||
"/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`),
|
"/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`),
|
||||||
"/sys/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/revoke-prefix/.+$`),
|
"/sys/revoke-prefix/{prefix}": regexp.MustCompile(`^/sys/revoke-prefix/.+$`),
|
||||||
"/sys/rotate": regexp.MustCompile(`^/sys/rotate$`),
|
"/sys/rotate": regexp.MustCompile(`^/sys/rotate$`),
|
||||||
|
"/sys/internal/inspect/router/{tag}": regexp.MustCompile(`^/sys/internal/inspect/router/.+$`),
|
||||||
|
|
||||||
// enterprise-only paths
|
// enterprise-only paths
|
||||||
"/sys/replication/dr/primary/secondary-token": regexp.MustCompile(`^/sys/replication/dr/primary/secondary-token$`),
|
"/sys/replication/dr/primary/secondary-token": regexp.MustCompile(`^/sys/replication/dr/primary/secondary-token$`),
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
sys/internal/inspect: Creates an endpoint to look to inspect internal subsystems.
|
||||||
|
```
|
|
@ -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{}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -113,6 +113,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
||||||
"leases/lookup/*",
|
"leases/lookup/*",
|
||||||
"storage/raft/snapshot-auto/config/*",
|
"storage/raft/snapshot-auto/config/*",
|
||||||
"leases",
|
"leases",
|
||||||
|
"internal/inspect/*",
|
||||||
},
|
},
|
||||||
|
|
||||||
Unauthenticated: []string{
|
Unauthenticated: []string{
|
||||||
|
@ -4274,6 +4275,20 @@ func (b *SystemBackend) pathInternalCountersEntities(ctx context.Context, req *l
|
||||||
return resp, nil
|
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) {
|
func (b *SystemBackend) pathInternalUIResultantACL(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
if req.ClientToken == "" {
|
if req.ClientToken == "" {
|
||||||
// 204 -- no ACL
|
// 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.",
|
||||||
"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": {
|
"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.",
|
||||||
`Information about the host instance that this Vault server is running on.
|
`Information about the host instance that this Vault server is running on.
|
||||||
|
|
|
@ -1063,6 +1063,23 @@ func (b *SystemBackend) internalPaths() []*framework.Path {
|
||||||
HelpSynopsis: strings.TrimSpace(sysHelp["internal-counters-entities"][0]),
|
HelpSynopsis: strings.TrimSpace(sysHelp["internal-counters-entities"][0]),
|
||||||
HelpDescription: strings.TrimSpace(sysHelp["internal-counters-entities"][1]),
|
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]),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ func TestSystemBackend_RootPaths(t *testing.T) {
|
||||||
"leases/lookup/*",
|
"leases/lookup/*",
|
||||||
"storage/raft/snapshot-auto/config/*",
|
"storage/raft/snapshot-auto/config/*",
|
||||||
"leases",
|
"leases",
|
||||||
|
"internal/inspect/*",
|
||||||
}
|
}
|
||||||
|
|
||||||
b := testSystemBackend(t)
|
b := testSystemBackend(t)
|
||||||
|
|
|
@ -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) {
|
func (c *Core) decodeMountTable(ctx context.Context, raw []byte) (*MountTable, error) {
|
||||||
// Decode into mount table
|
// Decode into mount table
|
||||||
mountTable := new(MountTable)
|
mountTable := new(MountTable)
|
||||||
|
|
|
@ -94,6 +94,43 @@ func (r *Router) reset() {
|
||||||
r.mountAccessorCache = radix.New()
|
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
|
// ValidateMountByAccessor returns the mount type and ID for a given mount
|
||||||
// accessor
|
// accessor
|
||||||
func (r *Router) ValidateMountByAccessor(accessor string) *ValidateMountResponse {
|
func (r *Router) ValidateMountByAccessor(accessor string) *ValidateMountResponse {
|
||||||
|
|
Loading…
Reference in New Issue