auth.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. package docker
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "io/ioutil"
  8. "net/url"
  9. "os"
  10. "path/filepath"
  11. "regexp"
  12. "strings"
  13. "time"
  14. api "github.com/porter-dev/porter/api/client"
  15. "github.com/porter-dev/porter/api/types"
  16. "k8s.io/client-go/util/homedir"
  17. )
  18. // AuthEntry is a stored token for registry access with an expiration time.
  19. type AuthEntry struct {
  20. AuthorizationToken string
  21. RequestedAt time.Time
  22. ExpiresAt time.Time
  23. ProxyEndpoint string
  24. }
  25. // IsValid checks if AuthEntry is still valid at runtime. AuthEntries expire at 1/2 of their original
  26. // requested window.
  27. func (authEntry *AuthEntry) IsValid(testTime time.Time) bool {
  28. validWindow := authEntry.ExpiresAt.Sub(authEntry.RequestedAt)
  29. refreshTime := authEntry.ExpiresAt.Add(-1 * validWindow / time.Duration(2))
  30. return testTime.Before(refreshTime)
  31. }
  32. // CredentialsCache is a simple interface for getting/setting auth credentials
  33. // so that we don't request new tokens when previous ones haven't expired
  34. type CredentialsCache interface {
  35. Get(registry string) *AuthEntry
  36. Set(registry string, entry *AuthEntry)
  37. List() []*AuthEntry
  38. }
  39. // AuthGetter retrieves
  40. type AuthGetter struct {
  41. Client *api.Client
  42. Cache CredentialsCache
  43. ProjectID uint
  44. }
  45. func (a *AuthGetter) GetCredentials(serverURL string) (user string, secret string, err error) {
  46. if strings.Contains(serverURL, "gcr.io") {
  47. return a.GetGCRCredentials(serverURL, a.ProjectID)
  48. } else if strings.Contains(serverURL, "pkg.dev") {
  49. return a.GetGARCredentials(serverURL, a.ProjectID)
  50. } else if strings.Contains(serverURL, "registry.digitalocean.com") {
  51. return a.GetDOCRCredentials(serverURL, a.ProjectID)
  52. } else if strings.Contains(serverURL, "index.docker.io") {
  53. return a.GetDockerHubCredentials(serverURL, a.ProjectID)
  54. } else if strings.Contains(serverURL, "azurecr.io") {
  55. return a.GetACRCredentials(serverURL, a.ProjectID)
  56. }
  57. return a.GetECRCredentials(serverURL, a.ProjectID)
  58. }
  59. func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
  60. if err != nil {
  61. return "", "", err
  62. }
  63. cachedEntry := a.Cache.Get(serverURL)
  64. var token string
  65. if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  66. token = cachedEntry.AuthorizationToken
  67. } else {
  68. // get a token from the server
  69. tokenResp, err := a.Client.GetGCRAuthorizationToken(context.Background(), projID, &types.GetRegistryGCRTokenRequest{
  70. ServerURL: serverURL,
  71. })
  72. if err != nil {
  73. return "", "", err
  74. }
  75. token = tokenResp.Token
  76. // set the token in cache
  77. a.Cache.Set(serverURL, &AuthEntry{
  78. AuthorizationToken: token,
  79. RequestedAt: time.Now(),
  80. ExpiresAt: *tokenResp.ExpiresAt,
  81. ProxyEndpoint: serverURL,
  82. })
  83. }
  84. return "oauth2accesstoken", token, nil
  85. }
  86. func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user string, secret string, err error) {
  87. if err != nil {
  88. return "", "", err
  89. }
  90. cachedEntry := a.Cache.Get(serverURL)
  91. if !strings.HasPrefix(serverURL, "https://") {
  92. serverURL = "https://" + serverURL
  93. }
  94. parsedURL, err := url.Parse(serverURL)
  95. if err != nil {
  96. return "", "", err
  97. }
  98. serverURL = parsedURL.Host + "/" + strings.Split(parsedURL.Path, "/")[0]
  99. var token string
  100. if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  101. token = cachedEntry.AuthorizationToken
  102. } else {
  103. // get a token from the server
  104. tokenResp, err := a.Client.GetGARAuthorizationToken(context.Background(), projID, &types.GetRegistryGARTokenRequest{
  105. ServerURL: serverURL,
  106. })
  107. if err != nil {
  108. return "", "", err
  109. }
  110. token = tokenResp.Token
  111. // set the token in cache
  112. a.Cache.Set(serverURL, &AuthEntry{
  113. AuthorizationToken: token,
  114. RequestedAt: time.Now(),
  115. ExpiresAt: *tokenResp.ExpiresAt,
  116. ProxyEndpoint: serverURL,
  117. })
  118. }
  119. return "oauth2accesstoken", token, nil
  120. }
  121. func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
  122. cachedEntry := a.Cache.Get(serverURL)
  123. var token string
  124. if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  125. token = cachedEntry.AuthorizationToken
  126. } else {
  127. // get a token from the server
  128. tokenResp, err := a.Client.GetDOCRAuthorizationToken(context.Background(), projID, &types.GetRegistryGCRTokenRequest{
  129. ServerURL: serverURL,
  130. })
  131. if err != nil {
  132. return "", "", err
  133. }
  134. token = tokenResp.Token
  135. if t := *tokenResp.ExpiresAt; len(token) > 0 && !t.IsZero() {
  136. // set the token in cache
  137. a.Cache.Set(serverURL, &AuthEntry{
  138. AuthorizationToken: token,
  139. RequestedAt: time.Now(),
  140. ExpiresAt: t,
  141. ProxyEndpoint: serverURL,
  142. })
  143. }
  144. }
  145. return token, token, nil
  146. }
  147. var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
  148. func (a *AuthGetter) GetECRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
  149. // parse the server url for region
  150. matches := ecrPattern.FindStringSubmatch(serverURL)
  151. if len(matches) == 0 {
  152. err := fmt.Errorf("only ECR registry URLs are supported")
  153. return "", "", err
  154. } else if len(matches) < 3 {
  155. err := fmt.Errorf("%s is not a valid ECR repository URI", serverURL)
  156. return "", "", err
  157. }
  158. cachedEntry := a.Cache.Get(serverURL)
  159. var token string
  160. if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  161. token = cachedEntry.AuthorizationToken
  162. } else {
  163. // get a token from the server
  164. tokenResp, err := a.Client.GetECRAuthorizationToken(context.Background(), projID, &types.GetRegistryECRTokenRequest{
  165. Region: matches[3],
  166. AccountID: matches[1],
  167. })
  168. if err != nil {
  169. return "", "", err
  170. }
  171. token = tokenResp.Token
  172. // set the token in cache
  173. a.Cache.Set(serverURL, &AuthEntry{
  174. AuthorizationToken: token,
  175. RequestedAt: time.Now(),
  176. ExpiresAt: *tokenResp.ExpiresAt,
  177. ProxyEndpoint: serverURL,
  178. })
  179. }
  180. return decodeDockerToken(token)
  181. }
  182. func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (user string, secret string, err error) {
  183. cachedEntry := a.Cache.Get(serverURL)
  184. var token string
  185. if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  186. token = cachedEntry.AuthorizationToken
  187. } else {
  188. // get a token from the server
  189. tokenResp, err := a.Client.GetDockerhubAuthorizationToken(context.Background(), projID)
  190. if err != nil {
  191. return "", "", err
  192. }
  193. token = tokenResp.Token
  194. // set the token in cache
  195. a.Cache.Set(serverURL, &AuthEntry{
  196. AuthorizationToken: token,
  197. RequestedAt: time.Now(),
  198. ExpiresAt: *tokenResp.ExpiresAt,
  199. ProxyEndpoint: serverURL,
  200. })
  201. }
  202. return decodeDockerToken(token)
  203. }
  204. func (a *AuthGetter) GetACRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
  205. cachedEntry := a.Cache.Get(serverURL)
  206. var token string
  207. if false && cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  208. fmt.Println("reuse token: ", token)
  209. token = cachedEntry.AuthorizationToken
  210. } else {
  211. // get a token from the server
  212. tokenResp, err := a.Client.GetACRAuthorizationToken(context.Background(), projID)
  213. if err != nil {
  214. return "", "", err
  215. }
  216. token = tokenResp.Token
  217. // set the token in cache
  218. a.Cache.Set(serverURL, &AuthEntry{
  219. AuthorizationToken: token,
  220. RequestedAt: time.Now(),
  221. ExpiresAt: *tokenResp.ExpiresAt,
  222. ProxyEndpoint: serverURL,
  223. })
  224. }
  225. return decodeDockerToken(token)
  226. }
  227. func decodeDockerToken(token string) (string, string, error) {
  228. decodedToken, err := base64.StdEncoding.DecodeString(token)
  229. if err != nil {
  230. return "", "", fmt.Errorf("Invalid token: %v", err)
  231. }
  232. parts := strings.SplitN(string(decodedToken), ":", 2)
  233. if len(parts) < 2 {
  234. return "", "", fmt.Errorf("Invalid token: expected two parts, got %d", len(parts))
  235. }
  236. return parts[0], parts[1], nil
  237. }
  238. type FileCredentialCache struct {
  239. path string
  240. filename string
  241. cachePrefixKey string
  242. }
  243. const registryCacheVersion = "1.0"
  244. type RegistryCache struct {
  245. Registries map[string]*AuthEntry
  246. Version string
  247. }
  248. type fileCredentialCache struct {
  249. path string
  250. filename string
  251. cachePrefixKey string
  252. }
  253. func newRegistryCache() *RegistryCache {
  254. return &RegistryCache{
  255. Registries: make(map[string]*AuthEntry),
  256. Version: registryCacheVersion,
  257. }
  258. }
  259. // NewFileCredentialsCache returns a new file credentials cache.
  260. //
  261. // path is used for temporary files during save, and filename should be a relative filename
  262. // in the same directory where the cache is serialized and deserialized.
  263. //
  264. // cachePrefixKey is used for scoping credentials for a given credential cache (i.e. region and
  265. // accessKey).
  266. func NewFileCredentialsCache() CredentialsCache {
  267. home := homedir.HomeDir()
  268. path := filepath.Join(home, ".porter")
  269. if _, err := os.Stat(path); err != nil {
  270. os.MkdirAll(path, 0o700)
  271. }
  272. return &FileCredentialCache{path: path, filename: "cache.json"}
  273. }
  274. func (f *FileCredentialCache) Get(registry string) *AuthEntry {
  275. registryCache := f.init()
  276. return registryCache.Registries[f.cachePrefixKey+registry]
  277. }
  278. func (f *FileCredentialCache) Set(registry string, entry *AuthEntry) {
  279. registryCache := f.init()
  280. registryCache.Registries[f.cachePrefixKey+registry] = entry
  281. f.save(registryCache)
  282. }
  283. func (f *FileCredentialCache) Clear() {
  284. os.Remove(f.fullFilePath())
  285. }
  286. // List returns all of the available AuthEntries (regardless of prefix)
  287. func (f *FileCredentialCache) List() []*AuthEntry {
  288. registryCache := f.init()
  289. // optimize allocation for copy
  290. entries := make([]*AuthEntry, 0, len(registryCache.Registries))
  291. for _, entry := range registryCache.Registries {
  292. entries = append(entries, entry)
  293. }
  294. return entries
  295. }
  296. func (f *FileCredentialCache) fullFilePath() string {
  297. return filepath.Join(f.path, f.filename)
  298. }
  299. // Saves credential cache to disk. This writes to a temporary file first, then moves the file to the config location.
  300. // This eliminates from reading partially written credential files, and reduces (but does not eliminate) concurrent
  301. // file access. There is not guarantee here for handling multiple writes at once since there is no out of process locking.
  302. func (f *FileCredentialCache) save(registryCache *RegistryCache) error {
  303. file, err := ioutil.TempFile(f.path, ".config.json.tmp")
  304. if err != nil {
  305. return err
  306. }
  307. buff, err := json.MarshalIndent(registryCache, "", " ")
  308. if err != nil {
  309. file.Close()
  310. os.Remove(file.Name())
  311. return err
  312. }
  313. _, err = file.Write(buff)
  314. if err != nil {
  315. file.Close()
  316. os.Remove(file.Name())
  317. return err
  318. }
  319. file.Close()
  320. // note this is only atomic when relying on linux syscalls
  321. os.Rename(file.Name(), f.fullFilePath())
  322. return err
  323. }
  324. func (f *FileCredentialCache) init() *RegistryCache {
  325. registryCache, err := f.load()
  326. if err != nil {
  327. f.Clear()
  328. registryCache = newRegistryCache()
  329. }
  330. return registryCache
  331. }
  332. // Loading a cache from disk will return errors for malformed or incompatible cache files.
  333. func (f *FileCredentialCache) load() (*RegistryCache, error) {
  334. registryCache := newRegistryCache()
  335. file, err := os.Open(f.fullFilePath())
  336. if os.IsNotExist(err) {
  337. return registryCache, nil
  338. }
  339. if err != nil {
  340. return nil, err
  341. }
  342. defer file.Close()
  343. if err = json.NewDecoder(file).Decode(&registryCache); err != nil {
  344. return nil, err
  345. }
  346. return registryCache, nil
  347. }