| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- /*
- Copyright 2020 Docker Compose CLI authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package login
- import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "os"
- "strings"
- "github.com/pkg/errors"
- )
- const (
- // AzurePublicCloudName is the moniker of the Azure public cloud
- AzurePublicCloudName = "AzureCloud"
- // AcrSuffixKey is the well-known name of the DNS suffix for Azure Container Registries
- AcrSuffixKey = "acrLoginServer"
- // 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
- CloudMetadataURLVar = "ARM_CLOUD_METADATA_URL"
- // DefaultCloudMetadataURL is the URL of the cloud metadata service maintained by Azure public cloud
- DefaultCloudMetadataURL = "https://management.azure.com/metadata/endpoints?api-version=2019-05-01"
- )
- // CloudEnvironmentService exposed metadata about Azure cloud environments
- type CloudEnvironmentService interface {
- Get(name string) (CloudEnvironment, error)
- }
- type cloudEnvironmentService struct {
- cloudEnvironments map[string]CloudEnvironment
- cloudMetadataURL string
- // True if we have queried the cloud metadata endpoint already.
- // We do it only once per CLI invocation.
- metadataQueried bool
- }
- var (
- // CloudEnvironments is the default instance of the CloudEnvironmentService
- CloudEnvironments CloudEnvironmentService
- )
- func init() {
- CloudEnvironments = newCloudEnvironmentService()
- }
- // CloudEnvironmentAuthentication data for logging into, and obtaining tokens for, Azure sovereign clouds
- type CloudEnvironmentAuthentication struct {
- LoginEndpoint string `json:"loginEndpoint"`
- Audiences []string `json:"audiences"`
- Tenant string `json:"tenant"`
- }
- // CloudEnvironment describes Azure sovereign cloud instance (e.g. Azure public, Azure US government, Azure China etc.)
- type CloudEnvironment struct {
- Name string `json:"name"`
- Authentication CloudEnvironmentAuthentication `json:"authentication"`
- ResourceManagerURL string `json:"resourceManager"`
- Suffixes map[string]string `json:"suffixes"`
- }
- func newCloudEnvironmentService() *cloudEnvironmentService {
- retval := cloudEnvironmentService{
- metadataQueried: false,
- }
- retval.resetCloudMetadata()
- return &retval
- }
- func (ces *cloudEnvironmentService) Get(name string) (CloudEnvironment, error) {
- if ce, present := ces.cloudEnvironments[name]; present {
- return ce, nil
- }
- if !ces.metadataQueried {
- ces.metadataQueried = true
- if ces.cloudMetadataURL == "" {
- ces.cloudMetadataURL = os.Getenv(CloudMetadataURLVar)
- if _, err := url.ParseRequestURI(ces.cloudMetadataURL); err != nil {
- ces.cloudMetadataURL = DefaultCloudMetadataURL
- }
- }
- res, err := http.Get(ces.cloudMetadataURL)
- if err != nil {
- return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
- }
- if res.StatusCode != 200 {
- return CloudEnvironment{}, errors.Errorf("Cloud metadata retrieval from '%s' failed: server response was '%s'", ces.cloudMetadataURL, res.Status)
- }
- bytes, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
- }
- if err = ces.applyCloudMetadata(bytes); err != nil {
- return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
- }
- }
- if ce, present := ces.cloudEnvironments[name]; present {
- return ce, nil
- }
- return CloudEnvironment{}, errors.Errorf("Cloud environment '%s' was not found", name)
- }
- func (ces *cloudEnvironmentService) applyCloudMetadata(jsonBytes []byte) error {
- input := []CloudEnvironment{}
- if err := json.Unmarshal(jsonBytes, &input); err != nil {
- return err
- }
- newEnvironments := make(map[string]CloudEnvironment, len(input))
- // If _any_ of the submitted data is invalid, we bail out.
- for _, e := range input {
- if len(e.Name) == 0 {
- return errors.New("Azure cloud environment metadata is invalid (an environment with no name has been encountered)")
- }
- e.normalizeURLs()
- if _, err := url.ParseRequestURI(e.Authentication.LoginEndpoint); err != nil {
- return errors.Errorf("Metadata of cloud environment '%s' has invalid login endpoint URL: %s", e.Name, e.Authentication.LoginEndpoint)
- }
- if _, err := url.ParseRequestURI(e.ResourceManagerURL); err != nil {
- return errors.Errorf("Metadata of cloud environment '%s' has invalid resource manager URL: %s", e.Name, e.ResourceManagerURL)
- }
- if len(e.Authentication.Audiences) == 0 {
- return errors.Errorf("Metadata of cloud environment '%s' is invalid (no authentication audiences)", e.Name)
- }
- newEnvironments[e.Name] = e
- }
- for name, e := range newEnvironments {
- ces.cloudEnvironments[name] = e
- }
- return nil
- }
- func (ces *cloudEnvironmentService) resetCloudMetadata() {
- azurePublicCloud := CloudEnvironment{
- Name: AzurePublicCloudName,
- Authentication: CloudEnvironmentAuthentication{
- LoginEndpoint: "https://login.microsoftonline.com",
- Audiences: []string{
- "https://management.core.windows.net",
- "https://management.azure.com",
- },
- Tenant: "common",
- },
- ResourceManagerURL: "https://management.azure.com",
- Suffixes: map[string]string{
- AcrSuffixKey: "azurecr.io",
- },
- }
- azureChinaCloud := CloudEnvironment{
- Name: "AzureChinaCloud",
- Authentication: CloudEnvironmentAuthentication{
- LoginEndpoint: "https://login.chinacloudapi.cn",
- Audiences: []string{
- "https://management.core.chinacloudapi.cn",
- "https://management.chinacloudapi.cn",
- },
- Tenant: "common",
- },
- ResourceManagerURL: "https://management.chinacloudapi.cn",
- Suffixes: map[string]string{
- AcrSuffixKey: "azurecr.cn",
- },
- }
- azureUSGovernment := CloudEnvironment{
- Name: "AzureUSGovernment",
- Authentication: CloudEnvironmentAuthentication{
- LoginEndpoint: "https://login.microsoftonline.us",
- Audiences: []string{
- "https://management.core.usgovcloudapi.net",
- "https://management.usgovcloudapi.net",
- },
- Tenant: "common",
- },
- ResourceManagerURL: "https://management.usgovcloudapi.net",
- Suffixes: map[string]string{
- AcrSuffixKey: "azurecr.us",
- },
- }
- azureGermanCloud := CloudEnvironment{
- Name: "AzureGermanCloud",
- Authentication: CloudEnvironmentAuthentication{
- LoginEndpoint: "https://login.microsoftonline.de",
- Audiences: []string{
- "https://management.core.cloudapi.de",
- "https://management.microsoftazure.de",
- },
- Tenant: "common",
- },
- ResourceManagerURL: "https://management.microsoftazure.de",
- // There is no separate container registry suffix for German cloud
- Suffixes: map[string]string{},
- }
- ces.cloudEnvironments = map[string]CloudEnvironment{
- azurePublicCloud.Name: azurePublicCloud,
- azureChinaCloud.Name: azureChinaCloud,
- azureUSGovernment.Name: azureUSGovernment,
- azureGermanCloud.Name: azureGermanCloud,
- }
- }
- // GetTenantQueryURL returns an URL that can be used to fetch the list of Azure Active Directory tenants from a given cloud environment
- func (ce *CloudEnvironment) GetTenantQueryURL() string {
- tenantURL := ce.ResourceManagerURL + "/tenants?api-version=2019-11-01"
- return tenantURL
- }
- // GetTokenScope returns a token scope that fits Docker CLI Azure management API usage
- func (ce *CloudEnvironment) GetTokenScope() string {
- scope := "offline_access " + ce.ResourceManagerURL + "/.default"
- return scope
- }
- // GetAuthorizeRequestFormat returns a string format that can be used to construct authorization code request in an OAuth2 flow.
- // The URL uses login endpoint appropriate for given cloud environment.
- func (ce *CloudEnvironment) GetAuthorizeRequestFormat() string {
- 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"
- return authorizeFormat
- }
- // GetTokenRequestFormat returns a string format that can be used to construct a security token request against Azure Active Directory
- func (ce *CloudEnvironment) GetTokenRequestFormat() string {
- tokenEndpoint := ce.Authentication.LoginEndpoint + "/%s/oauth2/v2.0/token"
- return tokenEndpoint
- }
- func (ce *CloudEnvironment) normalizeURLs() {
- ce.ResourceManagerURL = removeTrailingSlash(ce.ResourceManagerURL)
- ce.Authentication.LoginEndpoint = removeTrailingSlash(ce.Authentication.LoginEndpoint)
- for i, s := range ce.Authentication.Audiences {
- ce.Authentication.Audiences[i] = removeTrailingSlash(s)
- }
- }
- func removeTrailingSlash(s string) string {
- return strings.TrimSuffix(s, "/")
- }
|