package api import ( "context" "errors" "maps" "os" "slices" "strings" "sync" "time" "git.julianfamily.org/keepassgo/apitokens" "git.julianfamily.org/keepassgo/clipboard" "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/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 Lock() error Unlock(vault.MasterKey) 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, mapLifecycleError("open vault", err) } model, err := s.lifecycle.Current() if err != nil { return nil, mapLifecycleError("load opened vault", 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, mapLifecycleError("open remote vault", err) } model, err := s.lifecycle.Current() if err != nil { return nil, mapLifecycleError("load opened remote vault", 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, mapLifecycleError("save vault", 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) { if s.lifecycle == nil { return nil, status.Error(codes.FailedPrecondition, "vault lifecycle backend is not configured") } if err := s.lifecycle.Lock(); err != nil { return nil, mapLifecycleError("lock vault", err) } s.mu.Lock() s.locked = true s.mu.Unlock() return &keepassgov1.LockVaultResponse{}, nil } func (s *Server) UnlockVault(_ context.Context, req *keepassgov1.UnlockVaultRequest) (*keepassgov1.UnlockVaultResponse, 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.Unlock(key); err != nil { return nil, mapLifecycleError("unlock vault", err) } model, err := s.lifecycle.Current() if err != nil { return nil, mapLifecycleError("load unlocked vault", err) } s.mu.Lock() s.model = model s.locked = false s.mu.Unlock() return &keepassgov1.UnlockVaultResponse{}, nil } func mapLifecycleError(operation string, err error) error { switch { case errors.Is(err, os.ErrNotExist): return status.Errorf(codes.NotFound, "%s: %v", operation, err) case errors.Is(err, vault.ErrInvalidMasterKey): return status.Errorf(codes.InvalidArgument, "%s: %v", operation, err) case errors.Is(err, session.ErrLocked), errors.Is(err, session.ErrNoPath): return status.Errorf(codes.FailedPrecondition, "%s: %v", operation, err) case errors.Is(err, webdav.ErrConflict): return status.Errorf(codes.Aborted, "%s: %v", operation, err) default: return status.Errorf(codes.Internal, "%s: %v", operation, err) } } func (s *Server) ListEntries(ctx 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") } if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, req.GetPath()); err != nil { return nil, err } model := s.visibleModel() var entries []vault.Entry if strings.TrimSpace(req.GetQuery()) != "" { results := model.Search(req.GetQuery()) entries = make([]vault.Entry, 0, len(results)) for _, result := range results { entries = append(entries, result.Entry) } } else { entries = 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(ctx 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") } if _, err := s.authorizePathRequest(ctx, apitokens.OperationListGroups, req.GetPath()); err != nil { return nil, err } return &keepassgov1.ListGroupsResponse{ Names: s.visibleModel().ChildGroups(req.GetPath()), }, nil } func (s *Server) CreateGroup(ctx context.Context, req *keepassgov1.CreateGroupRequest) (*keepassgov1.CreateGroupResponse, error) { s.mu.Lock() defer s.mu.Unlock() if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetParentPath()); err != nil { return nil, err } 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(ctx context.Context, req *keepassgov1.RenameGroupRequest) (*keepassgov1.RenameGroupResponse, error) { s.mu.Lock() defer s.mu.Unlock() if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetPath()); err != nil { return nil, err } 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) DeleteGroup(ctx context.Context, req *keepassgov1.DeleteGroupRequest) (*keepassgov1.DeleteGroupResponse, error) { s.mu.Lock() defer s.mu.Unlock() if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetPath()); err != nil { return nil, err } if s.locked { return nil, status.Error(codes.FailedPrecondition, "vault is locked") } if err := s.model.DeleteGroup(req.GetPath()); err != nil { switch { case errors.Is(err, vault.ErrEntryNotFound): return nil, status.Error(codes.NotFound, err.Error()) case errors.Is(err, vault.ErrGroupNotEmpty): return nil, status.Error(codes.FailedPrecondition, err.Error()) default: return nil, status.Errorf(codes.Internal, "delete group: %v", err) } } s.dirty = true return &keepassgov1.DeleteGroupResponse{}, nil } func (s *Server) UpsertEntry(ctx 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") } if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil { s.mu.Unlock() return nil, err } s.model.UpsertEntry(entry) s.dirty = true s.mu.Unlock() return &keepassgov1.UpsertEntryResponse{Entry: entryToProto(entry)}, nil } func (s *Server) DeleteEntry(ctx 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 entry, err := findEntryByID(s.model, req.GetId()); err == nil { if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil { return nil, err } } 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(ctx 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 restored.ID != "" { if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, restored); err != nil { return nil, err } } 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(ctx 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()) } if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationReadEntry, entry); err != nil { return nil, err } 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(ctx 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") } entry, err := findEntryByID(s.model, req.GetId()) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil { return nil, err } 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(ctx 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()) } if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationReadEntry, entry); err != nil { return nil, err } 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(ctx 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 _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil { return nil, err } 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(ctx 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()) } if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationReadEntry, entry); err != nil { return nil, err } 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(ctx 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 _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil { return nil, err } 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(ctx 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") } entry, err := findEntryByID(model, req.GetId()) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } if _, err := s.authorizeEntryRequest(ctx, copyOperation(req.GetTarget()), entry); err != nil { return nil, err } 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(ctx context.Context, req *keepassgov1.GeneratePasswordRequest) (*keepassgov1.GeneratePasswordResponse, error) { s.mu.RLock() defer s.mu.RUnlock() if _, err := s.authenticateRequest(ctx); err != nil { return nil, err } if s.locked { return nil, status.Error(codes.FailedPrecondition, "vault is locked") } profile, err := passwords.LookupProfile(req.GetProfile(), s.profiles) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } 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...), Fields: maps.Clone(entry.Fields), } } 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()...), Fields: maps.Clone(entry.GetFields()), } } 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 (s *Server) visibleModel() vault.Model { out := s.model out.Entries = nil for _, entry := range s.model.Entries { token, ok, err := apitokens.TokenFromEntry(entry) if err == nil && ok && token.ID != "" { continue } out.Entries = append(out.Entries, entry) } out.Groups = nil for _, path := range s.model.Groups { if len(path) >= 2 && path[0] == "Root" && path[1] == "API Tokens" { continue } out.Groups = append(out.Groups, path) } return out } var timeNow = func() time.Time { return time.Now().UTC() } func (s *Server) authenticateRequest(ctx context.Context) (apitokens.Token, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return apitokens.Token{}, status.Error(codes.Unauthenticated, "missing metadata") } values := md.Get("authorization") if len(values) == 0 { return apitokens.Token{}, status.Error(codes.Unauthenticated, "missing authorization") } const prefix = "Bearer " if !strings.HasPrefix(values[0], prefix) { return apitokens.Token{}, status.Error(codes.Unauthenticated, "invalid bearer token") } tokens, err := apitokens.Entries(s.model) if err != nil { return apitokens.Token{}, status.Errorf(codes.Internal, "load api tokens: %v", err) } token, err := apitokens.Authenticate(tokens, strings.TrimSpace(strings.TrimPrefix(values[0], prefix)), timeNow()) if err != nil { switch err { case apitokens.ErrInvalidToken, apitokens.ErrExpiredToken, apitokens.ErrDisabledToken: return apitokens.Token{}, status.Error(codes.Unauthenticated, err.Error()) default: return apitokens.Token{}, status.Errorf(codes.Internal, "authenticate api token: %v", err) } } return token, nil } func (s *Server) authorizePathRequest(ctx context.Context, op apitokens.Operation, path []string) (apitokens.Token, error) { token, err := s.authenticateRequest(ctx) if err != nil { return apitokens.Token{}, err } if apitokens.Evaluate(token, op, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: path}) != apitokens.DecisionAllow { return apitokens.Token{}, status.Error(codes.PermissionDenied, "access is not allowed for this token") } return token, nil } func (s *Server) authorizeEntryRequest(ctx context.Context, op apitokens.Operation, entry vault.Entry) (apitokens.Token, error) { token, err := s.authenticateRequest(ctx) if err != nil { return apitokens.Token{}, err } if apitokens.Evaluate(token, op, apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: entry.ID, Path: entry.Path}) != apitokens.DecisionAllow { return apitokens.Token{}, status.Error(codes.PermissionDenied, "access is not allowed for this token") } return token, nil } func copyOperation(target string) apitokens.Operation { switch clipboard.Target(target) { case clipboard.TargetUsername: return apitokens.OperationCopyUsername case clipboard.TargetURL: return apitokens.OperationCopyURL default: return apitokens.OperationCopyPassword } } func AuthInterceptor(server *Server) grpc.UnaryServerInterceptor { return func( ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { switch info.FullMethod { case "/keepassgo.v1.VaultService/GetSessionStatus", "/keepassgo.v1.VaultService/OpenVault", "/keepassgo.v1.VaultService/OpenRemoteVault", "/keepassgo.v1.VaultService/SaveVault", "/keepassgo.v1.VaultService/LockVault", "/keepassgo.v1.VaultService/UnlockVault": if _, err := server.authenticateRequest(ctx); err != nil { return nil, err } } return handler(ctx, req) } }