bump azure sdk

v1.3.0 of azidentity introduces support to workload identity.

Signed-off-by: Flavian Missi <fmissi@redhat.com>
This commit is contained in:
Flavian Missi
2023-05-11 15:23:47 +02:00
parent 8e29e870a4
commit 7caf058a65
169 changed files with 2892 additions and 1433 deletions

View File

@@ -10,6 +10,7 @@ import (
"net/url"
"reflect"
"strings"
"sync"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
@@ -27,27 +28,21 @@ const (
)
// manager provides an internal cache. It is defined to allow faking the cache in tests.
// In all production use it is a *storage.Manager.
// In production it's a *storage.Manager or *storage.PartitionedManager.
type manager interface {
Read(ctx context.Context, authParameters authority.AuthParams, account shared.Account) (storage.TokenResponse, error)
Write(authParameters authority.AuthParams, tokenResponse accesstokens.TokenResponse) (shared.Account, error)
cache.Serializer
Read(context.Context, authority.AuthParams) (storage.TokenResponse, error)
Write(authority.AuthParams, accesstokens.TokenResponse) (shared.Account, error)
}
// accountManager is a manager that also caches accounts. In production it's a *storage.Manager.
type accountManager interface {
manager
AllAccounts() []shared.Account
Account(homeAccountID string) shared.Account
RemoveAccount(account shared.Account, clientID string)
}
// partitionedManager provides an internal cache. It is defined to allow faking the cache in tests.
// In all production use it is a *storage.PartitionedManager.
type partitionedManager interface {
Read(ctx context.Context, authParameters authority.AuthParams) (storage.TokenResponse, error)
Write(authParameters authority.AuthParams, tokenResponse accesstokens.TokenResponse) (shared.Account, error)
}
type noopCacheAccessor struct{}
func (n noopCacheAccessor) Replace(cache cache.Unmarshaler, key string) {}
func (n noopCacheAccessor) Export(cache cache.Marshaler, key string) {}
// AcquireTokenSilentParameters contains the parameters to acquire a token silently (from cache).
type AcquireTokenSilentParameters struct {
Scopes []string
@@ -133,12 +128,14 @@ func NewAuthResult(tokenResponse accesstokens.TokenResponse, account shared.Acco
// Client is a base client that provides access to common methods and primatives that
// can be used by multiple clients.
type Client struct {
Token *oauth.Client
manager manager // *storage.Manager or fakeManager in tests
pmanager partitionedManager // *storage.PartitionedManager or fakeManager in tests
Token *oauth.Client
manager accountManager // *storage.Manager or fakeManager in tests
// pmanager is a partitioned cache for OBO authentication. *storage.PartitionedManager or fakeManager in tests
pmanager manager
AuthParams authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New().
cacheAccessor cache.ExportReplace
AuthParams authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New().
cacheAccessor cache.ExportReplace
cacheAccessorMu *sync.RWMutex
}
// Option is an optional argument to the New constructor.
@@ -210,11 +207,11 @@ func New(clientID string, authorityURI string, token *oauth.Client, options ...O
}
authParams := authority.NewAuthParams(clientID, authInfo)
client := Client{ // Note: Hey, don't even THINK about making Base into *Base. See "design notes" in public.go and confidential.go
Token: token,
AuthParams: authParams,
cacheAccessor: noopCacheAccessor{},
manager: storage.New(token),
pmanager: storage.NewPartitionedManager(token),
Token: token,
AuthParams: authParams,
cacheAccessorMu: &sync.RWMutex{},
manager: storage.New(token),
pmanager: storage.NewPartitionedManager(token),
}
for _, o := range options {
if err = o(&client); err != nil {
@@ -280,11 +277,12 @@ func (b Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, s
}
func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilentParameters) (AuthResult, error) {
// when tenant == "", the caller didn't specify a tenant and WithTenant will use the client's configured tenant
ar := AuthResult{}
// when tenant == "", the caller didn't specify a tenant and WithTenant will choose the client's configured tenant
tenant := silent.TenantID
authParams, err := b.AuthParams.WithTenant(tenant)
if err != nil {
return AuthResult{}, err
return ar, err
}
authParams.Scopes = silent.Scopes
authParams.HomeAccountID = silent.Account.HomeAccountID
@@ -292,52 +290,45 @@ func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilen
authParams.Claims = silent.Claims
authParams.UserAssertion = silent.UserAssertion
var storageTokenResponse storage.TokenResponse
if authParams.AuthorizationType == authority.ATOnBehalfOf {
if s, ok := b.pmanager.(cache.Serializer); ok {
suggestedCacheKey := authParams.CacheKey(silent.IsAppCache)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
storageTokenResponse, err = b.pmanager.Read(ctx, authParams)
if err != nil {
return AuthResult{}, err
}
} else {
if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := authParams.CacheKey(silent.IsAppCache)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
m := b.pmanager
if authParams.AuthorizationType != authority.ATOnBehalfOf {
authParams.AuthorizationType = authority.ATRefreshToken
storageTokenResponse, err = b.manager.Read(ctx, authParams, silent.Account)
if err != nil {
return AuthResult{}, err
}
m = b.manager
}
if b.cacheAccessor != nil {
key := authParams.CacheKey(silent.IsAppCache)
b.cacheAccessorMu.RLock()
err = b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
b.cacheAccessorMu.RUnlock()
}
if err != nil {
return ar, err
}
storageTokenResponse, err := m.Read(ctx, authParams)
if err != nil {
return ar, err
}
// ignore cached access tokens when given claims
if silent.Claims == "" {
result, err := AuthResultFromStorage(storageTokenResponse)
ar, err = AuthResultFromStorage(storageTokenResponse)
if err == nil {
return result, nil
return ar, err
}
}
// redeem a cached refresh token, if available
if reflect.ValueOf(storageTokenResponse.RefreshToken).IsZero() {
return AuthResult{}, errors.New("no token found")
return ar, errors.New("no token found")
}
var cc *accesstokens.Credential
if silent.RequestType == accesstokens.ATConfidential {
cc = silent.Credential
}
token, err := b.Token.Refresh(ctx, silent.RequestType, authParams, cc, storageTokenResponse.RefreshToken)
if err != nil {
return AuthResult{}, err
return ar, err
}
return b.AuthResultFromToken(ctx, authParams, token, true)
}
@@ -405,63 +396,72 @@ func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.Au
if !cacheWrite {
return NewAuthResult(token, shared.Account{})
}
var account shared.Account
var err error
var m manager = b.manager
if authParams.AuthorizationType == authority.ATOnBehalfOf {
if s, ok := b.pmanager.(cache.Serializer); ok {
suggestedCacheKey := token.CacheKey(authParams)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
account, err = b.pmanager.Write(authParams, token)
if err != nil {
return AuthResult{}, err
}
} else {
if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := token.CacheKey(authParams)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
account, err = b.manager.Write(authParams, token)
m = b.pmanager
}
key := token.CacheKey(authParams)
if b.cacheAccessor != nil {
b.cacheAccessorMu.Lock()
defer b.cacheAccessorMu.Unlock()
err := b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return AuthResult{}, err
}
}
return NewAuthResult(token, account)
account, err := m.Write(authParams, token)
if err != nil {
return AuthResult{}, err
}
ar, err := NewAuthResult(token, account)
if err == nil && b.cacheAccessor != nil {
err = b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
}
return ar, err
}
func (b Client) AllAccounts() []shared.Account {
if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := b.AuthParams.CacheKey(false)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
func (b Client) AllAccounts(ctx context.Context) ([]shared.Account, error) {
if b.cacheAccessor != nil {
b.cacheAccessorMu.RLock()
defer b.cacheAccessorMu.RUnlock()
key := b.AuthParams.CacheKey(false)
err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return nil, err
}
}
accounts := b.manager.AllAccounts()
return accounts
return b.manager.AllAccounts(), nil
}
func (b Client) Account(homeAccountID string) shared.Account {
authParams := b.AuthParams // This is a copy, as we dont' have a pointer receiver and .AuthParams is not a pointer.
authParams.AuthorizationType = authority.AccountByID
authParams.HomeAccountID = homeAccountID
if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := b.AuthParams.CacheKey(false)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
func (b Client) Account(ctx context.Context, homeAccountID string) (shared.Account, error) {
if b.cacheAccessor != nil {
b.cacheAccessorMu.RLock()
defer b.cacheAccessorMu.RUnlock()
authParams := b.AuthParams // This is a copy, as we don't have a pointer receiver and .AuthParams is not a pointer.
authParams.AuthorizationType = authority.AccountByID
authParams.HomeAccountID = homeAccountID
key := b.AuthParams.CacheKey(false)
err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return shared.Account{}, err
}
}
account := b.manager.Account(homeAccountID)
return account
return b.manager.Account(homeAccountID), nil
}
// RemoveAccount removes all the ATs, RTs and IDTs from the cache associated with this account.
func (b Client) RemoveAccount(account shared.Account) {
if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := b.AuthParams.CacheKey(false)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
func (b Client) RemoveAccount(ctx context.Context, account shared.Account) error {
if b.cacheAccessor == nil {
b.manager.RemoveAccount(account, b.AuthParams.ClientID)
return nil
}
b.cacheAccessorMu.Lock()
defer b.cacheAccessorMu.Unlock()
key := b.AuthParams.CacheKey(false)
err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return err
}
b.manager.RemoveAccount(account, b.AuthParams.ClientID)
return b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
}

View File

@@ -83,7 +83,7 @@ func isMatchingScopes(scopesOne []string, scopesTwo string) bool {
}
// Read reads a storage token from the cache if it exists.
func (m *Manager) Read(ctx context.Context, authParameters authority.AuthParams, account shared.Account) (TokenResponse, error) {
func (m *Manager) Read(ctx context.Context, authParameters authority.AuthParams) (TokenResponse, error) {
tr := TokenResponse{}
homeAccountID := authParameters.HomeAccountID
realm := authParameters.AuthorityInfo.Tenant
@@ -103,7 +103,8 @@ func (m *Manager) Read(ctx context.Context, authParameters authority.AuthParams,
accessToken := m.readAccessToken(homeAccountID, aliases, realm, clientID, scopes)
tr.AccessToken = accessToken
if account.IsZero() {
if homeAccountID == "" {
// caller didn't specify a user, so there's no reason to search for an ID or refresh token
return tr, nil
}
// errors returned by read* methods indicate a cache miss and are therefore non-fatal. We continue populating
@@ -122,7 +123,7 @@ func (m *Manager) Read(ctx context.Context, authParameters authority.AuthParams,
}
}
account, err = m.readAccount(homeAccountID, aliases, realm)
account, err := m.readAccount(homeAccountID, aliases, realm)
if err == nil {
tr.Account = account
}
@@ -493,6 +494,8 @@ func (m *Manager) update(cache *Contract) {
// Marshal implements cache.Marshaler.
func (m *Manager) Marshal() ([]byte, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
return json.Marshal(m.contract)
}

View File

@@ -76,12 +76,17 @@ func (t *Client) ResolveEndpoints(ctx context.Context, authorityInfo authority.I
return t.Resolver.ResolveEndpoints(ctx, authorityInfo, userPrincipalName)
}
// AADInstanceDiscovery attempts to discover a tenant endpoint (used in OIDC auth with an authorization endpoint).
// This is done by AAD which allows for aliasing of tenants (windows.sts.net is the same as login.windows.com).
func (t *Client) AADInstanceDiscovery(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryResponse, error) {
return t.Authority.AADInstanceDiscovery(ctx, authorityInfo)
}
// AuthCode returns a token based on an authorization code.
func (t *Client) AuthCode(ctx context.Context, req accesstokens.AuthCodeRequest) (accesstokens.TokenResponse, error) {
if err := scopeError(req.AuthParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if err := t.resolveEndpoint(ctx, &req.AuthParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
@@ -107,6 +112,10 @@ func (t *Client) Credential(ctx context.Context, authParams authority.AuthParams
}
tr, err := cred.TokenProvider(ctx, params)
if err != nil {
if len(scopes) == 0 {
err = fmt.Errorf("token request had an empty authority.AuthParams.Scopes, which may cause the following error: %w", err)
return accesstokens.TokenResponse{}, err
}
return accesstokens.TokenResponse{}, err
}
return accesstokens.TokenResponse{
@@ -134,6 +143,9 @@ func (t *Client) Credential(ctx context.Context, authParams authority.AuthParams
// Credential acquires a token from the authority using a client credentials grant.
func (t *Client) OnBehalfOf(ctx context.Context, authParams authority.AuthParams, cred *accesstokens.Credential) (accesstokens.TokenResponse, error) {
if err := scopeError(authParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
@@ -145,20 +157,35 @@ func (t *Client) OnBehalfOf(ctx context.Context, authParams authority.AuthParams
if err != nil {
return accesstokens.TokenResponse{}, err
}
return t.AccessTokens.FromUserAssertionClientCertificate(ctx, authParams, authParams.UserAssertion, jwt)
tr, err := t.AccessTokens.FromUserAssertionClientCertificate(ctx, authParams, authParams.UserAssertion, jwt)
if err != nil {
return accesstokens.TokenResponse{}, err
}
return tr, nil
}
func (t *Client) Refresh(ctx context.Context, reqType accesstokens.AppType, authParams authority.AuthParams, cc *accesstokens.Credential, refreshToken accesstokens.RefreshToken) (accesstokens.TokenResponse, error) {
if err := scopeError(authParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
return t.AccessTokens.FromRefreshToken(ctx, reqType, authParams, cc, refreshToken.Secret)
tr, err := t.AccessTokens.FromRefreshToken(ctx, reqType, authParams, cc, refreshToken.Secret)
if err != nil {
return accesstokens.TokenResponse{}, err
}
return tr, nil
}
// UsernamePassword retrieves a token where a username and password is used. However, if this is
// a user realm of "Federated", this uses SAML tokens. If "Managed", uses normal username/password.
func (t *Client) UsernamePassword(ctx context.Context, authParams authority.AuthParams) (accesstokens.TokenResponse, error) {
if err := scopeError(authParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if authParams.AuthorityInfo.AuthorityType == authority.ADFS {
if err := t.resolveEndpoint(ctx, &authParams, authParams.Username); err != nil {
return accesstokens.TokenResponse{}, err
@@ -171,22 +198,32 @@ func (t *Client) UsernamePassword(ctx context.Context, authParams authority.Auth
userRealm, err := t.Authority.UserRealm(ctx, authParams)
if err != nil {
return accesstokens.TokenResponse{}, fmt.Errorf("problem getting user realm(user: %s) from authority: %w", authParams.Username, err)
return accesstokens.TokenResponse{}, fmt.Errorf("problem getting user realm from authority: %w", err)
}
switch userRealm.AccountType {
case authority.Federated:
mexDoc, err := t.WSTrust.Mex(ctx, userRealm.FederationMetadataURL)
if err != nil {
return accesstokens.TokenResponse{}, fmt.Errorf("problem getting mex doc from federated url(%s): %w", userRealm.FederationMetadataURL, err)
err = fmt.Errorf("problem getting mex doc from federated url(%s): %w", userRealm.FederationMetadataURL, err)
return accesstokens.TokenResponse{}, err
}
saml, err := t.WSTrust.SAMLTokenInfo(ctx, authParams, userRealm.CloudAudienceURN, mexDoc.UsernamePasswordEndpoint)
if err != nil {
return accesstokens.TokenResponse{}, fmt.Errorf("problem getting SAML token info: %w", err)
err = fmt.Errorf("problem getting SAML token info: %w", err)
return accesstokens.TokenResponse{}, err
}
return t.AccessTokens.FromSamlGrant(ctx, authParams, saml)
tr, err := t.AccessTokens.FromSamlGrant(ctx, authParams, saml)
if err != nil {
return accesstokens.TokenResponse{}, err
}
return tr, nil
case authority.Managed:
if len(authParams.Scopes) == 0 {
err = fmt.Errorf("token request had an empty authority.AuthParams.Scopes, which may cause the following error: %w", err)
return accesstokens.TokenResponse{}, err
}
return t.AccessTokens.FromUsernamePassword(ctx, authParams)
}
return accesstokens.TokenResponse{}, errors.New("unknown account type")
@@ -212,7 +249,6 @@ func (d DeviceCode) Token(ctx context.Context) (accesstokens.TokenResponse, erro
}
var cancel context.CancelFunc
d.Result.ExpiresOn.Sub(time.Now().UTC())
if deadline, ok := ctx.Deadline(); !ok || d.Result.ExpiresOn.Before(deadline) {
ctx, cancel = context.WithDeadline(ctx, d.Result.ExpiresOn)
} else {
@@ -275,6 +311,10 @@ func isWaitDeviceCodeErr(err error) bool {
// DeviceCode returns a DeviceCode object that can be used to get the code that must be entered on the second
// device and optionally the token once the code has been entered on the second device.
func (t *Client) DeviceCode(ctx context.Context, authParams authority.AuthParams) (DeviceCode, error) {
if err := scopeError(authParams); err != nil {
return DeviceCode{}, err
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return DeviceCode{}, err
}
@@ -295,3 +335,19 @@ func (t *Client) resolveEndpoint(ctx context.Context, authParams *authority.Auth
authParams.Endpoints = endpoints
return nil
}
// scopeError takes an authority.AuthParams and returns an error
// if len(AuthParams.Scope) == 0.
func scopeError(a authority.AuthParams) error {
// TODO(someone): we could look deeper at the message to determine if
// it's a scope error, but this is a good start.
/*
{error":"invalid_scope","error_description":"AADSTS1002012: The provided value for scope
openid offline_access profile is not valid. Client credential flows must have a scope value
with /.default suffixed to the resource identifier (application ID URI)...}
*/
if len(a.Scopes) == 0 {
return fmt.Errorf("token request had an empty authority.AuthParams.Scopes, which is invalid")
}
return nil
}

View File

@@ -28,10 +28,19 @@ const (
regionName = "REGION_NAME"
defaultAPIVersion = "2021-10-01"
imdsEndpoint = "http://169.254.169.254/metadata/instance/compute/location?format=text&api-version=" + defaultAPIVersion
defaultHost = "login.microsoftonline.com"
autoDetectRegion = "TryAutoDetect"
)
// These are various hosts that host AAD Instance discovery endpoints.
const (
defaultHost = "login.microsoftonline.com"
loginMicrosoft = "login.microsoft.com"
loginWindows = "login.windows.net"
loginSTSWindows = "sts.windows.net"
loginMicrosoftOnline = defaultHost
)
// jsonCaller is an interface that allows us to mock the JSONCall method.
type jsonCaller interface {
JSONCall(ctx context.Context, endpoint string, headers http.Header, qv url.Values, body, resp interface{}) error
}
@@ -54,6 +63,8 @@ func TrustedHost(host string) bool {
return false
}
// OAuthResponseBase is the base JSON return message for an OAuth call.
// This is embedded in other calls to get the base fields from every response.
type OAuthResponseBase struct {
Error string `json:"error"`
SubError string `json:"suberror"`
@@ -309,31 +320,24 @@ func firstPathSegment(u *url.URL) (string, error) {
return pathParts[1], nil
}
return "", errors.New("authority does not have two segments")
return "", errors.New(`authority must be an https URL such as "https://login.microsoftonline.com/<your tenant>"`)
}
// NewInfoFromAuthorityURI creates an AuthorityInfo instance from the authority URL provided.
func NewInfoFromAuthorityURI(authorityURI string, validateAuthority bool, instanceDiscoveryDisabled bool) (Info, error) {
authorityURI = strings.ToLower(authorityURI)
var authorityType string
u, err := url.Parse(authorityURI)
if err != nil {
return Info{}, fmt.Errorf("authorityURI passed could not be parsed: %w", err)
}
if u.Scheme != "https" {
return Info{}, fmt.Errorf("authorityURI(%s) must have scheme https", authorityURI)
func NewInfoFromAuthorityURI(authority string, validateAuthority bool, instanceDiscoveryDisabled bool) (Info, error) {
u, err := url.Parse(strings.ToLower(authority))
if err != nil || u.Scheme != "https" {
return Info{}, errors.New(`authority must be an https URL such as "https://login.microsoftonline.com/<your tenant>"`)
}
tenant, err := firstPathSegment(u)
if tenant == "adfs" {
authorityType = ADFS
} else {
authorityType = AAD
}
if err != nil {
return Info{}, err
}
authorityType := AAD
if tenant == "adfs" {
authorityType = ADFS
}
// u.Host includes the port, if any, which is required for private cloud deployments
return Info{
@@ -449,6 +453,8 @@ func (c Client) GetTenantDiscoveryResponse(ctx context.Context, openIDConfigurat
return resp, err
}
// AADInstanceDiscovery attempts to discover a tenant endpoint (used in OIDC auth with an authorization endpoint).
// This is done by AAD which allows for aliasing of tenants (windows.sts.net is the same as login.windows.com).
func (c Client) AADInstanceDiscovery(ctx context.Context, authorityInfo Info) (InstanceDiscoveryResponse, error) {
region := ""
var err error
@@ -461,9 +467,10 @@ func (c Client) AADInstanceDiscovery(ctx context.Context, authorityInfo Info) (I
if region != "" {
environment := authorityInfo.Host
switch environment {
case "login.microsoft.com", "login.windows.net", "sts.windows.net", defaultHost:
environment = "r." + defaultHost
case loginMicrosoft, loginWindows, loginSTSWindows, defaultHost:
environment = loginMicrosoft
}
resp.TenantDiscoveryEndpoint = fmt.Sprintf(tenantDiscoveryEndpointWithRegion, region, environment, authorityInfo.Tenant)
metadata := InstanceDiscoveryMetadata{
PreferredNetwork: fmt.Sprintf("%v.%v", region, authorityInfo.Host),

View File

@@ -5,4 +5,4 @@
package version
// Version is the version of this client package that is communicated to the server.
const Version = "0.8.1"
const Version = "1.0.0"