direct_items.go

v1.0.0
Doc Versions Source
1
package bitwarden
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"fmt"
7
	"slices"
8
	"strings"
9
)
10
11
// DirectItemsService handles vault item operations via the direct server API.
12
type DirectItemsService struct {
13
	dc *DirectClient
14
}
15
16
func (s *DirectItemsService) List(ctx context.Context, filter ListFilter) ([]Item, error) {
17
	kc := s.dc.getKeyChain()
18
	if kc == nil {
19
		return nil, &VaultLockedError{}
20
	}
21
22
	// Try cache first
23
	if cached, ok := s.dc.cache.getCiphers(); ok {
24
		return filterCiphersFromCache(cached, filter), nil
25
	}
26
27
	data, err := s.dc.transport.Get(ctx, "/api/ciphers", nil)
28
	if err != nil {
29
		return nil, err
30
	}
31
32
	var resp struct {
33
		Data []json.RawMessage `json:"data"`
34
	}
35
	if err := json.Unmarshal(data, &resp); err != nil {
36
		return nil, fmt.Errorf("parse ciphers list: %w", err)
37
	}
38
39
	var items []Item
40
	for _, raw := range resp.Data {
41
		item, err := decryptCipher(raw, kc)
42
		if err != nil {
43
			continue // skip items we can't decrypt
44
		}
45
		if matchesFilter(item, filter) {
46
			items = append(items, *item)
47
		}
48
	}
49
	return items, nil
50
}
51
52
func (s *DirectItemsService) Get(ctx context.Context, id string) (*Item, error) {
53
	kc := s.dc.getKeyChain()
54
	if kc == nil {
55
		return nil, &VaultLockedError{}
56
	}
57
58
	// Try cache first
59
	if cached, ok := s.dc.cache.getCipher(id); ok {
60
		return cached, nil
61
	}
62
63
	data, err := s.dc.transport.Get(ctx, "/api/ciphers/"+id, nil)
64
	if err != nil {
65
		return nil, err
66
	}
67
68
	return decryptCipher(data, kc)
69
}
70
71
func (s *DirectItemsService) Create(ctx context.Context, item Item) (*Item, error) {
72
	kc := s.dc.getKeyChain()
73
	if kc == nil {
74
		return nil, &VaultLockedError{}
75
	}
76
77
	enc, err := encryptItem(item, kc)
78
	if err != nil {
79
		return nil, fmt.Errorf("encrypt item: %w", err)
80
	}
81
82
	data, err := s.dc.transport.Post(ctx, "/api/ciphers", enc)
83
	if err != nil {
84
		return nil, err
85
	}
86
87
	created, err := decryptCipher(data, kc)
88
	if err != nil {
89
		return nil, err
90
	}
91
	s.dc.cache.upsertCipher(*created)
92
	return created, nil
93
}
94
95
func (s *DirectItemsService) Update(ctx context.Context, id string, item Item) (*Item, error) {
96
	kc := s.dc.getKeyChain()
97
	if kc == nil {
98
		return nil, &VaultLockedError{}
99
	}
100
101
	enc, err := encryptItem(item, kc)
102
	if err != nil {
103
		return nil, fmt.Errorf("encrypt item: %w", err)
104
	}
105
106
	data, err := s.dc.transport.Put(ctx, "/api/ciphers/"+id, enc)
107
	if err != nil {
108
		return nil, err
109
	}
110
111
	updated, err := decryptCipher(data, kc)
112
	if err != nil {
113
		return nil, err
114
	}
115
	s.dc.cache.upsertCipher(*updated)
116
	return updated, nil
117
}
118
119
func (s *DirectItemsService) Delete(ctx context.Context, id string) error {
120
	// Use PUT for soft delete (matching bw serve behavior)
121
	_, err := s.dc.transport.Put(ctx, "/api/ciphers/"+id+"/delete", nil)
122
	if err == nil {
123
		s.dc.cache.softDeleteCipher(id)
124
	}
125
	return err
126
}
127
128
func (s *DirectItemsService) Restore(ctx context.Context, id string) error {
129
	_, err := s.dc.transport.Put(ctx, "/api/ciphers/"+id+"/restore", nil)
130
	if err == nil {
131
		s.dc.cache.restoreCipher(id)
132
	}
133
	return err
134
}
135
136
func (s *DirectItemsService) Move(ctx context.Context, id, organizationID string, collectionIDs []string) error {
137
	body := map[string]any{
138
		"collectionIds": collectionIDs,
139
	}
140
	_, err := s.dc.transport.Post(ctx, "/api/ciphers/"+id+"/share", body)
141
	if err == nil {
142
		s.dc.cache.invalidate() // org move changes key context, full invalidation
143
	}
144
	return err
145
}
146
147
// matchesFilter performs client-side filtering of decrypted items.
148
func matchesFilter(item *Item, f ListFilter) bool {
149
	if f.Search != "" {
150
		search := strings.ToLower(f.Search)
151
		if !strings.Contains(strings.ToLower(item.Name), search) {
152
			return false
153
		}
154
	}
155
	if f.OrganizationID != "" && item.OrganizationID != f.OrganizationID {
156
		return false
157
	}
158
	if f.FolderID != "" {
159
		if item.FolderID == nil || *item.FolderID != f.FolderID {
160
			return false
161
		}
162
	}
163
	if f.CollectionID != "" {
164
		found := slices.Contains(item.CollectionIDs, f.CollectionID)
165
		if !found {
166
			return false
167
		}
168
	}
169
	if f.URL != "" && item.Login != nil {
170
		found := false
171
		for _, u := range item.Login.URIs {
172
			if strings.Contains(u.URI, f.URL) {
173
				found = true
174
				break
175
			}
176
		}
177
		if !found {
178
			return false
179
		}
180
	}
181
	if f.Trash {
182
		if item.DeletedDate == nil {
183
			return false
184
		}
185
	} else {
186
		if item.DeletedDate != nil {
187
			return false
188
		}
189
	}
190
	return true
191
}
192

Source Files