Reconstruct KeePassGO repository
This commit is contained in:
+626
@@ -0,0 +1,626 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
"strings"
|
||||
|
||||
"git.julianfamily.org/keepassgo/clipboard"
|
||||
"git.julianfamily.org/keepassgo/passwords"
|
||||
keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1"
|
||||
"git.julianfamily.org/keepassgo/webdav"
|
||||
"git.julianfamily.org/keepassgo/vault"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
keepassgov1.UnimplementedVaultServiceServer
|
||||
|
||||
mu sync.RWMutex
|
||||
model vault.Model
|
||||
locked bool
|
||||
dirty bool
|
||||
lifecycle lifecycleBackend
|
||||
profiles map[string]passwords.Profile
|
||||
clipboard clipboard.Writer
|
||||
}
|
||||
|
||||
type lifecycleBackend interface {
|
||||
Current() (vault.Model, error)
|
||||
Open(string, vault.MasterKey) error
|
||||
OpenRemote(webdav.Client, string, vault.MasterKey) error
|
||||
Save() error
|
||||
}
|
||||
|
||||
func NewServer(model vault.Model, profiles map[string]passwords.Profile, clipboardWriter clipboard.Writer) *Server {
|
||||
return &Server{
|
||||
model: model,
|
||||
profiles: profiles,
|
||||
clipboard: clipboardWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func NewServerWithLifecycle(model vault.Model, profiles map[string]passwords.Profile, clipboardWriter clipboard.Writer, lifecycle lifecycleBackend) *Server {
|
||||
server := NewServer(model, profiles, clipboardWriter)
|
||||
server.lifecycle = lifecycle
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *Server) GetSessionStatus(_ context.Context, _ *keepassgov1.GetSessionStatusRequest) (*keepassgov1.GetSessionStatusResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return &keepassgov1.GetSessionStatusResponse{
|
||||
Locked: s.locked,
|
||||
Dirty: s.dirty,
|
||||
EntryCount: uint32(len(s.model.Entries)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) OpenVault(_ context.Context, req *keepassgov1.OpenVaultRequest) (*keepassgov1.OpenVaultResponse, error) {
|
||||
if s.lifecycle == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault lifecycle backend is not configured")
|
||||
}
|
||||
|
||||
key := vault.MasterKey{Password: req.GetPassword(), KeyFileData: append([]byte(nil), req.GetKeyFileData()...)}
|
||||
if err := s.lifecycle.Open(req.GetPath(), key); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "open vault: %v", err)
|
||||
}
|
||||
|
||||
model, err := s.lifecycle.Current()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "load opened vault: %v", err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.model = model
|
||||
s.locked = false
|
||||
s.dirty = false
|
||||
s.mu.Unlock()
|
||||
|
||||
return &keepassgov1.OpenVaultResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) OpenRemoteVault(_ context.Context, req *keepassgov1.OpenRemoteVaultRequest) (*keepassgov1.OpenRemoteVaultResponse, error) {
|
||||
if s.lifecycle == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault lifecycle backend is not configured")
|
||||
}
|
||||
|
||||
client := webdav.Client{
|
||||
BaseURL: req.GetBaseUrl(),
|
||||
Username: req.GetUsername(),
|
||||
Password: req.GetPassword(),
|
||||
}
|
||||
key := vault.MasterKey{Password: req.GetMasterPassword(), KeyFileData: append([]byte(nil), req.GetKeyFileData()...)}
|
||||
if err := s.lifecycle.OpenRemote(client, req.GetPath(), key); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "open remote vault: %v", err)
|
||||
}
|
||||
|
||||
model, err := s.lifecycle.Current()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "load opened remote vault: %v", err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.model = model
|
||||
s.locked = false
|
||||
s.dirty = false
|
||||
s.mu.Unlock()
|
||||
|
||||
return &keepassgov1.OpenRemoteVaultResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) SaveVault(_ context.Context, _ *keepassgov1.SaveVaultRequest) (*keepassgov1.SaveVaultResponse, error) {
|
||||
if s.lifecycle == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault lifecycle backend is not configured")
|
||||
}
|
||||
|
||||
if err := s.lifecycle.Save(); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "save vault: %v", err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.dirty = false
|
||||
s.mu.Unlock()
|
||||
|
||||
return &keepassgov1.SaveVaultResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) LockVault(_ context.Context, _ *keepassgov1.LockVaultRequest) (*keepassgov1.LockVaultResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.locked = true
|
||||
|
||||
return &keepassgov1.LockVaultResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UnlockVault(_ context.Context, _ *keepassgov1.UnlockVaultRequest) (*keepassgov1.UnlockVaultResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.locked = false
|
||||
|
||||
return &keepassgov1.UnlockVaultResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListEntries(_ context.Context, req *keepassgov1.ListEntriesRequest) (*keepassgov1.ListEntriesResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
var entries []vault.Entry
|
||||
if strings.TrimSpace(req.GetQuery()) != "" {
|
||||
results := s.model.Search(req.GetQuery())
|
||||
entries = make([]vault.Entry, 0, len(results))
|
||||
for _, result := range results {
|
||||
entries = append(entries, result.Entry)
|
||||
}
|
||||
} else {
|
||||
entries = s.model.EntriesInPath(req.GetPath())
|
||||
}
|
||||
|
||||
resp := &keepassgov1.ListEntriesResponse{
|
||||
Entries: make([]*keepassgov1.Entry, 0, len(entries)),
|
||||
}
|
||||
for _, entry := range entries {
|
||||
resp.Entries = append(resp.Entries, entryToProto(entry))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListGroups(_ context.Context, req *keepassgov1.ListGroupsRequest) (*keepassgov1.ListGroupsResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
return &keepassgov1.ListGroupsResponse{
|
||||
Names: s.model.ChildGroups(req.GetPath()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateGroup(_ context.Context, req *keepassgov1.CreateGroupRequest) (*keepassgov1.CreateGroupResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
s.model.CreateGroup(req.GetParentPath(), req.GetName())
|
||||
s.dirty = true
|
||||
return &keepassgov1.CreateGroupResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RenameGroup(_ context.Context, req *keepassgov1.RenameGroupRequest) (*keepassgov1.RenameGroupResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
if err := s.model.RenameGroup(req.GetPath(), req.GetNewName()); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, "rename group: %v", err)
|
||||
}
|
||||
|
||||
s.dirty = true
|
||||
return &keepassgov1.RenameGroupResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UpsertEntry(_ context.Context, req *keepassgov1.UpsertEntryRequest) (*keepassgov1.UpsertEntryResponse, error) {
|
||||
if req.GetEntry() == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing entry")
|
||||
}
|
||||
|
||||
entry := entryFromProto(req.GetEntry())
|
||||
|
||||
s.mu.Lock()
|
||||
if s.locked {
|
||||
s.mu.Unlock()
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
s.model.UpsertEntry(entry)
|
||||
s.dirty = true
|
||||
s.mu.Unlock()
|
||||
|
||||
return &keepassgov1.UpsertEntryResponse{Entry: entryToProto(entry)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteEntry(_ context.Context, req *keepassgov1.DeleteEntryRequest) (*keepassgov1.DeleteEntryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
if err := s.model.DeleteEntry(req.GetId()); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, "delete entry: %v", err)
|
||||
}
|
||||
|
||||
s.dirty = true
|
||||
return &keepassgov1.DeleteEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RestoreEntry(_ context.Context, req *keepassgov1.RestoreEntryRequest) (*keepassgov1.RestoreEntryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
var restored vault.Entry
|
||||
for _, entry := range s.model.RecycleBin {
|
||||
if entry.ID == req.GetId() {
|
||||
restored = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.model.RestoreEntry(req.GetId()); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, "restore entry: %v", err)
|
||||
}
|
||||
|
||||
s.dirty = true
|
||||
return &keepassgov1.RestoreEntryResponse{Entry: entryToProto(restored)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListEntryHistory(_ context.Context, req *keepassgov1.ListEntryHistoryRequest) (*keepassgov1.ListEntryHistoryResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, err := findEntryByID(s.model, req.GetId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
resp := &keepassgov1.ListEntryHistoryResponse{
|
||||
Entries: make([]*keepassgov1.Entry, 0, len(entry.History)),
|
||||
}
|
||||
for _, historical := range entry.History {
|
||||
resp.Entries = append(resp.Entries, entryToProto(historical))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) RestoreEntryHistory(_ context.Context, req *keepassgov1.RestoreEntryHistoryRequest) (*keepassgov1.RestoreEntryHistoryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
if err := s.model.RestoreEntryVersion(req.GetId(), int(req.GetHistoryIndex())); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, "restore entry history: %v", err)
|
||||
}
|
||||
|
||||
entry, err := findEntryByID(s.model, req.GetId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
s.dirty = true
|
||||
return &keepassgov1.RestoreEntryHistoryResponse{Entry: entryToProto(entry)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListTemplates(_ context.Context, _ *keepassgov1.ListTemplatesRequest) (*keepassgov1.ListTemplatesResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
resp := &keepassgov1.ListTemplatesResponse{
|
||||
Templates: make([]*keepassgov1.Entry, 0, len(s.model.Templates)),
|
||||
}
|
||||
for _, template := range s.model.Templates {
|
||||
resp.Templates = append(resp.Templates, entryToProto(template))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) UpsertTemplate(_ context.Context, req *keepassgov1.UpsertTemplateRequest) (*keepassgov1.UpsertTemplateResponse, error) {
|
||||
if req.GetTemplate() == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing template")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry := entryFromProto(req.GetTemplate())
|
||||
s.model.UpsertTemplate(entry)
|
||||
s.dirty = true
|
||||
|
||||
return &keepassgov1.UpsertTemplateResponse{Template: entryToProto(entry)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteTemplate(_ context.Context, req *keepassgov1.DeleteTemplateRequest) (*keepassgov1.DeleteTemplateResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
if err := s.model.DeleteTemplate(req.GetId()); err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, "delete template: %v", err)
|
||||
}
|
||||
s.dirty = true
|
||||
|
||||
return &keepassgov1.DeleteTemplateResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) InstantiateTemplate(_ context.Context, req *keepassgov1.InstantiateTemplateRequest) (*keepassgov1.InstantiateTemplateResponse, error) {
|
||||
if req.GetOverrides() == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing overrides")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, err := s.model.InstantiateTemplate(req.GetTemplateId(), entryFromProto(req.GetOverrides()))
|
||||
if err != nil {
|
||||
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return nil, status.Errorf(codes.Internal, "instantiate template: %v", err)
|
||||
}
|
||||
|
||||
s.dirty = true
|
||||
return &keepassgov1.InstantiateTemplateResponse{Entry: entryToProto(entry)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListAttachments(_ context.Context, req *keepassgov1.ListAttachmentsRequest) (*keepassgov1.ListAttachmentsResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, err := findEntryByID(s.model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(entry.Attachments))
|
||||
for name := range entry.Attachments {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
|
||||
return &keepassgov1.ListAttachmentsResponse{Names: names}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UploadAttachment(_ context.Context, req *keepassgov1.UploadAttachmentRequest) (*keepassgov1.UploadAttachmentResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, index, err := findMutableEntryByID(&s.model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
if entry.Attachments == nil {
|
||||
entry.Attachments = map[string][]byte{}
|
||||
}
|
||||
entry.Attachments[req.GetName()] = append([]byte(nil), req.GetContent()...)
|
||||
s.model.Entries[index] = entry
|
||||
s.dirty = true
|
||||
|
||||
return &keepassgov1.UploadAttachmentResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DownloadAttachment(_ context.Context, req *keepassgov1.DownloadAttachmentRequest) (*keepassgov1.DownloadAttachmentResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, err := findEntryByID(s.model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
content, ok := entry.Attachments[req.GetName()]
|
||||
if !ok {
|
||||
return nil, status.Error(codes.NotFound, "attachment not found")
|
||||
}
|
||||
|
||||
return &keepassgov1.DownloadAttachmentResponse{
|
||||
Content: append([]byte(nil), content...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteAttachment(_ context.Context, req *keepassgov1.DeleteAttachmentRequest) (*keepassgov1.DeleteAttachmentResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
entry, index, err := findMutableEntryByID(&s.model, req.GetEntryId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
if _, ok := entry.Attachments[req.GetName()]; !ok {
|
||||
return nil, status.Error(codes.NotFound, "attachment not found")
|
||||
}
|
||||
|
||||
delete(entry.Attachments, req.GetName())
|
||||
if len(entry.Attachments) == 0 {
|
||||
entry.Attachments = nil
|
||||
}
|
||||
s.model.Entries[index] = entry
|
||||
s.dirty = true
|
||||
|
||||
return &keepassgov1.DeleteAttachmentResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CopyEntryField(_ context.Context, req *keepassgov1.CopyEntryFieldRequest) (*keepassgov1.CopyEntryFieldResponse, error) {
|
||||
s.mu.RLock()
|
||||
model := s.model
|
||||
locked := s.locked
|
||||
s.mu.RUnlock()
|
||||
|
||||
if locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
service := clipboard.Service{Writer: s.clipboard}
|
||||
if err := service.Copy(model, req.GetId(), clipboard.Target(req.GetTarget())); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, vault.ErrEntryNotFound):
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, clipboard.ErrUnsupportedTarget):
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
default:
|
||||
return nil, status.Errorf(codes.Internal, "copy entry field: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &keepassgov1.CopyEntryFieldResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GeneratePassword(_ context.Context, req *keepassgov1.GeneratePasswordRequest) (*keepassgov1.GeneratePasswordResponse, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.locked {
|
||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
||||
}
|
||||
|
||||
profile, ok := s.profiles[req.GetProfile()]
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "unknown password profile %q", req.GetProfile())
|
||||
}
|
||||
|
||||
password, err := passwords.Generate(profile)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "generate password: %v", err)
|
||||
}
|
||||
|
||||
return &keepassgov1.GeneratePasswordResponse{Password: password}, nil
|
||||
}
|
||||
|
||||
func entryToProto(entry vault.Entry) *keepassgov1.Entry {
|
||||
return &keepassgov1.Entry{
|
||||
Id: entry.ID,
|
||||
Title: entry.Title,
|
||||
Username: entry.Username,
|
||||
Password: entry.Password,
|
||||
Url: entry.URL,
|
||||
Notes: entry.Notes,
|
||||
Tags: append([]string(nil), entry.Tags...),
|
||||
Path: append([]string(nil), entry.Path...),
|
||||
}
|
||||
}
|
||||
|
||||
func entryFromProto(entry *keepassgov1.Entry) vault.Entry {
|
||||
return vault.Entry{
|
||||
ID: entry.GetId(),
|
||||
Title: entry.GetTitle(),
|
||||
Username: entry.GetUsername(),
|
||||
Password: entry.GetPassword(),
|
||||
URL: entry.GetUrl(),
|
||||
Notes: entry.GetNotes(),
|
||||
Tags: append([]string(nil), entry.GetTags()...),
|
||||
Path: append([]string(nil), entry.GetPath()...),
|
||||
}
|
||||
}
|
||||
|
||||
func findEntryByID(model vault.Model, id string) (vault.Entry, error) {
|
||||
for _, entry := range model.Entries {
|
||||
if entry.ID == id {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
return vault.Entry{}, vault.ErrEntryNotFound
|
||||
}
|
||||
|
||||
func findMutableEntryByID(model *vault.Model, id string) (vault.Entry, int, error) {
|
||||
for i, entry := range model.Entries {
|
||||
if entry.ID == id {
|
||||
entry.Attachments = maps.Clone(entry.Attachments)
|
||||
return entry, i, nil
|
||||
}
|
||||
}
|
||||
return vault.Entry{}, -1, vault.ErrEntryNotFound
|
||||
}
|
||||
|
||||
func BearerTokenInterceptor(expectedToken string) grpc.UnaryServerInterceptor {
|
||||
return func(
|
||||
ctx context.Context,
|
||||
req any,
|
||||
info *grpc.UnaryServerInfo,
|
||||
handler grpc.UnaryHandler,
|
||||
) (any, error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing metadata")
|
||||
}
|
||||
|
||||
values := md.Get("authorization")
|
||||
if len(values) == 0 {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing authorization")
|
||||
}
|
||||
|
||||
if values[0] != "Bearer "+expectedToken {
|
||||
return nil, status.Error(codes.Unauthenticated, "invalid bearer token")
|
||||
}
|
||||
|
||||
return handler(ctx, req)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"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 TestVaultServiceReportsSessionStatusAndSupportsLockUnlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClient(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
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 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 := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
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: "rustyryan",
|
||||
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 TestVaultServiceListsEntriesForAuthorizedClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClient(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
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 != "Vault Console" {
|
||||
t.Fatalf("ListEntries().Entries[0].Title = %q, want %q", resp.Entries[0].Title, "Vault Console")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultServiceListsCreatesAndRenamesGroupsForAuthorizedClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClient(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
|
||||
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 TestVaultServiceGeneratesPasswordsForAuthorizedClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClient(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
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 TestVaultServiceCopiesEntryFieldsForAuthorizedClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, clipboardWriter, cleanup := newTestClient(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
if _, err := client.CopyEntryField(ctx, &keepassgov1.CopyEntryFieldRequest{
|
||||
Id: "vault-console",
|
||||
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 := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
upserted, err := client.UpsertEntry(ctx, &keepassgov1.UpsertEntryRequest{
|
||||
Entry: &keepassgov1.Entry{
|
||||
Id: "surveillance-console",
|
||||
Title: "Surveillance Console",
|
||||
Username: "codex",
|
||||
Password: "token-2",
|
||||
Url: "https://surveillance.crew.example.invalid",
|
||||
Path: []string{"Root", "Home Assistant"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertEntry() error = %v", err)
|
||||
}
|
||||
|
||||
if upserted.Entry.Title != "Surveillance Console" {
|
||||
t.Fatalf("UpsertEntry().Entry.Title = %q, want %q", upserted.Entry.Title, "Surveillance Console")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultServiceDeletesAndRestoresEntriesForAuthorizedClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClient(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
if _, err := client.DeleteEntry(ctx, &keepassgov1.DeleteEntryRequest{Id: "vault-console"}); 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: "vault-console"})
|
||||
if err != nil {
|
||||
t.Fatalf("RestoreEntry() error = %v", err)
|
||||
}
|
||||
|
||||
if restored.Entry.Title != "Vault Console" {
|
||||
t.Fatalf("RestoreEntry().Entry.Title = %q, want %q", restored.Entry.Title, "Vault Console")
|
||||
}
|
||||
|
||||
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 != "Vault Console" {
|
||||
t.Fatalf("ListEntries().Entries = %#v, want restored Vault Console entry", listed.Entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultServiceListsAndInstantiatesTemplatesForAuthorizedClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClient(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
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)
|
||||
}
|
||||
|
||||
instantiated, err := client.InstantiateTemplate(ctx, &keepassgov1.InstantiateTemplateRequest{
|
||||
TemplateId: "website-login",
|
||||
Overrides: &keepassgov1.Entry{
|
||||
Id: "bellagio",
|
||||
Title: "Bellagio",
|
||||
Username: "rustyryan",
|
||||
Password: "hunter2",
|
||||
Url: "https://bellagio.example.invalid",
|
||||
Path: []string{"Root", "Internet"},
|
||||
Tags: []string{"dns"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("InstantiateTemplate() error = %v", err)
|
||||
}
|
||||
|
||||
if instantiated.Entry.Title != "Bellagio" || instantiated.Entry.Notes != "Reusable template for website accounts." {
|
||||
t.Fatalf("InstantiateTemplate().Entry = %#v, want Bellagio entry with template notes", instantiated.Entry)
|
||||
}
|
||||
|
||||
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 := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
upserted, err := client.UpsertTemplate(ctx, &keepassgov1.UpsertTemplateRequest{
|
||||
Template: &keepassgov1.Entry{
|
||||
Id: "website-login",
|
||||
Title: "Website Login Updated",
|
||||
Username: "template-user",
|
||||
Password: "template-password",
|
||||
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)
|
||||
}
|
||||
|
||||
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 _, 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 := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
history, err := client.ListEntryHistory(ctx, &keepassgov1.ListEntryHistoryRequest{Id: "vault-console"})
|
||||
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: "vault-console",
|
||||
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 := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer test-token")
|
||||
uploaded := []byte("attachment-content")
|
||||
|
||||
if _, err := client.UploadAttachment(ctx, &keepassgov1.UploadAttachmentRequest{
|
||||
EntryId: "vault-console",
|
||||
Name: "token.txt",
|
||||
Content: uploaded,
|
||||
}); err != nil {
|
||||
t.Fatalf("UploadAttachment() error = %v", err)
|
||||
}
|
||||
|
||||
listed, err := client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "vault-console"})
|
||||
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: "vault-console",
|
||||
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: "vault-console",
|
||||
Name: "token.txt",
|
||||
}); err != nil {
|
||||
t.Fatalf("DeleteAttachment() error = %v", err)
|
||||
}
|
||||
|
||||
listed, err = client.ListAttachments(ctx, &keepassgov1.ListAttachmentsRequest{EntryId: "vault-console"})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T) (keepassgov1.VaultServiceClient, *memoryClipboardWriter, func()) {
|
||||
t.Helper()
|
||||
|
||||
listener := bufconn.Listen(1024 * 1024)
|
||||
server := grpc.NewServer(grpc.UnaryInterceptor(BearerTokenInterceptor("test-token")))
|
||||
clipboardWriter := &memoryClipboardWriter{}
|
||||
keepassgov1.RegisterVaultServiceServer(server, NewServer(
|
||||
vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{
|
||||
ID: "vault-console",
|
||||
Title: "Vault Console",
|
||||
Username: "dannyocean",
|
||||
Password: "token-1",
|
||||
URL: "https://vault.crew.example.invalid",
|
||||
History: []vault.Entry{
|
||||
{
|
||||
ID: "vault-console-h1",
|
||||
Title: "Vault Console",
|
||||
Username: "dannyocean",
|
||||
Password: "token-0",
|
||||
URL: "https://vault.crew.example.invalid",
|
||||
Path: []string{"Root", "Internet"},
|
||||
},
|
||||
},
|
||||
Path: []string{"Root", "Internet"},
|
||||
},
|
||||
{
|
||||
ID: "surveillance-console",
|
||||
Title: "Surveillance Console",
|
||||
Username: "codex",
|
||||
Password: "token-2",
|
||||
URL: "https://surveillance.crew.example.invalid",
|
||||
Path: []string{"Root", "Home Assistant"},
|
||||
},
|
||||
},
|
||||
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.",
|
||||
Tags: []string{"template", "web"},
|
||||
Path: []string{"Templates"},
|
||||
},
|
||||
},
|
||||
},
|
||||
passwords.DefaultProfiles(),
|
||||
clipboardWriter,
|
||||
))
|
||||
|
||||
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)
|
||||
server := grpc.NewServer(grpc.UnaryInterceptor(BearerTokenInterceptor("test-token")))
|
||||
clipboardWriter := &memoryClipboardWriter{}
|
||||
keepassgov1.RegisterVaultServiceServer(server, NewServerWithLifecycle(
|
||||
vault.Model{},
|
||||
passwords.DefaultProfiles(),
|
||||
clipboardWriter,
|
||||
lifecycle,
|
||||
))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
s.openPath = path
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubLifecycle) OpenRemote(client webdav.Client, path string, _ vault.MasterKey) error {
|
||||
s.remoteBaseURL = client.BaseURL
|
||||
s.remotePath = path
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubLifecycle) Save() error {
|
||||
s.saved = true
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user