auth.go 9.0 KB

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