package vault import ( "fmt" "strings" "github.com/hashicorp/go-multierror" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" ) const ( DenyCapability = "deny" CreateCapability = "create" ReadCapability = "read" UpdateCapability = "update" DeleteCapability = "delete" ListCapability = "list" SudoCapability = "sudo" RootCapability = "root" // Backwards compatibility OldDenyPathPolicy = "deny" OldReadPathPolicy = "read" OldWritePathPolicy = "write" OldSudoPathPolicy = "sudo" ) const ( DenyCapabilityInt uint32 = 1 << iota CreateCapabilityInt ReadCapabilityInt UpdateCapabilityInt DeleteCapabilityInt ListCapabilityInt SudoCapabilityInt ) var ( cap2Int = map[string]uint32{ DenyCapability: DenyCapabilityInt, CreateCapability: CreateCapabilityInt, ReadCapability: ReadCapabilityInt, UpdateCapability: UpdateCapabilityInt, DeleteCapability: DeleteCapabilityInt, ListCapability: ListCapabilityInt, SudoCapability: SudoCapabilityInt, } ) // Policy is used to represent the policy specified by // an ACL configuration. type Policy struct { Name string `hcl:"name"` Paths []*PathCapabilities `hcl:"-"` Raw string } // PathCapabilities represents a policy for a path in the namespace. type PathCapabilities struct { Prefix string Policy string Capabilities []string CapabilitiesBitmap uint32 `hcl:"-"` Glob bool } // Parse is used to parse the specified ACL rules into an // intermediary set of policies, before being compiled into // the ACL func Parse(rules string) (*Policy, error) { // Parse the rules root, err := hcl.Parse(rules) if err != nil { return nil, fmt.Errorf("Failed to parse policy: %s", err) } // Top-level item should be the object list list, ok := root.Node.(*ast.ObjectList) if !ok { return nil, fmt.Errorf("Failed to parse policy: does not contain a root object") } // Check for invalid top-level keys valid := []string{ "name", "path", } if err := checkHCLKeys(list, valid); err != nil { return nil, fmt.Errorf("Failed to parse policy: %s", err) } // Create the initial policy and store the raw text of the rules var p Policy p.Raw = rules if err := hcl.DecodeObject(&p, list); err != nil { return nil, fmt.Errorf("Failed to parse policy: %s", err) } if o := list.Filter("path"); len(o.Items) > 0 { if err := parsePaths(&p, o); err != nil { return nil, fmt.Errorf("Failed to parse policy: %s", err) } } return &p, nil } func parsePaths(result *Policy, list *ast.ObjectList) error { paths := make([]*PathCapabilities, 0, len(list.Items)) for _, item := range list.Items { key := "path" if len(item.Keys) > 0 { key = item.Keys[0].Token.Value().(string) } valid := []string{ "policy", "capabilities", } if err := checkHCLKeys(item.Val, valid); err != nil { return multierror.Prefix(err, fmt.Sprintf("path %q:", key)) } var pc PathCapabilities pc.Prefix = key if err := hcl.DecodeObject(&pc, item.Val); err != nil { return multierror.Prefix(err, fmt.Sprintf("path %q:", key)) } // Strip a leading '/' as paths in Vault start after the / in the API path if len(pc.Prefix) > 0 && pc.Prefix[0] == '/' { pc.Prefix = pc.Prefix[1:] } // Strip the glob character if found if strings.HasSuffix(pc.Prefix, "*") { pc.Prefix = strings.TrimSuffix(pc.Prefix, "*") pc.Glob = true } // Map old-style policies into capabilities if len(pc.Policy) > 0 { switch pc.Policy { case OldDenyPathPolicy: pc.Capabilities = []string{DenyCapability} case OldReadPathPolicy: pc.Capabilities = append(pc.Capabilities, []string{ReadCapability, ListCapability}...) case OldWritePathPolicy: pc.Capabilities = append(pc.Capabilities, []string{CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability}...) case OldSudoPathPolicy: pc.Capabilities = append(pc.Capabilities, []string{CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability}...) default: return fmt.Errorf("path %q: invalid policy '%s'", key, pc.Policy) } } // Initialize the map pc.CapabilitiesBitmap = 0 for _, cap := range pc.Capabilities { switch cap { // If it's deny, don't include any other capability case DenyCapability: pc.Capabilities = []string{DenyCapability} pc.CapabilitiesBitmap = DenyCapabilityInt goto PathFinished case CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability: pc.CapabilitiesBitmap |= cap2Int[cap] default: return fmt.Errorf("path %q: invalid capability '%s'", key, cap) } } PathFinished: paths = append(paths, &pc) } result.Paths = paths return nil } func checkHCLKeys(node ast.Node, valid []string) error { var list *ast.ObjectList switch n := node.(type) { case *ast.ObjectList: list = n case *ast.ObjectType: list = n.List default: return fmt.Errorf("cannot check HCL keys of type %T", n) } validMap := make(map[string]struct{}, len(valid)) for _, v := range valid { validMap[v] = struct{}{} } var result error for _, item := range list.Items { key := item.Keys[0].Token.Value().(string) if _, ok := validMap[key]; !ok { result = multierror.Append(result, fmt.Errorf( "invalid key '%s' on line %d", key, item.Assign.Line)) } } return result }