mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
VAULT-35079: Scaffolding for recover operations on backends (#30556)
* allow snapshot read, recover operation, snapshot ID in request * lint and add comment
This commit is contained in:
parent
23ab4d924c
commit
1c37b94d65
10 changed files with 737 additions and 552 deletions
|
|
@ -174,6 +174,14 @@ type Paths struct {
|
|||
//
|
||||
// For more details, consult limits/registry.go.
|
||||
Limited []string
|
||||
|
||||
// AllowSnapshotRead paths are API paths that are allowed to be read from
|
||||
// a loaded snapshot. These can't be regular expressions, it is either an
|
||||
// exact match, a prefix match and/or a wildcard match.
|
||||
// For prefix match, append '*' as a suffix.
|
||||
// For a wildcard match, use '+' in the segment to match any identifier
|
||||
// (e.g. 'foo/+/bar'). Note that '+' can't be adjacent to a non-slash.
|
||||
AllowSnapshotRead []string
|
||||
}
|
||||
|
||||
type Auditor interface {
|
||||
|
|
|
|||
|
|
@ -263,6 +263,10 @@ type Request struct {
|
|||
|
||||
// RequestLimiterDisabled tells whether the request context has Request Limiter applied.
|
||||
RequestLimiterDisabled bool `json:"request_limiter_disabled,omitempty"`
|
||||
|
||||
// RequiresSnapshotID holds a loaded snapshot ID that the request will use,
|
||||
// for either a read, list, or recover operation
|
||||
RequiresSnapshotID string `json:"snapshot_id,omitempty"`
|
||||
}
|
||||
|
||||
// Clone returns a deep copy (almost) of the request.
|
||||
|
|
@ -400,6 +404,13 @@ func (r *Request) SetTokenEntry(te *TokenEntry) {
|
|||
r.tokenEntry = te
|
||||
}
|
||||
|
||||
// IsSnapshotReadOrList checks whether the request reads or lists from a
|
||||
// snapshot. When this method returns true, handling the request should not
|
||||
// modify any internal caches or state
|
||||
func (r *Request) IsSnapshotReadOrList() bool {
|
||||
return (r.Operation == ReadOperation || r.Operation == ListOperation) && r.RequiresSnapshotID != ""
|
||||
}
|
||||
|
||||
// RenewRequest creates the structure of the renew request.
|
||||
func RenewRequest(path string, secret *Secret, data map[string]interface{}) *Request {
|
||||
return &Request{
|
||||
|
|
@ -455,6 +466,7 @@ const (
|
|||
AliasLookaheadOperation = "alias-lookahead"
|
||||
ResolveRoleOperation = "resolve-role"
|
||||
HeaderOperation = "header"
|
||||
RecoverOperation = "recover"
|
||||
|
||||
// The operations below are called globally, the path is less relevant.
|
||||
RevokeOperation Operation = "revoke"
|
||||
|
|
@ -611,3 +623,24 @@ func CtxRedactionSettingsValue(ctx context.Context) (redactVersion, redactAddres
|
|||
func CreateContextRedactionSettings(parent context.Context, redactVersion, redactAddresses, redactClusterName bool) context.Context {
|
||||
return context.WithValue(parent, ctxKeyRedactionSettings{}, []bool{redactVersion, redactAddresses, redactClusterName})
|
||||
}
|
||||
|
||||
type ctxKeySnapshotID struct{}
|
||||
|
||||
func (c ctxKeySnapshotID) String() string {
|
||||
return "snapshot-id"
|
||||
}
|
||||
|
||||
// CreateContextWithSnapshotID creates a new context indicating that any storage
|
||||
// operations should be done using the given snapshot ID. If the value is empty,
|
||||
// it means that the request should use the normal storage.
|
||||
func CreateContextWithSnapshotID(parent context.Context, value string) context.Context {
|
||||
return context.WithValue(parent, ctxKeySnapshotID{}, value)
|
||||
}
|
||||
|
||||
// ContextSnapshotIDValue retrieves the snapshot ID value stored in the context.
|
||||
// This value can be empty, indicating that the request should use the normal
|
||||
// storage.
|
||||
func ContextSnapshotIDValue(ctx context.Context) (value string, ok bool) {
|
||||
value, ok = ctx.Value(ctxKeySnapshotID{}).(string)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,8 @@ func (b *backendGRPCPluginClient) SpecialPaths() *logical.Paths {
|
|||
LocalStorage: reply.Paths.LocalStorage,
|
||||
SealWrapStorage: reply.Paths.SealWrapStorage,
|
||||
WriteForwardedStorage: reply.Paths.WriteForwardedStorage,
|
||||
Limited: reply.Paths.Limited,
|
||||
AllowSnapshotRead: reply.Paths.AllowSnapshotRead,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -68,6 +68,11 @@ message Paths {
|
|||
//
|
||||
// See note in /sdk/logical/logical.go.
|
||||
repeated string limited = 7;
|
||||
|
||||
// AllowSnapshotRead paths are allowed to be read from a loaded snapshot
|
||||
//
|
||||
// See note in /sdk/logical/logical.go
|
||||
repeated string allow_snapshot_read = 8;
|
||||
}
|
||||
|
||||
message Request {
|
||||
|
|
@ -156,6 +161,10 @@ message Request {
|
|||
// inspect the connection information and potentially use it for
|
||||
// authentication/protection.
|
||||
Connection connection = 20;
|
||||
|
||||
// RequiresSnapshotID will be present when the request is a list, read, or
|
||||
// recover from snapshot operation
|
||||
string requires_snapshot_id = 21;
|
||||
}
|
||||
|
||||
message Auth {
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ func LogicalRequestToProtoRequest(r *logical.Request) (*Request, error) {
|
|||
EntityID: r.EntityID,
|
||||
PolicyOverride: r.PolicyOverride,
|
||||
Unauthenticated: r.Unauthenticated,
|
||||
RequiresSnapshotID: r.RequiresSnapshotID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -315,6 +316,7 @@ func ProtoRequestToLogicalRequest(r *Request) (*logical.Request, error) {
|
|||
EntityID: r.EntityID,
|
||||
PolicyOverride: r.PolicyOverride,
|
||||
Unauthenticated: r.Unauthenticated,
|
||||
RequiresSnapshotID: r.RequiresSnapshotID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ func TestTranslation_Request(t *testing.T) {
|
|||
PeerCertificates: certs,
|
||||
},
|
||||
},
|
||||
RequiresSnapshotID: "abcd",
|
||||
},
|
||||
{
|
||||
ID: "ID",
|
||||
|
|
@ -170,6 +171,7 @@ func TestTranslation_Request(t *testing.T) {
|
|||
EntityID: "tester",
|
||||
PolicyOverride: true,
|
||||
Unauthenticated: true,
|
||||
RequiresSnapshotID: "abcd",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,14 +63,15 @@ type routeEntry struct {
|
|||
tainted atomic.Bool
|
||||
// backend is the actual backend instance for this route entry; lock l must
|
||||
// be held to access this field.
|
||||
backend logical.Backend
|
||||
mountEntry *MountEntry
|
||||
storageView logical.Storage
|
||||
storagePrefix string
|
||||
rootPaths atomic.Value
|
||||
loginPaths atomic.Value
|
||||
binaryPaths atomic.Value
|
||||
limitedPaths atomic.Value
|
||||
backend logical.Backend
|
||||
mountEntry *MountEntry
|
||||
storageView logical.Storage
|
||||
storagePrefix string
|
||||
rootPaths atomic.Value
|
||||
loginPaths atomic.Value
|
||||
binaryPaths atomic.Value
|
||||
limitedPaths atomic.Value
|
||||
allowSnapshotReadPaths atomic.Value
|
||||
// l is the lock used to protect access to backend during reloads
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
|
@ -89,7 +90,8 @@ type specialPathsEntry struct {
|
|||
}
|
||||
|
||||
// specialPathsLookupFunc is used by (*Router).specialPath to look up a
|
||||
// specialPathsEntry corresponding to loginPath, binaryPath, or limitedPath.
|
||||
// specialPathsEntry corresponding to loginPath, binaryPath, limitedPath, or
|
||||
// allowSnapshotReadPath.
|
||||
type specialPathsLookupFunc func(re *routeEntry) *specialPathsEntry
|
||||
|
||||
type ValidateMountResponse struct {
|
||||
|
|
@ -225,6 +227,12 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount
|
|||
}
|
||||
re.limitedPaths.Store(limitedPathsEntry)
|
||||
|
||||
allowSnapshotReadPathsEntry, err := parseUnauthenticatedPaths(paths.AllowSnapshotRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
re.allowSnapshotReadPaths.Store(allowSnapshotReadPathsEntry)
|
||||
|
||||
switch {
|
||||
case prefix == "":
|
||||
return fmt.Errorf("missing prefix to be used for router entry; mount_path: %q, mount_type: %q", re.mountEntry.Path, re.mountEntry.Type)
|
||||
|
|
@ -915,9 +923,18 @@ func (r *Router) LimitedPath(ctx context.Context, path string) bool {
|
|||
})
|
||||
}
|
||||
|
||||
// AllowSnapshotReadPath checks if the given path is allowed to be used for a
|
||||
// snapshot read
|
||||
func (r *Router) AllowSnapshotReadPath(ctx context.Context, path string) bool {
|
||||
return r.specialPath(ctx, path,
|
||||
func(re *routeEntry) *specialPathsEntry {
|
||||
return re.allowSnapshotReadPaths.Load().(*specialPathsEntry)
|
||||
})
|
||||
}
|
||||
|
||||
// specialPath is a common method for checking if the given path has a matching
|
||||
// PathsSpecial entry. This is used for Login, Binary, and Limited PathsSpecial
|
||||
// fields.
|
||||
// PathsSpecial entry. This is used for Login, Binary, Limited, and
|
||||
// AllowSnapshotRead PathsSpecial fields.
|
||||
// Matching Priority
|
||||
// 1. prefix
|
||||
// 2. exact
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRouter_Mount(t *testing.T) {
|
||||
|
|
@ -683,3 +684,84 @@ func TestWellKnownRedirectMatching(t *testing.T) {
|
|||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouter_AllowSnapshotReadPaths mounts a backend with a set of paths that
|
||||
// are allowed to be read from a snapshot. The test verifies that paths that
|
||||
// match the allowed paths return true, while paths that do not match return
|
||||
// false from the router.
|
||||
func TestRouter_AllowSnapshotReadPaths(t *testing.T) {
|
||||
r := NewRouter()
|
||||
_, barrier, _ := mockBarrier(t)
|
||||
view := NewBarrierView(barrier, "logical/")
|
||||
|
||||
meUUID, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := namespace.RootContext(context.Background())
|
||||
n := &NoopBackend{
|
||||
AllowSnapshotRead: []string{
|
||||
"config/root",
|
||||
"roles/+",
|
||||
"roles/+/status",
|
||||
"static-creds/+",
|
||||
"static-creds/+/sub",
|
||||
},
|
||||
BackendType: logical.TypeCredential,
|
||||
}
|
||||
err = r.Mount(n, "foo/", &MountEntry{UUID: meUUID, Accessor: "fooaccessor", NamespaceID: namespace.RootNamespaceID, namespace: namespace.RootNamespace}, view)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
queryPath string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
queryPath: "config",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
queryPath: "config/root",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
queryPath: "config/root/key",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
queryPath: "roles/name",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
queryPath: "roles/name/status",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
queryPath: "roles/name/status/sub",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
queryPath: "static-creds/name",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
queryPath: "static-creds/name/sub",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
queryPath: "static-creds/name/more/sub",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
queryPath: "creds/name",
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(strings.ReplaceAll(tc.queryPath, "/", "_"), func(t *testing.T) {
|
||||
require.Equal(t, tc.expect, r.AllowSnapshotReadPath(ctx, "foo/"+tc.queryPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,17 @@ type RouterTestHandlerFunc func(context.Context, *logical.Request) (*logical.Res
|
|||
type NoopBackend struct {
|
||||
sync.Mutex
|
||||
|
||||
Root []string
|
||||
Login []string
|
||||
Paths []string
|
||||
Requests []*logical.Request
|
||||
Response *logical.Response
|
||||
RequestHandler RouterTestHandlerFunc
|
||||
Invalidations []string
|
||||
DefaultLeaseTTL time.Duration
|
||||
MaxLeaseTTL time.Duration
|
||||
BackendType logical.BackendType
|
||||
Root []string
|
||||
Login []string
|
||||
Paths []string
|
||||
AllowSnapshotRead []string
|
||||
Requests []*logical.Request
|
||||
Response *logical.Response
|
||||
RequestHandler RouterTestHandlerFunc
|
||||
Invalidations []string
|
||||
DefaultLeaseTTL time.Duration
|
||||
MaxLeaseTTL time.Duration
|
||||
BackendType logical.BackendType
|
||||
|
||||
RollbackErrs bool
|
||||
}
|
||||
|
|
@ -79,8 +80,9 @@ func (n *NoopBackend) HandleExistenceCheck(ctx context.Context, req *logical.Req
|
|||
|
||||
func (n *NoopBackend) SpecialPaths() *logical.Paths {
|
||||
return &logical.Paths{
|
||||
Root: n.Root,
|
||||
Unauthenticated: n.Login,
|
||||
Root: n.Root,
|
||||
Unauthenticated: n.Login,
|
||||
AllowSnapshotRead: n.AllowSnapshotRead,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue