provider.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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 crd
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "strings"
  19. authv1 "k8s.io/api/authorization/v1"
  20. corev1 "k8s.io/api/core/v1"
  21. "k8s.io/apimachinery/pkg/api/meta"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/runtime/schema"
  24. "k8s.io/client-go/kubernetes"
  25. "k8s.io/client-go/rest"
  26. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  27. "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
  28. ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
  29. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  30. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  31. "github.com/external-secrets/external-secrets/runtime/esutils"
  32. )
  33. var (
  34. errMissingStore = errors.New("missing store")
  35. errMissingCRDProvider = errors.New("missing CRD provider configuration")
  36. errMissingKind = errors.New("resource.kind is required")
  37. errMissingVersion = errors.New("resource.version is required")
  38. errKindIsSecret = errors.New("kind \"Secret\" is not allowed: use the Kubernetes provider to read Kubernetes Secrets")
  39. errEmptyWhitelistRule = errors.New("whitelist rule must define name, namespace, or properties")
  40. errNotImplemented = errors.New("not implemented")
  41. errClientNotReady = errors.New("crd: client has no active connection; a referent ClusterSecretStore is resolved per-ExternalSecret at reconcile")
  42. )
  43. // isCoreV1Secret reports whether the configured resource is the core
  44. // Kubernetes Secret (group "" or "core", version "v1", kind "Secret"). The
  45. // case-insensitive Kind match guards against the lowercase / mixed-case
  46. // variants the CRD discovery API will canonicalise. CRDs in custom groups
  47. // that happen to be named "Secret" are not affected.
  48. func isCoreV1Secret(res esv1.CRDProviderResource) bool {
  49. if !strings.EqualFold(res.Kind, "Secret") {
  50. return false
  51. }
  52. if res.Version != "v1" {
  53. return false
  54. }
  55. return res.Group == "" || res.Group == "core"
  56. }
  57. // Provider is the top-level CRD provider that implements esv1.Provider.
  58. type Provider struct {
  59. // buildClientFn builds a controller-runtime client from the authenticated
  60. // config and resolves the target resource's plural name and scope (namespaced
  61. // vs cluster-scoped) via a RESTMapper. Overridable in tests without a live
  62. // cluster.
  63. buildClientFn func(cfg *rest.Config, res esv1.CRDProviderResource) (kclient.Client, string, bool, error)
  64. // accessCheckFn verifies that the caller can perform the requested verbs
  65. // on the resolved resource (used for SSAR-based preflight + per-call list
  66. // checks).
  67. accessCheckFn func(ctx context.Context, cfg *rest.Config, res esv1.CRDProviderResource, plural, namespace string, verbs []string) error
  68. }
  69. var _ esv1.Provider = &Provider{}
  70. // newProvider returns a Provider with the default (real) client builder.
  71. func newProvider() *Provider {
  72. return &Provider{
  73. buildClientFn: buildClientFromCluster,
  74. accessCheckFn: ensureResourceAccess,
  75. }
  76. }
  77. // Capabilities returns ReadOnly - this provider never writes secrets.
  78. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  79. return esv1.SecretStoreReadOnly
  80. }
  81. // NewClient constructs a CRD client from the store configuration.
  82. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  83. ctrlCfg, err := ctrlcfg.GetConfig()
  84. if err != nil {
  85. return nil, fmt.Errorf("crd: failed to get kubeconfig: %w", err)
  86. }
  87. clientset, err := kubernetes.NewForConfig(ctrlCfg)
  88. if err != nil {
  89. return nil, fmt.Errorf("crd: failed to create kubernetes clientset: %w", err)
  90. }
  91. return p.newClient(ctx, store, kube, clientset, namespace)
  92. }
  93. // newClient builds the CRD provider client. Every store authenticates the same
  94. // way as the Kubernetes provider: via server + auth (serviceAccount, token, or
  95. // cert) or a kubeconfig authRef. In-cluster stores omit server (the URL defaults
  96. // to kubernetes.default) and set auth.serviceAccount.
  97. func (p *Provider) newClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, clientset kubernetes.Interface, namespace string) (esv1.SecretsClient, error) {
  98. provSpec, err := getProvider(store)
  99. if err != nil {
  100. return nil, err
  101. }
  102. storeKind := store.GetKind()
  103. // A referent ClusterSecretStore (auth without an explicit namespace) resolves
  104. // its ServiceAccount in the consuming ExternalSecret's namespace, unknown ("")
  105. // at store-validation time. Return a stub so validation passes; the operational
  106. // client is rebuilt per-ExternalSecret at reconcile, when the namespace is known.
  107. if storeKind == esv1.ClusterSecretStoreKind && namespace == "" && esutils.IsReferentKubernetesAuth(provSpec.Auth) {
  108. return &Client{store: provSpec, storeKind: storeKind, referent: true}, nil
  109. }
  110. cfg, err := esutils.BuildRESTConfigFromKubernetesConnection(
  111. ctx,
  112. kube,
  113. clientset.CoreV1(),
  114. storeKind,
  115. namespace,
  116. provSpec.Server,
  117. provSpec.Auth,
  118. provSpec.AuthRef,
  119. )
  120. if err != nil {
  121. return nil, fmt.Errorf("crd: failed to prepare api connection: %w", err)
  122. }
  123. return p.newClientWithRESTConfig(ctx, store, cfg, namespace)
  124. }
  125. // Client holds the runtime state for a single SecretStore/ClusterSecretStore.
  126. type Client struct {
  127. store *esv1.CRDProvider
  128. // kube is a controller-runtime client bound to the store's authenticated
  129. // connection. Reads target arbitrary CRs as unstructured objects; the client's
  130. // RESTMapper resolves GroupVersionKind to the correct resource and scope.
  131. kube kclient.Client
  132. namespace string
  133. // namespaced is true when the API resource is namespace-scoped (from the RESTMapper).
  134. namespaced bool
  135. // storeKind is SecretStore or ClusterSecretStore (controls remoteRef.key parsing).
  136. storeKind string
  137. // whitelistRules is the pre-compiled form of store.Whitelist.Rules, built once
  138. // at construction time so per-read calls do not recompile regexes.
  139. whitelistRules []compiledWhitelistRule
  140. // listAccessCheck performs a SelfSubjectAccessReview for "list" against
  141. // the same scope used for actual listing. Called by GetAllSecrets so the
  142. // "list" permission is only required when listing is actually used.
  143. // nil when no access check is configured (test/no-op).
  144. listAccessCheck func(ctx context.Context) error
  145. // referent marks a stub client returned at store-validation time for a
  146. // referent ClusterSecretStore (no explicit SA namespace). It has no
  147. // kube client and only answers Validate() with an "unknown" result; the
  148. // operational client is rebuilt per-ExternalSecret at reconcile.
  149. referent bool
  150. }
  151. var _ esv1.SecretsClient = &Client{}
  152. // newClientWithRESTConfig builds the Client from a fully authenticated REST config.
  153. // Exposed for tests that inject a token or explicit connection config without a live cluster.
  154. func (p *Provider) newClientWithRESTConfig(ctx context.Context, store esv1.GenericStore, authedCfg *rest.Config, targetNamespace string) (esv1.SecretsClient, error) {
  155. provSpec, err := getProvider(store)
  156. if err != nil {
  157. return nil, err
  158. }
  159. // Build the controller-runtime client and resolve the requested
  160. // group/version/kind to its plural resource name and scope via a RESTMapper.
  161. // A mapping error means the kind is not registered in the target cluster.
  162. kubeClient, plural, resourceNamespaced, err := p.buildClientFn(authedCfg, provSpec.Resource)
  163. if err != nil {
  164. return nil, err
  165. }
  166. // accessNS is the namespace passed to SelfSubjectAccessReview. For a
  167. // ClusterSecretStore listing a namespaced resource the controller operates
  168. // across all namespaces; falsely scoping the SSAR to the controller's own
  169. // namespace would let a SA with only-local access pass preflight and then
  170. // fail at request time. Use "" (cluster-wide).
  171. accessNS := targetNamespace
  172. if !resourceNamespaced {
  173. accessNS = ""
  174. } else if store.GetKind() == esv1.ClusterSecretStoreKind {
  175. accessNS = ""
  176. }
  177. if p.accessCheckFn != nil {
  178. // Preflight checks only "get". The "list" permission is checked lazily
  179. // in GetAllSecrets so a SA that only ever does GetSecret does not need
  180. // list rights at store bootstrap time.
  181. if err := p.accessCheckFn(ctx, authedCfg, provSpec.Resource, plural, accessNS, []string{"get"}); err != nil {
  182. return nil, err
  183. }
  184. }
  185. whitelistRules, err := compileWhitelistRules(provSpec.Whitelist)
  186. if err != nil {
  187. return nil, err
  188. }
  189. // Bind the list-permission preflight as a closure on the Client so
  190. // GetAllSecrets can invoke it without holding onto cfg/plural directly.
  191. var listAccessCheck func(ctx context.Context) error
  192. if p.accessCheckFn != nil {
  193. fn := p.accessCheckFn
  194. res := provSpec.Resource
  195. listAccessCheck = func(ctx context.Context) error {
  196. return fn(ctx, authedCfg, res, plural, accessNS, []string{"list"})
  197. }
  198. }
  199. return &Client{
  200. store: provSpec,
  201. kube: kubeClient,
  202. namespace: targetNamespace,
  203. namespaced: resourceNamespaced,
  204. storeKind: store.GetKind(),
  205. whitelistRules: whitelistRules,
  206. listAccessCheck: listAccessCheck,
  207. }, nil
  208. }
  209. // PushSecret is not supported by the CRD provider (read-only).
  210. func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1.PushSecretData) error {
  211. return fmt.Errorf("crd: PushSecret: %w", errNotImplemented)
  212. }
  213. // DeleteSecret is not supported by the CRD provider (read-only).
  214. func (c *Client) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
  215. return fmt.Errorf("crd: DeleteSecret: %w", errNotImplemented)
  216. }
  217. // buildClientFromCluster builds a controller-runtime client bound to the
  218. // authenticated connection and resolves the requested group/version/kind to its
  219. // plural resource name and scope via a dynamic RESTMapper. The RESTMapping also
  220. // serves as registration validation: an unregistered kind yields an error. This
  221. // matches how the rest of ESO reads arbitrary custom resources (unstructured
  222. // objects through a controller-runtime client) rather than a raw dynamic client.
  223. func buildClientFromCluster(cfg *rest.Config, res esv1.CRDProviderResource) (kclient.Client, string, bool, error) {
  224. httpClient, err := rest.HTTPClientFor(cfg)
  225. if err != nil {
  226. return nil, "", false, fmt.Errorf("crd: failed to create http client: %w", err)
  227. }
  228. mapper, err := apiutil.NewDynamicRESTMapper(cfg, httpClient)
  229. if err != nil {
  230. return nil, "", false, fmt.Errorf("crd: failed to create rest mapper: %w", err)
  231. }
  232. mapping, err := mapper.RESTMapping(schema.GroupKind{Group: res.Group, Kind: res.Kind}, res.Version)
  233. if err != nil {
  234. return nil, "", false, fmt.Errorf("crd: group %q version %q kind %q is not registered in the cluster: %w", res.Group, res.Version, res.Kind, err)
  235. }
  236. c, err := kclient.New(cfg, kclient.Options{Mapper: mapper, HTTPClient: httpClient})
  237. if err != nil {
  238. return nil, "", false, fmt.Errorf("crd: failed to create client: %w", err)
  239. }
  240. namespaced := mapping.Scope.Name() == meta.RESTScopeNameNamespace
  241. return c, mapping.Resource.Resource, namespaced, nil
  242. }
  243. // ensureResourceAccess performs a SelfSubjectAccessReview for each of the
  244. // supplied verbs against the target resource, returning the first denial as an
  245. // error. Callers pass {"get"} at preflight and {"list"} from GetAllSecrets so
  246. // "list" permission is only required for callers that actually list.
  247. func ensureResourceAccess(ctx context.Context, cfg *rest.Config, res esv1.CRDProviderResource, plural, namespace string, verbs []string) error {
  248. cs, err := kubernetes.NewForConfig(cfg)
  249. if err != nil {
  250. return fmt.Errorf("crd: failed to create kubernetes client for access review: %w", err)
  251. }
  252. for _, verb := range verbs {
  253. review := &authv1.SelfSubjectAccessReview{
  254. Spec: authv1.SelfSubjectAccessReviewSpec{
  255. ResourceAttributes: &authv1.ResourceAttributes{
  256. Group: res.Group,
  257. Version: res.Version,
  258. Resource: plural,
  259. Verb: verb,
  260. Namespace: namespace,
  261. },
  262. },
  263. }
  264. resp, err := cs.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, review, metav1.CreateOptions{})
  265. if err != nil {
  266. return fmt.Errorf("crd: failed to verify %q permission for resource %q: %w", verb, plural, err)
  267. }
  268. if !resp.Status.Allowed {
  269. return fmt.Errorf("crd: serviceaccount is not allowed to %q resource %q in apiGroup %q", verb, plural, res.Group)
  270. }
  271. }
  272. return nil
  273. }
  274. // ValidateStore checks the store configuration.
  275. func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  276. spec := store.GetSpec()
  277. if spec == nil || spec.Provider == nil || spec.Provider.CRD == nil {
  278. return nil, nil
  279. }
  280. prov := spec.Provider.CRD
  281. // server.url requires credentials (auth or authRef) to connect with.
  282. if prov.Server.URL != "" && prov.Auth == nil && prov.AuthRef == nil {
  283. return nil, errors.New("server.url requires auth or authRef when set")
  284. }
  285. // The server/auth/authRef fields reuse the Kubernetes provider's connection
  286. // types, so their validation is shared via esutils rather than duplicated.
  287. warnings, err := esutils.ValidateKubernetesConnection(store, prov.Server, prov.Auth, prov.AuthRef)
  288. if err != nil {
  289. return warnings, err
  290. }
  291. if prov.Resource.Version == "" {
  292. return nil, errMissingVersion
  293. }
  294. if prov.Resource.Kind == "" {
  295. return nil, errMissingKind
  296. }
  297. // Only block reading the core v1 Kubernetes Secret resource; CRDs that
  298. // happen to be named "Secret" in a different API group are legitimate.
  299. if isCoreV1Secret(prov.Resource) {
  300. return nil, errKindIsSecret
  301. }
  302. if _, err := compileWhitelistRules(prov.Whitelist); err != nil {
  303. return warnings, err
  304. }
  305. // A SecretStore only ever reads its own namespace, so a whitelist rule that
  306. // constrains the namespace can never match: it looks like a restriction but
  307. // silently denies everything. Reject it at admission rather than letting the
  308. // misconfiguration surface as empty reads later. Namespace rules remain valid
  309. // for a ClusterSecretStore, which reads across namespaces.
  310. if store.GetKind() == esv1.SecretStoreKind && prov.Whitelist != nil {
  311. for i, r := range prov.Whitelist.Rules {
  312. if r.Namespace != "" {
  313. return warnings, fmt.Errorf("crd: whitelist.rules[%d].namespace is not supported for a SecretStore (it only reads its own namespace); remove it or use a ClusterSecretStore", i)
  314. }
  315. }
  316. }
  317. return warnings, nil
  318. }
  319. // getProvider extracts the CRDProvider spec from a GenericStore, returning an
  320. // error if the store is nil or the CRD provider block is missing.
  321. func getProvider(store esv1.GenericStore) (*esv1.CRDProvider, error) {
  322. if store == nil {
  323. return nil, errMissingStore
  324. }
  325. spec := store.GetSpec()
  326. if spec == nil || spec.Provider == nil || spec.Provider.CRD == nil {
  327. return nil, errMissingCRDProvider
  328. }
  329. return spec.Provider.CRD, nil
  330. }
  331. // NewProvider creates a new Provider instance.
  332. func NewProvider() esv1.Provider {
  333. return newProvider()
  334. }
  335. // ProviderSpec returns the SecretStoreProvider spec used for registration.
  336. func ProviderSpec() *esv1.SecretStoreProvider {
  337. return &esv1.SecretStoreProvider{
  338. CRD: &esv1.CRDProvider{},
  339. }
  340. }
  341. // MaintenanceStatus returns the maintenance status for this provider.
  342. func MaintenanceStatus() esv1.MaintenanceStatus {
  343. return esv1.MaintenanceStatusMaintained
  344. }