provider.go 14 KB

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