gitlab.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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 implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package gitlab
  13. import (
  14. "context"
  15. "encoding/json"
  16. "fmt"
  17. "net/http"
  18. "sort"
  19. "strconv"
  20. "strings"
  21. "github.com/tidwall/gjson"
  22. "github.com/xanzy/go-gitlab"
  23. corev1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/types"
  25. ctrl "sigs.k8s.io/controller-runtime"
  26. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  27. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  28. "github.com/external-secrets/external-secrets/pkg/find"
  29. "github.com/external-secrets/external-secrets/pkg/provider/metrics"
  30. "github.com/external-secrets/external-secrets/pkg/utils"
  31. )
  32. const (
  33. errGitlabCredSecretName = "credentials are empty"
  34. errInvalidClusterStoreMissingSAKNamespace = "invalid clusterStore missing SAK namespace"
  35. errFetchSAKSecret = "couldn't find secret on cluster: %w"
  36. errMissingSAK = "missing credentials while setting auth"
  37. errList = "could not verify whether the gilabClient is valid: %w"
  38. errProjectAuth = "gitlabClient is not allowed to get secrets for project id [%s]"
  39. errGroupAuth = "gitlabClient is not allowed to get secrets for group id [%s]"
  40. errUninitializedGitlabProvider = "provider gitlab is not initialized"
  41. errNameNotDefined = "'find.name' is mandatory"
  42. errEnvironmentIsConstricted = "'find.tags' is constrained by 'environment_scope' of the store"
  43. errTagsOnlyEnvironmentSupported = "'find.tags' only supports 'environment_scope'"
  44. errPathNotImplemented = "'find.path' is not implemented in the Gitlab provider"
  45. errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
  46. )
  47. // https://github.com/external-secrets/external-secrets/issues/644
  48. var _ esv1beta1.SecretsClient = &Gitlab{}
  49. var _ esv1beta1.Provider = &Gitlab{}
  50. type ProjectsClient interface {
  51. ListProjectsGroups(pid interface{}, opt *gitlab.ListProjectGroupOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectGroup, *gitlab.Response, error)
  52. }
  53. type ProjectVariablesClient interface {
  54. GetVariable(pid interface{}, key string, opt *gitlab.GetProjectVariableOptions, options ...gitlab.RequestOptionFunc) (*gitlab.ProjectVariable, *gitlab.Response, error)
  55. ListVariables(pid interface{}, opt *gitlab.ListProjectVariablesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectVariable, *gitlab.Response, error)
  56. }
  57. type GroupVariablesClient interface {
  58. GetVariable(gid interface{}, key string, options ...gitlab.RequestOptionFunc) (*gitlab.GroupVariable, *gitlab.Response, error)
  59. ListVariables(gid interface{}, opt *gitlab.ListGroupVariablesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.GroupVariable, *gitlab.Response, error)
  60. }
  61. // Gitlab Provider struct with reference to GitLab clients, a projectID and groupIDs.
  62. type Gitlab struct {
  63. projectsClient ProjectsClient
  64. projectVariablesClient ProjectVariablesClient
  65. groupVariablesClient GroupVariablesClient
  66. url string
  67. projectID string
  68. inheritFromGroups bool
  69. groupIDs []string
  70. environment string
  71. }
  72. // gClient for interacting with kubernetes cluster...?
  73. type gClient struct {
  74. kube kclient.Client
  75. store *esv1beta1.GitlabProvider
  76. namespace string
  77. storeKind string
  78. credentials []byte
  79. }
  80. type ProjectGroupPathSorter []*gitlab.ProjectGroup
  81. func (a ProjectGroupPathSorter) Len() int { return len(a) }
  82. func (a ProjectGroupPathSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  83. func (a ProjectGroupPathSorter) Less(i, j int) bool { return len(a[i].FullPath) < len(a[j].FullPath) }
  84. var log = ctrl.Log.WithName("provider").WithName("gitlab")
  85. func init() {
  86. esv1beta1.Register(&Gitlab{}, &esv1beta1.SecretStoreProvider{
  87. Gitlab: &esv1beta1.GitlabProvider{},
  88. })
  89. }
  90. // Set gClient credentials to Access Token.
  91. func (c *gClient) setAuth(ctx context.Context) error {
  92. credentialsSecret := &corev1.Secret{}
  93. credentialsSecretName := c.store.Auth.SecretRef.AccessToken.Name
  94. if credentialsSecretName == "" {
  95. return fmt.Errorf(errGitlabCredSecretName)
  96. }
  97. objectKey := types.NamespacedName{
  98. Name: credentialsSecretName,
  99. Namespace: c.namespace,
  100. }
  101. // only ClusterStore is allowed to set namespace (and then it's required)
  102. if c.storeKind == esv1beta1.ClusterSecretStoreKind {
  103. if c.store.Auth.SecretRef.AccessToken.Namespace == nil {
  104. return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
  105. }
  106. objectKey.Namespace = *c.store.Auth.SecretRef.AccessToken.Namespace
  107. }
  108. err := c.kube.Get(ctx, objectKey, credentialsSecret)
  109. if err != nil {
  110. return fmt.Errorf(errFetchSAKSecret, err)
  111. }
  112. c.credentials = credentialsSecret.Data[c.store.Auth.SecretRef.AccessToken.Key]
  113. if c.credentials == nil || len(c.credentials) == 0 {
  114. return fmt.Errorf(errMissingSAK)
  115. }
  116. return nil
  117. }
  118. // Function newGitlabProvider returns a reference to a new instance of a 'Gitlab' struct.
  119. func NewGitlabProvider() *Gitlab {
  120. return &Gitlab{}
  121. }
  122. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  123. func (g *Gitlab) Capabilities() esv1beta1.SecretStoreCapabilities {
  124. return esv1beta1.SecretStoreReadOnly
  125. }
  126. // Method on Gitlab Provider to set up projectVariablesClient with credentials, populate projectID and environment.
  127. func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
  128. storeSpec := store.GetSpec()
  129. if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Gitlab == nil {
  130. return nil, fmt.Errorf("no store type or wrong store type")
  131. }
  132. storeSpecGitlab := storeSpec.Provider.Gitlab
  133. cliStore := gClient{
  134. kube: kube,
  135. store: storeSpecGitlab,
  136. namespace: namespace,
  137. storeKind: store.GetObjectKind().GroupVersionKind().Kind,
  138. }
  139. if err := cliStore.setAuth(ctx); err != nil {
  140. return nil, err
  141. }
  142. var err error
  143. // Create projectVariablesClient options
  144. var opts []gitlab.ClientOptionFunc
  145. if cliStore.store.URL != "" {
  146. opts = append(opts, gitlab.WithBaseURL(cliStore.store.URL))
  147. }
  148. // ClientOptionFunc from the gitlab package can be mapped with the CRD
  149. // in a similar way to extend functionality of the provider
  150. // Create a new Gitlab Client using credentials and options
  151. gitlabClient, err := gitlab.NewClient(string(cliStore.credentials), opts...)
  152. if err != nil {
  153. return nil, err
  154. }
  155. g.projectsClient = gitlabClient.Projects
  156. g.projectVariablesClient = gitlabClient.ProjectVariables
  157. g.groupVariablesClient = gitlabClient.GroupVariables
  158. g.projectID = cliStore.store.ProjectID
  159. g.inheritFromGroups = cliStore.store.InheritFromGroups
  160. g.groupIDs = cliStore.store.GroupIDs
  161. g.environment = cliStore.store.Environment
  162. g.url = cliStore.store.URL
  163. return g, nil
  164. }
  165. func (g *Gitlab) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
  166. return fmt.Errorf("not implemented")
  167. }
  168. // Not Implemented PushSecret.
  169. func (g *Gitlab) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
  170. return fmt.Errorf("not implemented")
  171. }
  172. // GetAllSecrets syncs all gitlab project and group variables into a single Kubernetes Secret.
  173. func (g *Gitlab) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  174. if utils.IsNil(g.projectVariablesClient) {
  175. return nil, fmt.Errorf(errUninitializedGitlabProvider)
  176. }
  177. if ref.Tags != nil {
  178. environment, err := ExtractTag(ref.Tags)
  179. if err != nil {
  180. return nil, err
  181. }
  182. if !isEmptyOrWildcard(g.environment) && !isEmptyOrWildcard(environment) {
  183. return nil, fmt.Errorf(errEnvironmentIsConstricted)
  184. }
  185. g.environment = environment
  186. }
  187. if ref.Path != nil {
  188. return nil, fmt.Errorf(errPathNotImplemented)
  189. }
  190. if ref.Name == nil {
  191. return nil, fmt.Errorf(errNameNotDefined)
  192. }
  193. var matcher *find.Matcher
  194. if ref.Name != nil {
  195. m, err := find.New(*ref.Name)
  196. if err != nil {
  197. return nil, err
  198. }
  199. matcher = m
  200. }
  201. err := g.ResolveGroupIds()
  202. if err != nil {
  203. return nil, err
  204. }
  205. var gopts = &gitlab.ListGroupVariablesOptions{PerPage: 100}
  206. secretData := make(map[string][]byte)
  207. for _, groupID := range g.groupIDs {
  208. for groupPage := 1; ; groupPage++ {
  209. gopts.Page = groupPage
  210. groupVars, response, err := g.groupVariablesClient.ListVariables(groupID, gopts)
  211. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabGroupListVariables, err)
  212. if err != nil {
  213. return nil, err
  214. }
  215. for _, data := range groupVars {
  216. matching, key, isWildcard := matchesFilter(g.environment, data.EnvironmentScope, data.Key, matcher)
  217. if !matching && !isWildcard {
  218. continue
  219. }
  220. secretData[key] = []byte(data.Value)
  221. }
  222. if response.CurrentPage >= response.TotalPages {
  223. break
  224. }
  225. }
  226. }
  227. var popts = &gitlab.ListProjectVariablesOptions{PerPage: 100}
  228. for projectPage := 1; ; projectPage++ {
  229. popts.Page = projectPage
  230. projectData, response, err := g.projectVariablesClient.ListVariables(g.projectID, popts)
  231. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabProjectListVariables, err)
  232. if err != nil {
  233. return nil, err
  234. }
  235. for _, data := range projectData {
  236. matching, key, isWildcard := matchesFilter(g.environment, data.EnvironmentScope, data.Key, matcher)
  237. if !matching {
  238. continue
  239. }
  240. _, exists := secretData[key]
  241. if exists && isWildcard {
  242. continue
  243. }
  244. secretData[key] = []byte(data.Value)
  245. }
  246. if response.CurrentPage >= response.TotalPages {
  247. break
  248. }
  249. }
  250. return secretData, nil
  251. }
  252. func ExtractTag(tags map[string]string) (string, error) {
  253. var environmentScope string
  254. for tag, value := range tags {
  255. if tag != "environment_scope" {
  256. return "", fmt.Errorf(errTagsOnlyEnvironmentSupported)
  257. }
  258. environmentScope = value
  259. }
  260. return environmentScope, nil
  261. }
  262. func (g *Gitlab) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  263. if utils.IsNil(g.projectVariablesClient) || utils.IsNil(g.groupVariablesClient) {
  264. return nil, fmt.Errorf(errUninitializedGitlabProvider)
  265. }
  266. // Need to replace hyphens with underscores to work with Gitlab API
  267. ref.Key = strings.ReplaceAll(ref.Key, "-", "_")
  268. // Retrieves a gitlab variable in the form
  269. // {
  270. // "key": "TEST_VARIABLE_1",
  271. // "variable_type": "env_var",
  272. // "value": "TEST_1",
  273. // "protected": false,
  274. // "masked": true,
  275. // "environment_scope": "*"
  276. // }
  277. var vopts *gitlab.GetProjectVariableOptions
  278. if g.environment != "" {
  279. vopts = &gitlab.GetProjectVariableOptions{Filter: &gitlab.VariableFilter{EnvironmentScope: g.environment}}
  280. }
  281. data, resp, err := g.projectVariablesClient.GetVariable(g.projectID, ref.Key, vopts)
  282. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabProjectVariableGet, err)
  283. if !isEmptyOrWildcard(g.environment) && resp.StatusCode == http.StatusNotFound {
  284. vopts.Filter.EnvironmentScope = "*"
  285. data, resp, err = g.projectVariablesClient.GetVariable(g.projectID, ref.Key, vopts)
  286. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabProjectVariableGet, err)
  287. }
  288. if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound && err != nil {
  289. return nil, err
  290. }
  291. err = g.ResolveGroupIds()
  292. if err != nil {
  293. return nil, err
  294. }
  295. var result []byte
  296. if resp.StatusCode < 300 {
  297. result, err = extractVariable(ref, data.Value)
  298. }
  299. for i := len(g.groupIDs) - 1; i >= 0; i-- {
  300. groupID := g.groupIDs[i]
  301. if result != nil {
  302. return result, nil
  303. }
  304. groupVar, resp, err := g.groupVariablesClient.GetVariable(groupID, ref.Key, nil)
  305. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabGroupGetVariable, err)
  306. if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound && err != nil {
  307. return nil, err
  308. }
  309. if resp.StatusCode < 300 {
  310. result, _ = extractVariable(ref, groupVar.Value)
  311. }
  312. }
  313. if result != nil {
  314. return result, nil
  315. }
  316. return nil, err
  317. }
  318. func extractVariable(ref esv1beta1.ExternalSecretDataRemoteRef, value string) ([]byte, error) {
  319. if ref.Property == "" {
  320. if value != "" {
  321. return []byte(value), nil
  322. }
  323. return nil, fmt.Errorf("invalid secret received. no secret string for key: %s", ref.Key)
  324. }
  325. var payload string
  326. if value != "" {
  327. payload = value
  328. }
  329. val := gjson.Get(payload, ref.Property)
  330. if !val.Exists() {
  331. return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
  332. }
  333. return []byte(val.String()), nil
  334. }
  335. func (g *Gitlab) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  336. // Gets a secret as normal, expecting secret value to be a json object
  337. data, err := g.GetSecret(ctx, ref)
  338. if err != nil {
  339. return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
  340. }
  341. // Maps the json data to a string:string map
  342. kv := make(map[string]string)
  343. err = json.Unmarshal(data, &kv)
  344. if err != nil {
  345. return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
  346. }
  347. // Converts values in K:V pairs into bytes, while leaving keys as strings
  348. secretData := make(map[string][]byte)
  349. for k, v := range kv {
  350. secretData[k] = []byte(v)
  351. }
  352. return secretData, nil
  353. }
  354. func isEmptyOrWildcard(environment string) bool {
  355. return environment == "" || environment == "*"
  356. }
  357. func matchesFilter(environment, varEnvironment, key string, matcher *find.Matcher) (bool, string, bool) {
  358. isWildcard := isEmptyOrWildcard(varEnvironment)
  359. if !isWildcard && !isEmptyOrWildcard(environment) {
  360. // as of now gitlab does not support filtering of EnvironmentScope through the api call
  361. if varEnvironment != environment {
  362. return false, "", isWildcard
  363. }
  364. }
  365. if key == "" || (matcher != nil && !matcher.MatchName(key)) {
  366. return false, "", isWildcard
  367. }
  368. return true, key, isWildcard
  369. }
  370. func (g *Gitlab) Close(ctx context.Context) error {
  371. return nil
  372. }
  373. // Validate will use the gitlab projectVariablesClient/groupVariablesClient to validate the gitlab provider using the ListVariable call to ensure get permissions without needing a specific key.
  374. func (g *Gitlab) Validate() (esv1beta1.ValidationResult, error) {
  375. if g.projectID != "" {
  376. _, resp, err := g.projectVariablesClient.ListVariables(g.projectID, nil)
  377. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabProjectListVariables, err)
  378. if err != nil {
  379. return esv1beta1.ValidationResultError, fmt.Errorf(errList, err)
  380. } else if resp == nil || resp.StatusCode != http.StatusOK {
  381. return esv1beta1.ValidationResultError, fmt.Errorf(errProjectAuth, g.projectID)
  382. }
  383. err = g.ResolveGroupIds()
  384. if err != nil {
  385. return esv1beta1.ValidationResultError, fmt.Errorf(errList, err)
  386. }
  387. log.V(1).Info("discovered project groups", "name", g.groupIDs)
  388. }
  389. if len(g.groupIDs) > 0 {
  390. for _, groupID := range g.groupIDs {
  391. _, resp, err := g.groupVariablesClient.ListVariables(groupID, nil)
  392. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabGroupListVariables, err)
  393. if err != nil {
  394. return esv1beta1.ValidationResultError, fmt.Errorf(errList, err)
  395. } else if resp == nil || resp.StatusCode != http.StatusOK {
  396. return esv1beta1.ValidationResultError, fmt.Errorf(errGroupAuth, groupID)
  397. }
  398. }
  399. }
  400. return esv1beta1.ValidationResultReady, nil
  401. }
  402. func (g *Gitlab) ResolveGroupIds() error {
  403. if g.inheritFromGroups {
  404. projectGroups, resp, err := g.projectsClient.ListProjectsGroups(g.projectID, nil)
  405. metrics.ObserveAPICall(metrics.ProviderGitLab, metrics.CallGitLabListProjectsGroups, err)
  406. if resp.StatusCode >= 400 && err != nil {
  407. return err
  408. }
  409. sort.Sort(ProjectGroupPathSorter(projectGroups))
  410. discoveredIds := make([]string, len(projectGroups))
  411. for i, group := range projectGroups {
  412. discoveredIds[i] = strconv.Itoa(group.ID)
  413. }
  414. g.groupIDs = discoveredIds
  415. }
  416. return nil
  417. }
  418. func (g *Gitlab) ValidateStore(store esv1beta1.GenericStore) error {
  419. storeSpec := store.GetSpec()
  420. gitlabSpec := storeSpec.Provider.Gitlab
  421. accessToken := gitlabSpec.Auth.SecretRef.AccessToken
  422. err := utils.ValidateSecretSelector(store, accessToken)
  423. if err != nil {
  424. return err
  425. }
  426. if gitlabSpec.ProjectID == "" && len(gitlabSpec.GroupIDs) == 0 {
  427. return fmt.Errorf("projectID and groupIDs must not both be empty")
  428. }
  429. if gitlabSpec.InheritFromGroups && len(gitlabSpec.GroupIDs) > 0 {
  430. return fmt.Errorf("defining groupIDs and inheritFromGroups = true is not allowed")
  431. }
  432. if accessToken.Key == "" {
  433. return fmt.Errorf("accessToken.key cannot be empty")
  434. }
  435. if accessToken.Name == "" {
  436. return fmt.Errorf("accessToken.name cannot be empty")
  437. }
  438. return nil
  439. }