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 1 commit
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
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"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new line is one too much.

"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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the default 24 should be a const shared across packages or passed down. Aren't we setting that somewhere else too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, indeed. Will change that.

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(),
}
}