mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
Add new /sys/well-known interface to get information about registered labels (#25695)
* Add new /sys/well-known interface to get information about registered labels - Add two new interfaces LIST/GET /sys/well-known which will provide a list of keys which are registered labels within the /.well-known space on the local server, along with a detailed info map for each - Add GET /sys/well-known/<label> to get details on a specific registered label - Add docs and tests for the new api endpoints * Add test doc and remove copied comment * Rename returned fields to use snake case * Remove extra newline added when resolving the merge conflict * Apply suggestions from code review Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> --------- Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
"hash"
|
"hash"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -224,6 +225,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
|
|||||||
b.Backend.Paths = append(b.Backend.Paths, b.loginMFAPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.loginMFAPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.experimentPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.experimentPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.introspectionPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.introspectionPaths()...)
|
||||||
|
b.Backend.Paths = append(b.Backend.Paths, b.wellKnownPaths()...)
|
||||||
|
|
||||||
if core.rawEnabled {
|
if core.rawEnabled {
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.rawPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.rawPaths()...)
|
||||||
@@ -6001,6 +6003,62 @@ func (b *SystemBackend) handleReadExperiments(ctx context.Context, _ *logical.Re
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleWellKnownList handles /sys/well-known/ endpoint to provide the list of registered labels
|
||||||
|
// within the /.well-known path on this server
|
||||||
|
func (b *SystemBackend) handleWellKnownList() framework.OperationFunc {
|
||||||
|
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
labels := b.Core.WellKnownRedirects.List()
|
||||||
|
|
||||||
|
respKeys := []string{}
|
||||||
|
respKeyInfo := map[string]interface{}{}
|
||||||
|
|
||||||
|
for label, wk := range labels {
|
||||||
|
respKeys = append(respKeys, label)
|
||||||
|
|
||||||
|
mountPath := ""
|
||||||
|
m := b.Core.router.MatchingMountByUUID(wk.mountUUID)
|
||||||
|
if m != nil {
|
||||||
|
mountPath, _ = url.JoinPath(m.namespace.Path, m.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
respKeyInfo[label] = map[string]interface{}{
|
||||||
|
"mount_path": mountPath,
|
||||||
|
"mount_uuid": wk.mountUUID,
|
||||||
|
"prefix": wk.prefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return logical.ListResponseWithInfo(respKeys, respKeyInfo), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWellKnownRead handles the "/sys/well-known/<name>" endpoint to read a registered well-known label
|
||||||
|
func (b *SystemBackend) handleWellKnownRead() framework.OperationFunc {
|
||||||
|
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
label := data.Get("label").(string)
|
||||||
|
|
||||||
|
wk, ok := b.Core.WellKnownRedirects.Get(label)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mountPath := ""
|
||||||
|
m := b.Core.router.MatchingMountByUUID(wk.mountUUID)
|
||||||
|
if m != nil {
|
||||||
|
mountPath, _ = url.JoinPath(m.namespace.Path, m.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"label": label,
|
||||||
|
"mount_path": mountPath,
|
||||||
|
"mount_uuid": wk.mountUUID,
|
||||||
|
"prefix": wk.prefix,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sanitizePath(path string) string {
|
func sanitizePath(path string) string {
|
||||||
if !strings.HasSuffix(path, "/") {
|
if !strings.HasSuffix(path, "/") {
|
||||||
path += "/"
|
path += "/"
|
||||||
@@ -7005,4 +7063,31 @@ This path responds to the following HTTP methods.
|
|||||||
returns the manual license reporting data.
|
returns the manual license reporting data.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"well-known-list": {
|
||||||
|
`List the registered well-known labels to associated mounts.`,
|
||||||
|
`
|
||||||
|
This path responds to the following HTTP methods.
|
||||||
|
LIST /
|
||||||
|
List the names of the registered labels within the .well-known folder on this server.
|
||||||
|
|
||||||
|
GET /
|
||||||
|
List the names of the registered labels within the .well-known folder on this server.
|
||||||
|
|
||||||
|
GET /<label>
|
||||||
|
Retrieve the information regarding a specific registered label on this server.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"well-known": {
|
||||||
|
`Read the associated mount info for a registered label within the well-known path prefix`,
|
||||||
|
`
|
||||||
|
Retrieve the information regarding a specific registered label on this server.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
"well-known-label": {
|
||||||
|
`The label representing a path-prefix within the /.well-known/ path`,
|
||||||
|
"",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5200,3 +5200,98 @@ func (b *SystemBackend) eventPaths() []*framework.Path {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *SystemBackend) wellKnownPaths() []*framework.Path {
|
||||||
|
return []*framework.Path{
|
||||||
|
{
|
||||||
|
Pattern: "well-known/?$",
|
||||||
|
|
||||||
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
|
OperationPrefix: "well-known",
|
||||||
|
OperationVerb: "list",
|
||||||
|
},
|
||||||
|
|
||||||
|
Operations: map[logical.Operation]framework.OperationHandler{
|
||||||
|
logical.ReadOperation: &framework.PathOperation{
|
||||||
|
Callback: b.handleWellKnownList(),
|
||||||
|
Responses: map[int][]framework.Response{
|
||||||
|
http.StatusOK: {{
|
||||||
|
Description: "OK",
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"keys": {
|
||||||
|
Type: framework.TypeStringSlice,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logical.ListOperation: &framework.PathOperation{
|
||||||
|
Callback: b.handleWellKnownList(),
|
||||||
|
Responses: map[int][]framework.Response{
|
||||||
|
http.StatusOK: {{
|
||||||
|
Description: "OK",
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"keys": {
|
||||||
|
Type: framework.TypeStringSlice,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: strings.TrimSpace(sysHelp["well-known-list"][0]),
|
||||||
|
HelpDescription: strings.TrimSpace(sysHelp["well-known-list"][1]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "well-known/(?P<label>.+)",
|
||||||
|
|
||||||
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
|
OperationPrefix: "well-known",
|
||||||
|
OperationSuffix: "label",
|
||||||
|
},
|
||||||
|
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"label": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: strings.TrimSpace(sysHelp["well-known-label"][0]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Operations: map[logical.Operation]framework.OperationHandler{
|
||||||
|
logical.ReadOperation: &framework.PathOperation{
|
||||||
|
Callback: b.handleWellKnownRead(),
|
||||||
|
Responses: map[int][]framework.Response{
|
||||||
|
http.StatusOK: {{
|
||||||
|
Description: "OK",
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"label": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"mount_uuid": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"mount_path": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Summary: "Retrieve the associated mount information for a registered well-known label.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: strings.TrimSpace(sysHelp["well-known"][0]),
|
||||||
|
HelpDescription: strings.TrimSpace(sysHelp["well-known"][1]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||||
aeadwrapper "github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2"
|
aeadwrapper "github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2"
|
||||||
|
"github.com/hashicorp/go-uuid"
|
||||||
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||||
"github.com/hashicorp/vault/helper/builtinplugins"
|
"github.com/hashicorp/vault/helper/builtinplugins"
|
||||||
"github.com/hashicorp/vault/helper/experiments"
|
"github.com/hashicorp/vault/helper/experiments"
|
||||||
@@ -7023,3 +7024,52 @@ func TestGetSealStatus_RedactionSettings(t *testing.T) {
|
|||||||
assert.Empty(t, resp.BuildDate)
|
assert.Empty(t, resp.BuildDate)
|
||||||
assert.Empty(t, resp.ClusterName)
|
assert.Empty(t, resp.ClusterName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWellKnownSysApi verifies the GET/LIST endpoints of /sys/well-known and /sys/well-known/<label>
|
||||||
|
func TestWellKnownSysApi(t *testing.T) {
|
||||||
|
c, _, _ := TestCoreUnsealed(t)
|
||||||
|
b := c.systemBackend
|
||||||
|
|
||||||
|
myUuid, err := uuid.GenerateUUID()
|
||||||
|
require.NoError(t, err, "failed generating uuid")
|
||||||
|
|
||||||
|
err = c.WellKnownRedirects.TryRegister(context.Background(), c, myUuid, "mylabel1", "test-path/foo1")
|
||||||
|
require.NoError(t, err, "failed registering well-known redirect 1")
|
||||||
|
|
||||||
|
err = c.WellKnownRedirects.TryRegister(context.Background(), c, myUuid, "mylabel2", "test-path/foo2")
|
||||||
|
require.NoError(t, err, "failed registering well-known redirect 2")
|
||||||
|
|
||||||
|
// Test LIST operation
|
||||||
|
req := logical.TestRequest(t, logical.ListOperation, "well-known")
|
||||||
|
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
require.NoError(t, err, "failed list well-known request")
|
||||||
|
require.NotNil(t, resp, "response from list well-known request was nil")
|
||||||
|
|
||||||
|
require.Contains(t, resp.Data["keys"], "mylabel1")
|
||||||
|
require.Contains(t, resp.Data["keys"], "mylabel2")
|
||||||
|
require.Len(t, resp.Data["keys"], 2)
|
||||||
|
|
||||||
|
keyInfo := resp.Data["key_info"].(map[string]interface{})
|
||||||
|
keyInfoLabel1 := keyInfo["mylabel1"].(map[string]interface{})
|
||||||
|
require.Equal(t, myUuid, keyInfoLabel1["mount_uuid"])
|
||||||
|
require.Equal(t, "test-path/foo1", keyInfoLabel1["prefix"])
|
||||||
|
|
||||||
|
keyInfoLabel2 := keyInfo["mylabel2"].(map[string]interface{})
|
||||||
|
require.Equal(t, myUuid, keyInfoLabel2["mount_uuid"])
|
||||||
|
require.Equal(t, "test-path/foo2", keyInfoLabel2["prefix"])
|
||||||
|
|
||||||
|
// Test GET operation
|
||||||
|
req = logical.TestRequest(t, logical.ReadOperation, "well-known/mylabel1")
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
require.NoError(t, err, "failed get well-known request")
|
||||||
|
require.NotNil(t, resp, "response from get well-known request was nil")
|
||||||
|
|
||||||
|
require.Equal(t, myUuid, resp.Data["mount_uuid"])
|
||||||
|
require.Equal(t, "test-path/foo1", resp.Data["prefix"])
|
||||||
|
|
||||||
|
// Test GET operation on unknown label
|
||||||
|
req = logical.TestRequest(t, logical.ReadOperation, "well-known/mylabel1-unknown")
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
require.NoError(t, err, "failed get well-known request")
|
||||||
|
require.Nil(t, resp, "response from unknown should have been nil was %v", resp)
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,14 +15,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type wellKnownRedirect struct {
|
type wellKnownRedirect struct {
|
||||||
c *Core
|
c *Core
|
||||||
mountUUID string
|
mountUUID string
|
||||||
prefix string
|
prefix string
|
||||||
isPrefixMatch bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type wellKnownRedirectRegistry struct {
|
type wellKnownRedirectRegistry struct {
|
||||||
lock sync.Mutex
|
lock sync.RWMutex
|
||||||
paths *radix.Tree
|
paths *radix.Tree
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +53,9 @@ func (reg *wellKnownRedirectRegistry) TryRegister(ctx context.Context, core *Cor
|
|||||||
|
|
||||||
// Find any relevant redirects for a given source path
|
// Find any relevant redirects for a given source path
|
||||||
func (reg *wellKnownRedirectRegistry) Find(path string) (*wellKnownRedirect, string) {
|
func (reg *wellKnownRedirectRegistry) Find(path string) (*wellKnownRedirect, string) {
|
||||||
|
reg.lock.RLock()
|
||||||
|
defer reg.lock.RUnlock()
|
||||||
|
|
||||||
s, a, found := reg.paths.LongestPrefix(path)
|
s, a, found := reg.paths.LongestPrefix(path)
|
||||||
if found {
|
if found {
|
||||||
remaining := strings.TrimPrefix(path, s)
|
remaining := strings.TrimPrefix(path, s)
|
||||||
@@ -107,6 +109,32 @@ func (reg *wellKnownRedirectRegistry) DeregisterSource(mountUuid, src string) bo
|
|||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns a map keyed by the registered label and associated well known redirect
|
||||||
|
func (reg *wellKnownRedirectRegistry) List() map[string]*wellKnownRedirect {
|
||||||
|
reg.lock.RLock()
|
||||||
|
defer reg.lock.RUnlock()
|
||||||
|
|
||||||
|
labels := map[string]*wellKnownRedirect{}
|
||||||
|
reg.paths.Walk(func(s string, v interface{}) bool {
|
||||||
|
labels[s] = v.(*wellKnownRedirect)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a well known redirect for a specific registered label
|
||||||
|
func (reg *wellKnownRedirectRegistry) Get(label string) (*wellKnownRedirect, bool) {
|
||||||
|
reg.lock.RLock()
|
||||||
|
defer reg.lock.RUnlock()
|
||||||
|
|
||||||
|
if v, ok := reg.paths.Get(label); ok {
|
||||||
|
return v.(*wellKnownRedirect), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
// Construct the full destination of the redirect, including any remaining path past the src
|
// Construct the full destination of the redirect, including any remaining path past the src
|
||||||
func (a *wellKnownRedirect) Destination(remaining string) (string, error) {
|
func (a *wellKnownRedirect) Destination(remaining string) (string, error) {
|
||||||
var destPath string
|
var destPath string
|
||||||
|
|||||||
74
website/content/api-docs/system/well-known.mdx
Normal file
74
website/content/api-docs/system/well-known.mdx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
layout: api
|
||||||
|
page_title: /sys/well-known - HTTP API
|
||||||
|
description: The `/sys/well-known` endpoint is used to list and read registered .well-known labels in Vault.
|
||||||
|
---
|
||||||
|
|
||||||
|
# `/sys/well-known`
|
||||||
|
|
||||||
|
Use the `/sys/well-known` endpoint to list and read registered labels on the `.well-known/` path prefix for your Vault instance.
|
||||||
|
|
||||||
|
## List well-known
|
||||||
|
|
||||||
|
List all registered labels.
|
||||||
|
|
||||||
|
| Method | Path |
|
||||||
|
|:-------|:------------------|
|
||||||
|
| `GET` | `/sys/well-known` |
|
||||||
|
| `LIST` | `/sys/well-known` |
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
http://127.0.0.1:8200/v1/sys/well-known
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key_info": {
|
||||||
|
"est/cacerts": {
|
||||||
|
"mount_path": "ns1/pki_int/",
|
||||||
|
"mount_uuid": "fc9d3ee4-ae92-4e3e-c0e1-a1fdb3e3b8cf",
|
||||||
|
"prefix": "est/cacerts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keys": [
|
||||||
|
"est/cacerts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Read well-known
|
||||||
|
|
||||||
|
Retrieve information for the specified label.
|
||||||
|
|
||||||
|
| Method | Path |
|
||||||
|
| :----- |:-------------------------|
|
||||||
|
| `GET` | `/sys/well-known/:label` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `label` `(string: <required>)` – URL path parameter specifying the registered label to fetch.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
http://127.0.0.1:8200/v1/sys/well-known/est/cacerts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"label": "est/cacerts",
|
||||||
|
"mount_path": "ns1/pki_int/",
|
||||||
|
"mount_uuid": "fc9d3ee4-ae92-4e3e-c0e1-a1fdb3e3b8cf",
|
||||||
|
"prefix": "est/cacerts"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -761,6 +761,10 @@
|
|||||||
"title": "<code>/sys/version-history</code>",
|
"title": "<code>/sys/version-history</code>",
|
||||||
"path": "system/version-history"
|
"path": "system/version-history"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "<code>/sys/well-known</code>",
|
||||||
|
"path": "system/well-known"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "<code>/sys/wrapping/lookup</code>",
|
"title": "<code>/sys/wrapping/lookup</code>",
|
||||||
"path": "system/wrapping-lookup"
|
"path": "system/wrapping-lookup"
|
||||||
|
|||||||
Reference in New Issue
Block a user