Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add template feature #1

Merged
merged 2 commits into from
May 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ gopass
└── 0xB5B44266A3683834 - Gopher <gopher@golang.org>
```

### Password Templates

With gopass you can create templates which are searched when executing `gopass edit` on a new secret. If the folder, or any parent folder, contains a file called `.pass-template` it's parsed as a Go template, executed with the name of the new secret and an auto-generated password and loaded into your `$EDITOR`.

This makes it easy to e.g. generate database passwords or use templates for certain kind of secrets.

## Known Limitations and Caveats

### GnuPG
Expand Down
14 changes: 13 additions & 1 deletion action/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (

"github.com/justwatchcom/gopass/fsutil"
"github.com/justwatchcom/gopass/password"
"github.com/justwatchcom/gopass/pwgen"
"github.com/justwatchcom/gopass/tpl"
shellquote "github.com/kballard/go-shellquote"
"github.com/urfave/cli"
)
Expand All @@ -27,11 +29,21 @@ func (s *Action) Edit(c *cli.Context) error {
}

var content []byte
var changed bool
if exists {
content, err = s.Store.Get(name)
if err != nil {
return fmt.Errorf("failed to decrypt %s: %v", name, err)
}
} else if tmpl, found := s.Store.LookupTemplate(name); found {
changed = true
// load template if it exists
content = pwgen.GeneratePassword(defaultLength, false)
if nc, err := tpl.Execute(string(tmpl), name, content, s.Store); err == nil {
content = nc
} else {
fmt.Printf("failed to execute template: %s\n", err)
}
}

nContent, err := s.editor(content)
Expand All @@ -40,7 +52,7 @@ func (s *Action) Edit(c *cli.Context) error {
}

// If content is equal, nothing changed, exiting
if bytes.Equal(content, nContent) {
if bytes.Equal(content, nContent) && !changed {
return nil
}

Expand Down
76 changes: 76 additions & 0 deletions action/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package action

import (
"bytes"
"fmt"

"github.com/urfave/cli"
)

// TemplatesPrint will pretty-print a tree of templates
func (s *Action) TemplatesPrint(c *cli.Context) error {
tree, err := s.Store.TemplateTree()
if err != nil {
return err
}
fmt.Println(tree.Format())
return nil
}

// TemplateEdit will load and existing or new template into an
// editor
func (s *Action) TemplateEdit(c *cli.Context) error {
name := c.Args().First()
if name == "" {
return fmt.Errorf("provide a template name")
}

var content []byte
if s.Store.HasTemplate(name) {
var err error
content, err = s.Store.GetTemplate(name)
if err != nil {
return err
}
}

nContent, err := s.editor(content)
if err != nil {
return err
}

// If content is equal, nothing changed, exiting
if bytes.Equal(content, nContent) {
return nil
}

return s.Store.SetTemplate(name, nContent)
}

// TemplateRemove will remove a single template
func (s *Action) TemplateRemove(c *cli.Context) error {
name := c.Args().First()
if name == "" {
return fmt.Errorf("provide a template name")
}

if !s.Store.HasTemplate(name) {
return fmt.Errorf("template not found")
}

return s.Store.RemoveTemplate(name)
}

// TemplatesComplete prints a list of all templates for bash completion
func (s *Action) TemplatesComplete(*cli.Context) {
tree, err := s.Store.TemplateTree()
if err != nil {
fmt.Println(err)
return
}

for _, v := range tree.List() {
fmt.Println(v)
}
return
}
27 changes: 27 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,33 @@ func main() {
},
},
},
{
Name: "templates",
Usage: "List and edit secret templates.",
Description: "" +
"List existing templates in the password store and allow for editing " +
"and creating them.",
Before: action.Initialized,
Action: action.TemplatesPrint,
Subcommands: []cli.Command{
{
Name: "edit",
Usage: "Edit secret templates.",
Description: "Edit an existing or new template",
Before: action.Initialized,
Action: action.TemplateEdit,
BashComplete: action.TemplatesComplete,
},
{
Name: "remove",
Usage: "Remove secret templates.",
Description: "Remove an existing template",
Before: action.Initialized,
Action: action.TemplateRemove,
BashComplete: action.TemplatesComplete,
},
},
},
{
Name: "unclip",
Usage: "Internal command to clear clipboard",
Expand Down
170 changes: 170 additions & 0 deletions password/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package password

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"

"github.com/justwatchcom/gopass/fsutil"
"github.com/justwatchcom/gopass/tree"
)

const (
// TemplateFile is the name of a pass template
TemplateFile = ".pass-template"
)

// LookupTemplate will lookup and return a template
func (r *RootStore) LookupTemplate(name string) ([]byte, bool) {
store := r.getStore(name)
return store.LookupTemplate(strings.TrimPrefix(name, store.alias))
}

// LookupTemplate will lookup and return a template
func (s *Store) LookupTemplate(name string) ([]byte, bool) {
for {
if !strings.Contains(name, string(filepath.Separator)) {
return []byte{}, false
}
name = filepath.Dir(name)
tpl := filepath.Join(s.path, name, TemplateFile)
if fsutil.IsFile(tpl) {
if content, err := ioutil.ReadFile(tpl); err == nil {
return content, true
}
}
}
}

// TemplateTree returns a tree of all templates
func (r *RootStore) TemplateTree() (*tree.Folder, error) {
root := tree.New("gopass")
mps := r.mountPoints()
sort.Sort(sort.Reverse(byLen(mps)))
for _, alias := range mps {
substore := r.mounts[alias]
if substore == nil {
continue
}
if err := root.AddMount(alias, substore.path); err != nil {
return nil, fmt.Errorf("failed to add mount: %s", err)
}
for _, t := range substore.ListTemplates(alias) {
// TODO(dschulz) maybe: if err := root.AddFile(t); err != nil {
if err := root.AddFile(alias + "/" + t); err != nil {
fmt.Println(err)
}
}
}

for _, t := range r.store.ListTemplates("") {
if err := root.AddFile(t); err != nil {
fmt.Println(err)
}
}

return root, nil
}

func mkTemplateStoreWalkerFunc(alias, folder string, fn func(...string)) func(string, os.FileInfo, error) error {
return func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && strings.HasPrefix(info.Name(), ".") && path != folder {
return filepath.SkipDir
}
if info.IsDir() {
return nil
}
if info.Name() != TemplateFile {
return nil
}
if path == folder {
return nil
}
if info.Mode()&os.ModeSymlink != 0 {
return nil
}
s := strings.TrimPrefix(path, folder+"/")
s = strings.TrimSuffix(s, "/"+TemplateFile)
if alias != "" {
s = alias + "/" + s
}
fn(s)
return nil
}
}

// ListTemplates will list all templates in this store
func (s *Store) ListTemplates(prefix string) []string {
lst := make([]string, 0, 10)
addFunc := func(in ...string) {
for _, s := range in {
lst = append(lst, s)
}
}

if err := filepath.Walk(s.path, mkTemplateStoreWalkerFunc(prefix, s.path, addFunc)); err != nil {
fmt.Printf("Failed to list templates: %s\n", err)
}

return lst
}

// templatefile returns the name of the given template on disk
func (s *Store) templatefile(name string) string {
return filepath.Join(s.path, name, TemplateFile)
}

// HasTemplate returns true if the template exists
func (r *RootStore) HasTemplate(name string) bool {
store := r.getStore(name)
return store.HasTemplate(strings.TrimPrefix(name, store.alias))
}

// HasTemplate returns true if the template exists
func (s *Store) HasTemplate(name string) bool {
return fsutil.IsFile(s.templatefile(name))
}

// GetTemplate will return the content of the named template
func (r *RootStore) GetTemplate(name string) ([]byte, error) {
store := r.getStore(name)
return store.GetTemplate(strings.TrimPrefix(name, store.alias))
}

// GetTemplate will return the content of the named template
func (s *Store) GetTemplate(name string) ([]byte, error) {
return ioutil.ReadFile(s.templatefile(name))
}

// SetTemplate will (over)write the content to the template file
func (r *RootStore) SetTemplate(name string, content []byte) error {
store := r.getStore(name)
return store.SetTemplate(strings.TrimPrefix(name, store.alias), content)
}

// SetTemplate will (over)write the content to the template file
func (s *Store) SetTemplate(name string, content []byte) error {
return ioutil.WriteFile(s.templatefile(name), content, 0600)
}

// RemoveTemplate will delete the named template if it exists
func (r *RootStore) RemoveTemplate(name string) error {
store := r.getStore(name)
return store.RemoveTemplate(strings.TrimPrefix(name, store.alias))
}

// RemoveTemplate will delete the named template if it exists
func (s *Store) RemoveTemplate(name string) error {
t := s.templatefile(name)
if !fsutil.IsFile(t) {
return fmt.Errorf("template not found")
}

return os.Remove(t)
}
48 changes: 48 additions & 0 deletions tpl/funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package tpl

import (
"crypto/md5"
"crypto/sha1"
"fmt"
"text/template"
)

// These constants defined the template function names used
const (
FuncMd5sum = "md5sum"
FuncSha1sum = "sha1sum"
FuncGet = "get"
)

func md5sum() func(...string) (string, error) {
return func(s ...string) (string, error) {
return fmt.Sprintf("%x", md5.Sum([]byte(s[0]))), nil
}
}

func sha1sum() func(...string) (string, error) {
return func(s ...string) (string, error) {
if len(s) < 2 {
return "", nil
}
return fmt.Sprintf("%x", sha1.Sum([]byte(s[1]))), nil
}
}

func get(kv kvstore) func(...string) (string, error) {
return func(s ...string) (string, error) {
if len(s) < 2 {
return "", nil
}
buf, err := kv.Get(s[1])
return string(buf), err
}
}

func funcMap(kv kvstore) template.FuncMap {
return template.FuncMap{
FuncGet: get(kv),
FuncMd5sum: md5sum(),
FuncSha1sum: sha1sum(),
}
}