feat: share env files as encrypted links #54
No files matched your search
@@ -7,6 +7,11 @@ go test ./...
|
||||
go test ./internal/ui/app -run TestGolden -update # after intentional UI changes
|
||||
```
|
||||
|
||||
In `--demo`, sharing env files works end to end: `S` → `f` on a demo
|
||||
repo, pick `kv-shared-prod / env-share` (the second account's) as the
|
||||
key, then paste the link back. The demo repos aren't on disk, so opening
|
||||
asks where to save, as for a repo you haven't cloned.
|
||||
|
||||
A stripped binary is about 21 MB instead of 30 MB:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -331,6 +331,10 @@ func Azure(delay time.Duration) *Provider {
|
||||
}, demoCert)
|
||||
|
||||
shared := p.addContainer(prod, "kv-shared-prod", "https://kv-shared-prod.vault.azure.net", allTypes...)
|
||||
// A key to share env files with (S → f): random, as share.keyRules wants.
|
||||
p.add(shared, provider.TypeSecret, provider.Object{
|
||||
Name: "env-share", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "env-share"),
|
||||
}, "q8Zt3VwL1mXc7Rk2Pj9Ys4Hn6Bf0Ga5Ud+Te/OiWlE=")
|
||||
p.add(shared, provider.TypeSecret, provider.Object{
|
||||
Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"),
|
||||
}, "smtp-demo-password")
|
||||
|
||||
@@ -134,3 +134,18 @@ func format(path string) string {
|
||||
}
|
||||
return "dotenv"
|
||||
}
|
||||
|
||||
var _ provider.Contents = (*Provider)(nil)
|
||||
|
||||
// Contents is a plain env file as KEY=value lines, from its keys, so the
|
||||
// demo repos' env files can be shared.
|
||||
func (p *Provider) Contents(ctx context.Context, c provider.Container) ([]byte, error) {
|
||||
if c.Detail != provider.DetailPlainEnv {
|
||||
return nil, provider.ErrUnsupported
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, o := range p.objects[listKey{c.ID, provider.TypeSecret}] {
|
||||
b.WriteString(o.Name + "=" + p.values[o.ID] + "\n")
|
||||
}
|
||||
return []byte(b.String()), ctx.Err()
|
||||
}
|
||||
@@ -189,6 +189,12 @@ type Files interface {
|
||||
File(c Container, o *Object) (File, error)
|
||||
}
|
||||
|
||||
// Contents is implemented by providers whose containers are files, so
|
||||
// they can be shared (package share): the file as it is.
|
||||
type Contents interface {
|
||||
Contents(ctx context.Context, c Container) ([]byte, error)
|
||||
}
|
||||
|
||||
// File is a container's file, to open.
|
||||
type File struct {
|
||||
Path string
|
||||
|
||||
@@ -412,6 +412,15 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
|
||||
return provider.NewSecretValue(stdout.Bytes()), nil
|
||||
}
|
||||
|
||||
// Contents reads the file c is.
|
||||
func (p *Provider) Contents(_ context.Context, c provider.Container) ([]byte, error) {
|
||||
f, err := p.File(c, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(f.Path)
|
||||
}
|
||||
|
||||
// File is the file c is, and the line o's key is on.
|
||||
func (p *Provider) File(c provider.Container, o *provider.Object) (provider.File, error) {
|
||||
path := c.ID
|
||||
|
||||
@@ -40,9 +40,9 @@ type sealedMsg struct {
|
||||
}
|
||||
|
||||
// canShareFiles reports whether src's env files can be shared: it's a
|
||||
// repo whose files are on disk.
|
||||
// repo whose files can be read.
|
||||
func (m Model) canShareFiles(src provider.Source) bool {
|
||||
_, ok := m.providerFor(src).(provider.Files)
|
||||
_, ok := m.providerFor(src).(provider.Contents)
|
||||
return src.Kind == provider.KindRepo && ok
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (m Model) canShareFiles(src provider.Source) bool {
|
||||
// plain ones.
|
||||
func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd {
|
||||
p := m.providerFor(src)
|
||||
files, ok := p.(provider.Files)
|
||||
files, ok := p.(provider.Contents)
|
||||
if !ok {
|
||||
m.flash = warning("these files can't be shared")
|
||||
return nil
|
||||
@@ -78,16 +78,12 @@ func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd
|
||||
}
|
||||
}
|
||||
for _, c := range cs {
|
||||
f, err := files.File(c, nil)
|
||||
if err == nil {
|
||||
var b []byte
|
||||
b, err = os.ReadFile(f.Path)
|
||||
msg.files = append(msg.files, share.File{Path: c.Name, Data: b})
|
||||
}
|
||||
b, err := files.Contents(ctx, c)
|
||||
if err != nil {
|
||||
msg.err = err
|
||||
return msg
|
||||
}
|
||||
msg.files = append(msg.files, share.File{Path: c.Name, Data: b})
|
||||
}
|
||||
key, ok, err := share.DefaultKey(src.ID, general)
|
||||
if ok {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/share"
|
||||
@@ -175,3 +176,44 @@ func TestShareLinkGarbled(t *testing.T) {
|
||||
t.Errorf("not reported:\n%s", d.screen())
|
||||
}
|
||||
}
|
||||
|
||||
// TestShareInDemo shares a demo repo's env files with the demo key and
|
||||
// opens the link, as lazysecrets --demo does: the demo repos aren't on
|
||||
// disk, so it asks where to save.
|
||||
func TestShareInDemo(t *testing.T) {
|
||||
clip := &fakeClipboard{}
|
||||
d := newDriverOpts(t, Options{Clipboard: clip},
|
||||
fake.Azure(0), fake.AzureOps(0).As("Azure:admin", "Azure · admin"), fake.Repos())
|
||||
d.wait = scryptWait
|
||||
var src provider.Source
|
||||
for _, s := range d.m.sourceResults["Repos"].sources {
|
||||
if s.Here {
|
||||
src = s
|
||||
}
|
||||
}
|
||||
if src.ID == "" {
|
||||
t.Fatal("no demo repo")
|
||||
}
|
||||
src.Provider = "Repos"
|
||||
d.run(d.m.shareFiles(src, nil))
|
||||
if d.m.search == nil || d.m.search.pick == nil {
|
||||
t.Fatalf("no picker: %s", d.m.flash.text)
|
||||
}
|
||||
d.send(tea.PasteMsg{Content: "https://kv-shared-prod.vault.azure.net/secrets/env-share"})
|
||||
if !strings.HasPrefix(clip.content, "lazysecrets://share/") {
|
||||
t.Fatalf("copied %q (%s)", clip.content, d.m.flash.text)
|
||||
}
|
||||
|
||||
dir, _ := filepath.EvalSymlinks(t.TempDir())
|
||||
d.m.placeDir = dir
|
||||
d.send(tea.PasteMsg{Content: clip.content})
|
||||
d.keys("o")
|
||||
if d.m.prompt == nil || d.m.prompt.input == nil || !strings.Contains(d.screen(), "isn't among your repos") {
|
||||
t.Fatalf("doesn't ask where:\n%s", d.screen())
|
||||
}
|
||||
d.keys("enter", "w")
|
||||
entries, _ := os.ReadDir(dir)
|
||||
if len(entries) == 0 {
|
||||
t.Fatalf("nothing saved (%s)", d.m.flash.text)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user