Skip to content

Commit

Permalink
Add template feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominik Schulz committed Apr 5, 2017
1 parent fefd505 commit 2084587
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 1 deletion.
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
15 changes: 14 additions & 1 deletion action/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"os"
"os/exec"

"github.com/justwatchcom/gopass/pwgen"
"github.com/justwatchcom/gopass/tpl"

"github.com/justwatchcom/gopass/fsutil"
"github.com/justwatchcom/gopass/password"
shellquote "github.com/kballard/go-shellquote"
Expand All @@ -27,11 +30,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(24, 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 +53,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(),
}
}

0 comments on commit 2084587

Please sign in to comment.