provider.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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 beyondtrustworkloadcredentials
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "net/url"
  19. "regexp"
  20. "strings"
  21. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  22. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  23. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/httpclient"
  25. btwcutil "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/util"
  26. "github.com/external-secrets/external-secrets/runtime/esutils"
  27. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  28. )
  29. var (
  30. // ErrNoStore is returned when the BeyondtrustWorkloadCredentials SecretStore is missing or invalid.
  31. ErrNoStore = errors.New("missing or invalid BeyondtrustWorkloadCredentials SecretStore")
  32. // ErrNoAPIKey is returned when the API Token is missing or invalid.
  33. ErrNoAPIKey = errors.New("missing or invalid BeyondtrustWorkloadCredentials API Token in BeyondtrustWorkloadCredentials SecretStore")
  34. // ErrNoTokenName is returned when the API Token name is missing or invalid.
  35. ErrNoTokenName = errors.New("missing or invalid BeyondtrustWorkloadCredentials API Token name in BeyondtrustWorkloadCredentials SecretStore")
  36. // ErrNoTokenKey is returned when the API Token key is missing or invalid.
  37. ErrNoTokenKey = errors.New("missing or invalid BeyondtrustWorkloadCredentials API Token key in BeyondtrustWorkloadCredentials SecretStore")
  38. // ErrNoServer is returned when the BeyondtrustWorkloadCredentials Server is missing or invalid.
  39. ErrNoServer = errors.New("missing or invalid BeyondtrustWorkloadCredentials Server in BeyondtrustWorkloadCredentials SecretStore")
  40. // ErrNoAPIURL is returned when the Server API URL is missing or invalid.
  41. ErrNoAPIURL = errors.New("missing or invalid BeyondtrustWorkloadCredentials Server API URL in BeyondtrustWorkloadCredentials SecretStore")
  42. // ErrNoSiteID is returned when the Server site ID is missing or invalid.
  43. ErrNoSiteID = errors.New("missing or invalid BeyondtrustWorkloadCredentials Server site ID in BeyondtrustWorkloadCredentials SecretStore")
  44. )
  45. // Provider is a BeyondtrustWorkloadCredentials provider implementing NewClient and ValidateStore for the esv1.Provider interface.
  46. type Provider struct {
  47. // NewBeyondtrustWorkloadCredentialsClient is a function that returns a new BeyondTrust Secrets client.
  48. // This is used for testing to inject a fake client.
  49. NewBeyondtrustWorkloadCredentialsClient func(server, token string) (btwcutil.Client, error)
  50. }
  51. // https://github.com/external-secrets/external-secrets/issues/644
  52. var _ esv1.SecretsClient = &Client{}
  53. var _ esv1.Provider = &Provider{}
  54. // NewClient constructs a BeyondtrustWorkloadCredentials SecretsManager Provider.
  55. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  56. storeSpec := store.GetSpec()
  57. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.BeyondtrustWorkloadCredentials == nil {
  58. return nil, ErrNoStore
  59. }
  60. BeyondtrustWorkloadCredentialsStoreSpec := storeSpec.Provider.BeyondtrustWorkloadCredentials
  61. storeKind := store.GetKind()
  62. // fetch server values from spec
  63. serverURL, apiKey, err := fetchServerValuesFromSpec(ctx, BeyondtrustWorkloadCredentialsStoreSpec, kube, namespace, storeKind)
  64. if err != nil {
  65. return nil, err
  66. }
  67. // create BeyondtrustWorkloadCredentials client
  68. BeyondtrustWorkloadCredentialsClient, err := p.newClient(ctx, serverURL, apiKey, BeyondtrustWorkloadCredentialsStoreSpec, kube, namespace, storeKind)
  69. if err != nil {
  70. return nil, fmt.Errorf("failed to create BeyondtrustWorkloadCredentials client: %w", err)
  71. }
  72. client := &Client{
  73. beyondtrustWorkloadCredentialsClient: BeyondtrustWorkloadCredentialsClient,
  74. store: BeyondtrustWorkloadCredentialsStoreSpec,
  75. }
  76. return client, nil
  77. }
  78. // newClient is a shared helper creates the appropriate BeyondtrustWorkloadCredentials client based on the provided spec.
  79. func (p *Provider) newClient(
  80. ctx context.Context,
  81. serverURL, apiKey string,
  82. btSpec *esv1.BeyondtrustWorkloadCredentialsProvider,
  83. kube kclient.Client,
  84. namespace, storeKind string,
  85. ) (btwcutil.Client, error) {
  86. // Fetch CA from CABundle/CAProvider using ESO helper
  87. var caCert []byte
  88. var err error
  89. if btSpec != nil {
  90. caCert, err = esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
  91. StoreKind: storeKind,
  92. Client: kube,
  93. Namespace: namespace,
  94. CABundle: btSpec.CABundle,
  95. CAProvider: btSpec.CAProvider,
  96. })
  97. if err != nil {
  98. return nil, fmt.Errorf("failed to fetch CA certificate: %w", err)
  99. }
  100. }
  101. if len(caCert) > 0 {
  102. return httpclient.NewBeyondtrustWorkloadCredentialsClientWithCustomCA(serverURL, apiKey, caCert)
  103. }
  104. if p.NewBeyondtrustWorkloadCredentialsClient != nil {
  105. return p.NewBeyondtrustWorkloadCredentialsClient(serverURL, apiKey)
  106. }
  107. return httpclient.NewBeyondtrustWorkloadCredentialsClient(serverURL, apiKey)
  108. }
  109. // NewGeneratorClient creates a new BeyondtrustWorkloadCredentials client for the generator controller.
  110. func (p *Provider) NewGeneratorClient(ctx context.Context, kube kclient.Client, btSpec *esv1.BeyondtrustWorkloadCredentialsProvider, namespace string) (btwcutil.Client, error) {
  111. if btSpec == nil {
  112. return nil, ErrNoStore
  113. }
  114. serverURL, apiKey, err := fetchServerValuesFromSpec(ctx, btSpec, kube, namespace, "")
  115. if err != nil {
  116. return nil, err
  117. }
  118. client, err := p.newClient(ctx, serverURL, apiKey, btSpec, kube, namespace, "")
  119. if err != nil {
  120. return nil, fmt.Errorf("failed to create BeyondtrustWorkloadCredentials client: %w", err)
  121. }
  122. return client, nil
  123. }
  124. // ValidateStore checks if the BeyondtrustWorkloadCredentials store is valid.
  125. // The provider may return a warning and an error.
  126. // The intended use of the warning to indicate a deprecation of behavior
  127. // or other type of message that is NOT a validation failure but should be noticed by the user.
  128. func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  129. storeSpec := store.GetSpec()
  130. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.BeyondtrustWorkloadCredentials == nil {
  131. return nil, ErrNoStore
  132. }
  133. BeyondtrustWorkloadCredentialsStoreSpec := storeSpec.Provider.BeyondtrustWorkloadCredentials
  134. // validate token selector
  135. if BeyondtrustWorkloadCredentialsStoreSpec.Auth == nil {
  136. return nil, ErrNoAPIKey
  137. }
  138. tokenRef := BeyondtrustWorkloadCredentialsStoreSpec.Auth.APIKey.Token
  139. if err := esutils.ValidateSecretSelector(store, tokenRef); err != nil {
  140. return nil, err
  141. }
  142. if tokenRef.Name == "" {
  143. return nil, ErrNoTokenName
  144. }
  145. // validate server config is present and contains required fields
  146. if BeyondtrustWorkloadCredentialsStoreSpec.Server == nil {
  147. return nil, ErrNoServer
  148. }
  149. if BeyondtrustWorkloadCredentialsStoreSpec.Server.APIURL == "" {
  150. return nil, ErrNoAPIURL
  151. }
  152. // Validate APIURL format
  153. if err := validateAPIURL(BeyondtrustWorkloadCredentialsStoreSpec.Server.APIURL); err != nil {
  154. return nil, fmt.Errorf("invalid apiUrl: %w", err)
  155. }
  156. if BeyondtrustWorkloadCredentialsStoreSpec.Server.SiteID == "" {
  157. return nil, ErrNoSiteID
  158. }
  159. // Validate SiteID format (should be UUID)
  160. if err := validateSiteID(BeyondtrustWorkloadCredentialsStoreSpec.Server.SiteID); err != nil {
  161. return nil, fmt.Errorf("invalid siteId: %w", err)
  162. }
  163. return nil, nil
  164. }
  165. // Capabilities returns the BeyondtrustWorkloadCredentials provider Capabilities (Read, Write, ReadWrite).
  166. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  167. return esv1.SecretStoreReadOnly
  168. }
  169. func loadAPIKeyFromSpec(ctx context.Context, spec *esv1.BeyondtrustWorkloadCredentialsProvider, kube kclient.Client, namespace, storeKind string) (string, error) {
  170. if spec == nil {
  171. return "", ErrNoStore
  172. }
  173. if spec.Auth == nil {
  174. return "", ErrNoAPIKey
  175. }
  176. tokenRef := spec.Auth.APIKey.Token
  177. if tokenRef.Name == "" {
  178. return "", ErrNoTokenName
  179. }
  180. if tokenRef.Key == "" {
  181. return "", ErrNoTokenKey
  182. }
  183. return resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &tokenRef)
  184. }
  185. func loadURLFromSpec(spec *esv1.BeyondtrustWorkloadCredentialsProvider) (string, string, error) {
  186. if spec == nil {
  187. return "", "", ErrNoStore
  188. }
  189. if spec.Server == nil {
  190. return "", "", ErrNoServer
  191. }
  192. if spec.Server.APIURL == "" {
  193. return "", "", ErrNoAPIURL
  194. }
  195. // Validate APIURL format
  196. if err := validateAPIURL(spec.Server.APIURL); err != nil {
  197. return "", "", fmt.Errorf("invalid apiUrl: %w", err)
  198. }
  199. if spec.Server.SiteID == "" {
  200. return "", "", ErrNoSiteID
  201. }
  202. // Validate SiteID format
  203. if err := validateSiteID(spec.Server.SiteID); err != nil {
  204. return "", "", fmt.Errorf("invalid siteId: %w", err)
  205. }
  206. return spec.Server.APIURL, spec.Server.SiteID, nil
  207. }
  208. func fetchServerValuesFromSpec(ctx context.Context, spec *esv1.BeyondtrustWorkloadCredentialsProvider, kube kclient.Client, namespace, storeKind string) (string, string, error) {
  209. if spec == nil {
  210. return "", "", ErrNoStore
  211. }
  212. apiKey, err := loadAPIKeyFromSpec(ctx, spec, kube, namespace, storeKind)
  213. if err != nil {
  214. return "", "", fmt.Errorf("failed to load credentials: %w", err)
  215. }
  216. baseURL, siteID, err := loadURLFromSpec(spec)
  217. if err != nil {
  218. return "", "", fmt.Errorf("failed to load server URL configuration: %w", err)
  219. }
  220. // Normalize baseURL by removing trailing slash to prevent double slashes
  221. baseURL = strings.TrimRight(baseURL, "/")
  222. serverURL := fmt.Sprintf("%s/%s/secrets", baseURL, siteID)
  223. return serverURL, apiKey, nil
  224. }
  225. // NewProvider creates a new Provider instance.
  226. func NewProvider() esv1.Provider {
  227. return &Provider{
  228. NewBeyondtrustWorkloadCredentialsClient: httpclient.NewBeyondtrustWorkloadCredentialsClient,
  229. }
  230. }
  231. // ProviderSpec returns the provider specification for registration.
  232. func ProviderSpec() *esv1.SecretStoreProvider {
  233. return &esv1.SecretStoreProvider{
  234. BeyondtrustWorkloadCredentials: &esv1.BeyondtrustWorkloadCredentialsProvider{},
  235. }
  236. }
  237. // MaintenanceStatus returns the maintenance status of the provider.
  238. func MaintenanceStatus() esv1.MaintenanceStatus {
  239. return esv1.MaintenanceStatusMaintained
  240. }
  241. // validateAPIURL validates the BeyondTrust API URL format.
  242. func validateAPIURL(apiURL string) error {
  243. if apiURL == "" {
  244. return fmt.Errorf("apiUrl cannot be empty")
  245. }
  246. parsedURL, err := url.Parse(apiURL)
  247. if err != nil {
  248. return fmt.Errorf("failed to parse apiUrl: %w", err)
  249. }
  250. if parsedURL.Scheme == "" {
  251. return fmt.Errorf("apiUrl must include a scheme (https)")
  252. }
  253. if parsedURL.Scheme != "https" {
  254. return fmt.Errorf("apiUrl must use https scheme, got %q", parsedURL.Scheme)
  255. }
  256. if parsedURL.Host == "" {
  257. return fmt.Errorf("apiUrl must include a host")
  258. }
  259. return nil
  260. }
  261. // validateSiteID validates the BeyondTrust site ID format (must be a valid UUID).
  262. func validateSiteID(siteID string) error {
  263. if siteID == "" {
  264. return fmt.Errorf("siteId cannot be empty")
  265. }
  266. // Validate UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
  267. // Where y is one of [8, 9, a, b] (RFC 4122 variant bits)
  268. uuidPattern := `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$`
  269. matched, err := regexp.MatchString(uuidPattern, siteID)
  270. if err != nil {
  271. return fmt.Errorf("failed to validate siteId format: %w", err)
  272. }
  273. if !matched {
  274. return fmt.Errorf("siteId must be a valid UUID format, got %q", siteID)
  275. }
  276. return nil
  277. }