feat: share env files as encrypted links #54

Merged
keb merged 25 commits from share-files into main 2026-10-13 14:55:22 +02:00
7 changed files with 86 additions and 9 deletions
Showing only changes of commit 777c8cb532 - Show all commits

No files matched your search

+5
View File
@@ -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
+4
View File
@@ -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")
+15
View File
@@ -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()
}
+6
View File
@@ -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
+9
View File
@@ -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
+5 -9
View File
@@ -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 {
+42
View File
@@ -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)
}
}