client.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package secretserver
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "strconv"
  20. "strings"
  21. "unicode/utf8"
  22. "github.com/DelineaXPM/tss-sdk-go/v3/server"
  23. "github.com/tidwall/gjson"
  24. corev1 "k8s.io/api/core/v1"
  25. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  26. "github.com/external-secrets/external-secrets/runtime/esutils"
  27. "github.com/external-secrets/external-secrets/runtime/esutils/metadata"
  28. )
  29. const (
  30. // errMsgNoMatchingSecrets is returned by getSecretByName when a search returns zero results.
  31. // This preserves backward compatibility with the original error message used
  32. // before the PushSecret feature was added.
  33. errMsgNoMatchingSecrets = "unable to retrieve secret at this time"
  34. // errMsgNotFound is returned when a secret is not found in a specific folder.
  35. errMsgNotFound = "not found"
  36. // errMsgAmbiguousName is returned by lookupSecretStrict when a plain name
  37. // matches multiple secrets across folders and no folder scope is provided.
  38. errMsgAmbiguousName = "multiple secrets found with the same name across different folders; use the 'folderId:<id>/<name>' key format, a path-based key, or a numeric ID to disambiguate"
  39. // folderPrefix is the prefix used to encode a folder ID in a remote key.
  40. // Format: "folderId:<id>/<name>" (e.g. "folderId:73/my-secret").
  41. folderPrefix = "folderId:"
  42. // validateSearchText is intentionally unlikely to match a real secret. The
  43. // Secret Server search call authenticates the client and returns an empty
  44. // result set when no records match.
  45. validateSearchText = "external-secrets-validation-check"
  46. )
  47. // isNotFoundError checks if an error indicates a secret was not found.
  48. // The TSS SDK (v3) returns all errors as plain fmt.Errorf strings with format
  49. // "<StatusCode> <StatusText>: <body>" — no typed/sentinel errors.
  50. //
  51. // This function uses case-insensitive substring matching with explicit exclusions
  52. // for false-positive patterns produced by our own code (e.g. "not found in secret"
  53. // from updateSecret or "not found in secret template" from createSecret) and
  54. // non-404 HTTP errors that happen to contain "not found" in their body.
  55. func isNotFoundError(err error) bool {
  56. if err == nil {
  57. return false
  58. }
  59. msg := strings.ToLower(err.Error())
  60. // Our own sentinel from getSecretByName / getSecretByNameStrict.
  61. if strings.Contains(msg, errMsgNoMatchingSecrets) {
  62. return true
  63. }
  64. // SDK HTTP 404 responses start with "404 ".
  65. if strings.HasPrefix(msg, "404 ") {
  66. return true
  67. }
  68. // Generic "not found" substring — but exclude false positives.
  69. if strings.Contains(msg, errMsgNotFound) {
  70. // Patterns like "field X not found in secret" or "field X not found in secret template"
  71. // are field-level errors, not secret-not-found errors.
  72. if strings.Contains(msg, "not found in secret") {
  73. return false
  74. }
  75. // Exclude non-404 HTTP errors that happen to contain "not found" in the body
  76. // (e.g. "401 Unauthorized: user not found"). The SDK formats all HTTP errors
  77. // as "<StatusCode> <StatusText>: <body>", so any message starting with a
  78. // 3-digit code followed by a space is an HTTP error.
  79. if len(msg) >= 4 && msg[3] == ' ' && msg[0] >= '0' && msg[0] <= '9' && msg[1] >= '0' && msg[1] <= '9' && msg[2] >= '0' && msg[2] <= '9' {
  80. return false // non-404 HTTP error (404 was already handled above)
  81. }
  82. return true
  83. }
  84. return false
  85. }
  86. // parseFolderPrefix extracts a folder ID and secret name from a key with the
  87. // format "folderId:<id>/<name>". If the key does not match the prefix format,
  88. // it returns (0, key, false) so callers can fall through to other resolution
  89. // strategies.
  90. func parseFolderPrefix(key string) (folderID int, name string, hasFolderPrefix bool) {
  91. if !strings.HasPrefix(key, folderPrefix) {
  92. return 0, key, false
  93. }
  94. rest := strings.TrimPrefix(key, folderPrefix) // "<id>/<name>"
  95. slashIdx := strings.Index(rest, "/") //nolint:modernize // Need index of first slash to separate folder ID from name
  96. if slashIdx < 0 {
  97. // "folderId:73" with no slash/name — treat as not having the prefix.
  98. return 0, key, false
  99. }
  100. idStr := rest[:slashIdx]
  101. secretName := rest[slashIdx+1:]
  102. id, err := strconv.Atoi(idStr)
  103. if err != nil || id <= 0 {
  104. // Non-numeric or non-positive folder ID — treat as not having the prefix.
  105. return 0, key, false
  106. }
  107. if secretName == "" {
  108. // "folderId:73/" with empty name — treat as not having the prefix.
  109. return 0, key, false
  110. }
  111. return id, secretName, true
  112. }
  113. // PushSecretMetadataSpec contains metadata information for pushing secrets to Delinea Secret Server.
  114. type PushSecretMetadataSpec struct {
  115. FolderID int `json:"folderId"`
  116. SecretTemplateID int `json:"secretTemplateId"`
  117. }
  118. type client struct {
  119. api secretAPI
  120. }
  121. var _ esv1.SecretsClient = &client{}
  122. // GetSecret supports several lookup modes:
  123. // 1. Get the secret using the secret ID in ref.Key (e.g. key: 53974).
  124. // 2. Get the secret using the secret "name" (e.g. key: "secretNameHere").
  125. // - Secret names must not contain spaces.
  126. // - If using the secret "name" and multiple secrets are found,
  127. // the first secret in the array will be the secret returned.
  128. // 3. Get the full secret as a JSON-encoded value by leaving ref.Property empty.
  129. // 4. Get a specific value by using a key from the JSON-formatted secret in
  130. // Items.0.ItemValue via gjson (supports nested paths like "server.1").
  131. // If the first field's ItemValue is not valid JSON or the gjson path
  132. // does not match, fall back to matching ref.Property against each field's
  133. // Slug or FieldName (useful for multi-field secrets).
  134. func (c *client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  135. secret, err := c.getSecret(ctx, ref)
  136. if err != nil {
  137. return nil, err
  138. }
  139. if len(secret.Fields) == 0 {
  140. return nil, errors.New("secret contains no fields")
  141. }
  142. jsonStr, err := json.Marshal(secret)
  143. if err != nil {
  144. return nil, err
  145. }
  146. // Intentionally fetch and return the full secret as raw JSON when no specific property is provided.
  147. // This requires calling the API to retrieve the entire secret object.
  148. if ref.Property == "" {
  149. return jsonStr, nil
  150. }
  151. // Primary path: extract ref.Property from the first field's ItemValue via gjson.
  152. // This preserves backward compatibility with the original single-field JSON blob pattern.
  153. val := gjson.Get(string(jsonStr), "Items.0.ItemValue")
  154. if val.Exists() && gjson.Valid(val.String()) {
  155. out := gjson.Get(val.String(), ref.Property)
  156. if out.Exists() {
  157. return []byte(out.String()), nil
  158. }
  159. }
  160. // Fallback: match ref.Property against field Slug or FieldName.
  161. // This supports multi-field secrets where fields are accessed by name.
  162. for index := range secret.Fields {
  163. if secret.Fields[index].Slug == ref.Property || secret.Fields[index].FieldName == ref.Property {
  164. return []byte(secret.Fields[index].ItemValue), nil
  165. }
  166. }
  167. return nil, esv1.NoSecretError{}
  168. }
  169. // PushSecret creates or updates a secret in Delinea Secret Server.
  170. func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  171. if data.GetRemoteKey() == "" {
  172. return errors.New("remote key must be defined")
  173. }
  174. value, err := esutils.ExtractSecretData(data, secret)
  175. if err != nil {
  176. return fmt.Errorf("failed to extract secret data: %w", err)
  177. }
  178. if !utf8.Valid(value) {
  179. return errors.New("secret value is not valid UTF-8")
  180. }
  181. meta, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](data.GetMetadata())
  182. if err != nil {
  183. return fmt.Errorf("failed to parse metadata: %w", err)
  184. }
  185. // Resolve the effective folder ID for both lookups AND creation.
  186. // A folderId encoded in the remoteKey takes precedence over metadata
  187. // (delete and existence-check never see metadata, so the prefix must win
  188. // to keep lookup and creation consistent).
  189. folderID := 0
  190. if meta != nil {
  191. folderID = meta.Spec.FolderID
  192. }
  193. if prefixFolderID, _, ok := parseFolderPrefix(data.GetRemoteKey()); ok {
  194. folderID = prefixFolderID
  195. }
  196. existingSecret, err := c.findExistingSecret(ctx, data.GetRemoteKey(), folderID)
  197. if err != nil {
  198. if !isNotFoundError(err) {
  199. return fmt.Errorf("failed to get secret: %w", err)
  200. }
  201. existingSecret = nil
  202. }
  203. if existingSecret != nil {
  204. // Update existing secret
  205. return c.updateSecret(existingSecret, data.GetProperty(), string(value))
  206. }
  207. if meta == nil || meta.Spec.SecretTemplateID <= 0 {
  208. return errors.New("folderId and secretTemplateId must be provided in metadata to create a new secret")
  209. }
  210. // Use the effective folderID (prefix-overridden or metadata-supplied) for creation.
  211. if folderID <= 0 {
  212. return errors.New("folderId and secretTemplateId must be provided in metadata to create a new secret")
  213. }
  214. createSpec := meta.Spec
  215. createSpec.FolderID = folderID
  216. return c.createSecret(data.GetRemoteKey(), data.GetProperty(), string(value), createSpec)
  217. }
  218. // updateSecret updates an existing secret in Delinea Secret Server.
  219. func (c *client) updateSecret(secret *server.Secret, property, value string) error {
  220. if property == "" {
  221. // If property is empty, put the JSON value in the first field, matching GetSecretMap logic
  222. if len(secret.Fields) > 0 {
  223. secret.Fields[0].ItemValue = value
  224. } else {
  225. return errors.New("secret has no fields to update")
  226. }
  227. } else {
  228. found := false
  229. for i, field := range secret.Fields {
  230. if field.Slug == property || field.FieldName == property {
  231. secret.Fields[i].ItemValue = value
  232. found = true
  233. break
  234. }
  235. }
  236. if !found {
  237. return fmt.Errorf("field %s not found in secret", property)
  238. }
  239. }
  240. _, err := c.api.UpdateSecret(*secret)
  241. if err != nil {
  242. return fmt.Errorf("failed to update secret: %w", err)
  243. }
  244. return nil
  245. }
  246. // createSecret creates a new secret in Delinea Secret Server.
  247. // Only the targeted field is populated; other required template fields
  248. // may cause an API error.
  249. func (c *client) createSecret(name, property, value string, meta PushSecretMetadataSpec) error {
  250. template, err := c.api.SecretTemplate(meta.SecretTemplateID)
  251. if err != nil {
  252. return fmt.Errorf("failed to get secret template: %w", err)
  253. }
  254. if strings.HasSuffix(name, "/") {
  255. return fmt.Errorf("invalid secret name %q: name must not be empty or end with a trailing slash", name)
  256. }
  257. // Strip the "folderId:<id>/" prefix if present so the secret is created
  258. // with just the plain name. The folder is already specified in meta.FolderID.
  259. if _, stripped, ok := parseFolderPrefix(name); ok {
  260. name = stripped
  261. // After prefix stripping, the name should be a simple name (no slashes).
  262. // The folderId format is "folderId:<id>/<name>", not "folderId:<id>/<path>".
  263. if strings.Contains(name, "/") {
  264. return fmt.Errorf("invalid secret name %q in folderId prefix: name must not contain path separators", name)
  265. }
  266. }
  267. // For path-based keys (e.g. "/Folder/SubFolder/SecretName"), extract the
  268. // basename. The folder structure is controlled by meta.FolderID.
  269. normalizedName := strings.TrimPrefix(name, "/")
  270. if strings.Contains(normalizedName, "/") {
  271. parts := strings.Split(normalizedName, "/")
  272. normalizedName = parts[len(parts)-1]
  273. }
  274. newSecret := server.Secret{
  275. Name: normalizedName,
  276. FolderID: meta.FolderID,
  277. SecretTemplateID: meta.SecretTemplateID,
  278. Fields: make([]server.SecretField, 0),
  279. }
  280. if property == "" {
  281. // No property specified: use the first template field.
  282. if len(template.Fields) == 0 {
  283. return errors.New("secret template has no fields")
  284. }
  285. newSecret.Fields = append(newSecret.Fields, server.SecretField{
  286. FieldID: template.Fields[0].SecretTemplateFieldID,
  287. ItemValue: value,
  288. })
  289. } else {
  290. // Use the field matching the specified property.
  291. fieldID, found := findTemplateFieldID(template, property)
  292. if !found {
  293. return fmt.Errorf("field %s not found in secret template", property)
  294. }
  295. newSecret.Fields = append(newSecret.Fields, server.SecretField{
  296. FieldID: fieldID,
  297. ItemValue: value,
  298. })
  299. }
  300. _, err = c.api.CreateSecret(newSecret)
  301. if err != nil {
  302. return fmt.Errorf("failed to create secret: %w", err)
  303. }
  304. return nil
  305. }
  306. // DeleteSecret deletes a secret in Delinea Secret Server.
  307. func (c *client) DeleteSecret(_ context.Context, ref esv1.PushSecretRemoteRef) error {
  308. secret, err := c.lookupSecretStrict(ref.GetRemoteKey())
  309. if err != nil {
  310. // If already deleted/not found, ignore
  311. if isNotFoundError(err) {
  312. return nil
  313. }
  314. return fmt.Errorf("failed to get secret for deletion: %w", err)
  315. }
  316. err = c.api.DeleteSecret(secret.ID)
  317. if err != nil {
  318. return fmt.Errorf("failed to delete secret: %w", err)
  319. }
  320. return nil
  321. }
  322. // SecretExists checks if a secret exists in Delinea Secret Server.
  323. func (c *client) SecretExists(_ context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  324. _, err := c.lookupSecretStrict(ref.GetRemoteKey())
  325. if err != nil {
  326. if isNotFoundError(err) {
  327. return false, nil
  328. }
  329. return false, fmt.Errorf("failed to check if secret exists: %w", err)
  330. }
  331. return true, nil
  332. }
  333. func (c *client) Validate() (esv1.ValidationResult, error) {
  334. if c.api == nil {
  335. return esv1.ValidationResultError, errors.New("secret server API client is not initialized")
  336. }
  337. if _, err := c.api.Secrets(validateSearchText, "Name"); err != nil {
  338. return esv1.ValidationResultError, fmt.Errorf("failed to validate Secret Server credentials: %w", err)
  339. }
  340. return esv1.ValidationResultReady, nil
  341. }
  342. // GetSecretMap retrieves the secret referenced by ref from the Secret Server API
  343. // and returns it as a map of byte slices.
  344. func (c *client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  345. // When a property is requested, GetSecret already fetches the secret and
  346. // applies its own empty-fields guard, so resolve it here and avoid the
  347. // second round-trip a separate getSecret call would incur.
  348. if ref.Property != "" {
  349. value, err := c.GetSecret(ctx, ref)
  350. if err != nil {
  351. return nil, err
  352. }
  353. if data, ok, err := jsonObjectToByteMap(string(value)); ok || err != nil {
  354. return data, err
  355. }
  356. return map[string][]byte{ref.Property: value}, nil
  357. }
  358. secret, err := c.getSecret(ctx, ref)
  359. if err != nil {
  360. return nil, err
  361. }
  362. // Ensure secret has fields before indexing into them
  363. if len(secret.Fields) == 0 {
  364. return nil, errors.New("secret contains no fields")
  365. }
  366. if data, ok, err := jsonObjectToByteMap(secret.Fields[0].ItemValue); ok || err != nil {
  367. return data, err
  368. }
  369. return fieldsToByteMap(secret.Fields)
  370. }
  371. func jsonObjectToByteMap(value string) (map[string][]byte, bool, error) {
  372. trimmed := strings.TrimSpace(value)
  373. // Require a valid JSON object: gating on validity (not just a leading "{")
  374. // means a value that merely starts with "{" but is not valid JSON falls
  375. // through to the plain-text/field-map path instead of hard-failing, so the
  376. // json.Unmarshal error below is effectively defensive only.
  377. if !strings.HasPrefix(trimmed, "{") || !gjson.Valid(trimmed) {
  378. return nil, false, nil
  379. }
  380. secretData := make(map[string]any)
  381. err := json.Unmarshal([]byte(value), &secretData)
  382. if err != nil {
  383. // Do not return the raw error as json.Unmarshal errors may contain
  384. // sensitive secret data in the error message
  385. return nil, true, errors.New("failed to unmarshal secret: invalid JSON format")
  386. }
  387. data := make(map[string][]byte)
  388. for k := range secretData {
  389. data[k], err = esutils.GetByteValueFromMap(secretData, k)
  390. if err != nil {
  391. return nil, true, err
  392. }
  393. }
  394. return data, true, nil
  395. }
  396. func fieldsToByteMap(fields []server.SecretField) (map[string][]byte, error) {
  397. data := make(map[string][]byte, len(fields))
  398. for _, field := range fields {
  399. key := field.Slug
  400. if key == "" {
  401. key = field.FieldName
  402. }
  403. if key == "" && field.FieldID > 0 {
  404. key = strconv.Itoa(field.FieldID)
  405. }
  406. if key == "" {
  407. return nil, errors.New("secret field has no slug, field name, or field ID")
  408. }
  409. data[key] = []byte(field.ItemValue)
  410. }
  411. return data, nil
  412. }
  413. // GetAllSecrets is not supported. The tss-sdk-go v3 SDK search is hard-capped
  414. // at 30 results with no pagination, no tag filtering, and no folder enumeration.
  415. func (c *client) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  416. return nil, errors.New("getting all secrets is not supported by Delinea Secret Server")
  417. }
  418. func (c *client) Close(context.Context) error {
  419. return nil
  420. }
  421. // getSecret retrieves the secret referenced by ref from the Secret Server API.
  422. func (c *client) getSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) (*server.Secret, error) {
  423. if ref.Version != "" {
  424. return nil, errors.New("specifying a version is not supported")
  425. }
  426. return c.lookupSecret(ref.Key, 0)
  427. }
  428. // findExistingSecret looks up a secret for PushSecret's find-or-create logic.
  429. // Unlike getSecret (used for reads), this refuses ambiguous plain-name matches
  430. // when no folder scope is available, matching the safety behavior of DeleteSecret
  431. // and SecretExists.
  432. func (c *client) findExistingSecret(_ context.Context, key string, folderID int) (*server.Secret, error) {
  433. // When a folder scope is available (either from prefix or metadata),
  434. // the lookup is unambiguous — use the regular (non-strict) resolver.
  435. if folderID > 0 {
  436. return c.lookupSecret(key, folderID)
  437. }
  438. // No folder scope: use strict lookup to reject ambiguous plain names.
  439. return c.lookupSecretStrict(key)
  440. }
  441. // lookupSecret resolves a secret by path ("/..."), numeric ID, folder-scoped
  442. // name ("folderId:<id>/<name>"), or plain name.
  443. // The folderID scopes name-based lookups (0 = any folder). A folder prefix
  444. // encoded in the key takes precedence over the folderID argument.
  445. func (c *client) lookupSecret(key string, folderID int) (*server.Secret, error) {
  446. // 1. Folder-scoped prefix: "folderId:<id>/<name>" — override folderID and
  447. // resolve by name within the specified folder.
  448. if prefixFolderID, name, ok := parseFolderPrefix(key); ok {
  449. return c.getSecretByName(name, prefixFolderID)
  450. }
  451. // 2. Path-based key: fully qualified, no disambiguation needed.
  452. if strings.HasPrefix(key, "/") {
  453. return c.api.SecretByPath(key)
  454. }
  455. // 3. Numeric key: treat as ID first; fall back to name-based lookup so that
  456. // secrets whose name happens to be a numeric string can still be resolved.
  457. if id, err := strconv.Atoi(key); err == nil {
  458. secret, err := c.api.Secret(id)
  459. if err == nil && secret != nil {
  460. return secret, nil
  461. }
  462. if !isNotFoundError(err) {
  463. return nil, err
  464. }
  465. }
  466. return c.getSecretByName(key, folderID)
  467. }
  468. // lookupSecretStrict resolves a secret like lookupSecret but refuses to
  469. // silently pick the first match when a plain name (no folderId prefix, no
  470. // path, no numeric ID) matches more than one secret across folders.
  471. // This is used by destructive operations (DeleteSecret) and existence checks
  472. // (SecretExists) that must not accidentally act on the wrong secret.
  473. func (c *client) lookupSecretStrict(key string) (*server.Secret, error) {
  474. // 1. Folder-scoped prefix: unambiguous — delegate directly.
  475. if prefixFolderID, name, ok := parseFolderPrefix(key); ok {
  476. return c.getSecretByName(name, prefixFolderID)
  477. }
  478. // 2. Path-based key: unambiguous.
  479. if strings.HasPrefix(key, "/") {
  480. return c.api.SecretByPath(key)
  481. }
  482. // 3. Numeric key: try as ID first; fall back to name-based lookup.
  483. if id, err := strconv.Atoi(key); err == nil {
  484. secret, err := c.api.Secret(id)
  485. if err == nil && secret != nil {
  486. return secret, nil
  487. }
  488. if !isNotFoundError(err) {
  489. return nil, err
  490. }
  491. }
  492. // 4. Plain name: reject if ambiguous (multiple matches without folder scope).
  493. return c.getSecretByNameStrict(key)
  494. }
  495. // getSecretByNameStrict searches for a secret by name and returns an error if
  496. // multiple secrets share the same name across different folders.
  497. func (c *client) getSecretByNameStrict(name string) (*server.Secret, error) {
  498. secrets, err := c.api.Secrets(name, "Name")
  499. if err != nil {
  500. return nil, err
  501. }
  502. if len(secrets) == 0 {
  503. return nil, errors.New(errMsgNoMatchingSecrets)
  504. }
  505. if len(secrets) > 1 {
  506. return nil, errors.New(errMsgAmbiguousName)
  507. }
  508. return &secrets[0], nil
  509. }
  510. func (c *client) getSecretByName(name string, folderID int) (*server.Secret, error) {
  511. secrets, err := c.api.Secrets(name, "Name")
  512. if err != nil {
  513. return nil, err
  514. }
  515. if len(secrets) == 0 {
  516. return nil, errors.New(errMsgNoMatchingSecrets)
  517. }
  518. // No folder constraint: return the first match.
  519. if folderID == 0 {
  520. return &secrets[0], nil
  521. }
  522. // Find the first secret matching the requested folder.
  523. for i, s := range secrets {
  524. if s.FolderID == folderID {
  525. return &secrets[i], nil
  526. }
  527. }
  528. return nil, errors.New(errMsgNotFound)
  529. }
  530. func findTemplateFieldID(template *server.SecretTemplate, property string) (int, bool) {
  531. fieldID, found := template.FieldSlugToId(property)
  532. if found {
  533. return fieldID, true
  534. }
  535. // fallback check if they used name instead of slug
  536. for _, f := range template.Fields {
  537. if f.Name == property || f.FieldSlugName == property {
  538. return f.SecretTemplateFieldID, true
  539. }
  540. }
  541. return 0, false
  542. }