Clean up browser bridge and mutation helpers
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"git.julianfamily.org/keepassgo/internal/browserbridge"
|
"git.julianfamily.org/keepassgo/internal/browserbridge"
|
||||||
"git.julianfamily.org/keepassgo/internal/grpcaddr"
|
"git.julianfamily.org/keepassgo/internal/grpcaddr"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -91,11 +92,7 @@ func runStatus(args []string) error {
|
|||||||
GRPCAddress: strings.TrimSpace(*grpcAddr),
|
GRPCAddress: strings.TrimSpace(*grpcAddr),
|
||||||
BearerToken: strings.TrimSpace(*token),
|
BearerToken: strings.TrimSpace(*token),
|
||||||
}
|
}
|
||||||
connCfg, err := req.Connection()
|
conn, client, ctx, err := dialBridge(context.Background(), req)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conn, client, ctx, err := browserbridge.Dial(context.Background(), connCfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -111,11 +108,7 @@ func runNativeMessage() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
connCfg, err := req.Connection()
|
conn, client, ctx, err := dialBridge(context.Background(), req)
|
||||||
if err != nil {
|
|
||||||
return browserbridge.WriteResponse(os.Stdout, browserbridge.Response{Success: false, Error: err.Error()})
|
|
||||||
}
|
|
||||||
conn, client, ctx, err := browserbridge.Dial(context.Background(), connCfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return browserbridge.WriteResponse(os.Stdout, browserbridge.Response{Success: false, Error: err.Error()})
|
return browserbridge.WriteResponse(os.Stdout, browserbridge.Response{Success: false, Error: err.Error()})
|
||||||
}
|
}
|
||||||
@@ -123,6 +116,14 @@ func runNativeMessage() error {
|
|||||||
return browserbridge.WriteResponse(os.Stdout, browserbridge.HandleRequest(ctx, req, client))
|
return browserbridge.WriteResponse(os.Stdout, browserbridge.HandleRequest(ctx, req, client))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dialBridge(ctx context.Context, req browserbridge.Request) (*grpc.ClientConn, *browserbridge.GRPCClient, context.Context, error) {
|
||||||
|
connCfg, err := req.Connection()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return browserbridge.Dial(ctx, connCfg)
|
||||||
|
}
|
||||||
|
|
||||||
func defaultBinaryPath() (string, error) {
|
func defaultBinaryPath() (string, error) {
|
||||||
return browserbridge.ResolveBridgeBinaryPath("")
|
return browserbridge.ResolveBridgeBinaryPath("")
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-61
@@ -113,9 +113,6 @@ func (s *Server) GetSessionStatus(ctx context.Context, _ *keepassgov1.GetSession
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
pendingApprovals := s.approvals.Pending()
|
pendingApprovals := s.approvals.Pending()
|
||||||
var tokenPending uint32
|
var tokenPending uint32
|
||||||
for _, pending := range pendingApprovals {
|
for _, pending := range pendingApprovals {
|
||||||
@@ -123,11 +120,16 @@ func (s *Server) GetSessionStatus(ctx context.Context, _ *keepassgov1.GetSession
|
|||||||
tokenPending++
|
tokenPending++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.mu.RLock()
|
||||||
|
locked := s.locked
|
||||||
|
dirty := s.dirty
|
||||||
|
entryCount := uint32(len(s.model.Entries))
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
return &keepassgov1.GetSessionStatusResponse{
|
return &keepassgov1.GetSessionStatusResponse{
|
||||||
Locked: s.locked,
|
Locked: locked,
|
||||||
Dirty: s.dirty,
|
Dirty: dirty,
|
||||||
EntryCount: uint32(len(s.model.Entries)),
|
EntryCount: entryCount,
|
||||||
PendingApprovalCount: uint32(len(pendingApprovals)),
|
PendingApprovalCount: uint32(len(pendingApprovals)),
|
||||||
TokenPendingApprovalCount: tokenPending,
|
TokenPendingApprovalCount: tokenPending,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -486,76 +488,75 @@ func (s *Server) ListGroups(ctx context.Context, req *keepassgov1.ListGroupsRequ
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) CreateGroup(ctx context.Context, req *keepassgov1.CreateGroupRequest) (*keepassgov1.CreateGroupResponse, error) {
|
func (s *Server) mutateAuthorizedVisiblePath(ctx context.Context, clientPath []string, op apitokens.Operation, mutate func(*vault.Model, []string) error) error {
|
||||||
model, locked := s.snapshotModel()
|
model, locked := s.snapshotModel()
|
||||||
if locked {
|
if locked {
|
||||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
return status.Error(codes.FailedPrecondition, "vault is locked")
|
||||||
}
|
}
|
||||||
parentPath := expandClientPath(visibleModel(model), req.GetParentPath())
|
internalPath := expandClientPath(visibleModel(model), clientPath)
|
||||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, parentPath); err != nil {
|
if _, err := s.authorizePathRequest(ctx, op, internalPath); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
return s.mutateAuthorizedModel(func() error { return nil }, func(model *vault.Model) error {
|
||||||
|
return mutate(model, internalPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) mutateAuthorizedModel(authorize func() error, mutate func(*vault.Model) error) error {
|
||||||
|
if err := authorize(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
if err := mutate(&s.model); err != nil {
|
||||||
s.model.CreateGroup(parentPath, req.GetName())
|
return err
|
||||||
|
}
|
||||||
s.dirty = true
|
s.dirty = true
|
||||||
s.syncMutationLocked()
|
s.syncMutationLocked()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) CreateGroup(ctx context.Context, req *keepassgov1.CreateGroupRequest) (*keepassgov1.CreateGroupResponse, error) {
|
||||||
|
if err := s.mutateAuthorizedVisiblePath(ctx, req.GetParentPath(), apitokens.OperationMutateGroup, func(model *vault.Model, parentPath []string) error {
|
||||||
|
model.CreateGroup(parentPath, req.GetName())
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &keepassgov1.CreateGroupResponse{}, nil
|
return &keepassgov1.CreateGroupResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) RenameGroup(ctx context.Context, req *keepassgov1.RenameGroupRequest) (*keepassgov1.RenameGroupResponse, error) {
|
func (s *Server) RenameGroup(ctx context.Context, req *keepassgov1.RenameGroupRequest) (*keepassgov1.RenameGroupResponse, error) {
|
||||||
model, locked := s.snapshotModel()
|
if err := s.mutateAuthorizedVisiblePath(ctx, req.GetPath(), apitokens.OperationMutateGroup, func(model *vault.Model, groupPath []string) error {
|
||||||
if locked {
|
if err := model.RenameGroup(groupPath, req.GetNewName()); err != nil {
|
||||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
if errors.Is(err, vault.ErrEntryNotFound) {
|
||||||
}
|
return status.Error(codes.NotFound, err.Error())
|
||||||
groupPath := expandClientPath(visibleModel(model), req.GetPath())
|
}
|
||||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, groupPath); err != nil {
|
return status.Errorf(codes.Internal, "rename group: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if err := s.model.RenameGroup(groupPath, 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
|
|
||||||
s.syncMutationLocked()
|
|
||||||
return &keepassgov1.RenameGroupResponse{}, nil
|
return &keepassgov1.RenameGroupResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) DeleteGroup(ctx context.Context, req *keepassgov1.DeleteGroupRequest) (*keepassgov1.DeleteGroupResponse, error) {
|
func (s *Server) DeleteGroup(ctx context.Context, req *keepassgov1.DeleteGroupRequest) (*keepassgov1.DeleteGroupResponse, error) {
|
||||||
model, locked := s.snapshotModel()
|
if err := s.mutateAuthorizedVisiblePath(ctx, req.GetPath(), apitokens.OperationMutateGroup, func(model *vault.Model, groupPath []string) error {
|
||||||
if locked {
|
if err := model.DeleteGroup(groupPath); err != nil {
|
||||||
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
|
switch {
|
||||||
}
|
case errors.Is(err, vault.ErrEntryNotFound):
|
||||||
groupPath := expandClientPath(visibleModel(model), req.GetPath())
|
return status.Error(codes.NotFound, err.Error())
|
||||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, groupPath); err != nil {
|
case errors.Is(err, vault.ErrGroupNotEmpty):
|
||||||
|
return status.Error(codes.FailedPrecondition, err.Error())
|
||||||
|
default:
|
||||||
|
return status.Errorf(codes.Internal, "delete group: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if err := s.model.DeleteGroup(groupPath); 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
|
|
||||||
s.syncMutationLocked()
|
|
||||||
return &keepassgov1.DeleteGroupResponse{}, nil
|
return &keepassgov1.DeleteGroupResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,12 +573,12 @@ func (s *Server) UpsertEntry(ctx context.Context, req *keepassgov1.UpsertEntryRe
|
|||||||
if _, err := s.authorizeUpsertEntryRequest(ctx, entry); err != nil {
|
if _, err := s.authorizeUpsertEntryRequest(ctx, entry); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.mutateAuthorizedModel(func() error { return nil }, func(model *vault.Model) error {
|
||||||
s.mu.Lock()
|
model.UpsertEntry(entry)
|
||||||
s.model.UpsertEntry(entry)
|
return nil
|
||||||
s.dirty = true
|
}); err != nil {
|
||||||
s.syncMutationLocked()
|
return nil, err
|
||||||
s.mu.Unlock()
|
}
|
||||||
|
|
||||||
return &keepassgov1.UpsertEntryResponse{Entry: entryToProtoWithModel(visibleModel(model), entry)}, nil
|
return &keepassgov1.UpsertEntryResponse{Entry: entryToProtoWithModel(visibleModel(model), entry)}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrRequestDenied = errors.New("authorization request denied")
|
ErrRequestDenied = errors.New("authorization request denied")
|
||||||
ErrRequestCanceled = errors.New("authorization request canceled")
|
ErrRequestCanceled = errors.New("authorization request canceled")
|
||||||
ErrRequestTimedOut = errors.New("authorization request timed out")
|
ErrRequestTimedOut = errors.New("authorization request timed out")
|
||||||
ErrRequestNotFound = errors.New("authorization request not found")
|
ErrRequestNotFound = errors.New("authorization request not found")
|
||||||
|
ErrBrokerNotConfigured = errors.New("authorization broker is not configured")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Outcome string
|
type Outcome string
|
||||||
@@ -120,7 +121,7 @@ func (b *Broker) SetChangeNotifier(notify func()) {
|
|||||||
|
|
||||||
func (b *Broker) Request(ctx context.Context, token apitokens.Token, op apitokens.Operation, resource apitokens.Resource) (Result, error) {
|
func (b *Broker) Request(ctx context.Context, token apitokens.Token, op apitokens.Operation, resource apitokens.Resource) (Result, error) {
|
||||||
if b == nil {
|
if b == nil {
|
||||||
return Result{}, ErrRequestTimedOut
|
return Result{}, ErrBrokerNotConfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
pending := &pendingRequest{
|
pending := &pendingRequest{
|
||||||
|
|||||||
@@ -121,6 +121,16 @@ func TestBrokerTimesOutPendingRequests(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNilBrokerReturnsConfigurationError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var broker *Broker
|
||||||
|
_, err := broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI"}, apitokens.OperationListGroups, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}})
|
||||||
|
if !errors.Is(err, ErrBrokerNotConfigured) {
|
||||||
|
t.Fatalf("Request(nil broker) error = %v, want %v", err, ErrBrokerNotConfigured)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBrokerNotifiesWhenPendingRequestsChange(t *testing.T) {
|
func TestBrokerNotifiesWhenPendingRequestsChange(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+73
-101
@@ -192,139 +192,111 @@ func (s *State) RemoteCredentialEntries() ([]vault.Entry, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) IssueAPIToken(name, clientName string, expiresAt *time.Time, now time.Time) (apitokens.Token, string, error) {
|
func (s *State) IssueAPIToken(name, clientName string, expiresAt *time.Time, now time.Time) (apitokens.Token, string, error) {
|
||||||
session, ok := s.Session.(MutableSession)
|
result, err := s.mutateAPITokens(apiaudit.EventTokenIssued, "issued API token", func(model *vault.Model) (tokenMutationResult, error) {
|
||||||
if !ok {
|
token, secret, err := apitokens.Issue(name, clientName, expiresAt, now)
|
||||||
return apitokens.Token{}, "", fmt.Errorf("session is not mutable")
|
if err != nil {
|
||||||
}
|
return tokenMutationResult{}, err
|
||||||
model, err := session.Current()
|
}
|
||||||
|
apitokens.Upsert(model, token)
|
||||||
|
return tokenMutationResult{token: token, secret: secret}, nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apitokens.Token{}, "", err
|
return apitokens.Token{}, "", err
|
||||||
}
|
}
|
||||||
token, secret, err := apitokens.Issue(name, clientName, expiresAt, now)
|
return result.token, result.secret, nil
|
||||||
if err != nil {
|
|
||||||
return apitokens.Token{}, "", err
|
|
||||||
}
|
|
||||||
apitokens.Upsert(&model, token)
|
|
||||||
session.Replace(model)
|
|
||||||
if err := s.markDirtyAndAutoSave(); err != nil {
|
|
||||||
return apitokens.Token{}, "", err
|
|
||||||
}
|
|
||||||
s.recordTokenAudit(apiaudit.EventTokenIssued, token, "issued API token")
|
|
||||||
return token, secret, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) RotateAPIToken(id string, now time.Time) (apitokens.Token, string, error) {
|
func (s *State) RotateAPIToken(id string, now time.Time) (apitokens.Token, string, error) {
|
||||||
session, ok := s.Session.(MutableSession)
|
result, err := s.mutateAPITokens(apiaudit.EventTokenRotated, "rotated API token", func(model *vault.Model) (tokenMutationResult, error) {
|
||||||
if !ok {
|
token, err := apitokens.Find(*model, id)
|
||||||
return apitokens.Token{}, "", fmt.Errorf("session is not mutable")
|
if err != nil {
|
||||||
}
|
return tokenMutationResult{}, err
|
||||||
model, err := session.Current()
|
}
|
||||||
|
token, secret, err := apitokens.Rotate(token, now)
|
||||||
|
if err != nil {
|
||||||
|
return tokenMutationResult{}, err
|
||||||
|
}
|
||||||
|
apitokens.Upsert(model, token)
|
||||||
|
return tokenMutationResult{token: token, secret: secret}, nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apitokens.Token{}, "", err
|
return apitokens.Token{}, "", err
|
||||||
}
|
}
|
||||||
token, err := apitokens.Find(model, id)
|
return result.token, result.secret, nil
|
||||||
if err != nil {
|
|
||||||
return apitokens.Token{}, "", err
|
|
||||||
}
|
|
||||||
token, secret, err := apitokens.Rotate(token, now)
|
|
||||||
if err != nil {
|
|
||||||
return apitokens.Token{}, "", err
|
|
||||||
}
|
|
||||||
apitokens.Upsert(&model, token)
|
|
||||||
session.Replace(model)
|
|
||||||
if err := s.markDirtyAndAutoSave(); err != nil {
|
|
||||||
return apitokens.Token{}, "", err
|
|
||||||
}
|
|
||||||
s.recordTokenAudit(apiaudit.EventTokenRotated, token, "rotated API token")
|
|
||||||
return token, secret, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) UpsertAPIToken(token apitokens.Token) error {
|
func (s *State) UpsertAPIToken(token apitokens.Token) error {
|
||||||
session, ok := s.Session.(MutableSession)
|
_, err := s.mutateAPITokens(apiaudit.EventTokenUpdated, "updated API token", func(model *vault.Model) (tokenMutationResult, error) {
|
||||||
if !ok {
|
apitokens.Upsert(model, token)
|
||||||
return fmt.Errorf("session is not mutable")
|
return tokenMutationResult{token: token}, nil
|
||||||
}
|
})
|
||||||
model, err := session.Current()
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
apitokens.Upsert(&model, token)
|
|
||||||
session.Replace(model)
|
|
||||||
if err := s.markDirtyAndAutoSave(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.recordTokenAudit(apiaudit.EventTokenUpdated, token, "updated API token")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) DisableAPIToken(id string) error {
|
func (s *State) DisableAPIToken(id string) error {
|
||||||
session, ok := s.Session.(MutableSession)
|
_, err := s.mutateAPITokens(apiaudit.EventTokenDisabled, "disabled API token", func(model *vault.Model) (tokenMutationResult, error) {
|
||||||
if !ok {
|
token, err := apitokens.Find(*model, id)
|
||||||
return fmt.Errorf("session is not mutable")
|
if err != nil {
|
||||||
}
|
return tokenMutationResult{}, err
|
||||||
model, err := session.Current()
|
}
|
||||||
if err != nil {
|
token = apitokens.Disable(token)
|
||||||
return err
|
apitokens.Upsert(model, token)
|
||||||
}
|
return tokenMutationResult{token: token}, nil
|
||||||
token, err := apitokens.Find(model, id)
|
})
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
token = apitokens.Disable(token)
|
|
||||||
apitokens.Upsert(&model, token)
|
|
||||||
session.Replace(model)
|
|
||||||
if err := s.markDirtyAndAutoSave(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.recordTokenAudit(apiaudit.EventTokenDisabled, token, "disabled API token")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) RevokeAPIToken(id string, when time.Time) error {
|
func (s *State) RevokeAPIToken(id string, when time.Time) error {
|
||||||
session, ok := s.Session.(MutableSession)
|
_, err := s.mutateAPITokens(apiaudit.EventTokenRevoked, "revoked API token", func(model *vault.Model) (tokenMutationResult, error) {
|
||||||
if !ok {
|
token, err := apitokens.Find(*model, id)
|
||||||
return fmt.Errorf("session is not mutable")
|
if err != nil {
|
||||||
}
|
return tokenMutationResult{}, err
|
||||||
model, err := session.Current()
|
}
|
||||||
if err != nil {
|
token = apitokens.Revoke(token, when)
|
||||||
return err
|
apitokens.Upsert(model, token)
|
||||||
}
|
return tokenMutationResult{token: token}, nil
|
||||||
token, err := apitokens.Find(model, id)
|
})
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
token = apitokens.Revoke(token, when)
|
|
||||||
apitokens.Upsert(&model, token)
|
|
||||||
session.Replace(model)
|
|
||||||
if err := s.markDirtyAndAutoSave(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.recordTokenAudit(apiaudit.EventTokenRevoked, token, "revoked API token")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) DeleteAPIToken(id string) error {
|
func (s *State) DeleteAPIToken(id string) error {
|
||||||
|
_, err := s.mutateAPITokens(apiaudit.EventTokenDeleted, "deleted API token", func(model *vault.Model) (tokenMutationResult, error) {
|
||||||
|
token, err := apitokens.Find(*model, id)
|
||||||
|
if err != nil {
|
||||||
|
return tokenMutationResult{}, err
|
||||||
|
}
|
||||||
|
if err := apitokens.Delete(model, id); err != nil {
|
||||||
|
return tokenMutationResult{}, err
|
||||||
|
}
|
||||||
|
return tokenMutationResult{token: token}, nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenMutationResult struct {
|
||||||
|
token apitokens.Token
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) mutateAPITokens(eventType apiaudit.EventType, message string, mutate func(*vault.Model) (tokenMutationResult, error)) (tokenMutationResult, error) {
|
||||||
session, ok := s.Session.(MutableSession)
|
session, ok := s.Session.(MutableSession)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("session is not mutable")
|
return tokenMutationResult{}, fmt.Errorf("session is not mutable")
|
||||||
}
|
}
|
||||||
model, err := session.Current()
|
model, err := session.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return tokenMutationResult{}, err
|
||||||
}
|
}
|
||||||
token, err := apitokens.Find(model, id)
|
result, err := mutate(&model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return tokenMutationResult{}, err
|
||||||
}
|
|
||||||
if err := apitokens.Delete(&model, id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
session.Replace(model)
|
session.Replace(model)
|
||||||
if err := s.markDirtyAndAutoSave(); err != nil {
|
if err := s.markDirtyAndAutoSave(); err != nil {
|
||||||
return err
|
return tokenMutationResult{}, err
|
||||||
}
|
}
|
||||||
s.recordTokenAudit(apiaudit.EventTokenDeleted, token, "deleted API token")
|
s.recordTokenAudit(eventType, result.token, message)
|
||||||
return nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) recordTokenAudit(eventType apiaudit.EventType, token apitokens.Token, message string) {
|
func (s *State) recordTokenAudit(eventType apiaudit.EventType, token apitokens.Token, message string) {
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ func ensureBrowserNativeHosts() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = browserbridge.EnsureNativeHostManifests(appBinaryPath)
|
if err := browserbridge.EnsureNativeHostManifests(appBinaryPath); err != nil {
|
||||||
|
platform.LogInfo("KeePassGO", fmt.Sprintf("keepassgo browser native host registration failed: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type uiApprovalManager struct {
|
type uiApprovalManager struct {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import (
|
|||||||
|
|
||||||
"git.julianfamily.org/keepassgo/internal/grpcaddr"
|
"git.julianfamily.org/keepassgo/internal/grpcaddr"
|
||||||
keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1"
|
keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1"
|
||||||
|
gcodes "google.golang.org/grpc/codes"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -22,6 +24,7 @@ const (
|
|||||||
defaultFirefoxID = "browser@keepassgo.com"
|
defaultFirefoxID = "browser@keepassgo.com"
|
||||||
maxNativeMessageSize = 1024 * 1024
|
maxNativeMessageSize = 1024 * 1024
|
||||||
chromiumIDBytes = 16
|
chromiumIDBytes = 16
|
||||||
|
responseVersion = "1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
@@ -162,33 +165,25 @@ func HandleRequest(ctx context.Context, req Request, client Client) Response {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
||||||
}
|
}
|
||||||
return Response{Success: true, Status: status, Version: "1"}
|
return Response{Success: true, Status: status, Version: responseVersion}
|
||||||
case "find-logins":
|
case "find-logins":
|
||||||
status, err := statusResponse(ctx, client, conn.GRPCAddress)
|
|
||||||
if err != nil {
|
|
||||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
|
||||||
}
|
|
||||||
if status.Locked {
|
|
||||||
return Response{Success: true, Status: status, Matches: nil, Version: "1"}
|
|
||||||
}
|
|
||||||
matches, err := findMatches(ctx, client, req.URL)
|
matches, err := findMatches(ctx, client, req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{Success: false, Error: err.Error(), Status: status}
|
if status := inferredActionStatus(conn.GRPCAddress, err); status != nil {
|
||||||
}
|
return Response{Success: true, Status: status, Matches: nil, Version: responseVersion}
|
||||||
return Response{Success: true, Status: status, Matches: matches, Version: "1"}
|
}
|
||||||
case "get-login":
|
|
||||||
status, err := statusResponse(ctx, client, conn.GRPCAddress)
|
|
||||||
if err != nil {
|
|
||||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
||||||
}
|
}
|
||||||
if status.Locked {
|
return Response{Success: true, Status: availableStatus(conn.GRPCAddress), Matches: matches, Version: responseVersion}
|
||||||
return Response{Success: false, Error: "vault is locked", Status: status}
|
case "get-login":
|
||||||
}
|
|
||||||
credential, err := loadCredential(ctx, client, req.EntryID, req.URL)
|
credential, err := loadCredential(ctx, client, req.EntryID, req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{Success: false, Error: err.Error(), Status: status}
|
if status := inferredActionStatus(conn.GRPCAddress, err); status != nil {
|
||||||
|
return Response{Success: false, Error: err.Error(), Status: status}
|
||||||
|
}
|
||||||
|
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
||||||
}
|
}
|
||||||
return Response{Success: true, Status: status, Credential: credential, Version: "1"}
|
return Response{Success: true, Status: availableStatus(conn.GRPCAddress), Credential: credential, Version: responseVersion}
|
||||||
default:
|
default:
|
||||||
return Response{Success: false, Error: fmt.Sprintf("unsupported action %q", action)}
|
return Response{Success: false, Error: fmt.Sprintf("unsupported action %q", action)}
|
||||||
}
|
}
|
||||||
@@ -198,6 +193,21 @@ func disconnectedStatus(addr string) *Status {
|
|||||||
return &Status{Connected: false, GRPCAddress: strings.TrimSpace(addr)}
|
return &Status{Connected: false, GRPCAddress: strings.TrimSpace(addr)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func availableStatus(addr string) *Status {
|
||||||
|
return &Status{Connected: true, Locked: false, GRPCAddress: strings.TrimSpace(addr)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inferredActionStatus(addr string, err error) *Status {
|
||||||
|
switch gstatus.Code(err) {
|
||||||
|
case gcodes.FailedPrecondition:
|
||||||
|
return &Status{Connected: true, Locked: true, GRPCAddress: strings.TrimSpace(addr)}
|
||||||
|
case gcodes.OK:
|
||||||
|
return availableStatus(addr)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func statusResponse(ctx context.Context, client Client, addr string) (*Status, error) {
|
func statusResponse(ctx context.Context, client Client, addr string) (*Status, error) {
|
||||||
resp, err := client.Status(ctx)
|
resp, err := client.Status(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1"
|
keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1"
|
||||||
|
gcodes "google.golang.org/grpc/codes"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadRequestAndWriteResponse(t *testing.T) {
|
func TestReadRequestAndWriteResponse(t *testing.T) {
|
||||||
@@ -70,8 +72,7 @@ func TestReadRequestAndWriteResponse(t *testing.T) {
|
|||||||
func TestHandleRequestFindLogins(t *testing.T) {
|
func TestHandleRequestFindLogins(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := fakeClient{
|
client := &fakeClient{
|
||||||
status: &keepassgov1.GetSessionStatusResponse{Locked: false, EntryCount: 2},
|
|
||||||
matches: []*keepassgov1.BrowserLoginMatch{
|
matches: []*keepassgov1.BrowserLoginMatch{
|
||||||
{Id: "vault-console", Title: "Vault Console", Username: "dannyocean", Url: "https://vault.example.invalid", Quality: "exact-host"},
|
{Id: "vault-console", Title: "Vault Console", Username: "dannyocean", Url: "https://vault.example.invalid", Quality: "exact-host"},
|
||||||
},
|
},
|
||||||
@@ -87,12 +88,15 @@ func TestHandleRequestFindLogins(t *testing.T) {
|
|||||||
if len(resp.Matches) != 1 || resp.Matches[0].ID != "vault-console" {
|
if len(resp.Matches) != 1 || resp.Matches[0].ID != "vault-console" {
|
||||||
t.Fatalf("HandleRequest().Matches = %#v, want vault-console", resp.Matches)
|
t.Fatalf("HandleRequest().Matches = %#v, want vault-console", resp.Matches)
|
||||||
}
|
}
|
||||||
|
if client.statusCalls != 0 {
|
||||||
|
t.Fatalf("HandleRequest(find-logins) statusCalls = %d, want 0", client.statusCalls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleRequestStatusIncludesPendingApprovalCounts(t *testing.T) {
|
func TestHandleRequestStatusIncludesPendingApprovalCounts(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := fakeClient{
|
client := &fakeClient{
|
||||||
status: &keepassgov1.GetSessionStatusResponse{
|
status: &keepassgov1.GetSessionStatusResponse{
|
||||||
Locked: false,
|
Locked: false,
|
||||||
EntryCount: 2,
|
EntryCount: 2,
|
||||||
@@ -121,8 +125,7 @@ func TestHandleRequestStatusIncludesPendingApprovalCounts(t *testing.T) {
|
|||||||
func TestHandleRequestGetLogin(t *testing.T) {
|
func TestHandleRequestGetLogin(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := fakeClient{
|
client := &fakeClient{
|
||||||
status: &keepassgov1.GetSessionStatusResponse{Locked: false, EntryCount: 1},
|
|
||||||
credential: &keepassgov1.GetBrowserCredentialResponse{
|
credential: &keepassgov1.GetBrowserCredentialResponse{
|
||||||
Id: "vault-console",
|
Id: "vault-console",
|
||||||
Username: "dannyocean",
|
Username: "dannyocean",
|
||||||
@@ -142,12 +145,35 @@ func TestHandleRequestGetLogin(t *testing.T) {
|
|||||||
if resp.Credential == nil || resp.Credential.ID != "vault-console" {
|
if resp.Credential == nil || resp.Credential.ID != "vault-console" {
|
||||||
t.Fatalf("HandleRequest().Credential = %#v, want vault-console", resp.Credential)
|
t.Fatalf("HandleRequest().Credential = %#v, want vault-console", resp.Credential)
|
||||||
}
|
}
|
||||||
|
if client.statusCalls != 0 {
|
||||||
|
t.Fatalf("HandleRequest(get-login) statusCalls = %d, want 0", client.statusCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleRequestFindLoginsInfersLockedStatusFromRPC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := &fakeClient{matchesErr: gstatus.Error(gcodes.FailedPrecondition, "vault is locked")}
|
||||||
|
resp := HandleRequest(context.Background(), Request{
|
||||||
|
Action: "find-logins",
|
||||||
|
BearerToken: "secret",
|
||||||
|
URL: "https://vault.example.invalid/login",
|
||||||
|
}, client)
|
||||||
|
if !resp.Success {
|
||||||
|
t.Fatalf("HandleRequest(find-logins locked) success = false, error = %q", resp.Error)
|
||||||
|
}
|
||||||
|
if resp.Status == nil || !resp.Status.Locked {
|
||||||
|
t.Fatalf("HandleRequest(find-logins locked).Status = %#v, want locked status", resp.Status)
|
||||||
|
}
|
||||||
|
if client.statusCalls != 0 {
|
||||||
|
t.Fatalf("HandleRequest(find-logins locked) statusCalls = %d, want 0", client.statusCalls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleRequestRequiresBearerToken(t *testing.T) {
|
func TestHandleRequestRequiresBearerToken(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
resp := HandleRequest(context.Background(), Request{Action: "status"}, fakeClient{})
|
resp := HandleRequest(context.Background(), Request{Action: "status"}, &fakeClient{})
|
||||||
if resp.Success {
|
if resp.Success {
|
||||||
t.Fatal("HandleRequest().Success = true, want false without token")
|
t.Fatal("HandleRequest().Success = true, want false without token")
|
||||||
}
|
}
|
||||||
@@ -282,10 +308,13 @@ func TestEnsureNativeHostManifestsInstallsFirefoxAndDiscoveredChromium(t *testin
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
status *keepassgov1.GetSessionStatusResponse
|
status *keepassgov1.GetSessionStatusResponse
|
||||||
matches []*keepassgov1.BrowserLoginMatch
|
matches []*keepassgov1.BrowserLoginMatch
|
||||||
credential *keepassgov1.GetBrowserCredentialResponse
|
credential *keepassgov1.GetBrowserCredentialResponse
|
||||||
err error
|
err error
|
||||||
|
matchesErr error
|
||||||
|
credentialErr error
|
||||||
|
statusCalls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeExtensionManifest(t *testing.T, path, name string) {
|
func writeExtensionManifest(t *testing.T, path, name string) {
|
||||||
@@ -333,7 +362,8 @@ func assertManifestContainsExtension(t *testing.T, path, field, want string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) Status(context.Context) (*keepassgov1.GetSessionStatusResponse, error) {
|
func (f *fakeClient) Status(context.Context) (*keepassgov1.GetSessionStatusResponse, error) {
|
||||||
|
f.statusCalls++
|
||||||
if f.err != nil {
|
if f.err != nil {
|
||||||
return nil, f.err
|
return nil, f.err
|
||||||
}
|
}
|
||||||
@@ -343,14 +373,20 @@ func (f fakeClient) Status(context.Context) (*keepassgov1.GetSessionStatusRespon
|
|||||||
return f.status, nil
|
return f.status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) FindBrowserLogins(context.Context, string) ([]*keepassgov1.BrowserLoginMatch, error) {
|
func (f *fakeClient) FindBrowserLogins(context.Context, string) ([]*keepassgov1.BrowserLoginMatch, error) {
|
||||||
|
if f.matchesErr != nil {
|
||||||
|
return nil, f.matchesErr
|
||||||
|
}
|
||||||
if f.err != nil {
|
if f.err != nil {
|
||||||
return nil, f.err
|
return nil, f.err
|
||||||
}
|
}
|
||||||
return f.matches, nil
|
return f.matches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeClient) GetBrowserCredential(context.Context, string, string) (*keepassgov1.GetBrowserCredentialResponse, error) {
|
func (f *fakeClient) GetBrowserCredential(context.Context, string, string) (*keepassgov1.GetBrowserCredentialResponse, error) {
|
||||||
|
if f.credentialErr != nil {
|
||||||
|
return nil, f.credentialErr
|
||||||
|
}
|
||||||
if f.err != nil {
|
if f.err != nil {
|
||||||
return nil, f.err
|
return nil, f.err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user