cloud_environment.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package login
  14. import (
  15. "encoding/json"
  16. "fmt"
  17. "io/ioutil"
  18. "net/http"
  19. "net/url"
  20. "os"
  21. "strings"
  22. "github.com/pkg/errors"
  23. )
  24. const (
  25. // AzurePublicCloudName is the moniker of the Azure public cloud
  26. AzurePublicCloudName = "AzureCloud"
  27. // AcrSuffixKey is the well-known name of the DNS suffix for Azure Container Registries
  28. AcrSuffixKey = "acrLoginServer"
  29. // CloudMetadataURLVar is the name of the environment variable that (if defined), points to a URL that should be used by Docker CLI to retrieve cloud metadata
  30. CloudMetadataURLVar = "ARM_CLOUD_METADATA_URL"
  31. // DefaultCloudMetadataURL is the URL of the cloud metadata service maintained by Azure public cloud
  32. DefaultCloudMetadataURL = "https://management.azure.com/metadata/endpoints?api-version=2019-05-01"
  33. )
  34. // CloudEnvironmentService exposed metadata about Azure cloud environments
  35. type CloudEnvironmentService interface {
  36. Get(name string) (CloudEnvironment, error)
  37. }
  38. type cloudEnvironmentService struct {
  39. cloudEnvironments map[string]CloudEnvironment
  40. cloudMetadataURL string
  41. // True if we have queried the cloud metadata endpoint already.
  42. // We do it only once per CLI invocation.
  43. metadataQueried bool
  44. }
  45. var (
  46. // CloudEnvironments is the default instance of the CloudEnvironmentService
  47. CloudEnvironments CloudEnvironmentService
  48. )
  49. func init() {
  50. CloudEnvironments = newCloudEnvironmentService()
  51. }
  52. // CloudEnvironmentAuthentication data for logging into, and obtaining tokens for, Azure sovereign clouds
  53. type CloudEnvironmentAuthentication struct {
  54. LoginEndpoint string `json:"loginEndpoint"`
  55. Audiences []string `json:"audiences"`
  56. Tenant string `json:"tenant"`
  57. }
  58. // CloudEnvironment describes Azure sovereign cloud instance (e.g. Azure public, Azure US government, Azure China etc.)
  59. type CloudEnvironment struct {
  60. Name string `json:"name"`
  61. Authentication CloudEnvironmentAuthentication `json:"authentication"`
  62. ResourceManagerURL string `json:"resourceManager"`
  63. Suffixes map[string]string `json:"suffixes"`
  64. }
  65. func newCloudEnvironmentService() *cloudEnvironmentService {
  66. retval := cloudEnvironmentService{
  67. metadataQueried: false,
  68. }
  69. retval.resetCloudMetadata()
  70. return &retval
  71. }
  72. func (ces *cloudEnvironmentService) Get(name string) (CloudEnvironment, error) {
  73. if ce, present := ces.cloudEnvironments[name]; present {
  74. return ce, nil
  75. }
  76. if !ces.metadataQueried {
  77. ces.metadataQueried = true
  78. if ces.cloudMetadataURL == "" {
  79. ces.cloudMetadataURL = os.Getenv(CloudMetadataURLVar)
  80. if _, err := url.ParseRequestURI(ces.cloudMetadataURL); err != nil {
  81. ces.cloudMetadataURL = DefaultCloudMetadataURL
  82. }
  83. }
  84. res, err := http.Get(ces.cloudMetadataURL)
  85. if err != nil {
  86. return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
  87. }
  88. if res.StatusCode != 200 {
  89. return CloudEnvironment{}, errors.Errorf("Cloud metadata retrieval from '%s' failed: server response was '%s'", ces.cloudMetadataURL, res.Status)
  90. }
  91. bytes, err := ioutil.ReadAll(res.Body)
  92. if err != nil {
  93. return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
  94. }
  95. if err = ces.applyCloudMetadata(bytes); err != nil {
  96. return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
  97. }
  98. }
  99. if ce, present := ces.cloudEnvironments[name]; present {
  100. return ce, nil
  101. }
  102. return CloudEnvironment{}, errors.Errorf("Cloud environment '%s' was not found", name)
  103. }
  104. func (ces *cloudEnvironmentService) applyCloudMetadata(jsonBytes []byte) error {
  105. input := []CloudEnvironment{}
  106. if err := json.Unmarshal(jsonBytes, &input); err != nil {
  107. return err
  108. }
  109. newEnvironments := make(map[string]CloudEnvironment, len(input))
  110. // If _any_ of the submitted data is invalid, we bail out.
  111. for _, e := range input {
  112. if len(e.Name) == 0 {
  113. return errors.New("Azure cloud environment metadata is invalid (an environment with no name has been encountered)")
  114. }
  115. e.normalizeURLs()
  116. if _, err := url.ParseRequestURI(e.Authentication.LoginEndpoint); err != nil {
  117. return errors.Errorf("Metadata of cloud environment '%s' has invalid login endpoint URL: %s", e.Name, e.Authentication.LoginEndpoint)
  118. }
  119. if _, err := url.ParseRequestURI(e.ResourceManagerURL); err != nil {
  120. return errors.Errorf("Metadata of cloud environment '%s' has invalid resource manager URL: %s", e.Name, e.ResourceManagerURL)
  121. }
  122. if len(e.Authentication.Audiences) == 0 {
  123. return errors.Errorf("Metadata of cloud environment '%s' is invalid (no authentication audiences)", e.Name)
  124. }
  125. newEnvironments[e.Name] = e
  126. }
  127. for name, e := range newEnvironments {
  128. ces.cloudEnvironments[name] = e
  129. }
  130. return nil
  131. }
  132. func (ces *cloudEnvironmentService) resetCloudMetadata() {
  133. azurePublicCloud := CloudEnvironment{
  134. Name: AzurePublicCloudName,
  135. Authentication: CloudEnvironmentAuthentication{
  136. LoginEndpoint: "https://login.microsoftonline.com",
  137. Audiences: []string{
  138. "https://management.core.windows.net",
  139. "https://management.azure.com",
  140. },
  141. Tenant: "common",
  142. },
  143. ResourceManagerURL: "https://management.azure.com",
  144. Suffixes: map[string]string{
  145. AcrSuffixKey: "azurecr.io",
  146. },
  147. }
  148. azureChinaCloud := CloudEnvironment{
  149. Name: "AzureChinaCloud",
  150. Authentication: CloudEnvironmentAuthentication{
  151. LoginEndpoint: "https://login.chinacloudapi.cn",
  152. Audiences: []string{
  153. "https://management.core.chinacloudapi.cn",
  154. "https://management.chinacloudapi.cn",
  155. },
  156. Tenant: "common",
  157. },
  158. ResourceManagerURL: "https://management.chinacloudapi.cn",
  159. Suffixes: map[string]string{
  160. AcrSuffixKey: "azurecr.cn",
  161. },
  162. }
  163. azureUSGovernment := CloudEnvironment{
  164. Name: "AzureUSGovernment",
  165. Authentication: CloudEnvironmentAuthentication{
  166. LoginEndpoint: "https://login.microsoftonline.us",
  167. Audiences: []string{
  168. "https://management.core.usgovcloudapi.net",
  169. "https://management.usgovcloudapi.net",
  170. },
  171. Tenant: "common",
  172. },
  173. ResourceManagerURL: "https://management.usgovcloudapi.net",
  174. Suffixes: map[string]string{
  175. AcrSuffixKey: "azurecr.us",
  176. },
  177. }
  178. azureGermanCloud := CloudEnvironment{
  179. Name: "AzureGermanCloud",
  180. Authentication: CloudEnvironmentAuthentication{
  181. LoginEndpoint: "https://login.microsoftonline.de",
  182. Audiences: []string{
  183. "https://management.core.cloudapi.de",
  184. "https://management.microsoftazure.de",
  185. },
  186. Tenant: "common",
  187. },
  188. ResourceManagerURL: "https://management.microsoftazure.de",
  189. // There is no separate container registry suffix for German cloud
  190. Suffixes: map[string]string{},
  191. }
  192. ces.cloudEnvironments = map[string]CloudEnvironment{
  193. azurePublicCloud.Name: azurePublicCloud,
  194. azureChinaCloud.Name: azureChinaCloud,
  195. azureUSGovernment.Name: azureUSGovernment,
  196. azureGermanCloud.Name: azureGermanCloud,
  197. }
  198. }
  199. // GetTenantQueryURL returns an URL that can be used to fetch the list of Azure Active Directory tenants from a given cloud environment
  200. func (ce *CloudEnvironment) GetTenantQueryURL() string {
  201. tenantURL := ce.ResourceManagerURL + "/tenants?api-version=2019-11-01"
  202. return tenantURL
  203. }
  204. // GetTokenScope returns a token scope that fits Docker CLI Azure management API usage
  205. func (ce *CloudEnvironment) GetTokenScope() string {
  206. scope := "offline_access " + ce.ResourceManagerURL + "/.default"
  207. return scope
  208. }
  209. // GetAuthorizeRequestFormat returns a string format that can be used to construct authorization code request in an OAuth2 flow.
  210. // The URL uses login endpoint appropriate for given cloud environment.
  211. func (ce *CloudEnvironment) GetAuthorizeRequestFormat() string {
  212. authorizeFormat := ce.Authentication.LoginEndpoint + "/organizations/oauth2/v2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s&prompt=select_account&response_mode=query&scope=%s"
  213. return authorizeFormat
  214. }
  215. // GetTokenRequestFormat returns a string format that can be used to construct a security token request against Azure Active Directory
  216. func (ce *CloudEnvironment) GetTokenRequestFormat() string {
  217. tokenEndpoint := ce.Authentication.LoginEndpoint + "/%s/oauth2/v2.0/token"
  218. return tokenEndpoint
  219. }
  220. func (ce *CloudEnvironment) normalizeURLs() {
  221. ce.ResourceManagerURL = removeTrailingSlash(ce.ResourceManagerURL)
  222. ce.Authentication.LoginEndpoint = removeTrailingSlash(ce.Authentication.LoginEndpoint)
  223. for i, s := range ce.Authentication.Audiences {
  224. ce.Authentication.Audiences[i] = removeTrailingSlash(s)
  225. }
  226. }
  227. func removeTrailingSlash(s string) string {
  228. return strings.TrimSuffix(s, "/")
  229. }