feat: share env files as encrypted links #54
No files matched your search
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"charm.land/bubbles/v2/textinput"
|
"charm.land/bubbles/v2/textinput"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||||
@@ -288,7 +289,7 @@ func (m *Model) handleShareOpened(msg shareOpenedMsg) tea.Cmd {
|
|||||||
switch {
|
switch {
|
||||||
case msg.err != nil:
|
case msg.err != nil:
|
||||||
m.noteErr(msg.err)
|
m.noteErr(msg.err)
|
||||||
m.flash = flash{text: "open: " + msg.err.Error(), isErr: true}
|
m.openFailed(msg.sealed, msg.err, false)
|
||||||
case msg.dir == "":
|
case msg.dir == "":
|
||||||
return m.askShareDir(msg)
|
return m.askShareDir(msg)
|
||||||
default:
|
default:
|
||||||
@@ -302,6 +303,38 @@ func (m *Model) handleShareOpened(msg shareOpenedMsg) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// openFailed says why a share link couldn't be decrypted, in a popup
|
||||||
|
// where its offer was: its first line, or with whole all of it
|
||||||
|
// (Key Vault's refusals say what to check on later lines). From there,
|
||||||
|
// the key's secret can be gone to: its version, which may be disabled.
|
||||||
|
func (m *Model) openFailed(s share.Sealed, err error, whole bool) {
|
||||||
|
width := max(30, min(m.w-12, 76))
|
||||||
|
text := capitalize(err.Error())
|
||||||
|
first, rest, _ := strings.Cut(text, "\n")
|
||||||
|
cut := !whole && (rest != "" || ansi.StringWidth(first) > width-2)
|
||||||
|
if !whole {
|
||||||
|
text = ansi.Truncate(first, width-2, "…")
|
||||||
|
}
|
||||||
|
var lines []string
|
||||||
|
for i, l := range strings.Split(ansi.Wordwrap(text, width-2, " "), "\n") {
|
||||||
|
if i == 0 {
|
||||||
|
l = "✗ " + l
|
||||||
|
} else if l != "" {
|
||||||
|
l = " " + l
|
||||||
|
}
|
||||||
|
lines = append(lines, m.st.Error.Render(l))
|
||||||
|
}
|
||||||
|
var opts []option
|
||||||
|
if cut {
|
||||||
|
opts = append(opts, option{key: "r", label: "Show the whole message", run: func(m *Model) tea.Cmd {
|
||||||
|
m.openFailed(s, err, true)
|
||||||
|
return nil
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
opts = append(opts, option{key: "g", label: "Go to " + s.Key.Name, run: func(m *Model) tea.Cmd { return m.openRef(s.Key, actGo) }})
|
||||||
|
m.showMenu("Can't decrypt the shared files", lines, opts)
|
||||||
|
}
|
||||||
|
|
||||||
// askShareDir asks where to save files from a repo that isn't here. It
|
// askShareDir asks where to save files from a repo that isn't here. It
|
||||||
// stays until they're saved, saying why when they can't be.
|
// stays until they're saved, saying why when they can't be.
|
||||||
func (m *Model) askShareDir(msg shareOpenedMsg) tea.Cmd {
|
func (m *Model) askShareDir(msg shareOpenedMsg) tea.Cmd {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -312,3 +314,42 @@ func TestPlainEnvNotPickableFromFavourites(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Fatalf("the starred key isn't among the favourites: %d", len(favs))
|
t.Fatalf("the starred key isn't among the favourites: %d", len(favs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refusing is a provider whose values can't be read, like a disabled
|
||||||
|
// version in Key Vault.
|
||||||
|
type refusing struct{ provider.Provider }
|
||||||
|
|
||||||
|
func (refusing) Value(context.Context, provider.Object) (provider.SecretValue, error) {
|
||||||
|
return provider.SecretValue{}, &provider.Denied{
|
||||||
|
Err: errors.New("Operation get is not allowed on a disabled secret."),
|
||||||
|
Checks: []string{"the secret's version is enabled"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenFailsInAPopup(t *testing.T) {
|
||||||
|
key, _ := share.ParseKey("https://" + kv + "/secrets/db-password")
|
||||||
|
link, err := share.Seal(share.Header{Key: key, Repo: "git.example.com/team/app"},
|
||||||
|
[]byte("correct-horse-battery-staple"), []share.File{{Path: ".env", Data: []byte("A=1\n")}}, demoRules)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
d := newDriverOpts(t, Options{}, refusing{fake.Azure(0)})
|
||||||
|
d.wait = scryptWait
|
||||||
|
d.send(tea.PasteMsg{Content: link.String()})
|
||||||
|
d.keys("o")
|
||||||
|
p := d.m.prompt
|
||||||
|
if p == nil || p.title != "Can't decrypt the shared files" || !strings.Contains(d.screen(), "Can't use db-password: Operation get is not allowed") {
|
||||||
|
t.Fatalf("not in a popup:\n%s", d.screen())
|
||||||
|
}
|
||||||
|
if strings.Contains(d.screen(), "version is enabled") {
|
||||||
|
t.Error("the whole message is shown before asking")
|
||||||
|
}
|
||||||
|
d.keys("r")
|
||||||
|
if !strings.Contains(d.screen(), "version is enabled") {
|
||||||
|
t.Errorf("r doesn't show the whole message:\n%s", d.screen())
|
||||||
|
}
|
||||||
|
d.keys("g")
|
||||||
|
if _, _, sel := d.selected(); d.m.prompt != nil || (sel != "db-password" && (d.m.nav == nil || d.m.nav.ref.Name != "db-password")) {
|
||||||
|
t.Errorf("g doesn't go to the key: %q, %q\n%s", sel, d.m.flash.text, d.screen())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user