diff --git a/docs/development.md b/docs/development.md index 40e1a24..9fdede4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 diff --git a/internal/provider/fake/fake.go b/internal/provider/fake/fake.go index cd86071..d7b021c 100644 --- a/internal/provider/fake/fake.go +++ b/internal/provider/fake/fake.go @@ -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") diff --git a/internal/provider/fake/repos.go b/internal/provider/fake/repos.go index 1dc5038..2757c62 100644 --- a/internal/provider/fake/repos.go +++ b/internal/provider/fake/repos.go @@ -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() +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7ad3fa8..a10ea66 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -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 diff --git a/internal/provider/repo/repo.go b/internal/provider/repo/repo.go index 234917f..1f53ad7 100644 --- a/internal/provider/repo/repo.go +++ b/internal/provider/repo/repo.go @@ -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 diff --git a/internal/ui/app/sharefiles.go b/internal/ui/app/sharefiles.go index faed1df..3f61a7e 100644 --- a/internal/ui/app/sharefiles.go +++ b/internal/ui/app/sharefiles.go @@ -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 { diff --git a/internal/ui/app/sharefiles_test.go b/internal/ui/app/sharefiles_test.go index 8535f95..526f8a7 100644 --- a/internal/ui/app/sharefiles_test.go +++ b/internal/ui/app/sharefiles_test.go @@ -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) + } +}