kubeconfig.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. package kubernetes
  2. import (
  3. "context"
  4. "errors"
  5. "strings"
  6. "github.com/porter-dev/porter/internal/models"
  7. "golang.org/x/oauth2/google"
  8. "k8s.io/client-go/tools/clientcmd"
  9. "k8s.io/client-go/tools/clientcmd/api"
  10. "github.com/aws/aws-sdk-go/aws"
  11. "github.com/aws/aws-sdk-go/aws/credentials"
  12. "github.com/aws/aws-sdk-go/aws/session"
  13. token "sigs.k8s.io/aws-iam-authenticator/pkg/token"
  14. )
  15. // GetServiceAccountCandidates parses a kubeconfig for a list of service account
  16. // candidates.
  17. func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCandidate, error) {
  18. config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
  19. if err != nil {
  20. return nil, err
  21. }
  22. rawConf, err := config.RawConfig()
  23. if err != nil {
  24. return nil, err
  25. }
  26. res := make([]*models.ServiceAccountCandidate, 0)
  27. for contextName, context := range rawConf.Contexts {
  28. clusterName := context.Cluster
  29. awsClusterID := ""
  30. authInfoName := context.AuthInfo
  31. // get the auth mechanism and actions
  32. authMechanism, authInfoActions := parseAuthInfoForActions(rawConf.AuthInfos[authInfoName])
  33. clusterActions := parseClusterForActions(rawConf.Clusters[clusterName])
  34. actions := append(authInfoActions, clusterActions...)
  35. // if auth mechanism is unsupported, we'll skip it
  36. if authMechanism == models.NotAvailable {
  37. continue
  38. } else if authMechanism == models.AWS {
  39. // if the auth mechanism is AWS, we need to parse more explicitly
  40. // for the cluster id
  41. awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
  42. }
  43. // construct the raw kubeconfig that's relevant for that context
  44. contextConf, err := getConfigForContext(&rawConf, contextName)
  45. if err != nil {
  46. continue
  47. }
  48. rawBytes, err := clientcmd.Write(*contextConf)
  49. if err == nil {
  50. // create the candidate service account
  51. res = append(res, &models.ServiceAccountCandidate{
  52. Actions: actions,
  53. Kind: "connector",
  54. ClusterName: clusterName,
  55. ClusterEndpoint: rawConf.Clusters[clusterName].Server,
  56. AuthMechanism: authMechanism,
  57. AWSClusterIDGuess: awsClusterID,
  58. Kubeconfig: rawBytes,
  59. })
  60. }
  61. }
  62. return res, nil
  63. }
  64. // GetRawConfigFromBytes returns the clientcmdapi.Config from kubeconfig
  65. // bytes
  66. func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
  67. config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
  68. if err != nil {
  69. return nil, err
  70. }
  71. rawConf, err := config.RawConfig()
  72. if err != nil {
  73. return nil, err
  74. }
  75. return &rawConf, nil
  76. }
  77. // Parsing rules are:
  78. //
  79. // (1) If a client certificate + client key exist, uses x509 auth mechanism
  80. // (2) If an oidc/gcp/aws plugin exists, uses that auth mechanism
  81. // (3) If a bearer token exists, uses bearer token auth mechanism
  82. // (4) If a username/password exist, uses basic auth mechanism
  83. // (5) Otherwise, the config gets skipped
  84. //
  85. func parseAuthInfoForActions(authInfo *api.AuthInfo) (authMechanism string, actions []models.ServiceAccountAction) {
  86. actions = make([]models.ServiceAccountAction, 0)
  87. if (authInfo.ClientCertificate != "" || len(authInfo.ClientCertificateData) != 0) &&
  88. (authInfo.ClientKey != "" || len(authInfo.ClientKeyData) != 0) {
  89. if len(authInfo.ClientCertificateData) == 0 {
  90. actions = append(actions, models.ServiceAccountAction{
  91. Name: models.ClientCertDataAction,
  92. Resolved: false,
  93. Filename: authInfo.ClientCertificate,
  94. })
  95. }
  96. if len(authInfo.ClientKeyData) == 0 {
  97. actions = append(actions, models.ServiceAccountAction{
  98. Name: models.ClientKeyDataAction,
  99. Resolved: false,
  100. Filename: authInfo.ClientKey,
  101. })
  102. }
  103. return models.X509, actions
  104. }
  105. if authInfo.AuthProvider != nil {
  106. switch authInfo.AuthProvider.Name {
  107. case "oidc":
  108. filename, isFile := authInfo.AuthProvider.Config["idp-certificate-authority"]
  109. data, isData := authInfo.AuthProvider.Config["idp-certificate-authority-data"]
  110. if isFile && (!isData || data == "") {
  111. return models.OIDC, []models.ServiceAccountAction{
  112. models.ServiceAccountAction{
  113. Name: models.OIDCIssuerDataAction,
  114. Resolved: false,
  115. Filename: filename,
  116. },
  117. }
  118. }
  119. return models.OIDC, actions
  120. case "gcp":
  121. return models.GCP, []models.ServiceAccountAction{
  122. models.ServiceAccountAction{
  123. Name: models.GCPKeyDataAction,
  124. Resolved: false,
  125. },
  126. }
  127. }
  128. }
  129. if authInfo.Exec != nil {
  130. if authInfo.Exec.Command == "aws" || authInfo.Exec.Command == "aws-iam-authenticator" {
  131. return models.AWS, []models.ServiceAccountAction{
  132. models.ServiceAccountAction{
  133. Name: models.AWSDataAction,
  134. Resolved: false,
  135. },
  136. }
  137. }
  138. }
  139. if authInfo.Token != "" || authInfo.TokenFile != "" {
  140. if authInfo.Token == "" {
  141. return models.Bearer, []models.ServiceAccountAction{
  142. models.ServiceAccountAction{
  143. Name: models.TokenDataAction,
  144. Resolved: false,
  145. Filename: authInfo.TokenFile,
  146. },
  147. }
  148. }
  149. return models.Bearer, actions
  150. }
  151. if authInfo.Username != "" && authInfo.Password != "" {
  152. return models.Basic, actions
  153. }
  154. return models.NotAvailable, actions
  155. }
  156. // Parses the cluster object to determine actions -- only currently supported action is
  157. // population of the cluster certificate authority data
  158. func parseClusterForActions(cluster *api.Cluster) (actions []models.ServiceAccountAction) {
  159. actions = make([]models.ServiceAccountAction, 0)
  160. if cluster.CertificateAuthority != "" && len(cluster.CertificateAuthorityData) == 0 {
  161. return []models.ServiceAccountAction{
  162. models.ServiceAccountAction{
  163. Name: models.ClusterCADataAction,
  164. Resolved: false,
  165. Filename: cluster.CertificateAuthority,
  166. },
  167. }
  168. }
  169. return actions
  170. }
  171. func parseAuthInfoForAWSClusterID(authInfo *api.AuthInfo, fallback string) string {
  172. if authInfo.Exec != nil {
  173. if authInfo.Exec.Command == "aws" {
  174. // look for --cluster-name flag
  175. for i, arg := range authInfo.Exec.Args {
  176. if arg == "--cluster-name" && len(authInfo.Exec.Args) > i+1 {
  177. return authInfo.Exec.Args[i+1]
  178. }
  179. }
  180. } else if authInfo.Exec.Command == "aws-iam-authenticator" {
  181. // look for -i or --cluster-id flag
  182. for i, arg := range authInfo.Exec.Args {
  183. if (arg == "-i" || arg == "--cluster-id") && len(authInfo.Exec.Args) > i+1 {
  184. return authInfo.Exec.Args[i+1]
  185. }
  186. }
  187. }
  188. }
  189. return fallback
  190. }
  191. // getKubeconfigForContext returns the raw kubeconfig associated with only a
  192. // single context of the raw config
  193. func getConfigForContext(
  194. rawConf *api.Config,
  195. contextName string,
  196. ) (*api.Config, error) {
  197. copyConf := rawConf.DeepCopy()
  198. copyConf.Clusters = make(map[string]*api.Cluster)
  199. copyConf.AuthInfos = make(map[string]*api.AuthInfo)
  200. copyConf.Contexts = make(map[string]*api.Context)
  201. copyConf.CurrentContext = contextName
  202. context, ok := rawConf.Contexts[contextName]
  203. if ok {
  204. userName := context.AuthInfo
  205. clusterName := context.Cluster
  206. authInfo, userFound := rawConf.AuthInfos[userName]
  207. cluster, clusterFound := rawConf.Clusters[clusterName]
  208. if userFound && clusterFound {
  209. copyConf.Clusters[clusterName] = cluster
  210. copyConf.AuthInfos[userName] = authInfo
  211. copyConf.Contexts[contextName] = context
  212. } else {
  213. return nil, errors.New("linked user and cluster not found")
  214. }
  215. } else {
  216. return nil, errors.New("context not found")
  217. }
  218. return copyConf, nil
  219. }
  220. // GetClientConfigFromServiceAccount will construct new clientcmd.ClientConfig using
  221. // the configuration saved within a ServiceAccount model
  222. func GetClientConfigFromServiceAccount(
  223. sa *models.ServiceAccount,
  224. clusterID uint,
  225. updateTokenCache UpdateTokenCacheFunc,
  226. ) (clientcmd.ClientConfig, error) {
  227. apiConfig, err := createRawConfigFromServiceAccount(sa, clusterID, updateTokenCache)
  228. if err != nil {
  229. return nil, err
  230. }
  231. config := clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{})
  232. return config, nil
  233. }
  234. func createRawConfigFromServiceAccount(
  235. sa *models.ServiceAccount,
  236. clusterID uint,
  237. updateTokenCache UpdateTokenCacheFunc,
  238. ) (*api.Config, error) {
  239. apiConfig := &api.Config{}
  240. var cluster *models.Cluster = nil
  241. // find the cluster within the ServiceAccount configuration
  242. for _, _cluster := range sa.Clusters {
  243. if _cluster.ID == clusterID {
  244. cluster = &_cluster
  245. }
  246. }
  247. if cluster == nil {
  248. return nil, errors.New("cluster not found")
  249. }
  250. clusterMap := make(map[string]*api.Cluster)
  251. clusterMap[cluster.Name] = &api.Cluster{
  252. LocationOfOrigin: cluster.LocationOfOrigin,
  253. Server: cluster.Server,
  254. TLSServerName: cluster.TLSServerName,
  255. InsecureSkipTLSVerify: cluster.InsecureSkipTLSVerify,
  256. CertificateAuthorityData: cluster.CertificateAuthorityData,
  257. }
  258. // construct the auth infos
  259. authInfoName := cluster.Name + "-" + sa.AuthMechanism
  260. authInfoMap := make(map[string]*api.AuthInfo)
  261. authInfoMap[authInfoName] = &api.AuthInfo{
  262. LocationOfOrigin: sa.LocationOfOrigin,
  263. Impersonate: sa.Impersonate,
  264. }
  265. if groups := strings.Split(sa.ImpersonateGroups, ","); len(groups) > 0 && groups[0] != "" {
  266. authInfoMap[authInfoName].ImpersonateGroups = groups
  267. }
  268. switch sa.AuthMechanism {
  269. case models.X509:
  270. authInfoMap[authInfoName].ClientCertificateData = sa.ClientCertificateData
  271. authInfoMap[authInfoName].ClientKeyData = sa.ClientKeyData
  272. case models.Basic:
  273. authInfoMap[authInfoName].Username = sa.Username
  274. authInfoMap[authInfoName].Password = sa.Password
  275. case models.Bearer:
  276. authInfoMap[authInfoName].Token = sa.Token
  277. case models.OIDC:
  278. authInfoMap[authInfoName].AuthProvider = &api.AuthProviderConfig{
  279. Name: "oidc",
  280. Config: map[string]string{
  281. "idp-issuer-url": sa.OIDCIssuerURL,
  282. "client-id": sa.OIDCClientID,
  283. "client-secret": sa.OIDCClientSecret,
  284. "idp-certificate-authority-data": sa.OIDCCertificateAuthorityData,
  285. "id-token": sa.OIDCIDToken,
  286. "refresh-token": sa.OIDCRefreshToken,
  287. },
  288. }
  289. case models.GCP:
  290. tok, err := getGCPToken(sa, updateTokenCache)
  291. if err != nil {
  292. return nil, err
  293. }
  294. // add this as a bearer token
  295. authInfoMap[authInfoName].Token = tok
  296. case models.AWS:
  297. tok, err := getAWSToken(sa, updateTokenCache)
  298. if err != nil {
  299. return nil, err
  300. }
  301. // add this as a bearer token
  302. authInfoMap[authInfoName].Token = tok
  303. default:
  304. return nil, errors.New("not a supported auth mechanism")
  305. }
  306. // create a context of the cluster name
  307. contextMap := make(map[string]*api.Context)
  308. contextMap[cluster.Name] = &api.Context{
  309. LocationOfOrigin: cluster.LocationOfOrigin,
  310. Cluster: cluster.Name,
  311. AuthInfo: authInfoName,
  312. }
  313. apiConfig.Clusters = clusterMap
  314. apiConfig.AuthInfos = authInfoMap
  315. apiConfig.Contexts = contextMap
  316. apiConfig.CurrentContext = cluster.Name
  317. return apiConfig, nil
  318. }
  319. func getGCPToken(
  320. sa *models.ServiceAccount,
  321. updateTokenCache UpdateTokenCacheFunc,
  322. ) (string, error) {
  323. // check the token cache for a non-expired token
  324. if tok := sa.TokenCache.Token; !sa.TokenCache.IsExpired() && tok != "" {
  325. return tok, nil
  326. }
  327. creds, err := google.CredentialsFromJSON(
  328. context.Background(),
  329. sa.GCPKeyData,
  330. "https://www.googleapis.com/auth/cloud-platform",
  331. )
  332. if err != nil {
  333. return "", err
  334. }
  335. tok, err := creds.TokenSource.Token()
  336. if err != nil {
  337. return "", err
  338. }
  339. // update the token cache
  340. updateTokenCache(tok.AccessToken, tok.Expiry)
  341. return tok.AccessToken, nil
  342. }
  343. func getAWSToken(
  344. sa *models.ServiceAccount,
  345. updateTokenCache UpdateTokenCacheFunc,
  346. ) (string, error) {
  347. // check the token cache for a non-expired token
  348. if tok := sa.TokenCache.Token; !sa.TokenCache.IsExpired() && tok != "" {
  349. return tok, nil
  350. }
  351. generator, err := token.NewGenerator(false, false)
  352. if err != nil {
  353. return "", err
  354. }
  355. sess, err := session.NewSessionWithOptions(session.Options{
  356. SharedConfigState: session.SharedConfigEnable,
  357. Config: aws.Config{
  358. Credentials: credentials.NewStaticCredentials(
  359. sa.AWSAccessKeyID,
  360. sa.AWSSecretAccessKey,
  361. "",
  362. ),
  363. },
  364. })
  365. if err != nil {
  366. return "", err
  367. }
  368. tok, err := generator.GetWithOptions(&token.GetTokenOptions{
  369. Session: sess,
  370. ClusterID: sa.AWSClusterID,
  371. })
  372. if err != nil {
  373. return "", err
  374. }
  375. updateTokenCache(tok.Token, tok.Expiration)
  376. return tok.Token, nil
  377. }
  378. // GetRestrictedClientConfigFromBytes returns a clientcmd.ClientConfig from a raw kubeconfig,
  379. // a context name, and the set of allowed contexts.
  380. func GetRestrictedClientConfigFromBytes(
  381. bytes []byte,
  382. contextName string,
  383. allowedContexts []string,
  384. ) (clientcmd.ClientConfig, error) {
  385. config, err := clientcmd.NewClientConfigFromBytes(bytes)
  386. if err != nil {
  387. return nil, err
  388. }
  389. rawConf, err := config.RawConfig()
  390. if err != nil {
  391. return nil, err
  392. }
  393. // grab a copy to get the pointer and set clusters, authinfos, and contexts to empty
  394. copyConf := rawConf.DeepCopy()
  395. copyConf.Clusters = make(map[string]*api.Cluster)
  396. copyConf.AuthInfos = make(map[string]*api.AuthInfo)
  397. copyConf.Contexts = make(map[string]*api.Context)
  398. copyConf.CurrentContext = contextName
  399. // put allowed clusters in a map
  400. aContextMap := CreateAllowedContextMap(allowedContexts)
  401. context, ok := rawConf.Contexts[contextName]
  402. if ok {
  403. userName := context.AuthInfo
  404. clusterName := context.Cluster
  405. authInfo, userFound := rawConf.AuthInfos[userName]
  406. cluster, clusterFound := rawConf.Clusters[clusterName]
  407. // make sure the cluster is "allowed"
  408. _, isAllowed := aContextMap[contextName]
  409. if userFound && clusterFound && isAllowed {
  410. copyConf.Clusters[clusterName] = cluster
  411. copyConf.AuthInfos[userName] = authInfo
  412. copyConf.Contexts[contextName] = context
  413. }
  414. }
  415. // validate the copyConf and create a ClientConfig
  416. err = clientcmd.Validate(*copyConf)
  417. if err != nil {
  418. return nil, err
  419. }
  420. clientConf := clientcmd.NewDefaultClientConfig(*copyConf, &clientcmd.ConfigOverrides{})
  421. return clientConf, nil
  422. }
  423. // GetContextsFromBytes converts a raw string to a set of Contexts
  424. // by unmarshaling and calling toContexts
  425. func GetContextsFromBytes(bytes []byte, allowedContexts []string) ([]models.Context, error) {
  426. config, err := clientcmd.NewClientConfigFromBytes(bytes)
  427. if err != nil {
  428. return nil, err
  429. }
  430. rawConf, err := config.RawConfig()
  431. if err != nil {
  432. return nil, err
  433. }
  434. err = clientcmd.Validate(rawConf)
  435. if err != nil {
  436. return nil, err
  437. }
  438. contexts := toContexts(&rawConf, allowedContexts)
  439. return contexts, nil
  440. }
  441. func toContexts(rawConf *api.Config, allowedContexts []string) []models.Context {
  442. contexts := make([]models.Context, 0)
  443. // put allowed clusters in map
  444. aContextMap := CreateAllowedContextMap(allowedContexts)
  445. // iterate through contexts and switch on selected
  446. for name, context := range rawConf.Contexts {
  447. _, isAllowed := aContextMap[name]
  448. _, userFound := rawConf.AuthInfos[context.AuthInfo]
  449. cluster, clusterFound := rawConf.Clusters[context.Cluster]
  450. if userFound && clusterFound && isAllowed {
  451. contexts = append(contexts, models.Context{
  452. Name: name,
  453. Server: cluster.Server,
  454. Cluster: context.Cluster,
  455. User: context.AuthInfo,
  456. Selected: true,
  457. })
  458. } else if userFound && clusterFound {
  459. contexts = append(contexts, models.Context{
  460. Name: name,
  461. Server: cluster.Server,
  462. Cluster: context.Cluster,
  463. User: context.AuthInfo,
  464. Selected: false,
  465. })
  466. }
  467. }
  468. return contexts
  469. }
  470. // CreateAllowedContextMap creates a dummy map from context name to context name
  471. func CreateAllowedContextMap(contexts []string) map[string]string {
  472. aContextMap := make(map[string]string)
  473. for _, context := range contexts {
  474. aContextMap[context] = context
  475. }
  476. return aContextMap
  477. }