compose/aci/login/cloud_environment.go
Karol Zadora-Przylecki cc649d958c Add Azure sovereign cloud support
Signed-off-by: Karol Zadora-Przylecki <karolz@microsoft.com>
2021-02-15 18:38:59 -08:00

275 lines
9.0 KiB
Go

/*
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, "/")
}