direct_collections.go

v1.0.0
Doc Versions Source
1
package bitwarden
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"fmt"
7
	"strings"
8
9
	"sourcecraft.dev/bigbes/go-bitwarden/crypto"
10
)
11
12
// DirectCollectionsService handles collection operations via the direct server API.
13
type DirectCollectionsService struct {
14
	dc *DirectClient
15
}
16
17
func (s *DirectCollectionsService) List(ctx context.Context, organizationID, search string) ([]Collection, error) {
18
	kc := s.dc.getKeyChain()
19
	if kc == nil {
20
		return nil, &VaultLockedError{}
21
	}
22
23
	// Get collections from sync or direct API
24
	path := "/api/sync"
25
	data, err := s.dc.transport.Get(ctx, path, nil)
26
	if err != nil {
27
		return nil, err
28
	}
29
30
	var syncResp struct {
31
		Collections []json.RawMessage `json:"collections"`
32
	}
33
	if err := json.Unmarshal(data, &syncResp); err != nil {
34
		return nil, fmt.Errorf("parse sync: %w", err)
35
	}
36
37
	var collections []Collection
38
	for _, raw := range syncResp.Collections {
39
		var c struct {
40
			ID             string `json:"id"`
41
			OrganizationID string `json:"organizationId"`
42
			Name           string `json:"name"`
43
			ExternalID     string `json:"externalId"`
44
		}
45
		if err := json.Unmarshal(raw, &c); err != nil {
46
			continue
47
		}
48
49
		name, err := decryptCollectionName(c.Name, c.OrganizationID, kc)
50
		if err != nil {
51
			continue
52
		}
53
54
		if organizationID != "" && c.OrganizationID != organizationID {
55
			continue
56
		}
57
		if search != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(search)) {
58
			continue
59
		}
60
61
		collections = append(collections, Collection{
62
			ID:             c.ID,
63
			OrganizationID: c.OrganizationID,
64
			Name:           name,
65
			ExternalID:     c.ExternalID,
66
		})
67
	}
68
	return collections, nil
69
}
70
71
func (s *DirectCollectionsService) Get(ctx context.Context, id string) (*Collection, error) {
72
	// Collections are typically fetched via sync; try direct endpoint
73
	kc := s.dc.getKeyChain()
74
	if kc == nil {
75
		return nil, &VaultLockedError{}
76
	}
77
78
	collections, err := s.List(ctx, "", "")
79
	if err != nil {
80
		return nil, err
81
	}
82
83
	for _, c := range collections {
84
		if c.ID == id {
85
			return &c, nil
86
		}
87
	}
88
	return nil, &NotFoundError{Object: "collection", ID: id}
89
}
90
91
// DirectOrgCollectionsService handles organization collection operations.
92
type DirectOrgCollectionsService struct {
93
	dc *DirectClient
94
}
95
96
func (s *DirectOrgCollectionsService) List(ctx context.Context, organizationID string, search string) ([]OrgCollection, error) {
97
	kc := s.dc.getKeyChain()
98
	if kc == nil {
99
		return nil, &VaultLockedError{}
100
	}
101
102
	data, err := s.dc.transport.Get(ctx, "/api/organizations/"+organizationID+"/collections", nil)
103
	if err != nil {
104
		return nil, err
105
	}
106
107
	var resp struct {
108
		Data []json.RawMessage `json:"data"`
109
	}
110
	if err := json.Unmarshal(data, &resp); err != nil {
111
		return nil, fmt.Errorf("parse org collections: %w", err)
112
	}
113
114
	var collections []OrgCollection
115
	for _, raw := range resp.Data {
116
		var c struct {
117
			ID             string   `json:"id"`
118
			OrganizationID string   `json:"organizationId"`
119
			Name           string   `json:"name"`
120
			ExternalID     string   `json:"externalId"`
121
			Groups         []string `json:"groups"`
122
		}
123
		if err := json.Unmarshal(raw, &c); err != nil {
124
			continue
125
		}
126
127
		name, err := decryptCollectionName(c.Name, c.OrganizationID, kc)
128
		if err != nil {
129
			name = c.Name // fallback
130
		}
131
132
		if search != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(search)) {
133
			continue
134
		}
135
136
		collections = append(collections, OrgCollection{
137
			ID:             c.ID,
138
			OrganizationID: c.OrganizationID,
139
			Name:           name,
140
			ExternalID:     c.ExternalID,
141
			Groups:         c.Groups,
142
		})
143
	}
