open-vault/vault/policy_store.go
2018-08-23 15:04:18 -04:00

566 lines
15 KiB
Go

package vault
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/golang-lru"
"github.com/hashicorp/vault/helper/consts"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
)
const (
// policySubPath is the sub-path used for the policy store
// view. This is nested under the system view.
policyACLSubPath = "policy/"
// policyCacheSize is the number of policies that are kept cached
policyCacheSize = 1024
// defaultPolicyName is the name of the default policy
defaultPolicyName = "default"
// responseWrappingPolicyName is the name of the fixed policy
responseWrappingPolicyName = "response-wrapping"
// controlGroupPolicyName is the name of the fixed policy for control group
// tokens
controlGroupPolicyName = "control-group"
// responseWrappingPolicy is the policy that ensures cubbyhole response
// wrapping can always succeed.
responseWrappingPolicy = `
path "cubbyhole/response" {
capabilities = ["create", "read"]
}
path "sys/wrapping/unwrap" {
capabilities = ["update"]
}
`
// defaultPolicy is the "default" policy
defaultPolicy = `
# Allow tokens to look up their own properties
path "auth/token/lookup-self" {
capabilities = ["read"]
}
# Allow tokens to renew themselves
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Allow tokens to revoke themselves
path "auth/token/revoke-self" {
capabilities = ["update"]
}
# Allow a token to look up its own capabilities on a path
path "sys/capabilities-self" {
capabilities = ["update"]
}
# Allow a token to look up its resultant ACL from all policies. This is useful
# for UIs. It is an internal path because the format may change at any time
# based on how the internal ACL features and capabilities change.
path "sys/internal/ui/resultant-acl" {
capabilities = ["read"]
}
# Allow a token to renew a lease via lease_id in the request body; old path for
# old clients, new path for newer
path "sys/renew" {
capabilities = ["update"]
}
path "sys/leases/renew" {
capabilities = ["update"]
}
# Allow looking up lease properties. This requires knowing the lease ID ahead
# of time and does not divulge any sensitive information.
path "sys/leases/lookup" {
capabilities = ["update"]
}
# Allow a token to manage its own cubbyhole
path "cubbyhole/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Allow a token to wrap arbitrary values in a response-wrapping token
path "sys/wrapping/wrap" {
capabilities = ["update"]
}
# Allow a token to look up the creation time and TTL of a given
# response-wrapping token
path "sys/wrapping/lookup" {
capabilities = ["update"]
}
# Allow a token to unwrap a response-wrapping token. This is a convenience to
# avoid client token swapping since this is also part of the response wrapping
# policy.
path "sys/wrapping/unwrap" {
capabilities = ["update"]
}
# Allow general purpose tools
path "sys/tools/hash" {
capabilities = ["update"]
}
path "sys/tools/hash/*" {
capabilities = ["update"]
}
path "sys/tools/random" {
capabilities = ["update"]
}
path "sys/tools/random/*" {
capabilities = ["update"]
}
# Allow checking the status of a Control Group request if the user has the
# accessor
path "sys/control-group/request" {
capabilities = ["update"]
}
`
)
var (
immutablePolicies = []string{
"root",
responseWrappingPolicyName,
controlGroupPolicyName,
}
nonAssignablePolicies = []string{
responseWrappingPolicyName,
controlGroupPolicyName,
}
)
// PolicyStore is used to provide durable storage of policy, and to
// manage ACLs associated with them.
type PolicyStore struct {
core *Core
aclView *BarrierView
tokenPoliciesLRU *lru.TwoQueueCache
// This is used to ensure that writes to the store (acl/rgp) or to the egp
// path tree don't happen concurrently. We are okay reading stale data so
// long as there aren't concurrent writes.
modifyLock *sync.RWMutex
// Stores whether a token policy is ACL or RGP
policyTypeMap sync.Map
// logger is the server logger copied over from core
logger log.Logger
}
// PolicyEntry is used to store a policy by name
type PolicyEntry struct {
Version int
Raw string
Templated bool
Type PolicyType
}
// NewPolicyStore creates a new PolicyStore that is backed
// using a given view. It used used to durable store and manage named policy.
func NewPolicyStore(ctx context.Context, core *Core, baseView *BarrierView, system logical.SystemView, logger log.Logger) *PolicyStore {
ps := &PolicyStore{
aclView: baseView.SubView(policyACLSubPath),
modifyLock: new(sync.RWMutex),
logger: logger,
core: core,
}
if !system.CachingDisabled() {
cache, _ := lru.New2Q(policyCacheSize)
ps.tokenPoliciesLRU = cache
}
keys, err := logical.CollectKeys(ctx, ps.aclView)
if err != nil {
ps.logger.Error("error collecting acl policy keys", "error", err)
return nil
}
for _, key := range keys {
ps.policyTypeMap.Store(ps.sanitizeName(key), PolicyTypeACL)
}
// Special-case root; doesn't exist on disk but does need to be found
ps.policyTypeMap.Store("root", PolicyTypeACL)
return ps
}
// setupPolicyStore is used to initialize the policy store
// when the vault is being unsealed.
func (c *Core) setupPolicyStore(ctx context.Context) error {
// Create the policy store
sysView := &dynamicSystemView{core: c}
c.policyStore = NewPolicyStore(ctx, c, c.systemBarrierView, sysView, c.baseLogger.Named("policy"))
if c.ReplicationState().HasState(consts.ReplicationPerformanceSecondary) {
// Policies will sync from the primary
return nil
}
// Ensure that the default policy exists, and if not, create it
if err := c.policyStore.loadACLPolicy(ctx, defaultPolicyName, defaultPolicy); err != nil {
return err
}
// Ensure that the response wrapping policy exists
if err := c.policyStore.loadACLPolicy(ctx, responseWrappingPolicyName, responseWrappingPolicy); err != nil {
return err
}
return nil
}
// teardownPolicyStore is used to reverse setupPolicyStore
// when the vault is being sealed.
func (c *Core) teardownPolicyStore() error {
c.policyStore = nil
return nil
}
func (ps *PolicyStore) invalidate(ctx context.Context, name string, policyType PolicyType) {
// This may come with a prefixed "/" due to joining the file path
saneName := strings.TrimPrefix(name, "/")
// We don't lock before removing from the LRU here because the worst that
// can happen is we load again if something since added it
switch policyType {
case PolicyTypeACL:
if ps.tokenPoliciesLRU != nil {
ps.tokenPoliciesLRU.Remove(saneName)
}
default:
// Can't do anything
return
}
// Force a reload
_, err := ps.GetPolicy(ctx, name, policyType)
if err != nil {
ps.logger.Error("error fetching policy after invalidation", "name", saneName)
}
}
// SetPolicy is used to create or update the given policy
func (ps *PolicyStore) SetPolicy(ctx context.Context, p *Policy) error {
defer metrics.MeasureSince([]string{"policy", "set_policy"}, time.Now())
if p == nil {
return fmt.Errorf("nil policy passed in for storage")
}
if p.Name == "" {
return fmt.Errorf("policy name missing")
}
// Policies are normalized to lower-case
p.Name = ps.sanitizeName(p.Name)
if strutil.StrListContains(immutablePolicies, p.Name) {
return fmt.Errorf("cannot update %q policy", p.Name)
}
return ps.setPolicyInternal(ctx, p)
}
func (ps *PolicyStore) setPolicyInternal(ctx context.Context, p *Policy) error {
ps.modifyLock.Lock()
defer ps.modifyLock.Unlock()
// Create the entry
entry, err := logical.StorageEntryJSON(p.Name, &PolicyEntry{
Version: 2,
Raw: p.Raw,
Type: p.Type,
Templated: p.Templated,
})
if err != nil {
return errwrap.Wrapf("failed to create entry: {{err}}", err)
}
switch p.Type {
case PolicyTypeACL:
if err := ps.aclView.Put(ctx, entry); err != nil {
return errwrap.Wrapf("failed to persist policy: {{err}}", err)
}
ps.policyTypeMap.Store(p.Name, PolicyTypeACL)
if ps.tokenPoliciesLRU != nil {
// Update the LRU cache
ps.tokenPoliciesLRU.Add(p.Name, p)
}
default:
return fmt.Errorf("unknown policy type, cannot set")
}
return nil
}
// GetPolicy is used to fetch the named policy
func (ps *PolicyStore) GetPolicy(ctx context.Context, name string, policyType PolicyType) (*Policy, error) {
defer metrics.MeasureSince([]string{"policy", "get_policy"}, time.Now())
// Policies are normalized to lower-case
name = ps.sanitizeName(name)
var cache *lru.TwoQueueCache
var view *BarrierView
switch policyType {
case PolicyTypeACL:
cache = ps.tokenPoliciesLRU
view = ps.aclView
case PolicyTypeToken:
cache = ps.tokenPoliciesLRU
val, ok := ps.policyTypeMap.Load(name)
if !ok {
// Doesn't exist
return nil, nil
}
policyType = val.(PolicyType)
switch policyType {
case PolicyTypeACL:
view = ps.aclView
default:
return nil, fmt.Errorf("invalid type of policy in type map: %q", policyType)
}
}
if cache != nil {
// Check for cached policy
if raw, ok := cache.Get(name); ok {
return raw.(*Policy), nil
}
}
// Special case the root policy
if policyType == PolicyTypeACL && name == "root" {
p := &Policy{Name: "root"}
if cache != nil {
cache.Add(p.Name, p)
}
return p, nil
}
ps.modifyLock.Lock()
defer ps.modifyLock.Unlock()
// See if anything has added it since we got the lock
if cache != nil {
if raw, ok := cache.Get(name); ok {
return raw.(*Policy), nil
}
}
out, err := view.Get(ctx, name)
if err != nil {
return nil, errwrap.Wrapf("failed to read policy: {{err}}", err)
}
if out == nil {
return nil, nil
}
policyEntry := new(PolicyEntry)
policy := new(Policy)
err = out.DecodeJSON(policyEntry)
if err != nil {
return nil, errwrap.Wrapf("failed to parse policy: {{err}}", err)
}
// Set these up here so that they're available for loading into
// Sentinel
policy.Name = name
policy.Raw = policyEntry.Raw
policy.Type = policyEntry.Type
policy.Templated = policyEntry.Templated
switch policyEntry.Type {
case PolicyTypeACL:
// Parse normally
p, err := ParseACLPolicy(policyEntry.Raw)
if err != nil {
return nil, errwrap.Wrapf("failed to parse policy: {{err}}", err)
}
policy.Paths = p.Paths
// Reset this in case they set the name in the policy itself
policy.Name = name
ps.policyTypeMap.Store(name, PolicyTypeACL)
default:
return nil, fmt.Errorf("unknown policy type %q", policyEntry.Type.String())
}
if cache != nil {
// Update the LRU cache
cache.Add(name, policy)
}
return policy, nil
}
// ListPolicies is used to list the available policies
func (ps *PolicyStore) ListPolicies(ctx context.Context, policyType PolicyType) ([]string, error) {
defer metrics.MeasureSince([]string{"policy", "list_policies"}, time.Now())
// Scan the view, since the policy names are the same as the
// key names.
var keys []string
var err error
switch policyType {
case PolicyTypeACL:
keys, err = logical.CollectKeys(ctx, ps.aclView)
default:
return nil, fmt.Errorf("unknown policy type %q", policyType)
}
// We only have non-assignable ACL policies at the moment
for _, nonAssignable := range nonAssignablePolicies {
deleteIndex := -1
//Find indices of non-assignable policies in keys
for index, key := range keys {
if key == nonAssignable {
// Delete collection outside the loop
deleteIndex = index
break
}
}
// Remove non-assignable policies when found
if deleteIndex != -1 {
keys = append(keys[:deleteIndex], keys[deleteIndex+1:]...)
}
}
return keys, err
}
// DeletePolicy is used to delete the named policy
func (ps *PolicyStore) DeletePolicy(ctx context.Context, name string, policyType PolicyType) error {
defer metrics.MeasureSince([]string{"policy", "delete_policy"}, time.Now())
ps.modifyLock.Lock()
defer ps.modifyLock.Unlock()
// Policies are normalized to lower-case
name = ps.sanitizeName(name)
switch policyType {
case PolicyTypeACL:
if strutil.StrListContains(immutablePolicies, name) {
return fmt.Errorf("cannot delete %q policy", name)
}
if name == "default" {
return fmt.Errorf("cannot delete default policy")
}
err := ps.aclView.Delete(ctx, name)
if err != nil {
return errwrap.Wrapf("failed to delete policy: {{err}}", err)
}
if ps.tokenPoliciesLRU != nil {
// Clear the cache
ps.tokenPoliciesLRU.Remove(name)
}
ps.policyTypeMap.Delete(name)
}
return nil
}
type TemplateError struct {
Err error
}
func (t *TemplateError) WrappedErrors() []error {
return []error{t.Err}
}
func (t *TemplateError) Error() string {
return t.Err.Error()
}
// ACL is used to return an ACL which is built using the
// named policies.
func (ps *PolicyStore) ACL(ctx context.Context, entity *identity.Entity, names ...string) (*ACL, error) {
// Fetch the policies
var policies []*Policy
for _, name := range names {
p, err := ps.GetPolicy(ctx, name, PolicyTypeToken)
if err != nil {
return nil, errwrap.Wrapf("failed to get policy: {{err}}", err)
}
if p != nil {
policies = append(policies, p)
}
}
var fetchedGroups bool
var groups []*identity.Group
for i, policy := range policies {
if policy.Type == PolicyTypeACL && policy.Templated {
if !fetchedGroups {
fetchedGroups = true
if entity != nil {
directGroups, inheritedGroups, err := ps.core.identityStore.groupsByEntityID(entity.ID)
if err != nil {
return nil, errwrap.Wrapf("failed to fetch group memberships: {{err}}", err)
}
groups = append(directGroups, inheritedGroups...)
}
}
p, err := parseACLPolicyWithTemplating(policy.Raw, true, entity, groups)
if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf("error parsing templated policy %q: {{err}}", policy.Name), err)
}
policies[i] = p
}
}
// Construct the ACL
acl, err := NewACL(policies)
if err != nil {
return nil, errwrap.Wrapf("failed to construct ACL: {{err}}", err)
}
return acl, nil
}
func (ps *PolicyStore) loadACLPolicy(ctx context.Context, policyName, policyText string) error {
// Check if the policy already exists
policy, err := ps.GetPolicy(ctx, policyName, PolicyTypeACL)
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("error fetching %s policy from store: {{err}}", policyName), err)
}
if policy != nil {
if !strutil.StrListContains(immutablePolicies, policyName) || policyText == policy.Raw {
return nil
}
}
policy, err = ParseACLPolicy(policyText)
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("error parsing %s policy: {{err}}", policyName), err)
}
if policy == nil {
return fmt.Errorf("parsing %q policy resulted in nil policy", policyName)
}
policy.Name = policyName
policy.Type = PolicyTypeACL
return ps.setPolicyInternal(ctx, policy)
}
func (ps *PolicyStore) sanitizeName(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}