1090 lines
34 KiB
Go
1090 lines
34 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.julianfamily.org/keepassgo/apitokens"
|
|
"git.julianfamily.org/keepassgo/passwords"
|
|
keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1"
|
|
"git.julianfamily.org/keepassgo/session"
|
|
"git.julianfamily.org/keepassgo/vault"
|
|
"git.julianfamily.org/keepassgo/webdav"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/grpc/test/bufconn"
|
|
)
|
|
|
|
func TestVaultServiceRejectsRequestsWithoutBearerToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
_, err := client.ListEntries(context.Background(), &keepassgov1.ListEntriesRequest{})
|
|
if status.Code(err) != codes.Unauthenticated {
|
|
t.Fatalf("ListEntries() code = %v, want %v", status.Code(err), codes.Unauthenticated)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceRejectsExpiredAPITokens(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expiredAt := time.Date(2026, 3, 29, 11, 59, 0, 0, time.UTC)
|
|
token, _, err := apitokens.Issue("Expired", "grpc-test", &expiredAt, time.Date(2026, 3, 29, 10, 0, 0, 0, time.UTC))
|
|
if err != nil {
|
|
t.Fatalf("Issue() error = %v", err)
|
|
}
|
|
token.SecretHash = hashSecretForTest(defaultTestTokenSecret)
|
|
client, _, cleanup := newTestClientForModel(t, vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "git-server",
|
|
Title: "Git Server",
|
|
Username: "joejulian",
|
|
Password: "token-1",
|
|
URL: "https://git.julianfamily.org",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
token.Entry([]string{"Root", "API Tokens"}),
|
|
},
|
|
})
|
|
defer cleanup()
|
|
|
|
oldNow := timeNow
|
|
timeNow = func() time.Time { return time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC) }
|
|
defer func() { timeNow = oldNow }()
|
|
|
|
_, err = client.ListEntries(tokenContext(defaultTestTokenSecret), &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
|
|
if status.Code(err) != codes.Unauthenticated {
|
|
t.Fatalf("ListEntries() code = %v, want %v for expired token", status.Code(err), codes.Unauthenticated)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceRejectsUnauthorizedEntryAccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClientForModel(t, vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "git-server",
|
|
Title: "Git Server",
|
|
Username: "joejulian",
|
|
Password: "token-1",
|
|
URL: "https://git.julianfamily.org",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
testAPITokenEntry(t,
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root", "Internet"}}},
|
|
),
|
|
},
|
|
})
|
|
defer cleanup()
|
|
|
|
_, err := client.CopyEntryField(tokenContext(defaultTestTokenSecret), &keepassgov1.CopyEntryFieldRequest{Id: "git-server", Target: "password"})
|
|
if status.Code(err) != codes.PermissionDenied {
|
|
t.Fatalf("CopyEntryField() code = %v, want %v", status.Code(err), codes.PermissionDenied)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceReportsSessionStatusAndSupportsLockUnlock(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
lifecycle := &stubLifecycle{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "git-server",
|
|
Title: "Git Server",
|
|
Username: "joejulian",
|
|
Password: "token-1",
|
|
URL: "https://git.julianfamily.org",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
client, _, cleanup := newTestClientWithLifecycle(t, lifecycle)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
statusResp, err := client.GetSessionStatus(ctx, &keepassgov1.GetSessionStatusRequest{})
|
|
if err != nil {
|
|
t.Fatalf("GetSessionStatus() error = %v", err)
|
|
}
|
|
if statusResp.Locked {
|
|
t.Fatal("GetSessionStatus().Locked = true, want false at startup")
|
|
}
|
|
if statusResp.EntryCount == 0 {
|
|
t.Fatal("GetSessionStatus().EntryCount = 0, want non-zero")
|
|
}
|
|
|
|
if _, err := client.LockVault(ctx, &keepassgov1.LockVaultRequest{}); err != nil {
|
|
t.Fatalf("LockVault() error = %v", err)
|
|
}
|
|
|
|
statusResp, err = client.GetSessionStatus(ctx, &keepassgov1.GetSessionStatusRequest{})
|
|
if err != nil {
|
|
t.Fatalf("GetSessionStatus() after lock error = %v", err)
|
|
}
|
|
if !statusResp.Locked {
|
|
t.Fatal("GetSessionStatus().Locked = false, want true after lock")
|
|
}
|
|
|
|
if _, err := client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}}); status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("ListEntries() code = %v, want FailedPrecondition while locked", status.Code(err))
|
|
}
|
|
|
|
if _, err := client.UnlockVault(ctx, &keepassgov1.UnlockVaultRequest{}); err != nil {
|
|
t.Fatalf("UnlockVault() error = %v", err)
|
|
}
|
|
|
|
statusResp, err = client.GetSessionStatus(ctx, &keepassgov1.GetSessionStatusRequest{})
|
|
if err != nil {
|
|
t.Fatalf("GetSessionStatus() after unlock error = %v", err)
|
|
}
|
|
if statusResp.Locked {
|
|
t.Fatal("GetSessionStatus().Locked = true, want false after unlock")
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceLockAndUnlockUseLifecycleBackend(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
lifecycle := &stubLifecycle{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "entry-1", Title: "Remote Git", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
unlockPassword: "correct horse battery staple",
|
|
unlockKeyFile: []byte("key-material"),
|
|
}
|
|
client, _, cleanup := newTestClientWithLifecycle(t, lifecycle)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
if _, err := client.OpenVault(ctx, &keepassgov1.OpenVaultRequest{
|
|
Path: "/tmp/test.kdbx",
|
|
Password: lifecycle.unlockPassword,
|
|
KeyFileData: lifecycle.unlockKeyFile,
|
|
}); err != nil {
|
|
t.Fatalf("OpenVault() error = %v", err)
|
|
}
|
|
|
|
if _, err := client.LockVault(ctx, &keepassgov1.LockVaultRequest{}); err != nil {
|
|
t.Fatalf("LockVault() error = %v", err)
|
|
}
|
|
if !lifecycle.locked {
|
|
t.Fatal("LockVault() did not lock lifecycle backend")
|
|
}
|
|
|
|
statusResp, err := client.GetSessionStatus(ctx, &keepassgov1.GetSessionStatusRequest{})
|
|
if err != nil {
|
|
t.Fatalf("GetSessionStatus() after lock error = %v", err)
|
|
}
|
|
if !statusResp.Locked {
|
|
t.Fatal("GetSessionStatus().Locked = false, want true after lock")
|
|
}
|
|
|
|
if _, err := client.UnlockVault(ctx, &keepassgov1.UnlockVaultRequest{
|
|
Password: "wrong password",
|
|
KeyFileData: lifecycle.unlockKeyFile,
|
|
}); status.Code(err) != codes.InvalidArgument {
|
|
t.Fatalf("UnlockVault(wrong password) code = %v, want %v", status.Code(err), codes.InvalidArgument)
|
|
}
|
|
|
|
statusResp, err = client.GetSessionStatus(ctx, &keepassgov1.GetSessionStatusRequest{})
|
|
if err != nil {
|
|
t.Fatalf("GetSessionStatus() after failed unlock error = %v", err)
|
|
}
|
|
if !statusResp.Locked {
|
|
t.Fatal("GetSessionStatus().Locked = false, want true after failed unlock")
|
|
}
|
|
|
|
if _, err := client.UnlockVault(ctx, &keepassgov1.UnlockVaultRequest{
|
|
Password: lifecycle.unlockPassword,
|
|
KeyFileData: lifecycle.unlockKeyFile,
|
|
}); err != nil {
|
|
t.Fatalf("UnlockVault() error = %v", err)
|
|
}
|
|
if lifecycle.lastUnlockKey.Password != lifecycle.unlockPassword {
|
|
t.Fatalf("UnlockVault() password = %q, want %q", lifecycle.lastUnlockKey.Password, lifecycle.unlockPassword)
|
|
}
|
|
if !bytes.Equal(lifecycle.lastUnlockKey.KeyFileData, lifecycle.unlockKeyFile) {
|
|
t.Fatalf("UnlockVault() key data = %q, want %q", lifecycle.lastUnlockKey.KeyFileData, lifecycle.unlockKeyFile)
|
|
}
|
|
|
|
listed, err := client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
|
|
if err != nil {
|
|
t.Fatalf("ListEntries() after unlock error = %v", err)
|
|
}
|
|
if len(listed.Entries) != 1 || listed.Entries[0].Title != "Remote Git" {
|
|
t.Fatalf("ListEntries().Entries = %#v, want Remote Git after unlock", listed.Entries)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceOpensAndSavesVaultThroughLifecycleBackend(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
lifecycle := &stubLifecycle{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "entry-1", Title: "Remote Git", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
}
|
|
client, _, cleanup := newTestClientWithLifecycle(t, lifecycle)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
if _, err := client.OpenVault(ctx, &keepassgov1.OpenVaultRequest{
|
|
Path: "/tmp/test.kdbx",
|
|
Password: "correct horse battery staple",
|
|
}); err != nil {
|
|
t.Fatalf("OpenVault() error = %v", err)
|
|
}
|
|
if lifecycle.openPath != "/tmp/test.kdbx" {
|
|
t.Fatalf("openPath = %q, want /tmp/test.kdbx", lifecycle.openPath)
|
|
}
|
|
|
|
listed, err := client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
|
|
if err != nil {
|
|
t.Fatalf("ListEntries() after open error = %v", err)
|
|
}
|
|
if len(listed.Entries) != 1 || listed.Entries[0].Title != "Remote Git" {
|
|
t.Fatalf("ListEntries().Entries = %#v, want Remote Git after open", listed.Entries)
|
|
}
|
|
|
|
if _, err := client.SaveVault(ctx, &keepassgov1.SaveVaultRequest{}); err != nil {
|
|
t.Fatalf("SaveVault() error = %v", err)
|
|
}
|
|
if !lifecycle.saved {
|
|
t.Fatal("SaveVault() did not call lifecycle Save")
|
|
}
|
|
|
|
if _, err := client.OpenRemoteVault(ctx, &keepassgov1.OpenRemoteVaultRequest{
|
|
BaseUrl: "https://dav.example.com",
|
|
Path: "vaults/main.kdbx",
|
|
Username: "jjulian",
|
|
Password: "dav-token",
|
|
MasterPassword: "correct horse battery staple",
|
|
}); err != nil {
|
|
t.Fatalf("OpenRemoteVault() error = %v", err)
|
|
}
|
|
if lifecycle.remoteBaseURL != "https://dav.example.com" || lifecycle.remotePath != "vaults/main.kdbx" {
|
|
t.Fatalf("remote open = %q %q, want dav.example.com vaults/main.kdbx", lifecycle.remoteBaseURL, lifecycle.remotePath)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceLifecycleMethodsRequireLifecycleBackend(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
call func() error
|
|
}{
|
|
{
|
|
name: "open",
|
|
call: func() error {
|
|
_, err := client.OpenVault(ctx, &keepassgov1.OpenVaultRequest{Path: "/tmp/test.kdbx"})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "open_remote",
|
|
call: func() error {
|
|
_, err := client.OpenRemoteVault(ctx, &keepassgov1.OpenRemoteVaultRequest{
|
|
BaseUrl: "https://dav.example.com",
|
|
Path: "vaults/main.kdbx",
|
|
})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "save",
|
|
call: func() error {
|
|
_, err := client.SaveVault(ctx, &keepassgov1.SaveVaultRequest{})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "lock",
|
|
call: func() error {
|
|
_, err := client.LockVault(ctx, &keepassgov1.LockVaultRequest{})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "unlock",
|
|
call: func() error {
|
|
_, err := client.UnlockVault(ctx, &keepassgov1.UnlockVaultRequest{})
|
|
return err
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.call()
|
|
if status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("%s code = %v, want %v", tt.name, status.Code(err), codes.FailedPrecondition)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceLifecycleMethodsMapBackendErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
call func(keepassgov1.VaultServiceClient, context.Context) error
|
|
err error
|
|
want codes.Code
|
|
}{
|
|
{
|
|
name: "open not found",
|
|
call: func(client keepassgov1.VaultServiceClient, ctx context.Context) error {
|
|
_, err := client.OpenVault(ctx, &keepassgov1.OpenVaultRequest{Path: "/tmp/missing.kdbx"})
|
|
return err
|
|
},
|
|
err: os.ErrNotExist,
|
|
want: codes.NotFound,
|
|
},
|
|
{
|
|
name: "open invalid master key",
|
|
call: func(client keepassgov1.VaultServiceClient, ctx context.Context) error {
|
|
_, err := client.OpenVault(ctx, &keepassgov1.OpenVaultRequest{Path: "/tmp/test.kdbx"})
|
|
return err
|
|
},
|
|
err: vault.ErrInvalidMasterKey,
|
|
want: codes.InvalidArgument,
|
|
},
|
|
{
|
|
name: "save no path",
|
|
call: func(client keepassgov1.VaultServiceClient, ctx context.Context) error {
|
|
_, err := client.SaveVault(ctx, &keepassgov1.SaveVaultRequest{})
|
|
return err
|
|
},
|
|
err: session.ErrNoPath,
|
|
want: codes.FailedPrecondition,
|
|
},
|
|
{
|
|
name: "lock already locked",
|
|
call: func(client keepassgov1.VaultServiceClient, ctx context.Context) error {
|
|
_, err := client.LockVault(ctx, &keepassgov1.LockVaultRequest{})
|
|
return err
|
|
},
|
|
err: session.ErrLocked,
|
|
want: codes.FailedPrecondition,
|
|
},
|
|
{
|
|
name: "unlock invalid master key",
|
|
call: func(client keepassgov1.VaultServiceClient, ctx context.Context) error {
|
|
_, err := client.UnlockVault(ctx, &keepassgov1.UnlockVaultRequest{Password: "wrong"})
|
|
return err
|
|
},
|
|
err: vault.ErrInvalidMasterKey,
|
|
want: codes.InvalidArgument,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
lifecycle := &stubLifecycle{err: tt.err}
|
|
client, _, cleanup := newTestClientWithLifecycle(t, lifecycle)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
err := tt.call(client, ctx)
|
|
if status.Code(err) != tt.want {
|
|
t.Fatalf("%s code = %v, want %v", tt.name, status.Code(err), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceListsEntriesForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
resp, err := client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
|
|
if err != nil {
|
|
t.Fatalf("ListEntries() error = %v", err)
|
|
}
|
|
|
|
if len(resp.Entries) != 1 {
|
|
t.Fatalf("len(ListEntries().Entries) = %d, want 1", len(resp.Entries))
|
|
}
|
|
|
|
if resp.Entries[0].Title != "Git Server" {
|
|
t.Fatalf("ListEntries().Entries[0].Title = %q, want %q", resp.Entries[0].Title, "Git Server")
|
|
}
|
|
if got := resp.Entries[0].Fields["X-Role"]; got != "automation" {
|
|
t.Fatalf("ListEntries().Entries[0].Fields[X-Role] = %q, want %q", got, "automation")
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceListsCreatesAndRenamesGroupsForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
|
|
listed, err := client.ListGroups(ctx, &keepassgov1.ListGroupsRequest{Path: []string{"Root"}})
|
|
if err != nil {
|
|
t.Fatalf("ListGroups() error = %v", err)
|
|
}
|
|
if len(listed.Names) != 2 || listed.Names[0] != "Home Assistant" || listed.Names[1] != "Internet" {
|
|
t.Fatalf("ListGroups().Names = %#v, want [Home Assistant Internet]", listed.Names)
|
|
}
|
|
|
|
if _, err := client.CreateGroup(ctx, &keepassgov1.CreateGroupRequest{
|
|
ParentPath: []string{"Root"},
|
|
Name: "Finance",
|
|
}); err != nil {
|
|
t.Fatalf("CreateGroup() error = %v", err)
|
|
}
|
|
|
|
listed, err = client.ListGroups(ctx, &keepassgov1.ListGroupsRequest{Path: []string{"Root"}})
|
|
if err != nil {
|
|
t.Fatalf("ListGroups() error = %v", err)
|
|
}
|
|
if len(listed.Names) != 3 || listed.Names[0] != "Finance" {
|
|
t.Fatalf("ListGroups().Names = %#v, want Finance present after create", listed.Names)
|
|
}
|
|
|
|
if _, err := client.RenameGroup(ctx, &keepassgov1.RenameGroupRequest{
|
|
Path: []string{"Root", "Internet"},
|
|
NewName: "Infra",
|
|
}); err != nil {
|
|
t.Fatalf("RenameGroup() error = %v", err)
|
|
}
|
|
|
|
listed, err = client.ListGroups(ctx, &keepassgov1.ListGroupsRequest{Path: []string{"Root"}})
|
|
if err != nil {
|
|
t.Fatalf("ListGroups() error = %v", err)
|
|
}
|
|
if len(listed.Names) != 3 || listed.Names[2] != "Infra" {
|
|
t.Fatalf("ListGroups().Names = %#v, want Infra after rename", listed.Names)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceDeletesEmptyGroupsAndRejectsNonEmptyGroups(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
if _, err := client.CreateGroup(ctx, &keepassgov1.CreateGroupRequest{
|
|
ParentPath: []string{"Root"},
|
|
Name: "Finance",
|
|
}); err != nil {
|
|
t.Fatalf("CreateGroup() error = %v", err)
|
|
}
|
|
|
|
if _, err := client.DeleteGroup(ctx, &keepassgov1.DeleteGroupRequest{
|
|
Path: []string{"Root", "Finance"},
|
|
}); err != nil {
|
|
t.Fatalf("DeleteGroup() error = %v, want success for empty group", err)
|
|
}
|
|
|
|
listed, err := client.ListGroups(ctx, &keepassgov1.ListGroupsRequest{Path: []string{"Root"}})
|
|
if err != nil {
|
|
t.Fatalf("ListGroups() error = %v", err)
|
|
}
|
|
if len(listed.Names) != 2 || listed.Names[0] != "Home Assistant" || listed.Names[1] != "Internet" {
|
|
t.Fatalf("ListGroups().Names = %#v, want empty Finance group removed", listed.Names)
|
|
}
|
|
|
|
_, err = client.DeleteGroup(ctx, &keepassgov1.DeleteGroupRequest{
|
|
Path: []string{"Root", "Internet"},
|
|
})
|
|
if status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("DeleteGroup() code = %v, want %v for non-empty group", status.Code(err), codes.FailedPrecondition)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceGeneratesPasswordsForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
resp, err := client.GeneratePassword(ctx, &keepassgov1.GeneratePasswordRequest{Profile: "strong"})
|
|
if err != nil {
|
|
t.Fatalf("GeneratePassword() error = %v", err)
|
|
}
|
|
|
|
if len(resp.Password) < passwords.DefaultProfiles()["strong"].Length {
|
|
t.Fatalf("len(GeneratePassword().Password) = %d, want at least %d", len(resp.Password), passwords.DefaultProfiles()["strong"].Length)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceGeneratePasswordRejectsUnknownProfiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
_, err := client.GeneratePassword(ctx, &keepassgov1.GeneratePasswordRequest{Profile: "invalid"})
|
|
if status.Code(err) != codes.InvalidArgument {
|
|
t.Fatalf("GeneratePassword() code = %v, want %v", status.Code(err), codes.InvalidArgument)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceCopiesEntryFieldsForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, clipboardWriter, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
if _, err := client.CopyEntryField(ctx, &keepassgov1.CopyEntryFieldRequest{
|
|
Id: "git-server",
|
|
Target: "password",
|
|
}); err != nil {
|
|
t.Fatalf("CopyEntryField() error = %v", err)
|
|
}
|
|
|
|
if clipboardWriter.content != "token-1" {
|
|
t.Fatalf("clipboard content = %q, want %q", clipboardWriter.content, "token-1")
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceUpsertsEntriesForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
upserted, err := client.UpsertEntry(ctx, &keepassgov1.UpsertEntryRequest{
|
|
Entry: &keepassgov1.Entry{
|
|
Id: "ha-codex",
|
|
Title: "Home Assistant (Codex)",
|
|
Username: "codex",
|
|
Password: "token-2",
|
|
Url: "https://lights.julianfamily.org",
|
|
Fields: map[string]string{
|
|
"X-Role": "lights-admin",
|
|
},
|
|
Path: []string{"Root", "Home Assistant"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpsertEntry() error = %v", err)
|
|
}
|
|
|
|
if upserted.Entry.Title != "Home Assistant (Codex)" {
|
|
t.Fatalf("UpsertEntry().Entry.Title = %q, want %q", upserted.Entry.Title, "Home Assistant (Codex)")
|
|
}
|
|
if got := upserted.Entry.Fields["X-Role"]; got != "lights-admin" {
|
|
t.Fatalf("UpsertEntry().Entry.Fields[X-Role] = %q, want %q", got, "lights-admin")
|
|
}
|
|
|
|
listed, err := client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Home Assistant"}})
|
|
if err != nil {
|
|
t.Fatalf("ListEntries() error = %v", err)
|
|
}
|
|
|
|
if len(listed.Entries) != 1 || listed.Entries[0].Password != "token-2" {
|
|
t.Fatalf("ListEntries().Entries = %#v, want persisted Home Assistant entry", listed.Entries)
|
|
}
|
|
if got := listed.Entries[0].Fields["X-Role"]; got != "lights-admin" {
|
|
t.Fatalf("ListEntries().Entries[0].Fields[X-Role] = %q, want %q", got, "lights-admin")
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceDeletesAndRestoresEntriesForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
if _, err := client.DeleteEntry(ctx, &keepassgov1.DeleteEntryRequest{Id: "git-server"}); err != nil {
|
|
t.Fatalf("DeleteEntry() error = %v", err)
|
|
}
|
|
|
|
listed, err := client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
|
|
if err != nil {
|
|
t.Fatalf("ListEntries() error = %v", err)
|
|
}
|
|
|
|
if len(listed.Entries) != 0 {
|
|
t.Fatalf("len(ListEntries().Entries) = %d, want 0 after delete", len(listed.Entries))
|
|
}
|
|
|
|
restored, err := client.RestoreEntry(ctx, &keepassgov1.RestoreEntryRequest{Id: "git-server"})
|
|
if err != nil {
|
|
t.Fatalf("RestoreEntry() error = %v", err)
|
|
}
|
|
|
|
if restored.Entry.Title != "Git Server" {
|
|
t.Fatalf("RestoreEntry().Entry.Title = %q, want %q", restored.Entry.Title, "Git Server")
|
|
}
|
|
|
|
listed, err = client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
|
|
if err != nil {
|
|
t.Fatalf("ListEntries() error = %v", err)
|
|
}
|
|
|
|
if len(listed.Entries) != 1 || listed.Entries[0].Title != "Git Server" {
|
|
t.Fatalf("ListEntries().Entries = %#v, want restored Git Server entry", listed.Entries)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceListsAndInstantiatesTemplatesForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
templates, err := client.ListTemplates(ctx, &keepassgov1.ListTemplatesRequest{})
|
|
if err != nil {
|
|
t.Fatalf("ListTemplates() error = %v", err)
|
|
}
|
|
|
|
if len(templates.Templates) != 1 || templates.Templates[0].Title != "Website Login" {
|
|
t.Fatalf("ListTemplates().Templates = %#v, want Website Login template", templates.Templates)
|
|
}
|
|
if got := templates.Templates[0].Fields["Environment"]; got != "prod" {
|
|
t.Fatalf("ListTemplates().Templates[0].Fields[Environment] = %q, want %q", got, "prod")
|
|
}
|
|
|
|
instantiated, err := client.InstantiateTemplate(ctx, &keepassgov1.InstantiateTemplateRequest{
|
|
TemplateId: "website-login",
|
|
Overrides: &keepassgov1.Entry{
|
|
Id: "dynadot",
|
|
Title: "Dynadot",
|
|
Username: "jjulian",
|
|
Password: "hunter2",
|
|
Url: "https://www.dynadot.com",
|
|
Fields: map[string]string{
|
|
"Environment": "staging",
|
|
},
|
|
Path: []string{"Root", "Internet"},
|
|
Tags: []string{"dns"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InstantiateTemplate() error = %v", err)
|
|
}
|
|
|
|
if instantiated.Entry.Title != "Dynadot" || instantiated.Entry.Notes != "Reusable template for website accounts." {
|
|
t.Fatalf("InstantiateTemplate().Entry = %#v, want Dynadot entry with template notes", instantiated.Entry)
|
|
}
|
|
if got := instantiated.Entry.Fields["Environment"]; got != "staging" {
|
|
t.Fatalf("InstantiateTemplate().Entry.Fields[Environment] = %q, want %q", got, "staging")
|
|
}
|
|
|
|
listed, err := client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{Path: []string{"Root", "Internet"}})
|
|
if err != nil {
|
|
t.Fatalf("ListEntries() error = %v", err)
|
|
}
|
|
|
|
if len(listed.Entries) != 2 {
|
|
t.Fatalf("len(ListEntries().Entries) = %d, want 2 after template instantiation", len(listed.Entries))
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceUpsertsAndDeletesTemplatesForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
upserted, err := client.UpsertTemplate(ctx, &keepassgov1.UpsertTemplateRequest{
|
|
Template: &keepassgov1.Entry{
|
|
Id: "website-login",
|
|
Title: "Website Login Updated",
|
|
Username: "template-user",
|
|
Password: "template-password",
|
|
Fields: map[string]string{
|
|
"Environment": "dev",
|
|
},
|
|
Path: []string{"Templates", "Web"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpsertTemplate() error = %v", err)
|
|
}
|
|
if upserted.Template.Title != "Website Login Updated" {
|
|
t.Fatalf("UpsertTemplate().Template.Title = %q, want updated title", upserted.Template.Title)
|
|
}
|
|
if got := upserted.Template.Fields["Environment"]; got != "dev" {
|
|
t.Fatalf("UpsertTemplate().Template.Fields[Environment] = %q, want %q", got, "dev")
|
|
}
|
|
|
|
listed, err := client.ListTemplates(ctx, &keepassgov1.ListTemplatesRequest{})
|
|
if err != nil {
|
|
t.Fatalf("ListTemplates() error = %v", err)
|
|
}
|
|
if len(listed.Templates) != 1 || listed.Templates[0].Title != "Website Login Updated" {
|
|
t.Fatalf("ListTemplates().Templates = %#v, want updated template", listed.Templates)
|
|
}
|
|
if got := listed.Templates[0].Fields["Environment"]; got != "dev" {
|
|
t.Fatalf("ListTemplates().Templates[0].Fields[Environment] = %q, want %q", got, "dev")
|
|
}
|
|
|
|
if _, err := client.DeleteTemplate(ctx, &keepassgov1.DeleteTemplateRequest{Id: "website-login"}); err != nil {
|
|
t.Fatalf("DeleteTemplate() error = %v", err)
|
|
}
|
|
|
|
listed, err = client.ListTemplates(ctx, &keepassgov1.ListTemplatesRequest{})
|
|
if err != nil {
|
|
t.Fatalf("ListTemplates() error = %v", err)
|
|
}
|
|
if len(listed.Templates) != 0 {
|
|
t.Fatalf("ListTemplates().Templates = %#v, want empty after delete", listed.Templates)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceListsAndRestoresEntryHistoryForAuthorizedClients(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
history, err := client.ListEntryHistory(ctx, &keepassgov1.ListEntryHistoryRequest{Id: "git-server"})
|
|
if err != nil {
|
|
t.Fatalf("ListEntryHistory() error = %v", err)
|
|
}
|
|
if len(history.Entries) != 1 || history.Entries[0].Password != "token-0" {
|
|
t.Fatalf("ListEntryHistory().Entries = %#v, want old token entry", history.Entries)
|
|
}
|
|
|
|
restored, err := client.RestoreEntryHistory(ctx, &keepassgov1.RestoreEntryHistoryRequest{
|
|
Id: "git-server",
|
|
HistoryIndex: 0,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RestoreEntryHistory() error = %v", err)
|
|
}
|
|
if restored.Entry.Password != "token-0" {
|
|
t.Fatalf("RestoreEntryHistory().Entry.Password = %q, want token-0", restored.Entry.Password)
|
|
}
|
|
}
|
|
|
|
func TestVaultServiceListsUploadsDownloadsAndDeletesAttachments(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, cleanup := newTestClient(t)
|
|
defer cleanup()
|
|
|
|
ctx := tokenContext(defaultTestTokenSecret)
|
|
uploaded := []byte("attachment-content")
|
|
|
|
if _, err := client.UploadAttachment(ctx, &keepassgov1.UploadAttachmentRequest{
|
|
EntryId: "git-server",
|
|
Name: "token.txt",
|
|
Content: uploaded,
|
|
}); err != nil {
|
|
t.Fatalf("UploadAttachment() error = %v", err)
|
|
}
|
|
|
|
listed, err := client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "git-server"})
|
|
if err != nil {
|
|
t.Fatalf("ListAttachments() error = %v", err)
|
|
}
|
|
if len(listed.Names) != 1 || listed.Names[0] != "token.txt" {
|
|
t.Fatalf("ListAttachments().Names = %#v, want [token.txt]", listed.Names)
|
|
}
|
|
|
|
downloaded, err := client.DownloadAttachment(ctx, &keepassgov1.DownloadAttachmentRequest{
|
|
EntryId: "git-server",
|
|
Name: "token.txt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DownloadAttachment() error = %v", err)
|
|
}
|
|
if !bytes.Equal(downloaded.Content, uploaded) {
|
|
t.Fatalf("DownloadAttachment().Content = %q, want %q", downloaded.Content, uploaded)
|
|
}
|
|
|
|
if _, err := client.DeleteAttachment(ctx, &keepassgov1.DeleteAttachmentRequest{
|
|
EntryId: "git-server",
|
|
Name: "token.txt",
|
|
}); err != nil {
|
|
t.Fatalf("DeleteAttachment() error = %v", err)
|
|
}
|
|
|
|
listed, err = client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "git-server"})
|
|
if err != nil {
|
|
t.Fatalf("ListAttachments() error = %v", err)
|
|
}
|
|
if len(listed.Names) != 0 {
|
|
t.Fatalf("ListAttachments().Names = %#v, want empty after delete", listed.Names)
|
|
}
|
|
}
|
|
|
|
const defaultTestTokenSecret = "test-token"
|
|
|
|
func tokenContext(secret string) context.Context {
|
|
return metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer "+secret)
|
|
}
|
|
|
|
func hashSecretForTest(secret string) string {
|
|
sum := sha256.Sum256([]byte(secret))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func testAPITokenEntry(t *testing.T, rules ...apitokens.PolicyRule) vault.Entry {
|
|
t.Helper()
|
|
token, _, err := apitokens.Issue("Test Client", "grpc-test", nil, time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC))
|
|
if err != nil {
|
|
t.Fatalf("Issue() error = %v", err)
|
|
}
|
|
token.SecretHash = hashSecretForTest(defaultTestTokenSecret)
|
|
token.Policies = append([]apitokens.PolicyRule(nil), rules...)
|
|
return token.Entry([]string{"Root", "API Tokens"})
|
|
}
|
|
|
|
func newTestClient(t *testing.T) (keepassgov1.VaultServiceClient, *memoryClipboardWriter, func()) {
|
|
t.Helper()
|
|
model := vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "git-server",
|
|
Title: "Git Server",
|
|
Username: "joejulian",
|
|
Password: "token-1",
|
|
URL: "https://git.julianfamily.org",
|
|
Fields: map[string]string{
|
|
"X-Role": "automation",
|
|
},
|
|
History: []vault.Entry{
|
|
{
|
|
ID: "git-server-h1",
|
|
Title: "Git Server",
|
|
Username: "joejulian",
|
|
Password: "token-0",
|
|
URL: "https://git.julianfamily.org",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
},
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
{
|
|
ID: "ha-codex",
|
|
Title: "Home Assistant (Codex)",
|
|
Username: "codex",
|
|
Password: "token-2",
|
|
URL: "https://lights.julianfamily.org",
|
|
Path: []string{"Root", "Home Assistant"},
|
|
},
|
|
testAPITokenEntry(t,
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationManageVault, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListGroups, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationMutateGroup, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationMutateEntry, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationReadEntry, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyPassword, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "git-server", Path: []string{"Root", "Internet"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyUsername, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "git-server", Path: []string{"Root", "Internet"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyURL, Resource: apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "git-server", Path: []string{"Root", "Internet"}}},
|
|
),
|
|
},
|
|
Templates: []vault.Entry{
|
|
{
|
|
ID: "website-login",
|
|
Title: "Website Login",
|
|
Username: "template-user",
|
|
Password: "template-password",
|
|
URL: "https://example.com",
|
|
Notes: "Reusable template for website accounts.",
|
|
Fields: map[string]string{
|
|
"Environment": "prod",
|
|
},
|
|
Tags: []string{"template", "web"},
|
|
Path: []string{"Templates"},
|
|
},
|
|
},
|
|
}
|
|
return newTestClientForModel(t, model)
|
|
}
|
|
|
|
func newTestClientForModel(t *testing.T, model vault.Model) (keepassgov1.VaultServiceClient, *memoryClipboardWriter, func()) {
|
|
t.Helper()
|
|
|
|
listener := bufconn.Listen(1024 * 1024)
|
|
clipboardWriter := &memoryClipboardWriter{}
|
|
service := NewServer(model, passwords.DefaultProfiles(), clipboardWriter)
|
|
server := grpc.NewServer(grpc.UnaryInterceptor(AuthInterceptor(service)))
|
|
keepassgov1.RegisterVaultServiceServer(server, service)
|
|
|
|
go func() {
|
|
_ = server.Serve(listener)
|
|
}()
|
|
|
|
conn, err := grpc.NewClient("passthrough:///bufnet",
|
|
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
|
return listener.Dial()
|
|
}),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("grpc.NewClient() error = %v", err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
_ = conn.Close()
|
|
server.Stop()
|
|
}
|
|
|
|
return keepassgov1.NewVaultServiceClient(conn), clipboardWriter, cleanup
|
|
}
|
|
|
|
func newTestClientWithLifecycle(t *testing.T, lifecycle *stubLifecycle) (keepassgov1.VaultServiceClient, *memoryClipboardWriter, func()) {
|
|
t.Helper()
|
|
|
|
listener := bufconn.Listen(1024 * 1024)
|
|
clipboardWriter := &memoryClipboardWriter{}
|
|
model := lifecycle.model
|
|
model.Entries = append(model.Entries, testAPITokenEntry(t,
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationManageVault, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationReadEntry, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
|
))
|
|
lifecycle.model = model
|
|
service := NewServerWithLifecycle(model, passwords.DefaultProfiles(), clipboardWriter, lifecycle)
|
|
server := grpc.NewServer(grpc.UnaryInterceptor(AuthInterceptor(service)))
|
|
keepassgov1.RegisterVaultServiceServer(server, service)
|
|
|
|
go func() {
|
|
_ = server.Serve(listener)
|
|
}()
|
|
|
|
conn, err := grpc.NewClient("passthrough:///bufnet",
|
|
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
|
return listener.Dial()
|
|
}),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("grpc.NewClient() error = %v", err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
_ = conn.Close()
|
|
server.Stop()
|
|
}
|
|
|
|
return keepassgov1.NewVaultServiceClient(conn), clipboardWriter, cleanup
|
|
}
|
|
|
|
type memoryClipboardWriter struct {
|
|
content string
|
|
}
|
|
|
|
func (w *memoryClipboardWriter) WriteText(text string) error {
|
|
w.content = text
|
|
return nil
|
|
}
|
|
|
|
type stubLifecycle struct {
|
|
model vault.Model
|
|
openPath string
|
|
remoteBaseURL string
|
|
remotePath string
|
|
saved bool
|
|
locked bool
|
|
err error
|
|
unlockPassword string
|
|
unlockKeyFile []byte
|
|
lastUnlockKey vault.MasterKey
|
|
}
|
|
|
|
func (s *stubLifecycle) Current() (vault.Model, error) {
|
|
if s.locked {
|
|
return vault.Model{}, session.ErrLocked
|
|
}
|
|
return s.model, nil
|
|
}
|
|
|
|
func (s *stubLifecycle) Open(path string, _ vault.MasterKey) error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
s.openPath = path
|
|
s.locked = false
|
|
return nil
|
|
}
|
|
|
|
func (s *stubLifecycle) OpenRemote(client webdav.Client, path string, _ vault.MasterKey) error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
s.remoteBaseURL = client.BaseURL
|
|
s.remotePath = path
|
|
s.locked = false
|
|
return nil
|
|
}
|
|
|
|
func (s *stubLifecycle) Save() error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
s.saved = true
|
|
return nil
|
|
}
|
|
|
|
func (s *stubLifecycle) Lock() error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
|
|
s.locked = true
|
|
return nil
|
|
}
|
|
|
|
func (s *stubLifecycle) Unlock(key vault.MasterKey) error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
if s.unlockPassword != "" && key.Password != s.unlockPassword {
|
|
return vault.ErrInvalidMasterKey
|
|
}
|
|
if s.unlockKeyFile != nil && !bytes.Equal(key.KeyFileData, s.unlockKeyFile) {
|
|
return vault.ErrInvalidMasterKey
|
|
}
|
|
|
|
s.lastUnlockKey = vault.MasterKey{
|
|
Password: key.Password,
|
|
KeyFileData: append([]byte(nil), key.KeyFileData...),
|
|
}
|
|
s.locked = false
|
|
return nil
|
|
}
|