base.go raw

   1  // Package base contains a "Base" client that is used by the external public.Client and confidential.Client.
   2  // Base holds shared attributes that must be available to both clients and methods that act as
   3  // shared calls.
   4  package base
   5  
   6  import (
   7  	"context"
   8  	"fmt"
   9  	"net/url"
  10  	"reflect"
  11  	"strings"
  12  	"sync"
  13  	"sync/atomic"
  14  	"time"
  15  
  16  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
  17  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
  18  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage"
  19  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
  20  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
  21  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
  22  	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
  23  )
  24  
  25  const (
  26  	// AuthorityPublicCloud is the default AAD authority host
  27  	AuthorityPublicCloud = "https://login.microsoftonline.com/common"
  28  	scopeSeparator       = " "
  29  )
  30  
  31  // manager provides an internal cache. It is defined to allow faking the cache in tests.
  32  // In production it's a *storage.Manager or *storage.PartitionedManager.
  33  type manager interface {
  34  	cache.Serializer
  35  	Read(context.Context, authority.AuthParams) (storage.TokenResponse, error)
  36  	Write(authority.AuthParams, accesstokens.TokenResponse) (shared.Account, error)
  37  }
  38  
  39  // accountManager is a manager that also caches accounts. In production it's a *storage.Manager.
  40  type accountManager interface {
  41  	manager
  42  	AllAccounts() []shared.Account
  43  	Account(homeAccountID string) shared.Account
  44  	RemoveAccount(account shared.Account, clientID string)
  45  }
  46  
  47  // AcquireTokenSilentParameters contains the parameters to acquire a token silently (from cache).
  48  type AcquireTokenSilentParameters struct {
  49  	Scopes              []string
  50  	Account             shared.Account
  51  	RequestType         accesstokens.AppType
  52  	Credential          *accesstokens.Credential
  53  	IsAppCache          bool
  54  	TenantID            string
  55  	UserAssertion       string
  56  	AuthorizationType   authority.AuthorizeType
  57  	Claims              string
  58  	AuthnScheme         authority.AuthenticationScheme
  59  	ExtraBodyParameters map[string]string
  60  	CacheKeyComponents  map[string]string
  61  }
  62  
  63  // AcquireTokenAuthCodeParameters contains the parameters required to acquire an access token using the auth code flow.
  64  // To use PKCE, set the CodeChallengeParameter.
  65  // Code challenges are used to secure authorization code grants; for more information, visit
  66  // https://tools.ietf.org/html/rfc7636.
  67  type AcquireTokenAuthCodeParameters struct {
  68  	Scopes      []string
  69  	Code        string
  70  	Challenge   string
  71  	Claims      string
  72  	RedirectURI string
  73  	AppType     accesstokens.AppType
  74  	Credential  *accesstokens.Credential
  75  	TenantID    string
  76  }
  77  
  78  type AcquireTokenOnBehalfOfParameters struct {
  79  	Scopes        []string
  80  	Claims        string
  81  	Credential    *accesstokens.Credential
  82  	TenantID      string
  83  	UserAssertion string
  84  }
  85  
  86  // AuthResult contains the results of one token acquisition operation in PublicClientApplication
  87  // or ConfidentialClientApplication. For details see https://aka.ms/msal-net-authenticationresult
  88  type AuthResult struct {
  89  	Account        shared.Account
  90  	IDToken        accesstokens.IDToken
  91  	AccessToken    string
  92  	ExpiresOn      time.Time
  93  	GrantedScopes  []string
  94  	DeclinedScopes []string
  95  	Metadata       AuthResultMetadata
  96  }
  97  
  98  // AuthResultMetadata which contains meta data for the AuthResult
  99  type AuthResultMetadata struct {
 100  	RefreshOn   time.Time
 101  	TokenSource TokenSource
 102  }
 103  
 104  type TokenSource int
 105  
 106  // These are all the types of token flows.
 107  const (
 108  	TokenSourceIdentityProvider TokenSource = 0
 109  	TokenSourceCache            TokenSource = 1
 110  )
 111  
 112  // AuthResultFromStorage creates an AuthResult from a storage token response (which is generated from the cache).
 113  func AuthResultFromStorage(storageTokenResponse storage.TokenResponse) (AuthResult, error) {
 114  	if err := storageTokenResponse.AccessToken.Validate(); err != nil {
 115  		return AuthResult{}, fmt.Errorf("problem with access token in StorageTokenResponse: %w", err)
 116  	}
 117  	account := storageTokenResponse.Account
 118  	accessToken := storageTokenResponse.AccessToken.Secret
 119  	grantedScopes := strings.Split(storageTokenResponse.AccessToken.Scopes, scopeSeparator)
 120  
 121  	// Checking if there was an ID token in the cache; this will throw an error in the case of confidential client applications.
 122  	var idToken accesstokens.IDToken
 123  	if !storageTokenResponse.IDToken.IsZero() {
 124  		err := idToken.UnmarshalJSON([]byte(storageTokenResponse.IDToken.Secret))
 125  		if err != nil {
 126  			return AuthResult{}, fmt.Errorf("problem decoding JWT token: %w", err)
 127  		}
 128  	}
 129  	return AuthResult{
 130  		Account:        account,
 131  		IDToken:        idToken,
 132  		AccessToken:    accessToken,
 133  		ExpiresOn:      storageTokenResponse.AccessToken.ExpiresOn.T,
 134  		GrantedScopes:  grantedScopes,
 135  		DeclinedScopes: nil,
 136  		Metadata: AuthResultMetadata{
 137  			TokenSource: TokenSourceCache,
 138  			RefreshOn:   storageTokenResponse.AccessToken.RefreshOn.T,
 139  		},
 140  	}, nil
 141  }
 142  
 143  // NewAuthResult creates an AuthResult.
 144  func NewAuthResult(tokenResponse accesstokens.TokenResponse, account shared.Account) (AuthResult, error) {
 145  	if len(tokenResponse.DeclinedScopes) > 0 {
 146  		return AuthResult{}, fmt.Errorf("token response failed because declined scopes are present: %s", strings.Join(tokenResponse.DeclinedScopes, ","))
 147  	}
 148  	return AuthResult{
 149  		Account:       account,
 150  		IDToken:       tokenResponse.IDToken,
 151  		AccessToken:   tokenResponse.AccessToken,
 152  		ExpiresOn:     tokenResponse.ExpiresOn,
 153  		GrantedScopes: tokenResponse.GrantedScopes.Slice,
 154  		Metadata: AuthResultMetadata{
 155  			TokenSource: TokenSourceIdentityProvider,
 156  			RefreshOn:   tokenResponse.RefreshOn.T,
 157  		},
 158  	}, nil
 159  }
 160  
 161  // Client is a base client that provides access to common methods and primatives that
 162  // can be used by multiple clients.
 163  type Client struct {
 164  	Token   *oauth.Client
 165  	manager accountManager // *storage.Manager or fakeManager in tests
 166  	// pmanager is a partitioned cache for OBO authentication. *storage.PartitionedManager or fakeManager in tests
 167  	pmanager manager
 168  
 169  	AuthParams      authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New().
 170  	cacheAccessor   cache.ExportReplace
 171  	cacheAccessorMu *sync.RWMutex
 172  	canRefresh      map[string]*atomic.Value
 173  	canRefreshMu    *sync.Mutex
 174  }
 175  
 176  // Option is an optional argument to the New constructor.
 177  type Option func(c *Client) error
 178  
 179  // WithCacheAccessor allows you to set some type of cache for storing authentication tokens.
 180  func WithCacheAccessor(ca cache.ExportReplace) Option {
 181  	return func(c *Client) error {
 182  		if ca != nil {
 183  			c.cacheAccessor = ca
 184  		}
 185  		return nil
 186  	}
 187  }
 188  
 189  // WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
 190  func WithClientCapabilities(capabilities []string) Option {
 191  	return func(c *Client) error {
 192  		var err error
 193  		if len(capabilities) > 0 {
 194  			cc, err := authority.NewClientCapabilities(capabilities)
 195  			if err == nil {
 196  				c.AuthParams.Capabilities = cc
 197  			}
 198  		}
 199  		return err
 200  	}
 201  }
 202  
 203  // WithKnownAuthorityHosts specifies hosts Client shouldn't validate or request metadata for because they're known to the user
 204  func WithKnownAuthorityHosts(hosts []string) Option {
 205  	return func(c *Client) error {
 206  		cp := make([]string, len(hosts))
 207  		copy(cp, hosts)
 208  		c.AuthParams.KnownAuthorityHosts = cp
 209  		return nil
 210  	}
 211  }
 212  
 213  // WithX5C specifies if x5c claim(public key of the certificate) should be sent to STS to enable Subject Name Issuer Authentication.
 214  func WithX5C(sendX5C bool) Option {
 215  	return func(c *Client) error {
 216  		c.AuthParams.SendX5C = sendX5C
 217  		return nil
 218  	}
 219  }
 220  
 221  func WithRegionDetection(region string) Option {
 222  	return func(c *Client) error {
 223  		c.AuthParams.AuthorityInfo.Region = region
 224  		return nil
 225  	}
 226  }
 227  
 228  func WithInstanceDiscovery(instanceDiscoveryEnabled bool) Option {
 229  	return func(c *Client) error {
 230  		c.AuthParams.AuthorityInfo.ValidateAuthority = instanceDiscoveryEnabled
 231  		c.AuthParams.AuthorityInfo.InstanceDiscoveryDisabled = !instanceDiscoveryEnabled
 232  		return nil
 233  	}
 234  }
 235  
 236  // New is the constructor for Base.
 237  func New(clientID string, authorityURI string, token *oauth.Client, options ...Option) (Client, error) {
 238  	//By default, validateAuthority is set to true and instanceDiscoveryDisabled is set to false
 239  	authInfo, err := authority.NewInfoFromAuthorityURI(authorityURI, true, false)
 240  	if err != nil {
 241  		return Client{}, err
 242  	}
 243  	authParams := authority.NewAuthParams(clientID, authInfo)
 244  	client := Client{ // Note: Hey, don't even THINK about making Base into *Base. See "design notes" in public.go and confidential.go
 245  		Token:           token,
 246  		AuthParams:      authParams,
 247  		cacheAccessorMu: &sync.RWMutex{},
 248  		manager:         storage.New(token),
 249  		pmanager:        storage.NewPartitionedManager(token),
 250  		canRefresh:      make(map[string]*atomic.Value),
 251  		canRefreshMu:    &sync.Mutex{},
 252  	}
 253  	for _, o := range options {
 254  		if err = o(&client); err != nil {
 255  			break
 256  		}
 257  	}
 258  	return client, err
 259  
 260  }
 261  
 262  // AuthCodeURL creates a URL used to acquire an authorization code.
 263  func (b Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, authParams authority.AuthParams) (string, error) {
 264  	endpoints, err := b.Token.ResolveEndpoints(ctx, authParams.AuthorityInfo, "")
 265  	if err != nil {
 266  		return "", err
 267  	}
 268  
 269  	baseURL, err := url.Parse(endpoints.AuthorizationEndpoint)
 270  	if err != nil {
 271  		return "", err
 272  	}
 273  
 274  	claims, err := authParams.MergeCapabilitiesAndClaims()
 275  	if err != nil {
 276  		return "", err
 277  	}
 278  
 279  	v := url.Values{}
 280  	v.Add("client_id", clientID)
 281  	v.Add("response_type", "code")
 282  	v.Add("redirect_uri", redirectURI)
 283  	v.Add("scope", strings.Join(scopes, scopeSeparator))
 284  	if authParams.State != "" {
 285  		v.Add("state", authParams.State)
 286  	}
 287  	if claims != "" {
 288  		v.Add("claims", claims)
 289  	}
 290  	if authParams.CodeChallenge != "" {
 291  		v.Add("code_challenge", authParams.CodeChallenge)
 292  	}
 293  	if authParams.CodeChallengeMethod != "" {
 294  		v.Add("code_challenge_method", authParams.CodeChallengeMethod)
 295  	}
 296  	if authParams.LoginHint != "" {
 297  		v.Add("login_hint", authParams.LoginHint)
 298  	}
 299  	if authParams.Prompt != "" {
 300  		v.Add("prompt", authParams.Prompt)
 301  	}
 302  	if authParams.DomainHint != "" {
 303  		v.Add("domain_hint", authParams.DomainHint)
 304  	}
 305  	// There were left over from an implementation that didn't use any of these.  We may
 306  	// need to add them later, but as of now aren't needed.
 307  	/*
 308  		if p.ResponseMode != "" {
 309  			urlParams.Add("response_mode", p.ResponseMode)
 310  		}
 311  	*/
 312  	baseURL.RawQuery = v.Encode()
 313  	return baseURL.String(), nil
 314  }
 315  
 316  func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilentParameters) (AuthResult, error) {
 317  	ar := AuthResult{}
 318  	// when tenant == "", the caller didn't specify a tenant and WithTenant will choose the client's configured tenant
 319  	tenant := silent.TenantID
 320  	authParams, err := b.AuthParams.WithTenant(tenant)
 321  	if err != nil {
 322  		return ar, err
 323  	}
 324  	authParams.Scopes = silent.Scopes
 325  	authParams.HomeAccountID = silent.Account.HomeAccountID
 326  	authParams.AuthorizationType = silent.AuthorizationType
 327  	authParams.Claims = silent.Claims
 328  	authParams.UserAssertion = silent.UserAssertion
 329  	if silent.AuthnScheme != nil {
 330  		authParams.AuthnScheme = silent.AuthnScheme
 331  	}
 332  	if silent.CacheKeyComponents != nil {
 333  		authParams.CacheKeyComponents = silent.CacheKeyComponents
 334  	}
 335  	if silent.ExtraBodyParameters != nil {
 336  		authParams.ExtraBodyParameters = silent.ExtraBodyParameters
 337  	}
 338  	m := b.pmanager
 339  	if authParams.AuthorizationType != authority.ATOnBehalfOf {
 340  		authParams.AuthorizationType = authority.ATRefreshToken
 341  		m = b.manager
 342  	}
 343  	if b.cacheAccessor != nil {
 344  		key := authParams.CacheKey(silent.IsAppCache)
 345  		b.cacheAccessorMu.RLock()
 346  		err = b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
 347  		b.cacheAccessorMu.RUnlock()
 348  	}
 349  	if err != nil {
 350  		return ar, err
 351  	}
 352  	storageTokenResponse, err := m.Read(ctx, authParams)
 353  	if err != nil {
 354  		return ar, err
 355  	}
 356  
 357  	// ignore cached access tokens when given claims
 358  	if silent.Claims == "" {
 359  		ar, err = AuthResultFromStorage(storageTokenResponse)
 360  		if err == nil {
 361  			if rt := storageTokenResponse.AccessToken.RefreshOn.T; !rt.IsZero() && Now().After(rt) {
 362  				b.canRefreshMu.Lock()
 363  				refreshValue, ok := b.canRefresh[tenant]
 364  				if !ok {
 365  					refreshValue = &atomic.Value{}
 366  					refreshValue.Store(false)
 367  					b.canRefresh[tenant] = refreshValue
 368  				}
 369  				b.canRefreshMu.Unlock()
 370  				if refreshValue.CompareAndSwap(false, true) {
 371  					defer refreshValue.Store(false)
 372  					// Added a check to see if the token is still same because there is a chance
 373  					// that the token is already refreshed by another thread.
 374  					// If the token is not same, we don't need to refresh it.
 375  					// Which means it refreshed.
 376  					if str, err := m.Read(ctx, authParams); err == nil && str.AccessToken.Secret == ar.AccessToken {
 377  						switch silent.RequestType {
 378  						case accesstokens.ATConfidential:
 379  							if tr, er := b.Token.Credential(ctx, authParams, silent.Credential); er == nil {
 380  								return b.AuthResultFromToken(ctx, authParams, tr)
 381  							}
 382  						case accesstokens.ATPublic:
 383  							token, err := b.Token.Refresh(ctx, silent.RequestType, authParams, silent.Credential, storageTokenResponse.RefreshToken)
 384  							if err != nil {
 385  								return ar, err
 386  							}
 387  							return b.AuthResultFromToken(ctx, authParams, token)
 388  						case accesstokens.ATUnknown:
 389  							return ar, errors.New("silent request type cannot be ATUnknown")
 390  						}
 391  					}
 392  				}
 393  			}
 394  			ar.AccessToken, err = authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
 395  			return ar, err
 396  		}
 397  	}
 398  
 399  	// redeem a cached refresh token, if available
 400  	if reflect.ValueOf(storageTokenResponse.RefreshToken).IsZero() {
 401  		return ar, errors.New("no token found")
 402  	}
 403  	var cc *accesstokens.Credential
 404  	if silent.RequestType == accesstokens.ATConfidential {
 405  		cc = silent.Credential
 406  	}
 407  	token, err := b.Token.Refresh(ctx, silent.RequestType, authParams, cc, storageTokenResponse.RefreshToken)
 408  	if err != nil {
 409  		return ar, err
 410  	}
 411  	return b.AuthResultFromToken(ctx, authParams, token)
 412  }
 413  
 414  func (b Client) AcquireTokenByAuthCode(ctx context.Context, authCodeParams AcquireTokenAuthCodeParameters) (AuthResult, error) {
 415  	authParams, err := b.AuthParams.WithTenant(authCodeParams.TenantID)
 416  	if err != nil {
 417  		return AuthResult{}, err
 418  	}
 419  	authParams.Claims = authCodeParams.Claims
 420  	authParams.Scopes = authCodeParams.Scopes
 421  	authParams.Redirecturi = authCodeParams.RedirectURI
 422  	authParams.AuthorizationType = authority.ATAuthCode
 423  
 424  	var cc *accesstokens.Credential
 425  	if authCodeParams.AppType == accesstokens.ATConfidential {
 426  		cc = authCodeParams.Credential
 427  		authParams.IsConfidentialClient = true
 428  	}
 429  
 430  	req, err := accesstokens.NewCodeChallengeRequest(authParams, authCodeParams.AppType, cc, authCodeParams.Code, authCodeParams.Challenge)
 431  	if err != nil {
 432  		return AuthResult{}, err
 433  	}
 434  
 435  	token, err := b.Token.AuthCode(ctx, req)
 436  	if err != nil {
 437  		return AuthResult{}, err
 438  	}
 439  
 440  	return b.AuthResultFromToken(ctx, authParams, token)
 441  }
 442  
 443  // AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
 444  func (b Client) AcquireTokenOnBehalfOf(ctx context.Context, onBehalfOfParams AcquireTokenOnBehalfOfParameters) (AuthResult, error) {
 445  	var ar AuthResult
 446  	silentParameters := AcquireTokenSilentParameters{
 447  		Scopes:            onBehalfOfParams.Scopes,
 448  		RequestType:       accesstokens.ATConfidential,
 449  		Credential:        onBehalfOfParams.Credential,
 450  		UserAssertion:     onBehalfOfParams.UserAssertion,
 451  		AuthorizationType: authority.ATOnBehalfOf,
 452  		TenantID:          onBehalfOfParams.TenantID,
 453  		Claims:            onBehalfOfParams.Claims,
 454  	}
 455  	ar, err := b.AcquireTokenSilent(ctx, silentParameters)
 456  	if err == nil {
 457  		return ar, err
 458  	}
 459  	authParams, err := b.AuthParams.WithTenant(onBehalfOfParams.TenantID)
 460  	if err != nil {
 461  		return AuthResult{}, err
 462  	}
 463  	authParams.AuthorizationType = authority.ATOnBehalfOf
 464  	authParams.Claims = onBehalfOfParams.Claims
 465  	authParams.Scopes = onBehalfOfParams.Scopes
 466  	authParams.UserAssertion = onBehalfOfParams.UserAssertion
 467  	if authParams.ExtraBodyParameters != nil {
 468  		authParams.ExtraBodyParameters = silentParameters.ExtraBodyParameters
 469  	}
 470  	token, err := b.Token.OnBehalfOf(ctx, authParams, onBehalfOfParams.Credential)
 471  	if err == nil {
 472  		ar, err = b.AuthResultFromToken(ctx, authParams, token)
 473  	}
 474  	return ar, err
 475  }
 476  
 477  func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.AuthParams, token accesstokens.TokenResponse) (AuthResult, error) {
 478  	var m manager = b.manager
 479  	if authParams.AuthorizationType == authority.ATOnBehalfOf {
 480  		m = b.pmanager
 481  	}
 482  	key := token.CacheKey(authParams)
 483  	if b.cacheAccessor != nil {
 484  		b.cacheAccessorMu.Lock()
 485  		defer b.cacheAccessorMu.Unlock()
 486  		err := b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
 487  		if err != nil {
 488  			return AuthResult{}, err
 489  		}
 490  	}
 491  	account, err := m.Write(authParams, token)
 492  	if err != nil {
 493  		return AuthResult{}, err
 494  	}
 495  	ar, err := NewAuthResult(token, account)
 496  	if err == nil && b.cacheAccessor != nil {
 497  		err = b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
 498  	}
 499  	if err != nil {
 500  		return AuthResult{}, err
 501  	}
 502  
 503  	ar.AccessToken, err = authParams.AuthnScheme.FormatAccessToken(ar.AccessToken)
 504  	return ar, err
 505  }
 506  
 507  // This function wraps time.Now() and is used for refreshing the application
 508  // was created to test the function against refreshin
 509  var Now = time.Now
 510  
 511  func (b Client) AllAccounts(ctx context.Context) ([]shared.Account, error) {
 512  	if b.cacheAccessor != nil {
 513  		b.cacheAccessorMu.RLock()
 514  		defer b.cacheAccessorMu.RUnlock()
 515  		key := b.AuthParams.CacheKey(false)
 516  		err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
 517  		if err != nil {
 518  			return nil, err
 519  		}
 520  	}
 521  	return b.manager.AllAccounts(), nil
 522  }
 523  
 524  func (b Client) Account(ctx context.Context, homeAccountID string) (shared.Account, error) {
 525  	if b.cacheAccessor != nil {
 526  		b.cacheAccessorMu.RLock()
 527  		defer b.cacheAccessorMu.RUnlock()
 528  		authParams := b.AuthParams // This is a copy, as we don't have a pointer receiver and .AuthParams is not a pointer.
 529  		authParams.AuthorizationType = authority.AccountByID
 530  		authParams.HomeAccountID = homeAccountID
 531  		key := b.AuthParams.CacheKey(false)
 532  		err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
 533  		if err != nil {
 534  			return shared.Account{}, err
 535  		}
 536  	}
 537  	return b.manager.Account(homeAccountID), nil
 538  }
 539  
 540  // RemoveAccount removes all the ATs, RTs and IDTs from the cache associated with this account.
 541  func (b Client) RemoveAccount(ctx context.Context, account shared.Account) error {
 542  	if b.cacheAccessor == nil {
 543  		b.manager.RemoveAccount(account, b.AuthParams.ClientID)
 544  		return nil
 545  	}
 546  	b.cacheAccessorMu.Lock()
 547  	defer b.cacheAccessorMu.Unlock()
 548  	key := b.AuthParams.CacheKey(false)
 549  	err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
 550  	if err != nil {
 551  		return err
 552  	}
 553  	b.manager.RemoveAccount(account, b.AuthParams.ClientID)
 554  	return b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
 555  }
 556