627 lines
18 KiB
Go
627 lines
18 KiB
Go
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)
|
|
}
|
|
}
|