client.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  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 keepersecurity
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "maps"
  20. "regexp"
  21. "strings"
  22. ksm "github.com/keeper-security/secrets-manager-go/core"
  23. corev1 "k8s.io/api/core/v1"
  24. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  25. "github.com/external-secrets/external-secrets/runtime/metrics"
  26. )
  27. const (
  28. errKeeperSecuritySecretsNotFound = "unable to find secrets. %w"
  29. errKeeperSecuritySecretNotFound = "unable to find secret %s. Error: %w"
  30. errKeeperSecuritySecretNotUnique = "more than 1 secret %s found"
  31. errKeeperSecurityRecordNotFound = "%w: no record matched %s"
  32. errKeeperSecurityInvalidSecretInvalidFormat = "invalid secret. Invalid format: %w"
  33. errKeeperSecurityInvalidSecretDuplicatedKey = "invalid Secret. Following keys are duplicated %s"
  34. errKeeperSecurityInvalidProperty = "invalid Property. Secret %s does not have any key matching %s"
  35. errKeeperSecurityInvalidField = "invalid Field. Key %s does not exists"
  36. errKeeperSecurityNoFields = "invalid Secret. Secret %s does not contain any valid field/file"
  37. keeperSecurityFileRef = "fileRef"
  38. keeperSecurityMfa = "oneTimeCode"
  39. errTagsNotImplemented = "'find.tags' is not implemented in the KeeperSecurity provider"
  40. errPathNotImplemented = "'find.path' is not implemented in the KeeperSecurity provider"
  41. errInvalidJSONSecret = "invalid Secret. Secret %s can not be converted to JSON. %w"
  42. errInvalidRegex = "find.name.regex. Invalid Regular expresion %s. %w"
  43. errInvalidRemoteRefKey = "match.remoteRef.remoteKey. Invalid format. Format should match secretName/key got %s"
  44. errInvalidSecretType = "ESO can only push/delete records of type %s. Secret %s is type %s"
  45. errFieldNotFound = "secret %s does not contain any custom field with label %s"
  46. errKeeperSecurityMissingFolderIDForCreate = "folderID must be set on the SecretStore to create a new Keeper Security record"
  47. externalSecretType = "externalSecrets"
  48. secretType = "secret"
  49. // LoginType represents the login field type.
  50. LoginType = "login"
  51. // LoginTypeExpr is the regex expression for matching login/username fields.
  52. LoginTypeExpr = "login|username"
  53. // PasswordType represents the password field type.
  54. PasswordType = "password"
  55. // URLTypeExpr is the regex expression for matching URL/baseurl fields.
  56. URLTypeExpr = "url|baseurl"
  57. // URLType represents the URL field type.
  58. URLType = "url"
  59. )
  60. // Client represents a KeeperSecurity client that can interact with the KeeperSecurity API.
  61. type Client struct {
  62. ksmClient SecurityClient
  63. folderID string
  64. getByTitleFallback bool
  65. }
  66. // SecurityClient defines the interface for interacting with KeeperSecurity's API.
  67. type SecurityClient interface {
  68. GetSecrets(filter []string) ([]*ksm.Record, error)
  69. GetSecretByTitle(recordTitle string) (*ksm.Record, error)
  70. GetSecretsByTitle(recordTitle string) (records []*ksm.Record, err error)
  71. CreateSecretWithRecordData(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error)
  72. DeleteSecrets(recrecordUids []string) (map[string]string, error)
  73. Save(record *ksm.Record) error
  74. }
  75. // Field represents a KeeperSecurity field with its type, label (optional), and value.
  76. type Field struct {
  77. Type string `json:"type"`
  78. Label string `json:"label,omitempty"`
  79. Value []any `json:"value"`
  80. }
  81. // CustomField represents a custom field in KeeperSecurity with its type, label and value.
  82. type CustomField struct {
  83. Type string `json:"type"`
  84. Label string `json:"label"`
  85. Value []any `json:"value"`
  86. }
  87. // File represents a file stored in KeeperSecurity with its title and content.
  88. type File struct {
  89. Title string `json:"type"`
  90. Content string `json:"content"`
  91. }
  92. // Secret represents a KeeperSecurity secret with its metadata and content.
  93. type Secret struct {
  94. Title string `json:"title"`
  95. Type string `json:"type"`
  96. Fields []Field `json:"fields"`
  97. Custom []CustomField `json:"custom"`
  98. Files []File `json:"files"`
  99. }
  100. // Validate performs validation of the Keeper Security client configuration.
  101. func (c *Client) Validate() (esv1.ValidationResult, error) {
  102. return esv1.ValidationResultReady, nil
  103. }
  104. // GetSecret retrieves a secret from Keeper Security by ID or name.
  105. // It first attempts to find the secret by ID, then falls back to name lookup.
  106. // The name lookup must be opted in by setting getByTitleFallback on the provider.
  107. // A record that does not exist yields esv1.NoSecretErr, which is what the
  108. // reconciler keys deletionPolicy off.
  109. func (c *Client) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  110. secret, err := c.findByIDWithNameFallback(ref.Key)
  111. if err != nil {
  112. return nil, err
  113. }
  114. return secret.getItem(ref)
  115. }
  116. // GetSecretMap retrieves a secret from Keeper Security and returns it as a map.
  117. func (c *Client) GetSecretMap(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  118. secret, err := c.findByIDWithNameFallback(ref.Key)
  119. if err != nil {
  120. return nil, err
  121. }
  122. return secret.getItems(ref)
  123. }
  124. // It first attempts to find the secret by ID, then falls back to name lookup.
  125. // The name lookup must be opted in by setting getByTitleFallback on the provider.
  126. func (c *Client) findByIDWithNameFallback(key string) (*Secret, error) {
  127. record, err := c.findSecretByID(key)
  128. if err != nil {
  129. return nil, err
  130. }
  131. if record == nil && c.getByTitleFallback {
  132. records, err := c.ksmClient.GetSecretsByTitle(key)
  133. metrics.ObserveAPICall(ProviderKeeperSecurity, CallKeeperSecurityGetSecretsByTitle, err)
  134. if err != nil {
  135. return nil, err
  136. }
  137. if len(records) > 1 {
  138. return nil, errors.New(errKeeperSecuritySecretNotUnique)
  139. } else if len(records) == 1 {
  140. record = records[0]
  141. }
  142. }
  143. if record == nil {
  144. // Only a genuinely absent record gets the sentinel; the API failures
  145. // wrapped by findSecretByID/GetSecretsByTitle above must stay generic so
  146. // an outage is not mistaken for a deletion.
  147. return nil, fmt.Errorf(errKeeperSecurityRecordNotFound, esv1.NoSecretErr, key)
  148. }
  149. secret, err := c.getValidKeeperSecret(record)
  150. if err != nil {
  151. return nil, err
  152. }
  153. return secret, nil
  154. }
  155. // GetAllSecrets retrieves all secrets from Keeper Security that match the given criteria.
  156. func (c *Client) GetAllSecrets(_ context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  157. if ref.Tags != nil {
  158. return nil, errors.New(errTagsNotImplemented)
  159. }
  160. if ref.Path != nil {
  161. return nil, errors.New(errPathNotImplemented)
  162. }
  163. secretData := make(map[string][]byte)
  164. records, err := c.findSecrets()
  165. // GetAllSecrets retrieves all secrets from Keeper Security that match the given criteria.
  166. // Currently supports filtering by name pattern only.
  167. if err != nil {
  168. return nil, err
  169. }
  170. for _, record := range records {
  171. secret, err := c.getValidKeeperSecret(record)
  172. if err != nil {
  173. return nil, err
  174. }
  175. match, err := regexp.MatchString(ref.Name.RegExp, secret.Title)
  176. if err != nil {
  177. return nil, fmt.Errorf(errInvalidRegex, ref.Name.RegExp, err)
  178. }
  179. if !match {
  180. continue
  181. }
  182. secretData[secret.Title], err = secret.getItem(esv1.ExternalSecretDataRemoteRef{})
  183. if err != nil {
  184. return nil, err
  185. }
  186. }
  187. return secretData, nil
  188. }
  189. // Close implements cleanup operations for the Keeper Security client.
  190. func (c *Client) Close(_ context.Context) error {
  191. return nil
  192. }
  193. // PushSecret creates or updates a secret in Keeper Security.
  194. func (c *Client) PushSecret(_ context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  195. if data.GetSecretKey() == "" {
  196. return errors.New("pushing the whole secret is not yet implemented")
  197. }
  198. // Close implements cleanup operations for the Keeper Security client
  199. value := secret.Data[data.GetSecretKey()]
  200. parts, err := c.buildSecretNameAndKey(data)
  201. if err != nil {
  202. return err
  203. // PushSecret creates or updates a secret in Keeper Security.
  204. // Currently only supports pushing individual secret values, not entire secrets.
  205. }
  206. record, err := c.findSecretByName(parts[0])
  207. if err != nil {
  208. return err
  209. }
  210. if record != nil {
  211. if record.Type() != externalSecretType {
  212. return fmt.Errorf(errInvalidSecretType, externalSecretType, record.Title(), record.Type())
  213. }
  214. return c.updateSecret(record, parts[1], value)
  215. }
  216. _, err = c.createSecret(parts[0], parts[1], value)
  217. return err
  218. }
  219. // DeleteSecret removes a secret from Keeper Security.
  220. func (c *Client) DeleteSecret(_ context.Context, remoteRef esv1.PushSecretRemoteRef) error {
  221. parts, err := c.buildSecretNameAndKey(remoteRef)
  222. if err != nil {
  223. return err
  224. }
  225. secret, err := c.findSecretByName(parts[0])
  226. if err != nil {
  227. return err
  228. } else if secret == nil {
  229. // DeleteSecret removes a secret from Keeper Security.
  230. // Returns nil if the secret doesn't exist (already deleted).
  231. return nil // not found == already deleted (success)
  232. }
  233. if secret.Type() != externalSecretType {
  234. return fmt.Errorf(errInvalidSecretType, externalSecretType, secret.Title(), secret.Type())
  235. }
  236. _, err = c.ksmClient.DeleteSecrets([]string{secret.Uid})
  237. metrics.ObserveAPICall(ProviderKeeperSecurity, CallKeeperSecurityDeleteSecrets, err)
  238. return err
  239. }
  240. // SecretExists checks if a secret exists in Keeper Security.
  241. func (c *Client) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  242. return false, errors.New("not implemented")
  243. }
  244. func (c *Client) buildSecretNameAndKey(remoteRef esv1.PushSecretRemoteRef) ([]string, error) {
  245. parts := strings.Split(remoteRef.GetRemoteKey(), "/")
  246. if len(parts) != 2 {
  247. return nil, fmt.Errorf(errInvalidRemoteRefKey, remoteRef.GetRemoteKey())
  248. }
  249. // SecretExists checks if a secret exists in Keeper Security.
  250. // This method is not implemented yet.
  251. return parts, nil
  252. }
  253. func (c *Client) createSecret(name, key string, value []byte) (string, error) {
  254. normalizedKey := strings.ToLower(key)
  255. externalSecretRecord := ksm.NewRecordCreate(externalSecretType, name)
  256. login := regexp.MustCompile(LoginTypeExpr)
  257. pass := regexp.MustCompile(PasswordType)
  258. url := regexp.MustCompile(URLTypeExpr)
  259. switch {
  260. case login.MatchString(normalizedKey):
  261. externalSecretRecord.Fields = append(externalSecretRecord.Fields,
  262. ksm.NewLogin(string(value)),
  263. )
  264. case pass.MatchString(normalizedKey):
  265. externalSecretRecord.Fields = append(externalSecretRecord.Fields,
  266. ksm.NewPassword(string(value)),
  267. )
  268. case url.MatchString(normalizedKey):
  269. externalSecretRecord.Fields = append(externalSecretRecord.Fields,
  270. ksm.NewUrl(string(value)),
  271. )
  272. default:
  273. field := ksm.KeeperRecordField{Type: secretType, Label: key}
  274. externalSecretRecord.Custom = append(externalSecretRecord.Custom,
  275. ksm.Secret{KeeperRecordField: field, Value: []string{string(value)}},
  276. )
  277. }
  278. if c.folderID == "" {
  279. return "", errors.New(errKeeperSecurityMissingFolderIDForCreate)
  280. }
  281. uid, err := c.ksmClient.CreateSecretWithRecordData("", c.folderID, externalSecretRecord)
  282. metrics.ObserveAPICall(ProviderKeeperSecurity, CallKeeperSecurityCreateSecretWithRecordData, err)
  283. return uid, err
  284. }
  285. func (c *Client) updateSecret(secret *ksm.Record, key string, value []byte) error {
  286. normalizedKey := strings.ToLower(key)
  287. login := regexp.MustCompile(LoginTypeExpr)
  288. pass := regexp.MustCompile(PasswordType)
  289. url := regexp.MustCompile(URLTypeExpr)
  290. custom := false
  291. switch {
  292. case login.MatchString(normalizedKey):
  293. secret.SetFieldValueSingle(LoginType, string(value))
  294. case pass.MatchString(normalizedKey):
  295. secret.SetPassword(string(value))
  296. case url.MatchString(normalizedKey):
  297. secret.SetFieldValueSingle(URLType, string(value))
  298. default:
  299. custom = true
  300. }
  301. if custom {
  302. field := secret.GetCustomFieldValueByLabel(key)
  303. if field != "" {
  304. secret.SetCustomFieldValueSingle(key, string(value))
  305. } else {
  306. return fmt.Errorf(errFieldNotFound, secret.Title(), key)
  307. }
  308. }
  309. err := c.ksmClient.Save(secret)
  310. metrics.ObserveAPICall(ProviderKeeperSecurity, CallKeeperSecuritySave, err)
  311. return err
  312. }
  313. func (c *Client) getValidKeeperSecret(secret *ksm.Record) (*Secret, error) {
  314. keeperSecret := Secret{}
  315. err := json.Unmarshal([]byte(secret.RawJson), &keeperSecret)
  316. if err != nil {
  317. return nil, fmt.Errorf(errKeeperSecurityInvalidSecretInvalidFormat, err)
  318. }
  319. keeperSecret.addFiles(secret.Files)
  320. err = keeperSecret.validate()
  321. if err != nil {
  322. return nil, err
  323. }
  324. return &keeperSecret, nil
  325. }
  326. func (c *Client) findSecrets() ([]*ksm.Record, error) {
  327. records, err := c.ksmClient.GetSecrets([]string{})
  328. metrics.ObserveAPICall(ProviderKeeperSecurity, CallKeeperSecurityGetSecrets, err)
  329. if err != nil {
  330. return nil, fmt.Errorf(errKeeperSecuritySecretsNotFound, err)
  331. }
  332. return records, nil
  333. }
  334. func (c *Client) findSecretByID(id string) (*ksm.Record, error) {
  335. records, err := c.ksmClient.GetSecrets([]string{id})
  336. metrics.ObserveAPICall(ProviderKeeperSecurity, CallKeeperSecurityGetSecrets, err)
  337. if err != nil {
  338. return nil, fmt.Errorf(errKeeperSecuritySecretNotFound, id, err)
  339. }
  340. if len(records) == 0 {
  341. return nil, nil
  342. }
  343. return records[0], nil
  344. }
  345. func (c *Client) findSecretByName(name string) (*ksm.Record, error) {
  346. records, err := c.ksmClient.GetSecretsByTitle(name)
  347. metrics.ObserveAPICall(ProviderKeeperSecurity, CallKeeperSecurityGetSecretsByTitle, err)
  348. if err != nil {
  349. return nil, err
  350. }
  351. // filter in-place, preserve only records of type externalSecretType
  352. n := 0
  353. for _, record := range records {
  354. if record.Type() == externalSecretType {
  355. records[n] = record
  356. n++
  357. }
  358. }
  359. records = records[:n]
  360. // record not found is not an error - handled differently:
  361. // PushSecret will create new record instead
  362. // DeleteSecret will consider record already deleted (no error)
  363. if len(records) == 0 {
  364. return nil, nil
  365. } else if len(records) == 1 {
  366. return records[0], nil
  367. }
  368. // len(records) > 1
  369. return nil, fmt.Errorf(errKeeperSecuritySecretNotUnique, name)
  370. }
  371. func (s *Secret) validate() error {
  372. fields := make(map[string]int)
  373. for _, field := range s.Fields {
  374. fieldKey := field.Label
  375. if fieldKey == "" {
  376. fieldKey = field.Type
  377. }
  378. fields[fieldKey]++
  379. }
  380. for _, customField := range s.Custom {
  381. fields[customField.Label]++
  382. }
  383. for _, file := range s.Files {
  384. fields[file.Title]++
  385. }
  386. var duplicates []string
  387. for key, ocurrences := range fields {
  388. if ocurrences > 1 {
  389. duplicates = append(duplicates, key)
  390. }
  391. }
  392. if len(duplicates) != 0 {
  393. return fmt.Errorf(errKeeperSecurityInvalidSecretDuplicatedKey, strings.Join(duplicates, ", "))
  394. }
  395. return nil
  396. }
  397. func (s *Secret) addFiles(keeperFiles []*ksm.KeeperFile) {
  398. for _, f := range keeperFiles {
  399. s.Files = append(
  400. s.Files,
  401. File{
  402. Title: f.Title,
  403. Content: string(f.GetFileData()),
  404. },
  405. )
  406. }
  407. }
  408. func (s *Secret) getItem(ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  409. if ref.Property != "" {
  410. return s.getProperty(ref.Property)
  411. }
  412. secret, err := s.toString()
  413. return []byte(secret), err
  414. }
  415. func (s *Secret) getItems(ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  416. secretData := make(map[string][]byte)
  417. if ref.Property != "" {
  418. value, err := s.getProperty(ref.Property)
  419. if err != nil {
  420. return nil, err
  421. }
  422. secretData[ref.Property] = value
  423. return secretData, nil
  424. }
  425. fields := s.getFields()
  426. maps.Copy(secretData, fields)
  427. customFields := s.getCustomFields()
  428. maps.Copy(secretData, customFields)
  429. files := s.getFiles()
  430. maps.Copy(secretData, files)
  431. if len(secretData) == 0 {
  432. return nil, fmt.Errorf(errKeeperSecurityNoFields, s.Title)
  433. }
  434. return secretData, nil
  435. }
  436. func getFieldValue(value []any) []byte {
  437. if len(value) < 1 {
  438. return []byte{}
  439. }
  440. if len(value) == 1 {
  441. res, _ := json.Marshal(value[0])
  442. if str, ok := value[0].(string); ok {
  443. res = []byte(str)
  444. }
  445. return res
  446. }
  447. res, _ := json.Marshal(value)
  448. return res
  449. }
  450. func (s *Secret) getField(key string) ([]byte, error) {
  451. for _, field := range s.Fields {
  452. fieldKey := field.Label
  453. if fieldKey == "" {
  454. fieldKey = field.Type
  455. }
  456. if fieldKey == key && field.Type != keeperSecurityFileRef && field.Type != keeperSecurityMfa && len(field.Value) > 0 {
  457. return getFieldValue(field.Value), nil
  458. }
  459. }
  460. return nil, fmt.Errorf(errKeeperSecurityInvalidField, key)
  461. }
  462. func (s *Secret) getFields() map[string][]byte {
  463. secretData := make(map[string][]byte)
  464. for _, field := range s.Fields {
  465. if len(field.Value) > 0 {
  466. fieldKey := field.Label
  467. if fieldKey == "" {
  468. fieldKey = field.Type
  469. }
  470. secretData[fieldKey] = getFieldValue(field.Value)
  471. }
  472. }
  473. return secretData
  474. }
  475. func (s *Secret) getCustomField(key string) ([]byte, error) {
  476. for _, field := range s.Custom {
  477. if field.Label == key && len(field.Value) > 0 {
  478. return getFieldValue(field.Value), nil
  479. }
  480. }
  481. return nil, fmt.Errorf(errKeeperSecurityInvalidField, key)
  482. }
  483. func (s *Secret) getCustomFields() map[string][]byte {
  484. secretData := make(map[string][]byte)
  485. for _, field := range s.Custom {
  486. if len(field.Value) > 0 {
  487. secretData[field.Label] = getFieldValue(field.Value)
  488. }
  489. }
  490. return secretData
  491. }
  492. func (s *Secret) getFile(key string) ([]byte, error) {
  493. for _, file := range s.Files {
  494. if file.Title == key {
  495. return []byte(file.Content), nil
  496. }
  497. }
  498. return nil, fmt.Errorf(errKeeperSecurityInvalidField, key)
  499. }
  500. func (s *Secret) getProperty(key string) ([]byte, error) {
  501. field, _ := s.getField(key)
  502. if field != nil {
  503. return field, nil
  504. }
  505. customField, _ := s.getCustomField(key)
  506. if customField != nil {
  507. return customField, nil
  508. }
  509. file, _ := s.getFile(key)
  510. if file != nil {
  511. return file, nil
  512. }
  513. return nil, fmt.Errorf(errKeeperSecurityInvalidProperty, s.Title, key)
  514. }
  515. func (s *Secret) getFiles() map[string][]byte {
  516. secretData := make(map[string][]byte)
  517. for _, file := range s.Files {
  518. secretData[file.Title] = []byte(file.Content)
  519. }
  520. return secretData
  521. }
  522. func (s *Secret) toString() (string, error) {
  523. secretJSON, err := json.Marshal(s)
  524. if err != nil {
  525. return "", fmt.Errorf(errInvalidJSONSecret, s.Title, err)
  526. }
  527. return string(secretJSON), nil
  528. }