144
	return collections, nil
145
}
146
147
func (s *DirectOrgCollectionsService) Get(ctx context.Context, id string) (*OrgCollection, error) {
148
	return nil, fmt.Errorf("OrgCollections.Get requires organizationID; use List instead")
149
}
150
151
func (s *DirectOrgCollectionsService) Create(ctx context.Context, collection OrgCollection) (*OrgCollection, error) {
152
	kc := s.dc.getKeyChain()
153
	if kc == nil {
154
		return nil, &VaultLockedError{}
155
	}
156
157
	encName, err := encryptCollectionName(collection.Name, collection.OrganizationID, kc)
158
	if err != nil {
159
		return nil, err
160
	}
161
162
	body := map[string]any{
163
		"name":       encName,
164
		"externalId": collection.ExternalID,
165
		"groups":     collection.Groups,
166
	}
167
168
	data, err := s.dc.transport.Post(ctx, "/api/organizations/"+collection.OrganizationID+"/collections", body)
169
	if err != nil {
170
		return nil, err
171
	}
172
173
	var resp OrgCollection
174
	if err := json.Unmarshal(data, &resp); err != nil {
175
		return nil, err
176
	}
177
	resp.Name, _ = decryptCollectionName(resp.Name, resp.OrganizationID, kc)
178
	return &resp, nil
179
}
180
181
func (s *DirectOrgCollectionsService) Update(ctx context.Context, id string, collection OrgCollection) (*OrgCollection, error) {
182
	kc := s.dc.getKeyChain()
183
	if kc == nil {
184
		return nil, &VaultLockedError{}
185
	}
186
187
	encName, err := encryptCollectionName(collection.Name, collection.OrganizationID, kc)
188
	if err != nil {
189
		return nil, err
190
	}
191
192
	body := map[string]any{
193
		"name":       encName,
194
		"externalId": collection.ExternalID,
195
		"groups":     collection.Groups,
196
	}
197
198
	data, err := s.dc.transport.Put(ctx, "/api/organizations/"+collection.OrganizationID+"/collections/"+id, body)
199
	if err != nil {
200
		return nil, err
201
	}
202
203
	var resp OrgCollection
204
	if err := json.Unmarshal(data, &resp); err != nil {
205
		return nil, err
206
	}
207
	resp.Name, _ = decryptCollectionName(resp.Name, resp.OrganizationID, kc)
208
	return &resp, nil
209
}
210
211
func (s *DirectOrgCollectionsService) Delete(ctx context.Context, id string) error {
212
	// Need org ID. Try to find it from the collection.
213
	// For now, require callers to use the org-specific delete endpoint.
214
	return fmt.Errorf("OrgCollections.Delete requires organizationID context; not yet implemented for direct mode")
215
}
216
217
// decryptCollectionName decrypts a collection name using the org key.
218
func decryptCollectionName(encName, orgID string, kc *crypto.KeyChain) (string, error) {
219
	key := kc.OrgKey(orgID)
220
	if key == nil {
221
		return encName, nil // can't decrypt without org key
222
	}
223
	return decryptStr(encName, key)
224
}
225
226
// encryptCollectionName encrypts a collection name using the org key.
227
func encryptCollectionName(name, orgID string, kc *crypto.KeyChain) (string, error) {
228
	key := kc.OrgKey(orgID)
229
	if key == nil {
230
		return "", fmt.Errorf("no key for organization %s", orgID)
231
	}
232
	return encryptStr(name, key)
233
}
234

Source Files