provider.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  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 beyondtrust provides a Password Safe secrets provider for External Secrets Operator.
  14. package beyondtrust
  15. import (
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "fmt"
  20. "net/url"
  21. "strings"
  22. "time"
  23. auth "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
  24. "github.com/BeyondTrust/go-client-library-passwordsafe/api/entities"
  25. "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
  26. managedaccount "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account"
  27. "github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets"
  28. "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
  29. "github.com/cenkalti/backoff/v4"
  30. v1 "k8s.io/api/core/v1"
  31. ctrl "sigs.k8s.io/controller-runtime"
  32. "sigs.k8s.io/controller-runtime/pkg/client"
  33. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  34. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  35. esutils "github.com/external-secrets/external-secrets/runtime/esutils"
  36. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  37. )
  38. const (
  39. errNilStore = "nil store found"
  40. errMissingStoreSpec = "store is missing spec"
  41. errMissingProvider = "storeSpec is missing provider"
  42. errInvalidProvider = "invalid provider spec. Missing field in store %s"
  43. errInvalidHostURL = "invalid host URL"
  44. errNoSuchKeyFmt = "no such key in secret: %q"
  45. errInvalidRetrievalPath = "invalid retrieval path. Provide one path, separator and name"
  46. errNotImplemented = "not implemented"
  47. usernameFieldName = "username"
  48. folderNameFieldName = "folder_name"
  49. fileNameFieldName = "file_name"
  50. titleFieldName = "title"
  51. descriptionFieldName = "description"
  52. ownerIDFieldName = "owner_id"
  53. groupIDFieldName = "group_id"
  54. ownerTypeFieldName = "owner_type"
  55. secretTypeFieldName = "secret_type"
  56. secretTypeCredential = "CREDENTIAL"
  57. secretTypeSecret = "SECRET"
  58. )
  59. var (
  60. errSecretRefAndValueConflict = errors.New("cannot specify both secret reference and value")
  61. errMissingSecretName = errors.New("must specify a secret name")
  62. errMissingSecretKey = errors.New("must specify a secret key")
  63. // ESOLogger is the logger instance for the Beyondtrust provider.
  64. ESOLogger = ctrl.Log.WithName("provider").WithName("beyondtrust")
  65. maxFileSecretSizeBytes = 5000000
  66. )
  67. // Provider is a Password Safe secrets provider implementing NewClient and ValidateStore for the esv1.Provider interface.
  68. type Provider struct {
  69. apiURL string
  70. retrievaltype string
  71. decrypt bool
  72. authenticate auth.AuthenticationObj
  73. log logging.LogrLogger
  74. separator string
  75. }
  76. // AuthenticatorInput is used to pass parameters to the getAuthenticator function.
  77. type AuthenticatorInput struct {
  78. Config *esv1.BeyondtrustProvider
  79. HTTPClientObj utils.HttpClientObj
  80. BackoffDefinition *backoff.ExponentialBackOff
  81. APIURL string
  82. APIVersion string
  83. ClientID string
  84. ClientSecret string
  85. APIKey string
  86. Logger *logging.LogrLogger
  87. RetryMaxElapsedTimeMinutes int
  88. }
  89. // Capabilities implements v1beta1.Provider.
  90. func (*Provider) Capabilities() esv1.SecretStoreCapabilities {
  91. return esv1.SecretStoreReadWrite
  92. }
  93. // Close implements v1beta1.SecretsClient.
  94. func (*Provider) Close(_ context.Context) error {
  95. return nil
  96. }
  97. // DeleteSecret implements v1beta1.SecretsClient.
  98. func (*Provider) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
  99. return errors.New(errNotImplemented)
  100. }
  101. // GetSecretMap implements v1beta1.SecretsClient.
  102. func (*Provider) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  103. return make(map[string][]byte), errors.New(errNotImplemented)
  104. }
  105. // Validate implements v1beta1.SecretsClient.
  106. func (p *Provider) Validate() (esv1.ValidationResult, error) {
  107. timeout := 15 * time.Second
  108. clientURL := p.apiURL
  109. if err := esutils.NetworkValidate(clientURL, timeout); err != nil {
  110. ESOLogger.Error(err, "Network Validate", "clientURL:", clientURL)
  111. return esv1.ValidationResultError, err
  112. }
  113. return esv1.ValidationResultReady, nil
  114. }
  115. // SecretExists checks if a secret exists in the provider.
  116. func (p *Provider) SecretExists(_ context.Context, pushSecretRef esv1.PushSecretRemoteRef) (bool, error) {
  117. logger := logging.NewLogrLogger(&ESOLogger)
  118. secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes, false)
  119. if err != nil {
  120. return false, err
  121. }
  122. _, err = secretObj.SearchSecretByTitleFlow(pushSecretRef.GetRemoteKey())
  123. if err == nil {
  124. return true, nil
  125. }
  126. return false, nil
  127. }
  128. // NewClient this is where we initialize the SecretClient and return it for the controller to use.
  129. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
  130. config := store.GetSpec().Provider.Beyondtrust
  131. logger := logging.NewLogrLogger(&ESOLogger)
  132. storeKind := store.GetKind()
  133. clientID, clientSecret, apiKey, err := loadCredentialsFromConfig(ctx, config, kube, namespace, storeKind)
  134. if err != nil {
  135. return nil, fmt.Errorf("error loading credentials: %w", err)
  136. }
  137. certificate, certificateKey, err := loadCertificateFromConfig(ctx, config, kube, namespace, storeKind)
  138. if err != nil {
  139. return nil, fmt.Errorf("error loading certificate: %w", err)
  140. }
  141. clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes := getConfigValues(config)
  142. backoffDefinition := getBackoffDefinition(retryMaxElapsedTimeMinutes)
  143. params := utils.ValidationParams{
  144. ApiKey: apiKey,
  145. ClientID: clientID,
  146. ClientSecret: clientSecret,
  147. ApiUrl: &config.Server.APIURL,
  148. ApiVersion: config.Server.APIVersion,
  149. ClientTimeOutInSeconds: clientTimeOutInSeconds,
  150. Separator: &separator,
  151. VerifyCa: config.Server.VerifyCA,
  152. Logger: logger,
  153. Certificate: certificate,
  154. CertificateKey: certificateKey,
  155. RetryMaxElapsedTimeMinutes: &retryMaxElapsedTimeMinutes,
  156. MaxFileSecretSizeBytes: &maxFileSecretSizeBytes,
  157. }
  158. if err := validateInputs(params); err != nil {
  159. return nil, fmt.Errorf("error in Inputs: %w", err)
  160. }
  161. httpClient, err := utils.GetHttpClient(clientTimeOutInSeconds, config.Server.VerifyCA, certificate, certificateKey, logger)
  162. if err != nil {
  163. return nil, fmt.Errorf("error creating HTTP client: %w", err)
  164. }
  165. authenticatorInput := AuthenticatorInput{
  166. Config: config,
  167. HTTPClientObj: *httpClient,
  168. BackoffDefinition: backoffDefinition,
  169. APIURL: config.Server.APIURL,
  170. APIVersion: config.Server.APIVersion,
  171. ClientID: clientID,
  172. ClientSecret: clientSecret,
  173. APIKey: apiKey,
  174. Logger: logger,
  175. RetryMaxElapsedTimeMinutes: retryMaxElapsedTimeMinutes,
  176. }
  177. authenticate, err := getAuthenticator(authenticatorInput)
  178. if err != nil {
  179. return nil, fmt.Errorf("error authenticating: %w", err)
  180. }
  181. return &Provider{
  182. apiURL: config.Server.APIURL,
  183. retrievaltype: config.Server.RetrievalType,
  184. authenticate: *authenticate,
  185. log: *logger,
  186. separator: separator,
  187. decrypt: config.Server.Decrypt,
  188. }, nil
  189. }
  190. func loadCredentialsFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, string, error) {
  191. if config.Auth.APIKey != nil {
  192. apiKey, err := loadConfigSecret(ctx, config.Auth.APIKey, kube, namespace, storeKind)
  193. return "", "", apiKey, err
  194. }
  195. clientID, err := loadConfigSecret(ctx, config.Auth.ClientID, kube, namespace, storeKind)
  196. if err != nil {
  197. return "", "", "", fmt.Errorf("error loading clientID: %w", err)
  198. }
  199. clientSecret, err := loadConfigSecret(ctx, config.Auth.ClientSecret, kube, namespace, storeKind)
  200. if err != nil {
  201. return "", "", "", fmt.Errorf("error loading clientSecret: %w", err)
  202. }
  203. return clientID, clientSecret, "", nil
  204. }
  205. func loadCertificateFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, error) {
  206. if config.Auth.Certificate == nil || config.Auth.CertificateKey == nil {
  207. return "", "", nil
  208. }
  209. certificate, err := loadConfigSecret(ctx, config.Auth.Certificate, kube, namespace, storeKind)
  210. if err != nil {
  211. return "", "", fmt.Errorf("error loading Certificate: %w", err)
  212. }
  213. certificateKey, err := loadConfigSecret(ctx, config.Auth.CertificateKey, kube, namespace, storeKind)
  214. if err != nil {
  215. return "", "", fmt.Errorf("error loading Certificate Key: %w", err)
  216. }
  217. return certificate, certificateKey, nil
  218. }
  219. func getConfigValues(config *esv1.BeyondtrustProvider) (int, string, int) {
  220. clientTimeOutInSeconds := 45
  221. separator := "/"
  222. retryMaxElapsedTimeMinutes := 15
  223. if config.Server.ClientTimeOutSeconds != 0 {
  224. clientTimeOutInSeconds = config.Server.ClientTimeOutSeconds
  225. }
  226. if config.Server.Separator != "" {
  227. separator = config.Server.Separator
  228. }
  229. return clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes
  230. }
  231. func getBackoffDefinition(retryMaxElapsedTimeMinutes int) *backoff.ExponentialBackOff {
  232. backoffDefinition := backoff.NewExponentialBackOff()
  233. backoffDefinition.InitialInterval = 1 * time.Second
  234. backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Minute
  235. backoffDefinition.RandomizationFactor = 0.5
  236. return backoffDefinition
  237. }
  238. func validateInputs(params utils.ValidationParams) error {
  239. return utils.ValidateInputs(params)
  240. }
  241. func getAuthenticator(input AuthenticatorInput) (*auth.AuthenticationObj, error) {
  242. parametersObj := auth.AuthenticationParametersObj{
  243. HTTPClient: input.HTTPClientObj,
  244. BackoffDefinition: input.BackoffDefinition,
  245. EndpointURL: input.APIURL,
  246. APIVersion: input.APIVersion,
  247. ApiKey: input.APIKey,
  248. Logger: input.Logger,
  249. RetryMaxElapsedTimeSeconds: input.RetryMaxElapsedTimeMinutes,
  250. }
  251. if input.Config.Auth.APIKey != nil {
  252. parametersObj.ApiKey = input.APIKey
  253. return auth.AuthenticateUsingApiKey(parametersObj)
  254. }
  255. parametersObj.ClientID = input.ClientID
  256. parametersObj.ClientSecret = input.ClientSecret
  257. return auth.Authenticate(parametersObj)
  258. }
  259. func loadConfigSecret(ctx context.Context, ref *esv1.BeyondTrustProviderSecretRef, kube client.Client, defaultNamespace, storeKind string) (string, error) {
  260. if ref.SecretRef == nil {
  261. return ref.Value, nil
  262. }
  263. if err := validateSecretRef(ref); err != nil {
  264. return "", err
  265. }
  266. return resolvers.SecretKeyRef(ctx, kube, storeKind, defaultNamespace, ref.SecretRef)
  267. }
  268. func validateSecretRef(ref *esv1.BeyondTrustProviderSecretRef) error {
  269. if ref.SecretRef != nil {
  270. if ref.Value != "" {
  271. return errSecretRefAndValueConflict
  272. }
  273. if ref.SecretRef.Name == "" {
  274. return errMissingSecretName
  275. }
  276. if ref.SecretRef.Key == "" {
  277. return errMissingSecretKey
  278. }
  279. }
  280. return nil
  281. }
  282. // GetAllSecrets retrieves all secrets from Beyondtrust.
  283. func (p *Provider) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  284. return nil, errors.New("GetAllSecrets not implemented")
  285. }
  286. // GetSecret reads the secret from the Password Safe server and returns it. The controller uses the value here to
  287. // create the Kubernetes secret.
  288. func (p *Provider) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  289. managedAccountType := !strings.EqualFold(p.retrievaltype, secretTypeSecret)
  290. retrievalPaths := utils.ValidatePaths([]string{ref.Key}, managedAccountType, p.separator, &p.log)
  291. if len(retrievalPaths) != 1 {
  292. return nil, errors.New(errInvalidRetrievalPath)
  293. }
  294. retrievalPath := retrievalPaths[0]
  295. _, err := p.authenticate.GetPasswordSafeAuthentication()
  296. if err != nil {
  297. return nil, fmt.Errorf("error getting authentication: %w", err)
  298. }
  299. managedFetch := func() (string, error) {
  300. ESOLogger.Info("retrieve managed account value", "retrievalPath:", retrievalPath)
  301. manageAccountObj, _ := managedaccount.NewManagedAccountObj(p.authenticate, &p.log)
  302. return manageAccountObj.GetSecret(retrievalPath, p.separator)
  303. }
  304. unmanagedFetch := func() (string, error) {
  305. ESOLogger.Info("retrieve secrets safe value", "retrievalPath:", retrievalPath)
  306. secretObj, _ := secrets.NewSecretObj(p.authenticate, &p.log, maxFileSecretSizeBytes, p.decrypt)
  307. return secretObj.GetSecret(retrievalPath, p.separator)
  308. }
  309. fetch := unmanagedFetch
  310. if managedAccountType {
  311. fetch = managedFetch
  312. }
  313. returnSecret, err := fetch()
  314. if err != nil {
  315. if serr := p.authenticate.SignOut(); serr != nil {
  316. return nil, errors.Join(err, serr)
  317. }
  318. return nil, fmt.Errorf("error getting secret/managed account: %w", err)
  319. }
  320. return []byte(returnSecret), nil
  321. }
  322. // ValidateStore validates the store configuration to prevent unexpected errors.
  323. func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  324. if store == nil {
  325. return nil, errors.New(errNilStore)
  326. }
  327. spec := store.GetSpec()
  328. if spec == nil {
  329. return nil, errors.New(errMissingStoreSpec)
  330. }
  331. if spec.Provider == nil {
  332. return nil, errors.New(errMissingProvider)
  333. }
  334. provider := spec.Provider.Beyondtrust
  335. if provider == nil {
  336. return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
  337. }
  338. apiURL, err := url.Parse(provider.Server.APIURL)
  339. if err != nil {
  340. return nil, errors.New(errInvalidHostURL)
  341. }
  342. if provider.Auth.ClientID.SecretRef != nil {
  343. return nil, err
  344. }
  345. if provider.Auth.ClientSecret.SecretRef != nil {
  346. return nil, err
  347. }
  348. if apiURL.Host == "" {
  349. return nil, errors.New(errInvalidHostURL)
  350. }
  351. return nil, nil
  352. }
  353. // NewProvider creates a new Provider instance.
  354. func NewProvider() esv1.Provider {
  355. return &Provider{}
  356. }
  357. // ProviderSpec returns the provider specification for registration.
  358. func ProviderSpec() *esv1.SecretStoreProvider {
  359. return &esv1.SecretStoreProvider{
  360. Beyondtrust: &esv1.BeyondtrustProvider{},
  361. }
  362. }
  363. // MaintenanceStatus returns the maintenance status of the provider.
  364. func MaintenanceStatus() esv1.MaintenanceStatus {
  365. return esv1.MaintenanceStatusMaintained
  366. }
  367. // PushSecret implements v1beta1.SecretsClient.
  368. func (p *Provider) PushSecret(_ context.Context, secret *v1.Secret, psd esv1.PushSecretData) error {
  369. ESOLogger.Info("Pushing secret to BeyondTrust Password Safe")
  370. value, err := esutils.ExtractSecretData(psd, secret)
  371. if err != nil {
  372. return fmt.Errorf("extract secret data failed: %w", err)
  373. }
  374. secretValue := string(value)
  375. metadata := psd.GetMetadata()
  376. data, err := json.Marshal(metadata)
  377. if err != nil {
  378. return fmt.Errorf("Error getting metadata: %w", err)
  379. }
  380. var metaDataObject map[string]any
  381. err = json.Unmarshal(data, &metaDataObject)
  382. if err != nil {
  383. return fmt.Errorf("Error in parameters: %w", err)
  384. }
  385. signAppinResponse, err := p.authenticate.GetPasswordSafeAuthentication()
  386. if err != nil {
  387. return fmt.Errorf("Error in authentication: %w", err)
  388. }
  389. err = p.CreateSecret(secretValue, metaDataObject, signAppinResponse)
  390. if err != nil {
  391. return fmt.Errorf("Error in creating the secret: %w", err)
  392. }
  393. return nil
  394. }
  395. // CreateSecret creates a secret in BeyondTrust Password Safe.
  396. func (p *Provider) CreateSecret(secret string, data map[string]any, signAppinResponse entities.SignAppinResponse) error {
  397. logger := logging.NewLogrLogger(&ESOLogger)
  398. secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes, false)
  399. if err != nil {
  400. return err
  401. }
  402. username := utils.GetStringField(data, usernameFieldName, "")
  403. folderName := utils.GetStringField(data, folderNameFieldName, "")
  404. fileName := utils.GetStringField(data, fileNameFieldName, "")
  405. title := utils.GetStringField(data, titleFieldName, "")
  406. description := utils.GetStringField(data, descriptionFieldName, "")
  407. ownerID := utils.GetIntField(data, ownerIDFieldName, 0)
  408. groupID := utils.GetIntField(data, groupIDFieldName, 0)
  409. ownerType := utils.GetStringField(data, ownerTypeFieldName, "")
  410. secretType := utils.GetStringField(data, secretTypeFieldName, secretTypeCredential)
  411. var notes string
  412. var urls []entities.UrlDetails
  413. var ownerDetailsOwnerID []entities.OwnerDetailsOwnerId
  414. var ownerDetailsGroupID []entities.OwnerDetailsGroupId
  415. _, ok := data["notes"]
  416. if ok {
  417. notes = data["notes"].(string)
  418. }
  419. _, ok = data["urls"]
  420. if ok {
  421. urls = utils.GetUrlsDetailsList(data)
  422. }
  423. ownerDetailsOwnerID = utils.GetOwnerDetailsOwnerIdList(data, signAppinResponse)
  424. ownerDetailsGroupID = utils.GetOwnerDetailsGroupIdList(data, groupID, signAppinResponse)
  425. secretDetailsConfig := entities.SecretDetailsBaseConfig{
  426. Title: title,
  427. Description: description,
  428. Urls: urls,
  429. Notes: notes,
  430. }
  431. // Build a version-neutral input per secret type; CreateSecretFlow
  432. // selects Config30/Config31 from p.authenticate.ApiVersion internally.
  433. var secretInput any
  434. switch strings.ToUpper(secretType) {
  435. case secretTypeCredential:
  436. secretInput = entities.SecretCredentialInput{
  437. SecretDetailsBaseConfig: secretDetailsConfig,
  438. Username: username,
  439. Password: secret,
  440. OwnerId: ownerID,
  441. OwnerType: ownerType,
  442. OwnersByOwnerId: ownerDetailsOwnerID,
  443. OwnersByGroupId: ownerDetailsGroupID,
  444. }
  445. case "FILE":
  446. secretInput = entities.SecretFileInput{
  447. SecretDetailsBaseConfig: secretDetailsConfig,
  448. FileName: fileName,
  449. FileContent: secret,
  450. OwnerId: ownerID,
  451. OwnerType: ownerType,
  452. OwnersByOwnerId: ownerDetailsOwnerID,
  453. OwnersByGroupId: ownerDetailsGroupID,
  454. }
  455. case "TEXT":
  456. secretInput = entities.SecretTextInput{
  457. SecretDetailsBaseConfig: secretDetailsConfig,
  458. Text: secret,
  459. OwnerId: ownerID,
  460. OwnerType: ownerType,
  461. OwnersByOwnerId: ownerDetailsOwnerID,
  462. OwnersByGroupId: ownerDetailsGroupID,
  463. }
  464. default:
  465. return fmt.Errorf("Unknown secret type")
  466. }
  467. _, err = secretObj.CreateSecretFlow(folderName, secretInput)
  468. if err != nil {
  469. return err
  470. }
  471. ESOLogger.Info("Secret pushed to BeyondTrust Password Safe")
  472. return nil
  473. }