feat: share env files as encrypted links
S → f on a repo, an env file or a key in one copies a lazysecrets://share link holding the env files, encrypted (age) with a secret's value: whoever can read that secret can open it. The key is share.key in the repo's .lazysecrets.yaml, else in the config, else picked when sharing; on Azure the link pins its version. Opening one says what it holds, decrypts when asked, and saves the files in the clone of the repo (or a folder chosen), replacing existing ones only when asked. lazysecrets share prints a link from the command line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
26 files changed
+1541
-98
No files matched your search
@@ -453,11 +453,8 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
|
||||
}
|
||||
switch o.Type {
|
||||
case provider.TypeSecret:
|
||||
resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil)
|
||||
if err != nil {
|
||||
return provider.SecretValue{}, cleanError(err)
|
||||
}
|
||||
return provider.NewSecretValue([]byte(deref(resp.Value))), nil
|
||||
v, _, err := p.VersionedValue(ctx, o)
|
||||
return v, err
|
||||
case provider.TypeKey:
|
||||
resp, err := vc.keys.GetKey(ctx, o.Name, o.Version, nil)
|
||||
if err != nil {
|
||||
@@ -481,7 +478,31 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
|
||||
return provider.SecretValue{}, provider.ErrUnsupported
|
||||
}
|
||||
|
||||
var _ provider.Locator = (*Provider)(nil)
|
||||
// VersionedValue is Value, with the version read.
|
||||
func (p *Provider) VersionedValue(ctx context.Context, o provider.Object) (provider.SecretValue, string, error) {
|
||||
if o.Type != provider.TypeSecret {
|
||||
v, err := p.Value(ctx, o)
|
||||
return v, o.Version, err
|
||||
}
|
||||
vc, err := p.clients(o.Container.Detail)
|
||||
if err != nil {
|
||||
return provider.SecretValue{}, "", err
|
||||
}
|
||||
resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil)
|
||||
if err != nil {
|
||||
return provider.SecretValue{}, "", cleanError(err)
|
||||
}
|
||||
version := o.Version
|
||||
if resp.ID != nil {
|
||||
version = resp.ID.Version()
|
||||
}
|
||||
return provider.NewSecretValue([]byte(deref(resp.Value))), version, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ provider.Locator = (*Provider)(nil)
|
||||
_ provider.Versioned = (*Provider)(nil)
|
||||
)
|
||||
|
||||
// validHost guards the Resource Graph query against odd input.
|
||||
var validHost = regexp.MustCompile(`^[a-z0-9.-]+$`)
|
||||
|
||||
@@ -35,7 +35,7 @@ func Repos() *Provider {
|
||||
// commit are what git says of it: "tracked", "ignored" or "untracked".
|
||||
file := func(src provider.Source, path, method, git, commit string, kv ...string) {
|
||||
sops := method != ""
|
||||
detail := "env · plain text"
|
||||
detail := provider.DetailPlainEnv
|
||||
if sops {
|
||||
detail = "sops · " + method
|
||||
}
|
||||
|
||||
@@ -167,6 +167,13 @@ type Current interface {
|
||||
Current(ctx context.Context) (Source, bool)
|
||||
}
|
||||
|
||||
// Versioned is implemented by providers whose secrets have versions: it
|
||||
// is Value, also returning the version read (the current one when
|
||||
// o.Version is empty), so a link can pin it.
|
||||
type Versioned interface {
|
||||
VersionedValue(ctx context.Context, o Object) (SecretValue, string, error)
|
||||
}
|
||||
|
||||
// Locator is implemented by providers that can find where a container lives,
|
||||
// to open links. address is a vault host (kv.vault.azure.net) or a
|
||||
// workspace host. It returns the ID of the source holding it.
|
||||
@@ -220,6 +227,10 @@ func (d *Denied) Error() string {
|
||||
|
||||
func (d *Denied) Unwrap() error { return d.Err }
|
||||
|
||||
// DetailPlainEnv is the Detail of a container that is a plain (not
|
||||
// encrypted) env file in a repo.
|
||||
const DetailPlainEnv = "env · plain text"
|
||||
|
||||
// ContainerDetails is implemented by providers that can say more about a
|
||||
// container than its listing does: how a file is encrypted and what git
|
||||
// knows of it. ErrUnsupported means there's nothing more for c.
|
||||
|
||||
@@ -311,8 +311,8 @@ func (p *Provider) Containers(ctx context.Context, src provider.Source) ([]provi
|
||||
c.Detail = "sops · " + strings.Join(meta.methods, ", ")
|
||||
case isSops:
|
||||
return // a SOPS file where .sops.yaml says SOPS files don't go
|
||||
case isEnv(f):
|
||||
c.Detail = "env · plain text"
|
||||
case IsEnv(f):
|
||||
c.Detail = provider.DetailPlainEnv
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -464,6 +464,16 @@ func (p *Provider) Locate(_ context.Context, host string) (string, bool, error)
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// Root is the root of the git repo dir is in, and the repo's ID: its
|
||||
// remote without scheme or .git (a repo link's host), else the root.
|
||||
func Root(dir string) (root, id string, err error) {
|
||||
root, err = gitRoot(dir)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("%s isn't in a git repository", dir)
|
||||
}
|
||||
return root, info(root, "").id, nil
|
||||
}
|
||||
|
||||
// --- git ---
|
||||
|
||||
func gitRoot(dir string) (string, error) {
|
||||
@@ -568,6 +578,27 @@ func normalizeRemote(remote string) (string, string) {
|
||||
type lazysecretsFile struct {
|
||||
Include []string `yaml:"include"`
|
||||
Exclude []string `yaml:"exclude"`
|
||||
Share struct {
|
||||
// Key is the link to the secret that encrypts the repo's
|
||||
// shared env files (package share).
|
||||
Key string `yaml:"key"`
|
||||
} `yaml:"share"`
|
||||
}
|
||||
|
||||
// ShareKey is the share.key of the repo's .lazysecrets.yaml, if any.
|
||||
func ShareKey(root string) (string, error) {
|
||||
b, err := os.ReadFile(filepath.Join(root, ".lazysecrets.yaml"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var f lazysecretsFile
|
||||
if err := yaml.Unmarshal(b, &f); err != nil {
|
||||
return "", fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err)
|
||||
}
|
||||
return f.Share.Key, nil
|
||||
}
|
||||
|
||||
// selector says which files to look at: user (the include/exclude of the
|
||||
@@ -618,7 +649,7 @@ func envFiles(root string, tracked []string) []string {
|
||||
dirs := map[string]bool{".": true}
|
||||
for _, f := range tracked {
|
||||
isTracked[f] = true
|
||||
if isEnv(f) {
|
||||
if IsEnv(f) {
|
||||
out = append(out, f)
|
||||
}
|
||||
if d := path.Dir(f); !dirs[d] && !inPackages(f) {
|
||||
@@ -632,7 +663,7 @@ func envFiles(root string, tracked []string) []string {
|
||||
}
|
||||
for _, e := range entries {
|
||||
f := path.Join(d, e.Name())
|
||||
if e.Type().IsRegular() && !isTracked[f] && isEnv(f) {
|
||||
if e.Type().IsRegular() && !isTracked[f] && IsEnv(f) {
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
@@ -663,10 +694,10 @@ func inPackages(f string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isEnv reports whether a file is named like an env file with real values:
|
||||
// IsEnv reports whether a file is named like an env file with real values:
|
||||
// .env, .env.local, prod.env; not .env.example and the like, nor one in
|
||||
// dependencies.
|
||||
func isEnv(f string) bool {
|
||||
func IsEnv(f string) bool {
|
||||
if inPackages(f) {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user