6930568076
* Add WriteForwardedStorage to sdk's plugin, logical in OSS This should allow backends to specify paths to forward write (storage.Put(...) and storage.Delete(...)) operations for. Notably, these semantics are subject to change and shouldn't yet be relied on. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Collect paths for write forwarding in OSS This adds a path manager to Core, allowing tracking across all Vault versions of paths which could use write forwarding if available. In particular, even on OSS offerings, we'll need to template {{clusterId}} into the paths, in the event of later upgrading to Enterprise. If we didn't, we'd end up writing paths which will no longer be accessible post-migration, due to write forwarding now replacing the sentinel with the actual cluster identifier. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add forwarded writer implementation to OSS Here, for paths given to us, we determine if we need to do cluster translation and perform local writing. This is the OSS variant. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Wire up mount-specific request forwarding in OSS Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Clarify that state lock needs to be held to call HAState in OSS Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Move cluster sentinel constant to sdk/logical Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Expose ClusterID to Plugins via SystemView This will let plugins learn what the Cluster's ID is, without having to resort to hacks like writing a random string to its cluster-prefixed namespace and then reading it once it has replicated. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add GRPC ClusterID implementation For any external plugins which wish to use it. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
427 lines
12 KiB
Go
427 lines
12 KiB
Go
package vault
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/helper/identity"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/helper/random"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/helper/license"
|
|
"github.com/hashicorp/vault/sdk/helper/pluginutil"
|
|
"github.com/hashicorp/vault/sdk/helper/wrapping"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/hashicorp/vault/version"
|
|
)
|
|
|
|
type ctxKeyForwardedRequestMountAccessor struct{}
|
|
|
|
func (c ctxKeyForwardedRequestMountAccessor) String() string {
|
|
return "forwarded-req-mount-accessor"
|
|
}
|
|
|
|
type dynamicSystemView struct {
|
|
core *Core
|
|
mountEntry *MountEntry
|
|
perfStandby bool
|
|
}
|
|
|
|
type extendedSystemView interface {
|
|
logical.SystemView
|
|
logical.ExtendedSystemView
|
|
// SudoPrivilege won't work over the plugin system so we keep it here
|
|
// instead of in sdk/logical to avoid exposing to plugins
|
|
SudoPrivilege(context.Context, string, string) bool
|
|
}
|
|
|
|
type extendedSystemViewImpl struct {
|
|
dynamicSystemView
|
|
}
|
|
|
|
func (e extendedSystemViewImpl) Auditor() logical.Auditor {
|
|
return genericAuditor{
|
|
mountType: e.mountEntry.Type,
|
|
namespace: e.mountEntry.Namespace(),
|
|
c: e.core,
|
|
}
|
|
}
|
|
|
|
func (e extendedSystemViewImpl) ForwardGenericRequest(ctx context.Context, req *logical.Request) (*logical.Response, error) {
|
|
// Forward the request if allowed
|
|
if couldForward(e.core) {
|
|
ctx = namespace.ContextWithNamespace(ctx, e.mountEntry.Namespace())
|
|
ctx = logical.IndexStateContext(ctx, &logical.WALState{})
|
|
ctx = context.WithValue(ctx, ctxKeyForwardedRequestMountAccessor{}, e.mountEntry.Accessor)
|
|
return forward(ctx, e.core, req)
|
|
}
|
|
|
|
return nil, logical.ErrReadOnly
|
|
}
|
|
|
|
// SudoPrivilege returns true if given path has sudo privileges
|
|
// for the given client token
|
|
func (e extendedSystemViewImpl) SudoPrivilege(ctx context.Context, path string, token string) bool {
|
|
// Resolve the token policy
|
|
te, err := e.core.tokenStore.Lookup(ctx, token)
|
|
if err != nil {
|
|
e.core.logger.Error("failed to lookup sudo token", "error", err)
|
|
return false
|
|
}
|
|
|
|
// Ensure the token is valid
|
|
if te == nil {
|
|
e.core.logger.Error("entry not found for given token")
|
|
return false
|
|
}
|
|
|
|
policyNames := make(map[string][]string)
|
|
// Add token policies
|
|
policyNames[te.NamespaceID] = append(policyNames[te.NamespaceID], te.Policies...)
|
|
|
|
tokenNS, err := NamespaceByID(ctx, te.NamespaceID, e.core)
|
|
if err != nil {
|
|
e.core.logger.Error("failed to lookup token namespace", "error", err)
|
|
return false
|
|
}
|
|
if tokenNS == nil {
|
|
e.core.logger.Error("failed to lookup token namespace", "error", namespace.ErrNoNamespace)
|
|
return false
|
|
}
|
|
|
|
// Add identity policies from all the namespaces
|
|
entity, identityPolicies, err := e.core.fetchEntityAndDerivedPolicies(ctx, tokenNS, te.EntityID, te.NoIdentityPolicies)
|
|
if err != nil {
|
|
e.core.logger.Error("failed to fetch identity policies", "error", err)
|
|
return false
|
|
}
|
|
for nsID, nsPolicies := range identityPolicies {
|
|
policyNames[nsID] = append(policyNames[nsID], nsPolicies...)
|
|
}
|
|
|
|
tokenCtx := namespace.ContextWithNamespace(ctx, tokenNS)
|
|
|
|
// Add the inline policy if it's set
|
|
policies := make([]*Policy, 0)
|
|
if te.InlinePolicy != "" {
|
|
inlinePolicy, err := ParseACLPolicy(tokenNS, te.InlinePolicy)
|
|
if err != nil {
|
|
e.core.logger.Error("failed to parse the token's inline policy", "error", err)
|
|
return false
|
|
}
|
|
policies = append(policies, inlinePolicy)
|
|
}
|
|
|
|
// Construct the corresponding ACL object. Derive and use a new context that
|
|
// uses the req.ClientToken's namespace
|
|
acl, err := e.core.policyStore.ACL(tokenCtx, entity, policyNames, policies...)
|
|
if err != nil {
|
|
e.core.logger.Error("failed to retrieve ACL for token's policies", "token_policies", te.Policies, "error", err)
|
|
return false
|
|
}
|
|
|
|
// The operation type isn't important here as this is run from a path the
|
|
// user has already been given access to; we only care about whether they
|
|
// have sudo. Note that we use root context because the path that comes in
|
|
// must be fully-qualified already so we don't want AllowOperation to
|
|
// prepend a namespace prefix onto it.
|
|
req := new(logical.Request)
|
|
req.Operation = logical.ReadOperation
|
|
req.Path = path
|
|
authResults := acl.AllowOperation(namespace.RootContext(ctx), req, true)
|
|
return authResults.RootPrivs
|
|
}
|
|
|
|
func (d dynamicSystemView) DefaultLeaseTTL() time.Duration {
|
|
def, _ := d.fetchTTLs()
|
|
return def
|
|
}
|
|
|
|
func (d dynamicSystemView) MaxLeaseTTL() time.Duration {
|
|
_, max := d.fetchTTLs()
|
|
return max
|
|
}
|
|
|
|
// TTLsByPath returns the default and max TTLs corresponding to a particular
|
|
// mount point, or the system default
|
|
func (d dynamicSystemView) fetchTTLs() (def, max time.Duration) {
|
|
def = d.core.defaultLeaseTTL
|
|
max = d.core.maxLeaseTTL
|
|
|
|
if d.mountEntry != nil {
|
|
if d.mountEntry.Config.DefaultLeaseTTL != 0 {
|
|
def = d.mountEntry.Config.DefaultLeaseTTL
|
|
}
|
|
if d.mountEntry.Config.MaxLeaseTTL != 0 {
|
|
max = d.mountEntry.Config.MaxLeaseTTL
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Tainted indicates that the mount is in the process of being removed
|
|
func (d dynamicSystemView) Tainted() bool {
|
|
return d.mountEntry.Tainted
|
|
}
|
|
|
|
// CachingDisabled indicates whether to use caching behavior
|
|
func (d dynamicSystemView) CachingDisabled() bool {
|
|
return d.core.cachingDisabled || (d.mountEntry != nil && d.mountEntry.Config.ForceNoCache)
|
|
}
|
|
|
|
func (d dynamicSystemView) LocalMount() bool {
|
|
return d.mountEntry != nil && d.mountEntry.Local
|
|
}
|
|
|
|
// Checks if this is a primary Vault instance. Caller should hold the stateLock
|
|
// in read mode.
|
|
func (d dynamicSystemView) ReplicationState() consts.ReplicationState {
|
|
state := d.core.ReplicationState()
|
|
if d.perfStandby {
|
|
state |= consts.ReplicationPerformanceStandby
|
|
}
|
|
return state
|
|
}
|
|
|
|
func (d dynamicSystemView) HasFeature(feature license.Features) bool {
|
|
return d.core.HasFeature(feature)
|
|
}
|
|
|
|
// ResponseWrapData wraps the given data in a cubbyhole and returns the
|
|
// token used to unwrap.
|
|
func (d dynamicSystemView) ResponseWrapData(ctx context.Context, data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) {
|
|
req := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "sys/wrapping/wrap",
|
|
}
|
|
|
|
resp := &logical.Response{
|
|
WrapInfo: &wrapping.ResponseWrapInfo{
|
|
TTL: ttl,
|
|
},
|
|
Data: data,
|
|
}
|
|
|
|
if jwt {
|
|
resp.WrapInfo.Format = "jwt"
|
|
}
|
|
|
|
_, err := d.core.wrapInCubbyhole(ctx, req, resp, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp.WrapInfo, nil
|
|
}
|
|
|
|
func (d dynamicSystemView) NewPluginClient(ctx context.Context, config pluginutil.PluginClientConfig) (pluginutil.PluginClient, error) {
|
|
if d.core == nil {
|
|
return nil, fmt.Errorf("system view core is nil")
|
|
}
|
|
if d.core.pluginCatalog == nil {
|
|
return nil, fmt.Errorf("system view core plugin catalog is nil")
|
|
}
|
|
|
|
c, err := d.core.pluginCatalog.NewPluginClient(ctx, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// LookupPlugin looks for a plugin with the given name in the plugin catalog. It
|
|
// returns a PluginRunner or an error if no plugin was found.
|
|
func (d dynamicSystemView) LookupPlugin(ctx context.Context, name string, pluginType consts.PluginType) (*pluginutil.PluginRunner, error) {
|
|
return d.LookupPluginVersion(ctx, name, pluginType, "")
|
|
}
|
|
|
|
// LookupPluginVersion looks for a plugin with the given name and version in the plugin catalog. It
|
|
// returns a PluginRunner or an error if no plugin was found.
|
|
func (d dynamicSystemView) LookupPluginVersion(ctx context.Context, name string, pluginType consts.PluginType, version string) (*pluginutil.PluginRunner, error) {
|
|
if d.core == nil {
|
|
return nil, fmt.Errorf("system view core is nil")
|
|
}
|
|
if d.core.pluginCatalog == nil {
|
|
return nil, fmt.Errorf("system view core plugin catalog is nil")
|
|
}
|
|
r, err := d.core.pluginCatalog.Get(ctx, name, pluginType, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r == nil {
|
|
errContext := name
|
|
if version != "" {
|
|
errContext += fmt.Sprintf(", version=%s", version)
|
|
}
|
|
return nil, fmt.Errorf("%w: %s", ErrPluginNotFound, errContext)
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// ListVersionedPlugins returns information about all plugins of a certain
|
|
// typein the catalog, including any versioning information stored for them.
|
|
func (d dynamicSystemView) ListVersionedPlugins(ctx context.Context, pluginType consts.PluginType) ([]pluginutil.VersionedPlugin, error) {
|
|
if d.core == nil {
|
|
return nil, fmt.Errorf("system view core is nil")
|
|
}
|
|
if d.core.pluginCatalog == nil {
|
|
return nil, fmt.Errorf("system view core plugin catalog is nil")
|
|
}
|
|
return d.core.pluginCatalog.ListVersionedPlugins(ctx, pluginType)
|
|
}
|
|
|
|
// MlockEnabled returns the configuration setting for enabling mlock on plugins.
|
|
func (d dynamicSystemView) MlockEnabled() bool {
|
|
return d.core.enableMlock
|
|
}
|
|
|
|
func (d dynamicSystemView) EntityInfo(entityID string) (*logical.Entity, error) {
|
|
// Requests from token created from the token backend will not have entity information.
|
|
// Return missing entity instead of error when requesting from MemDB.
|
|
if entityID == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
if d.core == nil {
|
|
return nil, fmt.Errorf("system view core is nil")
|
|
}
|
|
if d.core.identityStore == nil {
|
|
return nil, fmt.Errorf("system view identity store is nil")
|
|
}
|
|
|
|
// Retrieve the entity from MemDB
|
|
entity, err := d.core.identityStore.MemDBEntityByID(entityID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entity == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Return a subset of the data
|
|
ret := &logical.Entity{
|
|
ID: entity.ID,
|
|
Name: entity.Name,
|
|
Disabled: entity.Disabled,
|
|
}
|
|
|
|
if entity.Metadata != nil {
|
|
ret.Metadata = make(map[string]string, len(entity.Metadata))
|
|
for k, v := range entity.Metadata {
|
|
ret.Metadata[k] = v
|
|
}
|
|
}
|
|
|
|
aliases := make([]*logical.Alias, 0, len(entity.Aliases))
|
|
for _, a := range entity.Aliases {
|
|
|
|
// Don't return aliases from other namespaces
|
|
if a.NamespaceID != d.mountEntry.NamespaceID {
|
|
continue
|
|
}
|
|
|
|
alias := identity.ToSDKAlias(a)
|
|
|
|
// MountType is not stored with the entity and must be looked up
|
|
if mount := d.core.router.ValidateMountByAccessor(a.MountAccessor); mount != nil {
|
|
alias.MountType = mount.MountType
|
|
}
|
|
|
|
aliases = append(aliases, alias)
|
|
}
|
|
ret.Aliases = aliases
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (d dynamicSystemView) GroupsForEntity(entityID string) ([]*logical.Group, error) {
|
|
// Requests from token created from the token backend will not have entity information.
|
|
// Return missing entity instead of error when requesting from MemDB.
|
|
if entityID == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
if d.core == nil {
|
|
return nil, fmt.Errorf("system view core is nil")
|
|
}
|
|
if d.core.identityStore == nil {
|
|
return nil, fmt.Errorf("system view identity store is nil")
|
|
}
|
|
|
|
groups, inheritedGroups, err := d.core.identityStore.groupsByEntityID(entityID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
groups = append(groups, inheritedGroups...)
|
|
|
|
logicalGroups := make([]*logical.Group, 0, len(groups))
|
|
for _, g := range groups {
|
|
// Don't return groups from other namespaces
|
|
if g.NamespaceID != d.mountEntry.NamespaceID {
|
|
continue
|
|
}
|
|
|
|
logicalGroups = append(logicalGroups, identity.ToSDKGroup(g))
|
|
}
|
|
|
|
return logicalGroups, nil
|
|
}
|
|
|
|
func (d dynamicSystemView) PluginEnv(_ context.Context) (*logical.PluginEnvironment, error) {
|
|
v := version.GetVersion()
|
|
return &logical.PluginEnvironment{
|
|
VaultVersion: v.Version,
|
|
VaultVersionPrerelease: v.VersionPrerelease,
|
|
VaultVersionMetadata: v.VersionMetadata,
|
|
}, nil
|
|
}
|
|
|
|
func (d dynamicSystemView) VaultVersion(_ context.Context) (string, error) {
|
|
return version.GetVersion().Version, nil
|
|
}
|
|
|
|
func (d dynamicSystemView) GeneratePasswordFromPolicy(ctx context.Context, policyName string) (password string, err error) {
|
|
if policyName == "" {
|
|
return "", fmt.Errorf("missing password policy name")
|
|
}
|
|
|
|
// Ensure there's a timeout on the context of some sort
|
|
if _, hasTimeout := ctx.Deadline(); !hasTimeout {
|
|
var cancel func()
|
|
ctx, cancel = context.WithTimeout(ctx, 1*time.Second)
|
|
defer cancel()
|
|
}
|
|
|
|
ctx = namespace.ContextWithNamespace(ctx, d.mountEntry.Namespace())
|
|
|
|
policyCfg, err := d.retrievePasswordPolicy(ctx, policyName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to retrieve password policy: %w", err)
|
|
}
|
|
|
|
if policyCfg == nil {
|
|
return "", fmt.Errorf("no password policy found")
|
|
}
|
|
|
|
passPolicy, err := random.ParsePolicy(policyCfg.HCLPolicy)
|
|
if err != nil {
|
|
return "", fmt.Errorf("stored password policy is invalid: %w", err)
|
|
}
|
|
|
|
return passPolicy.Generate(ctx, nil)
|
|
}
|
|
|
|
func (d dynamicSystemView) ClusterID(ctx context.Context) (string, error) {
|
|
clusterInfo, err := d.core.Cluster(ctx)
|
|
if err != nil || clusterInfo.ID == "" {
|
|
return "", fmt.Errorf("unable to retrieve cluster info or empty ID: %w", err)
|
|
}
|
|
|
|
return clusterInfo.ID, nil
|
|
}
|