crd.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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 implements an External Secrets provider that reads data from
  14. // arbitrary Kubernetes Custom Resources (CRDs) using ServiceAccount token auth.
  15. package crd
  16. import (
  17. "context"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "regexp"
  22. "github.com/tidwall/gjson"
  23. apierrors "k8s.io/apimachinery/pkg/api/errors"
  24. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  25. "k8s.io/apimachinery/pkg/runtime/schema"
  26. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  27. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. "github.com/external-secrets/external-secrets/runtime/esutils"
  29. )
  30. // ensureConnected rejects reads on the referent stub client, which has no kube
  31. // client (see newClient): the stub only answers Validate() while the store's SA
  32. // namespace is unknown, and the operational client is rebuilt per-ExternalSecret
  33. // at reconcile. Without this guard a read would nil-panic on c.kube.
  34. func (c *Client) ensureConnected() error {
  35. if c.kube == nil {
  36. return errClientNotReady
  37. }
  38. return nil
  39. }
  40. // GetSecret retrieves a single value from a CRD object.
  41. // ref.Key is interpreted per store kind (see parseRemoteRefKey); ref.Property is an optional GJSON path expression.
  42. func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  43. obj, err := c.fetchObject(ctx, ref)
  44. if err != nil {
  45. return nil, err
  46. }
  47. return extractValue(obj, ref.Property, nil)
  48. }
  49. // GetSecretMap returns a map of key/value pairs extracted from a CRD object.
  50. func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  51. obj, err := c.fetchObject(ctx, ref)
  52. if err != nil {
  53. return nil, err
  54. }
  55. raw, err := extractValue(obj, ref.Property, nil)
  56. if err != nil {
  57. return nil, err
  58. }
  59. return jsonBytesToMap(raw)
  60. }
  61. // fetchObject validates ref.Key, enforces the whitelist, and retrieves the named CRD object.
  62. func (c *Client) fetchObject(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (*unstructured.Unstructured, error) {
  63. return c.resolveWhitelistedObject(ctx, ref.Key, ref.Property)
  64. }
  65. // resolveWhitelistedObject validates the key, enforces the whitelist, and
  66. // retrieves the named CRD object. Shared by GetSecret and SecretExists so both
  67. // read paths apply the same whitelist gate; without it SecretExists could be
  68. // used to probe the existence of objects the whitelist does not permit.
  69. func (c *Client) resolveWhitelistedObject(ctx context.Context, key, property string) (*unstructured.Unstructured, error) {
  70. if err := c.ensureConnected(); err != nil {
  71. return nil, err
  72. }
  73. if key == "" {
  74. return nil, errors.New("crd: ref.key must not be empty")
  75. }
  76. objectName, keyNamespace, err := parseRemoteRefKey(c.storeKind, key)
  77. if err != nil {
  78. return nil, err
  79. }
  80. ns := ""
  81. if keyNamespace != nil {
  82. ns = *keyNamespace
  83. }
  84. var requestedKeys []string
  85. if property != "" {
  86. requestedKeys = []string{property}
  87. }
  88. if !c.matchesWhitelistRule(objectName, ns, requestedKeys) {
  89. return nil, fmt.Errorf("crd: request for %q denied by whitelist rules", key)
  90. }
  91. return c.getObject(ctx, objectName, keyNamespace)
  92. }
  93. // GetAllSecrets lists CRD objects whose logical keys match the store Name pattern
  94. // (regex) and returns a map of logicalKey to serialized value.
  95. // For SecretStore (namespaced kind), listing is limited to the store namespace and keys are object names.
  96. // For ClusterSecretStore with a namespaced kind, listing spans all namespaces and keys are
  97. // namespace/name. Cluster-scoped kinds use object names only.
  98. func (c *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  99. if err := c.ensureConnected(); err != nil {
  100. return nil, err
  101. }
  102. // Verify the caller actually has "list" permission. The preflight at store
  103. // bootstrap only checks "get" — moving "list" here means a SA that only
  104. // ever uses GetSecret does not need list rights, but anything that calls
  105. // dataFrom.find must.
  106. if c.listAccessCheck != nil {
  107. if err := c.listAccessCheck(ctx); err != nil {
  108. return nil, err
  109. }
  110. }
  111. list := &unstructured.UnstructuredList{}
  112. gvk := c.buildGVK()
  113. list.SetGroupVersionKind(gvk.GroupVersion().WithKind(gvk.Kind + "List"))
  114. var opts []kclient.ListOption
  115. if c.namespaced && c.storeKind != esv1.ClusterSecretStoreKind {
  116. // SecretStore over a namespaced kind lists within its own namespace.
  117. // Cluster-scoped kinds, and a ClusterSecretStore over a namespaced kind,
  118. // list across all namespaces (no namespace option).
  119. if c.namespace == "" {
  120. return nil, fmt.Errorf("crd: namespace is required for namespaced resource kind %q", c.store.Resource.Kind)
  121. }
  122. opts = append(opts, kclient.InNamespace(c.namespace))
  123. }
  124. if err := c.kube.List(ctx, list, opts...); err != nil {
  125. return nil, fmt.Errorf("crd: failed to list %s: %w", c.store.Resource.Kind, err)
  126. }
  127. var re *regexp.Regexp
  128. if ref.Name != nil && ref.Name.RegExp != "" {
  129. compiled, err := regexp.Compile(ref.Name.RegExp)
  130. if err != nil {
  131. return nil, fmt.Errorf("crd: invalid name pattern %q: %w", ref.Name.RegExp, err)
  132. }
  133. re = compiled
  134. }
  135. result := make(map[string][]byte, len(list.Items))
  136. for i := range list.Items {
  137. item := &list.Items[i]
  138. objName := item.GetName()
  139. objNS := item.GetNamespace()
  140. logicalKey := objName
  141. if c.namespaced && c.storeKind == esv1.ClusterSecretStoreKind {
  142. logicalKey = objNS + "/" + objName
  143. }
  144. if re != nil && !re.MatchString(logicalKey) {
  145. continue
  146. }
  147. if !c.matchesWhitelistRule(objName, objNS, nil) {
  148. continue
  149. }
  150. b, err := extractValue(item, "", nil)
  151. if err != nil {
  152. return nil, fmt.Errorf("crd: failed to extract value from %s/%s: %w", c.store.Resource.Kind, logicalKey, err)
  153. }
  154. result[logicalKey] = b
  155. }
  156. return esutils.ConvertKeys(ref.ConversionStrategy, result)
  157. }
  158. // SecretExists returns true when the named CRD object exists and is permitted
  159. // by the whitelist. The whitelist is enforced here (not just in GetSecret) so
  160. // this cannot be used to probe for objects outside the allowed set.
  161. func (c *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  162. _, err := c.resolveWhitelistedObject(ctx, ref.GetRemoteKey(), ref.GetProperty())
  163. if err != nil {
  164. if errors.Is(err, esv1.NoSecretError{}) {
  165. return false, nil
  166. }
  167. return false, err
  168. }
  169. return true, nil
  170. }
  171. // Validate checks that the provider is correctly configured.
  172. func (c *Client) Validate() (esv1.ValidationResult, error) {
  173. // A referent ClusterSecretStore cannot be validated at store-creation time:
  174. // the ServiceAccount namespace is only known once an ExternalSecret consumes
  175. // the store. Report "unknown" rather than a false "ready".
  176. if c.referent {
  177. return esv1.ValidationResultUnknown, nil
  178. }
  179. return esv1.ValidationResultReady, nil
  180. }
  181. // Close is a no-op for the CRD provider.
  182. func (c *Client) Close(_ context.Context) error {
  183. return nil
  184. }
  185. // buildGVK returns the GroupVersionKind of the configured target resource. The
  186. // controller-runtime client's RESTMapper resolves this to the correct resource
  187. // and scope at request time.
  188. func (c *Client) buildGVK() schema.GroupVersionKind {
  189. return schema.GroupVersionKind{
  190. Group: c.store.Resource.Group,
  191. Version: c.store.Resource.Version,
  192. Kind: c.store.Resource.Kind,
  193. }
  194. }
  195. // getObject fetches a CRD object from the already-parsed remoteRef.key
  196. // components (see parseRemoteRefKey). Callers parse the key once and pass the
  197. // object name and optional namespace in, so the key is not re-parsed here.
  198. func (c *Client) getObject(ctx context.Context, objName string, keyNS *string) (*unstructured.Unstructured, error) {
  199. obj := &unstructured.Unstructured{}
  200. obj.SetGroupVersionKind(c.buildGVK())
  201. if c.namespaced {
  202. var requestNS string
  203. switch {
  204. case keyNS != nil:
  205. requestNS = *keyNS
  206. case c.storeKind == esv1.SecretStoreKind:
  207. requestNS = c.namespace
  208. default:
  209. return nil, fmt.Errorf("crd: namespaced resource kind %q requires remoteRef.key in the form namespace/objectName when using ClusterSecretStore", c.store.Resource.Kind)
  210. }
  211. if requestNS == "" {
  212. return nil, fmt.Errorf("crd: namespace is required for namespaced resource kind %q", c.store.Resource.Kind)
  213. }
  214. if err := c.kube.Get(ctx, kclient.ObjectKey{Namespace: requestNS, Name: objName}, obj); err != nil {
  215. if apierrors.IsNotFound(err) {
  216. return nil, esv1.NoSecretError{}
  217. }
  218. return nil, fmt.Errorf("crd: failed to get %s %s/%s: %w", c.store.Resource.Kind, requestNS, objName, err)
  219. }
  220. return obj, nil
  221. }
  222. if keyNS != nil {
  223. return nil, fmt.Errorf("crd: cluster-scoped resource kind %q does not allow '/' in remoteRef.key (use object name only)", c.store.Resource.Kind)
  224. }
  225. if err := c.kube.Get(ctx, kclient.ObjectKey{Name: objName}, obj); err != nil {
  226. if apierrors.IsNotFound(err) {
  227. return nil, esv1.NoSecretError{}
  228. }
  229. return nil, fmt.Errorf("crd: failed to get %s/%s: %w", c.store.Resource.Kind, objName, err)
  230. }
  231. return obj, nil
  232. }
  233. // extractValue serializes an unstructured object (or a sub-field) to bytes.
  234. // property is a GJSON path expression taking precedence over fields; it uses the
  235. // same syntax as the Kubernetes provider (see
  236. // https://github.com/tidwall/gjson/blob/master/SYNTAX.md) so the property
  237. // dialect is consistent across ESO providers.
  238. // fields is the store-level Properties list restricting which fields are included.
  239. func extractValue(obj *unstructured.Unstructured, property string, fields []string) ([]byte, error) {
  240. raw, err := json.Marshal(obj.Object)
  241. if err != nil {
  242. return nil, fmt.Errorf("crd: failed to marshal object: %w", err)
  243. }
  244. if property != "" {
  245. res := gjson.GetBytes(raw, property)
  246. if !res.Exists() {
  247. return nil, fmt.Errorf("crd: property %q not found in object %q", property, obj.GetName())
  248. }
  249. // String leaves are returned unwrapped; everything else (objects,
  250. // arrays, numbers, booleans) is returned as its raw JSON.
  251. if res.Type == gjson.String {
  252. return []byte(res.Str), nil
  253. }
  254. return []byte(res.Raw), nil
  255. }
  256. if len(fields) > 0 {
  257. subset := make(map[string]any, len(fields))
  258. for _, f := range fields {
  259. res := gjson.GetBytes(raw, f)
  260. if res.Exists() {
  261. subset[f] = res.Value()
  262. }
  263. }
  264. return esutils.JSONMarshal(subset)
  265. }
  266. return raw, nil
  267. }
  268. // jsonBytesToMap converts a JSON byte slice to map[string][]byte.
  269. // String values are unwrapped (JSON quotes removed); non-string values
  270. // (objects, arrays, numbers, booleans) are kept as raw JSON bytes.
  271. //
  272. // When the input is valid JSON but not an object (e.g. a bare string
  273. // `"hello"` or an array `[1,2]`), it cannot be mapped to key/value
  274. // pairs. In that case the raw payload is returned under a single
  275. // "value" key. This is intentional: the input always originates from
  276. // extractValue which already validated it via json.Marshal, so a
  277. // non-object result is expected for non-map properties.
  278. func jsonBytesToMap(raw []byte) (map[string][]byte, error) {
  279. var kv map[string]json.RawMessage
  280. if err := json.Unmarshal(raw, &kv); err != nil {
  281. return map[string][]byte{"value": raw}, nil
  282. }
  283. out := make(map[string][]byte, len(kv))
  284. for k, v := range kv {
  285. var s string
  286. if err := json.Unmarshal(v, &s); err == nil {
  287. out[k] = []byte(s)
  288. } else {
  289. out[k] = v
  290. }
  291. }
  292. return out, nil
  293. }
  294. // compiledWhitelistRule is a pre-validated, pre-compiled form of
  295. // CRDProviderWhitelistRule. Patterns are compiled once at Client construction
  296. // and reused on every read instead of recompiling on the hot path.
  297. type compiledWhitelistRule struct {
  298. name *regexp.Regexp // nil when the rule does not constrain the object name
  299. namespace *regexp.Regexp // nil when the rule does not constrain the namespace
  300. properties []*regexp.Regexp // empty when the rule does not constrain properties
  301. }
  302. // compileWhitelistRules validates and compiles every regex in the whitelist.
  303. // Returns nil with no error when the whitelist is unset or has no rules.
  304. // Empty rules (no name, no namespace, no properties) are rejected because they
  305. // would match anything and silently widen access.
  306. func compileWhitelistRules(wl *esv1.CRDProviderWhitelist) ([]compiledWhitelistRule, error) {
  307. if wl == nil || len(wl.Rules) == 0 {
  308. return nil, nil
  309. }
  310. rules := make([]compiledWhitelistRule, 0, len(wl.Rules))
  311. for i, r := range wl.Rules {
  312. if r.Name == "" && r.Namespace == "" && len(r.Properties) == 0 {
  313. return nil, fmt.Errorf("crd: whitelist.rules[%d]: %w", i, errEmptyWhitelistRule)
  314. }
  315. var cr compiledWhitelistRule
  316. if r.Name != "" {
  317. re, err := regexp.Compile(r.Name)
  318. if err != nil {
  319. return nil, fmt.Errorf("crd: invalid whitelist.rules[%d].name regex %q: %w", i, r.Name, err)
  320. }
  321. cr.name = re
  322. }
  323. if r.Namespace != "" {
  324. re, err := regexp.Compile(r.Namespace)
  325. if err != nil {
  326. return nil, fmt.Errorf("crd: invalid whitelist.rules[%d].namespace regex %q: %w", i, r.Namespace, err)
  327. }
  328. cr.namespace = re
  329. }
  330. if len(r.Properties) > 0 {
  331. cr.properties = make([]*regexp.Regexp, 0, len(r.Properties))
  332. for j, p := range r.Properties {
  333. re, err := regexp.Compile(p)
  334. if err != nil {
  335. return nil, fmt.Errorf("crd: invalid whitelist.rules[%d].properties[%d] regex %q: %w", i, j, p, err)
  336. }
  337. cr.properties = append(cr.properties, re)
  338. }
  339. }
  340. rules = append(rules, cr)
  341. }
  342. return rules, nil
  343. }
  344. // matchesWhitelistRule checks whether the given object (identified by its bare
  345. // name and namespace) is permitted by the store's whitelist rules.
  346. // objectName is always the bare name without any namespace prefix.
  347. // namespace is the object's namespace; it is only considered when the store is
  348. // a ClusterSecretStore and rule.namespace is set – for SecretStore the field
  349. // is ignored because the namespace is implicitly fixed to the store namespace.
  350. func (c *Client) matchesWhitelistRule(objectName, namespace string, requestedKeys []string) bool {
  351. if len(c.whitelistRules) == 0 {
  352. return true
  353. }
  354. for _, rule := range c.whitelistRules {
  355. if rule.name != nil && !rule.name.MatchString(objectName) {
  356. continue
  357. }
  358. // Namespace check: only evaluated for ClusterSecretStore. Cluster-scoped
  359. // objects (namespace=="") never match a namespace rule — the rule
  360. // explicitly targets namespaced objects.
  361. if rule.namespace != nil && c.storeKind == esv1.ClusterSecretStoreKind {
  362. if namespace == "" || !rule.namespace.MatchString(namespace) {
  363. continue
  364. }
  365. }
  366. if len(rule.properties) == 0 {
  367. return true
  368. }
  369. if len(requestedKeys) == 0 {
  370. continue
  371. }
  372. allMatched := true
  373. for _, key := range requestedKeys {
  374. matched := false
  375. for _, re := range rule.properties {
  376. if re.MatchString(key) {
  377. matched = true
  378. break
  379. }
  380. }
  381. if !matched {
  382. allMatched = false
  383. break
  384. }
  385. }
  386. if allMatched {
  387. return true
  388. }
  389. }
  390. return false
  391. }