Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
c00f4b14c4
133 files changed
+26979
No files matched your search
@@ -0,0 +1,375 @@
|
||||
// Package ref parses and builds references to vaults, scopes and secrets:
|
||||
// lazysecrets:// links, and the formats people already share (Key Vault
|
||||
// identifier URLs, App Service @Microsoft.KeyVault references, portal links
|
||||
// and Databricks {{secrets/scope/key}}).
|
||||
//
|
||||
// A link names a vault, scope or object; what to do with it is up to
|
||||
// whoever opens it:
|
||||
//
|
||||
// lazysecrets://azure/subscriptions/<subscription-id>
|
||||
// lazysecrets://azure/<vault-host>[/<secrets|keys|certificates>[/<name>[/<version>]]]
|
||||
// lazysecrets://databricks/<workspace-host>[/<scope>[/<key>]]
|
||||
// lazysecrets://repo/<repo>[/<file>[/<key>]]
|
||||
//
|
||||
// A repo is its git remote without scheme or .git
|
||||
// (git.kebcloud.no/keb/infra), the file its path in the repo and the key
|
||||
// its path in the file (db.password); slashes inside them are escaped.
|
||||
//
|
||||
// Vault hosts always contain dots, so "subscriptions" can't be one.
|
||||
//
|
||||
// Every path segment is percent-encoded, so names may hold any character.
|
||||
// Query parameters are ignored (early links carried ?action=).
|
||||
package ref
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
)
|
||||
|
||||
const Scheme = "lazysecrets"
|
||||
|
||||
// Ref points at a container, or an object in one.
|
||||
type Ref struct {
|
||||
Kind provider.Kind
|
||||
// Host is the vault host (Azure), workspace host (Databricks) or repo
|
||||
// (Repo). It is empty for a bare {{secrets/scope/key}}, whose workspace
|
||||
// is unknown.
|
||||
Host string
|
||||
// Scope is the Databricks scope or the repo's file; empty for Azure.
|
||||
Scope string
|
||||
// Subscription is set for a link to an Azure subscription.
|
||||
Subscription string
|
||||
// Type is the object type, or for an Azure vault link the tab to show
|
||||
// (empty: the default tab).
|
||||
Type provider.ObjectType
|
||||
Name string // object name; empty for a container link
|
||||
Version string
|
||||
}
|
||||
|
||||
// IsSource reports whether r points at a subscription or workspace.
|
||||
func (r Ref) IsSource() bool {
|
||||
if r.Kind != provider.KindAzure {
|
||||
return r.Scope == ""
|
||||
}
|
||||
return r.Subscription != ""
|
||||
}
|
||||
|
||||
// IsContainer reports whether r points at a vault or scope.
|
||||
func (r Ref) IsContainer() bool { return r.Name == "" && !r.IsSource() }
|
||||
|
||||
// ContainerName is the vault name (from its host) or the scope.
|
||||
func (r Ref) ContainerName() string {
|
||||
if r.Kind != provider.KindAzure {
|
||||
return r.Scope
|
||||
}
|
||||
name, _, _ := strings.Cut(r.Host, ".")
|
||||
return name
|
||||
}
|
||||
|
||||
// VaultURL is the Azure vault's URL, as providers store it in
|
||||
// Container.Detail.
|
||||
func (r Ref) VaultURL() string { return "https://" + r.Host + "/" }
|
||||
|
||||
var collections = map[provider.ObjectType]string{
|
||||
provider.TypeSecret: "secrets",
|
||||
provider.TypeKey: "keys",
|
||||
provider.TypeCertificate: "certificates",
|
||||
}
|
||||
|
||||
func typeOf(collection string) (provider.ObjectType, bool) {
|
||||
for t, c := range collections {
|
||||
if strings.EqualFold(c, collection) {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// IdentifierURL is the plain Key Vault URL of an Azure reference, e.g.
|
||||
// https://kv.vault.azure.net/secrets/name[/version], usable without
|
||||
// lazysecrets (az keyvault secret show --id). Empty for Databricks.
|
||||
func (r Ref) IdentifierURL() string {
|
||||
if r.Kind != provider.KindAzure {
|
||||
return ""
|
||||
}
|
||||
u := "https://" + r.Host
|
||||
if r.Type == "" {
|
||||
return u + "/"
|
||||
}
|
||||
for _, seg := range []string{collections[r.Type], r.Name, r.Version} {
|
||||
if seg != "" {
|
||||
u += "/" + url.PathEscape(seg)
|
||||
}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// String is the lazysecrets:// link.
|
||||
func (r Ref) String() string {
|
||||
if r.Kind == provider.KindAzure && r.Subscription != "" {
|
||||
return Scheme + "://azure/subscriptions/" + url.PathEscape(r.Subscription)
|
||||
}
|
||||
segs := []string{strings.ToLower(string(r.Kind)), r.Host}
|
||||
switch r.Kind {
|
||||
case provider.KindDatabricks, provider.KindRepo:
|
||||
segs = append(segs, r.Scope, r.Name)
|
||||
default:
|
||||
if r.Type != "" {
|
||||
segs = append(segs, collections[r.Type])
|
||||
}
|
||||
segs = append(segs, r.Name, r.Version)
|
||||
}
|
||||
for len(segs) > 2 && segs[len(segs)-1] == "" {
|
||||
segs = segs[:len(segs)-1]
|
||||
}
|
||||
for i := range segs {
|
||||
segs[i] = url.PathEscape(segs[i])
|
||||
if r.Kind == provider.KindRepo && i > 0 {
|
||||
// Repos and files have slashes: keep them in their part.
|
||||
segs[i] = strings.ReplaceAll(segs[i], "/", "%2F")
|
||||
}
|
||||
}
|
||||
return Scheme + "://" + strings.Join(segs, "/")
|
||||
}
|
||||
|
||||
// ForSource is a link to a subscription or workspace.
|
||||
func ForSource(src provider.Source) (Ref, error) {
|
||||
switch src.Kind {
|
||||
case provider.KindAzure:
|
||||
return Ref{Kind: provider.KindAzure, Subscription: src.ID}, nil
|
||||
case provider.KindDatabricks:
|
||||
if src.Detail == "" {
|
||||
return Ref{}, errors.New("workspace host unknown")
|
||||
}
|
||||
return Ref{Kind: provider.KindDatabricks, Host: strings.ToLower(src.Detail)}, nil
|
||||
case provider.KindRepo:
|
||||
return Ref{Kind: provider.KindRepo, Host: strings.ToLower(src.Detail)}, nil
|
||||
}
|
||||
return Ref{}, fmt.Errorf("can't link to %s", src.Kind)
|
||||
}
|
||||
|
||||
// ForContainer is a link to a vault or scope.
|
||||
func ForContainer(c provider.Container, tab provider.ObjectType) (Ref, error) {
|
||||
switch c.Source.Kind {
|
||||
case provider.KindAzure:
|
||||
host, err := vaultHost(c.Detail)
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
}
|
||||
if tab == provider.TypeSecret {
|
||||
tab = "" // the default tab
|
||||
}
|
||||
return Ref{Kind: provider.KindAzure, Host: host, Type: tab}, nil
|
||||
case provider.KindDatabricks:
|
||||
if c.Source.Detail == "" {
|
||||
return Ref{}, errors.New("workspace host unknown")
|
||||
}
|
||||
return Ref{Kind: provider.KindDatabricks, Host: strings.ToLower(c.Source.Detail), Scope: c.Name}, nil
|
||||
case provider.KindRepo:
|
||||
return Ref{Kind: provider.KindRepo, Host: strings.ToLower(c.Source.Detail), Scope: c.Name}, nil
|
||||
}
|
||||
return Ref{}, fmt.Errorf("can't link to %s", c.Source.Kind)
|
||||
}
|
||||
|
||||
// ForObject is a link to an object, pinned to its version if given.
|
||||
func ForObject(o provider.Object, version string) (Ref, error) {
|
||||
r, err := ForContainer(o.Container, o.Type)
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
}
|
||||
r.Name, r.Type = o.Name, o.Type
|
||||
if r.Kind == provider.KindAzure {
|
||||
r.Version = version // Databricks secrets have no versions
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func vaultHost(vaultURL string) (string, error) {
|
||||
u, err := url.Parse(vaultURL)
|
||||
if err != nil || u.Host == "" {
|
||||
return "", fmt.Errorf("not a vault URL: %q", vaultURL)
|
||||
}
|
||||
return strings.ToLower(u.Host), nil
|
||||
}
|
||||
|
||||
var (
|
||||
errUnknown = errors.New("not a lazysecrets link or a Key Vault/Databricks reference")
|
||||
|
||||
// @Microsoft.KeyVault(SecretUri=…) or (VaultName=…;SecretName=…[;SecretVersion=…])
|
||||
appServiceRef = regexp.MustCompile(`(?i)^@Microsoft\.KeyVault\((.*)\)$`)
|
||||
// {{secrets/scope/key}}
|
||||
databricksRef = regexp.MustCompile(`^\{\{\s*secrets/([^/{}\s]+)/([^/{}\s]+)\s*\}\}$`)
|
||||
// …/providers/Microsoft.KeyVault/vaults/<name> in a portal link
|
||||
portalVault = regexp.MustCompile(`(?i)/providers/Microsoft\.KeyVault/vaults/([A-Za-z0-9-]+)`)
|
||||
// …/asset/Microsoft_Azure_KeyVault/<Secret|Key|Certificate>/<identifier URL>
|
||||
portalAsset = regexp.MustCompile(`(?i)/asset/Microsoft_Azure_KeyVault/[A-Za-z]+/(https://\S+)$`)
|
||||
)
|
||||
|
||||
// Parse reads any supported reference.
|
||||
func Parse(s string) (Ref, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
switch {
|
||||
case strings.HasPrefix(strings.ToLower(s), Scheme+"://"):
|
||||
return parseLink(s)
|
||||
case appServiceRef.MatchString(s):
|
||||
return parseAppService(appServiceRef.FindStringSubmatch(s)[1])
|
||||
case databricksRef.MatchString(s):
|
||||
m := databricksRef.FindStringSubmatch(s)
|
||||
return Ref{Kind: provider.KindDatabricks, Scope: m[1], Name: m[2], Type: provider.TypeSecret}, nil
|
||||
case strings.Contains(s, "portal.azure.com"):
|
||||
return parsePortal(s)
|
||||
case strings.HasPrefix(strings.ToLower(s), "https://"):
|
||||
return parseIdentifier(s)
|
||||
}
|
||||
return Ref{}, errUnknown
|
||||
}
|
||||
|
||||
func parseLink(s string) (Ref, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return Ref{}, fmt.Errorf("bad link: %w", err)
|
||||
}
|
||||
segs, err := pathSegments(u.EscapedPath())
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
}
|
||||
var r Ref
|
||||
if len(segs) == 0 || segs[0] == "" {
|
||||
return Ref{}, errors.New("link has no vault or workspace")
|
||||
}
|
||||
r.Host = strings.ToLower(segs[0])
|
||||
rest := segs[1:]
|
||||
|
||||
switch strings.ToLower(u.Host) {
|
||||
case "azure":
|
||||
r.Kind = provider.KindAzure
|
||||
if r.Host == "subscriptions" {
|
||||
if len(rest) != 1 || rest[0] == "" {
|
||||
return Ref{}, errors.New("subscription link needs exactly one subscription ID")
|
||||
}
|
||||
return Ref{Kind: provider.KindAzure, Subscription: rest[0]}, nil
|
||||
}
|
||||
if len(rest) > 3 {
|
||||
return Ref{}, errors.New("azure link has too many parts")
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
t, ok := typeOf(rest[0])
|
||||
if !ok {
|
||||
return Ref{}, fmt.Errorf("unknown object type %q (want secrets, keys or certificates)", rest[0])
|
||||
}
|
||||
r.Type = t
|
||||
}
|
||||
if len(rest) > 1 {
|
||||
r.Name = rest[1]
|
||||
}
|
||||
if len(rest) > 2 {
|
||||
r.Version = rest[2]
|
||||
}
|
||||
case "databricks", "repo":
|
||||
r.Kind = provider.KindDatabricks
|
||||
if strings.EqualFold(u.Host, "repo") {
|
||||
r.Kind = provider.KindRepo
|
||||
}
|
||||
if len(rest) > 2 {
|
||||
return Ref{}, fmt.Errorf("%s link has too many parts", strings.ToLower(u.Host))
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
r.Scope = rest[0]
|
||||
}
|
||||
if len(rest) > 1 {
|
||||
r.Name, r.Type = rest[1], provider.TypeSecret
|
||||
}
|
||||
|
||||
default:
|
||||
return Ref{}, fmt.Errorf("unknown provider %q in link", u.Host)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// pathSegments splits an escaped path on / and unescapes each segment, so
|
||||
// an escaped slash (%2F) stays inside its segment.
|
||||
func pathSegments(escaped string) ([]string, error) {
|
||||
escaped = strings.Trim(escaped, "/")
|
||||
if escaped == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parts := strings.Split(escaped, "/")
|
||||
for i, p := range parts {
|
||||
s, err := url.PathUnescape(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad link: %w", err)
|
||||
}
|
||||
parts[i] = s
|
||||
}
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
// parseIdentifier reads a Key Vault URL: a vault, or an object identifier
|
||||
// like https://kv.vault.azure.net/secrets/name[/version].
|
||||
func parseIdentifier(s string) (Ref, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil || u.Host == "" {
|
||||
return Ref{}, errUnknown
|
||||
}
|
||||
host := strings.ToLower(u.Host)
|
||||
if !strings.Contains(host, ".vault.") {
|
||||
return Ref{}, errUnknown
|
||||
}
|
||||
segs, err := pathSegments(u.EscapedPath())
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
}
|
||||
r := Ref{Kind: provider.KindAzure, Host: host}
|
||||
if len(segs) > 0 {
|
||||
t, ok := typeOf(segs[0])
|
||||
if !ok {
|
||||
return Ref{}, fmt.Errorf("unknown Key Vault path %q", u.Path)
|
||||
}
|
||||
r.Type = t
|
||||
}
|
||||
if len(segs) > 1 {
|
||||
r.Name = segs[1]
|
||||
}
|
||||
if len(segs) > 2 {
|
||||
r.Version = segs[2]
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func parseAppService(args string) (Ref, error) {
|
||||
fields := map[string]string{}
|
||||
for _, kv := range strings.Split(args, ";") {
|
||||
k, v, ok := strings.Cut(kv, "=")
|
||||
if ok {
|
||||
fields[strings.ToLower(strings.TrimSpace(k))] = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
if uri := fields["secreturi"]; uri != "" {
|
||||
return parseIdentifier(uri)
|
||||
}
|
||||
vault, name := fields["vaultname"], fields["secretname"]
|
||||
if vault == "" || name == "" {
|
||||
return Ref{}, errors.New("@Microsoft.KeyVault reference needs SecretUri, or VaultName and SecretName")
|
||||
}
|
||||
return Ref{
|
||||
Kind: provider.KindAzure, Host: strings.ToLower(vault) + ".vault.azure.net",
|
||||
Type: provider.TypeSecret, Name: name, Version: fields["secretversion"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parsePortal reads Azure portal links to a vault or a Key Vault object.
|
||||
func parsePortal(s string) (Ref, error) {
|
||||
if m := portalAsset.FindStringSubmatch(s); m != nil {
|
||||
return parseIdentifier(m[1])
|
||||
}
|
||||
if m := portalVault.FindStringSubmatch(s); m != nil {
|
||||
// Portal vault links don't carry the cloud; assume public Azure.
|
||||
return Ref{Kind: provider.KindAzure, Host: strings.ToLower(m[1]) + ".vault.azure.net"}, nil
|
||||
}
|
||||
return Ref{}, errUnknown
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package ref
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
kvHost = "kv-app-prod.vault.azure.net"
|
||||
dbHost = "adb-1111111111111111.11.azuredatabricks.net"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
secret := Ref{Kind: provider.KindAzure, Host: kvHost, Type: provider.TypeSecret, Name: "db-password"}
|
||||
versioned := secret
|
||||
versioned.Version = "91d3e0"
|
||||
vault := Ref{Kind: provider.KindAzure, Host: kvHost}
|
||||
keysTab := vault
|
||||
keysTab.Type = provider.TypeKey
|
||||
cert := Ref{Kind: provider.KindAzure, Host: kvHost, Type: provider.TypeCertificate, Name: "app-tls"}
|
||||
scope := Ref{Kind: provider.KindDatabricks, Host: dbHost, Scope: "app"}
|
||||
dbxSecret := Ref{Kind: provider.KindDatabricks, Host: dbHost, Scope: "app", Name: "storage-key", Type: provider.TypeSecret}
|
||||
|
||||
for in, want := range map[string]Ref{
|
||||
// lazysecrets links
|
||||
"lazysecrets://azure/" + kvHost: vault,
|
||||
"lazysecrets://azure/" + kvHost + "/": vault,
|
||||
"lazysecrets://azure/" + kvHost + "/keys": keysTab,
|
||||
"lazysecrets://azure/" + kvHost + "/secrets/db-password": secret,
|
||||
"lazysecrets://azure/" + kvHost + "/secrets/db-password/91d3e0": versioned,
|
||||
"lazysecrets://azure/" + kvHost + "/secrets/db-password?action=copy": secret, // early links: ignored
|
||||
"LazySecrets://Azure/" + strings.ToUpper(kvHost) + "/secrets/db-password": secret,
|
||||
"lazysecrets://databricks/" + dbHost + "/app": scope,
|
||||
"lazysecrets://databricks/" + dbHost + "/app/storage-key": dbxSecret,
|
||||
// formats people already share
|
||||
"https://" + kvHost + "/secrets/db-password": secret,
|
||||
"https://" + kvHost + "/secrets/db-password/91d3e0": versioned,
|
||||
"https://" + kvHost + "/": vault,
|
||||
"https://" + kvHost + "/certificates/app-tls": cert,
|
||||
"@Microsoft.KeyVault(SecretUri=https://" + kvHost + "/secrets/db-password/)": secret,
|
||||
"@Microsoft.KeyVault(VaultName=kv-app-prod;SecretName=db-password)": secret,
|
||||
"@Microsoft.KeyVault(VaultName=kv-app-prod;SecretName=db-password;SecretVersion=91d3e0)": versioned,
|
||||
" {{secrets/app/storage-key}} ": {Kind: provider.KindDatabricks, Scope: "app", Name: "storage-key", Type: provider.TypeSecret},
|
||||
"https://portal.azure.com/#@t/resource/subscriptions/s/resourceGroups/rg/providers/Microsoft.KeyVault/vaults/kv-app-prod/overview": vault,
|
||||
"https://portal.azure.com/#@t/asset/Microsoft_Azure_KeyVault/Certificate/https://" + kvHost + "/certificates/app-tls": cert,
|
||||
} {
|
||||
got, err := Parse(in)
|
||||
if err != nil {
|
||||
t.Errorf("Parse(%q): %v", in, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Parse(%q)\n got %+v\nwant %+v", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejects(t *testing.T) {
|
||||
for _, in := range []string{
|
||||
"",
|
||||
"hunter2",
|
||||
"https://example.com/secrets/x",
|
||||
"lazysecrets://aws/secret",
|
||||
"lazysecrets://azure/",
|
||||
"lazysecrets://azure/" + kvHost + "/passwords/x",
|
||||
"lazysecrets://azure/" + kvHost + "/secrets/a/b/c",
|
||||
"lazysecrets://azure/subscriptions/",
|
||||
"lazysecrets://azure/subscriptions/a/b",
|
||||
"@Microsoft.KeyVault(VaultName=kv)",
|
||||
} {
|
||||
if r, err := Parse(in); err == nil {
|
||||
t.Errorf("Parse(%q) = %+v, want an error", in, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinksRoundTrip(t *testing.T) {
|
||||
kv := provider.Container{
|
||||
Source: provider.Source{Kind: provider.KindAzure, ID: "sub"},
|
||||
Name: "kv-app-prod", Detail: "https://" + kvHost + "/",
|
||||
}
|
||||
scope := provider.Container{
|
||||
Source: provider.Source{Kind: provider.KindDatabricks, ID: "prod", Detail: dbHost},
|
||||
Name: "team@data",
|
||||
}
|
||||
key := provider.Object{Container: kv, Type: provider.TypeKey, Name: "signing-key"}
|
||||
dbx := provider.Object{Container: scope, Type: provider.TypeSecret, Name: "storage.key"}
|
||||
|
||||
links := map[string]func() (Ref, error){
|
||||
"lazysecrets://azure/" + kvHost: func() (Ref, error) { return ForContainer(kv, provider.TypeSecret) },
|
||||
"lazysecrets://azure/" + kvHost + "/keys": func() (Ref, error) { return ForContainer(kv, provider.TypeKey) },
|
||||
"lazysecrets://azure/" + kvHost + "/keys/signing-key/v9": func() (Ref, error) { return ForObject(key, "v9") },
|
||||
"lazysecrets://databricks/" + dbHost + "/team@data": func() (Ref, error) { return ForContainer(scope, provider.TypeSecret) },
|
||||
"lazysecrets://databricks/" + dbHost + "/team@data/storage.key": func() (Ref, error) { return ForObject(dbx, "") },
|
||||
}
|
||||
for want, build := range links {
|
||||
r, err := build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := r.String(); got != want {
|
||||
t.Errorf("link = %q, want %q", got, want)
|
||||
}
|
||||
back, err := Parse(r.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Parse(%q): %v", r.String(), err)
|
||||
}
|
||||
if !reflect.DeepEqual(back, r) {
|
||||
t.Errorf("round trip of %q\n got %+v\nwant %+v", want, back, r)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSlashInNamesIsEscaped(t *testing.T) {
|
||||
r := Ref{Kind: provider.KindDatabricks, Host: dbHost, Scope: "a/b", Name: "c", Type: provider.TypeSecret}
|
||||
back, err := Parse(r.String())
|
||||
if err != nil || back.Scope != "a/b" || back.Name != "c" {
|
||||
t.Errorf("%q → %+v, %v", r.String(), back, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifierURL(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"lazysecrets://azure/" + kvHost + "/secrets/db-password/91d3e0": "https://" + kvHost + "/secrets/db-password/91d3e0",
|
||||
"lazysecrets://azure/" + kvHost + "/keys/signing-key": "https://" + kvHost + "/keys/signing-key",
|
||||
"lazysecrets://azure/" + kvHost: "https://" + kvHost + "/",
|
||||
"lazysecrets://databricks/" + dbHost + "/app/k": "",
|
||||
} {
|
||||
r, err := Parse(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := r.IdentifierURL(); got != want {
|
||||
t.Errorf("%s: %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceLinks(t *testing.T) {
|
||||
az := provider.Source{Kind: provider.KindAzure, ID: "4d3073a0-1111"}
|
||||
dbx := provider.Source{Kind: provider.KindDatabricks, ID: "prod", Detail: dbHost}
|
||||
for src, want := range map[provider.Source]string{
|
||||
az: "lazysecrets://azure/subscriptions/4d3073a0-1111",
|
||||
dbx: "lazysecrets://databricks/" + dbHost,
|
||||
} {
|
||||
r, err := ForSource(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.String() != want || !r.IsSource() || r.IsContainer() {
|
||||
t.Errorf("%s: %q source=%v container=%v", src.Kind, r.String(), r.IsSource(), r.IsContainer())
|
||||
}
|
||||
back, err := Parse(want)
|
||||
if err != nil || !reflect.DeepEqual(back, r) {
|
||||
t.Errorf("round trip %q: %+v, %v", want, back, err)
|
||||
}
|
||||
}
|
||||
// Vault and scope links aren't source links.
|
||||
for _, link := range []string{"lazysecrets://azure/" + kvHost, "lazysecrets://databricks/" + dbHost + "/app"} {
|
||||
if r, _ := Parse(link); r.IsSource() || !r.IsContainer() {
|
||||
t.Errorf("%s: source=%v", link, r.IsSource())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoLinks(t *testing.T) {
|
||||
src := provider.Source{Kind: provider.KindRepo, Detail: "git.kebcloud.no/keb/nix-config"}
|
||||
c := provider.Container{Source: src, Name: "secrets/server.yaml"}
|
||||
r, err := ForObject(provider.Object{Container: c, Name: "db.password", Type: provider.TypeSecret}, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := r.String()
|
||||
if link != "lazysecrets://repo/git.kebcloud.no%2Fkeb%2Fnix-config/secrets%2Fserver.yaml/db.password" {
|
||||
t.Errorf("link %s", link)
|
||||
}
|
||||
back, err := Parse(link)
|
||||
if err != nil || back.Kind != provider.KindRepo || back.Host != "git.kebcloud.no/keb/nix-config" ||
|
||||
back.Scope != "secrets/server.yaml" || back.Name != "db.password" {
|
||||
t.Errorf("parsed %+v, %v", back, err)
|
||||
}
|
||||
if fr, _ := ForSource(src); !fr.IsSource() || fr.String() != "lazysecrets://repo/git.kebcloud.no%2Fkeb%2Fnix-config" {
|
||||
t.Errorf("source link %s", fr.String())
|
||||
}
|
||||
if cr, _ := ForContainer(c, provider.TypeSecret); !cr.IsContainer() || cr.ContainerName() != "secrets/server.yaml" {
|
||||
t.Errorf("file link %+v", cr)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user