provider.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implieclient.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package beyondtrust
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "net/url"
  18. "strings"
  19. "time"
  20. auth "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
  21. "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
  22. managed_account "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account"
  23. "github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets"
  24. "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
  25. "github.com/cenkalti/backoff/v4"
  26. v1 "k8s.io/api/core/v1"
  27. ctrl "sigs.k8s.io/controller-runtime"
  28. "sigs.k8s.io/controller-runtime/pkg/client"
  29. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  30. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  31. esoClient "github.com/external-secrets/external-secrets/pkg/utils"
  32. )
  33. const (
  34. errNilStore = "nil store found"
  35. errMissingStoreSpec = "store is missing spec"
  36. errMissingProvider = "storeSpec is missing provider"
  37. errInvalidProvider = "invalid provider spec. Missing field in store %s"
  38. errInvalidHostURL = "invalid host URL"
  39. errNoSuchKeyFmt = "no such key in secret: %q"
  40. errInvalidRetrievalPath = "invalid retrieval path. Provide one path, separator and name"
  41. errNotImplemented = "not implemented"
  42. )
  43. var (
  44. errSecretRefAndValueConflict = errors.New("cannot specify both secret reference and value")
  45. errMissingSecretName = errors.New("must specify a secret name")
  46. errMissingSecretKey = errors.New("must specify a secret key")
  47. ESOLogger = ctrl.Log.WithName("provider").WithName("beyondtrust")
  48. maxFileSecretSizeBytes = 5000000
  49. )
  50. // Provider is a Password Safe secrets provider implementing NewClient and ValidateStore for the esv1beta1.Provider interface.
  51. type Provider struct {
  52. apiURL string
  53. retrievaltype string
  54. authenticate auth.AuthenticationObj
  55. log logging.LogrLogger
  56. separator string
  57. }
  58. type AuthenticatorInput struct {
  59. Config *esv1beta1.BeyondtrustProvider
  60. HTTPClientObj utils.HttpClientObj
  61. BackoffDefinition *backoff.ExponentialBackOff
  62. APIURL string
  63. ClientID string
  64. ClientSecret string
  65. APIKey string
  66. Logger *logging.LogrLogger
  67. RetryMaxElapsedTimeMinutes int
  68. }
  69. // Capabilities implements v1beta1.Provider.
  70. func (*Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
  71. return esv1beta1.SecretStoreReadOnly
  72. }
  73. // Close implements v1beta1.SecretsClient.
  74. func (*Provider) Close(_ context.Context) error {
  75. return nil
  76. }
  77. // DeleteSecret implements v1beta1.SecretsClient.
  78. func (*Provider) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
  79. return errors.New(errNotImplemented)
  80. }
  81. // GetSecretMap implements v1beta1.SecretsClient.
  82. func (*Provider) GetSecretMap(_ context.Context, _ esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  83. return make(map[string][]byte), errors.New(errNotImplemented)
  84. }
  85. // PushSecret implements v1beta1.SecretsClient.
  86. func (*Provider) PushSecret(_ context.Context, _ *v1.Secret, _ esv1beta1.PushSecretData) error {
  87. return errors.New(errNotImplemented)
  88. }
  89. // Validate implements v1beta1.SecretsClient.
  90. func (p *Provider) Validate() (esv1beta1.ValidationResult, error) {
  91. timeout := 15 * time.Second
  92. clientURL := p.apiURL
  93. if err := esoClient.NetworkValidate(clientURL, timeout); err != nil {
  94. ESOLogger.Error(err, "Network Validate", "clientURL:", clientURL)
  95. return esv1beta1.ValidationResultError, err
  96. }
  97. return esv1beta1.ValidationResultReady, nil
  98. }
  99. func (*Provider) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
  100. return false, errors.New(errNotImplemented)
  101. }
  102. // NewClient this is where we initialize the SecretClient and return it for the controller to use.
  103. func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
  104. config := store.GetSpec().Provider.Beyondtrust
  105. logger := logging.NewLogrLogger(&ESOLogger)
  106. clientID, clientSecret, apiKey, err := loadCredentialsFromConfig(ctx, config, kube, namespace)
  107. if err != nil {
  108. return nil, fmt.Errorf("error loading credentials: %w", err)
  109. }
  110. certificate, certificateKey, err := loadCertificateFromConfig(ctx, config, kube, namespace)
  111. if err != nil {
  112. return nil, fmt.Errorf("error loading certificate: %w", err)
  113. }
  114. if err != nil {
  115. return nil, fmt.Errorf("error loading secrets: %w", err)
  116. }
  117. clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes := getConfigValues(config)
  118. backoffDefinition := getBackoffDefinition(retryMaxElapsedTimeMinutes)
  119. params := utils.ValidationParams{
  120. ApiKey: apiKey,
  121. ClientID: clientID,
  122. ClientSecret: clientSecret,
  123. ApiUrl: &config.Server.APIURL,
  124. ClientTimeOutInSeconds: clientTimeOutInSeconds,
  125. Separator: &separator,
  126. VerifyCa: config.Server.VerifyCA,
  127. Logger: logger,
  128. Certificate: certificate,
  129. CertificateKey: certificateKey,
  130. RetryMaxElapsedTimeMinutes: &retryMaxElapsedTimeMinutes,
  131. MaxFileSecretSizeBytes: &maxFileSecretSizeBytes,
  132. }
  133. if err := validateInputs(params); err != nil {
  134. return nil, fmt.Errorf("error in Inputs: %w", err)
  135. }
  136. httpClient, err := utils.GetHttpClient(clientTimeOutInSeconds, config.Server.VerifyCA, certificate, certificateKey, logger)
  137. if err != nil {
  138. return nil, fmt.Errorf("error creating HTTP client: %w", err)
  139. }
  140. authenticatorInput := AuthenticatorInput{
  141. Config: config,
  142. HTTPClientObj: *httpClient,
  143. BackoffDefinition: backoffDefinition,
  144. APIURL: config.Server.APIURL,
  145. ClientID: clientID,
  146. ClientSecret: clientSecret,
  147. APIKey: apiKey,
  148. Logger: logger,
  149. RetryMaxElapsedTimeMinutes: retryMaxElapsedTimeMinutes,
  150. }
  151. authenticate, err := getAuthenticator(authenticatorInput)
  152. if err != nil {
  153. return nil, fmt.Errorf("error authenticating: %w", err)
  154. }
  155. return &Provider{
  156. apiURL: config.Server.APIURL,
  157. retrievaltype: config.Server.RetrievalType,
  158. authenticate: *authenticate,
  159. log: *logger,
  160. separator: separator,
  161. }, nil
  162. }
  163. func loadCredentialsFromConfig(ctx context.Context, config *esv1beta1.BeyondtrustProvider, kube client.Client, namespace string) (string, string, string, error) {
  164. var clientID, clientSecret, apiKey string
  165. var err error
  166. if config.Auth.APIKey != nil {
  167. apiKey, err = loadConfigSecret(ctx, config.Auth.APIKey, kube, namespace)
  168. if err != nil {
  169. return "", "", "", fmt.Errorf("error loading apiKey: %w", err)
  170. }
  171. } else {
  172. clientID, err = loadConfigSecret(ctx, config.Auth.ClientID, kube, namespace)
  173. if err != nil {
  174. return "", "", "", fmt.Errorf("error loading clientID: %w", err)
  175. }
  176. clientSecret, err = loadConfigSecret(ctx, config.Auth.ClientSecret, kube, namespace)
  177. if err != nil {
  178. return "", "", "", fmt.Errorf("error loading clientSecret: %w", err)
  179. }
  180. }
  181. return clientID, clientSecret, apiKey, nil
  182. }
  183. func loadCertificateFromConfig(ctx context.Context, config *esv1beta1.BeyondtrustProvider, kube client.Client, namespace string) (string, string, error) {
  184. var certificate, certificateKey string
  185. var err error
  186. if config.Auth.Certificate != nil && config.Auth.CertificateKey != nil {
  187. certificate, err = loadConfigSecret(ctx, config.Auth.Certificate, kube, namespace)
  188. if err != nil {
  189. return "", "", fmt.Errorf("error loading Certificate: %w", err)
  190. }
  191. certificateKey, err = loadConfigSecret(ctx, config.Auth.CertificateKey, kube, namespace)
  192. if err != nil {
  193. return "", "", fmt.Errorf("error loading Certificate Key: %w", err)
  194. }
  195. }
  196. return certificate, certificateKey, nil
  197. }
  198. func getConfigValues(config *esv1beta1.BeyondtrustProvider) (int, string, int) {
  199. clientTimeOutInSeconds := 45
  200. separator := "/"
  201. retryMaxElapsedTimeMinutes := 15
  202. if config.Server.ClientTimeOutSeconds != 0 {
  203. clientTimeOutInSeconds = config.Server.ClientTimeOutSeconds
  204. }
  205. if config.Server.Separator != "" {
  206. separator = config.Server.Separator
  207. }
  208. return clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes
  209. }
  210. func getBackoffDefinition(retryMaxElapsedTimeMinutes int) *backoff.ExponentialBackOff {
  211. backoffDefinition := backoff.NewExponentialBackOff()
  212. backoffDefinition.InitialInterval = 1 * time.Second
  213. backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Minute
  214. backoffDefinition.RandomizationFactor = 0.5
  215. return backoffDefinition
  216. }
  217. func validateInputs(params utils.ValidationParams) error {
  218. return utils.ValidateInputs(params)
  219. }
  220. func getAuthenticator(input AuthenticatorInput) (*auth.AuthenticationObj, error) {
  221. if input.Config.Auth.APIKey != nil {
  222. return auth.AuthenticateUsingApiKey(input.HTTPClientObj, input.BackoffDefinition, input.APIURL, input.Logger, input.RetryMaxElapsedTimeMinutes, input.APIKey)
  223. }
  224. return auth.Authenticate(input.HTTPClientObj, input.BackoffDefinition, input.APIURL, input.ClientID, input.ClientSecret, input.Logger, input.RetryMaxElapsedTimeMinutes)
  225. }
  226. func loadConfigSecret(ctx context.Context, ref *esv1beta1.BeyondTrustProviderSecretRef, kube client.Client, defaultNamespace string) (string, error) {
  227. if ref.SecretRef == nil {
  228. return ref.Value, nil
  229. }
  230. if err := validateSecretRef(ref); err != nil {
  231. return "", err
  232. }
  233. namespace := defaultNamespace
  234. if ref.SecretRef.Namespace != nil {
  235. namespace = *ref.SecretRef.Namespace
  236. }
  237. ESOLogger.Info("using k8s secret", "name:", ref.SecretRef.Name, "namespace:", namespace)
  238. objKey := client.ObjectKey{Namespace: namespace, Name: ref.SecretRef.Name}
  239. secret := v1.Secret{}
  240. err := kube.Get(ctx, objKey, &secret)
  241. if err != nil {
  242. return "", err
  243. }
  244. value, ok := secret.Data[ref.SecretRef.Key]
  245. if !ok {
  246. return "", fmt.Errorf(errNoSuchKeyFmt, ref.SecretRef.Key)
  247. }
  248. return string(value), nil
  249. }
  250. func validateSecretRef(ref *esv1beta1.BeyondTrustProviderSecretRef) error {
  251. if ref.SecretRef != nil {
  252. if ref.Value != "" {
  253. return errSecretRefAndValueConflict
  254. }
  255. if ref.SecretRef.Name == "" {
  256. return errMissingSecretName
  257. }
  258. if ref.SecretRef.Key == "" {
  259. return errMissingSecretKey
  260. }
  261. }
  262. return nil
  263. }
  264. func (p *Provider) GetAllSecrets(_ context.Context, _ esv1beta1.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 esv1beta1.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, _ := managed_account.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 esv1beta1.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. esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
  337. Beyondtrust: &esv1beta1.BeyondtrustProvider{},
  338. })
  339. }