auth.go 11 KB

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