| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| 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 (e extendedSystemViewImpl) APILockShouldBlockRequest() (bool, error) { |
| mountEntry := e.mountEntry |
| if mountEntry == nil { |
| return false, fmt.Errorf("no mount entry") |
| } |
| ns := mountEntry.Namespace() |
| |
| if err := enterpriseBlockRequestIfError(e.core, ns.Path, mountEntry.Path); err != nil { |
| return true, nil |
| } |
| |
| return false, nil |
| } |
| |
| 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 |
| } |