provider.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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. "errors"
  18. "fmt"
  19. "net/url"
  20. "strings"
  21. "time"
  22. auth "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
  23. "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
  24. managedaccount "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account"
  25. "github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets"
  26. "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
  27. "github.com/cenkalti/backoff/v4"
  28. v1 "k8s.io/api/core/v1"
  29. ctrl "sigs.k8s.io/controller-runtime"
  30. "sigs.k8s.io/controller-runtime/pkg/client"
  31. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  32. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  33. esutils "github.com/external-secrets/external-secrets/pkg/esutils"
  34. "github.com/external-secrets/external-secrets/pkg/esutils/resolvers"
  35. )
  36. const (
  37. errNilStore = "nil store found"
  38. errMissingStoreSpec = "store is missing spec"
  39. errMissingProvider = "storeSpec is missing provider"
  40. errInvalidProvider = "invalid provider spec. Missing field in store %s"
  41. errInvalidHostURL = "invalid host URL"
  42. errNoSuchKeyFmt = "no such key in secret: %q"
  43. errInvalidRetrievalPath = "invalid retrieval path. Provide one path, separator and name"
  44. errNotImplemented = "not implemented"
  45. )
  46. var (
  47. errSecretRefAndValueConflict = errors.New("cannot specify both secret reference and value")
  48. errMissingSecretName = errors.New("must specify a secret name")
  49. errMissingSecretKey = errors.New("must specify a secret key")
  50. // ESOLogger is the logger instance for the Beyondtrust provider.
  51. ESOLogger = ctrl.Log.WithName("provider").WithName("beyondtrust")
  52. maxFileSecretSizeBytes = 5000000
  53. )
  54. // Provider is a Password Safe secrets provider implementing NewClient and ValidateStore for the esv1.Provider interface.
  55. type Provider struct {
  56. apiURL string
  57. retrievaltype string
  58. authenticate auth.AuthenticationObj
  59. log logging.LogrLogger
  60. separator string
  61. }
  62. // AuthenticatorInput is used to pass parameters to the getAuthenticator function.
  63. type AuthenticatorInput struct {
  64. Config *esv1.BeyondtrustProvider
  65. HTTPClientObj utils.HttpClientObj
  66. BackoffDefinition *backoff.ExponentialBackOff
  67. APIURL string
  68. APIVersion string
  69. ClientID string
  70. ClientSecret string
  71. APIKey string
  72. Logger *logging.LogrLogger
  73. RetryMaxElapsedTimeMinutes int
  74. }
  75. // Capabilities implements v1beta1.Provider.
  76. func (*Provider) Capabilities() esv1.SecretStoreCapabilities {
  77. return esv1.SecretStoreReadOnly
  78. }
  79. // Close implements v1beta1.SecretsClient.
  80. func (*Provider) Close(_ context.Context) error {
  81. return nil
  82. }
  83. // DeleteSecret implements v1beta1.SecretsClient.
  84. func (*Provider) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
  85. return errors.New(errNotImplemented)
  86. }
  87. // GetSecretMap implements v1beta1.SecretsClient.
  88. func (*Provider) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  89. return make(map[string][]byte), errors.New(errNotImplemented)
  90. }
  91. // PushSecret implements v1beta1.SecretsClient.
  92. func (*Provider) PushSecret(_ context.Context, _ *v1.Secret, _ esv1.PushSecretData) error {
  93. return errors.New(errNotImplemented)
  94. }
  95. // Validate implements v1beta1.SecretsClient.
  96. func (p *Provider) Validate() (esv1.ValidationResult, error) {
  97. timeout := 15 * time.Second
  98. clientURL := p.apiURL
  99. if err := esutils.NetworkValidate(clientURL, timeout); err != nil {
  100. ESOLogger.Error(err, "Network Validate", "clientURL:", clientURL)
  101. return esv1.ValidationResultError, err
  102. }
  103. return esv1.ValidationResultReady, nil
  104. }
  105. // SecretExists checks if a secret exists in the provider.
  106. // Currently not implemented for this provider.
  107. func (*Provider) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  108. return false, errors.New(errNotImplemented)
  109. }
  110. // NewClient this is where we initialize the SecretClient and return it for the controller to use.
  111. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
  112. config := store.GetSpec().Provider.Beyondtrust
  113. logger := logging.NewLogrLogger(&ESOLogger)
  114. storeKind := store.GetKind()
  115. clientID, clientSecret, apiKey, err := loadCredentialsFromConfig(ctx, config, kube, namespace, storeKind)
  116. if err != nil {
  117. return nil, fmt.Errorf("error loading credentials: %w", err)
  118. }
  119. certificate, certificateKey, err := loadCertificateFromConfig(ctx, config, kube, namespace, storeKind)
  120. if err != nil {
  121. return nil, fmt.Errorf("error loading certificate: %w", err)
  122. }
  123. clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes := getConfigValues(config)
  124. backoffDefinition := getBackoffDefinition(retryMaxElapsedTimeMinutes)
  125. params := utils.ValidationParams{
  126. ApiKey: apiKey,
  127. ClientID: clientID,
  128. ClientSecret: clientSecret,
  129. ApiUrl: &config.Server.APIURL,
  130. ApiVersion: config.Server.APIVersion,
  131. ClientTimeOutInSeconds: clientTimeOutInSeconds,
  132. Separator: &separator,
  133. VerifyCa: config.Server.VerifyCA,
  134. Logger: logger,
  135. Certificate: certificate,
  136. CertificateKey: certificateKey,
  137. RetryMaxElapsedTimeMinutes: &retryMaxElapsedTimeMinutes,
  138. MaxFileSecretSizeBytes: &maxFileSecretSizeBytes,
  139. }
  140. if err := validateInputs(params); err != nil {
  141. return nil, fmt.Errorf("error in Inputs: %w", err)
  142. }
  143. httpClient, err := utils.GetHttpClient(clientTimeOutInSeconds, config.Server.VerifyCA, certificate, certificateKey, logger)
  144. if err != nil {
  145. return nil, fmt.Errorf("error creating HTTP client: %w", err)
  146. }
  147. authenticatorInput := AuthenticatorInput{
  148. Config: config,
  149. HTTPClientObj: *httpClient,
  150. BackoffDefinition: backoffDefinition,
  151. APIURL: config.Server.APIURL,
  152. APIVersion: config.Server.APIVersion,
  153. ClientID: clientID,
  154. ClientSecret: clientSecret,
  155. APIKey: apiKey,
  156. Logger: logger,
  157. RetryMaxElapsedTimeMinutes: retryMaxElapsedTimeMinutes,
  158. }
  159. authenticate, err := getAuthenticator(authenticatorInput)
  160. if err != nil {
  161. return nil, fmt.Errorf("error authenticating: %w", err)
  162. }
  163. return &Provider{
  164. apiURL: config.Server.APIURL,
  165. retrievaltype: config.Server.RetrievalType,
  166. authenticate: *authenticate,
  167. log: *logger,
  168. separator: separator,
  169. }, nil
  170. }
  171. func loadCredentialsFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, string, error) {
  172. if config.Auth.APIKey != nil {
  173. apiKey, err := loadConfigSecret(ctx, config.Auth.APIKey, kube, namespace, storeKind)
  174. return "", "", apiKey, err
  175. }
  176. clientID, err := loadConfigSecret(ctx, config.Auth.ClientID, kube, namespace, storeKind)
  177. if err != nil {
  178. return "", "", "", fmt.Errorf("error loading clientID: %w", err)
  179. }
  180. clientSecret, err := loadConfigSecret(ctx, config.Auth.ClientSecret, kube, namespace, storeKind)
  181. if err != nil {
  182. return "", "", "", fmt.Errorf("error loading clientSecret: %w", err)
  183. }
  184. return clientID, clientSecret, "", nil
  185. }
  186. func loadCertificateFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, error) {
  187. if config.Auth.Certificate == nil || config.Auth.CertificateKey == nil {
  188. return "", "", nil
  189. }
  190. certificate, err := loadConfigSecret(ctx, config.Auth.Certificate, kube, namespace, storeKind)
  191. if err != nil {
  192. return "", "", fmt.Errorf("error loading Certificate: %w", err)
  193. }
  194. certificateKey, err := loadConfigSecret(ctx, config.Auth.CertificateKey, kube, namespace, storeKind)
  195. if err != nil {
  196. return "", "", fmt.Errorf("error loading Certificate Key: %w", err)
  197. }
  198. return certificate, certificateKey, nil
  199. }
  200. func getConfigValues(config *esv1.BeyondtrustProvider) (int, string, int) {
  201. clientTimeOutInSeconds := 45
  202. separator := "/"
  203. retryMaxElapsedTimeMinutes := 15
  204. if config.Server.ClientTimeOutSeconds != 0 {
  205. clientTimeOutInSeconds = config.Server.ClientTimeOutSeconds
  206. }
  207. if config.Server.Separator != "" {
  208. separator = config.Server.Separator
  209. }
  210. return clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes
  211. }
  212. func getBackoffDefinition(retryMaxElapsedTimeMinutes int) *backoff.ExponentialBackOff {
  213. backoffDefinition := backoff.NewExponentialBackOff()
  214. backoffDefinition.InitialInterval = 1 * time.Second
  215. backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Minute
  216. backoffDefinition.RandomizationFactor = 0.5
  217. return backoffDefinition
  218. }
  219. func validateInputs(params utils.ValidationParams) error {
  220. return utils.ValidateInputs(params)
  221. }
  222. func getAuthenticator(input AuthenticatorInput) (*auth.AuthenticationObj, error) {
  223. parametersObj := auth.AuthenticationParametersObj{
  224. HTTPClient: input.HTTPClientObj,
  225. BackoffDefinition: input.BackoffDefinition,
  226. EndpointURL: input.APIURL,
  227. APIVersion: input.APIVersion,
  228. ApiKey: input.APIKey,
  229. Logger: input.Logger,
  230. RetryMaxElapsedTimeSeconds: input.RetryMaxElapsedTimeMinutes,
  231. }
  232. if input.Config.Auth.APIKey != nil {
  233. parametersObj.ApiKey = input.APIKey
  234. return auth.AuthenticateUsingApiKey(parametersObj)
  235. }
  236. parametersObj.ClientID = input.ClientID
  237. parametersObj.ClientSecret = input.ClientSecret
  238. return auth.Authenticate(parametersObj)
  239. }
  240. func loadConfigSecret(ctx context.Context, ref *esv1.BeyondTrustProviderSecretRef, kube client.Client, defaultNamespace, storeKind string) (string, error) {
  241. if ref.SecretRef == nil {
  242. return ref.Value, nil
  243. }
  244. if err := validateSecretRef(ref); err != nil {
  245. return "", err
  246. }
  247. return resolvers.SecretKeyRef(ctx, kube, storeKind, defaultNamespace, ref.SecretRef)
  248. }
  249. func validateSecretRef(ref *esv1.BeyondTrustProviderSecretRef) error {
  250. if ref.SecretRef != nil {
  251. if ref.Value != "" {
  252. return errSecretRefAndValueConflict
  253. }
  254. if ref.SecretRef.Name == "" {
  255. return errMissingSecretName
  256. }
  257. if ref.SecretRef.Key == "" {
  258. return errMissingSecretKey
  259. }
  260. }
  261. return nil
  262. }
  263. // GetAllSecrets retrieves all secrets from Beyondtrust.
  264. func (p *Provider) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  265. return nil, errors.New("GetAllSecrets not implemented")
  266. }
  267. // GetSecret reads the secret from the Password Safe server and returns it. The controller uses the value here to
  268. // create the Kubernetes secret.
  269. func (p *Provider) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  270. managedAccountType := !strings.EqualFold(p.retrievaltype, "SECRET")
  271. retrievalPaths := utils.ValidatePaths([]string{ref.Key}, managedAccountType, p.separator, &p.log)
  272. if len(retrievalPaths) != 1 {
  273. return nil, errors.New(errInvalidRetrievalPath)
  274. }
  275. retrievalPath := retrievalPaths[0]
  276. _, err := p.authenticate.GetPasswordSafeAuthentication()
  277. if err != nil {
  278. return nil, fmt.Errorf("error getting authentication: %w", err)
  279. }
  280. managedFetch := func() (string, error) {
  281. ESOLogger.Info("retrieve managed account value", "retrievalPath:", retrievalPath)
  282. manageAccountObj, _ := managedaccount.NewManagedAccountObj(p.authenticate, &p.log)
  283. return manageAccountObj.GetSecret(retrievalPath, p.separator)
  284. }
  285. unmanagedFetch := func() (string, error) {
  286. ESOLogger.Info("retrieve secrets safe value", "retrievalPath:", retrievalPath)
  287. secretObj, _ := secrets.NewSecretObj(p.authenticate, &p.log, maxFileSecretSizeBytes)
  288. return secretObj.GetSecret(retrievalPath, p.separator)
  289. }
  290. fetch := unmanagedFetch
  291. if managedAccountType {
  292. fetch = managedFetch
  293. }
  294. returnSecret, err := fetch()
  295. if err != nil {
  296. if serr := p.authenticate.SignOut(); serr != nil {
  297. return nil, errors.Join(err, serr)
  298. }
  299. return nil, fmt.Errorf("error getting secret/managed account: %w", err)
  300. }
  301. return []byte(returnSecret), nil
  302. }
  303. // ValidateStore validates the store configuration to prevent unexpected errors.
  304. func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  305. if store == nil {
  306. return nil, errors.New(errNilStore)
  307. }
  308. spec := store.GetSpec()
  309. if spec == nil {
  310. return nil, errors.New(errMissingStoreSpec)
  311. }
  312. if spec.Provider == nil {
  313. return nil, errors.New(errMissingProvider)
  314. }
  315. provider := spec.Provider.Beyondtrust
  316. if provider == nil {
  317. return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
  318. }
  319. apiURL, err := url.Parse(provider.Server.APIURL)
  320. if err != nil {
  321. return nil, errors.New(errInvalidHostURL)
  322. }
  323. if provider.Auth.ClientID.SecretRef != nil {
  324. return nil, err
  325. }
  326. if provider.Auth.ClientSecret.SecretRef != nil {
  327. return nil, err
  328. }
  329. if apiURL.Host == "" {
  330. return nil, errors.New(errInvalidHostURL)
  331. }
  332. return nil, nil
  333. }
  334. // registers the provider object to process on each reconciliation loop.
  335. func init() {
  336. esv1.Register(&Provider{}, &esv1.SecretStoreProvider{
  337. Beyondtrust: &esv1.BeyondtrustProvider{},
  338. }, esv1.MaintenanceStatusMaintained)
  339. }