auth.go 12 KB

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