forked from DelineaXPM/tss-sdk-go
/
secret.go
274 lines (240 loc) · 9.14 KB
/
secret.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package server
import (
"encoding/json"
"fmt"
"log"
"strconv"
)
// resource is the HTTP URL path component for the secrets resource
const resource = "secrets"
// Secret represents a secret from Delinea Secret Server
type Secret struct {
Name string
FolderID, ID, SiteID, SecretTemplateID int
SecretPolicyID, PasswordTypeWebScriptID int `json:",omitempty"`
LauncherConnectAsSecretID, CheckOutIntervalMinutes int
Active, CheckedOut, CheckOutEnabled bool
AutoChangeEnabled, CheckOutChangePasswordEnabled, DelayIndexing bool
EnableInheritPermissions, EnableInheritSecretPolicy, ProxyEnabled bool
RequiresComment, SessionRecordingEnabled, WebLauncherRequiresIncognitoMode bool
Fields []SecretField `json:"Items"`
SshKeyArgs *SshKeyArgs `json:",omitempty"`
}
// SecretField is an item (field) in the secret
type SecretField struct {
ItemID, FieldID, FileAttachmentID int
FieldName, Slug string
FieldDescription, Filename, ItemValue string
IsFile, IsNotes, IsPassword bool
}
type SearchResult struct {
SearchText string
Records []Secret
}
// SshKeyArgs control whether to generate an SSH key pair and a private key
// passphrase when the secret template supports such generation.
//
// WARNING: this struct is only used for write _request_ bodies, and will not
// be present in _response_ bodies.
type SshKeyArgs struct {
GeneratePassphrase, GenerateSshKeys bool
}
// Secret gets the secret with id from the Secret Server of the given tenant
func (s Server) Secret(id int) (*Secret, error) {
secret := new(Secret)
if data, err := s.accessResource("GET", resource, strconv.Itoa(id), nil); err == nil {
if err = json.Unmarshal(data, secret); err != nil {
log.Printf("[ERROR] error parsing response from /%s/%d: %q", resource, id, data)
return nil, err
}
} else {
return nil, err
}
// automatically download file attachments and substitute them for the
// (dummy) ItemValue, so as to make the process transparent to the caller
for index, element := range secret.Fields {
if element.IsFile && element.FileAttachmentID != 0 && element.Filename != "" {
path := fmt.Sprintf("%d/fields/%s", id, element.Slug)
if data, err := s.accessResource("GET", resource, path, nil); err == nil {
secret.Fields[index].ItemValue = string(data)
} else {
return nil, err
}
}
}
return secret, nil
}
// Secret gets the secret with id from the Secret Server of the given tenant
func (s Server) Secrets(searchText, field string) ([]Secret, error) {
searchResult := new(SearchResult)
if data, err := s.searchResources(resource, searchText, field); err == nil {
if err = json.Unmarshal(data, searchResult); err != nil {
log.Printf("[ERROR] error parsing response from /%s/%s: %q", resource, searchText, data)
return nil, err
}
} else {
return nil, err
}
searchRecords := searchResult.Records
secrets := make([]Secret, len(searchRecords))
for i, record := range searchRecords {
//secrets returned in search results are not fully populated
secret, err := s.Secret(record.ID)
if err != nil {
return nil, err
}
secrets[i] = *secret
}
return secrets, nil
}
func (s Server) CreateSecret(secret Secret) (*Secret, error) {
return s.writeSecret(secret, "POST", "/")
}
func (s Server) UpdateSecret(secret Secret) (*Secret, error) {
if secret.SshKeyArgs != nil && (secret.SshKeyArgs.GenerateSshKeys || secret.SshKeyArgs.GeneratePassphrase) {
err := fmt.Errorf("[ERROR] SSH key and passphrase generation is only supported during secret creation. "+
"Could not update the secret named '%s'", secret.Name)
return nil, err
}
secret.SshKeyArgs = nil
return s.writeSecret(secret, "PUT", strconv.Itoa(secret.ID))
}
func (s Server) writeSecret(secret Secret, method string, path string) (*Secret, error) {
writtenSecret := new(Secret)
template, err := s.SecretTemplate(secret.SecretTemplateID)
if err != nil {
return nil, err
}
// If the user did not request SSH key generation, separate the
// secret's fields into file fields and general fields, since we
// need to take active control of either providing the files'
// contents or deleting them. Otherwise, SSH key generation is
// responsible for populating the contents of the file fields.
//
// NOTE!!! This implies support for *either* file contents provided
// by the SSH generator *or* file contents provided by the user.
// This SDK does support secret templates that accept both kinds
// of file fields.
fileFields := make([]SecretField, 0)
generalFields := make([]SecretField, 0)
if secret.SshKeyArgs == nil || !secret.SshKeyArgs.GenerateSshKeys {
fileFields, generalFields, err = secret.separateFileFields(template)
if err != nil {
return nil, err
}
secret.Fields = generalFields
}
// If no SSH generation is called for, remove the SshKeyArgs value.
// Simply having the value in the Secret object causes the
// server to throw an error if the template is not geared towards
// SSH key generation, even if both of the struct's members are
// false.
if secret.SshKeyArgs != nil {
if !secret.SshKeyArgs.GenerateSshKeys && !secret.SshKeyArgs.GeneratePassphrase {
secret.SshKeyArgs = nil
}
}
// If the user specifies no items, perhaps because all the fields are
// generated, apply an empty array to keep the server from rejecting the
// request for missing a required element.
if secret.Fields == nil {
secret.Fields = make([]SecretField, 0)
}
if data, err := s.accessResource(method, resource, path, secret); err == nil {
if err = json.Unmarshal(data, writtenSecret); err != nil {
log.Printf("[ERROR] error parsing response from /%s: %q", resource, data)
return nil, err
}
} else {
return nil, err
}
if err := s.updateFiles(writtenSecret.ID, fileFields); err != nil {
return nil, err
}
return s.Secret(writtenSecret.ID)
}
func (s Server) DeleteSecret(id int) error {
_, err := s.accessResource("DELETE", resource, strconv.Itoa(id), nil)
return err
}
// Field returns the value of the field with the name fieldName
func (s Secret) Field(fieldName string) (string, bool) {
for _, field := range s.Fields {
if fieldName == field.FieldName || fieldName == field.Slug {
log.Printf("[DEBUG] field with name '%s' matches '%s'", field.FieldName, fieldName)
return field.ItemValue, true
}
}
log.Printf("[DEBUG] no matching field for name '%s' in secret '%s'", fieldName, s.Name)
return "", false
}
// FieldById returns the value of the field with the given field ID
func (s Secret) FieldById(fieldId int) (string, bool) {
for _, field := range s.Fields {
if fieldId == field.FieldID {
log.Printf("[DEBUG] field with name '%s' matches field ID '%d'", field.FieldName, fieldId)
return field.ItemValue, true
}
}
log.Printf("[DEBUG] no matching field for ID '%d' in secret '%s'", fieldId, s.Name)
return "", false
}
// updateFiles iterates the list of file fields and if the field's item value is empty,
// deletes the file, otherwise, uploads the contents of the item value as the new/updated
// file attachment.
func (s Server) updateFiles(secretId int, fileFields []SecretField) error {
type fieldMod struct {
Slug string
Dirty bool
Value interface{}
}
type fieldMods struct {
SecretFields []fieldMod
}
type secretPatch struct {
Data fieldMods
}
for _, element := range fileFields {
var path string
var input interface{}
if element.ItemValue == "" {
path = fmt.Sprintf("%d/general", secretId)
input = secretPatch{Data: fieldMods{SecretFields: []fieldMod{{Slug: element.Slug, Dirty: true, Value: nil}}}}
if _, err := s.accessResource("PATCH", resource, path, input); err != nil {
return err
}
} else {
if err := s.uploadFile(secretId, element); err != nil {
return err
}
}
}
return nil
}
// separateFileFields iterates the fields on this secret, and separates them into file
// fields and non-file fields, using the field definitions in the given template as a
// guide. File fields are returned as the first output, non file fields as the second
// output.
func (s Secret) separateFileFields(template *SecretTemplate) ([]SecretField, []SecretField, error) {
var fileFields []SecretField
var nonFileFields []SecretField
for _, field := range s.Fields {
var templateField *SecretTemplateField
var found bool
fieldSlug := field.Slug
if fieldSlug == "" {
if fieldSlug, found = template.FieldIdToSlug(field.FieldID); !found {
return nil, nil, fmt.Errorf("[ERROR] field id '%d' is not defined on the secret template with id '%d'", field.FieldID, template.ID)
}
}
if templateField, found = template.GetField(fieldSlug); !found {
return nil, nil, fmt.Errorf("[ERROR] field name '%s' is not defined on the secret template with id '%d'", fieldSlug, template.ID)
}
if templateField.IsFile {
fileFields = append(fileFields, field)
} else {
nonFileFields = append(nonFileFields, field)
}
}
return fileFields, nonFileFields, nil
}