rebase: Azure key vault module dependency update

This commit adds the Azure SDK for Azure key vault KMS
integration to the Ceph CSI driver.

Signed-off-by: Praveen M <m.praveen@ibm.com>
This commit is contained in:
Praveen M 2024-02-21 11:22:35 +05:30 committed by mergify[bot]
parent b2087e4517
commit 47b202554e
232 changed files with 29303 additions and 0 deletions

View File

@ -0,0 +1,735 @@
# Release History
## 1.9.1 (2023-12-11)
### Bugs Fixed
* The `retry-after-ms` and `x-ms-retry-after-ms` headers weren't being checked during retries.
### Other Changes
* Update dependencies.
## 1.9.0 (2023-11-06)
### Breaking Changes
> These changes affect only code written against previous beta versions of `v1.7.0` and `v1.8.0`
* The function `NewTokenCredential` has been removed from the `fake` package. Use a literal `&fake.TokenCredential{}` instead.
* The field `TracingNamespace` in `runtime.PipelineOptions` has been replaced by `TracingOptions`.
### Bugs Fixed
* Fixed an issue that could cause some allowed HTTP header values to not show up in logs.
* Include error text instead of error type in traces when the transport returns an error.
* Fixed an issue that could cause an HTTP/2 request to hang when the TCP connection becomes unresponsive.
* Block key and SAS authentication for non TLS protected endpoints.
* Passing a `nil` credential value will no longer cause a panic. Instead, the authentication is skipped.
* Calling `Error` on a zero-value `azcore.ResponseError` will no longer panic.
* Fixed an issue in `fake.PagerResponder[T]` that would cause a trailing error to be omitted when iterating over pages.
* Context values created by `azcore` will no longer flow across disjoint HTTP requests.
### Other Changes
* Skip generating trace info for no-op tracers.
* The `clientName` paramater in client constructors has been renamed to `moduleName`.
## 1.9.0-beta.1 (2023-10-05)
### Other Changes
* The beta features for tracing and fakes have been reinstated.
## 1.8.0 (2023-10-05)
### Features Added
* This includes the following features from `v1.8.0-beta.N` releases.
* Claims and CAE for authentication.
* New `messaging` package.
* Various helpers in the `runtime` package.
* Deprecation of `runtime.With*` funcs and their replacements in the `policy` package.
* Added types `KeyCredential` and `SASCredential` to the `azcore` package.
* Includes their respective constructor functions.
* Added types `KeyCredentialPolicy` and `SASCredentialPolicy` to the `azcore/runtime` package.
* Includes their respective constructor functions and options types.
### Breaking Changes
> These changes affect only code written against beta versions of `v1.8.0`
* The beta features for tracing and fakes have been omitted for this release.
### Bugs Fixed
* Fixed an issue that could cause some ARM RPs to not be automatically registered.
* Block bearer token authentication for non TLS protected endpoints.
### Other Changes
* Updated dependencies.
## 1.8.0-beta.3 (2023-09-07)
### Features Added
* Added function `FetcherForNextLink` and `FetcherForNextLinkOptions` to the `runtime` package to centralize creation of `Pager[T].Fetcher` from a next link URL.
### Bugs Fixed
* Suppress creating spans for nested SDK API calls. The HTTP span will be a child of the outer API span.
### Other Changes
* The following functions in the `runtime` package are now exposed from the `policy` package, and the `runtime` versions have been deprecated.
* `WithCaptureResponse`
* `WithHTTPHeader`
* `WithRetryOptions`
## 1.7.2 (2023-09-06)
### Bugs Fixed
* Fix default HTTP transport to work in WASM modules.
## 1.8.0-beta.2 (2023-08-14)
### Features Added
* Added function `SanitizePagerPollerPath` to the `server` package to centralize sanitization and formalize the contract.
* Added `TokenRequestOptions.EnableCAE` to indicate whether to request a CAE token.
### Breaking Changes
> This change affects only code written against beta version `v1.8.0-beta.1`.
* `messaging.CloudEvent` deserializes JSON objects as `[]byte`, instead of `json.RawMessage`. See the documentation for CloudEvent.Data for more information.
> This change affects only code written against beta versions `v1.7.0-beta.2` and `v1.8.0-beta.1`.
* Removed parameter from method `Span.End()` and its type `tracing.SpanEndOptions`. This API GA'ed in `v1.2.0` so we cannot change it.
### Bugs Fixed
* Propagate any query parameters when constructing a fake poller and/or injecting next links.
## 1.7.1 (2023-08-14)
## Bugs Fixed
* Enable TLS renegotiation in the default transport policy.
## 1.8.0-beta.1 (2023-07-12)
### Features Added
- `messaging/CloudEvent` allows you to serialize/deserialize CloudEvents, as described in the CloudEvents 1.0 specification: [link](https://github.com/cloudevents/spec)
### Other Changes
* The beta features for CAE, tracing, and fakes have been reinstated.
## 1.7.0 (2023-07-12)
### Features Added
* Added method `WithClientName()` to type `azcore.Client` to support shallow cloning of a client with a new name used for tracing.
### Breaking Changes
> These changes affect only code written against beta versions v1.7.0-beta.1 or v1.7.0-beta.2
* The beta features for CAE, tracing, and fakes have been omitted for this release.
## 1.7.0-beta.2 (2023-06-06)
### Breaking Changes
> These changes affect only code written against beta version v1.7.0-beta.1
* Method `SpanFromContext()` on type `tracing.Tracer` had the `bool` return value removed.
* This includes the field `SpanFromContext` in supporting type `tracing.TracerOptions`.
* Method `AddError()` has been removed from type `tracing.Span`.
* Method `Span.End()` now requires an argument of type `*tracing.SpanEndOptions`.
## 1.6.1 (2023-06-06)
### Bugs Fixed
* Fixed an issue in `azcore.NewClient()` and `arm.NewClient()` that could cause an incorrect module name to be used in telemetry.
### Other Changes
* This version contains all bug fixes from `v1.7.0-beta.1`
## 1.7.0-beta.1 (2023-05-24)
### Features Added
* Restored CAE support for ARM clients.
* Added supporting features to enable distributed tracing.
* Added func `runtime.StartSpan()` for use by SDKs to start spans.
* Added method `WithContext()` to `runtime.Request` to support shallow cloning with a new context.
* Added field `TracingNamespace` to `runtime.PipelineOptions`.
* Added field `Tracer` to `runtime.NewPollerOptions` and `runtime.NewPollerFromResumeTokenOptions` types.
* Added field `SpanFromContext` to `tracing.TracerOptions`.
* Added methods `Enabled()`, `SetAttributes()`, and `SpanFromContext()` to `tracing.Tracer`.
* Added supporting pipeline policies to include HTTP spans when creating clients.
* Added package `fake` to support generated fakes packages in SDKs.
* The package contains public surface area exposed by fake servers and supporting APIs intended only for use by the fake server implementations.
* Added an internal fake poller implementation.
### Bugs Fixed
* Retry policy always clones the underlying `*http.Request` before invoking the next policy.
* Added some non-standard error codes to the list of error codes for unregistered resource providers.
## 1.6.0 (2023-05-04)
### Features Added
* Added support for ARM cross-tenant authentication. Set the `AuxiliaryTenants` field of `arm.ClientOptions` to enable.
* Added `TenantID` field to `policy.TokenRequestOptions`.
## 1.5.0 (2023-04-06)
### Features Added
* Added `ShouldRetry` to `policy.RetryOptions` for finer-grained control over when to retry.
### Breaking Changes
> These changes affect only code written against a beta version such as v1.5.0-beta.1
> These features will return in v1.6.0-beta.1.
* Removed `TokenRequestOptions.Claims` and `.TenantID`
* Removed ARM client support for CAE and cross-tenant auth.
### Bugs Fixed
* Added non-conformant LRO terminal states `Cancelled` and `Completed`.
### Other Changes
* Updated to latest `internal` module.
## 1.5.0-beta.1 (2023-03-02)
### Features Added
* This release includes the features added in v1.4.0-beta.1
## 1.4.0 (2023-03-02)
> This release doesn't include features added in v1.4.0-beta.1. They will return in v1.5.0-beta.1.
### Features Added
* Add `Clone()` method for `arm/policy.ClientOptions`.
### Bugs Fixed
* ARM's RP registration policy will no longer swallow unrecognized errors.
* Fixed an issue in `runtime.NewPollerFromResumeToken()` when resuming a `Poller` with a custom `PollingHandler`.
* Fixed wrong policy copy in `arm/runtime.NewPipeline()`.
## 1.4.0-beta.1 (2023-02-02)
### Features Added
* Added support for ARM cross-tenant authentication. Set the `AuxiliaryTenants` field of `arm.ClientOptions` to enable.
* Added `Claims` and `TenantID` fields to `policy.TokenRequestOptions`.
* ARM bearer token policy handles CAE challenges.
## 1.3.1 (2023-02-02)
### Other Changes
* Update dependencies to latest versions.
## 1.3.0 (2023-01-06)
### Features Added
* Added `BearerTokenOptions.AuthorizationHandler` to enable extending `runtime.BearerTokenPolicy`
with custom authorization logic
* Added `Client` types and matching constructors to the `azcore` and `arm` packages. These represent a basic client for HTTP and ARM respectively.
### Other Changes
* Updated `internal` module to latest version.
* `policy/Request.SetBody()` allows replacing a request's body with an empty one
## 1.2.0 (2022-11-04)
### Features Added
* Added `ClientOptions.APIVersion` field, which overrides the default version a client
requests of the service, if the client supports this (all ARM clients do).
* Added package `tracing` that contains the building blocks for distributed tracing.
* Added field `TracingProvider` to type `policy.ClientOptions` that will be used to set the per-client tracing implementation.
### Bugs Fixed
* Fixed an issue in `runtime.SetMultipartFormData` to properly handle slices of `io.ReadSeekCloser`.
* Fixed the MaxRetryDelay default to be 60s.
* Failure to poll the state of an LRO will now return an `*azcore.ResponseError` for poller types that require this behavior.
* Fixed a bug in `runtime.NewPipeline` that would cause pipeline-specified allowed headers and query parameters to be lost.
### Other Changes
* Retain contents of read-only fields when sending requests.
## 1.1.4 (2022-10-06)
### Bugs Fixed
* Don't retry a request if the `Retry-After` delay is greater than the configured `RetryOptions.MaxRetryDelay`.
* `runtime.JoinPaths`: do not unconditionally add a forward slash before the query string
### Other Changes
* Removed logging URL from retry policy as it's redundant.
* Retry policy logs when it exits due to a non-retriable status code.
## 1.1.3 (2022-09-01)
### Bugs Fixed
* Adjusted the initial retry delay to 800ms per the Azure SDK guidelines.
## 1.1.2 (2022-08-09)
### Other Changes
* Fixed various doc bugs.
## 1.1.1 (2022-06-30)
### Bugs Fixed
* Avoid polling when a RELO LRO synchronously terminates.
## 1.1.0 (2022-06-03)
### Other Changes
* The one-second floor for `Frequency` when calling `PollUntilDone()` has been removed when running tests.
## 1.0.0 (2022-05-12)
### Features Added
* Added interface `runtime.PollingHandler` to support custom poller implementations.
* Added field `PollingHandler` of this type to `runtime.NewPollerOptions[T]` and `runtime.NewPollerFromResumeTokenOptions[T]`.
### Breaking Changes
* Renamed `cloud.Configuration.LoginEndpoint` to `.ActiveDirectoryAuthorityHost`
* Renamed `cloud.AzurePublicCloud` to `cloud.AzurePublic`
* Removed `AuxiliaryTenants` field from `arm/ClientOptions` and `arm/policy/BearerTokenOptions`
* Removed `TokenRequestOptions.TenantID`
* `Poller[T].PollUntilDone()` now takes an `options *PollUntilDoneOptions` param instead of `freq time.Duration`
* Removed `arm/runtime.Poller[T]`, `arm/runtime.NewPoller[T]()` and `arm/runtime.NewPollerFromResumeToken[T]()`
* Removed `arm/runtime.FinalStateVia` and related `const` values
* Renamed `runtime.PageProcessor` to `runtime.PagingHandler`
* The `arm/runtime.ProviderRepsonse` and `arm/runtime.Provider` types are no longer exported.
* Renamed `NewRequestIdPolicy()` to `NewRequestIDPolicy()`
* `TokenCredential.GetToken` now returns `AccessToken` by value.
### Bugs Fixed
* When per-try timeouts are enabled, only cancel the context after the body has been read and closed.
* The `Operation-Location` poller now properly handles `final-state-via` values.
* Improvements in `runtime.Poller[T]`
* `Poll()` shouldn't cache errors, allowing for additional retries when in a non-terminal state.
* `Result()` will cache the terminal result or error but not transient errors, allowing for additional retries.
### Other Changes
* Updated to latest `internal` module and absorbed breaking changes.
* Use `temporal.Resource` and deleted copy.
* The internal poller implementation has been refactored.
* The implementation in `internal/pollers/poller.go` has been merged into `runtime/poller.go` with some slight modification.
* The internal poller types had their methods updated to conform to the `runtime.PollingHandler` interface.
* The creation of resume tokens has been refactored so that implementers of `runtime.PollingHandler` don't need to know about it.
* `NewPipeline()` places policies from `ClientOptions` after policies from `PipelineOptions`
* Default User-Agent headers no longer include `azcore` version information
## 0.23.1 (2022-04-14)
### Bugs Fixed
* Include XML header when marshalling XML content.
* Handle XML namespaces when searching for error code.
* Handle `odata.error` when searching for error code.
## 0.23.0 (2022-04-04)
### Features Added
* Added `runtime.Pager[T any]` and `runtime.Poller[T any]` supporting types for central, generic, implementations.
* Added `cloud` package with a new API for cloud configuration
* Added `FinalStateVia` field to `runtime.NewPollerOptions[T any]` type.
### Breaking Changes
* Removed the `Poller` type-alias to the internal poller implementation.
* Added `Ptr[T any]` and `SliceOfPtrs[T any]` in the `to` package and removed all non-generic implementations.
* `NullValue` and `IsNullValue` now take a generic type parameter instead of an interface func parameter.
* Replaced `arm.Endpoint` with `cloud` API
* Removed the `endpoint` parameter from `NewRPRegistrationPolicy()`
* `arm/runtime.NewPipeline()` and `.NewRPRegistrationPolicy()` now return an `error`
* Refactored `NewPoller` and `NewPollerFromResumeToken` funcs in `arm/runtime` and `runtime` packages.
* Removed the `pollerID` parameter as it's no longer required.
* Created optional parameter structs and moved optional parameters into them.
* Changed `FinalStateVia` field to a `const` type.
### Other Changes
* Converted expiring resource and dependent types to use generics.
## 0.22.0 (2022-03-03)
### Features Added
* Added header `WWW-Authenticate` to the default allow-list of headers for logging.
* Added a pipeline policy that enables the retrieval of HTTP responses from API calls.
* Added `runtime.WithCaptureResponse` to enable the policy at the API level (off by default).
### Breaking Changes
* Moved `WithHTTPHeader` and `WithRetryOptions` from the `policy` package to the `runtime` package.
## 0.21.1 (2022-02-04)
### Bugs Fixed
* Restore response body after reading in `Poller.FinalResponse()`. (#16911)
* Fixed bug in `NullValue` that could lead to incorrect comparisons for empty maps/slices (#16969)
### Other Changes
* `BearerTokenPolicy` is more resilient to transient authentication failures. (#16789)
## 0.21.0 (2022-01-11)
### Features Added
* Added `AllowedHeaders` and `AllowedQueryParams` to `policy.LogOptions` to control which headers and query parameters are written to the logger.
* Added `azcore.ResponseError` type which is returned from APIs when a non-success HTTP status code is received.
### Breaking Changes
* Moved `[]policy.Policy` parameters of `arm/runtime.NewPipeline` and `runtime.NewPipeline` into a new struct, `runtime.PipelineOptions`
* Renamed `arm/ClientOptions.Host` to `.Endpoint`
* Moved `Request.SkipBodyDownload` method to function `runtime.SkipBodyDownload`
* Removed `azcore.HTTPResponse` interface type
* `arm.NewPoller()` and `runtime.NewPoller()` no longer require an `eu` parameter
* `runtime.NewResponseError()` no longer requires an `error` parameter
## 0.20.0 (2021-10-22)
### Breaking Changes
* Removed `arm.Connection`
* Removed `azcore.Credential` and `.NewAnonymousCredential()`
* `NewRPRegistrationPolicy` now requires an `azcore.TokenCredential`
* `runtime.NewPipeline` has a new signature that simplifies implementing custom authentication
* `arm/runtime.RegistrationOptions` embeds `policy.ClientOptions`
* Contents in the `log` package have been slightly renamed.
* Removed `AuthenticationOptions` in favor of `policy.BearerTokenOptions`
* Changed parameters for `NewBearerTokenPolicy()`
* Moved policy config options out of `arm/runtime` and into `arm/policy`
### Features Added
* Updating Documentation
* Added string typdef `arm.Endpoint` to provide a hint toward expected ARM client endpoints
* `azcore.ClientOptions` contains common pipeline configuration settings
* Added support for multi-tenant authorization in `arm/runtime`
* Require one second minimum when calling `PollUntilDone()`
### Bug Fixes
* Fixed a potential panic when creating the default Transporter.
* Close LRO initial response body when creating a poller.
* Fixed a panic when recursively cloning structs that contain time.Time.
## 0.19.0 (2021-08-25)
### Breaking Changes
* Split content out of `azcore` into various packages. The intent is to separate content based on its usage (common, uncommon, SDK authors).
* `azcore` has all core functionality.
* `log` contains facilities for configuring in-box logging.
* `policy` is used for configuring pipeline options and creating custom pipeline policies.
* `runtime` contains various helpers used by SDK authors and generated content.
* `streaming` has helpers for streaming IO operations.
* `NewTelemetryPolicy()` now requires module and version parameters and the `Value` option has been removed.
* As a result, the `Request.Telemetry()` method has been removed.
* The telemetry policy now includes the SDK prefix `azsdk-go-` so callers no longer need to provide it.
* The `*http.Request` in `runtime.Request` is no longer anonymously embedded. Use the `Raw()` method to access it.
* The `UserAgent` and `Version` constants have been made internal, `Module` and `Version` respectively.
### Bug Fixes
* Fixed an issue in the retry policy where the request body could be overwritten after a rewind.
### Other Changes
* Moved modules `armcore` and `to` content into `arm` and `to` packages respectively.
* The `Pipeline()` method on `armcore.Connection` has been replaced by `NewPipeline()` in `arm.Connection`. It takes module and version parameters used by the telemetry policy.
* Poller logic has been consolidated across ARM and core implementations.
* This required some changes to the internal interfaces for core pollers.
* The core poller types have been improved, including more logging and test coverage.
## 0.18.1 (2021-08-20)
### Features Added
* Adds an `ETag` type for comparing etags and handling etags on requests
* Simplifies the `requestBodyProgess` and `responseBodyProgress` into a single `progress` object
### Bugs Fixed
* `JoinPaths` will preserve query parameters encoded in the `root` url.
### Other Changes
* Bumps dependency on `internal` module to the latest version (v0.7.0)
## 0.18.0 (2021-07-29)
### Features Added
* Replaces methods from Logger type with two package methods for interacting with the logging functionality.
* `azcore.SetClassifications` replaces `azcore.Logger().SetClassifications`
* `azcore.SetListener` replaces `azcore.Logger().SetListener`
### Breaking Changes
* Removes `Logger` type from `azcore`
## 0.17.0 (2021-07-27)
### Features Added
* Adding TenantID to TokenRequestOptions (https://github.com/Azure/azure-sdk-for-go/pull/14879)
* Adding AuxiliaryTenants to AuthenticationOptions (https://github.com/Azure/azure-sdk-for-go/pull/15123)
### Breaking Changes
* Rename `AnonymousCredential` to `NewAnonymousCredential` (https://github.com/Azure/azure-sdk-for-go/pull/15104)
* rename `AuthenticationPolicyOptions` to `AuthenticationOptions` (https://github.com/Azure/azure-sdk-for-go/pull/15103)
* Make Header constants private (https://github.com/Azure/azure-sdk-for-go/pull/15038)
## 0.16.2 (2021-05-26)
### Features Added
* Improved support for byte arrays [#14715](https://github.com/Azure/azure-sdk-for-go/pull/14715)
## 0.16.1 (2021-05-19)
### Features Added
* Add license.txt to azcore module [#14682](https://github.com/Azure/azure-sdk-for-go/pull/14682)
## 0.16.0 (2021-05-07)
### Features Added
* Remove extra `*` in UnmarshalAsByteArray() [#14642](https://github.com/Azure/azure-sdk-for-go/pull/14642)
## 0.15.1 (2021-05-06)
### Features Added
* Cache the original request body on Request [#14634](https://github.com/Azure/azure-sdk-for-go/pull/14634)
## 0.15.0 (2021-05-05)
### Features Added
* Add support for null map and slice
* Export `Response.Payload` method
### Breaking Changes
* remove `Response.UnmarshalError` as it's no longer required
## 0.14.5 (2021-04-23)
### Features Added
* Add `UnmarshalError()` on `azcore.Response`
## 0.14.4 (2021-04-22)
### Features Added
* Support for basic LRO polling
* Added type `LROPoller` and supporting types for basic polling on long running operations.
* rename poller param and added doc comment
### Bugs Fixed
* Fixed content type detection bug in logging.
## 0.14.3 (2021-03-29)
### Features Added
* Add support for multi-part form data
* Added method `WriteMultipartFormData()` to Request.
## 0.14.2 (2021-03-17)
### Features Added
* Add support for encoding JSON null values
* Adds `NullValue()` and `IsNullValue()` functions for setting and detecting sentinel values used for encoding a JSON null.
* Documentation fixes
### Bugs Fixed
* Fixed improper error wrapping
## 0.14.1 (2021-02-08)
### Features Added
* Add `Pager` and `Poller` interfaces to azcore
## 0.14.0 (2021-01-12)
### Features Added
* Accept zero-value options for default values
* Specify zero-value options structs to accept default values.
* Remove `DefaultXxxOptions()` methods.
* Do not silently change TryTimeout on negative values
* make per-try timeout opt-in
## 0.13.4 (2020-11-20)
### Features Added
* Include telemetry string in User Agent
## 0.13.3 (2020-11-20)
### Features Added
* Updating response body handling on `azcore.Response`
## 0.13.2 (2020-11-13)
### Features Added
* Remove implementation of stateless policies as first-class functions.
## 0.13.1 (2020-11-05)
### Features Added
* Add `Telemetry()` method to `azcore.Request()`
## 0.13.0 (2020-10-14)
### Features Added
* Rename `log` to `logger` to avoid name collision with the log package.
* Documentation improvements
* Simplified `DefaultHTTPClientTransport()` implementation
## 0.12.1 (2020-10-13)
### Features Added
* Update `internal` module dependence to `v0.5.0`
## 0.12.0 (2020-10-08)
### Features Added
* Removed storage specific content
* Removed internal content to prevent API clutter
* Refactored various policy options to conform with our options pattern
## 0.11.0 (2020-09-22)
### Features Added
* Removed `LogError` and `LogSlowResponse`.
* Renamed `options` in `RequestLogOptions`.
* Updated `NewRequestLogPolicy()` to follow standard pattern for options.
* Refactored `requestLogPolicy.Do()` per above changes.
* Cleaned up/added logging in retry policy.
* Export `NewResponseError()`
* Fix `RequestLogOptions` comment
## 0.10.1 (2020-09-17)
### Features Added
* Add default console logger
* Default console logger writes to stderr. To enable it, set env var `AZURE_SDK_GO_LOGGING` to the value 'all'.
* Added `Logger.Writef()` to reduce the need for `ShouldLog()` checks.
* Add `LogLongRunningOperation`
## 0.10.0 (2020-09-10)
### Features Added
* The `request` and `transport` interfaces have been refactored to align with the patterns in the standard library.
* `NewRequest()` now uses `http.NewRequestWithContext()` and performs additional validation, it also requires a context parameter.
* The `Policy` and `Transport` interfaces have had their context parameter removed as the context is associated with the underlying `http.Request`.
* `Pipeline.Do()` will validate the HTTP request before sending it through the pipeline, avoiding retries on a malformed request.
* The `Retrier` interface has been replaced with the `NonRetriableError` interface, and the retry policy updated to test for this.
* `Request.SetBody()` now requires a content type parameter for setting the request's MIME type.
* moved path concatenation into `JoinPaths()` func
## 0.9.6 (2020-08-18)
### Features Added
* Improvements to body download policy
* Always download the response body for error responses, i.e. HTTP status codes >= 400.
* Simplify variable declarations
## 0.9.5 (2020-08-11)
### Features Added
* Set the Content-Length header in `Request.SetBody`
## 0.9.4 (2020-08-03)
### Features Added
* Fix cancellation of per try timeout
* Per try timeout is used to ensure that an HTTP operation doesn't take too long, e.g. that a GET on some URL doesn't take an inordinant amount of time.
* Once the HTTP request returns, the per try timeout should be cancelled, not when the response has been read to completion.
* Do not drain response body if there are no more retries
* Do not retry non-idempotent operations when body download fails
## 0.9.3 (2020-07-28)
### Features Added
* Add support for custom HTTP request headers
* Inserts an internal policy into the pipeline that can extract HTTP header values from the caller's context, adding them to the request.
* Use `azcore.WithHTTPHeader` to add HTTP headers to a context.
* Remove method specific to Go 1.14
## 0.9.2 (2020-07-28)
### Features Added
* Omit read-only content from request payloads
* If any field in a payload's object graph contains `azure:"ro"`, make a clone of the object graph, omitting all fields with this annotation.
* Verify no fields were dropped
* Handle embedded struct types
* Added test for cloning by value
* Add messages to failures
## 0.9.1 (2020-07-22)
### Features Added
* Updated dependency on internal module to fix race condition.
## 0.9.0 (2020-07-09)
### Features Added
* Add `HTTPResponse` interface to be used by callers to access the raw HTTP response from an error in the event of an API call failure.
* Updated `sdk/internal` dependency to latest version.
* Rename package alias
## 0.8.2 (2020-06-29)
### Features Added
* Added missing documentation comments
### Bugs Fixed
* Fixed a bug in body download policy.
## 0.8.1 (2020-06-26)
### Features Added
* Miscellaneous clean-up reported by linters
## 0.8.0 (2020-06-01)
### Features Added
* Differentiate between standard and URL encoding.
## 0.7.1 (2020-05-27)
### Features Added
* Add support for for base64 encoding and decoding of payloads.
## 0.7.0 (2020-05-12)
### Features Added
* Change `RetryAfter()` to a function.
## 0.6.0 (2020-04-29)
### Features Added
* Updating `RetryAfter` to only return the detaion in the RetryAfter header
## 0.5.0 (2020-03-23)
### Features Added
* Export `TransportFunc`
### Breaking Changes
* Removed `IterationDone`
## 0.4.1 (2020-02-25)
### Features Added
* Ensure per-try timeout is properly cancelled
* Explicitly call cancel the per-try timeout when the response body has been read/closed by the body download policy.
* When the response body is returned to the caller for reading/closing, wrap it in a `responseBodyReader` that will cancel the timeout when the body is closed.
* `Logger.Should()` will return false if no listener is set.
## 0.4.0 (2020-02-18)
### Features Added
* Enable custom `RetryOptions` to be specified per API call
* Added `WithRetryOptions()` that adds a custom `RetryOptions` to the provided context, allowing custom settings per API call.
* Remove 429 from the list of default HTTP status codes for retry.
* Change StatusCodesForRetry to a slice so consumers can append to it.
* Added support for retry-after in HTTP-date format.
* Cleaned up some comments specific to storage.
* Remove `Request.SetQueryParam()`
* Renamed `MaxTries` to `MaxRetries`
## 0.3.0 (2020-01-16)
### Features Added
* Added `DefaultRetryOptions` to create initialized default options.
### Breaking Changes
* Removed `Response.CheckStatusCode()`
## 0.2.0 (2020-01-15)
### Features Added
* Add support for marshalling and unmarshalling JSON
* Removed `Response.Payload` field
* Exit early when unmarsahlling if there is no payload
## 0.1.0 (2020-01-10)
### Features Added
* Initial release

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

View File

@ -0,0 +1,39 @@
# Azure Core Client Module for Go
[![PkgGoDev](https://pkg.go.dev/badge/github.com/Azure/azure-sdk-for-go/sdk/azcore)](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore)
[![Build Status](https://dev.azure.com/azure-sdk/public/_apis/build/status/go/go%20-%20azcore%20-%20ci?branchName=main)](https://dev.azure.com/azure-sdk/public/_build/latest?definitionId=1843&branchName=main)
[![Code Coverage](https://img.shields.io/azure-devops/coverage/azure-sdk/public/1843/main)](https://img.shields.io/azure-devops/coverage/azure-sdk/public/1843/main)
The `azcore` module provides a set of common interfaces and types for Go SDK client modules.
These modules follow the [Azure SDK Design Guidelines for Go](https://azure.github.io/azure-sdk/golang_introduction.html).
## Getting started
This project uses [Go modules](https://github.com/golang/go/wiki/Modules) for versioning and dependency management.
Typically, you will not need to explicitly install `azcore` as it will be installed as a client module dependency.
To add the latest version to your `go.mod` file, execute the following command.
```bash
go get github.com/Azure/azure-sdk-for-go/sdk/azcore
```
General documentation and examples can be found on [pkg.go.dev](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore).
## Contributing
This project welcomes contributions and suggestions. Most contributions require
you to agree to a Contributor License Agreement (CLA) declaring that you have
the right to, and actually do, grant us the rights to use your contribution.
For details, visit [https://cla.microsoft.com](https://cla.microsoft.com).
When you submit a pull request, a CLA-bot will automatically determine whether
you need to provide a CLA and decorate the PR appropriately (e.g., label,
comment). Simply follow the instructions provided by the bot. You will only
need to do this once across all repos using our CLA.
This project has adopted the
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information, see the
[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any
additional questions or comments.

View File

@ -0,0 +1,224 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package resource
import (
"fmt"
"strings"
)
const (
providersKey = "providers"
subscriptionsKey = "subscriptions"
resourceGroupsLowerKey = "resourcegroups"
locationsKey = "locations"
builtInResourceNamespace = "Microsoft.Resources"
)
// RootResourceID defines the tenant as the root parent of all other ResourceID.
var RootResourceID = &ResourceID{
Parent: nil,
ResourceType: TenantResourceType,
Name: "",
}
// ResourceID represents a resource ID such as `/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg`.
// Don't create this type directly, use ParseResourceID instead.
type ResourceID struct {
// Parent is the parent ResourceID of this instance.
// Can be nil if there is no parent.
Parent *ResourceID
// SubscriptionID is the subscription ID in this resource ID.
// The value can be empty if the resource ID does not contain a subscription ID.
SubscriptionID string
// ResourceGroupName is the resource group name in this resource ID.
// The value can be empty if the resource ID does not contain a resource group name.
ResourceGroupName string
// Provider represents the provider name in this resource ID.
// This is only valid when the resource ID represents a resource provider.
// Example: `/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Insights`
Provider string
// Location is the location in this resource ID.
// The value can be empty if the resource ID does not contain a location name.
Location string
// ResourceType represents the type of this resource ID.
ResourceType ResourceType
// Name is the resource name of this resource ID.
Name string
isChild bool
stringValue string
}
// ParseResourceID parses a string to an instance of ResourceID
func ParseResourceID(id string) (*ResourceID, error) {
if len(id) == 0 {
return nil, fmt.Errorf("invalid resource ID: id cannot be empty")
}
if !strings.HasPrefix(id, "/") {
return nil, fmt.Errorf("invalid resource ID: resource id '%s' must start with '/'", id)
}
parts := splitStringAndOmitEmpty(id, "/")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}
if !strings.EqualFold(parts[0], subscriptionsKey) && !strings.EqualFold(parts[0], providersKey) {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}
return appendNext(RootResourceID, parts, id)
}
// String returns the string of the ResourceID
func (id *ResourceID) String() string {
if len(id.stringValue) > 0 {
return id.stringValue
}
if id.Parent == nil {
return ""
}
builder := strings.Builder{}
builder.WriteString(id.Parent.String())
if id.isChild {
builder.WriteString(fmt.Sprintf("/%s", id.ResourceType.lastType()))
if len(id.Name) > 0 {
builder.WriteString(fmt.Sprintf("/%s", id.Name))
}
} else {
builder.WriteString(fmt.Sprintf("/providers/%s/%s/%s", id.ResourceType.Namespace, id.ResourceType.Type, id.Name))
}
id.stringValue = builder.String()
return id.stringValue
}
func newResourceID(parent *ResourceID, resourceTypeName string, resourceName string) *ResourceID {
id := &ResourceID{}
id.init(parent, chooseResourceType(resourceTypeName, parent), resourceName, true)
return id
}
func newResourceIDWithResourceType(parent *ResourceID, resourceType ResourceType, resourceName string) *ResourceID {
id := &ResourceID{}
id.init(parent, resourceType, resourceName, true)
return id
}
func newResourceIDWithProvider(parent *ResourceID, providerNamespace, resourceTypeName, resourceName string) *ResourceID {
id := &ResourceID{}
id.init(parent, NewResourceType(providerNamespace, resourceTypeName), resourceName, false)
return id
}
func chooseResourceType(resourceTypeName string, parent *ResourceID) ResourceType {
if strings.EqualFold(resourceTypeName, resourceGroupsLowerKey) {
return ResourceGroupResourceType
} else if strings.EqualFold(resourceTypeName, subscriptionsKey) && parent != nil && parent.ResourceType.String() == TenantResourceType.String() {
return SubscriptionResourceType
}
return parent.ResourceType.AppendChild(resourceTypeName)
}
func (id *ResourceID) init(parent *ResourceID, resourceType ResourceType, name string, isChild bool) {
if parent != nil {
id.Provider = parent.Provider
id.SubscriptionID = parent.SubscriptionID
id.ResourceGroupName = parent.ResourceGroupName
id.Location = parent.Location
}
if resourceType.String() == SubscriptionResourceType.String() {
id.SubscriptionID = name
}
if resourceType.lastType() == locationsKey {
id.Location = name
}
if resourceType.String() == ResourceGroupResourceType.String() {
id.ResourceGroupName = name
}
if resourceType.String() == ProviderResourceType.String() {
id.Provider = name
}
if parent == nil {
id.Parent = RootResourceID
} else {
id.Parent = parent
}
id.isChild = isChild
id.ResourceType = resourceType
id.Name = name
}
func appendNext(parent *ResourceID, parts []string, id string) (*ResourceID, error) {
if len(parts) == 0 {
return parent, nil
}
if len(parts) == 1 {
// subscriptions and resourceGroups are not valid ids without their names
if strings.EqualFold(parts[0], subscriptionsKey) || strings.EqualFold(parts[0], resourceGroupsLowerKey) {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}
// resourceGroup must contain either child or provider resource type
if parent.ResourceType.String() == ResourceGroupResourceType.String() {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}
return newResourceID(parent, parts[0], ""), nil
}
if strings.EqualFold(parts[0], providersKey) && (len(parts) == 2 || strings.EqualFold(parts[2], providersKey)) {
//provider resource can only be on a tenant or a subscription parent
if parent.ResourceType.String() != SubscriptionResourceType.String() && parent.ResourceType.String() != TenantResourceType.String() {
return nil, fmt.Errorf("invalid resource ID: %s", id)
}
return appendNext(newResourceIDWithResourceType(parent, ProviderResourceType, parts[1]), parts[2:], id)
}
if len(parts) > 3 && strings.EqualFold(parts[0], providersKey) {
return appendNext(newResourceIDWithProvider(parent, parts[1], parts[2], parts[3]), parts[4:], id)
}
if len(parts) > 1 && !strings.EqualFold(parts[0], providersKey) {
return appendNext(newResourceID(parent, parts[0], parts[1]), parts[2:], id)
}
return nil, fmt.Errorf("invalid resource ID: %s", id)
}
func splitStringAndOmitEmpty(v, sep string) []string {
r := make([]string, 0)
for _, s := range strings.Split(v, sep) {
if len(s) == 0 {
continue
}
r = append(r, s)
}
return r
}

View File

@ -0,0 +1,114 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package resource
import (
"fmt"
"strings"
)
// SubscriptionResourceType is the ResourceType of a subscription
var SubscriptionResourceType = NewResourceType(builtInResourceNamespace, "subscriptions")
// ResourceGroupResourceType is the ResourceType of a resource group
var ResourceGroupResourceType = NewResourceType(builtInResourceNamespace, "resourceGroups")
// TenantResourceType is the ResourceType of a tenant
var TenantResourceType = NewResourceType(builtInResourceNamespace, "tenants")
// ProviderResourceType is the ResourceType of a provider
var ProviderResourceType = NewResourceType(builtInResourceNamespace, "providers")
// ResourceType represents an Azure resource type, e.g. "Microsoft.Network/virtualNetworks/subnets".
// Don't create this type directly, use ParseResourceType or NewResourceType instead.
type ResourceType struct {
// Namespace is the namespace of the resource type.
// e.g. "Microsoft.Network" in resource type "Microsoft.Network/virtualNetworks/subnets"
Namespace string
// Type is the full type name of the resource type.
// e.g. "virtualNetworks/subnets" in resource type "Microsoft.Network/virtualNetworks/subnets"
Type string
// Types is the slice of all the sub-types of this resource type.
// e.g. ["virtualNetworks", "subnets"] in resource type "Microsoft.Network/virtualNetworks/subnets"
Types []string
stringValue string
}
// String returns the string of the ResourceType
func (t ResourceType) String() string {
return t.stringValue
}
// IsParentOf returns true when the receiver is the parent resource type of the child.
func (t ResourceType) IsParentOf(child ResourceType) bool {
if !strings.EqualFold(t.Namespace, child.Namespace) {
return false
}
if len(t.Types) >= len(child.Types) {
return false
}
for i := range t.Types {
if !strings.EqualFold(t.Types[i], child.Types[i]) {
return false
}
}
return true
}
// AppendChild creates an instance of ResourceType using the receiver as the parent with childType appended to it.
func (t ResourceType) AppendChild(childType string) ResourceType {
return NewResourceType(t.Namespace, fmt.Sprintf("%s/%s", t.Type, childType))
}
// NewResourceType creates an instance of ResourceType using a provider namespace
// such as "Microsoft.Network" and type such as "virtualNetworks/subnets".
func NewResourceType(providerNamespace, typeName string) ResourceType {
return ResourceType{
Namespace: providerNamespace,
Type: typeName,
Types: splitStringAndOmitEmpty(typeName, "/"),
stringValue: fmt.Sprintf("%s/%s", providerNamespace, typeName),
}
}
// ParseResourceType parses the ResourceType from a resource type string (e.g. Microsoft.Network/virtualNetworks/subsets)
// or a resource identifier string.
// e.g. /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/mySubnet)
func ParseResourceType(resourceIDOrType string) (ResourceType, error) {
// split the path into segments
parts := splitStringAndOmitEmpty(resourceIDOrType, "/")
// There must be at least a namespace and type name
if len(parts) < 1 {
return ResourceType{}, fmt.Errorf("invalid resource ID or type: %s", resourceIDOrType)
}
// if the type is just subscriptions, it is a built-in type in the Microsoft.Resources namespace
if len(parts) == 1 {
// Simple resource type
return NewResourceType(builtInResourceNamespace, parts[0]), nil
} else if strings.Contains(parts[0], ".") {
// Handle resource types (Microsoft.Compute/virtualMachines, Microsoft.Network/virtualNetworks/subnets)
// it is a full type name
return NewResourceType(parts[0], strings.Join(parts[1:], "/")), nil
} else {
// Check if ResourceID
id, err := ParseResourceID(resourceIDOrType)
if err != nil {
return ResourceType{}, err
}
return NewResourceType(id.ResourceType.Namespace, id.ResourceType.Type), nil
}
}
func (t ResourceType) lastType() string {
return t.Types[len(t.Types)-1]
}

View File

@ -0,0 +1,98 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package policy
import (
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// BearerTokenOptions configures the bearer token policy's behavior.
type BearerTokenOptions struct {
// AuxiliaryTenants are additional tenant IDs for authenticating cross-tenant requests.
// The policy will add a token from each of these tenants to every request. The
// authenticating user or service principal must be a guest in these tenants, and the
// policy's credential must support multitenant authentication.
AuxiliaryTenants []string
// Scopes contains the list of permission scopes required for the token.
Scopes []string
}
// RegistrationOptions configures the registration policy's behavior.
// All zero-value fields will be initialized with their default values.
type RegistrationOptions struct {
policy.ClientOptions
// MaxAttempts is the total number of times to attempt automatic registration
// in the event that an attempt fails.
// The default value is 3.
// Set to a value less than zero to disable the policy.
MaxAttempts int
// PollingDelay is the amount of time to sleep between polling intervals.
// The default value is 15 seconds.
// A value less than zero means no delay between polling intervals (not recommended).
PollingDelay time.Duration
// PollingDuration is the amount of time to wait before abandoning polling.
// The default valule is 5 minutes.
// NOTE: Setting this to a small value might cause the policy to prematurely fail.
PollingDuration time.Duration
}
// ClientOptions contains configuration settings for a client's pipeline.
type ClientOptions struct {
policy.ClientOptions
// AuxiliaryTenants are additional tenant IDs for authenticating cross-tenant requests.
// The client will add a token from each of these tenants to every request. The
// authenticating user or service principal must be a guest in these tenants, and the
// client's credential must support multitenant authentication.
AuxiliaryTenants []string
// DisableRPRegistration disables the auto-RP registration policy. Defaults to false.
DisableRPRegistration bool
}
// Clone return a deep copy of the current options.
func (o *ClientOptions) Clone() *ClientOptions {
if o == nil {
return nil
}
copiedOptions := *o
copiedOptions.Cloud.Services = copyMap(copiedOptions.Cloud.Services)
copiedOptions.Logging.AllowedHeaders = copyArray(copiedOptions.Logging.AllowedHeaders)
copiedOptions.Logging.AllowedQueryParams = copyArray(copiedOptions.Logging.AllowedQueryParams)
copiedOptions.Retry.StatusCodes = copyArray(copiedOptions.Retry.StatusCodes)
copiedOptions.PerRetryPolicies = copyArray(copiedOptions.PerRetryPolicies)
copiedOptions.PerCallPolicies = copyArray(copiedOptions.PerCallPolicies)
return &copiedOptions
}
// copyMap return a new map with all the key value pair in the src map
func copyMap[K comparable, V any](src map[K]V) map[K]V {
if src == nil {
return nil
}
copiedMap := make(map[K]V)
for k, v := range src {
copiedMap[k] = v
}
return copiedMap
}
// copyMap return a new array with all the elements in the src array
func copyArray[T any](src []T) []T {
if src == nil {
return nil
}
copiedArray := make([]T, len(src))
copy(copiedArray, src)
return copiedArray
}

View File

@ -0,0 +1,65 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"errors"
"reflect"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
)
// NewPipeline creates a pipeline from connection options. Policies from ClientOptions are
// placed after policies from PipelineOptions. The telemetry policy, when enabled, will
// use the specified module and version info.
func NewPipeline(module, version string, cred azcore.TokenCredential, plOpts azruntime.PipelineOptions, options *armpolicy.ClientOptions) (azruntime.Pipeline, error) {
if options == nil {
options = &armpolicy.ClientOptions{}
}
conf, err := getConfiguration(&options.ClientOptions)
if err != nil {
return azruntime.Pipeline{}, err
}
authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{
AuxiliaryTenants: options.AuxiliaryTenants,
Scopes: []string{conf.Audience + "/.default"},
})
perRetry := make([]azpolicy.Policy, len(plOpts.PerRetry), len(plOpts.PerRetry)+1)
copy(perRetry, plOpts.PerRetry)
plOpts.PerRetry = append(perRetry, authPolicy, exported.PolicyFunc(httpTraceNamespacePolicy))
if !options.DisableRPRegistration {
regRPOpts := armpolicy.RegistrationOptions{ClientOptions: options.ClientOptions}
regPolicy, err := NewRPRegistrationPolicy(cred, &regRPOpts)
if err != nil {
return azruntime.Pipeline{}, err
}
perCall := make([]azpolicy.Policy, len(plOpts.PerCall), len(plOpts.PerCall)+1)
copy(perCall, plOpts.PerCall)
plOpts.PerCall = append(perCall, regPolicy)
}
if plOpts.APIVersion.Name == "" {
plOpts.APIVersion.Name = "api-version"
}
return azruntime.NewPipeline(module, version, plOpts, &options.ClientOptions), nil
}
func getConfiguration(o *azpolicy.ClientOptions) (cloud.ServiceConfiguration, error) {
c := cloud.AzurePublic
if !reflect.ValueOf(o.Cloud).IsZero() {
c = o.Cloud
}
if conf, ok := c.Services[cloud.ResourceManager]; ok && conf.Endpoint != "" && conf.Audience != "" {
return conf, nil
} else {
return conf, errors.New("provided Cloud field is missing Azure Resource Manager configuration")
}
}

View File

@ -0,0 +1,145 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/internal/temporal"
)
const headerAuxiliaryAuthorization = "x-ms-authorization-auxiliary"
// acquiringResourceState holds data for an auxiliary token request
type acquiringResourceState struct {
ctx context.Context
p *BearerTokenPolicy
tenant string
}
// acquireAuxToken acquires a token from an auxiliary tenant. Only one thread/goroutine at a time ever calls this function.
func acquireAuxToken(state acquiringResourceState) (newResource azcore.AccessToken, newExpiration time.Time, err error) {
tk, err := state.p.cred.GetToken(state.ctx, azpolicy.TokenRequestOptions{
EnableCAE: true,
Scopes: state.p.scopes,
TenantID: state.tenant,
})
if err != nil {
return azcore.AccessToken{}, time.Time{}, err
}
return tk, tk.ExpiresOn, nil
}
// BearerTokenPolicy authorizes requests with bearer tokens acquired from a TokenCredential.
type BearerTokenPolicy struct {
auxResources map[string]*temporal.Resource[azcore.AccessToken, acquiringResourceState]
btp *azruntime.BearerTokenPolicy
cred azcore.TokenCredential
scopes []string
}
// NewBearerTokenPolicy creates a policy object that authorizes requests with bearer tokens.
// cred: an azcore.TokenCredential implementation such as a credential object from azidentity
// opts: optional settings. Pass nil to accept default values; this is the same as passing a zero-value options.
func NewBearerTokenPolicy(cred azcore.TokenCredential, opts *armpolicy.BearerTokenOptions) *BearerTokenPolicy {
if opts == nil {
opts = &armpolicy.BearerTokenOptions{}
}
p := &BearerTokenPolicy{cred: cred}
p.auxResources = make(map[string]*temporal.Resource[azcore.AccessToken, acquiringResourceState], len(opts.AuxiliaryTenants))
for _, t := range opts.AuxiliaryTenants {
p.auxResources[t] = temporal.NewResource(acquireAuxToken)
}
p.scopes = make([]string, len(opts.Scopes))
copy(p.scopes, opts.Scopes)
p.btp = azruntime.NewBearerTokenPolicy(cred, opts.Scopes, &azpolicy.BearerTokenOptions{
AuthorizationHandler: azpolicy.AuthorizationHandler{
OnChallenge: p.onChallenge,
OnRequest: p.onRequest,
},
})
return p
}
func (b *BearerTokenPolicy) onChallenge(req *azpolicy.Request, res *http.Response, authNZ func(azpolicy.TokenRequestOptions) error) error {
challenge := res.Header.Get(shared.HeaderWWWAuthenticate)
claims, err := parseChallenge(challenge)
if err != nil {
// the challenge contains claims we can't parse
return err
} else if claims != "" {
// request a new token having the specified claims, send the request again
return authNZ(azpolicy.TokenRequestOptions{Claims: claims, EnableCAE: true, Scopes: b.scopes})
}
// auth challenge didn't include claims, so this is a simple authorization failure
return azruntime.NewResponseError(res)
}
// onRequest authorizes requests with one or more bearer tokens
func (b *BearerTokenPolicy) onRequest(req *azpolicy.Request, authNZ func(azpolicy.TokenRequestOptions) error) error {
// authorize the request with a token for the primary tenant
err := authNZ(azpolicy.TokenRequestOptions{EnableCAE: true, Scopes: b.scopes})
if err != nil || len(b.auxResources) == 0 {
return err
}
// add tokens for auxiliary tenants
as := acquiringResourceState{
ctx: req.Raw().Context(),
p: b,
}
auxTokens := make([]string, 0, len(b.auxResources))
for tenant, er := range b.auxResources {
as.tenant = tenant
auxTk, err := er.Get(as)
if err != nil {
return err
}
auxTokens = append(auxTokens, fmt.Sprintf("%s%s", shared.BearerTokenPrefix, auxTk.Token))
}
req.Raw().Header.Set(headerAuxiliaryAuthorization, strings.Join(auxTokens, ", "))
return nil
}
// Do authorizes a request with a bearer token
func (b *BearerTokenPolicy) Do(req *azpolicy.Request) (*http.Response, error) {
return b.btp.Do(req)
}
// parseChallenge parses claims from an authentication challenge issued by ARM so a client can request a token
// that will satisfy conditional access policies. It returns a non-nil error when the given value contains
// claims it can't parse. If the value contains no claims, it returns an empty string and a nil error.
func parseChallenge(wwwAuthenticate string) (string, error) {
claims := ""
var err error
for _, param := range strings.Split(wwwAuthenticate, ",") {
if _, after, found := strings.Cut(param, "claims="); found {
if claims != "" {
// The header contains multiple challenges, at least two of which specify claims. The specs allow this
// but it's unclear what a client should do in this case and there's as yet no concrete example of it.
err = fmt.Errorf("found multiple claims challenges in %q", wwwAuthenticate)
break
}
// trim stuff that would get an error from RawURLEncoding; claims may or may not be padded
claims = strings.Trim(after, `\"=`)
// we don't return this error because it's something unhelpful like "illegal base64 data at input byte 42"
if b, decErr := base64.RawURLEncoding.DecodeString(claims); decErr == nil {
claims = string(b)
} else {
err = fmt.Errorf("failed to parse claims from %q", wwwAuthenticate)
break
}
}
}
return claims, err
}

View File

@ -0,0 +1,347 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
const (
// LogRPRegistration entries contain information specific to the automatic registration of an RP.
// Entries of this classification are written IFF the policy needs to take any action.
LogRPRegistration log.Event = "RPRegistration"
)
// init sets any default values
func setDefaults(r *armpolicy.RegistrationOptions) {
if r.MaxAttempts == 0 {
r.MaxAttempts = 3
} else if r.MaxAttempts < 0 {
r.MaxAttempts = 0
}
if r.PollingDelay == 0 {
r.PollingDelay = 15 * time.Second
} else if r.PollingDelay < 0 {
r.PollingDelay = 0
}
if r.PollingDuration == 0 {
r.PollingDuration = 5 * time.Minute
}
}
// NewRPRegistrationPolicy creates a policy object configured using the specified options.
// The policy controls whether an unregistered resource provider should automatically be
// registered. See https://aka.ms/rps-not-found for more information.
func NewRPRegistrationPolicy(cred azcore.TokenCredential, o *armpolicy.RegistrationOptions) (azpolicy.Policy, error) {
if o == nil {
o = &armpolicy.RegistrationOptions{}
}
conf, err := getConfiguration(&o.ClientOptions)
if err != nil {
return nil, err
}
authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{Scopes: []string{conf.Audience + "/.default"}})
p := &rpRegistrationPolicy{
endpoint: conf.Endpoint,
pipeline: runtime.NewPipeline(shared.Module, shared.Version, runtime.PipelineOptions{PerRetry: []azpolicy.Policy{authPolicy}}, &o.ClientOptions),
options: *o,
}
// init the copy
setDefaults(&p.options)
return p, nil
}
type rpRegistrationPolicy struct {
endpoint string
pipeline runtime.Pipeline
options armpolicy.RegistrationOptions
}
func (r *rpRegistrationPolicy) Do(req *azpolicy.Request) (*http.Response, error) {
if r.options.MaxAttempts == 0 {
// policy is disabled
return req.Next()
}
const registeredState = "Registered"
var rp string
var resp *http.Response
for attempts := 0; attempts < r.options.MaxAttempts; attempts++ {
var err error
// make the original request
resp, err = req.Next()
// getting a 409 is the first indication that the RP might need to be registered, check error response
if err != nil || resp.StatusCode != http.StatusConflict {
return resp, err
}
var reqErr requestError
if err = runtime.UnmarshalAsJSON(resp, &reqErr); err != nil {
return resp, err
}
if reqErr.ServiceError == nil {
// missing service error info. just return the response
// to the caller so its error unmarshalling will kick in
return resp, err
}
if !isUnregisteredRPCode(reqErr.ServiceError.Code) {
// not a 409 due to unregistered RP. just return the response
// to the caller so its error unmarshalling will kick in
return resp, err
}
// RP needs to be registered. start by getting the subscription ID from the original request
subID, err := getSubscription(req.Raw().URL.Path)
if err != nil {
return resp, err
}
// now get the RP from the error
rp, err = getProvider(reqErr)
if err != nil {
return resp, err
}
logRegistrationExit := func(v interface{}) {
log.Writef(LogRPRegistration, "END registration for %s: %v", rp, v)
}
log.Writef(LogRPRegistration, "BEGIN registration for %s", rp)
// create client and make the registration request
// we use the scheme and host from the original request
rpOps := &providersOperations{
p: r.pipeline,
u: r.endpoint,
subID: subID,
}
if _, err = rpOps.Register(&shared.ContextWithDeniedValues{Context: req.Raw().Context()}, rp); err != nil {
logRegistrationExit(err)
return resp, err
}
// RP was registered, however we need to wait for the registration to complete
pollCtx, pollCancel := context.WithTimeout(&shared.ContextWithDeniedValues{Context: req.Raw().Context()}, r.options.PollingDuration)
var lastRegState string
for {
// get the current registration state
getResp, err := rpOps.Get(pollCtx, rp)
if err != nil {
pollCancel()
logRegistrationExit(err)
return resp, err
}
if getResp.Provider.RegistrationState != nil && !strings.EqualFold(*getResp.Provider.RegistrationState, lastRegState) {
// registration state has changed, or was updated for the first time
lastRegState = *getResp.Provider.RegistrationState
log.Writef(LogRPRegistration, "registration state is %s", lastRegState)
}
if strings.EqualFold(lastRegState, registeredState) {
// registration complete
pollCancel()
logRegistrationExit(lastRegState)
break
}
// wait before trying again
select {
case <-time.After(r.options.PollingDelay):
// continue polling
case <-pollCtx.Done():
pollCancel()
logRegistrationExit(pollCtx.Err())
return resp, pollCtx.Err()
}
}
// RP was successfully registered, retry the original request
err = req.RewindBody()
if err != nil {
return resp, err
}
}
// if we get here it means we exceeded the number of attempts
return resp, fmt.Errorf("exceeded attempts to register %s", rp)
}
var unregisteredRPCodes = []string{
"MissingSubscriptionRegistration",
"MissingRegistrationForResourceProvider",
"Subscription Not Registered",
"SubscriptionNotRegistered",
}
func isUnregisteredRPCode(errorCode string) bool {
for _, code := range unregisteredRPCodes {
if strings.EqualFold(errorCode, code) {
return true
}
}
return false
}
func getSubscription(path string) (string, error) {
parts := strings.Split(path, "/")
for i, v := range parts {
if v == "subscriptions" && (i+1) < len(parts) {
return parts[i+1], nil
}
}
return "", fmt.Errorf("failed to obtain subscription ID from %s", path)
}
func getProvider(re requestError) (string, error) {
if len(re.ServiceError.Details) > 0 {
return re.ServiceError.Details[0].Target, nil
}
return "", errors.New("unexpected empty Details")
}
// minimal error definitions to simplify detection
type requestError struct {
ServiceError *serviceError `json:"error"`
}
type serviceError struct {
Code string `json:"code"`
Details []serviceErrorDetails `json:"details"`
}
type serviceErrorDetails struct {
Code string `json:"code"`
Target string `json:"target"`
}
///////////////////////////////////////////////////////////////////////////////////////////////
// the following code was copied from module armresources, providers.go and models.go
// only the minimum amount of code was copied to get this working and some edits were made.
///////////////////////////////////////////////////////////////////////////////////////////////
type providersOperations struct {
p runtime.Pipeline
u string
subID string
}
// Get - Gets the specified resource provider.
func (client *providersOperations) Get(ctx context.Context, resourceProviderNamespace string) (providerResponse, error) {
req, err := client.getCreateRequest(ctx, resourceProviderNamespace)
if err != nil {
return providerResponse{}, err
}
resp, err := client.p.Do(req)
if err != nil {
return providerResponse{}, err
}
result, err := client.getHandleResponse(resp)
if err != nil {
return providerResponse{}, err
}
return result, nil
}
// getCreateRequest creates the Get request.
func (client *providersOperations) getCreateRequest(ctx context.Context, resourceProviderNamespace string) (*azpolicy.Request, error) {
urlPath := "/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}"
urlPath = strings.ReplaceAll(urlPath, "{resourceProviderNamespace}", url.PathEscape(resourceProviderNamespace))
urlPath = strings.ReplaceAll(urlPath, "{subscriptionId}", url.PathEscape(client.subID))
req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.u, urlPath))
if err != nil {
return nil, err
}
query := req.Raw().URL.Query()
query.Set("api-version", "2019-05-01")
req.Raw().URL.RawQuery = query.Encode()
return req, nil
}
// getHandleResponse handles the Get response.
func (client *providersOperations) getHandleResponse(resp *http.Response) (providerResponse, error) {
if !runtime.HasStatusCode(resp, http.StatusOK) {
return providerResponse{}, exported.NewResponseError(resp)
}
result := providerResponse{RawResponse: resp}
err := runtime.UnmarshalAsJSON(resp, &result.Provider)
if err != nil {
return providerResponse{}, err
}
return result, err
}
// Register - Registers a subscription with a resource provider.
func (client *providersOperations) Register(ctx context.Context, resourceProviderNamespace string) (providerResponse, error) {
req, err := client.registerCreateRequest(ctx, resourceProviderNamespace)
if err != nil {
return providerResponse{}, err
}
resp, err := client.p.Do(req)
if err != nil {
return providerResponse{}, err
}
result, err := client.registerHandleResponse(resp)
if err != nil {
return providerResponse{}, err
}
return result, nil
}
// registerCreateRequest creates the Register request.
func (client *providersOperations) registerCreateRequest(ctx context.Context, resourceProviderNamespace string) (*azpolicy.Request, error) {
urlPath := "/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}/register"
urlPath = strings.ReplaceAll(urlPath, "{resourceProviderNamespace}", url.PathEscape(resourceProviderNamespace))
urlPath = strings.ReplaceAll(urlPath, "{subscriptionId}", url.PathEscape(client.subID))
req, err := runtime.NewRequest(ctx, http.MethodPost, runtime.JoinPaths(client.u, urlPath))
if err != nil {
return nil, err
}
query := req.Raw().URL.Query()
query.Set("api-version", "2019-05-01")
req.Raw().URL.RawQuery = query.Encode()
return req, nil
}
// registerHandleResponse handles the Register response.
func (client *providersOperations) registerHandleResponse(resp *http.Response) (providerResponse, error) {
if !runtime.HasStatusCode(resp, http.StatusOK) {
return providerResponse{}, exported.NewResponseError(resp)
}
result := providerResponse{RawResponse: resp}
err := runtime.UnmarshalAsJSON(resp, &result.Provider)
if err != nil {
return providerResponse{}, err
}
return result, err
}
// ProviderResponse is the response envelope for operations that return a Provider type.
type providerResponse struct {
// Resource provider information.
Provider *provider
// RawResponse contains the underlying HTTP response.
RawResponse *http.Response
}
// Provider - Resource provider information.
type provider struct {
// The provider ID.
ID *string `json:"id,omitempty"`
// The namespace of the resource provider.
Namespace *string `json:"namespace,omitempty"`
// The registration policy of the resource provider.
RegistrationPolicy *string `json:"registrationPolicy,omitempty"`
// The registration state of the resource provider.
RegistrationState *string `json:"registrationState,omitempty"`
}

View File

@ -0,0 +1,30 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/resource"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
)
// httpTraceNamespacePolicy is a policy that adds the az.namespace attribute to the current Span
func httpTraceNamespacePolicy(req *policy.Request) (resp *http.Response, err error) {
rawTracer := req.Raw().Context().Value(shared.CtxWithTracingTracer{})
if tracer, ok := rawTracer.(tracing.Tracer); ok && tracer.Enabled() {
rt, err := resource.ParseResourceType(req.Raw().URL.Path)
if err == nil {
// add the namespace attribute to the current span
span := tracer.SpanFromContext(req.Raw().Context())
span.SetAttributes(tracing.Attribute{Key: shared.TracingNamespaceAttrName, Value: rt.Namespace})
}
}
return req.Next()
}

View File

@ -0,0 +1,24 @@
//go:build go1.16
// +build go1.16
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
func init() {
cloud.AzureChina.Services[cloud.ResourceManager] = cloud.ServiceConfiguration{
Audience: "https://management.core.chinacloudapi.cn",
Endpoint: "https://management.chinacloudapi.cn",
}
cloud.AzureGovernment.Services[cloud.ResourceManager] = cloud.ServiceConfiguration{
Audience: "https://management.core.usgovcloudapi.net",
Endpoint: "https://management.usgovcloudapi.net",
}
cloud.AzurePublic.Services[cloud.ResourceManager] = cloud.ServiceConfiguration{
Audience: "https://management.core.windows.net/",
Endpoint: "https://management.azure.com",
}
}

View File

@ -0,0 +1,29 @@
# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file.
trigger:
branches:
include:
- main
- feature/*
- hotfix/*
- release/*
paths:
include:
- sdk/azcore/
- eng/
pr:
branches:
include:
- main
- feature/*
- hotfix/*
- release/*
paths:
include:
- sdk/azcore/
- eng/
stages:
- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml
parameters:
ServiceDirectory: azcore

View File

@ -0,0 +1,44 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package cloud
var (
// AzureChina contains configuration for Azure China.
AzureChina = Configuration{
ActiveDirectoryAuthorityHost: "https://login.chinacloudapi.cn/", Services: map[ServiceName]ServiceConfiguration{},
}
// AzureGovernment contains configuration for Azure Government.
AzureGovernment = Configuration{
ActiveDirectoryAuthorityHost: "https://login.microsoftonline.us/", Services: map[ServiceName]ServiceConfiguration{},
}
// AzurePublic contains configuration for Azure Public Cloud.
AzurePublic = Configuration{
ActiveDirectoryAuthorityHost: "https://login.microsoftonline.com/", Services: map[ServiceName]ServiceConfiguration{},
}
)
// ServiceName identifies a cloud service.
type ServiceName string
// ResourceManager is a global constant identifying Azure Resource Manager.
const ResourceManager ServiceName = "resourceManager"
// ServiceConfiguration configures a specific cloud service such as Azure Resource Manager.
type ServiceConfiguration struct {
// Audience is the audience the client will request for its access tokens.
Audience string
// Endpoint is the service's base URL.
Endpoint string
}
// Configuration configures a cloud.
type Configuration struct {
// ActiveDirectoryAuthorityHost is the base URL of the cloud's Azure Active Directory.
ActiveDirectoryAuthorityHost string
// Services contains configuration for the cloud's services.
Services map[ServiceName]ServiceConfiguration
}

View File

@ -0,0 +1,53 @@
//go:build go1.16
// +build go1.16
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/*
Package cloud implements a configuration API for applications deployed to sovereign or private Azure clouds.
Azure SDK client configuration defaults are appropriate for Azure Public Cloud (sometimes referred to as
"Azure Commercial" or simply "Microsoft Azure"). This package enables applications deployed to other
Azure Clouds to configure clients appropriately.
This package contains predefined configuration for well-known sovereign clouds such as Azure Government and
Azure China. Azure SDK clients accept this configuration via the Cloud field of azcore.ClientOptions. For
example, configuring a credential and ARM client for Azure Government:
opts := azcore.ClientOptions{Cloud: cloud.AzureGovernment}
cred, err := azidentity.NewDefaultAzureCredential(
&azidentity.DefaultAzureCredentialOptions{ClientOptions: opts},
)
handle(err)
client, err := armsubscription.NewClient(
cred, &arm.ClientOptions{ClientOptions: opts},
)
handle(err)
Applications deployed to a private cloud such as Azure Stack create a Configuration object with
appropriate values:
c := cloud.Configuration{
ActiveDirectoryAuthorityHost: "https://...",
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
cloud.ResourceManager: {
Audience: "...",
Endpoint: "https://...",
},
},
}
opts := azcore.ClientOptions{Cloud: c}
cred, err := azidentity.NewDefaultAzureCredential(
&azidentity.DefaultAzureCredentialOptions{ClientOptions: opts},
)
handle(err)
client, err := armsubscription.NewClient(
cred, &arm.ClientOptions{ClientOptions: opts},
)
handle(err)
*/
package cloud

View File

@ -0,0 +1,154 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azcore
import (
"reflect"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
)
// AccessToken represents an Azure service bearer access token with expiry information.
type AccessToken = exported.AccessToken
// TokenCredential represents a credential capable of providing an OAuth token.
type TokenCredential = exported.TokenCredential
// KeyCredential contains an authentication key used to authenticate to an Azure service.
type KeyCredential = exported.KeyCredential
// NewKeyCredential creates a new instance of [KeyCredential] with the specified values.
// - key is the authentication key
func NewKeyCredential(key string) *KeyCredential {
return exported.NewKeyCredential(key)
}
// SASCredential contains a shared access signature used to authenticate to an Azure service.
type SASCredential = exported.SASCredential
// NewSASCredential creates a new instance of [SASCredential] with the specified values.
// - sas is the shared access signature
func NewSASCredential(sas string) *SASCredential {
return exported.NewSASCredential(sas)
}
// holds sentinel values used to send nulls
var nullables map[reflect.Type]interface{} = map[reflect.Type]interface{}{}
// NullValue is used to send an explicit 'null' within a request.
// This is typically used in JSON-MERGE-PATCH operations to delete a value.
func NullValue[T any]() T {
t := shared.TypeOfT[T]()
v, found := nullables[t]
if !found {
var o reflect.Value
if k := t.Kind(); k == reflect.Map {
o = reflect.MakeMap(t)
} else if k == reflect.Slice {
// empty slices appear to all point to the same data block
// which causes comparisons to become ambiguous. so we create
// a slice with len/cap of one which ensures a unique address.
o = reflect.MakeSlice(t, 1, 1)
} else {
o = reflect.New(t.Elem())
}
v = o.Interface()
nullables[t] = v
}
// return the sentinel object
return v.(T)
}
// IsNullValue returns true if the field contains a null sentinel value.
// This is used by custom marshallers to properly encode a null value.
func IsNullValue[T any](v T) bool {
// see if our map has a sentinel object for this *T
t := reflect.TypeOf(v)
if o, found := nullables[t]; found {
o1 := reflect.ValueOf(o)
v1 := reflect.ValueOf(v)
// we found it; return true if v points to the sentinel object.
// NOTE: maps and slices can only be compared to nil, else you get
// a runtime panic. so we compare addresses instead.
return o1.Pointer() == v1.Pointer()
}
// no sentinel object for this *t
return false
}
// ClientOptions contains optional settings for a client's pipeline.
// Instances can be shared across calls to SDK client constructors when uniform configuration is desired.
// Zero-value fields will have their specified default values applied during use.
type ClientOptions = policy.ClientOptions
// Client is a basic HTTP client. It consists of a pipeline and tracing provider.
type Client struct {
pl runtime.Pipeline
tr tracing.Tracer
// cached on the client to support shallow copying with new values
tp tracing.Provider
modVer string
namespace string
}
// NewClient creates a new Client instance with the provided values.
// - moduleName - the fully qualified name of the module where the client is defined; used by the telemetry policy and tracing provider.
// - moduleVersion - the semantic version of the module; used by the telemetry policy and tracing provider.
// - plOpts - pipeline configuration options; can be the zero-value
// - options - optional client configurations; pass nil to accept the default values
func NewClient(moduleName, moduleVersion string, plOpts runtime.PipelineOptions, options *ClientOptions) (*Client, error) {
if options == nil {
options = &ClientOptions{}
}
if !options.Telemetry.Disabled {
if err := shared.ValidateModVer(moduleVersion); err != nil {
return nil, err
}
}
pl := runtime.NewPipeline(moduleName, moduleVersion, plOpts, options)
tr := options.TracingProvider.NewTracer(moduleName, moduleVersion)
if tr.Enabled() && plOpts.Tracing.Namespace != "" {
tr.SetAttributes(tracing.Attribute{Key: shared.TracingNamespaceAttrName, Value: plOpts.Tracing.Namespace})
}
return &Client{
pl: pl,
tr: tr,
tp: options.TracingProvider,
modVer: moduleVersion,
namespace: plOpts.Tracing.Namespace,
}, nil
}
// Pipeline returns the pipeline for this client.
func (c *Client) Pipeline() runtime.Pipeline {
return c.pl
}
// Tracer returns the tracer for this client.
func (c *Client) Tracer() tracing.Tracer {
return c.tr
}
// WithClientName returns a shallow copy of the Client with its tracing client name changed to clientName.
// Note that the values for module name and version will be preserved from the source Client.
// - clientName - the fully qualified name of the client ("package.Client"); this is used by the tracing provider when creating spans
func (c *Client) WithClientName(clientName string) *Client {
tr := c.tp.NewTracer(clientName, c.modVer)
if tr.Enabled() && c.namespace != "" {
tr.SetAttributes(tracing.Attribute{Key: shared.TracingNamespaceAttrName, Value: c.namespace})
}
return &Client{pl: c.pl, tr: tr, tp: c.tp, modVer: c.modVer, namespace: c.namespace}
}

View File

@ -0,0 +1,264 @@
//go:build go1.18
// +build go1.18
// Copyright 2017 Microsoft Corporation. All rights reserved.
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
/*
Package azcore implements an HTTP request/response middleware pipeline used by Azure SDK clients.
The middleware consists of three components.
- One or more Policy instances.
- A Transporter instance.
- A Pipeline instance that combines the Policy and Transporter instances.
# Implementing the Policy Interface
A Policy can be implemented in two ways; as a first-class function for a stateless Policy, or as
a method on a type for a stateful Policy. Note that HTTP requests made via the same pipeline share
the same Policy instances, so if a Policy mutates its state it MUST be properly synchronized to
avoid race conditions.
A Policy's Do method is called when an HTTP request wants to be sent over the network. The Do method can
perform any operation(s) it desires. For example, it can log the outgoing request, mutate the URL, headers,
and/or query parameters, inject a failure, etc. Once the Policy has successfully completed its request
work, it must call the Next() method on the *policy.Request instance in order to pass the request to the
next Policy in the chain.
When an HTTP response comes back, the Policy then gets a chance to process the response/error. The Policy instance
can log the response, retry the operation if it failed due to a transient error or timeout, unmarshal the response
body, etc. Once the Policy has successfully completed its response work, it must return the *http.Response
and error instances to its caller.
Template for implementing a stateless Policy:
type policyFunc func(*policy.Request) (*http.Response, error)
// Do implements the Policy interface on policyFunc.
func (pf policyFunc) Do(req *policy.Request) (*http.Response, error) {
return pf(req)
}
func NewMyStatelessPolicy() policy.Policy {
return policyFunc(func(req *policy.Request) (*http.Response, error) {
// TODO: mutate/process Request here
// forward Request to next Policy & get Response/error
resp, err := req.Next()
// TODO: mutate/process Response/error here
// return Response/error to previous Policy
return resp, err
})
}
Template for implementing a stateful Policy:
type MyStatefulPolicy struct {
// TODO: add configuration/setting fields here
}
// TODO: add initialization args to NewMyStatefulPolicy()
func NewMyStatefulPolicy() policy.Policy {
return &MyStatefulPolicy{
// TODO: initialize configuration/setting fields here
}
}
func (p *MyStatefulPolicy) Do(req *policy.Request) (resp *http.Response, err error) {
// TODO: mutate/process Request here
// forward Request to next Policy & get Response/error
resp, err := req.Next()
// TODO: mutate/process Response/error here
// return Response/error to previous Policy
return resp, err
}
# Implementing the Transporter Interface
The Transporter interface is responsible for sending the HTTP request and returning the corresponding
HTTP response or error. The Transporter is invoked by the last Policy in the chain. The default Transporter
implementation uses a shared http.Client from the standard library.
The same stateful/stateless rules for Policy implementations apply to Transporter implementations.
# Using Policy and Transporter Instances Via a Pipeline
To use the Policy and Transporter instances, an application passes them to the runtime.NewPipeline function.
func NewPipeline(transport Transporter, policies ...Policy) Pipeline
The specified Policy instances form a chain and are invoked in the order provided to NewPipeline
followed by the Transporter.
Once the Pipeline has been created, create a runtime.Request instance and pass it to Pipeline's Do method.
func NewRequest(ctx context.Context, httpMethod string, endpoint string) (*Request, error)
func (p Pipeline) Do(req *Request) (*http.Request, error)
The Pipeline.Do method sends the specified Request through the chain of Policy and Transporter
instances. The response/error is then sent through the same chain of Policy instances in reverse
order. For example, assuming there are Policy types PolicyA, PolicyB, and PolicyC along with
TransportA.
pipeline := NewPipeline(TransportA, PolicyA, PolicyB, PolicyC)
The flow of Request and Response looks like the following:
policy.Request -> PolicyA -> PolicyB -> PolicyC -> TransportA -----+
|
HTTP(S) endpoint
|
caller <--------- PolicyA <- PolicyB <- PolicyC <- http.Response-+
# Creating a Request Instance
The Request instance passed to Pipeline's Do method is a wrapper around an *http.Request. It also
contains some internal state and provides various convenience methods. You create a Request instance
by calling the runtime.NewRequest function:
func NewRequest(ctx context.Context, httpMethod string, endpoint string) (*Request, error)
If the Request should contain a body, call the SetBody method.
func (req *Request) SetBody(body ReadSeekCloser, contentType string) error
A seekable stream is required so that upon retry, the retry Policy instance can seek the stream
back to the beginning before retrying the network request and re-uploading the body.
# Sending an Explicit Null
Operations like JSON-MERGE-PATCH send a JSON null to indicate a value should be deleted.
{
"delete-me": null
}
This requirement conflicts with the SDK's default marshalling that specifies "omitempty" as
a means to resolve the ambiguity between a field to be excluded and its zero-value.
type Widget struct {
Name *string `json:",omitempty"`
Count *int `json:",omitempty"`
}
In the above example, Name and Count are defined as pointer-to-type to disambiguate between
a missing value (nil) and a zero-value (0) which might have semantic differences.
In a PATCH operation, any fields left as nil are to have their values preserved. When updating
a Widget's count, one simply specifies the new value for Count, leaving Name nil.
To fulfill the requirement for sending a JSON null, the NullValue() function can be used.
w := Widget{
Count: azcore.NullValue[*int](),
}
This sends an explict "null" for Count, indicating that any current value for Count should be deleted.
# Processing the Response
When the HTTP response is received, the *http.Response is returned directly. Each Policy instance
can inspect/mutate the *http.Response.
# Built-in Logging
To enable logging, set environment variable AZURE_SDK_GO_LOGGING to "all" before executing your program.
By default the logger writes to stderr. This can be customized by calling log.SetListener, providing
a callback that writes to the desired location. Any custom logging implementation MUST provide its
own synchronization to handle concurrent invocations.
See the docs for the log package for further details.
# Pageable Operations
Pageable operations return potentially large data sets spread over multiple GET requests. The result of
each GET is a "page" of data consisting of a slice of items.
Pageable operations can be identified by their New*Pager naming convention and return type of *runtime.Pager[T].
func (c *WidgetClient) NewListWidgetsPager(o *Options) *runtime.Pager[PageResponse]
The call to WidgetClient.NewListWidgetsPager() returns an instance of *runtime.Pager[T] for fetching pages
and determining if there are more pages to fetch. No IO calls are made until the NextPage() method is invoked.
pager := widgetClient.NewListWidgetsPager(nil)
for pager.More() {
page, err := pager.NextPage(context.TODO())
// handle err
for _, widget := range page.Values {
// process widget
}
}
# Long-Running Operations
Long-running operations (LROs) are operations consisting of an initial request to start the operation followed
by polling to determine when the operation has reached a terminal state. An LRO's terminal state is one
of the following values.
- Succeeded - the LRO completed successfully
- Failed - the LRO failed to complete
- Canceled - the LRO was canceled
LROs can be identified by their Begin* prefix and their return type of *runtime.Poller[T].
func (c *WidgetClient) BeginCreateOrUpdate(ctx context.Context, w Widget, o *Options) (*runtime.Poller[Response], error)
When a call to WidgetClient.BeginCreateOrUpdate() returns a nil error, it means that the LRO has started.
It does _not_ mean that the widget has been created or updated (or failed to be created/updated).
The *runtime.Poller[T] provides APIs for determining the state of the LRO. To wait for the LRO to complete,
call the PollUntilDone() method.
poller, err := widgetClient.BeginCreateOrUpdate(context.TODO(), Widget{}, nil)
// handle err
result, err := poller.PollUntilDone(context.TODO(), nil)
// handle err
// use result
The call to PollUntilDone() will block the current goroutine until the LRO has reached a terminal state or the
context is canceled/timed out.
Note that LROs can take anywhere from several seconds to several minutes. The duration is operation-dependent. Due to
this variant behavior, pollers do _not_ have a preconfigured time-out. Use a context with the appropriate cancellation
mechanism as required.
# Resume Tokens
Pollers provide the ability to serialize their state into a "resume token" which can be used by another process to
recreate the poller. This is achieved via the runtime.Poller[T].ResumeToken() method.
token, err := poller.ResumeToken()
// handle error
Note that a token can only be obtained for a poller that's in a non-terminal state. Also note that any subsequent calls
to poller.Poll() might change the poller's state. In this case, a new token should be created.
After the token has been obtained, it can be used to recreate an instance of the originating poller.
poller, err := widgetClient.BeginCreateOrUpdate(nil, Widget{}, &Options{
ResumeToken: token,
})
When resuming a poller, no IO is performed, and zero-value arguments can be used for everything but the Options.ResumeToken.
Resume tokens are unique per service client and operation. Attempting to resume a poller for LRO BeginB() with a token from LRO
BeginA() will result in an error.
# Fakes
The fake package contains types used for constructing in-memory fake servers used in unit tests.
This allows writing tests to cover various success/error conditions without the need for connecting to a live service.
Please see https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/samples/fakes for details and examples on how to use fakes.
*/
package azcore

View File

@ -0,0 +1,14 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azcore
import "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
// ResponseError is returned when a request is made to a service and
// the service returns a non-success HTTP status code.
// Use errors.As() to access this type in the error chain.
type ResponseError = exported.ResponseError

View File

@ -0,0 +1,48 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azcore
import (
"strings"
)
// ETag is a property used for optimistic concurrency during updates
// ETag is a validator based on https://tools.ietf.org/html/rfc7232#section-2.3.2
// An ETag can be empty ("").
type ETag string
// ETagAny is an ETag that represents everything, the value is "*"
const ETagAny ETag = "*"
// Equals does a strong comparison of two ETags. Equals returns true when both
// ETags are not weak and the values of the underlying strings are equal.
func (e ETag) Equals(other ETag) bool {
return !e.IsWeak() && !other.IsWeak() && e == other
}
// WeakEquals does a weak comparison of two ETags. Two ETags are equivalent if their opaque-tags match
// character-by-character, regardless of either or both being tagged as "weak".
func (e ETag) WeakEquals(other ETag) bool {
getStart := func(e1 ETag) int {
if e1.IsWeak() {
return 2
}
return 0
}
aStart := getStart(e)
bStart := getStart(other)
aVal := e[aStart:]
bVal := other[bStart:]
return aVal == bVal
}
// IsWeak specifies whether the ETag is strong or weak.
func (e ETag) IsWeak() bool {
return len(e) >= 4 && strings.HasPrefix(string(e), "W/\"") && strings.HasSuffix(string(e), "\"")
}

View File

@ -0,0 +1,175 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package exported
import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
"sync/atomic"
"time"
)
type nopCloser struct {
io.ReadSeeker
}
func (n nopCloser) Close() error {
return nil
}
// NopCloser returns a ReadSeekCloser with a no-op close method wrapping the provided io.ReadSeeker.
// Exported as streaming.NopCloser().
func NopCloser(rs io.ReadSeeker) io.ReadSeekCloser {
return nopCloser{rs}
}
// HasStatusCode returns true if the Response's status code is one of the specified values.
// Exported as runtime.HasStatusCode().
func HasStatusCode(resp *http.Response, statusCodes ...int) bool {
if resp == nil {
return false
}
for _, sc := range statusCodes {
if resp.StatusCode == sc {
return true
}
}
return false
}
// AccessToken represents an Azure service bearer access token with expiry information.
// Exported as azcore.AccessToken.
type AccessToken struct {
Token string
ExpiresOn time.Time
}
// TokenRequestOptions contain specific parameter that may be used by credentials types when attempting to get a token.
// Exported as policy.TokenRequestOptions.
type TokenRequestOptions struct {
// Claims are any additional claims required for the token to satisfy a conditional access policy, such as a
// service may return in a claims challenge following an authorization failure. If a service returned the
// claims value base64 encoded, it must be decoded before setting this field.
Claims string
// EnableCAE indicates whether to enable Continuous Access Evaluation (CAE) for the requested token. When true,
// azidentity credentials request CAE tokens for resource APIs supporting CAE. Clients are responsible for
// handling CAE challenges. If a client that doesn't handle CAE challenges receives a CAE token, it may end up
// in a loop retrying an API call with a token that has been revoked due to CAE.
EnableCAE bool
// Scopes contains the list of permission scopes required for the token.
Scopes []string
// TenantID identifies the tenant from which to request the token. azidentity credentials authenticate in
// their configured default tenants when this field isn't set.
TenantID string
}
// TokenCredential represents a credential capable of providing an OAuth token.
// Exported as azcore.TokenCredential.
type TokenCredential interface {
// GetToken requests an access token for the specified set of scopes.
GetToken(ctx context.Context, options TokenRequestOptions) (AccessToken, error)
}
// DecodeByteArray will base-64 decode the provided string into v.
// Exported as runtime.DecodeByteArray()
func DecodeByteArray(s string, v *[]byte, format Base64Encoding) error {
if len(s) == 0 {
return nil
}
payload := string(s)
if payload[0] == '"' {
// remove surrounding quotes
payload = payload[1 : len(payload)-1]
}
switch format {
case Base64StdFormat:
decoded, err := base64.StdEncoding.DecodeString(payload)
if err == nil {
*v = decoded
return nil
}
return err
case Base64URLFormat:
// use raw encoding as URL format should not contain any '=' characters
decoded, err := base64.RawURLEncoding.DecodeString(payload)
if err == nil {
*v = decoded
return nil
}
return err
default:
return fmt.Errorf("unrecognized byte array format: %d", format)
}
}
// KeyCredential contains an authentication key used to authenticate to an Azure service.
// Exported as azcore.KeyCredential.
type KeyCredential struct {
cred *keyCredential
}
// NewKeyCredential creates a new instance of [KeyCredential] with the specified values.
// - key is the authentication key
func NewKeyCredential(key string) *KeyCredential {
return &KeyCredential{cred: newKeyCredential(key)}
}
// Update replaces the existing key with the specified value.
func (k *KeyCredential) Update(key string) {
k.cred.Update(key)
}
// SASCredential contains a shared access signature used to authenticate to an Azure service.
// Exported as azcore.SASCredential.
type SASCredential struct {
cred *keyCredential
}
// NewSASCredential creates a new instance of [SASCredential] with the specified values.
// - sas is the shared access signature
func NewSASCredential(sas string) *SASCredential {
return &SASCredential{cred: newKeyCredential(sas)}
}
// Update replaces the existing shared access signature with the specified value.
func (k *SASCredential) Update(sas string) {
k.cred.Update(sas)
}
// KeyCredentialGet returns the key for cred.
func KeyCredentialGet(cred *KeyCredential) string {
return cred.cred.Get()
}
// SASCredentialGet returns the shared access sig for cred.
func SASCredentialGet(cred *SASCredential) string {
return cred.cred.Get()
}
type keyCredential struct {
key atomic.Value // string
}
func newKeyCredential(key string) *keyCredential {
keyCred := keyCredential{}
keyCred.key.Store(key)
return &keyCred
}
func (k *keyCredential) Get() string {
return k.key.Load().(string)
}
func (k *keyCredential) Update(key string) {
k.key.Store(key)
}

View File

@ -0,0 +1,77 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package exported
import (
"errors"
"net/http"
)
// Policy represents an extensibility point for the Pipeline that can mutate the specified
// Request and react to the received Response.
// Exported as policy.Policy.
type Policy interface {
// Do applies the policy to the specified Request. When implementing a Policy, mutate the
// request before calling req.Next() to move on to the next policy, and respond to the result
// before returning to the caller.
Do(req *Request) (*http.Response, error)
}
// Pipeline represents a primitive for sending HTTP requests and receiving responses.
// Its behavior can be extended by specifying policies during construction.
// Exported as runtime.Pipeline.
type Pipeline struct {
policies []Policy
}
// Transporter represents an HTTP pipeline transport used to send HTTP requests and receive responses.
// Exported as policy.Transporter.
type Transporter interface {
// Do sends the HTTP request and returns the HTTP response or error.
Do(req *http.Request) (*http.Response, error)
}
// used to adapt a TransportPolicy to a Policy
type transportPolicy struct {
trans Transporter
}
func (tp transportPolicy) Do(req *Request) (*http.Response, error) {
if tp.trans == nil {
return nil, errors.New("missing transporter")
}
resp, err := tp.trans.Do(req.Raw())
if err != nil {
return nil, err
} else if resp == nil {
// there was no response and no error (rare but can happen)
// this ensures the retry policy will retry the request
return nil, errors.New("received nil response")
}
return resp, nil
}
// NewPipeline creates a new Pipeline object from the specified Policies.
// Not directly exported, but used as part of runtime.NewPipeline().
func NewPipeline(transport Transporter, policies ...Policy) Pipeline {
// transport policy must always be the last in the slice
policies = append(policies, transportPolicy{trans: transport})
return Pipeline{
policies: policies,
}
}
// Do is called for each and every HTTP request. It passes the request through all
// the Policy objects (which can transform the Request's URL/query parameters/headers)
// and ultimately sends the transformed HTTP request over the network.
func (p Pipeline) Do(req *Request) (*http.Response, error) {
if req == nil {
return nil, errors.New("request cannot be nil")
}
req.policies = p.policies
return req.Next()
}

View File

@ -0,0 +1,213 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package exported
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
)
// Base64Encoding is usesd to specify which base-64 encoder/decoder to use when
// encoding/decoding a slice of bytes to/from a string.
// Exported as runtime.Base64Encoding
type Base64Encoding int
const (
// Base64StdFormat uses base64.StdEncoding for encoding and decoding payloads.
Base64StdFormat Base64Encoding = 0
// Base64URLFormat uses base64.RawURLEncoding for encoding and decoding payloads.
Base64URLFormat Base64Encoding = 1
)
// EncodeByteArray will base-64 encode the byte slice v.
// Exported as runtime.EncodeByteArray()
func EncodeByteArray(v []byte, format Base64Encoding) string {
if format == Base64URLFormat {
return base64.RawURLEncoding.EncodeToString(v)
}
return base64.StdEncoding.EncodeToString(v)
}
// Request is an abstraction over the creation of an HTTP request as it passes through the pipeline.
// Don't use this type directly, use NewRequest() instead.
// Exported as policy.Request.
type Request struct {
req *http.Request
body io.ReadSeekCloser
policies []Policy
values opValues
}
type opValues map[reflect.Type]interface{}
// Set adds/changes a value
func (ov opValues) set(value interface{}) {
ov[reflect.TypeOf(value)] = value
}
// Get looks for a value set by SetValue first
func (ov opValues) get(value interface{}) bool {
v, ok := ov[reflect.ValueOf(value).Elem().Type()]
if ok {
reflect.ValueOf(value).Elem().Set(reflect.ValueOf(v))
}
return ok
}
// NewRequest creates a new Request with the specified input.
// Exported as runtime.NewRequest().
func NewRequest(ctx context.Context, httpMethod string, endpoint string) (*Request, error) {
req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, nil)
if err != nil {
return nil, err
}
if req.URL.Host == "" {
return nil, errors.New("no Host in request URL")
}
if !(req.URL.Scheme == "http" || req.URL.Scheme == "https") {
return nil, fmt.Errorf("unsupported protocol scheme %s", req.URL.Scheme)
}
return &Request{req: req}, nil
}
// Body returns the original body specified when the Request was created.
func (req *Request) Body() io.ReadSeekCloser {
return req.body
}
// Raw returns the underlying HTTP request.
func (req *Request) Raw() *http.Request {
return req.req
}
// Next calls the next policy in the pipeline.
// If there are no more policies, nil and an error are returned.
// This method is intended to be called from pipeline policies.
// To send a request through a pipeline call Pipeline.Do().
func (req *Request) Next() (*http.Response, error) {
if len(req.policies) == 0 {
return nil, errors.New("no more policies")
}
nextPolicy := req.policies[0]
nextReq := *req
nextReq.policies = nextReq.policies[1:]
return nextPolicy.Do(&nextReq)
}
// SetOperationValue adds/changes a mutable key/value associated with a single operation.
func (req *Request) SetOperationValue(value interface{}) {
if req.values == nil {
req.values = opValues{}
}
req.values.set(value)
}
// OperationValue looks for a value set by SetOperationValue().
func (req *Request) OperationValue(value interface{}) bool {
if req.values == nil {
return false
}
return req.values.get(value)
}
// SetBody sets the specified ReadSeekCloser as the HTTP request body, and sets Content-Type and Content-Length
// accordingly. If the ReadSeekCloser is nil or empty, Content-Length won't be set. If contentType is "",
// Content-Type won't be set.
// Use streaming.NopCloser to turn an io.ReadSeeker into an io.ReadSeekCloser.
func (req *Request) SetBody(body io.ReadSeekCloser, contentType string) error {
var err error
var size int64
if body != nil {
size, err = body.Seek(0, io.SeekEnd) // Seek to the end to get the stream's size
if err != nil {
return err
}
}
if size == 0 {
// treat an empty stream the same as a nil one: assign req a nil body
body = nil
// RFC 9110 specifies a client shouldn't set Content-Length on a request containing no content
// (Del is a no-op when the header has no value)
req.req.Header.Del(shared.HeaderContentLength)
} else {
_, err = body.Seek(0, io.SeekStart)
if err != nil {
return err
}
req.req.Header.Set(shared.HeaderContentLength, strconv.FormatInt(size, 10))
req.Raw().GetBody = func() (io.ReadCloser, error) {
_, err := body.Seek(0, io.SeekStart) // Seek back to the beginning of the stream
return body, err
}
}
// keep a copy of the body argument. this is to handle cases
// where req.Body is replaced, e.g. httputil.DumpRequest and friends.
req.body = body
req.req.Body = body
req.req.ContentLength = size
if contentType == "" {
// Del is a no-op when the header has no value
req.req.Header.Del(shared.HeaderContentType)
} else {
req.req.Header.Set(shared.HeaderContentType, contentType)
}
return nil
}
// RewindBody seeks the request's Body stream back to the beginning so it can be resent when retrying an operation.
func (req *Request) RewindBody() error {
if req.body != nil {
// Reset the stream back to the beginning and restore the body
_, err := req.body.Seek(0, io.SeekStart)
req.req.Body = req.body
return err
}
return nil
}
// Close closes the request body.
func (req *Request) Close() error {
if req.body == nil {
return nil
}
return req.body.Close()
}
// Clone returns a deep copy of the request with its context changed to ctx.
func (req *Request) Clone(ctx context.Context) *Request {
r2 := *req
r2.req = req.req.Clone(ctx)
return &r2
}
// WithContext returns a shallow copy of the request with its context changed to ctx.
func (req *Request) WithContext(ctx context.Context) *Request {
r2 := new(Request)
*r2 = *req
r2.req = r2.req.WithContext(ctx)
return r2
}
// not exported but dependent on Request
// PolicyFunc is a type that implements the Policy interface.
// Use this type when implementing a stateless policy as a first-class function.
type PolicyFunc func(*Request) (*http.Response, error)
// Do implements the Policy interface on policyFunc.
func (pf PolicyFunc) Do(req *Request) (*http.Response, error) {
return pf(req)
}

View File

@ -0,0 +1,157 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package exported
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"regexp"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/internal/exported"
)
// NewResponseError creates a new *ResponseError from the provided HTTP response.
// Exported as runtime.NewResponseError().
func NewResponseError(resp *http.Response) error {
respErr := &ResponseError{
StatusCode: resp.StatusCode,
RawResponse: resp,
}
// prefer the error code in the response header
if ec := resp.Header.Get(shared.HeaderXMSErrorCode); ec != "" {
respErr.ErrorCode = ec
return respErr
}
// if we didn't get x-ms-error-code, check in the response body
body, err := exported.Payload(resp, nil)
if err != nil {
return err
}
if len(body) > 0 {
if code := extractErrorCodeJSON(body); code != "" {
respErr.ErrorCode = code
} else if code := extractErrorCodeXML(body); code != "" {
respErr.ErrorCode = code
}
}
return respErr
}
func extractErrorCodeJSON(body []byte) string {
var rawObj map[string]interface{}
if err := json.Unmarshal(body, &rawObj); err != nil {
// not a JSON object
return ""
}
// check if this is a wrapped error, i.e. { "error": { ... } }
// if so then unwrap it
if wrapped, ok := rawObj["error"]; ok {
unwrapped, ok := wrapped.(map[string]interface{})
if !ok {
return ""
}
rawObj = unwrapped
} else if wrapped, ok := rawObj["odata.error"]; ok {
// check if this a wrapped odata error, i.e. { "odata.error": { ... } }
unwrapped, ok := wrapped.(map[string]any)
if !ok {
return ""
}
rawObj = unwrapped
}
// now check for the error code
code, ok := rawObj["code"]
if !ok {
return ""
}
codeStr, ok := code.(string)
if !ok {
return ""
}
return codeStr
}
func extractErrorCodeXML(body []byte) string {
// regular expression is much easier than dealing with the XML parser
rx := regexp.MustCompile(`<(?:\w+:)?[c|C]ode>\s*(\w+)\s*<\/(?:\w+:)?[c|C]ode>`)
res := rx.FindStringSubmatch(string(body))
if len(res) != 2 {
return ""
}
// first submatch is the entire thing, second one is the captured error code
return res[1]
}
// ResponseError is returned when a request is made to a service and
// the service returns a non-success HTTP status code.
// Use errors.As() to access this type in the error chain.
// Exported as azcore.ResponseError.
type ResponseError struct {
// ErrorCode is the error code returned by the resource provider if available.
ErrorCode string
// StatusCode is the HTTP status code as defined in https://pkg.go.dev/net/http#pkg-constants.
StatusCode int
// RawResponse is the underlying HTTP response.
RawResponse *http.Response
}
// Error implements the error interface for type ResponseError.
// Note that the message contents are not contractual and can change over time.
func (e *ResponseError) Error() string {
const separator = "--------------------------------------------------------------------------------"
// write the request method and URL with response status code
msg := &bytes.Buffer{}
if e.RawResponse != nil {
if e.RawResponse.Request != nil {
fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.Path)
} else {
fmt.Fprintln(msg, "Request information not available")
}
fmt.Fprintln(msg, separator)
fmt.Fprintf(msg, "RESPONSE %d: %s\n", e.RawResponse.StatusCode, e.RawResponse.Status)
} else {
fmt.Fprintln(msg, "Missing RawResponse")
fmt.Fprintln(msg, separator)
}
if e.ErrorCode != "" {
fmt.Fprintf(msg, "ERROR CODE: %s\n", e.ErrorCode)
} else {
fmt.Fprintln(msg, "ERROR CODE UNAVAILABLE")
}
if e.RawResponse != nil {
fmt.Fprintln(msg, separator)
body, err := exported.Payload(e.RawResponse, nil)
if err != nil {
// this really shouldn't fail at this point as the response
// body is already cached (it was read in NewResponseError)
fmt.Fprintf(msg, "Error reading response body: %v", err)
} else if len(body) > 0 {
if err := json.Indent(msg, body, "", " "); err != nil {
// failed to pretty-print so just dump it verbatim
fmt.Fprint(msg, string(body))
}
// the standard library doesn't have a pretty-printer for XML
fmt.Fprintln(msg)
} else {
fmt.Fprintln(msg, "Response contained no body")
}
}
fmt.Fprintln(msg, separator)
return msg.String()
}

View File

@ -0,0 +1,38 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// This is an internal helper package to combine the complete logging APIs.
package log
import (
azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
type Event = log.Event
const (
EventRequest = azlog.EventRequest
EventResponse = azlog.EventResponse
EventRetryPolicy = azlog.EventRetryPolicy
EventLRO = azlog.EventLRO
)
func Write(cls log.Event, msg string) {
log.Write(cls, msg)
}
func Writef(cls log.Event, format string, a ...interface{}) {
log.Writef(cls, format, a...)
}
func SetListener(lst func(Event, string)) {
log.SetListener(lst)
}
func Should(cls log.Event) bool {
return log.Should(cls)
}

View File

@ -0,0 +1,159 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package async
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/internal/poller"
)
// see https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/async-api-reference.md
// Applicable returns true if the LRO is using Azure-AsyncOperation.
func Applicable(resp *http.Response) bool {
return resp.Header.Get(shared.HeaderAzureAsync) != ""
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
_, ok := token["asyncURL"]
return ok
}
// Poller is an LRO poller that uses the Azure-AsyncOperation pattern.
type Poller[T any] struct {
pl exported.Pipeline
resp *http.Response
// The URL from Azure-AsyncOperation header.
AsyncURL string `json:"asyncURL"`
// The URL from Location header.
LocURL string `json:"locURL"`
// The URL from the initial LRO request.
OrigURL string `json:"origURL"`
// The HTTP method from the initial LRO request.
Method string `json:"method"`
// The value of final-state-via from swagger, can be the empty string.
FinalState pollers.FinalStateVia `json:"finalState"`
// The LRO's current state.
CurState string `json:"state"`
}
// New creates a new Poller from the provided initial response and final-state type.
// Pass nil for response to create an empty Poller for rehydration.
func New[T any](pl exported.Pipeline, resp *http.Response, finalState pollers.FinalStateVia) (*Poller[T], error) {
if resp == nil {
log.Write(log.EventLRO, "Resuming Azure-AsyncOperation poller.")
return &Poller[T]{pl: pl}, nil
}
log.Write(log.EventLRO, "Using Azure-AsyncOperation poller.")
asyncURL := resp.Header.Get(shared.HeaderAzureAsync)
if asyncURL == "" {
return nil, errors.New("response is missing Azure-AsyncOperation header")
}
if !poller.IsValidURL(asyncURL) {
return nil, fmt.Errorf("invalid polling URL %s", asyncURL)
}
// check for provisioning state. if the operation is a RELO
// and terminates synchronously this will prevent extra polling.
// it's ok if there's no provisioning state.
state, _ := poller.GetProvisioningState(resp)
if state == "" {
state = poller.StatusInProgress
}
p := &Poller[T]{
pl: pl,
resp: resp,
AsyncURL: asyncURL,
LocURL: resp.Header.Get(shared.HeaderLocation),
OrigURL: resp.Request.URL.String(),
Method: resp.Request.Method,
FinalState: finalState,
CurState: state,
}
return p, nil
}
// Done returns true if the LRO is in a terminal state.
func (p *Poller[T]) Done() bool {
return poller.IsTerminalState(p.CurState)
}
// Poll retrieves the current state of the LRO.
func (p *Poller[T]) Poll(ctx context.Context) (*http.Response, error) {
err := pollers.PollHelper(ctx, p.AsyncURL, p.pl, func(resp *http.Response) (string, error) {
if !poller.StatusCodeValid(resp) {
p.resp = resp
return "", exported.NewResponseError(resp)
}
state, err := poller.GetStatus(resp)
if err != nil {
return "", err
} else if state == "" {
return "", errors.New("the response did not contain a status")
}
p.resp = resp
p.CurState = state
return p.CurState, nil
})
if err != nil {
return nil, err
}
return p.resp, nil
}
func (p *Poller[T]) Result(ctx context.Context, out *T) error {
if p.resp.StatusCode == http.StatusNoContent {
return nil
} else if poller.Failed(p.CurState) {
return exported.NewResponseError(p.resp)
}
var req *exported.Request
var err error
if p.Method == http.MethodPatch || p.Method == http.MethodPut {
// for PATCH and PUT, the final GET is on the original resource URL
req, err = exported.NewRequest(ctx, http.MethodGet, p.OrigURL)
} else if p.Method == http.MethodPost {
if p.FinalState == pollers.FinalStateViaAzureAsyncOp {
// no final GET required
} else if p.FinalState == pollers.FinalStateViaOriginalURI {
req, err = exported.NewRequest(ctx, http.MethodGet, p.OrigURL)
} else if p.LocURL != "" {
// ideally FinalState would be set to "location" but it isn't always.
// must check last due to more permissive condition.
req, err = exported.NewRequest(ctx, http.MethodGet, p.LocURL)
}
}
if err != nil {
return err
}
// if a final GET request has been created, execute it
if req != nil {
resp, err := p.pl.Do(req)
if err != nil {
return err
}
p.resp = resp
}
return pollers.ResultHelper(p.resp, poller.Failed(p.CurState), out)
}

View File

@ -0,0 +1,135 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package body
import (
"context"
"errors"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
"github.com/Azure/azure-sdk-for-go/sdk/internal/poller"
)
// Kind is the identifier of this type in a resume token.
const kind = "body"
// Applicable returns true if the LRO is using no headers, just provisioning state.
// This is only applicable to PATCH and PUT methods and assumes no polling headers.
func Applicable(resp *http.Response) bool {
// we can't check for absense of headers due to some misbehaving services
// like redis that return a Location header but don't actually use that protocol
return resp.Request.Method == http.MethodPatch || resp.Request.Method == http.MethodPut
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
t, ok := token["type"]
if !ok {
return false
}
tt, ok := t.(string)
if !ok {
return false
}
return tt == kind
}
// Poller is an LRO poller that uses the Body pattern.
type Poller[T any] struct {
pl exported.Pipeline
resp *http.Response
// The poller's type, used for resume token processing.
Type string `json:"type"`
// The URL for polling.
PollURL string `json:"pollURL"`
// The LRO's current state.
CurState string `json:"state"`
}
// New creates a new Poller from the provided initial response.
// Pass nil for response to create an empty Poller for rehydration.
func New[T any](pl exported.Pipeline, resp *http.Response) (*Poller[T], error) {
if resp == nil {
log.Write(log.EventLRO, "Resuming Body poller.")
return &Poller[T]{pl: pl}, nil
}
log.Write(log.EventLRO, "Using Body poller.")
p := &Poller[T]{
pl: pl,
resp: resp,
Type: kind,
PollURL: resp.Request.URL.String(),
}
// default initial state to InProgress. depending on the HTTP
// status code and provisioning state, we might change the value.
curState := poller.StatusInProgress
provState, err := poller.GetProvisioningState(resp)
if err != nil && !errors.Is(err, poller.ErrNoBody) {
return nil, err
}
if resp.StatusCode == http.StatusCreated && provState != "" {
// absense of provisioning state is ok for a 201, means the operation is in progress
curState = provState
} else if resp.StatusCode == http.StatusOK {
if provState != "" {
curState = provState
} else if provState == "" {
// for a 200, absense of provisioning state indicates success
curState = poller.StatusSucceeded
}
} else if resp.StatusCode == http.StatusNoContent {
curState = poller.StatusSucceeded
}
p.CurState = curState
return p, nil
}
func (p *Poller[T]) Done() bool {
return poller.IsTerminalState(p.CurState)
}
func (p *Poller[T]) Poll(ctx context.Context) (*http.Response, error) {
err := pollers.PollHelper(ctx, p.PollURL, p.pl, func(resp *http.Response) (string, error) {
if !poller.StatusCodeValid(resp) {
p.resp = resp
return "", exported.NewResponseError(resp)
}
if resp.StatusCode == http.StatusNoContent {
p.resp = resp
p.CurState = poller.StatusSucceeded
return p.CurState, nil
}
state, err := poller.GetProvisioningState(resp)
if errors.Is(err, poller.ErrNoBody) {
// a missing response body in non-204 case is an error
return "", err
} else if state == "" {
// a response body without provisioning state is considered terminal success
state = poller.StatusSucceeded
} else if err != nil {
return "", err
}
p.resp = resp
p.CurState = state
return p.CurState, nil
})
if err != nil {
return nil, err
}
return p.resp, nil
}
func (p *Poller[T]) Result(ctx context.Context, out *T) error {
return pollers.ResultHelper(p.resp, poller.Failed(p.CurState), out)
}

View File

@ -0,0 +1,133 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package fake
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/internal/poller"
)
// Applicable returns true if the LRO is a fake.
func Applicable(resp *http.Response) bool {
return resp.Header.Get(shared.HeaderFakePollerStatus) != ""
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
_, ok := token["fakeURL"]
return ok
}
// Poller is an LRO poller that uses the Core-Fake-Poller pattern.
type Poller[T any] struct {
pl exported.Pipeline
resp *http.Response
// The API name from CtxAPINameKey
APIName string `json:"apiName"`
// The URL from Core-Fake-Poller header.
FakeURL string `json:"fakeURL"`
// The LRO's current state.
FakeStatus string `json:"status"`
}
// lroStatusURLSuffix is the URL path suffix for a faked LRO.
const lroStatusURLSuffix = "/get/fake/status"
// New creates a new Poller from the provided initial response.
// Pass nil for response to create an empty Poller for rehydration.
func New[T any](pl exported.Pipeline, resp *http.Response) (*Poller[T], error) {
if resp == nil {
log.Write(log.EventLRO, "Resuming Core-Fake-Poller poller.")
return &Poller[T]{pl: pl}, nil
}
log.Write(log.EventLRO, "Using Core-Fake-Poller poller.")
fakeStatus := resp.Header.Get(shared.HeaderFakePollerStatus)
if fakeStatus == "" {
return nil, errors.New("response is missing Fake-Poller-Status header")
}
ctxVal := resp.Request.Context().Value(shared.CtxAPINameKey{})
if ctxVal == nil {
return nil, errors.New("missing value for CtxAPINameKey")
}
apiName, ok := ctxVal.(string)
if !ok {
return nil, fmt.Errorf("expected string for CtxAPINameKey, the type was %T", ctxVal)
}
qp := ""
if resp.Request.URL.RawQuery != "" {
qp = "?" + resp.Request.URL.RawQuery
}
p := &Poller[T]{
pl: pl,
resp: resp,
APIName: apiName,
// NOTE: any changes to this path format MUST be reflected in SanitizePollerPath()
FakeURL: fmt.Sprintf("%s://%s%s%s%s", resp.Request.URL.Scheme, resp.Request.URL.Host, resp.Request.URL.Path, lroStatusURLSuffix, qp),
FakeStatus: fakeStatus,
}
return p, nil
}
// Done returns true if the LRO is in a terminal state.
func (p *Poller[T]) Done() bool {
return poller.IsTerminalState(p.FakeStatus)
}
// Poll retrieves the current state of the LRO.
func (p *Poller[T]) Poll(ctx context.Context) (*http.Response, error) {
ctx = context.WithValue(ctx, shared.CtxAPINameKey{}, p.APIName)
err := pollers.PollHelper(ctx, p.FakeURL, p.pl, func(resp *http.Response) (string, error) {
if !poller.StatusCodeValid(resp) {
p.resp = resp
return "", exported.NewResponseError(resp)
}
fakeStatus := resp.Header.Get(shared.HeaderFakePollerStatus)
if fakeStatus == "" {
return "", errors.New("response is missing Fake-Poller-Status header")
}
p.resp = resp
p.FakeStatus = fakeStatus
return p.FakeStatus, nil
})
if err != nil {
return nil, err
}
return p.resp, nil
}
func (p *Poller[T]) Result(ctx context.Context, out *T) error {
if p.resp.StatusCode == http.StatusNoContent {
return nil
} else if poller.Failed(p.FakeStatus) {
return exported.NewResponseError(p.resp)
}
return pollers.ResultHelper(p.resp, poller.Failed(p.FakeStatus), out)
}
// SanitizePollerPath removes any fake-appended suffix from a URL's path.
func SanitizePollerPath(path string) string {
return strings.TrimSuffix(path, lroStatusURLSuffix)
}

View File

@ -0,0 +1,119 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package loc
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/internal/poller"
)
// Kind is the identifier of this type in a resume token.
const kind = "loc"
// Applicable returns true if the LRO is using Location.
func Applicable(resp *http.Response) bool {
return resp.Header.Get(shared.HeaderLocation) != ""
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
t, ok := token["type"]
if !ok {
return false
}
tt, ok := t.(string)
if !ok {
return false
}
return tt == kind
}
// Poller is an LRO poller that uses the Location pattern.
type Poller[T any] struct {
pl exported.Pipeline
resp *http.Response
Type string `json:"type"`
PollURL string `json:"pollURL"`
CurState string `json:"state"`
}
// New creates a new Poller from the provided initial response.
// Pass nil for response to create an empty Poller for rehydration.
func New[T any](pl exported.Pipeline, resp *http.Response) (*Poller[T], error) {
if resp == nil {
log.Write(log.EventLRO, "Resuming Location poller.")
return &Poller[T]{pl: pl}, nil
}
log.Write(log.EventLRO, "Using Location poller.")
locURL := resp.Header.Get(shared.HeaderLocation)
if locURL == "" {
return nil, errors.New("response is missing Location header")
}
if !poller.IsValidURL(locURL) {
return nil, fmt.Errorf("invalid polling URL %s", locURL)
}
// check for provisioning state. if the operation is a RELO
// and terminates synchronously this will prevent extra polling.
// it's ok if there's no provisioning state.
state, _ := poller.GetProvisioningState(resp)
if state == "" {
state = poller.StatusInProgress
}
return &Poller[T]{
pl: pl,
resp: resp,
Type: kind,
PollURL: locURL,
CurState: state,
}, nil
}
func (p *Poller[T]) Done() bool {
return poller.IsTerminalState(p.CurState)
}
func (p *Poller[T]) Poll(ctx context.Context) (*http.Response, error) {
err := pollers.PollHelper(ctx, p.PollURL, p.pl, func(resp *http.Response) (string, error) {
// location polling can return an updated polling URL
if h := resp.Header.Get(shared.HeaderLocation); h != "" {
p.PollURL = h
}
// if provisioning state is available, use that. this is only
// for some ARM LRO scenarios (e.g. DELETE with a Location header)
// so if it's missing then use HTTP status code.
provState, _ := poller.GetProvisioningState(resp)
p.resp = resp
if provState != "" {
p.CurState = provState
} else if resp.StatusCode == http.StatusAccepted {
p.CurState = poller.StatusInProgress
} else if resp.StatusCode > 199 && resp.StatusCode < 300 {
// any 2xx other than a 202 indicates success
p.CurState = poller.StatusSucceeded
} else {
p.CurState = poller.StatusFailed
}
return p.CurState, nil
})
if err != nil {
return nil, err
}
return p.resp, nil
}
func (p *Poller[T]) Result(ctx context.Context, out *T) error {
return pollers.ResultHelper(p.resp, poller.Failed(p.CurState), out)
}

View File

@ -0,0 +1,145 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package op
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/internal/poller"
)
// Applicable returns true if the LRO is using Operation-Location.
func Applicable(resp *http.Response) bool {
return resp.Header.Get(shared.HeaderOperationLocation) != ""
}
// CanResume returns true if the token can rehydrate this poller type.
func CanResume(token map[string]interface{}) bool {
_, ok := token["oplocURL"]
return ok
}
// Poller is an LRO poller that uses the Operation-Location pattern.
type Poller[T any] struct {
pl exported.Pipeline
resp *http.Response
OpLocURL string `json:"oplocURL"`
LocURL string `json:"locURL"`
OrigURL string `json:"origURL"`
Method string `json:"method"`
FinalState pollers.FinalStateVia `json:"finalState"`
CurState string `json:"state"`
}
// New creates a new Poller from the provided initial response.
// Pass nil for response to create an empty Poller for rehydration.
func New[T any](pl exported.Pipeline, resp *http.Response, finalState pollers.FinalStateVia) (*Poller[T], error) {
if resp == nil {
log.Write(log.EventLRO, "Resuming Operation-Location poller.")
return &Poller[T]{pl: pl}, nil
}
log.Write(log.EventLRO, "Using Operation-Location poller.")
opURL := resp.Header.Get(shared.HeaderOperationLocation)
if opURL == "" {
return nil, errors.New("response is missing Operation-Location header")
}
if !poller.IsValidURL(opURL) {
return nil, fmt.Errorf("invalid Operation-Location URL %s", opURL)
}
locURL := resp.Header.Get(shared.HeaderLocation)
// Location header is optional
if locURL != "" && !poller.IsValidURL(locURL) {
return nil, fmt.Errorf("invalid Location URL %s", locURL)
}
// default initial state to InProgress. if the
// service sent us a status then use that instead.
curState := poller.StatusInProgress
status, err := poller.GetStatus(resp)
if err != nil && !errors.Is(err, poller.ErrNoBody) {
return nil, err
}
if status != "" {
curState = status
}
return &Poller[T]{
pl: pl,
resp: resp,
OpLocURL: opURL,
LocURL: locURL,
OrigURL: resp.Request.URL.String(),
Method: resp.Request.Method,
FinalState: finalState,
CurState: curState,
}, nil
}
func (p *Poller[T]) Done() bool {
return poller.IsTerminalState(p.CurState)
}
func (p *Poller[T]) Poll(ctx context.Context) (*http.Response, error) {
err := pollers.PollHelper(ctx, p.OpLocURL, p.pl, func(resp *http.Response) (string, error) {
if !poller.StatusCodeValid(resp) {
p.resp = resp
return "", exported.NewResponseError(resp)
}
state, err := poller.GetStatus(resp)
if err != nil {
return "", err
} else if state == "" {
return "", errors.New("the response did not contain a status")
}
p.resp = resp
p.CurState = state
return p.CurState, nil
})
if err != nil {
return nil, err
}
return p.resp, nil
}
func (p *Poller[T]) Result(ctx context.Context, out *T) error {
var req *exported.Request
var err error
if p.FinalState == pollers.FinalStateViaLocation && p.LocURL != "" {
req, err = exported.NewRequest(ctx, http.MethodGet, p.LocURL)
} else if p.FinalState == pollers.FinalStateViaOpLocation && p.Method == http.MethodPost {
// no final GET required, terminal response should have it
} else if rl, rlErr := poller.GetResourceLocation(p.resp); rlErr != nil && !errors.Is(rlErr, poller.ErrNoBody) {
return rlErr
} else if rl != "" {
req, err = exported.NewRequest(ctx, http.MethodGet, rl)
} else if p.Method == http.MethodPatch || p.Method == http.MethodPut {
req, err = exported.NewRequest(ctx, http.MethodGet, p.OrigURL)
} else if p.Method == http.MethodPost && p.LocURL != "" {
req, err = exported.NewRequest(ctx, http.MethodGet, p.LocURL)
}
if err != nil {
return err
}
// if a final GET request has been created, execute it
if req != nil {
resp, err := p.pl.Do(req)
if err != nil {
return err
}
p.resp = resp
}
return pollers.ResultHelper(p.resp, poller.Failed(p.CurState), out)
}

View File

@ -0,0 +1,24 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package pollers
// FinalStateVia is the enumerated type for the possible final-state-via values.
type FinalStateVia string
const (
// FinalStateViaAzureAsyncOp indicates the final payload comes from the Azure-AsyncOperation URL.
FinalStateViaAzureAsyncOp FinalStateVia = "azure-async-operation"
// FinalStateViaLocation indicates the final payload comes from the Location URL.
FinalStateViaLocation FinalStateVia = "location"
// FinalStateViaOriginalURI indicates the final payload comes from the original URL.
FinalStateViaOriginalURI FinalStateVia = "original-uri"
// FinalStateViaOpLocation indicates the final payload comes from the Operation-Location URL.
FinalStateViaOpLocation FinalStateVia = "operation-location"
)

View File

@ -0,0 +1,187 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package pollers
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
azexported "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/internal/poller"
)
// getTokenTypeName creates a type name from the type parameter T.
func getTokenTypeName[T any]() (string, error) {
tt := shared.TypeOfT[T]()
var n string
if tt.Kind() == reflect.Pointer {
n = "*"
tt = tt.Elem()
}
n += tt.Name()
if n == "" {
return "", errors.New("nameless types are not allowed")
}
return n, nil
}
type resumeTokenWrapper[T any] struct {
Type string `json:"type"`
Token T `json:"token"`
}
// NewResumeToken creates a resume token from the specified type.
// An error is returned if the generic type has no name (e.g. struct{}).
func NewResumeToken[TResult, TSource any](from TSource) (string, error) {
n, err := getTokenTypeName[TResult]()
if err != nil {
return "", err
}
b, err := json.Marshal(resumeTokenWrapper[TSource]{
Type: n,
Token: from,
})
if err != nil {
return "", err
}
return string(b), nil
}
// ExtractToken returns the poller-specific token information from the provided token value.
func ExtractToken(token string) ([]byte, error) {
raw := map[string]json.RawMessage{}
if err := json.Unmarshal([]byte(token), &raw); err != nil {
return nil, err
}
// this is dependent on the type resumeTokenWrapper[T]
tk, ok := raw["token"]
if !ok {
return nil, errors.New("missing token value")
}
return tk, nil
}
// IsTokenValid returns an error if the specified token isn't applicable for generic type T.
func IsTokenValid[T any](token string) error {
raw := map[string]interface{}{}
if err := json.Unmarshal([]byte(token), &raw); err != nil {
return err
}
t, ok := raw["type"]
if !ok {
return errors.New("missing type value")
}
tt, ok := t.(string)
if !ok {
return fmt.Errorf("invalid type format %T", t)
}
n, err := getTokenTypeName[T]()
if err != nil {
return err
}
if tt != n {
return fmt.Errorf("cannot resume from this poller token. token is for type %s, not %s", tt, n)
}
return nil
}
// used if the operation synchronously completed
type NopPoller[T any] struct {
resp *http.Response
result T
}
// NewNopPoller creates a NopPoller from the provided response.
// It unmarshals the response body into an instance of T.
func NewNopPoller[T any](resp *http.Response) (*NopPoller[T], error) {
np := &NopPoller[T]{resp: resp}
if resp.StatusCode == http.StatusNoContent {
return np, nil
}
payload, err := exported.Payload(resp, nil)
if err != nil {
return nil, err
}
if len(payload) == 0 {
return np, nil
}
if err = json.Unmarshal(payload, &np.result); err != nil {
return nil, err
}
return np, nil
}
func (*NopPoller[T]) Done() bool {
return true
}
func (p *NopPoller[T]) Poll(context.Context) (*http.Response, error) {
return p.resp, nil
}
func (p *NopPoller[T]) Result(ctx context.Context, out *T) error {
*out = p.result
return nil
}
// PollHelper creates and executes the request, calling update() with the response.
// If the request fails, the update func is not called.
// The update func returns the state of the operation for logging purposes or an error
// if it fails to extract the required state from the response.
func PollHelper(ctx context.Context, endpoint string, pl azexported.Pipeline, update func(resp *http.Response) (string, error)) error {
req, err := azexported.NewRequest(ctx, http.MethodGet, endpoint)
if err != nil {
return err
}
resp, err := pl.Do(req)
if err != nil {
return err
}
state, err := update(resp)
if err != nil {
return err
}
log.Writef(log.EventLRO, "State %s", state)
return nil
}
// ResultHelper processes the response as success or failure.
// In the success case, it unmarshals the payload into either a new instance of T or out.
// In the failure case, it creates an *azcore.Response error from the response.
func ResultHelper[T any](resp *http.Response, failed bool, out *T) error {
// short-circuit the simple success case with no response body to unmarshal
if resp.StatusCode == http.StatusNoContent {
return nil
}
defer resp.Body.Close()
if !poller.StatusCodeValid(resp) || failed {
// the LRO failed. unmarshall the error and update state
return azexported.NewResponseError(resp)
}
// success case
payload, err := exported.Payload(resp, nil)
if err != nil {
return err
}
if len(payload) == 0 {
return nil
}
if err = json.Unmarshal(payload, out); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,44 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package shared
const (
ContentTypeAppJSON = "application/json"
ContentTypeAppXML = "application/xml"
ContentTypeTextPlain = "text/plain"
)
const (
HeaderAuthorization = "Authorization"
HeaderAuxiliaryAuthorization = "x-ms-authorization-auxiliary"
HeaderAzureAsync = "Azure-AsyncOperation"
HeaderContentLength = "Content-Length"
HeaderContentType = "Content-Type"
HeaderFakePollerStatus = "Fake-Poller-Status"
HeaderLocation = "Location"
HeaderOperationLocation = "Operation-Location"
HeaderRetryAfter = "Retry-After"
HeaderRetryAfterMS = "Retry-After-Ms"
HeaderUserAgent = "User-Agent"
HeaderWWWAuthenticate = "WWW-Authenticate"
HeaderXMSClientRequestID = "x-ms-client-request-id"
HeaderXMSRequestID = "x-ms-request-id"
HeaderXMSErrorCode = "x-ms-error-code"
HeaderXMSRetryAfterMS = "x-ms-retry-after-ms"
)
const BearerTokenPrefix = "Bearer "
const TracingNamespaceAttrName = "az.namespace"
const (
// Module is the name of the calling module used in telemetry data.
Module = "azcore"
// Version is the semantic version (see http://semver.org) of this module.
Version = "v1.9.1"
)

View File

@ -0,0 +1,149 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package shared
import (
"context"
"fmt"
"net/http"
"reflect"
"regexp"
"strconv"
"time"
)
// NOTE: when adding a new context key type, it likely needs to be
// added to the deny-list of key types in ContextWithDeniedValues
// CtxWithHTTPHeaderKey is used as a context key for adding/retrieving http.Header.
type CtxWithHTTPHeaderKey struct{}
// CtxWithRetryOptionsKey is used as a context key for adding/retrieving RetryOptions.
type CtxWithRetryOptionsKey struct{}
// CtxWithCaptureResponse is used as a context key for retrieving the raw response.
type CtxWithCaptureResponse struct{}
// CtxWithTracingTracer is used as a context key for adding/retrieving tracing.Tracer.
type CtxWithTracingTracer struct{}
// CtxAPINameKey is used as a context key for adding/retrieving the API name.
type CtxAPINameKey struct{}
// Delay waits for the duration to elapse or the context to be cancelled.
func Delay(ctx context.Context, delay time.Duration) error {
select {
case <-time.After(delay):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// RetryAfter returns non-zero if the response contains one of the headers with a "retry after" value.
// Headers are checked in the following order: retry-after-ms, x-ms-retry-after-ms, retry-after
func RetryAfter(resp *http.Response) time.Duration {
if resp == nil {
return 0
}
type retryData struct {
header string
units time.Duration
// custom is used when the regular algorithm failed and is optional.
// the returned duration is used verbatim (units is not applied).
custom func(string) time.Duration
}
nop := func(string) time.Duration { return 0 }
// the headers are listed in order of preference
retries := []retryData{
{
header: HeaderRetryAfterMS,
units: time.Millisecond,
custom: nop,
},
{
header: HeaderXMSRetryAfterMS,
units: time.Millisecond,
custom: nop,
},
{
header: HeaderRetryAfter,
units: time.Second,
// retry-after values are expressed in either number of
// seconds or an HTTP-date indicating when to try again
custom: func(ra string) time.Duration {
t, err := time.Parse(time.RFC1123, ra)
if err != nil {
return 0
}
return time.Until(t)
},
},
}
for _, retry := range retries {
v := resp.Header.Get(retry.header)
if v == "" {
continue
}
if retryAfter, _ := strconv.Atoi(v); retryAfter > 0 {
return time.Duration(retryAfter) * retry.units
} else if d := retry.custom(v); d > 0 {
return d
}
}
return 0
}
// TypeOfT returns the type of the generic type param.
func TypeOfT[T any]() reflect.Type {
// you can't, at present, obtain the type of
// a type parameter, so this is the trick
return reflect.TypeOf((*T)(nil)).Elem()
}
// TransportFunc is a helper to use a first-class func to satisfy the Transporter interface.
type TransportFunc func(*http.Request) (*http.Response, error)
// Do implements the Transporter interface for the TransportFunc type.
func (pf TransportFunc) Do(req *http.Request) (*http.Response, error) {
return pf(req)
}
// ValidateModVer verifies that moduleVersion is a valid semver 2.0 string.
func ValidateModVer(moduleVersion string) error {
modVerRegx := regexp.MustCompile(`^v\d+\.\d+\.\d+(?:-[a-zA-Z0-9_.-]+)?$`)
if !modVerRegx.MatchString(moduleVersion) {
return fmt.Errorf("malformed moduleVersion param value %s", moduleVersion)
}
return nil
}
// ContextWithDeniedValues wraps an existing [context.Context], denying access to certain context values.
// Pipeline policies that create new requests to be sent down their own pipeline MUST wrap the caller's
// context with an instance of this type. This is to prevent context values from flowing across disjoint
// requests which can have unintended side-effects.
type ContextWithDeniedValues struct {
context.Context
}
// Value implements part of the [context.Context] interface.
// It acts as a deny-list for certain context keys.
func (c *ContextWithDeniedValues) Value(key any) any {
switch key.(type) {
case CtxAPINameKey, CtxWithCaptureResponse, CtxWithHTTPHeaderKey, CtxWithRetryOptionsKey, CtxWithTracingTracer:
return nil
default:
return c.Context.Value(key)
}
}

View File

@ -0,0 +1,10 @@
//go:build go1.18
// +build go1.18
// Copyright 2017 Microsoft Corporation. All rights reserved.
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// Package log contains functionality for configuring logging behavior.
// Default logging to stderr can be enabled by setting environment variable AZURE_SDK_GO_LOGGING to "all".
package log

View File

@ -0,0 +1,50 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Package log provides functionality for configuring logging facilities.
package log
import (
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
// Event is used to group entries. Each group can be toggled on or off.
type Event = log.Event
const (
// EventRequest entries contain information about HTTP requests.
// This includes information like the URL, query parameters, and headers.
EventRequest Event = "Request"
// EventResponse entries contain information about HTTP responses.
// This includes information like the HTTP status code, headers, and request URL.
EventResponse Event = "Response"
// EventRetryPolicy entries contain information specific to the retry policy in use.
EventRetryPolicy Event = "Retry"
// EventLRO entries contain information specific to long-running operations.
// This includes information like polling location, operation state, and sleep intervals.
EventLRO Event = "LongRunningOperation"
)
// SetEvents is used to control which events are written to
// the log. By default all log events are writen.
// NOTE: this is not goroutine safe and should be called before using SDK clients.
func SetEvents(cls ...Event) {
log.SetEvents(cls...)
}
// SetListener will set the Logger to write to the specified Listener.
// NOTE: this is not goroutine safe and should be called before using SDK clients.
func SetListener(lst func(Event, string)) {
log.SetListener(lst)
}
// for testing purposes
func resetEvents() {
log.TestResetEvents()
}

View File

@ -0,0 +1,10 @@
//go:build go1.18
// +build go1.18
// Copyright 2017 Microsoft Corporation. All rights reserved.
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// Package policy contains the definitions needed for configuring in-box pipeline policies
// and creating custom policies.
package policy

View File

@ -0,0 +1,187 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package policy
import (
"context"
"net/http"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
)
// Policy represents an extensibility point for the Pipeline that can mutate the specified
// Request and react to the received Response.
type Policy = exported.Policy
// Transporter represents an HTTP pipeline transport used to send HTTP requests and receive responses.
type Transporter = exported.Transporter
// Request is an abstraction over the creation of an HTTP request as it passes through the pipeline.
// Don't use this type directly, use runtime.NewRequest() instead.
type Request = exported.Request
// ClientOptions contains optional settings for a client's pipeline.
// Instances can be shared across calls to SDK client constructors when uniform configuration is desired.
// Zero-value fields will have their specified default values applied during use.
type ClientOptions struct {
// APIVersion overrides the default version requested of the service.
// Set with caution as this package version has not been tested with arbitrary service versions.
APIVersion string
// Cloud specifies a cloud for the client. The default is Azure Public Cloud.
Cloud cloud.Configuration
// Logging configures the built-in logging policy.
Logging LogOptions
// Retry configures the built-in retry policy.
Retry RetryOptions
// Telemetry configures the built-in telemetry policy.
Telemetry TelemetryOptions
// TracingProvider configures the tracing provider.
// It defaults to a no-op tracer.
TracingProvider tracing.Provider
// Transport sets the transport for HTTP requests.
Transport Transporter
// PerCallPolicies contains custom policies to inject into the pipeline.
// Each policy is executed once per request.
PerCallPolicies []Policy
// PerRetryPolicies contains custom policies to inject into the pipeline.
// Each policy is executed once per request, and for each retry of that request.
PerRetryPolicies []Policy
}
// LogOptions configures the logging policy's behavior.
type LogOptions struct {
// IncludeBody indicates if request and response bodies should be included in logging.
// The default value is false.
// NOTE: enabling this can lead to disclosure of sensitive information, use with care.
IncludeBody bool
// AllowedHeaders is the slice of headers to log with their values intact.
// All headers not in the slice will have their values REDACTED.
// Applies to request and response headers.
AllowedHeaders []string
// AllowedQueryParams is the slice of query parameters to log with their values intact.
// All query parameters not in the slice will have their values REDACTED.
AllowedQueryParams []string
}
// RetryOptions configures the retry policy's behavior.
// Zero-value fields will have their specified default values applied during use.
// This allows for modification of a subset of fields.
type RetryOptions struct {
// MaxRetries specifies the maximum number of attempts a failed operation will be retried
// before producing an error.
// The default value is three. A value less than zero means one try and no retries.
MaxRetries int32
// TryTimeout indicates the maximum time allowed for any single try of an HTTP request.
// This is disabled by default. Specify a value greater than zero to enable.
// NOTE: Setting this to a small value might cause premature HTTP request time-outs.
TryTimeout time.Duration
// RetryDelay specifies the initial amount of delay to use before retrying an operation.
// The value is used only if the HTTP response does not contain a Retry-After header.
// The delay increases exponentially with each retry up to the maximum specified by MaxRetryDelay.
// The default value is four seconds. A value less than zero means no delay between retries.
RetryDelay time.Duration
// MaxRetryDelay specifies the maximum delay allowed before retrying an operation.
// Typically the value is greater than or equal to the value specified in RetryDelay.
// The default Value is 60 seconds. A value less than zero means there is no cap.
MaxRetryDelay time.Duration
// StatusCodes specifies the HTTP status codes that indicate the operation should be retried.
// A nil slice will use the following values.
// http.StatusRequestTimeout 408
// http.StatusTooManyRequests 429
// http.StatusInternalServerError 500
// http.StatusBadGateway 502
// http.StatusServiceUnavailable 503
// http.StatusGatewayTimeout 504
// Specifying values will replace the default values.
// Specifying an empty slice will disable retries for HTTP status codes.
StatusCodes []int
// ShouldRetry evaluates if the retry policy should retry the request.
// When specified, the function overrides comparison against the list of
// HTTP status codes and error checking within the retry policy. Context
// and NonRetriable errors remain evaluated before calling ShouldRetry.
// The *http.Response and error parameters are mutually exclusive, i.e.
// if one is nil, the other is not nil.
// A return value of true means the retry policy should retry.
ShouldRetry func(*http.Response, error) bool
}
// TelemetryOptions configures the telemetry policy's behavior.
type TelemetryOptions struct {
// ApplicationID is an application-specific identification string to add to the User-Agent.
// It has a maximum length of 24 characters and must not contain any spaces.
ApplicationID string
// Disabled will prevent the addition of any telemetry data to the User-Agent.
Disabled bool
}
// TokenRequestOptions contain specific parameter that may be used by credentials types when attempting to get a token.
type TokenRequestOptions = exported.TokenRequestOptions
// BearerTokenOptions configures the bearer token policy's behavior.
type BearerTokenOptions struct {
// AuthorizationHandler allows SDK developers to run client-specific logic when BearerTokenPolicy must authorize a request.
// When this field isn't set, the policy follows its default behavior of authorizing every request with a bearer token from
// its given credential.
AuthorizationHandler AuthorizationHandler
}
// AuthorizationHandler allows SDK developers to insert custom logic that runs when BearerTokenPolicy must authorize a request.
type AuthorizationHandler struct {
// OnRequest is called each time the policy receives a request. Its func parameter authorizes the request with a token
// from the policy's given credential. Implementations that need to perform I/O should use the Request's context,
// available from Request.Raw().Context(). When OnRequest returns an error, the policy propagates that error and doesn't
// send the request. When OnRequest is nil, the policy follows its default behavior, authorizing the request with a
// token from its credential according to its configuration.
OnRequest func(*Request, func(TokenRequestOptions) error) error
// OnChallenge is called when the policy receives a 401 response, allowing the AuthorizationHandler to re-authorize the
// request according to an authentication challenge (the Response's WWW-Authenticate header). OnChallenge is responsible
// for parsing parameters from the challenge. Its func parameter will authorize the request with a token from the policy's
// given credential. Implementations that need to perform I/O should use the Request's context, available from
// Request.Raw().Context(). When OnChallenge returns nil, the policy will send the request again. When OnChallenge is nil,
// the policy will return any 401 response to the client.
OnChallenge func(*Request, *http.Response, func(TokenRequestOptions) error) error
}
// WithCaptureResponse applies the HTTP response retrieval annotation to the parent context.
// The resp parameter will contain the HTTP response after the request has completed.
func WithCaptureResponse(parent context.Context, resp **http.Response) context.Context {
return context.WithValue(parent, shared.CtxWithCaptureResponse{}, resp)
}
// WithHTTPHeader adds the specified http.Header to the parent context.
// Use this to specify custom HTTP headers at the API-call level.
// Any overlapping headers will have their values replaced with the values specified here.
func WithHTTPHeader(parent context.Context, header http.Header) context.Context {
return context.WithValue(parent, shared.CtxWithHTTPHeaderKey{}, header)
}
// WithRetryOptions adds the specified RetryOptions to the parent context.
// Use this to specify custom RetryOptions at the API-call level.
func WithRetryOptions(parent context.Context, options RetryOptions) context.Context {
return context.WithValue(parent, shared.CtxWithRetryOptionsKey{}, options)
}

View File

@ -0,0 +1,10 @@
//go:build go1.18
// +build go1.18
// Copyright 2017 Microsoft Corporation. All rights reserved.
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// Package runtime contains various facilities for creating requests and handling responses.
// The content is intended for SDK authors.
package runtime

View File

@ -0,0 +1,19 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
)
// NewResponseError creates an *azcore.ResponseError from the provided HTTP response.
// Call this when a service request returns a non-successful status code.
func NewResponseError(resp *http.Response) error {
return exported.NewResponseError(resp)
}

View File

@ -0,0 +1,128 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
)
// PagingHandler contains the required data for constructing a Pager.
type PagingHandler[T any] struct {
// More returns a boolean indicating if there are more pages to fetch.
// It uses the provided page to make the determination.
More func(T) bool
// Fetcher fetches the first and subsequent pages.
Fetcher func(context.Context, *T) (T, error)
// Tracer contains the Tracer from the client that's creating the Pager.
Tracer tracing.Tracer
}
// Pager provides operations for iterating over paged responses.
type Pager[T any] struct {
current *T
handler PagingHandler[T]
tracer tracing.Tracer
firstPage bool
}
// NewPager creates an instance of Pager using the specified PagingHandler.
// Pass a non-nil T for firstPage if the first page has already been retrieved.
func NewPager[T any](handler PagingHandler[T]) *Pager[T] {
return &Pager[T]{
handler: handler,
tracer: handler.Tracer,
firstPage: true,
}
}
// More returns true if there are more pages to retrieve.
func (p *Pager[T]) More() bool {
if p.current != nil {
return p.handler.More(*p.current)
}
return true
}
// NextPage advances the pager to the next page.
func (p *Pager[T]) NextPage(ctx context.Context) (T, error) {
if p.current != nil {
if p.firstPage {
// we get here if it's an LRO-pager, we already have the first page
p.firstPage = false
return *p.current, nil
} else if !p.handler.More(*p.current) {
return *new(T), errors.New("no more pages")
}
} else {
// non-LRO case, first page
p.firstPage = false
}
var err error
ctx, endSpan := StartSpan(ctx, fmt.Sprintf("%s.NextPage", shortenTypeName(reflect.TypeOf(*p).Name())), p.tracer, nil)
defer func() { endSpan(err) }()
resp, err := p.handler.Fetcher(ctx, p.current)
if err != nil {
return *new(T), err
}
p.current = &resp
return *p.current, nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for Pager[T].
func (p *Pager[T]) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &p.current)
}
// FetcherForNextLinkOptions contains the optional values for [FetcherForNextLink].
type FetcherForNextLinkOptions struct {
// NextReq is the func to be called when requesting subsequent pages.
// Used for paged operations that have a custom next link operation.
NextReq func(context.Context, string) (*policy.Request, error)
}
// FetcherForNextLink is a helper containing boilerplate code to simplify creating a PagingHandler[T].Fetcher from a next link URL.
// - ctx is the [context.Context] controlling the lifetime of the HTTP operation
// - pl is the [Pipeline] used to dispatch the HTTP request
// - nextLink is the URL used to fetch the next page. the empty string indicates the first page is to be requested
// - firstReq is the func to be called when creating the request for the first page
// - options contains any optional parameters, pass nil to accept the default values
func FetcherForNextLink(ctx context.Context, pl Pipeline, nextLink string, firstReq func(context.Context) (*policy.Request, error), options *FetcherForNextLinkOptions) (*http.Response, error) {
var req *policy.Request
var err error
if nextLink == "" {
req, err = firstReq(ctx)
} else if nextLink, err = EncodeQueryParams(nextLink); err == nil {
if options != nil && options.NextReq != nil {
req, err = options.NextReq(ctx, nextLink)
} else {
req, err = NewRequest(ctx, http.MethodGet, nextLink)
}
}
if err != nil {
return nil, err
}
resp, err := pl.Do(req)
if err != nil {
return nil, err
}
if !HasStatusCode(resp, http.StatusOK) {
return nil, NewResponseError(resp)
}
return resp, nil
}

View File

@ -0,0 +1,94 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// PipelineOptions contains Pipeline options for SDK developers
type PipelineOptions struct {
// AllowedHeaders is the slice of headers to log with their values intact.
// All headers not in the slice will have their values REDACTED.
// Applies to request and response headers.
AllowedHeaders []string
// AllowedQueryParameters is the slice of query parameters to log with their values intact.
// All query parameters not in the slice will have their values REDACTED.
AllowedQueryParameters []string
// APIVersion overrides the default version requested of the service.
// Set with caution as this package version has not been tested with arbitrary service versions.
APIVersion APIVersionOptions
// PerCall contains custom policies to inject into the pipeline.
// Each policy is executed once per request.
PerCall []policy.Policy
// PerRetry contains custom policies to inject into the pipeline.
// Each policy is executed once per request, and for each retry of that request.
PerRetry []policy.Policy
// Tracing contains options used to configure distributed tracing.
Tracing TracingOptions
}
// TracingOptions contains tracing options for SDK developers.
type TracingOptions struct {
// Namespace contains the value to use for the az.namespace span attribute.
Namespace string
}
// Pipeline represents a primitive for sending HTTP requests and receiving responses.
// Its behavior can be extended by specifying policies during construction.
type Pipeline = exported.Pipeline
// NewPipeline creates a pipeline from connection options, with any additional policies as specified.
// Policies from ClientOptions are placed after policies from PipelineOptions.
// The module and version parameters are used by the telemetry policy, when enabled.
func NewPipeline(module, version string, plOpts PipelineOptions, options *policy.ClientOptions) Pipeline {
cp := policy.ClientOptions{}
if options != nil {
cp = *options
}
if len(plOpts.AllowedHeaders) > 0 {
headers := make([]string, len(plOpts.AllowedHeaders)+len(cp.Logging.AllowedHeaders))
copy(headers, plOpts.AllowedHeaders)
headers = append(headers, cp.Logging.AllowedHeaders...)
cp.Logging.AllowedHeaders = headers
}
if len(plOpts.AllowedQueryParameters) > 0 {
qp := make([]string, len(plOpts.AllowedQueryParameters)+len(cp.Logging.AllowedQueryParams))
copy(qp, plOpts.AllowedQueryParameters)
qp = append(qp, cp.Logging.AllowedQueryParams...)
cp.Logging.AllowedQueryParams = qp
}
// we put the includeResponsePolicy at the very beginning so that the raw response
// is populated with the final response (some policies might mutate the response)
policies := []policy.Policy{exported.PolicyFunc(includeResponsePolicy)}
if cp.APIVersion != "" {
policies = append(policies, newAPIVersionPolicy(cp.APIVersion, &plOpts.APIVersion))
}
if !cp.Telemetry.Disabled {
policies = append(policies, NewTelemetryPolicy(module, version, &cp.Telemetry))
}
policies = append(policies, plOpts.PerCall...)
policies = append(policies, cp.PerCallPolicies...)
policies = append(policies, NewRetryPolicy(&cp.Retry))
policies = append(policies, plOpts.PerRetry...)
policies = append(policies, cp.PerRetryPolicies...)
policies = append(policies, exported.PolicyFunc(httpHeaderPolicy))
policies = append(policies, newHTTPTracePolicy(cp.Logging.AllowedQueryParams))
policies = append(policies, NewLogPolicy(&cp.Logging))
policies = append(policies, exported.PolicyFunc(bodyDownloadPolicy))
transport := cp.Transport
if transport == nil {
transport = defaultHTTPClient
}
return exported.NewPipeline(transport, policies...)
}

View File

@ -0,0 +1,75 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"errors"
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// APIVersionOptions contains options for API versions
type APIVersionOptions struct {
// Location indicates where to set the version on a request, for example in a header or query param
Location APIVersionLocation
// Name is the name of the header or query parameter, for example "api-version"
Name string
}
// APIVersionLocation indicates which part of a request identifies the service version
type APIVersionLocation int
const (
// APIVersionLocationQueryParam indicates a query parameter
APIVersionLocationQueryParam = 0
// APIVersionLocationHeader indicates a header
APIVersionLocationHeader = 1
)
// newAPIVersionPolicy constructs an APIVersionPolicy. If version is "", Do will be a no-op. If version
// isn't empty and opts.Name is empty, Do will return an error.
func newAPIVersionPolicy(version string, opts *APIVersionOptions) *apiVersionPolicy {
if opts == nil {
opts = &APIVersionOptions{}
}
return &apiVersionPolicy{location: opts.Location, name: opts.Name, version: version}
}
// apiVersionPolicy enables users to set the API version of every request a client sends.
type apiVersionPolicy struct {
// location indicates whether "name" refers to a query parameter or header.
location APIVersionLocation
// name of the query param or header whose value should be overridden; provided by the client.
name string
// version is the value (provided by the user) that replaces the default version value.
version string
}
// Do sets the request's API version, if the policy is configured to do so, replacing any prior value.
func (a *apiVersionPolicy) Do(req *policy.Request) (*http.Response, error) {
if a.version != "" {
if a.name == "" {
// user set ClientOptions.APIVersion but the client ctor didn't set PipelineOptions.APIVersionOptions
return nil, errors.New("this client doesn't support overriding its API version")
}
switch a.location {
case APIVersionLocationHeader:
req.Raw().Header.Set(a.name, a.version)
case APIVersionLocationQueryParam:
q := req.Raw().URL.Query()
q.Set(a.name, a.version)
req.Raw().URL.RawQuery = q.Encode()
default:
return nil, fmt.Errorf("unknown APIVersionLocation %d", a.location)
}
}
return req.Next()
}

View File

@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"errors"
"net/http"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
"github.com/Azure/azure-sdk-for-go/sdk/internal/temporal"
)
// BearerTokenPolicy authorizes requests with bearer tokens acquired from a TokenCredential.
type BearerTokenPolicy struct {
// mainResource is the resource to be retreived using the tenant specified in the credential
mainResource *temporal.Resource[exported.AccessToken, acquiringResourceState]
// the following fields are read-only
authzHandler policy.AuthorizationHandler
cred exported.TokenCredential
scopes []string
}
type acquiringResourceState struct {
req *policy.Request
p *BearerTokenPolicy
tro policy.TokenRequestOptions
}
// acquire acquires or updates the resource; only one
// thread/goroutine at a time ever calls this function
func acquire(state acquiringResourceState) (newResource exported.AccessToken, newExpiration time.Time, err error) {
tk, err := state.p.cred.GetToken(&shared.ContextWithDeniedValues{Context: state.req.Raw().Context()}, state.tro)
if err != nil {
return exported.AccessToken{}, time.Time{}, err
}
return tk, tk.ExpiresOn, nil
}
// NewBearerTokenPolicy creates a policy object that authorizes requests with bearer tokens.
// cred: an azcore.TokenCredential implementation such as a credential object from azidentity
// scopes: the list of permission scopes required for the token.
// opts: optional settings. Pass nil to accept default values; this is the same as passing a zero-value options.
func NewBearerTokenPolicy(cred exported.TokenCredential, scopes []string, opts *policy.BearerTokenOptions) *BearerTokenPolicy {
if opts == nil {
opts = &policy.BearerTokenOptions{}
}
return &BearerTokenPolicy{
authzHandler: opts.AuthorizationHandler,
cred: cred,
scopes: scopes,
mainResource: temporal.NewResource(acquire),
}
}
// authenticateAndAuthorize returns a function which authorizes req with a token from the policy's credential
func (b *BearerTokenPolicy) authenticateAndAuthorize(req *policy.Request) func(policy.TokenRequestOptions) error {
return func(tro policy.TokenRequestOptions) error {
as := acquiringResourceState{p: b, req: req, tro: tro}
tk, err := b.mainResource.Get(as)
if err != nil {
return err
}
req.Raw().Header.Set(shared.HeaderAuthorization, shared.BearerTokenPrefix+tk.Token)
return nil
}
}
// Do authorizes a request with a bearer token
func (b *BearerTokenPolicy) Do(req *policy.Request) (*http.Response, error) {
// skip adding the authorization header if no TokenCredential was provided.
// this prevents a panic that might be hard to diagnose and allows testing
// against http endpoints that don't require authentication.
if b.cred == nil {
return req.Next()
}
if err := checkHTTPSForAuth(req); err != nil {
return nil, err
}
var err error
if b.authzHandler.OnRequest != nil {
err = b.authzHandler.OnRequest(req, b.authenticateAndAuthorize(req))
} else {
err = b.authenticateAndAuthorize(req)(policy.TokenRequestOptions{Scopes: b.scopes})
}
if err != nil {
return nil, errorinfo.NonRetriableError(err)
}
res, err := req.Next()
if err != nil {
return nil, err
}
if res.StatusCode == http.StatusUnauthorized {
b.mainResource.Expire()
if res.Header.Get("WWW-Authenticate") != "" && b.authzHandler.OnChallenge != nil {
if err = b.authzHandler.OnChallenge(req, res, b.authenticateAndAuthorize(req)); err == nil {
res, err = req.Next()
}
}
}
if err != nil {
err = errorinfo.NonRetriableError(err)
}
return res, err
}
func checkHTTPSForAuth(req *policy.Request) error {
if strings.ToLower(req.Raw().URL.Scheme) != "https" {
return errorinfo.NonRetriableError(errors.New("authenticated requests are not permitted for non TLS protected (https) endpoints"))
}
return nil
}

View File

@ -0,0 +1,72 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"fmt"
"net/http"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
)
// bodyDownloadPolicy creates a policy object that downloads the response's body to a []byte.
func bodyDownloadPolicy(req *policy.Request) (*http.Response, error) {
resp, err := req.Next()
if err != nil {
return resp, err
}
var opValues bodyDownloadPolicyOpValues
// don't skip downloading error response bodies
if req.OperationValue(&opValues); opValues.Skip && resp.StatusCode < 400 {
return resp, err
}
// Either bodyDownloadPolicyOpValues was not specified (so skip is false)
// or it was specified and skip is false: don't skip downloading the body
_, err = Payload(resp)
if err != nil {
return resp, newBodyDownloadError(err, req)
}
return resp, err
}
// bodyDownloadPolicyOpValues is the struct containing the per-operation values
type bodyDownloadPolicyOpValues struct {
Skip bool
}
type bodyDownloadError struct {
err error
}
func newBodyDownloadError(err error, req *policy.Request) error {
// on failure, only retry the request for idempotent operations.
// we currently identify them as DELETE, GET, and PUT requests.
if m := strings.ToUpper(req.Raw().Method); m == http.MethodDelete || m == http.MethodGet || m == http.MethodPut {
// error is safe for retry
return err
}
// wrap error to avoid retries
return &bodyDownloadError{
err: err,
}
}
func (b *bodyDownloadError) Error() string {
return fmt.Sprintf("body download policy: %s", b.err.Error())
}
func (b *bodyDownloadError) NonRetriable() {
// marker method
}
func (b *bodyDownloadError) Unwrap() error {
return b.err
}
var _ errorinfo.NonRetriable = (*bodyDownloadError)(nil)

View File

@ -0,0 +1,40 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// newHTTPHeaderPolicy creates a policy object that adds custom HTTP headers to a request
func httpHeaderPolicy(req *policy.Request) (*http.Response, error) {
// check if any custom HTTP headers have been specified
if header := req.Raw().Context().Value(shared.CtxWithHTTPHeaderKey{}); header != nil {
for k, v := range header.(http.Header) {
// use Set to replace any existing value
// it also canonicalizes the header key
req.Raw().Header.Set(k, v[0])
// add any remaining values
for i := 1; i < len(v); i++ {
req.Raw().Header.Add(k, v[i])
}
}
}
return req.Next()
}
// WithHTTPHeader adds the specified http.Header to the parent context.
// Use this to specify custom HTTP headers at the API-call level.
// Any overlapping headers will have their values replaced with the values specified here.
// Deprecated: use [policy.WithHTTPHeader] instead.
func WithHTTPHeader(parent context.Context, header http.Header) context.Context {
return policy.WithHTTPHeader(parent, header)
}

View File

@ -0,0 +1,143 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
)
const (
attrHTTPMethod = "http.method"
attrHTTPURL = "http.url"
attrHTTPUserAgent = "http.user_agent"
attrHTTPStatusCode = "http.status_code"
attrAZClientReqID = "az.client_request_id"
attrAZServiceReqID = "az.service_request_id"
attrNetPeerName = "net.peer.name"
)
// newHTTPTracePolicy creates a new instance of the httpTracePolicy.
// - allowedQueryParams contains the user-specified query parameters that don't need to be redacted from the trace
func newHTTPTracePolicy(allowedQueryParams []string) exported.Policy {
return &httpTracePolicy{allowedQP: getAllowedQueryParams(allowedQueryParams)}
}
// httpTracePolicy is a policy that creates a trace for the HTTP request and its response
type httpTracePolicy struct {
allowedQP map[string]struct{}
}
// Do implements the pipeline.Policy interfaces for the httpTracePolicy type.
func (h *httpTracePolicy) Do(req *policy.Request) (resp *http.Response, err error) {
rawTracer := req.Raw().Context().Value(shared.CtxWithTracingTracer{})
if tracer, ok := rawTracer.(tracing.Tracer); ok && tracer.Enabled() {
attributes := []tracing.Attribute{
{Key: attrHTTPMethod, Value: req.Raw().Method},
{Key: attrHTTPURL, Value: getSanitizedURL(*req.Raw().URL, h.allowedQP)},
{Key: attrNetPeerName, Value: req.Raw().URL.Host},
}
if ua := req.Raw().Header.Get(shared.HeaderUserAgent); ua != "" {
attributes = append(attributes, tracing.Attribute{Key: attrHTTPUserAgent, Value: ua})
}
if reqID := req.Raw().Header.Get(shared.HeaderXMSClientRequestID); reqID != "" {
attributes = append(attributes, tracing.Attribute{Key: attrAZClientReqID, Value: reqID})
}
ctx := req.Raw().Context()
ctx, span := tracer.Start(ctx, "HTTP "+req.Raw().Method, &tracing.SpanOptions{
Kind: tracing.SpanKindClient,
Attributes: attributes,
})
defer func() {
if resp != nil {
span.SetAttributes(tracing.Attribute{Key: attrHTTPStatusCode, Value: resp.StatusCode})
if resp.StatusCode > 399 {
span.SetStatus(tracing.SpanStatusError, resp.Status)
}
if reqID := resp.Header.Get(shared.HeaderXMSRequestID); reqID != "" {
span.SetAttributes(tracing.Attribute{Key: attrAZServiceReqID, Value: reqID})
}
} else if err != nil {
var urlErr *url.Error
if errors.As(err, &urlErr) {
// calling *url.Error.Error() will include the unsanitized URL
// which we don't want. in addition, we already have the HTTP verb
// and sanitized URL in the trace so we aren't losing any info
err = urlErr.Err
}
span.SetStatus(tracing.SpanStatusError, err.Error())
}
span.End()
}()
req = req.WithContext(ctx)
}
resp, err = req.Next()
return
}
// StartSpanOptions contains the optional values for StartSpan.
type StartSpanOptions struct {
// for future expansion
}
// StartSpan starts a new tracing span.
// You must call the returned func to terminate the span. Pass the applicable error
// if the span will exit with an error condition.
// - ctx is the parent context of the newly created context
// - name is the name of the span. this is typically the fully qualified name of an API ("Client.Method")
// - tracer is the client's Tracer for creating spans
// - options contains optional values. pass nil to accept any default values
func StartSpan(ctx context.Context, name string, tracer tracing.Tracer, options *StartSpanOptions) (context.Context, func(error)) {
if !tracer.Enabled() {
return ctx, func(err error) {}
}
// we MUST propagate the active tracer before returning so that the trace policy can access it
ctx = context.WithValue(ctx, shared.CtxWithTracingTracer{}, tracer)
const newSpanKind = tracing.SpanKindInternal
if activeSpan := ctx.Value(ctxActiveSpan{}); activeSpan != nil {
// per the design guidelines, if a SDK method Foo() calls SDK method Bar(),
// then the span for Bar() must be suppressed. however, if Bar() makes a REST
// call, then Bar's HTTP span must be a child of Foo's span.
// however, there is an exception to this rule. if the SDK method Foo() is a
// messaging producer/consumer, and it takes a callback that's a SDK method
// Bar(), then the span for Bar() must _not_ be suppressed.
if kind := activeSpan.(tracing.SpanKind); kind == tracing.SpanKindClient || kind == tracing.SpanKindInternal {
return ctx, func(err error) {}
}
}
ctx, span := tracer.Start(ctx, name, &tracing.SpanOptions{
Kind: newSpanKind,
})
ctx = context.WithValue(ctx, ctxActiveSpan{}, newSpanKind)
return ctx, func(err error) {
if err != nil {
errType := strings.Replace(fmt.Sprintf("%T", err), "*exported.", "*azcore.", 1)
span.SetStatus(tracing.SpanStatusError, fmt.Sprintf("%s:\n%s", errType, err.Error()))
}
span.End()
}
}
// ctxActiveSpan is used as a context key for indicating a SDK client span is in progress.
type ctxActiveSpan struct{}

View File

@ -0,0 +1,35 @@
//go:build go1.16
// +build go1.16
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// includeResponsePolicy creates a policy that retrieves the raw HTTP response upon request
func includeResponsePolicy(req *policy.Request) (*http.Response, error) {
resp, err := req.Next()
if resp == nil {
return resp, err
}
if httpOutRaw := req.Raw().Context().Value(shared.CtxWithCaptureResponse{}); httpOutRaw != nil {
httpOut := httpOutRaw.(**http.Response)
*httpOut = resp
}
return resp, err
}
// WithCaptureResponse applies the HTTP response retrieval annotation to the parent context.
// The resp parameter will contain the HTTP response after the request has completed.
// Deprecated: use [policy.WithCaptureResponse] instead.
func WithCaptureResponse(parent context.Context, resp **http.Response) context.Context {
return policy.WithCaptureResponse(parent, resp)
}

View File

@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// KeyCredentialPolicy authorizes requests with a [azcore.KeyCredential].
type KeyCredentialPolicy struct {
cred *exported.KeyCredential
header string
prefix string
}
// KeyCredentialPolicyOptions contains the optional values configuring [KeyCredentialPolicy].
type KeyCredentialPolicyOptions struct {
// Prefix is used if the key requires a prefix before it's inserted into the HTTP request.
Prefix string
}
// NewKeyCredentialPolicy creates a new instance of [KeyCredentialPolicy].
// - cred is the [azcore.KeyCredential] used to authenticate with the service
// - header is the name of the HTTP request header in which the key is placed
// - options contains optional configuration, pass nil to accept the default values
func NewKeyCredentialPolicy(cred *exported.KeyCredential, header string, options *KeyCredentialPolicyOptions) *KeyCredentialPolicy {
if options == nil {
options = &KeyCredentialPolicyOptions{}
}
return &KeyCredentialPolicy{
cred: cred,
header: header,
prefix: options.Prefix,
}
}
// Do implementes the Do method on the [policy.Polilcy] interface.
func (k *KeyCredentialPolicy) Do(req *policy.Request) (*http.Response, error) {
// skip adding the authorization header if no KeyCredential was provided.
// this prevents a panic that might be hard to diagnose and allows testing
// against http endpoints that don't require authentication.
if k.cred != nil {
if err := checkHTTPSForAuth(req); err != nil {
return nil, err
}
val := exported.KeyCredentialGet(k.cred)
if k.prefix != "" {
val = k.prefix + val
}
req.Raw().Header.Add(k.header, val)
}
return req.Next()
}

View File

@ -0,0 +1,264 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/diag"
)
type logPolicy struct {
includeBody bool
allowedHeaders map[string]struct{}
allowedQP map[string]struct{}
}
// NewLogPolicy creates a request/response logging policy object configured using the specified options.
// Pass nil to accept the default values; this is the same as passing a zero-value options.
func NewLogPolicy(o *policy.LogOptions) policy.Policy {
if o == nil {
o = &policy.LogOptions{}
}
// construct default hash set of allowed headers
allowedHeaders := map[string]struct{}{
"accept": {},
"cache-control": {},
"connection": {},
"content-length": {},
"content-type": {},
"date": {},
"etag": {},
"expires": {},
"if-match": {},
"if-modified-since": {},
"if-none-match": {},
"if-unmodified-since": {},
"last-modified": {},
"ms-cv": {},
"pragma": {},
"request-id": {},
"retry-after": {},
"server": {},
"traceparent": {},
"transfer-encoding": {},
"user-agent": {},
"www-authenticate": {},
"x-ms-request-id": {},
"x-ms-client-request-id": {},
"x-ms-return-client-request-id": {},
}
// add any caller-specified allowed headers to the set
for _, ah := range o.AllowedHeaders {
allowedHeaders[strings.ToLower(ah)] = struct{}{}
}
// now do the same thing for query params
allowedQP := getAllowedQueryParams(o.AllowedQueryParams)
return &logPolicy{
includeBody: o.IncludeBody,
allowedHeaders: allowedHeaders,
allowedQP: allowedQP,
}
}
// getAllowedQueryParams merges the default set of allowed query parameters
// with a custom set (usually comes from client options).
func getAllowedQueryParams(customAllowedQP []string) map[string]struct{} {
allowedQP := map[string]struct{}{
"api-version": {},
}
for _, qp := range customAllowedQP {
allowedQP[strings.ToLower(qp)] = struct{}{}
}
return allowedQP
}
// logPolicyOpValues is the struct containing the per-operation values
type logPolicyOpValues struct {
try int32
start time.Time
}
func (p *logPolicy) Do(req *policy.Request) (*http.Response, error) {
// Get the per-operation values. These are saved in the Message's map so that they persist across each retry calling into this policy object.
var opValues logPolicyOpValues
if req.OperationValue(&opValues); opValues.start.IsZero() {
opValues.start = time.Now() // If this is the 1st try, record this operation's start time
}
opValues.try++ // The first try is #1 (not #0)
req.SetOperationValue(opValues)
// Log the outgoing request as informational
if log.Should(log.EventRequest) {
b := &bytes.Buffer{}
fmt.Fprintf(b, "==> OUTGOING REQUEST (Try=%d)\n", opValues.try)
p.writeRequestWithResponse(b, req, nil, nil)
var err error
if p.includeBody {
err = writeReqBody(req, b)
}
log.Write(log.EventRequest, b.String())
if err != nil {
return nil, err
}
}
// Set the time for this particular retry operation and then Do the operation.
tryStart := time.Now()
response, err := req.Next() // Make the request
tryEnd := time.Now()
tryDuration := tryEnd.Sub(tryStart)
opDuration := tryEnd.Sub(opValues.start)
if log.Should(log.EventResponse) {
// We're going to log this; build the string to log
b := &bytes.Buffer{}
fmt.Fprintf(b, "==> REQUEST/RESPONSE (Try=%d/%v, OpTime=%v) -- ", opValues.try, tryDuration, opDuration)
if err != nil { // This HTTP request did not get a response from the service
fmt.Fprint(b, "REQUEST ERROR\n")
} else {
fmt.Fprint(b, "RESPONSE RECEIVED\n")
}
p.writeRequestWithResponse(b, req, response, err)
if err != nil {
// skip frames runtime.Callers() and runtime.StackTrace()
b.WriteString(diag.StackTrace(2, 32))
} else if p.includeBody {
err = writeRespBody(response, b)
}
log.Write(log.EventResponse, b.String())
}
return response, err
}
const redactedValue = "REDACTED"
// getSanitizedURL returns a sanitized string for the provided url.URL
func getSanitizedURL(u url.URL, allowedQueryParams map[string]struct{}) string {
// redact applicable query params
qp := u.Query()
for k := range qp {
if _, ok := allowedQueryParams[strings.ToLower(k)]; !ok {
qp.Set(k, redactedValue)
}
}
u.RawQuery = qp.Encode()
return u.String()
}
// writeRequestWithResponse appends a formatted HTTP request into a Buffer. If request and/or err are
// not nil, then these are also written into the Buffer.
func (p *logPolicy) writeRequestWithResponse(b *bytes.Buffer, req *policy.Request, resp *http.Response, err error) {
// Write the request into the buffer.
fmt.Fprint(b, " "+req.Raw().Method+" "+getSanitizedURL(*req.Raw().URL, p.allowedQP)+"\n")
p.writeHeader(b, req.Raw().Header)
if resp != nil {
fmt.Fprintln(b, " --------------------------------------------------------------------------------")
fmt.Fprint(b, " RESPONSE Status: "+resp.Status+"\n")
p.writeHeader(b, resp.Header)
}
if err != nil {
fmt.Fprintln(b, " --------------------------------------------------------------------------------")
fmt.Fprint(b, " ERROR:\n"+err.Error()+"\n")
}
}
// formatHeaders appends an HTTP request's or response's header into a Buffer.
func (p *logPolicy) writeHeader(b *bytes.Buffer, header http.Header) {
if len(header) == 0 {
b.WriteString(" (no headers)\n")
return
}
keys := make([]string, 0, len(header))
// Alphabetize the headers
for k := range header {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
// don't use Get() as it will canonicalize k which might cause a mismatch
value := header[k][0]
// redact all header values not in the allow-list
if _, ok := p.allowedHeaders[strings.ToLower(k)]; !ok {
value = redactedValue
}
fmt.Fprintf(b, " %s: %+v\n", k, value)
}
}
// returns true if the request/response body should be logged.
// this is determined by looking at the content-type header value.
func shouldLogBody(b *bytes.Buffer, contentType string) bool {
contentType = strings.ToLower(contentType)
if strings.HasPrefix(contentType, "text") ||
strings.Contains(contentType, "json") ||
strings.Contains(contentType, "xml") {
return true
}
fmt.Fprintf(b, " Skip logging body for %s\n", contentType)
return false
}
// writes to a buffer, used for logging purposes
func writeReqBody(req *policy.Request, b *bytes.Buffer) error {
if req.Raw().Body == nil {
fmt.Fprint(b, " Request contained no body\n")
return nil
}
if ct := req.Raw().Header.Get(shared.HeaderContentType); !shouldLogBody(b, ct) {
return nil
}
body, err := io.ReadAll(req.Raw().Body)
if err != nil {
fmt.Fprintf(b, " Failed to read request body: %s\n", err.Error())
return err
}
if err := req.RewindBody(); err != nil {
return err
}
logBody(b, body)
return nil
}
// writes to a buffer, used for logging purposes
func writeRespBody(resp *http.Response, b *bytes.Buffer) error {
ct := resp.Header.Get(shared.HeaderContentType)
if ct == "" {
fmt.Fprint(b, " Response contained no body\n")
return nil
} else if !shouldLogBody(b, ct) {
return nil
}
body, err := Payload(resp)
if err != nil {
fmt.Fprintf(b, " Failed to read response body: %s\n", err.Error())
return err
}
if len(body) > 0 {
logBody(b, body)
} else {
fmt.Fprint(b, " Response contained no body\n")
}
return nil
}
func logBody(b *bytes.Buffer, body []byte) {
fmt.Fprintln(b, " --------------------------------------------------------------------------------")
fmt.Fprintln(b, string(body))
fmt.Fprintln(b, " --------------------------------------------------------------------------------")
}

View File

@ -0,0 +1,34 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/uuid"
)
type requestIDPolicy struct{}
// NewRequestIDPolicy returns a policy that add the x-ms-client-request-id header
func NewRequestIDPolicy() policy.Policy {
return &requestIDPolicy{}
}
func (r *requestIDPolicy) Do(req *policy.Request) (*http.Response, error) {
if req.Raw().Header.Get(shared.HeaderXMSClientRequestID) == "" {
id, err := uuid.New()
if err != nil {
return nil, err
}
req.Raw().Header.Set(shared.HeaderXMSClientRequestID, id.String())
}
return req.Next()
}

View File

@ -0,0 +1,255 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"errors"
"io"
"math"
"math/rand"
"net/http"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
"github.com/Azure/azure-sdk-for-go/sdk/internal/exported"
)
const (
defaultMaxRetries = 3
)
func setDefaults(o *policy.RetryOptions) {
if o.MaxRetries == 0 {
o.MaxRetries = defaultMaxRetries
} else if o.MaxRetries < 0 {
o.MaxRetries = 0
}
// SDK guidelines specify the default MaxRetryDelay is 60 seconds
if o.MaxRetryDelay == 0 {
o.MaxRetryDelay = 60 * time.Second
} else if o.MaxRetryDelay < 0 {
// not really an unlimited cap, but sufficiently large enough to be considered as such
o.MaxRetryDelay = math.MaxInt64
}
if o.RetryDelay == 0 {
o.RetryDelay = 800 * time.Millisecond
} else if o.RetryDelay < 0 {
o.RetryDelay = 0
}
if o.StatusCodes == nil {
// NOTE: if you change this list, you MUST update the docs in policy/policy.go
o.StatusCodes = []int{
http.StatusRequestTimeout, // 408
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
}
}
}
func calcDelay(o policy.RetryOptions, try int32) time.Duration { // try is >=1; never 0
delay := time.Duration((1<<try)-1) * o.RetryDelay
// Introduce some jitter: [0.0, 1.0) / 2 = [0.0, 0.5) + 0.8 = [0.8, 1.3)
delay = time.Duration(delay.Seconds() * (rand.Float64()/2 + 0.8) * float64(time.Second)) // NOTE: We want math/rand; not crypto/rand
if delay > o.MaxRetryDelay {
delay = o.MaxRetryDelay
}
return delay
}
// NewRetryPolicy creates a policy object configured using the specified options.
// Pass nil to accept the default values; this is the same as passing a zero-value options.
func NewRetryPolicy(o *policy.RetryOptions) policy.Policy {
if o == nil {
o = &policy.RetryOptions{}
}
p := &retryPolicy{options: *o}
return p
}
type retryPolicy struct {
options policy.RetryOptions
}
func (p *retryPolicy) Do(req *policy.Request) (resp *http.Response, err error) {
options := p.options
// check if the retry options have been overridden for this call
if override := req.Raw().Context().Value(shared.CtxWithRetryOptionsKey{}); override != nil {
options = override.(policy.RetryOptions)
}
setDefaults(&options)
// Exponential retry algorithm: ((2 ^ attempt) - 1) * delay * random(0.8, 1.2)
// When to retry: connection failure or temporary/timeout.
var rwbody *retryableRequestBody
if req.Body() != nil {
// wrap the body so we control when it's actually closed.
// do this outside the for loop so defers don't accumulate.
rwbody = &retryableRequestBody{body: req.Body()}
defer rwbody.realClose()
}
try := int32(1)
for {
resp = nil // reset
log.Writef(log.EventRetryPolicy, "=====> Try=%d", try)
// For each try, seek to the beginning of the Body stream. We do this even for the 1st try because
// the stream may not be at offset 0 when we first get it and we want the same behavior for the
// 1st try as for additional tries.
err = req.RewindBody()
if err != nil {
return
}
// RewindBody() restores Raw().Body to its original state, so set our rewindable after
if rwbody != nil {
req.Raw().Body = rwbody
}
if options.TryTimeout == 0 {
clone := req.Clone(req.Raw().Context())
resp, err = clone.Next()
} else {
// Set the per-try time for this particular retry operation and then Do the operation.
tryCtx, tryCancel := context.WithTimeout(req.Raw().Context(), options.TryTimeout)
clone := req.Clone(tryCtx)
resp, err = clone.Next() // Make the request
// if the body was already downloaded or there was an error it's safe to cancel the context now
if err != nil {
tryCancel()
} else if exported.PayloadDownloaded(resp) {
tryCancel()
} else {
// must cancel the context after the body has been read and closed
resp.Body = &contextCancelReadCloser{cf: tryCancel, body: resp.Body}
}
}
if err == nil {
log.Writef(log.EventRetryPolicy, "response %d", resp.StatusCode)
} else {
log.Writef(log.EventRetryPolicy, "error %v", err)
}
if ctxErr := req.Raw().Context().Err(); ctxErr != nil {
// don't retry if the parent context has been cancelled or its deadline exceeded
err = ctxErr
log.Writef(log.EventRetryPolicy, "abort due to %v", err)
return
}
// check if the error is not retriable
var nre errorinfo.NonRetriable
if errors.As(err, &nre) {
// the error says it's not retriable so don't retry
log.Writef(log.EventRetryPolicy, "non-retriable error %T", nre)
return
}
if options.ShouldRetry != nil {
// a non-nil ShouldRetry overrides our HTTP status code check
if !options.ShouldRetry(resp, err) {
// predicate says we shouldn't retry
log.Write(log.EventRetryPolicy, "exit due to ShouldRetry")
return
}
} else if err == nil && !HasStatusCode(resp, options.StatusCodes...) {
// if there is no error and the response code isn't in the list of retry codes then we're done.
log.Write(log.EventRetryPolicy, "exit due to non-retriable status code")
return
}
if try == options.MaxRetries+1 {
// max number of tries has been reached, don't sleep again
log.Writef(log.EventRetryPolicy, "MaxRetries %d exceeded", options.MaxRetries)
return
}
// use the delay from retry-after if available
delay := shared.RetryAfter(resp)
if delay <= 0 {
delay = calcDelay(options, try)
} else if delay > options.MaxRetryDelay {
// the retry-after delay exceeds the the cap so don't retry
log.Writef(log.EventRetryPolicy, "Retry-After delay %s exceeds MaxRetryDelay of %s", delay, options.MaxRetryDelay)
return
}
// drain before retrying so nothing is leaked
Drain(resp)
log.Writef(log.EventRetryPolicy, "End Try #%d, Delay=%v", try, delay)
select {
case <-time.After(delay):
try++
case <-req.Raw().Context().Done():
err = req.Raw().Context().Err()
log.Writef(log.EventRetryPolicy, "abort due to %v", err)
return
}
}
}
// WithRetryOptions adds the specified RetryOptions to the parent context.
// Use this to specify custom RetryOptions at the API-call level.
// Deprecated: use [policy.WithRetryOptions] instead.
func WithRetryOptions(parent context.Context, options policy.RetryOptions) context.Context {
return policy.WithRetryOptions(parent, options)
}
// ********** The following type/methods implement the retryableRequestBody (a ReadSeekCloser)
// This struct is used when sending a body to the network
type retryableRequestBody struct {
body io.ReadSeeker // Seeking is required to support retries
}
// Read reads a block of data from an inner stream and reports progress
func (b *retryableRequestBody) Read(p []byte) (n int, err error) {
return b.body.Read(p)
}
func (b *retryableRequestBody) Seek(offset int64, whence int) (offsetFromStart int64, err error) {
return b.body.Seek(offset, whence)
}
func (b *retryableRequestBody) Close() error {
// We don't want the underlying transport to close the request body on transient failures so this is a nop.
// The retry policy closes the request body upon success.
return nil
}
func (b *retryableRequestBody) realClose() error {
if c, ok := b.body.(io.Closer); ok {
return c.Close()
}
return nil
}
// ********** The following type/methods implement the contextCancelReadCloser
// contextCancelReadCloser combines an io.ReadCloser with a cancel func.
// it ensures the cancel func is invoked once the body has been read and closed.
type contextCancelReadCloser struct {
cf context.CancelFunc
body io.ReadCloser
}
func (rc *contextCancelReadCloser) Read(p []byte) (n int, err error) {
return rc.body.Read(p)
}
func (rc *contextCancelReadCloser) Close() error {
err := rc.body.Close()
rc.cf()
return err
}

View File

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// SASCredentialPolicy authorizes requests with a [azcore.SASCredential].
type SASCredentialPolicy struct {
cred *exported.SASCredential
header string
}
// SASCredentialPolicyOptions contains the optional values configuring [SASCredentialPolicy].
type SASCredentialPolicyOptions struct {
// placeholder for future optional values
}
// NewSASCredentialPolicy creates a new instance of [SASCredentialPolicy].
// - cred is the [azcore.SASCredential] used to authenticate with the service
// - header is the name of the HTTP request header in which the shared access signature is placed
// - options contains optional configuration, pass nil to accept the default values
func NewSASCredentialPolicy(cred *exported.SASCredential, header string, options *SASCredentialPolicyOptions) *SASCredentialPolicy {
return &SASCredentialPolicy{
cred: cred,
header: header,
}
}
// Do implementes the Do method on the [policy.Polilcy] interface.
func (k *SASCredentialPolicy) Do(req *policy.Request) (*http.Response, error) {
// skip adding the authorization header if no SASCredential was provided.
// this prevents a panic that might be hard to diagnose and allows testing
// against http endpoints that don't require authentication.
if k.cred != nil {
if err := checkHTTPSForAuth(req); err != nil {
return nil, err
}
req.Raw().Header.Add(k.header, exported.SASCredentialGet(k.cred))
}
return req.Next()
}

View File

@ -0,0 +1,83 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"bytes"
"fmt"
"net/http"
"os"
"runtime"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
type telemetryPolicy struct {
telemetryValue string
}
// NewTelemetryPolicy creates a telemetry policy object that adds telemetry information to outgoing HTTP requests.
// The format is [<application_id> ]azsdk-go-<mod>/<ver> <platform_info>.
// Pass nil to accept the default values; this is the same as passing a zero-value options.
func NewTelemetryPolicy(mod, ver string, o *policy.TelemetryOptions) policy.Policy {
if o == nil {
o = &policy.TelemetryOptions{}
}
tp := telemetryPolicy{}
if o.Disabled {
return &tp
}
b := &bytes.Buffer{}
// normalize ApplicationID
if o.ApplicationID != "" {
o.ApplicationID = strings.ReplaceAll(o.ApplicationID, " ", "/")
if len(o.ApplicationID) > 24 {
o.ApplicationID = o.ApplicationID[:24]
}
b.WriteString(o.ApplicationID)
b.WriteRune(' ')
}
// mod might be the fully qualified name. in that case, we just want the package name
if i := strings.LastIndex(mod, "/"); i > -1 {
mod = mod[i+1:]
}
b.WriteString(formatTelemetry(mod, ver))
b.WriteRune(' ')
b.WriteString(platformInfo)
tp.telemetryValue = b.String()
return &tp
}
func formatTelemetry(comp, ver string) string {
return fmt.Sprintf("azsdk-go-%s/%s", comp, ver)
}
func (p telemetryPolicy) Do(req *policy.Request) (*http.Response, error) {
if p.telemetryValue == "" {
return req.Next()
}
// preserve the existing User-Agent string
if ua := req.Raw().Header.Get(shared.HeaderUserAgent); ua != "" {
p.telemetryValue = fmt.Sprintf("%s %s", p.telemetryValue, ua)
}
req.Raw().Header.Set(shared.HeaderUserAgent, p.telemetryValue)
return req.Next()
}
// NOTE: the ONLY function that should write to this variable is this func
var platformInfo = func() string {
operatingSystem := runtime.GOOS // Default OS string
switch operatingSystem {
case "windows":
operatingSystem = os.Getenv("OS") // Get more specific OS information
case "linux": // accept default OS info
case "freebsd": // accept default OS info
}
return fmt.Sprintf("(%s; %s)", runtime.Version(), operatingSystem)
}()

View File

@ -0,0 +1,384 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"net/http"
"reflect"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/fake"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/loc"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/op"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
"github.com/Azure/azure-sdk-for-go/sdk/internal/poller"
)
// FinalStateVia is the enumerated type for the possible final-state-via values.
type FinalStateVia = pollers.FinalStateVia
const (
// FinalStateViaAzureAsyncOp indicates the final payload comes from the Azure-AsyncOperation URL.
FinalStateViaAzureAsyncOp = pollers.FinalStateViaAzureAsyncOp
// FinalStateViaLocation indicates the final payload comes from the Location URL.
FinalStateViaLocation = pollers.FinalStateViaLocation
// FinalStateViaOriginalURI indicates the final payload comes from the original URL.
FinalStateViaOriginalURI = pollers.FinalStateViaOriginalURI
// FinalStateViaOpLocation indicates the final payload comes from the Operation-Location URL.
FinalStateViaOpLocation = pollers.FinalStateViaOpLocation
)
// NewPollerOptions contains the optional parameters for NewPoller.
type NewPollerOptions[T any] struct {
// FinalStateVia contains the final-state-via value for the LRO.
FinalStateVia FinalStateVia
// Response contains a preconstructed response type.
// The final payload will be unmarshaled into it and returned.
Response *T
// Handler[T] contains a custom polling implementation.
Handler PollingHandler[T]
// Tracer contains the Tracer from the client that's creating the Poller.
Tracer tracing.Tracer
}
// NewPoller creates a Poller based on the provided initial response.
func NewPoller[T any](resp *http.Response, pl exported.Pipeline, options *NewPollerOptions[T]) (*Poller[T], error) {
if options == nil {
options = &NewPollerOptions[T]{}
}
result := options.Response
if result == nil {
result = new(T)
}
if options.Handler != nil {
return &Poller[T]{
op: options.Handler,
resp: resp,
result: result,
tracer: options.Tracer,
}, nil
}
defer resp.Body.Close()
// this is a back-stop in case the swagger is incorrect (i.e. missing one or more status codes for success).
// ideally the codegen should return an error if the initial response failed and not even create a poller.
if !poller.StatusCodeValid(resp) {
return nil, errors.New("the operation failed or was cancelled")
}
// determine the polling method
var opr PollingHandler[T]
var err error
if fake.Applicable(resp) {
opr, err = fake.New[T](pl, resp)
} else if async.Applicable(resp) {
// async poller must be checked first as it can also have a location header
opr, err = async.New[T](pl, resp, options.FinalStateVia)
} else if op.Applicable(resp) {
// op poller must be checked before loc as it can also have a location header
opr, err = op.New[T](pl, resp, options.FinalStateVia)
} else if loc.Applicable(resp) {
opr, err = loc.New[T](pl, resp)
} else if body.Applicable(resp) {
// must test body poller last as it's a subset of the other pollers.
// TODO: this is ambiguous for PATCH/PUT if it returns a 200 with no polling headers (sync completion)
opr, err = body.New[T](pl, resp)
} else if m := resp.Request.Method; resp.StatusCode == http.StatusAccepted && (m == http.MethodDelete || m == http.MethodPost) {
// if we get here it means we have a 202 with no polling headers.
// for DELETE and POST this is a hard error per ARM RPC spec.
return nil, errors.New("response is missing polling URL")
} else {
opr, err = pollers.NewNopPoller[T](resp)
}
if err != nil {
return nil, err
}
return &Poller[T]{
op: opr,
resp: resp,
result: result,
tracer: options.Tracer,
}, nil
}
// NewPollerFromResumeTokenOptions contains the optional parameters for NewPollerFromResumeToken.
type NewPollerFromResumeTokenOptions[T any] struct {
// Response contains a preconstructed response type.
// The final payload will be unmarshaled into it and returned.
Response *T
// Handler[T] contains a custom polling implementation.
Handler PollingHandler[T]
// Tracer contains the Tracer from the client that's creating the Poller.
Tracer tracing.Tracer
}
// NewPollerFromResumeToken creates a Poller from a resume token string.
func NewPollerFromResumeToken[T any](token string, pl exported.Pipeline, options *NewPollerFromResumeTokenOptions[T]) (*Poller[T], error) {
if options == nil {
options = &NewPollerFromResumeTokenOptions[T]{}
}
result := options.Response
if result == nil {
result = new(T)
}
if err := pollers.IsTokenValid[T](token); err != nil {
return nil, err
}
raw, err := pollers.ExtractToken(token)
if err != nil {
return nil, err
}
var asJSON map[string]interface{}
if err := json.Unmarshal(raw, &asJSON); err != nil {
return nil, err
}
opr := options.Handler
// now rehydrate the poller based on the encoded poller type
if fake.CanResume(asJSON) {
opr, _ = fake.New[T](pl, nil)
} else if opr != nil {
log.Writef(log.EventLRO, "Resuming custom poller %T.", opr)
} else if async.CanResume(asJSON) {
opr, _ = async.New[T](pl, nil, "")
} else if body.CanResume(asJSON) {
opr, _ = body.New[T](pl, nil)
} else if loc.CanResume(asJSON) {
opr, _ = loc.New[T](pl, nil)
} else if op.CanResume(asJSON) {
opr, _ = op.New[T](pl, nil, "")
} else {
return nil, fmt.Errorf("unhandled poller token %s", string(raw))
}
if err := json.Unmarshal(raw, &opr); err != nil {
return nil, err
}
return &Poller[T]{
op: opr,
result: result,
tracer: options.Tracer,
}, nil
}
// PollingHandler[T] abstracts the differences among poller implementations.
type PollingHandler[T any] interface {
// Done returns true if the LRO has reached a terminal state.
Done() bool
// Poll fetches the latest state of the LRO.
Poll(context.Context) (*http.Response, error)
// Result is called once the LRO has reached a terminal state. It populates the out parameter
// with the result of the operation.
Result(ctx context.Context, out *T) error
}
// Poller encapsulates a long-running operation, providing polling facilities until the operation reaches a terminal state.
type Poller[T any] struct {
op PollingHandler[T]
resp *http.Response
err error
result *T
tracer tracing.Tracer
done bool
}
// PollUntilDoneOptions contains the optional values for the Poller[T].PollUntilDone() method.
type PollUntilDoneOptions struct {
// Frequency is the time to wait between polling intervals in absence of a Retry-After header. Allowed minimum is one second.
// Pass zero to accept the default value (30s).
Frequency time.Duration
}
// PollUntilDone will poll the service endpoint until a terminal state is reached, an error is received, or the context expires.
// It internally uses Poll(), Done(), and Result() in its polling loop, sleeping for the specified duration between intervals.
// options: pass nil to accept the default values.
// NOTE: the default polling frequency is 30 seconds which works well for most operations. However, some operations might
// benefit from a shorter or longer duration.
func (p *Poller[T]) PollUntilDone(ctx context.Context, options *PollUntilDoneOptions) (res T, err error) {
if options == nil {
options = &PollUntilDoneOptions{}
}
cp := *options
if cp.Frequency == 0 {
cp.Frequency = 30 * time.Second
}
ctx, endSpan := StartSpan(ctx, fmt.Sprintf("%s.PollUntilDone", shortenTypeName(reflect.TypeOf(*p).Name())), p.tracer, nil)
defer func() { endSpan(err) }()
// skip the floor check when executing tests so they don't take so long
if isTest := flag.Lookup("test.v"); isTest == nil && cp.Frequency < time.Second {
err = errors.New("polling frequency minimum is one second")
return
}
start := time.Now()
logPollUntilDoneExit := func(v interface{}) {
log.Writef(log.EventLRO, "END PollUntilDone() for %T: %v, total time: %s", p.op, v, time.Since(start))
}
log.Writef(log.EventLRO, "BEGIN PollUntilDone() for %T", p.op)
if p.resp != nil {
// initial check for a retry-after header existing on the initial response
if retryAfter := shared.RetryAfter(p.resp); retryAfter > 0 {
log.Writef(log.EventLRO, "initial Retry-After delay for %s", retryAfter.String())
if err = shared.Delay(ctx, retryAfter); err != nil {
logPollUntilDoneExit(err)
return
}
}
}
// begin polling the endpoint until a terminal state is reached
for {
var resp *http.Response
resp, err = p.Poll(ctx)
if err != nil {
logPollUntilDoneExit(err)
return
}
if p.Done() {
logPollUntilDoneExit("succeeded")
res, err = p.Result(ctx)
return
}
d := cp.Frequency
if retryAfter := shared.RetryAfter(resp); retryAfter > 0 {
log.Writef(log.EventLRO, "Retry-After delay for %s", retryAfter.String())
d = retryAfter
} else {
log.Writef(log.EventLRO, "delay for %s", d.String())
}
if err = shared.Delay(ctx, d); err != nil {
logPollUntilDoneExit(err)
return
}
}
}
// Poll fetches the latest state of the LRO. It returns an HTTP response or error.
// If Poll succeeds, the poller's state is updated and the HTTP response is returned.
// If Poll fails, the poller's state is unmodified and the error is returned.
// Calling Poll on an LRO that has reached a terminal state will return the last HTTP response.
func (p *Poller[T]) Poll(ctx context.Context) (resp *http.Response, err error) {
if p.Done() {
// the LRO has reached a terminal state, don't poll again
resp = p.resp
return
}
ctx, endSpan := StartSpan(ctx, fmt.Sprintf("%s.Poll", shortenTypeName(reflect.TypeOf(*p).Name())), p.tracer, nil)
defer func() { endSpan(err) }()
resp, err = p.op.Poll(ctx)
if err != nil {
return
}
p.resp = resp
return
}
// Done returns true if the LRO has reached a terminal state.
// Once a terminal state is reached, call Result().
func (p *Poller[T]) Done() bool {
return p.op.Done()
}
// Result returns the result of the LRO and is meant to be used in conjunction with Poll and Done.
// If the LRO completed successfully, a populated instance of T is returned.
// If the LRO failed or was canceled, an *azcore.ResponseError error is returned.
// Calling this on an LRO in a non-terminal state will return an error.
func (p *Poller[T]) Result(ctx context.Context) (res T, err error) {
if !p.Done() {
err = errors.New("poller is in a non-terminal state")
return
}
if p.done {
// the result has already been retrieved, return the cached value
if p.err != nil {
err = p.err
return
}
res = *p.result
return
}
ctx, endSpan := StartSpan(ctx, fmt.Sprintf("%s.Result", shortenTypeName(reflect.TypeOf(*p).Name())), p.tracer, nil)
defer func() { endSpan(err) }()
err = p.op.Result(ctx, p.result)
var respErr *exported.ResponseError
if errors.As(err, &respErr) {
// the LRO failed. record the error
p.err = err
} else if err != nil {
// the call to Result failed, don't cache anything in this case
return
}
p.done = true
if p.err != nil {
err = p.err
return
}
res = *p.result
return
}
// ResumeToken returns a value representing the poller that can be used to resume
// the LRO at a later time. ResumeTokens are unique per service operation.
// The token's format should be considered opaque and is subject to change.
// Calling this on an LRO in a terminal state will return an error.
func (p *Poller[T]) ResumeToken() (string, error) {
if p.Done() {
return "", errors.New("poller is in a terminal state")
}
tk, err := pollers.NewResumeToken[T](p.op)
if err != nil {
return "", err
}
return tk, err
}
// extracts the type name from the string returned from reflect.Value.Name()
func shortenTypeName(s string) string {
// the value is formatted as follows
// Poller[module/Package.Type].Method
// we want to shorten the generic type parameter string to Type
// anything we don't recognize will be left as-is
begin := strings.Index(s, "[")
end := strings.Index(s, "]")
if begin == -1 || end == -1 {
return s
}
typeName := s[begin+1 : end]
if i := strings.LastIndex(typeName, "."); i > -1 {
typeName = typeName[i+1:]
}
return s[:begin+1] + typeName + s[end:]
}

View File

@ -0,0 +1,177 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"mime/multipart"
"net/url"
"path"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
// Base64Encoding is usesd to specify which base-64 encoder/decoder to use when
// encoding/decoding a slice of bytes to/from a string.
type Base64Encoding = exported.Base64Encoding
const (
// Base64StdFormat uses base64.StdEncoding for encoding and decoding payloads.
Base64StdFormat Base64Encoding = exported.Base64StdFormat
// Base64URLFormat uses base64.RawURLEncoding for encoding and decoding payloads.
Base64URLFormat Base64Encoding = exported.Base64URLFormat
)
// NewRequest creates a new policy.Request with the specified input.
// The endpoint MUST be properly encoded before calling this function.
func NewRequest(ctx context.Context, httpMethod string, endpoint string) (*policy.Request, error) {
return exported.NewRequest(ctx, httpMethod, endpoint)
}
// EncodeQueryParams will parse and encode any query parameters in the specified URL.
func EncodeQueryParams(u string) (string, error) {
before, after, found := strings.Cut(u, "?")
if !found {
return u, nil
}
qp, err := url.ParseQuery(after)
if err != nil {
return "", err
}
return before + "?" + qp.Encode(), nil
}
// JoinPaths concatenates multiple URL path segments into one path,
// inserting path separation characters as required. JoinPaths will preserve
// query parameters in the root path
func JoinPaths(root string, paths ...string) string {
if len(paths) == 0 {
return root
}
qps := ""
if strings.Contains(root, "?") {
splitPath := strings.Split(root, "?")
root, qps = splitPath[0], splitPath[1]
}
p := path.Join(paths...)
// path.Join will remove any trailing slashes.
// if one was provided, preserve it.
if strings.HasSuffix(paths[len(paths)-1], "/") && !strings.HasSuffix(p, "/") {
p += "/"
}
if qps != "" {
p = p + "?" + qps
}
if strings.HasSuffix(root, "/") && strings.HasPrefix(p, "/") {
root = root[:len(root)-1]
} else if !strings.HasSuffix(root, "/") && !strings.HasPrefix(p, "/") {
p = "/" + p
}
return root + p
}
// EncodeByteArray will base-64 encode the byte slice v.
func EncodeByteArray(v []byte, format Base64Encoding) string {
return exported.EncodeByteArray(v, format)
}
// MarshalAsByteArray will base-64 encode the byte slice v, then calls SetBody.
// The encoded value is treated as a JSON string.
func MarshalAsByteArray(req *policy.Request, v []byte, format Base64Encoding) error {
// send as a JSON string
encode := fmt.Sprintf("\"%s\"", EncodeByteArray(v, format))
return req.SetBody(exported.NopCloser(strings.NewReader(encode)), shared.ContentTypeAppJSON)
}
// MarshalAsJSON calls json.Marshal() to get the JSON encoding of v then calls SetBody.
func MarshalAsJSON(req *policy.Request, v interface{}) error {
b, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("error marshalling type %T: %s", v, err)
}
return req.SetBody(exported.NopCloser(bytes.NewReader(b)), shared.ContentTypeAppJSON)
}
// MarshalAsXML calls xml.Marshal() to get the XML encoding of v then calls SetBody.
func MarshalAsXML(req *policy.Request, v interface{}) error {
b, err := xml.Marshal(v)
if err != nil {
return fmt.Errorf("error marshalling type %T: %s", v, err)
}
// inclue the XML header as some services require it
b = []byte(xml.Header + string(b))
return req.SetBody(exported.NopCloser(bytes.NewReader(b)), shared.ContentTypeAppXML)
}
// SetMultipartFormData writes the specified keys/values as multi-part form
// fields with the specified value. File content must be specified as a ReadSeekCloser.
// All other values are treated as string values.
func SetMultipartFormData(req *policy.Request, formData map[string]interface{}) error {
body := bytes.Buffer{}
writer := multipart.NewWriter(&body)
writeContent := func(fieldname, filename string, src io.Reader) error {
fd, err := writer.CreateFormFile(fieldname, filename)
if err != nil {
return err
}
// copy the data to the form file
if _, err = io.Copy(fd, src); err != nil {
return err
}
return nil
}
for k, v := range formData {
if rsc, ok := v.(io.ReadSeekCloser); ok {
if err := writeContent(k, k, rsc); err != nil {
return err
}
continue
} else if rscs, ok := v.([]io.ReadSeekCloser); ok {
for _, rsc := range rscs {
if err := writeContent(k, k, rsc); err != nil {
return err
}
}
continue
}
// ensure the value is in string format
s, ok := v.(string)
if !ok {
s = fmt.Sprintf("%v", v)
}
if err := writer.WriteField(k, s); err != nil {
return err
}
}
if err := writer.Close(); err != nil {
return err
}
return req.SetBody(exported.NopCloser(bytes.NewReader(body.Bytes())), writer.FormDataContentType())
}
// SkipBodyDownload will disable automatic downloading of the response body.
func SkipBodyDownload(req *policy.Request) {
req.SetOperationValue(bodyDownloadPolicyOpValues{Skip: true})
}
// CtxAPINameKey is used as a context key for adding/retrieving the API name.
type CtxAPINameKey = shared.CtxAPINameKey

View File

@ -0,0 +1,109 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
azexported "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
"github.com/Azure/azure-sdk-for-go/sdk/internal/exported"
)
// Payload reads and returns the response body or an error.
// On a successful read, the response body is cached.
// Subsequent reads will access the cached value.
func Payload(resp *http.Response) ([]byte, error) {
return exported.Payload(resp, nil)
}
// HasStatusCode returns true if the Response's status code is one of the specified values.
func HasStatusCode(resp *http.Response, statusCodes ...int) bool {
return exported.HasStatusCode(resp, statusCodes...)
}
// UnmarshalAsByteArray will base-64 decode the received payload and place the result into the value pointed to by v.
func UnmarshalAsByteArray(resp *http.Response, v *[]byte, format Base64Encoding) error {
p, err := Payload(resp)
if err != nil {
return err
}
return DecodeByteArray(string(p), v, format)
}
// UnmarshalAsJSON calls json.Unmarshal() to unmarshal the received payload into the value pointed to by v.
func UnmarshalAsJSON(resp *http.Response, v interface{}) error {
payload, err := Payload(resp)
if err != nil {
return err
}
// TODO: verify early exit is correct
if len(payload) == 0 {
return nil
}
err = removeBOM(resp)
if err != nil {
return err
}
err = json.Unmarshal(payload, v)
if err != nil {
err = fmt.Errorf("unmarshalling type %T: %s", v, err)
}
return err
}
// UnmarshalAsXML calls xml.Unmarshal() to unmarshal the received payload into the value pointed to by v.
func UnmarshalAsXML(resp *http.Response, v interface{}) error {
payload, err := Payload(resp)
if err != nil {
return err
}
// TODO: verify early exit is correct
if len(payload) == 0 {
return nil
}
err = removeBOM(resp)
if err != nil {
return err
}
err = xml.Unmarshal(payload, v)
if err != nil {
err = fmt.Errorf("unmarshalling type %T: %s", v, err)
}
return err
}
// Drain reads the response body to completion then closes it. The bytes read are discarded.
func Drain(resp *http.Response) {
if resp != nil && resp.Body != nil {
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
// removeBOM removes any byte-order mark prefix from the payload if present.
func removeBOM(resp *http.Response) error {
_, err := exported.Payload(resp, &exported.PayloadOptions{
BytesModifier: func(b []byte) []byte {
// UTF8
return bytes.TrimPrefix(b, []byte("\xef\xbb\xbf"))
},
})
if err != nil {
return err
}
return nil
}
// DecodeByteArray will base-64 decode the provided string into v.
func DecodeByteArray(s string, v *[]byte, format Base64Encoding) error {
return azexported.DecodeByteArray(s, v, format)
}

View File

@ -0,0 +1,15 @@
//go:build !wasm
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"net"
)
func defaultTransportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
return dialer.DialContext
}

View File

@ -0,0 +1,15 @@
//go:build (js && wasm) || wasip1
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"context"
"net"
)
func defaultTransportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
return nil
}

View File

@ -0,0 +1,48 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package runtime
import (
"crypto/tls"
"net"
"net/http"
"time"
"golang.org/x/net/http2"
)
var defaultHTTPClient *http.Client
func init() {
defaultTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
Renegotiation: tls.RenegotiateFreelyAsClient,
},
}
// TODO: evaluate removing this once https://github.com/golang/go/issues/59690 has been fixed
if http2Transport, err := http2.ConfigureTransports(defaultTransport); err == nil {
// if the connection has been idle for 10 seconds, send a ping frame for a health check
http2Transport.ReadIdleTimeout = 10 * time.Second
// if there's no response to the ping within the timeout, the connection will be closed
http2Transport.PingTimeout = 5 * time.Second
}
defaultHTTPClient = &http.Client{
Transport: defaultTransport,
}
}

View File

@ -0,0 +1,9 @@
//go:build go1.18
// +build go1.18
// Copyright 2017 Microsoft Corporation. All rights reserved.
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// Package streaming contains helpers for streaming IO operations and progress reporting.
package streaming

View File

@ -0,0 +1,75 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package streaming
import (
"io"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
)
type progress struct {
rc io.ReadCloser
rsc io.ReadSeekCloser
pr func(bytesTransferred int64)
offset int64
}
// NopCloser returns a ReadSeekCloser with a no-op close method wrapping the provided io.ReadSeeker.
// In addition to adding a Close method to an io.ReadSeeker, this can also be used to wrap an
// io.ReadSeekCloser with a no-op Close method to allow explicit control of when the io.ReedSeekCloser
// has its underlying stream closed.
func NopCloser(rs io.ReadSeeker) io.ReadSeekCloser {
return exported.NopCloser(rs)
}
// NewRequestProgress adds progress reporting to an HTTP request's body stream.
func NewRequestProgress(body io.ReadSeekCloser, pr func(bytesTransferred int64)) io.ReadSeekCloser {
return &progress{
rc: body,
rsc: body,
pr: pr,
offset: 0,
}
}
// NewResponseProgress adds progress reporting to an HTTP response's body stream.
func NewResponseProgress(body io.ReadCloser, pr func(bytesTransferred int64)) io.ReadCloser {
return &progress{
rc: body,
rsc: nil,
pr: pr,
offset: 0,
}
}
// Read reads a block of data from an inner stream and reports progress
func (p *progress) Read(b []byte) (n int, err error) {
n, err = p.rc.Read(b)
if err != nil && err != io.EOF {
return
}
p.offset += int64(n)
// Invokes the user's callback method to report progress
p.pr(p.offset)
return
}
// Seek only expects a zero or from beginning.
func (p *progress) Seek(offset int64, whence int) (int64, error) {
// This should only ever be called with offset = 0 and whence = io.SeekStart
n, err := p.rsc.Seek(offset, whence)
if err == nil {
p.offset = int64(n)
}
return n, err
}
// requestBodyProgress supports Close but the underlying stream may not; if it does, Close will close it.
func (p *progress) Close() error {
return p.rc.Close()
}

View File

@ -0,0 +1,9 @@
//go:build go1.18
// +build go1.18
// Copyright 2017 Microsoft Corporation. All rights reserved.
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
// Package to contains various type-conversion helper functions.
package to

View File

@ -0,0 +1,21 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package to
// Ptr returns a pointer to the provided value.
func Ptr[T any](v T) *T {
return &v
}
// SliceOfPtrs returns a slice of *T from the specified values.
func SliceOfPtrs[T any](vv ...T) []*T {
slc := make([]*T, len(vv))
for i := range vv {
slc[i] = Ptr(vv[i])
}
return slc
}

View File

@ -0,0 +1,41 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package tracing
// SpanKind represents the role of a Span inside a Trace. Often, this defines how a Span will be processed and visualized by various backends.
type SpanKind int
const (
// SpanKindInternal indicates the span represents an internal operation within an application.
SpanKindInternal SpanKind = 1
// SpanKindServer indicates the span covers server-side handling of a request.
SpanKindServer SpanKind = 2
// SpanKindClient indicates the span describes a request to a remote service.
SpanKindClient SpanKind = 3
// SpanKindProducer indicates the span was created by a messaging producer.
SpanKindProducer SpanKind = 4
// SpanKindConsumer indicates the span was created by a messaging consumer.
SpanKindConsumer SpanKind = 5
)
// SpanStatus represents the status of a span.
type SpanStatus int
const (
// SpanStatusUnset is the default status code.
SpanStatusUnset SpanStatus = 0
// SpanStatusError indicates the operation contains an error.
SpanStatusError SpanStatus = 1
// SpanStatusOK indicates the operation completed successfully.
SpanStatusOK SpanStatus = 2
)

View File

@ -0,0 +1,191 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Package tracing contains the definitions needed to support distributed tracing.
package tracing
import (
"context"
)
// ProviderOptions contains the optional values when creating a Provider.
type ProviderOptions struct {
// for future expansion
}
// NewProvider creates a new Provider with the specified values.
// - newTracerFn is the underlying implementation for creating Tracer instances
// - options contains optional values; pass nil to accept the default value
func NewProvider(newTracerFn func(name, version string) Tracer, options *ProviderOptions) Provider {
return Provider{
newTracerFn: newTracerFn,
}
}
// Provider is the factory that creates Tracer instances.
// It defaults to a no-op provider.
type Provider struct {
newTracerFn func(name, version string) Tracer
}
// NewTracer creates a new Tracer for the specified module name and version.
// - module - the fully qualified name of the module
// - version - the version of the module
func (p Provider) NewTracer(module, version string) (tracer Tracer) {
if p.newTracerFn != nil {
tracer = p.newTracerFn(module, version)
}
return
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// TracerOptions contains the optional values when creating a Tracer.
type TracerOptions struct {
// SpanFromContext contains the implementation for the Tracer.SpanFromContext method.
SpanFromContext func(context.Context) Span
}
// NewTracer creates a Tracer with the specified values.
// - newSpanFn is the underlying implementation for creating Span instances
// - options contains optional values; pass nil to accept the default value
func NewTracer(newSpanFn func(ctx context.Context, spanName string, options *SpanOptions) (context.Context, Span), options *TracerOptions) Tracer {
if options == nil {
options = &TracerOptions{}
}
return Tracer{
newSpanFn: newSpanFn,
spanFromContextFn: options.SpanFromContext,
}
}
// Tracer is the factory that creates Span instances.
type Tracer struct {
attrs []Attribute
newSpanFn func(ctx context.Context, spanName string, options *SpanOptions) (context.Context, Span)
spanFromContextFn func(ctx context.Context) Span
}
// Start creates a new span and a context.Context that contains it.
// - ctx is the parent context for this span. If it contains a Span, the newly created span will be a child of that span, else it will be a root span
// - spanName identifies the span within a trace, it's typically the fully qualified API name
// - options contains optional values for the span, pass nil to accept any defaults
func (t Tracer) Start(ctx context.Context, spanName string, options *SpanOptions) (context.Context, Span) {
if t.newSpanFn != nil {
opts := SpanOptions{}
if options != nil {
opts = *options
}
opts.Attributes = append(opts.Attributes, t.attrs...)
return t.newSpanFn(ctx, spanName, &opts)
}
return ctx, Span{}
}
// SetAttributes sets attrs to be applied to each Span. If a key from attrs
// already exists for an attribute of the Span it will be overwritten with
// the value contained in attrs.
func (t *Tracer) SetAttributes(attrs ...Attribute) {
t.attrs = append(t.attrs, attrs...)
}
// Enabled returns true if this Tracer is capable of creating Spans.
func (t Tracer) Enabled() bool {
return t.newSpanFn != nil
}
// SpanFromContext returns the Span associated with the current context.
// If the provided context has no Span, false is returned.
func (t Tracer) SpanFromContext(ctx context.Context) Span {
if t.spanFromContextFn != nil {
return t.spanFromContextFn(ctx)
}
return Span{}
}
// SpanOptions contains optional settings for creating a span.
type SpanOptions struct {
// Kind indicates the kind of Span.
Kind SpanKind
// Attributes contains key-value pairs of attributes for the span.
Attributes []Attribute
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SpanImpl abstracts the underlying implementation for Span,
// allowing it to work with various tracing implementations.
// Any zero-values will have their default, no-op behavior.
type SpanImpl struct {
// End contains the implementation for the Span.End method.
End func()
// SetAttributes contains the implementation for the Span.SetAttributes method.
SetAttributes func(...Attribute)
// AddEvent contains the implementation for the Span.AddEvent method.
AddEvent func(string, ...Attribute)
// SetStatus contains the implementation for the Span.SetStatus method.
SetStatus func(SpanStatus, string)
}
// NewSpan creates a Span with the specified implementation.
func NewSpan(impl SpanImpl) Span {
return Span{
impl: impl,
}
}
// Span is a single unit of a trace. A trace can contain multiple spans.
// A zero-value Span provides a no-op implementation.
type Span struct {
impl SpanImpl
}
// End terminates the span and MUST be called before the span leaves scope.
// Any further updates to the span will be ignored after End is called.
func (s Span) End() {
if s.impl.End != nil {
s.impl.End()
}
}
// SetAttributes sets the specified attributes on the Span.
// Any existing attributes with the same keys will have their values overwritten.
func (s Span) SetAttributes(attrs ...Attribute) {
if s.impl.SetAttributes != nil {
s.impl.SetAttributes(attrs...)
}
}
// AddEvent adds a named event with an optional set of attributes to the span.
func (s Span) AddEvent(name string, attrs ...Attribute) {
if s.impl.AddEvent != nil {
s.impl.AddEvent(name, attrs...)
}
}
// SetStatus sets the status on the span along with a description.
func (s Span) SetStatus(code SpanStatus, desc string) {
if s.impl.SetStatus != nil {
s.impl.SetStatus(code, desc)
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Attribute is a key-value pair.
type Attribute struct {
// Key is the name of the attribute.
Key string
// Value is the attribute's value.
// Types that are natively supported include int64, float64, int, bool, string.
// Any other type will be formatted per rules of fmt.Sprintf("%v").
Value any
}

View File

@ -0,0 +1,490 @@
# Release History
## 1.5.1 (2024-01-17)
### Bugs Fixed
* `InteractiveBrowserCredential` handles `AdditionallyAllowedTenants` correctly
## 1.5.0 (2024-01-16)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.5.0-beta.1
* Removed persistent token caching. It will return in v1.6.0-beta.1
### Bugs Fixed
* Credentials now preserve MSAL headers e.g. X-Client-Sku
### Other Changes
* Upgraded dependencies
## 1.5.0-beta.2 (2023-11-07)
### Features Added
* `DefaultAzureCredential` and `ManagedIdentityCredential` support Azure ML managed identity
* Added spans for distributed tracing.
## 1.5.0-beta.1 (2023-10-10)
### Features Added
* Optional persistent token caching for most credentials. Set `TokenCachePersistenceOptions`
on a credential's options to enable and configure this. See the package documentation for
this version and [TOKEN_CACHING.md](https://aka.ms/azsdk/go/identity/caching) for more
details.
* `AzureDeveloperCLICredential` authenticates with the Azure Developer CLI (`azd`). This
credential is also part of the `DefaultAzureCredential` authentication flow.
## 1.4.0 (2023-10-10)
### Bugs Fixed
* `ManagedIdentityCredential` will now retry when IMDS responds 410 or 503
## 1.4.0-beta.5 (2023-09-12)
### Features Added
* Service principal credentials can request CAE tokens
### Breaking Changes
> These changes affect only code written against a beta version such as v1.4.0-beta.4
* Whether `GetToken` requests a CAE token is now determined by `TokenRequestOptions.EnableCAE`. Azure
SDK clients which support CAE will set this option automatically. Credentials no longer request CAE
tokens by default or observe the environment variable "AZURE_IDENTITY_DISABLE_CP1".
### Bugs Fixed
* Credential chains such as `DefaultAzureCredential` now try their next credential, if any, when
managed identity authentication fails in a Docker Desktop container
([#21417](https://github.com/Azure/azure-sdk-for-go/issues/21417))
## 1.4.0-beta.4 (2023-08-16)
### Other Changes
* Upgraded dependencies
## 1.3.1 (2023-08-16)
### Other Changes
* Upgraded dependencies
## 1.4.0-beta.3 (2023-08-08)
### Bugs Fixed
* One invocation of `AzureCLICredential.GetToken()` and `OnBehalfOfCredential.GetToken()`
can no longer make two authentication attempts
## 1.4.0-beta.2 (2023-07-14)
### Other Changes
* `DefaultAzureCredentialOptions.TenantID` applies to workload identity authentication
* Upgraded dependencies
## 1.4.0-beta.1 (2023-06-06)
### Other Changes
* Re-enabled CAE support as in v1.3.0-beta.3
## 1.3.0 (2023-05-09)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.3.0-beta.5
* Renamed `NewOnBehalfOfCredentialFromCertificate` to `NewOnBehalfOfCredentialWithCertificate`
* Renamed `NewOnBehalfOfCredentialFromSecret` to `NewOnBehalfOfCredentialWithSecret`
### Other Changes
* Upgraded to MSAL v1.0.0
## 1.3.0-beta.5 (2023-04-11)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.3.0-beta.4
* Moved `NewWorkloadIdentityCredential()` parameters into `WorkloadIdentityCredentialOptions`.
The constructor now reads default configuration from environment variables set by the Azure
workload identity webhook by default.
([#20478](https://github.com/Azure/azure-sdk-for-go/pull/20478))
* Removed CAE support. It will return in v1.4.0-beta.1
([#20479](https://github.com/Azure/azure-sdk-for-go/pull/20479))
### Bugs Fixed
* Fixed an issue in `DefaultAzureCredential` that could cause the managed identity endpoint check to fail in rare circumstances.
## 1.3.0-beta.4 (2023-03-08)
### Features Added
* Added `WorkloadIdentityCredentialOptions.AdditionallyAllowedTenants` and `.DisableInstanceDiscovery`
### Bugs Fixed
* Credentials now synchronize within `GetToken()` so a single instance can be shared among goroutines
([#20044](https://github.com/Azure/azure-sdk-for-go/issues/20044))
### Other Changes
* Upgraded dependencies
## 1.2.2 (2023-03-07)
### Other Changes
* Upgraded dependencies
## 1.3.0-beta.3 (2023-02-07)
### Features Added
* By default, credentials set client capability "CP1" to enable support for
[Continuous Access Evaluation (CAE)](https://docs.microsoft.com/azure/active-directory/develop/app-resilience-continuous-access-evaluation).
This indicates to Microsoft Entra ID that your application can handle CAE claims challenges.
You can disable this behavior by setting the environment variable "AZURE_IDENTITY_DISABLE_CP1" to "true".
* `InteractiveBrowserCredentialOptions.LoginHint` enables pre-populating the login
prompt with a username ([#15599](https://github.com/Azure/azure-sdk-for-go/pull/15599))
* Service principal and user credentials support ADFS authentication on Azure Stack.
Specify "adfs" as the credential's tenant.
* Applications running in private or disconnected clouds can prevent credentials from
requesting Microsoft Entra instance metadata by setting the `DisableInstanceDiscovery`
field on credential options.
* Many credentials can now be configured to authenticate in multiple tenants. The
options types for these credentials have an `AdditionallyAllowedTenants` field
that specifies additional tenants in which the credential may authenticate.
## 1.3.0-beta.2 (2023-01-10)
### Features Added
* Added `OnBehalfOfCredential` to support the on-behalf-of flow
([#16642](https://github.com/Azure/azure-sdk-for-go/issues/16642))
### Bugs Fixed
* `AzureCLICredential` reports token expiration in local time (should be UTC)
### Other Changes
* `AzureCLICredential` imposes its default timeout only when the `Context`
passed to `GetToken()` has no deadline
* Added `NewCredentialUnavailableError()`. This function constructs an error indicating
a credential can't authenticate and an encompassing `ChainedTokenCredential` should
try its next credential, if any.
## 1.3.0-beta.1 (2022-12-13)
### Features Added
* `WorkloadIdentityCredential` and `DefaultAzureCredential` support
Workload Identity Federation on Kubernetes. `DefaultAzureCredential`
support requires environment variable configuration as set by the
Workload Identity webhook.
([#15615](https://github.com/Azure/azure-sdk-for-go/issues/15615))
## 1.2.0 (2022-11-08)
### Other Changes
* This version includes all fixes and features from 1.2.0-beta.*
## 1.2.0-beta.3 (2022-10-11)
### Features Added
* `ManagedIdentityCredential` caches tokens in memory
### Bugs Fixed
* `ClientCertificateCredential` sends only the leaf cert for SNI authentication
## 1.2.0-beta.2 (2022-08-10)
### Features Added
* Added `ClientAssertionCredential` to enable applications to authenticate
with custom client assertions
### Other Changes
* Updated AuthenticationFailedError with links to TROUBLESHOOTING.md for relevant errors
* Upgraded `microsoft-authentication-library-for-go` requirement to v0.6.0
## 1.2.0-beta.1 (2022-06-07)
### Features Added
* `EnvironmentCredential` reads certificate passwords from `AZURE_CLIENT_CERTIFICATE_PASSWORD`
([#17099](https://github.com/Azure/azure-sdk-for-go/pull/17099))
## 1.1.0 (2022-06-07)
### Features Added
* `ClientCertificateCredential` and `ClientSecretCredential` support ESTS-R. First-party
applications can set environment variable `AZURE_REGIONAL_AUTHORITY_NAME` with a
region name.
([#15605](https://github.com/Azure/azure-sdk-for-go/issues/15605))
## 1.0.1 (2022-06-07)
### Other Changes
* Upgrade `microsoft-authentication-library-for-go` requirement to v0.5.1
([#18176](https://github.com/Azure/azure-sdk-for-go/issues/18176))
## 1.0.0 (2022-05-12)
### Features Added
* `DefaultAzureCredential` reads environment variable `AZURE_CLIENT_ID` for the
client ID of a user-assigned managed identity
([#17293](https://github.com/Azure/azure-sdk-for-go/pull/17293))
### Breaking Changes
* Removed `AuthorizationCodeCredential`. Use `InteractiveBrowserCredential` instead
to authenticate a user with the authorization code flow.
* Instances of `AuthenticationFailedError` are now returned by pointer.
* `GetToken()` returns `azcore.AccessToken` by value
### Bugs Fixed
* `AzureCLICredential` panics after receiving an unexpected error type
([#17490](https://github.com/Azure/azure-sdk-for-go/issues/17490))
### Other Changes
* `GetToken()` returns an error when the caller specifies no scope
* Updated to the latest versions of `golang.org/x/crypto`, `azcore` and `internal`
## 0.14.0 (2022-04-05)
### Breaking Changes
* This module now requires Go 1.18
* Removed `AuthorityHost`. Credentials are now configured for sovereign or private
clouds with the API in `azcore/cloud`, for example:
```go
// before
opts := azidentity.ClientSecretCredentialOptions{AuthorityHost: azidentity.AzureGovernment}
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, &opts)
// after
import "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
opts := azidentity.ClientSecretCredentialOptions{}
opts.Cloud = cloud.AzureGovernment
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, &opts)
```
## 0.13.2 (2022-03-08)
### Bugs Fixed
* Prevented a data race in `DefaultAzureCredential` and `ChainedTokenCredential`
([#17144](https://github.com/Azure/azure-sdk-for-go/issues/17144))
### Other Changes
* Upgraded App Service managed identity version from 2017-09-01 to 2019-08-01
([#17086](https://github.com/Azure/azure-sdk-for-go/pull/17086))
## 0.13.1 (2022-02-08)
### Features Added
* `EnvironmentCredential` supports certificate SNI authentication when
`AZURE_CLIENT_SEND_CERTIFICATE_CHAIN` is "true".
([#16851](https://github.com/Azure/azure-sdk-for-go/pull/16851))
### Bugs Fixed
* `ManagedIdentityCredential.GetToken()` now returns an error when configured for
a user assigned identity in Azure Cloud Shell (which doesn't support such identities)
([#16946](https://github.com/Azure/azure-sdk-for-go/pull/16946))
### Other Changes
* `NewDefaultAzureCredential()` logs non-fatal errors. These errors are also included in the
error returned by `DefaultAzureCredential.GetToken()` when it's unable to acquire a token
from any source. ([#15923](https://github.com/Azure/azure-sdk-for-go/issues/15923))
## 0.13.0 (2022-01-11)
### Breaking Changes
* Replaced `AuthenticationFailedError.RawResponse()` with a field having the same name
* Unexported `CredentialUnavailableError`
* Instances of `ChainedTokenCredential` will now skip looping through the list of source credentials and re-use the first successful credential on subsequent calls to `GetToken`.
* If `ChainedTokenCredentialOptions.RetrySources` is true, `ChainedTokenCredential` will continue to try all of the originally provided credentials each time the `GetToken` method is called.
* `ChainedTokenCredential.successfulCredential` will contain a reference to the last successful credential.
* `DefaultAzureCredenial` will also re-use the first successful credential on subsequent calls to `GetToken`.
* `DefaultAzureCredential.chain.successfulCredential` will also contain a reference to the last successful credential.
### Other Changes
* `ManagedIdentityCredential` no longer probes IMDS before requesting a token
from it. Also, an error response from IMDS no longer disables a credential
instance. Following an error, a credential instance will continue to send
requests to IMDS as necessary.
* Adopted MSAL for user and service principal authentication
* Updated `azcore` requirement to 0.21.0
## 0.12.0 (2021-11-02)
### Breaking Changes
* Raised minimum go version to 1.16
* Removed `NewAuthenticationPolicy()` from credentials. Clients should instead use azcore's
`runtime.NewBearerTokenPolicy()` to construct a bearer token authorization policy.
* The `AuthorityHost` field in credential options structs is now a custom type,
`AuthorityHost`, with underlying type `string`
* `NewChainedTokenCredential` has a new signature to accommodate a placeholder
options struct:
```go
// before
cred, err := NewChainedTokenCredential(credA, credB)
// after
cred, err := NewChainedTokenCredential([]azcore.TokenCredential{credA, credB}, nil)
```
* Removed `ExcludeAzureCLICredential`, `ExcludeEnvironmentCredential`, and `ExcludeMSICredential`
from `DefaultAzureCredentialOptions`
* `NewClientCertificateCredential` requires a `[]*x509.Certificate` and `crypto.PrivateKey` instead of
a path to a certificate file. Added `ParseCertificates` to simplify getting these in common cases:
```go
// before
cred, err := NewClientCertificateCredential("tenant", "client-id", "/cert.pem", nil)
// after
certData, err := os.ReadFile("/cert.pem")
certs, key, err := ParseCertificates(certData, password)
cred, err := NewClientCertificateCredential(tenantID, clientID, certs, key, nil)
```
* Removed `InteractiveBrowserCredentialOptions.ClientSecret` and `.Port`
* Removed `AADAuthenticationFailedError`
* Removed `id` parameter of `NewManagedIdentityCredential()`. User assigned identities are now
specified by `ManagedIdentityCredentialOptions.ID`:
```go
// before
cred, err := NewManagedIdentityCredential("client-id", nil)
// or, for a resource ID
opts := &ManagedIdentityCredentialOptions{ID: ResourceID}
cred, err := NewManagedIdentityCredential("/subscriptions/...", opts)
// after
clientID := ClientID("7cf7db0d-...")
opts := &ManagedIdentityCredentialOptions{ID: clientID}
// or, for a resource ID
resID: ResourceID("/subscriptions/...")
opts := &ManagedIdentityCredentialOptions{ID: resID}
cred, err := NewManagedIdentityCredential(opts)
```
* `DeviceCodeCredentialOptions.UserPrompt` has a new type: `func(context.Context, DeviceCodeMessage) error`
* Credential options structs now embed `azcore.ClientOptions`. In addition to changing literal initialization
syntax, this change renames `HTTPClient` fields to `Transport`.
* Renamed `LogCredential` to `EventCredential`
* `AzureCLICredential` no longer reads the environment variable `AZURE_CLI_PATH`
* `NewManagedIdentityCredential` no longer reads environment variables `AZURE_CLIENT_ID` and
`AZURE_RESOURCE_ID`. Use `ManagedIdentityCredentialOptions.ID` instead.
* Unexported `AuthenticationFailedError` and `CredentialUnavailableError` structs. In their place are two
interfaces having the same names.
### Bugs Fixed
* `AzureCLICredential.GetToken` no longer mutates its `opts.Scopes`
### Features Added
* Added connection configuration options to `DefaultAzureCredentialOptions`
* `AuthenticationFailedError.RawResponse()` returns the HTTP response motivating the error,
if available
### Other Changes
* `NewDefaultAzureCredential()` returns `*DefaultAzureCredential` instead of `*ChainedTokenCredential`
* Added `TenantID` field to `DefaultAzureCredentialOptions` and `AzureCLICredentialOptions`
## 0.11.0 (2021-09-08)
### Breaking Changes
* Unexported `AzureCLICredentialOptions.TokenProvider` and its type,
`AzureCLITokenProvider`
### Bug Fixes
* `ManagedIdentityCredential.GetToken` returns `CredentialUnavailableError`
when IMDS has no assigned identity, signaling `DefaultAzureCredential` to
try other credentials
## 0.10.0 (2021-08-30)
### Breaking Changes
* Update based on `azcore` refactor [#15383](https://github.com/Azure/azure-sdk-for-go/pull/15383)
## 0.9.3 (2021-08-20)
### Bugs Fixed
* `ManagedIdentityCredential.GetToken` no longer mutates its `opts.Scopes`
### Other Changes
* Bumps version of `azcore` to `v0.18.1`
## 0.9.2 (2021-07-23)
### Features Added
* Adding support for Service Fabric environment in `ManagedIdentityCredential`
* Adding an option for using a resource ID instead of client ID in `ManagedIdentityCredential`
## 0.9.1 (2021-05-24)
### Features Added
* Add LICENSE.txt and bump version information
## 0.9.0 (2021-05-21)
### Features Added
* Add support for authenticating in Azure Stack environments
* Enable user assigned identities for the IMDS scenario in `ManagedIdentityCredential`
* Add scope to resource conversion in `GetToken()` on `ManagedIdentityCredential`
## 0.8.0 (2021-01-20)
### Features Added
* Updating documentation
## 0.7.1 (2021-01-04)
### Features Added
* Adding port option to `InteractiveBrowserCredential`
## 0.7.0 (2020-12-11)
### Features Added
* Add `redirectURI` parameter back to authentication code flow
## 0.6.1 (2020-12-09)
### Features Added
* Updating query parameter in `ManagedIdentityCredential` and updating datetime string for parsing managed identity access tokens.
## 0.6.0 (2020-11-16)
### Features Added
* Remove `RedirectURL` parameter from auth code flow to align with the MSAL implementation which relies on the native client redirect URL.
## 0.5.0 (2020-10-30)
### Features Added
* Flattening credential options
## 0.4.3 (2020-10-21)
### Features Added
* Adding Azure Arc support in `ManagedIdentityCredential`
## 0.4.2 (2020-10-16)
### Features Added
* Typo fixes
## 0.4.1 (2020-10-16)
### Features Added
* Ensure authority hosts are only HTTPs
## 0.4.0 (2020-10-16)
### Features Added
* Adding options structs for credentials
## 0.3.0 (2020-10-09)
### Features Added
* Update `DeviceCodeCredential` callback
## 0.2.2 (2020-10-09)
### Features Added
* Add `AuthorizationCodeCredential`
## 0.2.1 (2020-10-06)
### Features Added
* Add `InteractiveBrowserCredential`
## 0.2.0 (2020-09-11)
### Features Added
* Refactor `azidentity` on top of `azcore` refactor
* Updated policies to conform to `policy.Policy` interface changes.
* Updated non-retriable errors to conform to `azcore.NonRetriableError`.
* Fixed calls to `Request.SetBody()` to include content type.
* Switched endpoints to string types and removed extra parsing code.
## 0.1.1 (2020-09-02)
### Features Added
* Add `AzureCLICredential` to `DefaultAzureCredential` chain
## 0.1.0 (2020-07-23)
### Features Added
* Initial Release. Azure Identity library that provides Microsoft Entra token authentication support for the SDK.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

View File

@ -0,0 +1,307 @@
# Migrating from autorest/adal to azidentity
`azidentity` provides Microsoft Entra ID ([formerly Azure Active Directory](https://learn.microsoft.com/azure/active-directory/fundamentals/new-name)) authentication for the newest Azure SDK modules (`github.com/azure-sdk-for-go/sdk/...`). Older Azure SDK packages (`github.com/azure-sdk-for-go/services/...`) use types from `github.com/go-autorest/autorest/adal` instead.
This guide shows common authentication code using `autorest/adal` and its equivalent using `azidentity`.
## Table of contents
- [Acquire a token](#acquire-a-token)
- [Client certificate authentication](#client-certificate-authentication)
- [Client secret authentication](#client-secret-authentication)
- [Configuration](#configuration)
- [Device code authentication](#device-code-authentication)
- [Managed identity](#managed-identity)
- [Use azidentity credentials with older packages](#use-azidentity-credentials-with-older-packages)
## Configuration
### `autorest/adal`
Token providers require a token audience (resource identifier) and an instance of `adal.OAuthConfig`, which requires a Microsoft Entra endpoint and tenant:
```go
import "github.com/Azure/go-autorest/autorest/adal"
oauthCfg, err := adal.NewOAuthConfig("https://login.chinacloudapi.cn", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenWithSecret(
*oauthCfg, clientID, "https://management.chinacloudapi.cn/", &adal.ServicePrincipalTokenSecret{ClientSecret: secret},
)
```
### `azidentity`
A credential instance can acquire tokens for any audience. The audience for each token is determined by the client requesting it. Credentials require endpoint configuration only for sovereign or private clouds. The `azcore/cloud` package has predefined configuration for sovereign clouds such as Azure China:
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)
clientOpts := azcore.ClientOptions{Cloud: cloud.AzureChina}
cred, err := azidentity.NewClientSecretCredential(
tenantID, clientID, secret, &azidentity.ClientSecretCredentialOptions{ClientOptions: clientOpts},
)
handle(err)
```
## Client secret authentication
### `autorest/adal`
```go
import (
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenWithSecret(
*oauthCfg, clientID, "https://management.azure.com/", &adal.ServicePrincipalTokenSecret{ClientSecret: secret},
)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, nil)
handle(err)
client, err := armsubscriptions.NewClient(cred, nil)
handle(err)
```
## Client certificate authentication
### `autorest/adal`
```go
import (
"os"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
certData, err := os.ReadFile("./example.pfx")
handle(err)
certificate, rsaPrivateKey, err := decodePkcs12(certData, "")
handle(err)
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenFromCertificate(
*oauthConfig, clientID, certificate, rsaPrivateKey, "https://management.azure.com/",
)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
certData, err := os.ReadFile("./example.pfx")
handle(err)
certs, key, err := azidentity.ParseCertificates(certData, nil)
handle(err)
cred, err = azidentity.NewClientCertificateCredential(tenantID, clientID, certs, key, nil)
handle(err)
client, err := armsubscriptions.NewClient(cred, nil)
handle(err)
```
## Managed identity
### `autorest/adal`
```go
import (
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
spt, err := adal.NewServicePrincipalTokenFromManagedIdentity("https://management.azure.com/", nil)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
cred, err := azidentity.NewManagedIdentityCredential(nil)
handle(err)
client, err := armsubscriptions.NewClient(cred, nil)
handle(err)
```
### User-assigned identities
`autorest/adal`:
```go
import "github.com/Azure/go-autorest/autorest/adal"
opts := &adal.ManagedIdentityOptions{ClientID: "..."}
spt, err := adal.NewServicePrincipalTokenFromManagedIdentity("https://management.azure.com/")
handle(err)
```
`azidentity`:
```go
import "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
opts := azidentity.ManagedIdentityCredentialOptions{ID: azidentity.ClientID("...")}
cred, err := azidentity.NewManagedIdentityCredential(&opts)
handle(err)
```
## Device code authentication
### `autorest/adal`
```go
import (
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
oauthClient := &http.Client{}
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
resource := "https://management.azure.com/"
deviceCode, err := adal.InitiateDeviceAuth(oauthClient, *oauthCfg, clientID, resource)
handle(err)
// display instructions, wait for the user to authenticate
fmt.Println(*deviceCode.Message)
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
handle(err)
spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthCfg, clientID, resource, *token)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
cred, err := azidentity.NewDeviceCodeCredential(nil)
handle(err)
client, err := armsubscriptions.NewSubscriptionsClient(cred, nil)
handle(err)
```
`azidentity.DeviceCodeCredential` will guide a user through authentication, printing instructions to the console by default. The user prompt is customizable. For more information, see the [package documentation](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DeviceCodeCredential).
## Acquire a token
### `autorest/adal`
```go
import "github.com/Azure/go-autorest/autorest/adal"
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenWithSecret(
*oauthCfg, clientID, "https://vault.azure.net", &adal.ServicePrincipalTokenSecret{ClientSecret: secret},
)
err = spt.Refresh()
if err == nil {
token := spt.Token
}
```
### `azidentity`
In ordinary usage, application code doesn't need to request tokens from credentials directly. Azure SDK clients handle token acquisition and refreshing internally. However, applications may call `GetToken()` to do so. All credential types have this method.
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, nil)
handle(err)
tk, err := cred.GetToken(
context.TODO(), policy.TokenRequestOptions{Scopes: []string{"https://vault.azure.net/.default"}},
)
if err == nil {
token := tk.Token
}
```
Note that `azidentity` credentials use the Microsoft Entra endpoint, which requires OAuth 2 scopes instead of the resource identifiers `autorest/adal` expects. For more information, see [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/permissions-consent-overview).
## Use azidentity credentials with older packages
The [azidext module](https://pkg.go.dev/github.com/jongio/azidext/go/azidext) provides an adapter for `azidentity` credential types. The adapter enables using the credential types with older Azure SDK clients. For example:
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/jongio/azidext/go/azidext"
)
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, nil)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = azidext.NewTokenCredentialAdapter(cred, []string{"https://management.azure.com//.default"})
```
![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-go%2Fsdk%2Fazidentity%2FMIGRATION.png)

View File

@ -0,0 +1,257 @@
# Azure Identity Client Module for Go
The Azure Identity module provides Microsoft Entra ID ([formerly Azure Active Directory](https://learn.microsoft.com/azure/active-directory/fundamentals/new-name)) token authentication support across the Azure SDK. It includes a set of `TokenCredential` implementations, which can be used with Azure SDK clients supporting token authentication.
[![PkgGoDev](https://pkg.go.dev/badge/github.com/Azure/azure-sdk-for-go/sdk/azidentity)](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity)
| [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/)
| [Source code](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity)
# Getting started
## Install the module
This project uses [Go modules](https://github.com/golang/go/wiki/Modules) for versioning and dependency management.
Install the Azure Identity module:
```sh
go get -u github.com/Azure/azure-sdk-for-go/sdk/azidentity
```
## Prerequisites
- an [Azure subscription](https://azure.microsoft.com/free/)
- Go 1.18
### Authenticating during local development
When debugging and executing code locally, developers typically use their own accounts to authenticate calls to Azure services. The `azidentity` module supports authenticating through developer tools to simplify local development.
#### Authenticating via the Azure CLI
`DefaultAzureCredential` and `AzureCLICredential` can authenticate as the user
signed in to the [Azure CLI](https://docs.microsoft.com/cli/azure). To sign in to the Azure CLI, run `az login`. On a system with a default web browser, the Azure CLI will launch the browser to authenticate a user.
When no default browser is available, `az login` will use the device code
authentication flow. This can also be selected manually by running `az login --use-device-code`.
#### Authenticate via the Azure Developer CLI
Developers coding outside of an IDE can also use the [Azure Developer CLI](https://aka.ms/azure-dev) to authenticate. Applications using the `DefaultAzureCredential` or the `AzureDeveloperCLICredential` can use the account logged in to the Azure Developer CLI to authenticate calls in their application when running locally.
To authenticate with the Azure Developer CLI, run `azd auth login`. On a system with a default web browser, `azd` will launch the browser to authenticate. On systems without a default web browser, run `azd auth login --use-device-code` to use the device code authentication flow.
## Key concepts
### Credentials
A credential is a type which contains or can obtain the data needed for a
service client to authenticate requests. Service clients across the Azure SDK
accept a credential instance when they are constructed, and use that credential
to authenticate requests.
The `azidentity` module focuses on OAuth authentication with Microsoft Entra ID. It offers a variety of credential types capable of acquiring a Microsoft Entra access token. See [Credential Types](#credential-types "Credential Types") for a list of this module's credential types.
### DefaultAzureCredential
`DefaultAzureCredential` is appropriate for most apps that will be deployed to Azure. It combines common production credentials with development credentials. It attempts to authenticate via the following mechanisms in this order, stopping when one succeeds:
![DefaultAzureCredential authentication flow](img/mermaidjs/DefaultAzureCredentialAuthFlow.svg)
1. **Environment** - `DefaultAzureCredential` will read account information specified via [environment variables](#environment-variables) and use it to authenticate.
1. **Workload Identity** - If the app is deployed on Kubernetes with environment variables set by the workload identity webhook, `DefaultAzureCredential` will authenticate the configured identity.
1. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it.
1. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity.
1. **Azure Developer CLI** - If the developer has authenticated via the Azure Developer CLI `azd auth login` command, the `DefaultAzureCredential` will authenticate with that account.
> Note: `DefaultAzureCredential` is intended to simplify getting started with the SDK by handling common scenarios with reasonable default behaviors. Developers who want more control or whose scenario isn't served by the default settings should use other credential types.
## Managed Identity
`DefaultAzureCredential` and `ManagedIdentityCredential` support
[managed identity authentication](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview)
in any hosting environment which supports managed identities, such as (this list is not exhaustive):
* [Azure App Service](https://docs.microsoft.com/azure/app-service/overview-managed-identity)
* [Azure Arc](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)
* [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/msi-authorization)
* [Azure Kubernetes Service](https://docs.microsoft.com/azure/aks/use-managed-identity)
* [Azure Service Fabric](https://docs.microsoft.com/azure/service-fabric/concepts-managed-identity)
* [Azure Virtual Machines](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token)
## Examples
- [Authenticate with DefaultAzureCredential](#authenticate-with-defaultazurecredential "Authenticate with DefaultAzureCredential")
- [Define a custom authentication flow with ChainedTokenCredential](#define-a-custom-authentication-flow-with-chainedtokencredential "Define a custom authentication flow with ChainedTokenCredential")
- [Specify a user-assigned managed identity for DefaultAzureCredential](#specify-a-user-assigned-managed-identity-for-defaultazurecredential)
### Authenticate with DefaultAzureCredential
This example demonstrates authenticating a client from the `armresources` module with `DefaultAzureCredential`.
```go
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
// handle error
}
client := armresources.NewResourceGroupsClient("subscription ID", cred, nil)
```
### Specify a user-assigned managed identity for DefaultAzureCredential
To configure `DefaultAzureCredential` to authenticate a user-assigned managed identity, set the environment variable `AZURE_CLIENT_ID` to the identity's client ID.
### Define a custom authentication flow with `ChainedTokenCredential`
`DefaultAzureCredential` is generally the quickest way to get started developing apps for Azure. For more advanced scenarios, `ChainedTokenCredential` links multiple credential instances to be tried sequentially when authenticating. It will try each chained credential in turn until one provides a token or fails to authenticate due to an error.
The following example demonstrates creating a credential, which will attempt to authenticate using managed identity. It will fall back to authenticating via the Azure CLI when a managed identity is unavailable.
```go
managed, err := azidentity.NewManagedIdentityCredential(nil)
if err != nil {
// handle error
}
azCLI, err := azidentity.NewAzureCLICredential(nil)
if err != nil {
// handle error
}
chain, err := azidentity.NewChainedTokenCredential([]azcore.TokenCredential{managed, azCLI}, nil)
if err != nil {
// handle error
}
client := armresources.NewResourceGroupsClient("subscription ID", chain, nil)
```
## Credential Types
### Authenticating Azure Hosted Applications
|Credential|Usage
|-|-
|[DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential)|Simplified authentication experience for getting started developing Azure apps
|[ChainedTokenCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ChainedTokenCredential)|Define custom authentication flows, composing multiple credentials
|[EnvironmentCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential)|Authenticate a service principal or user configured by environment variables
|[ManagedIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential)|Authenticate the managed identity of an Azure resource
|[WorkloadIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#WorkloadIdentityCredential)|Authenticate a workload identity on Kubernetes
### Authenticating Service Principals
|Credential|Usage
|-|-
|[ClientAssertionCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientAssertionCredential)|Authenticate a service principal with a signed client assertion
|[ClientCertificateCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientCertificateCredential)|Authenticate a service principal with a certificate
|[ClientSecretCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientSecretCredential)|Authenticate a service principal with a secret
### Authenticating Users
|Credential|Usage
|-|-
|[InteractiveBrowserCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#InteractiveBrowserCredential)|Interactively authenticate a user with the default web browser
|[DeviceCodeCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DeviceCodeCredential)|Interactively authenticate a user on a device with limited UI
|[UsernamePasswordCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#UsernamePasswordCredential)|Authenticate a user with a username and password
### Authenticating via Development Tools
|Credential|Usage
|-|-
|[AzureCLICredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#AzureCLICredential)|Authenticate as the user signed in to the Azure CLI
|[`AzureDeveloperCLICredential`](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#AzureDeveloperCLICredential)|Authenticates as the user signed in to the Azure Developer CLI
## Environment Variables
`DefaultAzureCredential` and `EnvironmentCredential` can be configured with environment variables. Each type of authentication requires values for specific variables:
#### Service principal with secret
|variable name|value
|-|-
|`AZURE_CLIENT_ID`|ID of a Microsoft Entra application
|`AZURE_TENANT_ID`|ID of the application's Microsoft Entra tenant
|`AZURE_CLIENT_SECRET`|one of the application's client secrets
#### Service principal with certificate
|variable name|value
|-|-
|`AZURE_CLIENT_ID`|ID of a Microsoft Entra application
|`AZURE_TENANT_ID`|ID of the application's Microsoft Entra tenant
|`AZURE_CLIENT_CERTIFICATE_PATH`|path to a certificate file including private key
|`AZURE_CLIENT_CERTIFICATE_PASSWORD`|password of the certificate file, if any
#### Username and password
|variable name|value
|-|-
|`AZURE_CLIENT_ID`|ID of a Microsoft Entra application
|`AZURE_USERNAME`|a username (usually an email address)
|`AZURE_PASSWORD`|that user's password
Configuration is attempted in the above order. For example, if values for a
client secret and certificate are both present, the client secret will be used.
## Token caching
Token caching is an `azidentity` feature that allows apps to:
* Cache tokens in memory (default) or on disk (opt-in).
* Improve resilience and performance.
* Reduce the number of requests made to Microsoft Entra ID to obtain access tokens.
For more details, see the [token caching documentation](https://aka.ms/azsdk/go/identity/caching).
## Troubleshooting
### Error Handling
Credentials return an `error` when they fail to authenticate or lack data they require to authenticate. For guidance on resolving errors from specific credential types, see the [troubleshooting guide](https://aka.ms/azsdk/go/identity/troubleshoot).
For more details on handling specific Microsoft Entra errors, see the Microsoft Entra [error code documentation](https://learn.microsoft.com/azure/active-directory/develop/reference-error-codes).
### Logging
This module uses the classification-based logging implementation in `azcore`. To enable console logging for all SDK modules, set `AZURE_SDK_GO_LOGGING` to `all`. Use the `azcore/log` package to control log event output or to enable logs for `azidentity` only. For example:
```go
import azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log"
// print log output to stdout
azlog.SetListener(func(event azlog.Event, s string) {
fmt.Println(s)
})
// include only azidentity credential logs
azlog.SetEvents(azidentity.EventAuthentication)
```
Credentials log basic information only, such as `GetToken` success or failure and errors. These log entries don't contain authentication secrets but may contain sensitive information.
## Next steps
Client and management modules listed on the [Azure SDK releases page](https://azure.github.io/azure-sdk/releases/latest/go.html) support authenticating with `azidentity` credential types. You can learn more about using these libraries in their documentation, which is linked from the release page.
## Provide Feedback
If you encounter bugs or have suggestions, please
[open an issue](https://github.com/Azure/azure-sdk-for-go/issues).
## Contributing
This project welcomes contributions and suggestions. Most contributions require
you to agree to a Contributor License Agreement (CLA) declaring that you have
the right to, and actually do, grant us the rights to use your contribution.
For details, visit [https://cla.microsoft.com](https://cla.microsoft.com).
When you submit a pull request, a CLA-bot will automatically determine whether
you need to provide a CLA and decorate the PR appropriately (e.g., label,
comment). Simply follow the instructions provided by the bot. You will only
need to do this once across all repos using our CLA.
This project has adopted the
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information, see the
[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any
additional questions or comments.
![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-go%2Fsdk%2Fazidentity%2FREADME.png)

View File

@ -0,0 +1,70 @@
## Token caching in the Azure Identity client module
*Token caching* is a feature provided by the Azure Identity library that allows apps to:
- Improve their resilience and performance.
- Reduce the number of requests made to Microsoft Entra ID to obtain access tokens.
- Reduce the number of times the user is prompted to authenticate.
When an app needs to access a protected Azure resource, it typically needs to obtain an access token from Entra ID. Obtaining that token involves sending a request to Entra ID and may also involve prompting the user. Entra ID then validates the credentials provided in the request and issues an access token.
Token caching, via the Azure Identity library, allows the app to store this access token [in memory](#in-memory-token-caching), where it's accessible to the current process, or [on disk](#persistent-token-caching) where it can be accessed across application or process invocations. The token can then be retrieved quickly and easily the next time the app needs to access the same resource. The app can avoid making another request to Entra ID, which reduces network traffic and improves resilience. Additionally, in scenarios where the app is authenticating users, token caching also avoids prompting the user each time new tokens are requested.
### In-memory token caching
*In-memory token caching* is the default option provided by the Azure Identity library. This caching approach allows apps to store access tokens in memory. With in-memory token caching, the library first determines if a valid access token for the requested resource is already stored in memory. If a valid token is found, it's returned to the app without the need to make another request to Entra ID. If a valid token isn't found, the library will automatically acquire a token by sending a request to Entra ID. The in-memory token cache provided by the Azure Identity library is thread-safe.
**Note:** When Azure Identity library credentials are used with Azure service libraries (for example, Azure Blob Storage), the in-memory token caching is active in the `Pipeline` layer as well. All `TokenCredential` implementations are supported there, including custom implementations external to the Azure Identity library.
#### Caching cannot be disabled
As there are many levels of caching, it's not possible disable in-memory caching. However, the in-memory cache may be cleared by creating a new credential instance.
### Persistent token caching
> Only azidentity v1.5.0-beta versions support persistent token caching
*Persistent disk token caching* is an opt-in feature in the Azure Identity library. The feature allows apps to cache access tokens in an encrypted, persistent storage mechanism. As indicated in the following table, the storage mechanism differs across operating systems.
| Operating system | Storage mechanism |
|------------------|---------------------------------------|
| Linux | kernel key retention service (keyctl) |
| macOS | Keychain |
| Windows | DPAPI |
By default the token cache will protect any data which is persisted using the user data protection APIs available on the current platform.
However, there are cases where no data protection is available, and applications may choose to allow storing the token cache in an unencrypted state by setting `TokenCachePersistenceOptions.AllowUnencryptedStorage` to `true`. This allows a credential to fall back to unencrypted storage if it can't encrypt the cache. However, we do not recommend using this storage method due to its significantly lower security measures. In addition, tokens are not encrypted solely to the current user, which could potentially allow unauthorized access to the cache by individuals with machine access.
With persistent disk token caching enabled, the library first determines if a valid access token for the requested resource is already stored in the persistent cache. If a valid token is found, it's returned to the app without the need to make another request to Entra ID. Additionally, the tokens are preserved across app runs, which:
- Makes the app more resilient to failures.
- Ensures the app can continue to function during an Entra ID outage or disruption.
- Avoids having to prompt users to authenticate each time the process is restarted.
>IMPORTANT! The token cache contains sensitive data and **MUST** be protected to prevent compromising accounts. All application decisions regarding the persistence of the token cache must consider that a breach of its content will fully compromise all the accounts it contains.
#### Example code
See the [package documentation](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity@v1.5.0-beta.1#pkg-overview) for code examples demonstrating how to configure persistent caching and access cached data.
### Credentials supporting token caching
The following table indicates the state of in-memory and persistent caching in each credential type.
**Note:** In-memory caching is activated by default. Persistent token caching needs to be enabled as shown in [this example](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity@v1.5.0-beta.1#example-package-PersistentCache).
| Credential | In-memory token caching | Persistent token caching |
|--------------------------------|---------------------------------------------------------------------|--------------------------|
| `AzureCLICredential` | Not Supported | Not Supported |
| `AzureDeveloperCLICredential` | Not Supported | Not Supported |
| `ClientAssertionCredential` | Supported | Supported |
| `ClientCertificateCredential` | Supported | Supported |
| `ClientSecretCredential` | Supported | Supported |
| `DefaultAzureCredential` | Supported if the target credential in the default chain supports it | Not Supported |
| `DeviceCodeCredential` | Supported | Supported |
| `EnvironmentCredential` | Supported | Not Supported |
| `InteractiveBrowserCredential` | Supported | Supported |
| `ManagedIdentityCredential` | Supported | Not Supported |
| `OnBehalfOfCredential` | Supported | Supported |
| `UsernamePasswordCredential` | Supported | Supported |
| `WorkloadIdentityCredential` | Supported | Supported |

View File

@ -0,0 +1,231 @@
# Troubleshoot Azure Identity authentication issues
This troubleshooting guide covers failure investigation techniques, common errors for the credential types in the `azidentity` module, and mitigation steps to resolve these errors.
## Table of contents
- [Handle azidentity errors](#handle-azidentity-errors)
- [Permission issues](#permission-issues)
- [Find relevant information in errors](#find-relevant-information-in-errors)
- [Enable and configure logging](#enable-and-configure-logging)
- [Troubleshoot AzureCLICredential authentication issues](#troubleshoot-azureclicredential-authentication-issues)
- [Troubleshoot AzureDeveloperCLICredential authentication issues](#troubleshoot-azuredeveloperclicredential-authentication-issues)
- [Troubleshoot ClientCertificateCredential authentication issues](#troubleshoot-clientcertificatecredential-authentication-issues)
- [Troubleshoot ClientSecretCredential authentication issues](#troubleshoot-clientsecretcredential-authentication-issues)
- [Troubleshoot DefaultAzureCredential authentication issues](#troubleshoot-defaultazurecredential-authentication-issues)
- [Troubleshoot EnvironmentCredential authentication issues](#troubleshoot-environmentcredential-authentication-issues)
- [Troubleshoot ManagedIdentityCredential authentication issues](#troubleshoot-managedidentitycredential-authentication-issues)
- [Azure App Service and Azure Functions managed identity](#azure-app-service-and-azure-functions-managed-identity)
- [Azure Kubernetes Service managed identity](#azure-kubernetes-service-managed-identity)
- [Azure Virtual Machine managed identity](#azure-virtual-machine-managed-identity)
- [Troubleshoot UsernamePasswordCredential authentication issues](#troubleshoot-usernamepasswordcredential-authentication-issues)
- [Troubleshoot WorkloadIdentityCredential authentication issues](#troubleshoot-workloadidentitycredential-authentication-issues)
- [Get additional help](#get-additional-help)
## Handle azidentity errors
Any service client method that makes a request to the service may return an error due to authentication failure. This is because the credential authenticates on the first call to the service and on any subsequent call that needs to refresh an access token. Authentication errors include a description of the failure and possibly an error message from Microsoft Entra ID. Depending on the application, these errors may or may not be recoverable.
### Permission issues
Service client errors with a status code of 401 or 403 often indicate that authentication succeeded but the caller doesn't have permission to access the specified API. Check the service documentation to determine which RBAC roles are needed for the request, and ensure the authenticated user or service principal has the appropriate role assignments.
## Find relevant information in errors
Authentication errors can include responses from Microsoft Entra ID and often contain information helpful in diagnosis. Consider the following error message:
```
ClientSecretCredential authentication failed
POST https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a615913/oauth2/v2.0/token
--------------------------------------------------------------------------------
RESPONSE 401 Unauthorized
--------------------------------------------------------------------------------
{
"error": "invalid_client",
"error_description": "AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app '86be4c01-505b-45e9-bfc0-9b825fd84'.\r\nTrace ID: 03da4b8e-5ffe-48ca-9754-aff4276f0100\r\nCorrelation ID: 7b12f9bb-2eef-42e3-ad75-eee69ec9088d\r\nTimestamp: 2022-03-02 18:25:26Z",
"error_codes": [
7000215
],
"timestamp": "2022-03-02 18:25:26Z",
"trace_id": "03da4b8e-5ffe-48ca-9754-aff4276f0100",
"correlation_id": "7b12f9bb-2eef-42e3-ad75-eee69ec9088d",
"error_uri": "https://login.microsoftonline.com/error?code=7000215"
}
--------------------------------------------------------------------------------
```
This error contains several pieces of information:
- __Failing Credential Type__: The type of credential that failed to authenticate. This can be helpful when diagnosing issues with chained credential types such as `DefaultAzureCredential` or `ChainedTokenCredential`.
- __Microsoft Entra ID Error Code and Message__: The error code and message returned by Microsoft Entra ID. This can give insight into the specific reason the request failed. For instance, in this case authentication failed because the provided client secret is incorrect. [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/reference-error-codes#aadsts-error-codes) has more information on AADSTS error codes.
- __Correlation ID and Timestamp__: The correlation ID and timestamp identify the request in server-side logs. This information can be useful to support engineers diagnosing unexpected Microsoft Entra failures.
### Enable and configure logging
`azidentity` provides the same logging capabilities as the rest of the Azure SDK. The simplest way to see the logs to help debug authentication issues is to print credential logs to the console.
```go
import azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log"
// print log output to stdout
azlog.SetListener(func(event azlog.Event, s string) {
fmt.Println(s)
})
// include only azidentity credential logs
azlog.SetEvents(azidentity.EventAuthentication)
```
<a id="dac"></a>
## Troubleshoot DefaultAzureCredential authentication issues
| Error |Description| Mitigation |
|---|---|---|
|"DefaultAzureCredential failed to acquire a token"|No credential in the `DefaultAzureCredential` chain provided a token|<ul><li>[Enable logging](#enable-and-configure-logging) to get further diagnostic information.</li><li>Consult the troubleshooting guide for underlying credential types for more information.</li><ul><li>[EnvironmentCredential](#troubleshoot-environmentcredential-authentication-issues)</li><li>[ManagedIdentityCredential](#troubleshoot-managedidentitycredential-authentication-issues)</li><li>[AzureCLICredential](#troubleshoot-azureclicredential-authentication-issues)</li></ul>|
|Error from the client with a status code of 401 or 403|Authentication succeeded but the authorizing Azure service responded with a 401 (Unauthorized), or 403 (Forbidden) status code|<ul><li>[Enable logging](#enable-and-configure-logging) to determine which credential in the chain returned the authenticating token.</li><li>If an unexpected credential is returning a token, check application configuration such as environment variables.</li><li>Ensure the correct role is assigned to the authenticated identity. For example, a service specific role rather than the subscription Owner role.</li></ul>|
|"managed identity timed out"|`DefaultAzureCredential` sets a short timeout on its first managed identity authentication attempt to prevent very long timeouts during local development when no managed identity is available. That timeout causes this error in production when an application requests a token before the hosting environment is ready to provide one.|Use [ManagedIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential) directly, at least in production. It doesn't set a timeout on its authentication attempts.|
## Troubleshoot EnvironmentCredential authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|Missing or incomplete environment variable configuration|A valid combination of environment variables wasn't set|Ensure the appropriate environment variables are set for the intended authentication method as described in the [module documentation](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential)|
<a id="client-secret"></a>
## Troubleshoot ClientSecretCredential authentication issues
| Error Code | Issue | Mitigation |
|---|---|---|
|AADSTS7000215|An invalid client secret was provided.|Ensure the secret provided to the credential constructor is valid. If unsure, create a new client secret using the Azure portal. Details on creating a new client secret are in [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS7000222|An expired client secret was provided.|Create a new client secret using the Azure portal. Details on creating a new client secret are in [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Microsoft Entra ID instructions](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal).|
<a id="client-cert"></a>
## Troubleshoot ClientCertificateCredential authentication issues
| Error Code | Description | Mitigation |
|---|---|---|
|AADSTS700027|Client assertion contains an invalid signature.|Ensure the specified certificate has been uploaded to the application registration as described in [Microsoft Entra ID documentation](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-1-upload-a-certificate).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Microsoft Entra ID instructions](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal).|
<a id="username-password"></a>
## Troubleshoot UsernamePasswordCredential authentication issues
| Error Code | Issue | Mitigation |
|---|---|---|
|AADSTS50126|The provided username or password is invalid.|Ensure the username and password provided to the credential constructor are valid.|
<a id="managed-id"></a>
## Troubleshoot ManagedIdentityCredential authentication issues
`ManagedIdentityCredential` is designed to work on a variety of Azure hosts support managed identity. Configuration and troubleshooting vary from host to host. The below table lists the Azure hosts that can be assigned a managed identity and are supported by `ManagedIdentityCredential`.
|Host Environment| | |
|---|---|---|
|Azure Virtual Machines and Scale Sets|[Configuration](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm)|[Troubleshooting](#azure-virtual-machine-managed-identity)|
|Azure App Service and Azure Functions|[Configuration](https://docs.microsoft.com/azure/app-service/overview-managed-identity)|[Troubleshooting](#azure-app-service-and-azure-functions-managed-identity)|
|Azure Kubernetes Service|[Configuration](https://azure.github.io/aad-pod-identity/docs/)|[Troubleshooting](#azure-kubernetes-service-managed-identity)|
|Azure Arc|[Configuration](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)||
|Azure Service Fabric|[Configuration](https://docs.microsoft.com/azure/service-fabric/concepts-managed-identity)||
### Azure Virtual Machine managed identity
| Error Message |Description| Mitigation |
|---|---|---|
|The requested identity hasnt been assigned to this resource.|The IMDS endpoint responded with a status code of 400, indicating the requested identity isnt assigned to the VM.|If using a user assigned identity, ensure the specified ID is correct.<p/><p/>If using a system assigned identity, make sure it has been enabled as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm#enable-system-assigned-managed-identity-on-an-existing-vm).|
|The request failed due to a gateway error.|The request to the IMDS endpoint failed due to a gateway error, 502 or 504 status code.|IMDS doesn't support requests via proxy or gateway. Disable proxies or gateways running on the VM for requests to the IMDS endpoint `http://169.254.169.254`|
|No response received from the managed identity endpoint.|No response was received for the request to IMDS or the request timed out.|<ul><li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
|Multiple attempts failed to obtain a token from the managed identity endpoint.|The credential has exhausted its retries for a token request.|<ul><li>Refer to the error message for more details on specific failures.<li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
#### Verify IMDS is available on the VM
If you have access to the VM, you can use `curl` to verify the managed identity endpoint is available.
```sh
curl 'http://169.254.169.254/metadata/identity/oauth2/token?resource=https://management.core.windows.net&api-version=2018-02-01' -H "Metadata: true"
```
> This command's output will contain an access token and SHOULD NOT BE SHARED, to avoid compromising account security.
### Azure App Service and Azure Functions managed identity
| Error Message |Description| Mitigation |
|---|---|---|
|Get "`http://169.254.169.254/...`" i/o timeout|The App Service host hasn't set environment variables for managed identity configuration.|<ul><li>Ensure the App Service is configured for managed identity as described in [App Service documentation](https://docs.microsoft.com/azure/app-service/overview-managed-identity).</li><li>Verify the App Service environment is properly configured and the managed identity endpoint is available. See [below](#verify-the-app-service-managed-identity-endpoint-is-available) for instructions.</li></ul>|
#### Verify the App Service managed identity endpoint is available
If you can SSH into the App Service, you can verify managed identity is available in the environment. First ensure the environment variables `IDENTITY_ENDPOINT` and `IDENTITY_SECRET` are set. Then you can verify the managed identity endpoint is available using `curl`.
```sh
curl "$IDENTITY_ENDPOINT?resource=https://management.core.windows.net&api-version=2019-08-01" -H "X-IDENTITY-HEADER: $IDENTITY_HEADER"
```
> This command's output will contain an access token and SHOULD NOT BE SHARED, to avoid compromising account security.
### Azure Kubernetes Service managed identity
#### Pod Identity
| Error Message |Description| Mitigation |
|---|---|---|
|"no azure identity found for request clientID"|The application attempted to authenticate before an identity was assigned to its pod|Verify the pod is labeled correctly. This also occurs when a correctly labeled pod authenticates before the identity is ready. To prevent initialization races, configure NMI to set the Retry-After header in its responses as described in [Pod Identity documentation](https://azure.github.io/aad-pod-identity/docs/configure/feature_flags/#set-retry-after-header-in-nmi-response).
<a id="azure-cli"></a>
## Troubleshoot AzureCLICredential authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|Azure CLI not found on path|The Azure CLI isnt installed or isn't on the application's path.|<ul><li>Ensure the Azure CLI is installed as described in [Azure CLI documentation](https://docs.microsoft.com/cli/azure/install-azure-cli).</li><li>Validate the installation location is in the application's `PATH` environment variable.</li></ul>|
|Please run 'az login' to set up account|No account is currently logged into the Azure CLI, or the login has expired.|<ul><li>Run `az login` to log into the Azure CLI. More information about Azure CLI authentication is available in the [Azure CLI documentation](https://docs.microsoft.com/cli/azure/authenticate-azure-cli).</li><li>Verify that the Azure CLI can obtain tokens. See [below](#verify-the-azure-cli-can-obtain-tokens) for instructions.</li></ul>|
#### Verify the Azure CLI can obtain tokens
You can manually verify that the Azure CLI can authenticate and obtain tokens. First, use the `account` command to verify the logged in account.
```azurecli
az account show
```
Once you've verified the Azure CLI is using the correct account, you can validate that it's able to obtain tokens for that account.
```azurecli
az account get-access-token --output json --resource https://management.core.windows.net
```
> This command's output will contain an access token and SHOULD NOT BE SHARED, to avoid compromising account security.
<a id="azd"></a>
## Troubleshoot AzureDeveloperCLICredential authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|Azure Developer CLI not found on path|The Azure Developer CLI isn't installed or couldn't be found.|<ul><li>Ensure the Azure Developer CLI is properly installed. See the installation instructions at [Install or update the Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd).</li><li>Validate the installation location has been added to the `PATH` environment variable.</li></ul>|
|Please run "azd auth login"|No account is logged into the Azure Developer CLI, or the login has expired.|<ul><li>Log in to the Azure Developer CLI using the `azd login` command.</li><li>Validate that the Azure Developer CLI can obtain tokens. For instructions, see [Verify the Azure Developer CLI can obtain tokens](#verify-the-azure-developer-cli-can-obtain-tokens).</li></ul>|
#### Verify the Azure Developer CLI can obtain tokens
You can manually verify that the Azure Developer CLI is properly authenticated and can obtain tokens. First, use the `config` command to verify the account that is currently logged in to the Azure Developer CLI.
```sh
azd config list
```
Once you've verified the Azure Developer CLI is using correct account, you can validate that it's able to obtain tokens for this account.
```sh
azd auth token --output json --scope https://management.core.windows.net/.default
```
>Note that output of this command will contain a valid access token, and SHOULD NOT BE SHARED to avoid compromising account security.
<a id="workload"></a>
## Troubleshoot `WorkloadIdentityCredential` authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|no client ID/tenant ID/token file specified|Incomplete configuration|In most cases these values are provided via environment variables set by Azure Workload Identity.<ul><li>If your application runs on Azure Kubernetes Servide (AKS) or a cluster that has deployed the Azure Workload Identity admission webhook, check pod labels and service account configuration. See the [AKS documentation](https://learn.microsoft.com/azure/aks/workload-identity-deploy-cluster#disable-workload-identity) and [Azure Workload Identity troubleshooting guide](https://azure.github.io/azure-workload-identity/docs/troubleshooting.html) for more details.<li>If your application isn't running on AKS or your cluster hasn't deployed the Workload Identity admission webhook, set these values in `WorkloadIdentityCredentialOptions`
## Get additional help
Additional information on ways to reach out for support can be found in [SUPPORT.md](https://github.com/Azure/azure-sdk-for-go/blob/main/SUPPORT.md).

View File

@ -0,0 +1,6 @@
{
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "go",
"TagPrefix": "go/azidentity",
"Tag": "go/azidentity_db4a26f583"
}

View File

@ -0,0 +1,95 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
)
var supportedAuthRecordVersions = []string{"1.0"}
// authenticationRecord is non-secret account information about an authenticated user that user credentials such as
// [DeviceCodeCredential] and [InteractiveBrowserCredential] can use to access previously cached authentication
// data. Call these credentials' Authenticate method to get an authenticationRecord for a user.
type authenticationRecord struct {
// Authority is the URL of the authority that issued the token.
Authority string `json:"authority"`
// ClientID is the ID of the application that authenticated the user.
ClientID string `json:"clientId"`
// HomeAccountID uniquely identifies the account.
HomeAccountID string `json:"homeAccountId"`
// TenantID identifies the tenant in which the user authenticated.
TenantID string `json:"tenantId"`
// Username is the user's preferred username.
Username string `json:"username"`
// Version of the AuthenticationRecord.
Version string `json:"version"`
}
// UnmarshalJSON implements json.Unmarshaler for AuthenticationRecord
func (a *authenticationRecord) UnmarshalJSON(b []byte) error {
// Default unmarshaling is fine but we want to return an error if the record's version isn't supported i.e., we
// want to inspect the unmarshalled values before deciding whether to return an error. Unmarshaling a formally
// different type enables this by assigning all the fields without recursing into this method.
type r authenticationRecord
err := json.Unmarshal(b, (*r)(a))
if err != nil {
return err
}
if a.Version == "" {
return errors.New("AuthenticationRecord must have a version")
}
for _, v := range supportedAuthRecordVersions {
if a.Version == v {
return nil
}
}
return fmt.Errorf("unsupported AuthenticationRecord version %q. This module supports %v", a.Version, supportedAuthRecordVersions)
}
// account returns the AuthenticationRecord as an MSAL Account. The account is zero-valued when the AuthenticationRecord is zero-valued.
func (a *authenticationRecord) account() public.Account {
return public.Account{
Environment: a.Authority,
HomeAccountID: a.HomeAccountID,
PreferredUsername: a.Username,
}
}
func newAuthenticationRecord(ar public.AuthResult) (authenticationRecord, error) {
u, err := url.Parse(ar.IDToken.Issuer)
if err != nil {
return authenticationRecord{}, fmt.Errorf("Authenticate expected a URL issuer but got %q", ar.IDToken.Issuer)
}
tenant := ar.IDToken.TenantID
if tenant == "" {
tenant = strings.Trim(u.Path, "/")
}
username := ar.IDToken.PreferredUsername
if username == "" {
username = ar.IDToken.UPN
}
return authenticationRecord{
Authority: fmt.Sprintf("%s://%s", u.Scheme, u.Host),
ClientID: ar.IDToken.Audience,
HomeAccountID: ar.Account.HomeAccountID,
TenantID: tenant,
Username: username,
Version: "1.0",
}, nil
}

View File

@ -0,0 +1,187 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
)
const (
azureAdditionallyAllowedTenants = "AZURE_ADDITIONALLY_ALLOWED_TENANTS"
azureAuthorityHost = "AZURE_AUTHORITY_HOST"
azureClientCertificatePassword = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
azureClientCertificatePath = "AZURE_CLIENT_CERTIFICATE_PATH"
azureClientID = "AZURE_CLIENT_ID"
azureClientSecret = "AZURE_CLIENT_SECRET"
azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE"
azurePassword = "AZURE_PASSWORD"
azureRegionalAuthorityName = "AZURE_REGIONAL_AUTHORITY_NAME"
azureTenantID = "AZURE_TENANT_ID"
azureUsername = "AZURE_USERNAME"
organizationsTenantID = "organizations"
developerSignOnClientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
defaultSuffix = "/.default"
traceNamespace = "Microsoft.Entra"
traceOpGetToken = "GetToken"
traceOpAuthenticate = "Authenticate"
)
var (
// capability CP1 indicates the client application is capable of handling CAE claims challenges
cp1 = []string{"CP1"}
errInvalidTenantID = errors.New("invalid tenantID. You can locate your tenantID by following the instructions listed here: https://learn.microsoft.com/partner-center/find-ids-and-domain-names")
)
// tokenCachePersistenceOptions contains options for persistent token caching
type tokenCachePersistenceOptions = internal.TokenCachePersistenceOptions
// setAuthorityHost initializes the authority host for credentials. Precedence is:
// 1. cloud.Configuration.ActiveDirectoryAuthorityHost value set by user
// 2. value of AZURE_AUTHORITY_HOST
// 3. default: Azure Public Cloud
func setAuthorityHost(cc cloud.Configuration) (string, error) {
host := cc.ActiveDirectoryAuthorityHost
if host == "" {
if len(cc.Services) > 0 {
return "", errors.New("missing ActiveDirectoryAuthorityHost for specified cloud")
}
host = cloud.AzurePublic.ActiveDirectoryAuthorityHost
if envAuthorityHost := os.Getenv(azureAuthorityHost); envAuthorityHost != "" {
host = envAuthorityHost
}
}
u, err := url.Parse(host)
if err != nil {
return "", err
}
if u.Scheme != "https" {
return "", errors.New("cannot use an authority host without https")
}
return host, nil
}
// resolveAdditionalTenants returns a copy of tenants, simplified when tenants contains a wildcard
func resolveAdditionalTenants(tenants []string) []string {
if len(tenants) == 0 {
return nil
}
for _, t := range tenants {
// a wildcard makes all other values redundant
if t == "*" {
return []string{"*"}
}
}
cp := make([]string, len(tenants))
copy(cp, tenants)
return cp
}
// resolveTenant returns the correct tenant for a token request
func resolveTenant(defaultTenant, specified, credName string, additionalTenants []string) (string, error) {
if specified == "" || specified == defaultTenant {
return defaultTenant, nil
}
if defaultTenant == "adfs" {
return "", errors.New("ADFS doesn't support tenants")
}
if !validTenantID(specified) {
return "", errInvalidTenantID
}
for _, t := range additionalTenants {
if t == "*" || t == specified {
return specified, nil
}
}
return "", fmt.Errorf(`%s isn't configured to acquire tokens for tenant %q. To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add "*" to allow acquiring tokens for any tenant`, credName, specified)
}
func alphanumeric(r rune) bool {
return ('0' <= r && r <= '9') || ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z')
}
func validTenantID(tenantID string) bool {
for _, r := range tenantID {
if !(alphanumeric(r) || r == '.' || r == '-') {
return false
}
}
return true
}
func doForClient(client *azcore.Client, r *http.Request) (*http.Response, error) {
req, err := runtime.NewRequest(r.Context(), r.Method, r.URL.String())
if err != nil {
return nil, err
}
if r.Body != nil && r.Body != http.NoBody {
// create a rewindable body from the existing body as required
var body io.ReadSeekCloser
if rsc, ok := r.Body.(io.ReadSeekCloser); ok {
body = rsc
} else {
b, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
body = streaming.NopCloser(bytes.NewReader(b))
}
err = req.SetBody(body, r.Header.Get("Content-Type"))
if err != nil {
return nil, err
}
}
// copy headers to the new request, ignoring any for which the new request has a value
h := req.Raw().Header
for key, vals := range r.Header {
if _, has := h[key]; !has {
for _, val := range vals {
h.Add(key, val)
}
}
}
resp, err := client.Pipeline().Do(req)
if err != nil {
return nil, err
}
return resp, err
}
// enables fakes for test scenarios
type msalConfidentialClient interface {
AcquireTokenSilent(ctx context.Context, scopes []string, options ...confidential.AcquireSilentOption) (confidential.AuthResult, error)
AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...confidential.AcquireByAuthCodeOption) (confidential.AuthResult, error)
AcquireTokenByCredential(ctx context.Context, scopes []string, options ...confidential.AcquireByCredentialOption) (confidential.AuthResult, error)
AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, options ...confidential.AcquireOnBehalfOfOption) (confidential.AuthResult, error)
}
// enables fakes for test scenarios
type msalPublicClient interface {
AcquireTokenSilent(ctx context.Context, scopes []string, options ...public.AcquireSilentOption) (public.AuthResult, error)
AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username string, password string, options ...public.AcquireByUsernamePasswordOption) (public.AuthResult, error)
AcquireTokenByDeviceCode(ctx context.Context, scopes []string, options ...public.AcquireByDeviceCodeOption) (public.DeviceCode, error)
AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...public.AcquireByAuthCodeOption) (public.AuthResult, error)
AcquireTokenInteractive(ctx context.Context, scopes []string, options ...public.AcquireInteractiveOption) (public.AuthResult, error)
}

View File

@ -0,0 +1,195 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
const credNameAzureCLI = "AzureCLICredential"
type azTokenProvider func(ctx context.Context, scopes []string, tenant, subscription string) ([]byte, error)
// AzureCLICredentialOptions contains optional parameters for AzureCLICredential.
type AzureCLICredentialOptions struct {
// AdditionallyAllowedTenants specifies tenants for which the credential may acquire tokens, in addition
// to TenantID. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the
// logged in account can access.
AdditionallyAllowedTenants []string
// subscription is the name or ID of a subscription. Set this to acquire tokens for an account other
// than the Azure CLI's current account.
subscription string
// TenantID identifies the tenant the credential should authenticate in.
// Defaults to the CLI's default tenant, which is typically the home tenant of the logged in user.
TenantID string
// inDefaultChain is true when the credential is part of DefaultAzureCredential
inDefaultChain bool
// tokenProvider is used by tests to fake invoking az
tokenProvider azTokenProvider
}
// init returns an instance of AzureCLICredentialOptions initialized with default values.
func (o *AzureCLICredentialOptions) init() {
if o.tokenProvider == nil {
o.tokenProvider = defaultAzTokenProvider
}
}
// AzureCLICredential authenticates as the identity logged in to the Azure CLI.
type AzureCLICredential struct {
mu *sync.Mutex
opts AzureCLICredentialOptions
}
// NewAzureCLICredential constructs an AzureCLICredential. Pass nil to accept default options.
func NewAzureCLICredential(options *AzureCLICredentialOptions) (*AzureCLICredential, error) {
cp := AzureCLICredentialOptions{}
if options != nil {
cp = *options
}
for _, r := range cp.subscription {
if !(alphanumeric(r) || r == '-' || r == '_' || r == ' ' || r == '.') {
return nil, fmt.Errorf("%s: invalid Subscription %q", credNameAzureCLI, cp.subscription)
}
}
if cp.TenantID != "" && !validTenantID(cp.TenantID) {
return nil, errInvalidTenantID
}
cp.init()
cp.AdditionallyAllowedTenants = resolveAdditionalTenants(cp.AdditionallyAllowedTenants)
return &AzureCLICredential{mu: &sync.Mutex{}, opts: cp}, nil
}
// GetToken requests a token from the Azure CLI. This credential doesn't cache tokens, so every call invokes the CLI.
// This method is called automatically by Azure SDK clients.
func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
at := azcore.AccessToken{}
if len(opts.Scopes) != 1 {
return at, errors.New(credNameAzureCLI + ": GetToken() requires exactly one scope")
}
if !validScope(opts.Scopes[0]) {
return at, fmt.Errorf("%s.GetToken(): invalid scope %q", credNameAzureCLI, opts.Scopes[0])
}
tenant, err := resolveTenant(c.opts.TenantID, opts.TenantID, credNameAzureCLI, c.opts.AdditionallyAllowedTenants)
if err != nil {
return at, err
}
c.mu.Lock()
defer c.mu.Unlock()
b, err := c.opts.tokenProvider(ctx, opts.Scopes, tenant, c.opts.subscription)
if err == nil {
at, err = c.createAccessToken(b)
}
if err != nil {
err = unavailableIfInChain(err, c.opts.inDefaultChain)
return at, err
}
msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", credNameAzureCLI, strings.Join(opts.Scopes, ", "))
log.Write(EventAuthentication, msg)
return at, nil
}
// defaultAzTokenProvider invokes the Azure CLI to acquire a token. It assumes
// callers have verified that all string arguments are safe to pass to the CLI.
var defaultAzTokenProvider azTokenProvider = func(ctx context.Context, scopes []string, tenantID, subscription string) ([]byte, error) {
// pass the CLI a Microsoft Entra ID v1 resource because we don't know which CLI version is installed and older ones don't support v2 scopes
resource := strings.TrimSuffix(scopes[0], defaultSuffix)
// set a default timeout for this authentication iff the application hasn't done so already
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
ctx, cancel = context.WithTimeout(ctx, cliTimeout)
defer cancel()
}
commandLine := "az account get-access-token -o json --resource " + resource
if tenantID != "" {
commandLine += " --tenant " + tenantID
}
if subscription != "" {
// subscription needs quotes because it may contain spaces
commandLine += ` --subscription "` + subscription + `"`
}
var cliCmd *exec.Cmd
if runtime.GOOS == "windows" {
dir := os.Getenv("SYSTEMROOT")
if dir == "" {
return nil, newCredentialUnavailableError(credNameAzureCLI, "environment variable 'SYSTEMROOT' has no value")
}
cliCmd = exec.CommandContext(ctx, "cmd.exe", "/c", commandLine)
cliCmd.Dir = dir
} else {
cliCmd = exec.CommandContext(ctx, "/bin/sh", "-c", commandLine)
cliCmd.Dir = "/bin"
}
cliCmd.Env = os.Environ()
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
output, err := cliCmd.Output()
if err != nil {
msg := stderr.String()
var exErr *exec.ExitError
if errors.As(err, &exErr) && exErr.ExitCode() == 127 || strings.HasPrefix(msg, "'az' is not recognized") {
msg = "Azure CLI not found on path"
}
if msg == "" {
msg = err.Error()
}
return nil, newCredentialUnavailableError(credNameAzureCLI, msg)
}
return output, nil
}
func (c *AzureCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, error) {
t := struct {
AccessToken string `json:"accessToken"`
Authority string `json:"_authority"`
ClientID string `json:"_clientId"`
ExpiresOn string `json:"expiresOn"`
IdentityProvider string `json:"identityProvider"`
IsMRRT bool `json:"isMRRT"`
RefreshToken string `json:"refreshToken"`
Resource string `json:"resource"`
TokenType string `json:"tokenType"`
UserID string `json:"userId"`
}{}
err := json.Unmarshal(tk, &t)
if err != nil {
return azcore.AccessToken{}, err
}
// the Azure CLI's "expiresOn" is local time
exp, err := time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local)
if err != nil {
return azcore.AccessToken{}, fmt.Errorf("Error parsing token expiration time %q: %v", t.ExpiresOn, err)
}
converted := azcore.AccessToken{
Token: t.AccessToken,
ExpiresOn: exp.UTC(),
}
return converted, nil
}
var _ azcore.TokenCredential = (*AzureCLICredential)(nil)

View File

@ -0,0 +1,169 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
const credNameAzureDeveloperCLI = "AzureDeveloperCLICredential"
type azdTokenProvider func(ctx context.Context, scopes []string, tenant string) ([]byte, error)
// AzureDeveloperCLICredentialOptions contains optional parameters for AzureDeveloperCLICredential.
type AzureDeveloperCLICredentialOptions struct {
// AdditionallyAllowedTenants specifies tenants for which the credential may acquire tokens, in addition
// to TenantID. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the
// logged in account can access.
AdditionallyAllowedTenants []string
// TenantID identifies the tenant the credential should authenticate in. Defaults to the azd environment,
// which is the tenant of the selected Azure subscription.
TenantID string
// inDefaultChain is true when the credential is part of DefaultAzureCredential
inDefaultChain bool
// tokenProvider is used by tests to fake invoking azd
tokenProvider azdTokenProvider
}
// AzureDeveloperCLICredential authenticates as the identity logged in to the [Azure Developer CLI].
//
// [Azure Developer CLI]: https://learn.microsoft.com/azure/developer/azure-developer-cli/overview
type AzureDeveloperCLICredential struct {
mu *sync.Mutex
opts AzureDeveloperCLICredentialOptions
}
// NewAzureDeveloperCLICredential constructs an AzureDeveloperCLICredential. Pass nil to accept default options.
func NewAzureDeveloperCLICredential(options *AzureDeveloperCLICredentialOptions) (*AzureDeveloperCLICredential, error) {
cp := AzureDeveloperCLICredentialOptions{}
if options != nil {
cp = *options
}
if cp.TenantID != "" && !validTenantID(cp.TenantID) {
return nil, errInvalidTenantID
}
if cp.tokenProvider == nil {
cp.tokenProvider = defaultAzdTokenProvider
}
return &AzureDeveloperCLICredential{mu: &sync.Mutex{}, opts: cp}, nil
}
// GetToken requests a token from the Azure Developer CLI. This credential doesn't cache tokens, so every call invokes azd.
// This method is called automatically by Azure SDK clients.
func (c *AzureDeveloperCLICredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
at := azcore.AccessToken{}
if len(opts.Scopes) == 0 {
return at, errors.New(credNameAzureDeveloperCLI + ": GetToken() requires at least one scope")
}
for _, scope := range opts.Scopes {
if !validScope(scope) {
return at, fmt.Errorf("%s.GetToken(): invalid scope %q", credNameAzureDeveloperCLI, scope)
}
}
tenant, err := resolveTenant(c.opts.TenantID, opts.TenantID, credNameAzureDeveloperCLI, c.opts.AdditionallyAllowedTenants)
if err != nil {
return at, err
}
c.mu.Lock()
defer c.mu.Unlock()
b, err := c.opts.tokenProvider(ctx, opts.Scopes, tenant)
if err == nil {
at, err = c.createAccessToken(b)
}
if err != nil {
err = unavailableIfInChain(err, c.opts.inDefaultChain)
return at, err
}
msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", credNameAzureDeveloperCLI, strings.Join(opts.Scopes, ", "))
log.Write(EventAuthentication, msg)
return at, nil
}
// defaultAzTokenProvider invokes the Azure Developer CLI to acquire a token. It assumes
// callers have verified that all string arguments are safe to pass to the CLI.
var defaultAzdTokenProvider azdTokenProvider = func(ctx context.Context, scopes []string, tenant string) ([]byte, error) {
// set a default timeout for this authentication iff the application hasn't done so already
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
ctx, cancel = context.WithTimeout(ctx, cliTimeout)
defer cancel()
}
commandLine := "azd auth token -o json"
if tenant != "" {
commandLine += " --tenant-id " + tenant
}
for _, scope := range scopes {
commandLine += " --scope " + scope
}
var cliCmd *exec.Cmd
if runtime.GOOS == "windows" {
dir := os.Getenv("SYSTEMROOT")
if dir == "" {
return nil, newCredentialUnavailableError(credNameAzureDeveloperCLI, "environment variable 'SYSTEMROOT' has no value")
}
cliCmd = exec.CommandContext(ctx, "cmd.exe", "/c", commandLine)
cliCmd.Dir = dir
} else {
cliCmd = exec.CommandContext(ctx, "/bin/sh", "-c", commandLine)
cliCmd.Dir = "/bin"
}
cliCmd.Env = os.Environ()
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
output, err := cliCmd.Output()
if err != nil {
msg := stderr.String()
var exErr *exec.ExitError
if errors.As(err, &exErr) && exErr.ExitCode() == 127 || strings.HasPrefix(msg, "'azd' is not recognized") {
msg = "Azure Developer CLI not found on path"
} else if strings.Contains(msg, "azd auth login") {
msg = `please run "azd auth login" from a command prompt to authenticate before using this credential`
}
if msg == "" {
msg = err.Error()
}
return nil, newCredentialUnavailableError(credNameAzureDeveloperCLI, msg)
}
return output, nil
}
func (c *AzureDeveloperCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, error) {
t := struct {
AccessToken string `json:"token"`
ExpiresOn string `json:"expiresOn"`
}{}
err := json.Unmarshal(tk, &t)
if err != nil {
return azcore.AccessToken{}, err
}
exp, err := time.Parse("2006-01-02T15:04:05Z", t.ExpiresOn)
if err != nil {
return azcore.AccessToken{}, fmt.Errorf("error parsing token expiration time %q: %v", t.ExpiresOn, err)
}
return azcore.AccessToken{
ExpiresOn: exp.UTC(),
Token: t.AccessToken,
}, nil
}
var _ azcore.TokenCredential = (*AzureDeveloperCLICredential)(nil)

View File

@ -0,0 +1,138 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
// ChainedTokenCredentialOptions contains optional parameters for ChainedTokenCredential.
type ChainedTokenCredentialOptions struct {
// RetrySources configures how the credential uses its sources. When true, the credential always attempts to
// authenticate through each source in turn, stopping when one succeeds. When false, the credential authenticates
// only through this first successful source--it never again tries the sources which failed.
RetrySources bool
}
// ChainedTokenCredential links together multiple credentials and tries them sequentially when authenticating. By default,
// it tries all the credentials until one authenticates, after which it always uses that credential.
type ChainedTokenCredential struct {
cond *sync.Cond
iterating bool
name string
retrySources bool
sources []azcore.TokenCredential
successfulCredential azcore.TokenCredential
}
// NewChainedTokenCredential creates a ChainedTokenCredential. Pass nil for options to accept defaults.
func NewChainedTokenCredential(sources []azcore.TokenCredential, options *ChainedTokenCredentialOptions) (*ChainedTokenCredential, error) {
if len(sources) == 0 {
return nil, errors.New("sources must contain at least one TokenCredential")
}
for _, source := range sources {
if source == nil { // cannot have a nil credential in the chain or else the application will panic when GetToken() is called on nil
return nil, errors.New("sources cannot contain nil")
}
}
cp := make([]azcore.TokenCredential, len(sources))
copy(cp, sources)
if options == nil {
options = &ChainedTokenCredentialOptions{}
}
return &ChainedTokenCredential{
cond: sync.NewCond(&sync.Mutex{}),
name: "ChainedTokenCredential",
retrySources: options.RetrySources,
sources: cp,
}, nil
}
// GetToken calls GetToken on the chained credentials in turn, stopping when one returns a token.
// This method is called automatically by Azure SDK clients.
func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if !c.retrySources {
// ensure only one goroutine at a time iterates the sources and perhaps sets c.successfulCredential
c.cond.L.Lock()
for {
if c.successfulCredential != nil {
c.cond.L.Unlock()
return c.successfulCredential.GetToken(ctx, opts)
}
if !c.iterating {
c.iterating = true
// allow other goroutines to wait while this one iterates
c.cond.L.Unlock()
break
}
c.cond.Wait()
}
}
var (
err error
errs []error
successfulCredential azcore.TokenCredential
token azcore.AccessToken
unavailableErr *credentialUnavailableError
)
for _, cred := range c.sources {
token, err = cred.GetToken(ctx, opts)
if err == nil {
log.Writef(EventAuthentication, "%s authenticated with %s", c.name, extractCredentialName(cred))
successfulCredential = cred
break
}
errs = append(errs, err)
// continue to the next source iff this one returned credentialUnavailableError
if !errors.As(err, &unavailableErr) {
break
}
}
if c.iterating {
c.cond.L.Lock()
// this is nil when all credentials returned an error
c.successfulCredential = successfulCredential
c.iterating = false
c.cond.L.Unlock()
c.cond.Broadcast()
}
// err is the error returned by the last GetToken call. It will be nil when that call succeeds
if err != nil {
// return credentialUnavailableError iff all sources did so; return AuthenticationFailedError otherwise
msg := createChainedErrorMessage(errs)
if errors.As(err, &unavailableErr) {
err = newCredentialUnavailableError(c.name, msg)
} else {
res := getResponseFromError(err)
err = newAuthenticationFailedError(c.name, msg, res, err)
}
}
return token, err
}
func createChainedErrorMessage(errs []error) string {
msg := "failed to acquire a token.\nAttempted credentials:"
for _, err := range errs {
msg += fmt.Sprintf("\n\t%s", err.Error())
}
return msg
}
func extractCredentialName(credential azcore.TokenCredential) string {
return strings.TrimPrefix(fmt.Sprintf("%T", credential), "*azidentity.")
}
var _ azcore.TokenCredential = (*ChainedTokenCredential)(nil)

View File

@ -0,0 +1,35 @@
# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file.
trigger:
branches:
include:
- main
- feature/*
- hotfix/*
- release/*
paths:
include:
- sdk/azidentity/
pr:
branches:
include:
- main
- feature/*
- hotfix/*
- release/*
paths:
include:
- sdk/azidentity/
stages:
- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml
parameters:
RunLiveTests: true
UsePipelineProxy: false
ServiceDirectory: 'azidentity'
CloudConfig:
Public:
SubscriptionConfigurations:
- $(sub-config-azure-cloud-test-resources)
# Contains alternate tenant, AAD app and cert info for testing
- $(sub-config-identity-test-resources)

View File

@ -0,0 +1,85 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameAssertion = "ClientAssertionCredential"
// ClientAssertionCredential authenticates an application with assertions provided by a callback function.
// This credential is for advanced scenarios. [ClientCertificateCredential] has a more convenient API for
// the most common assertion scenario, authenticating a service principal with a certificate. See
// [Microsoft Entra ID documentation] for details of the assertion format.
//
// [Microsoft Entra ID documentation]: https://learn.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#assertion-format
type ClientAssertionCredential struct {
client *confidentialClient
}
// ClientAssertionCredentialOptions contains optional parameters for ClientAssertionCredential.
type ClientAssertionCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// tokenCachePersistenceOptions enables persistent token caching when not nil.
tokenCachePersistenceOptions *tokenCachePersistenceOptions
}
// NewClientAssertionCredential constructs a ClientAssertionCredential. The getAssertion function must be thread safe. Pass nil for options to accept defaults.
func NewClientAssertionCredential(tenantID, clientID string, getAssertion func(context.Context) (string, error), options *ClientAssertionCredentialOptions) (*ClientAssertionCredential, error) {
if getAssertion == nil {
return nil, errors.New("getAssertion must be a function that returns assertions")
}
if options == nil {
options = &ClientAssertionCredentialOptions{}
}
cred := confidential.NewCredFromAssertionCallback(
func(ctx context.Context, _ confidential.AssertionRequestOptions) (string, error) {
return getAssertion(ctx)
},
)
msalOpts := confidentialClientOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
tokenCachePersistenceOptions: options.tokenCachePersistenceOptions,
}
c, err := newConfidentialClient(tenantID, clientID, credNameAssertion, cred, msalOpts)
if err != nil {
return nil, err
}
return &ClientAssertionCredential{client: c}, nil
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (c *ClientAssertionCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameAssertion+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.GetToken(ctx, opts)
return tk, err
}
var _ azcore.TokenCredential = (*ClientAssertionCredential)(nil)

View File

@ -0,0 +1,171 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
"golang.org/x/crypto/pkcs12"
)
const credNameCert = "ClientCertificateCredential"
// ClientCertificateCredentialOptions contains optional parameters for ClientCertificateCredential.
type ClientCertificateCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// SendCertificateChain controls whether the credential sends the public certificate chain in the x5c
// header of each token request's JWT. This is required for Subject Name/Issuer (SNI) authentication.
// Defaults to False.
SendCertificateChain bool
// tokenCachePersistenceOptions enables persistent token caching when not nil.
tokenCachePersistenceOptions *tokenCachePersistenceOptions
}
// ClientCertificateCredential authenticates a service principal with a certificate.
type ClientCertificateCredential struct {
client *confidentialClient
}
// NewClientCertificateCredential constructs a ClientCertificateCredential. Pass nil for options to accept defaults.
func NewClientCertificateCredential(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *ClientCertificateCredentialOptions) (*ClientCertificateCredential, error) {
if len(certs) == 0 {
return nil, errors.New("at least one certificate is required")
}
if options == nil {
options = &ClientCertificateCredentialOptions{}
}
cred, err := confidential.NewCredFromCert(certs, key)
if err != nil {
return nil, err
}
msalOpts := confidentialClientOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
SendX5C: options.SendCertificateChain,
tokenCachePersistenceOptions: options.tokenCachePersistenceOptions,
}
c, err := newConfidentialClient(tenantID, clientID, credNameCert, cred, msalOpts)
if err != nil {
return nil, err
}
return &ClientCertificateCredential{client: c}, nil
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (c *ClientCertificateCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameCert+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.GetToken(ctx, opts)
return tk, err
}
// ParseCertificates loads certificates and a private key, in PEM or PKCS12 format, for use with NewClientCertificateCredential.
// Pass nil for password if the private key isn't encrypted. This function can't decrypt keys in PEM format.
func ParseCertificates(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) {
var blocks []*pem.Block
var err error
if len(password) == 0 {
blocks, err = loadPEMCert(certData)
}
if len(blocks) == 0 || err != nil {
blocks, err = loadPKCS12Cert(certData, string(password))
}
if err != nil {
return nil, nil, err
}
var certs []*x509.Certificate
var pk crypto.PrivateKey
for _, block := range blocks {
switch block.Type {
case "CERTIFICATE":
c, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
certs = append(certs, c)
case "PRIVATE KEY":
if pk != nil {
return nil, nil, errors.New("certData contains multiple private keys")
}
pk, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
pk, err = x509.ParsePKCS1PrivateKey(block.Bytes)
}
if err != nil {
return nil, nil, err
}
case "RSA PRIVATE KEY":
if pk != nil {
return nil, nil, errors.New("certData contains multiple private keys")
}
pk, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, nil, err
}
}
}
if len(certs) == 0 {
return nil, nil, errors.New("found no certificate")
}
if pk == nil {
return nil, nil, errors.New("found no private key")
}
return certs, pk, nil
}
func loadPEMCert(certData []byte) ([]*pem.Block, error) {
blocks := []*pem.Block{}
for {
var block *pem.Block
block, certData = pem.Decode(certData)
if block == nil {
break
}
blocks = append(blocks, block)
}
if len(blocks) == 0 {
return nil, errors.New("didn't find any PEM blocks")
}
return blocks, nil
}
func loadPKCS12Cert(certData []byte, password string) ([]*pem.Block, error) {
blocks, err := pkcs12.ToPEM(certData, password)
if err != nil {
return nil, err
}
if len(blocks) == 0 {
// not mentioning PKCS12 in this message because we end up here when certData is garbage
return nil, errors.New("didn't find any certificate content")
}
return blocks, err
}
var _ azcore.TokenCredential = (*ClientCertificateCredential)(nil)

View File

@ -0,0 +1,75 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameSecret = "ClientSecretCredential"
// ClientSecretCredentialOptions contains optional parameters for ClientSecretCredential.
type ClientSecretCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// tokenCachePersistenceOptions enables persistent token caching when not nil.
tokenCachePersistenceOptions *tokenCachePersistenceOptions
}
// ClientSecretCredential authenticates an application with a client secret.
type ClientSecretCredential struct {
client *confidentialClient
}
// NewClientSecretCredential constructs a ClientSecretCredential. Pass nil for options to accept defaults.
func NewClientSecretCredential(tenantID string, clientID string, clientSecret string, options *ClientSecretCredentialOptions) (*ClientSecretCredential, error) {
if options == nil {
options = &ClientSecretCredentialOptions{}
}
cred, err := confidential.NewCredFromSecret(clientSecret)
if err != nil {
return nil, err
}
msalOpts := confidentialClientOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
tokenCachePersistenceOptions: options.tokenCachePersistenceOptions,
}
c, err := newConfidentialClient(tenantID, clientID, credNameSecret, cred, msalOpts)
if err != nil {
return nil, err
}
return &ClientSecretCredential{client: c}, nil
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (c *ClientSecretCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameSecret+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.GetToken(ctx, opts)
return tk, err
}
var _ azcore.TokenCredential = (*ClientSecretCredential)(nil)

View File

@ -0,0 +1,184 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
type confidentialClientOptions struct {
azcore.ClientOptions
AdditionallyAllowedTenants []string
// Assertion for on-behalf-of authentication
Assertion string
DisableInstanceDiscovery, SendX5C bool
tokenCachePersistenceOptions *tokenCachePersistenceOptions
}
// confidentialClient wraps the MSAL confidential client
type confidentialClient struct {
cae, noCAE msalConfidentialClient
caeMu, noCAEMu, clientMu *sync.Mutex
clientID, tenantID string
cred confidential.Credential
host string
name string
opts confidentialClientOptions
region string
azClient *azcore.Client
}
func newConfidentialClient(tenantID, clientID, name string, cred confidential.Credential, opts confidentialClientOptions) (*confidentialClient, error) {
if !validTenantID(tenantID) {
return nil, errInvalidTenantID
}
host, err := setAuthorityHost(opts.Cloud)
if err != nil {
return nil, err
}
client, err := azcore.NewClient(module, version, runtime.PipelineOptions{
Tracing: runtime.TracingOptions{
Namespace: traceNamespace,
},
}, &opts.ClientOptions)
if err != nil {
return nil, err
}
opts.AdditionallyAllowedTenants = resolveAdditionalTenants(opts.AdditionallyAllowedTenants)
return &confidentialClient{
caeMu: &sync.Mutex{},
clientID: clientID,
clientMu: &sync.Mutex{},
cred: cred,
host: host,
name: name,
noCAEMu: &sync.Mutex{},
opts: opts,
region: os.Getenv(azureRegionalAuthorityName),
tenantID: tenantID,
azClient: client,
}, nil
}
// GetToken requests an access token from MSAL, checking the cache first.
func (c *confidentialClient) GetToken(ctx context.Context, tro policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(tro.Scopes) < 1 {
return azcore.AccessToken{}, fmt.Errorf("%s.GetToken() requires at least one scope", c.name)
}
// we don't resolve the tenant for managed identities because they acquire tokens only from their home tenants
if c.name != credNameManagedIdentity {
tenant, err := c.resolveTenant(tro.TenantID)
if err != nil {
return azcore.AccessToken{}, err
}
tro.TenantID = tenant
}
client, mu, err := c.client(ctx, tro)
if err != nil {
return azcore.AccessToken{}, err
}
mu.Lock()
defer mu.Unlock()
var ar confidential.AuthResult
if c.opts.Assertion != "" {
ar, err = client.AcquireTokenOnBehalfOf(ctx, c.opts.Assertion, tro.Scopes, confidential.WithClaims(tro.Claims), confidential.WithTenantID(tro.TenantID))
} else {
ar, err = client.AcquireTokenSilent(ctx, tro.Scopes, confidential.WithClaims(tro.Claims), confidential.WithTenantID(tro.TenantID))
if err != nil {
ar, err = client.AcquireTokenByCredential(ctx, tro.Scopes, confidential.WithClaims(tro.Claims), confidential.WithTenantID(tro.TenantID))
}
}
if err != nil {
// We could get a credentialUnavailableError from managed identity authentication because in that case the error comes from our code.
// We return it directly because it affects the behavior of credential chains. Otherwise, we return AuthenticationFailedError.
var unavailableErr *credentialUnavailableError
if !errors.As(err, &unavailableErr) {
res := getResponseFromError(err)
err = newAuthenticationFailedError(c.name, err.Error(), res, err)
}
} else {
msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", c.name, strings.Join(ar.GrantedScopes, ", "))
log.Write(EventAuthentication, msg)
}
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *confidentialClient) client(ctx context.Context, tro policy.TokenRequestOptions) (msalConfidentialClient, *sync.Mutex, error) {
c.clientMu.Lock()
defer c.clientMu.Unlock()
if tro.EnableCAE {
if c.cae == nil {
client, err := c.newMSALClient(true)
if err != nil {
return nil, nil, err
}
c.cae = client
}
return c.cae, c.caeMu, nil
}
if c.noCAE == nil {
client, err := c.newMSALClient(false)
if err != nil {
return nil, nil, err
}
c.noCAE = client
}
return c.noCAE, c.noCAEMu, nil
}
func (c *confidentialClient) newMSALClient(enableCAE bool) (msalConfidentialClient, error) {
cache, err := internal.NewCache(c.opts.tokenCachePersistenceOptions, enableCAE)
if err != nil {
return nil, err
}
authority := runtime.JoinPaths(c.host, c.tenantID)
o := []confidential.Option{
confidential.WithAzureRegion(c.region),
confidential.WithCache(cache),
confidential.WithHTTPClient(c),
}
if enableCAE {
o = append(o, confidential.WithClientCapabilities(cp1))
}
if c.opts.SendX5C {
o = append(o, confidential.WithX5C())
}
if c.opts.DisableInstanceDiscovery || strings.ToLower(c.tenantID) == "adfs" {
o = append(o, confidential.WithInstanceDiscovery(false))
}
return confidential.New(authority, c.clientID, c.cred, o...)
}
// resolveTenant returns the correct WithTenantID() argument for a token request given the client's
// configuration, or an error when that configuration doesn't allow the specified tenant
func (c *confidentialClient) resolveTenant(specified string) (string, error) {
return resolveTenant(c.tenantID, specified, c.name, c.opts.AdditionallyAllowedTenants)
}
// these methods satisfy the MSAL ops.HTTPClient interface
func (c *confidentialClient) CloseIdleConnections() {
// do nothing
}
func (c *confidentialClient) Do(r *http.Request) (*http.Response, error) {
return doForClient(c.azClient, r)
}

View File

@ -0,0 +1,208 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"os"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
// DefaultAzureCredentialOptions contains optional parameters for DefaultAzureCredential.
// These options may not apply to all credentials in the chain.
type DefaultAzureCredentialOptions struct {
// ClientOptions has additional options for credentials that use an Azure SDK HTTP pipeline. These options don't apply
// to credential types that authenticate via external tools such as the Azure CLI.
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens. Add
// the wildcard value "*" to allow the credential to acquire tokens for any tenant. This value can also be
// set as a semicolon delimited list of tenants in the environment variable AZURE_ADDITIONALLY_ALLOWED_TENANTS.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID sets the default tenant for authentication via the Azure CLI and workload identity.
TenantID string
}
// DefaultAzureCredential is a default credential chain for applications that will deploy to Azure.
// It combines credentials suitable for deployment with credentials suitable for local development.
// It attempts to authenticate with each of these credential types, in the following order, stopping
// when one provides a token:
//
// - [EnvironmentCredential]
// - [WorkloadIdentityCredential], if environment variable configuration is set by the Azure workload
// identity webhook. Use [WorkloadIdentityCredential] directly when not using the webhook or needing
// more control over its configuration.
// - [ManagedIdentityCredential]
// - [AzureCLICredential]
// - [AzureDeveloperCLICredential]
//
// Consult the documentation for these credential types for more information on how they authenticate.
// Once a credential has successfully authenticated, DefaultAzureCredential will use that credential for
// every subsequent authentication.
type DefaultAzureCredential struct {
chain *ChainedTokenCredential
}
// NewDefaultAzureCredential creates a DefaultAzureCredential. Pass nil for options to accept defaults.
func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*DefaultAzureCredential, error) {
var creds []azcore.TokenCredential
var errorMessages []string
if options == nil {
options = &DefaultAzureCredentialOptions{}
}
additionalTenants := options.AdditionallyAllowedTenants
if len(additionalTenants) == 0 {
if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
additionalTenants = strings.Split(tenants, ";")
}
}
envCred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
additionallyAllowedTenants: additionalTenants,
})
if err == nil {
creds = append(creds, envCred)
} else {
errorMessages = append(errorMessages, "EnvironmentCredential: "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: "EnvironmentCredential", err: err})
}
wic, err := NewWorkloadIdentityCredential(&WorkloadIdentityCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
TenantID: options.TenantID,
})
if err == nil {
creds = append(creds, wic)
} else {
errorMessages = append(errorMessages, credNameWorkloadIdentity+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err})
}
o := &ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions}
if ID, ok := os.LookupEnv(azureClientID); ok {
o.ID = ClientID(ID)
}
miCred, err := NewManagedIdentityCredential(o)
if err == nil {
creds = append(creds, &timeoutWrapper{mic: miCred, timeout: time.Second})
} else {
errorMessages = append(errorMessages, credNameManagedIdentity+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameManagedIdentity, err: err})
}
cliCred, err := NewAzureCLICredential(&AzureCLICredentialOptions{AdditionallyAllowedTenants: additionalTenants, TenantID: options.TenantID})
if err == nil {
creds = append(creds, cliCred)
} else {
errorMessages = append(errorMessages, credNameAzureCLI+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameAzureCLI, err: err})
}
azdCred, err := NewAzureDeveloperCLICredential(&AzureDeveloperCLICredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
TenantID: options.TenantID,
})
if err == nil {
creds = append(creds, azdCred)
} else {
errorMessages = append(errorMessages, credNameAzureDeveloperCLI+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameAzureDeveloperCLI, err: err})
}
if len(errorMessages) > 0 {
log.Writef(EventAuthentication, "NewDefaultAzureCredential failed to initialize some credentials:\n\t%s", strings.Join(errorMessages, "\n\t"))
}
chain, err := NewChainedTokenCredential(creds, nil)
if err != nil {
return nil, err
}
chain.name = "DefaultAzureCredential"
return &DefaultAzureCredential{chain: chain}, nil
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (c *DefaultAzureCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.chain.GetToken(ctx, opts)
}
var _ azcore.TokenCredential = (*DefaultAzureCredential)(nil)
// defaultCredentialErrorReporter is a substitute for credentials that couldn't be constructed.
// Its GetToken method always returns a credentialUnavailableError having the same message as
// the error that prevented constructing the credential. This ensures the message is present
// in the error returned by ChainedTokenCredential.GetToken()
type defaultCredentialErrorReporter struct {
credType string
err error
}
func (d *defaultCredentialErrorReporter) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if _, ok := d.err.(*credentialUnavailableError); ok {
return azcore.AccessToken{}, d.err
}
return azcore.AccessToken{}, newCredentialUnavailableError(d.credType, d.err.Error())
}
var _ azcore.TokenCredential = (*defaultCredentialErrorReporter)(nil)
// timeoutWrapper prevents a potentially very long timeout when managed identity isn't available
type timeoutWrapper struct {
mic *ManagedIdentityCredential
// timeout applies to all auth attempts until one doesn't time out
timeout time.Duration
}
// GetToken wraps DefaultAzureCredential's initial managed identity auth attempt with a short timeout
// because managed identity may not be available and connecting to IMDS can take several minutes to time out.
func (w *timeoutWrapper) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var tk azcore.AccessToken
var err error
// no need to synchronize around this value because it's written only within ChainedTokenCredential's critical section
if w.timeout > 0 {
c, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
tk, err = w.mic.GetToken(c, opts)
if isAuthFailedDueToContext(err) {
err = newCredentialUnavailableError(credNameManagedIdentity, "managed identity timed out. See https://aka.ms/azsdk/go/identity/troubleshoot#dac for more information")
} else {
// some managed identity implementation is available, so don't apply the timeout to future calls
w.timeout = 0
}
} else {
tk, err = w.mic.GetToken(ctx, opts)
}
return tk, err
}
// unwraps nested AuthenticationFailedErrors to get the root error
func isAuthFailedDueToContext(err error) bool {
for {
var authFailedErr *AuthenticationFailedError
if !errors.As(err, &authFailedErr) {
break
}
err = authFailedErr.err
}
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
}

View File

@ -0,0 +1,38 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"errors"
"time"
)
// cliTimeout is the default timeout for authentication attempts via CLI tools
const cliTimeout = 10 * time.Second
// unavailableIfInChain returns err or, if the credential was invoked by DefaultAzureCredential, a
// credentialUnavailableError having the same message. This ensures DefaultAzureCredential will try
// the next credential in its chain (another developer credential).
func unavailableIfInChain(err error, inDefaultChain bool) error {
if err != nil && inDefaultChain {
var unavailableErr *credentialUnavailableError
if !errors.As(err, &unavailableErr) {
err = newCredentialUnavailableError(credNameAzureDeveloperCLI, err.Error())
}
}
return err
}
// validScope is for credentials authenticating via external tools. The authority validates scopes for all other credentials.
func validScope(scope string) bool {
for _, r := range scope {
if !(alphanumeric(r) || r == '.' || r == '-' || r == '_' || r == '/' || r == ':') {
return false
}
}
return true
}

View File

@ -0,0 +1,138 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
)
const credNameDeviceCode = "DeviceCodeCredential"
// DeviceCodeCredentialOptions contains optional parameters for DeviceCodeCredential.
type DeviceCodeCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire
// tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant.
AdditionallyAllowedTenants []string
// authenticationRecord returned by a call to a credential's Authenticate method. Set this option
// to enable the credential to use data from a previous authentication.
authenticationRecord authenticationRecord
// ClientID is the ID of the application users will authenticate to.
// Defaults to the ID of an Azure development application.
ClientID string
// disableAutomaticAuthentication prevents the credential from automatically prompting the user to authenticate.
// When this option is true, [DeviceCodeCredential.GetToken] will return [ErrAuthenticationRequired] when user
// interaction is necessary to acquire a token.
disableAutomaticAuthentication bool
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID is the Microsoft Entra tenant the credential authenticates in. Defaults to the
// "organizations" tenant, which can authenticate work and school accounts. Required for single-tenant
// applications.
TenantID string
// tokenCachePersistenceOptions enables persistent token caching when not nil.
tokenCachePersistenceOptions *tokenCachePersistenceOptions
// UserPrompt controls how the credential presents authentication instructions. The credential calls
// this function with authentication details when it receives a device code. By default, the credential
// prints these details to stdout.
UserPrompt func(context.Context, DeviceCodeMessage) error
}
func (o *DeviceCodeCredentialOptions) init() {
if o.TenantID == "" {
o.TenantID = organizationsTenantID
}
if o.ClientID == "" {
o.ClientID = developerSignOnClientID
}
if o.UserPrompt == nil {
o.UserPrompt = func(ctx context.Context, dc DeviceCodeMessage) error {
fmt.Println(dc.Message)
return nil
}
}
}
// DeviceCodeMessage contains the information a user needs to complete authentication.
type DeviceCodeMessage struct {
// UserCode is the user code returned by the service.
UserCode string `json:"user_code"`
// VerificationURL is the URL at which the user must authenticate.
VerificationURL string `json:"verification_uri"`
// Message is user instruction from Microsoft Entra ID.
Message string `json:"message"`
}
// DeviceCodeCredential acquires tokens for a user via the device code flow, which has the
// user browse to a Microsoft Entra URL, enter a code, and authenticate. It's useful
// for authenticating a user in an environment without a web browser, such as an SSH session.
// If a web browser is available, [InteractiveBrowserCredential] is more convenient because it
// automatically opens a browser to the login page.
type DeviceCodeCredential struct {
client *publicClient
}
// NewDeviceCodeCredential creates a DeviceCodeCredential. Pass nil to accept default options.
func NewDeviceCodeCredential(options *DeviceCodeCredentialOptions) (*DeviceCodeCredential, error) {
cp := DeviceCodeCredentialOptions{}
if options != nil {
cp = *options
}
cp.init()
msalOpts := publicClientOptions{
AdditionallyAllowedTenants: cp.AdditionallyAllowedTenants,
ClientOptions: cp.ClientOptions,
DeviceCodePrompt: cp.UserPrompt,
DisableAutomaticAuthentication: cp.disableAutomaticAuthentication,
DisableInstanceDiscovery: cp.DisableInstanceDiscovery,
Record: cp.authenticationRecord,
TokenCachePersistenceOptions: cp.tokenCachePersistenceOptions,
}
c, err := newPublicClient(cp.TenantID, cp.ClientID, credNameDeviceCode, msalOpts)
if err != nil {
return nil, err
}
c.name = credNameDeviceCode
return &DeviceCodeCredential{client: c}, nil
}
// Authenticate a user via the device code flow. Subsequent calls to GetToken will automatically use the returned AuthenticationRecord.
func (c *DeviceCodeCredential) authenticate(ctx context.Context, opts *policy.TokenRequestOptions) (authenticationRecord, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameDeviceCode+"."+traceOpAuthenticate, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.Authenticate(ctx, opts)
return tk, err
}
// GetToken requests an access token from Microsoft Entra ID. It will begin the device code flow and poll until the user completes authentication.
// This method is called automatically by Azure SDK clients.
func (c *DeviceCodeCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameDeviceCode+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.GetToken(ctx, opts)
return tk, err
}
var _ azcore.TokenCredential = (*DeviceCodeCredential)(nil)

View File

@ -0,0 +1,164 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
const envVarSendCertChain = "AZURE_CLIENT_SEND_CERTIFICATE_CHAIN"
// EnvironmentCredentialOptions contains optional parameters for EnvironmentCredential
type EnvironmentCredentialOptions struct {
azcore.ClientOptions
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// additionallyAllowedTenants is used only by NewDefaultAzureCredential() to enable that constructor's explicit
// option to override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS. Applications using EnvironmentCredential
// directly should set that variable instead. This field should remain unexported to preserve this credential's
// unambiguous "all configuration from environment variables" design.
additionallyAllowedTenants []string
}
// EnvironmentCredential authenticates a service principal with a secret or certificate, or a user with a password, depending
// on environment variable configuration. It reads configuration from these variables, in the following order:
//
// # Service principal with client secret
//
// AZURE_TENANT_ID: ID of the service principal's tenant. Also called its "directory" ID.
//
// AZURE_CLIENT_ID: the service principal's client ID
//
// AZURE_CLIENT_SECRET: one of the service principal's client secrets
//
// # Service principal with certificate
//
// AZURE_TENANT_ID: ID of the service principal's tenant. Also called its "directory" ID.
//
// AZURE_CLIENT_ID: the service principal's client ID
//
// AZURE_CLIENT_CERTIFICATE_PATH: path to a PEM or PKCS12 certificate file including the private key.
//
// AZURE_CLIENT_CERTIFICATE_PASSWORD: (optional) password for the certificate file.
//
// # User with username and password
//
// AZURE_TENANT_ID: (optional) tenant to authenticate in. Defaults to "organizations".
//
// AZURE_CLIENT_ID: client ID of the application the user will authenticate to
//
// AZURE_USERNAME: a username (usually an email address)
//
// AZURE_PASSWORD: the user's password
//
// # Configuration for multitenant applications
//
// To enable multitenant authentication, set AZURE_ADDITIONALLY_ALLOWED_TENANTS with a semicolon delimited list of tenants
// the credential may request tokens from in addition to the tenant specified by AZURE_TENANT_ID. Set
// AZURE_ADDITIONALLY_ALLOWED_TENANTS to "*" to enable the credential to request a token from any tenant.
type EnvironmentCredential struct {
cred azcore.TokenCredential
}
// NewEnvironmentCredential creates an EnvironmentCredential. Pass nil to accept default options.
func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*EnvironmentCredential, error) {
if options == nil {
options = &EnvironmentCredentialOptions{}
}
tenantID := os.Getenv(azureTenantID)
if tenantID == "" {
return nil, errors.New("missing environment variable AZURE_TENANT_ID")
}
clientID := os.Getenv(azureClientID)
if clientID == "" {
return nil, errors.New("missing environment variable " + azureClientID)
}
// tenants set by NewDefaultAzureCredential() override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS
additionalTenants := options.additionallyAllowedTenants
if len(additionalTenants) == 0 {
if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
additionalTenants = strings.Split(tenants, ";")
}
}
if clientSecret := os.Getenv(azureClientSecret); clientSecret != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientSecretCredential")
o := &ClientSecretCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewClientSecretCredential(tenantID, clientID, clientSecret, o)
if err != nil {
return nil, err
}
return &EnvironmentCredential{cred: cred}, nil
}
if certPath := os.Getenv(azureClientCertificatePath); certPath != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientCertificateCredential")
certData, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf(`failed to read certificate file "%s": %v`, certPath, err)
}
var password []byte
if v := os.Getenv(azureClientCertificatePassword); v != "" {
password = []byte(v)
}
certs, key, err := ParseCertificates(certData, password)
if err != nil {
return nil, fmt.Errorf(`failed to load certificate from "%s": %v`, certPath, err)
}
o := &ClientCertificateCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
if v, ok := os.LookupEnv(envVarSendCertChain); ok {
o.SendCertificateChain = v == "1" || strings.ToLower(v) == "true"
}
cred, err := NewClientCertificateCredential(tenantID, clientID, certs, key, o)
if err != nil {
return nil, err
}
return &EnvironmentCredential{cred: cred}, nil
}
if username := os.Getenv(azureUsername); username != "" {
if password := os.Getenv(azurePassword); password != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with UsernamePasswordCredential")
o := &UsernamePasswordCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewUsernamePasswordCredential(tenantID, clientID, username, password, o)
if err != nil {
return nil, err
}
return &EnvironmentCredential{cred: cred}, nil
}
return nil, errors.New("no value for AZURE_PASSWORD")
}
return nil, errors.New("incomplete environment variable configuration. Only AZURE_TENANT_ID and AZURE_CLIENT_ID are set")
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (c *EnvironmentCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.cred.GetToken(ctx, opts)
}
var _ azcore.TokenCredential = (*EnvironmentCredential)(nil)

View File

@ -0,0 +1,140 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
msal "github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
)
// errAuthenticationRequired indicates a credential's Authenticate method must be called to acquire a token
// because user interaction is required and the credential is configured not to automatically prompt the user.
var errAuthenticationRequired error = &credentialUnavailableError{"can't acquire a token without user interaction. Call Authenticate to interactively authenticate a user"}
// getResponseFromError retrieves the response carried by
// an AuthenticationFailedError or MSAL CallErr, if any
func getResponseFromError(err error) *http.Response {
var a *AuthenticationFailedError
var c msal.CallErr
var res *http.Response
if errors.As(err, &c) {
res = c.Resp
} else if errors.As(err, &a) {
res = a.RawResponse
}
return res
}
// AuthenticationFailedError indicates an authentication request has failed.
type AuthenticationFailedError struct {
// RawResponse is the HTTP response motivating the error, if available.
RawResponse *http.Response
credType string
message string
err error
}
func newAuthenticationFailedError(credType string, message string, resp *http.Response, err error) error {
return &AuthenticationFailedError{credType: credType, message: message, RawResponse: resp, err: err}
}
// Error implements the error interface. Note that the message contents are not contractual and can change over time.
func (e *AuthenticationFailedError) Error() string {
if e.RawResponse == nil {
return e.credType + ": " + e.message
}
msg := &bytes.Buffer{}
fmt.Fprintf(msg, e.credType+" authentication failed\n")
if e.RawResponse.Request != nil {
fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.Path)
} else {
// this happens when the response is created from a custom HTTP transporter,
// which doesn't guarantee to bind the original request to the response
fmt.Fprintln(msg, "Request information not available")
}
fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
fmt.Fprintf(msg, "RESPONSE %s\n", e.RawResponse.Status)
fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
body, err := runtime.Payload(e.RawResponse)
switch {
case err != nil:
fmt.Fprintf(msg, "Error reading response body: %v", err)
case len(body) > 0:
if err := json.Indent(msg, body, "", " "); err != nil {
// failed to pretty-print so just dump it verbatim
fmt.Fprint(msg, string(body))
}
default:
fmt.Fprint(msg, "Response contained no body")
}
fmt.Fprintln(msg, "\n--------------------------------------------------------------------------------")
var anchor string
switch e.credType {
case credNameAzureCLI:
anchor = "azure-cli"
case credNameAzureDeveloperCLI:
anchor = "azd"
case credNameCert:
anchor = "client-cert"
case credNameSecret:
anchor = "client-secret"
case credNameManagedIdentity:
anchor = "managed-id"
case credNameUserPassword:
anchor = "username-password"
case credNameWorkloadIdentity:
anchor = "workload"
}
if anchor != "" {
fmt.Fprintf(msg, "To troubleshoot, visit https://aka.ms/azsdk/go/identity/troubleshoot#%s", anchor)
}
return msg.String()
}
// NonRetriable indicates the request which provoked this error shouldn't be retried.
func (*AuthenticationFailedError) NonRetriable() {
// marker method
}
var _ errorinfo.NonRetriable = (*AuthenticationFailedError)(nil)
// credentialUnavailableError indicates a credential can't attempt authentication because it lacks required
// data or state
type credentialUnavailableError struct {
message string
}
// newCredentialUnavailableError is an internal helper that ensures consistent error message formatting
func newCredentialUnavailableError(credType, message string) error {
msg := fmt.Sprintf("%s: %s", credType, message)
return &credentialUnavailableError{msg}
}
// NewCredentialUnavailableError constructs an error indicating a credential can't attempt authentication
// because it lacks required data or state. When [ChainedTokenCredential] receives this error it will try
// its next credential, if any.
func NewCredentialUnavailableError(message string) error {
return &credentialUnavailableError{message}
}
// Error implements the error interface. Note that the message contents are not contractual and can change over time.
func (e *credentialUnavailableError) Error() string {
return e.message
}
// NonRetriable is a marker method indicating this error should not be retried. It has no implementation.
func (e *credentialUnavailableError) NonRetriable() {}
var _ errorinfo.NonRetriable = (*credentialUnavailableError)(nil)

View File

@ -0,0 +1,6 @@
go 1.18
use (
.
./cache
)

View File

@ -0,0 +1,39 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1 h1:ODs3brnqQM99Tq1PffODpAViYv3Bf8zOg464MU7p5ew=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,118 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
)
const credNameBrowser = "InteractiveBrowserCredential"
// InteractiveBrowserCredentialOptions contains optional parameters for InteractiveBrowserCredential.
type InteractiveBrowserCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire
// tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant.
AdditionallyAllowedTenants []string
// authenticationRecord returned by a call to a credential's Authenticate method. Set this option
// to enable the credential to use data from a previous authentication.
authenticationRecord authenticationRecord
// ClientID is the ID of the application users will authenticate to.
// Defaults to the ID of an Azure development application.
ClientID string
// disableAutomaticAuthentication prevents the credential from automatically prompting the user to authenticate.
// When this option is true, [InteractiveBrowserCredential.GetToken] will return [ErrAuthenticationRequired] when
// user interaction is necessary to acquire a token.
disableAutomaticAuthentication bool
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// LoginHint pre-populates the account prompt with a username. Users may choose to authenticate a different account.
LoginHint string
// RedirectURL is the URL Microsoft Entra ID will redirect to with the access token. This is required
// only when setting ClientID, and must match a redirect URI in the application's registration.
// Applications which have registered "http://localhost" as a redirect URI need not set this option.
RedirectURL string
// TenantID is the Microsoft Entra tenant the credential authenticates in. Defaults to the
// "organizations" tenant, which can authenticate work and school accounts.
TenantID string
// tokenCachePersistenceOptions enables persistent token caching when not nil.
tokenCachePersistenceOptions *tokenCachePersistenceOptions
}
func (o *InteractiveBrowserCredentialOptions) init() {
if o.TenantID == "" {
o.TenantID = organizationsTenantID
}
if o.ClientID == "" {
o.ClientID = developerSignOnClientID
}
}
// InteractiveBrowserCredential opens a browser to interactively authenticate a user.
type InteractiveBrowserCredential struct {
client *publicClient
}
// NewInteractiveBrowserCredential constructs a new InteractiveBrowserCredential. Pass nil to accept default options.
func NewInteractiveBrowserCredential(options *InteractiveBrowserCredentialOptions) (*InteractiveBrowserCredential, error) {
cp := InteractiveBrowserCredentialOptions{}
if options != nil {
cp = *options
}
cp.init()
msalOpts := publicClientOptions{
AdditionallyAllowedTenants: cp.AdditionallyAllowedTenants,
ClientOptions: cp.ClientOptions,
DisableAutomaticAuthentication: cp.disableAutomaticAuthentication,
DisableInstanceDiscovery: cp.DisableInstanceDiscovery,
LoginHint: cp.LoginHint,
Record: cp.authenticationRecord,
RedirectURL: cp.RedirectURL,
TokenCachePersistenceOptions: cp.tokenCachePersistenceOptions,
}
c, err := newPublicClient(cp.TenantID, cp.ClientID, credNameBrowser, msalOpts)
if err != nil {
return nil, err
}
return &InteractiveBrowserCredential{client: c}, nil
}
// Authenticate a user via the default browser. Subsequent calls to GetToken will automatically use the returned AuthenticationRecord.
func (c *InteractiveBrowserCredential) authenticate(ctx context.Context, opts *policy.TokenRequestOptions) (authenticationRecord, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameBrowser+"."+traceOpAuthenticate, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.Authenticate(ctx, opts)
return tk, err
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (c *InteractiveBrowserCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameBrowser+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.GetToken(ctx, opts)
return tk, err
}
var _ azcore.TokenCredential = (*InteractiveBrowserCredential)(nil)

View File

@ -0,0 +1,18 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package internal
// TokenCachePersistenceOptions contains options for persistent token caching
type TokenCachePersistenceOptions struct {
// AllowUnencryptedStorage controls whether the cache should fall back to storing its data in plain text
// when encryption isn't possible. Setting this true doesn't disable encryption. The cache always attempts
// encryption before falling back to plaintext storage.
AllowUnencryptedStorage bool
// Name identifies the cache. Set this to isolate data from other applications.
Name string
}

View File

@ -0,0 +1,31 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package internal
import (
"errors"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
)
var errMissingImport = errors.New("import github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache to enable persistent caching")
// NewCache constructs a persistent token cache when "o" isn't nil. Applications that intend to
// use a persistent cache must first import the cache module, which will replace this function
// with a platform-specific implementation.
var NewCache = func(o *TokenCachePersistenceOptions, enableCAE bool) (cache.ExportReplace, error) {
if o == nil {
return nil, nil
}
return nil, errMissingImport
}
// CacheFilePath returns the path to the cache file for the given name.
// Defining it in this package makes it available to azidentity tests.
var CacheFilePath = func(name string) (string, error) {
return "", errMissingImport
}

View File

@ -0,0 +1,14 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import "github.com/Azure/azure-sdk-for-go/sdk/internal/log"
// EventAuthentication entries contain information about authentication.
// This includes information like the names of environment variables
// used when obtaining credentials and the type of credential used.
const EventAuthentication log.Event = "Authentication"

View File

@ -0,0 +1,446 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const (
arcIMDSEndpoint = "IMDS_ENDPOINT"
defaultIdentityClientID = "DEFAULT_IDENTITY_CLIENT_ID"
identityEndpoint = "IDENTITY_ENDPOINT"
identityHeader = "IDENTITY_HEADER"
identityServerThumbprint = "IDENTITY_SERVER_THUMBPRINT"
headerMetadata = "Metadata"
imdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
msiEndpoint = "MSI_ENDPOINT"
msiSecret = "MSI_SECRET"
imdsAPIVersion = "2018-02-01"
azureArcAPIVersion = "2019-08-15"
serviceFabricAPIVersion = "2019-07-01-preview"
qpClientID = "client_id"
qpResID = "mi_res_id"
)
type msiType int
const (
msiTypeAppService msiType = iota
msiTypeAzureArc
msiTypeAzureML
msiTypeCloudShell
msiTypeIMDS
msiTypeServiceFabric
)
// managedIdentityClient provides the base for authenticating in managed identity environments
// This type includes an runtime.Pipeline and TokenCredentialOptions.
type managedIdentityClient struct {
azClient *azcore.Client
msiType msiType
endpoint string
id ManagedIDKind
}
type wrappedNumber json.Number
func (n *wrappedNumber) UnmarshalJSON(b []byte) error {
c := string(b)
if c == "\"\"" {
return nil
}
return json.Unmarshal(b, (*json.Number)(n))
}
// setIMDSRetryOptionDefaults sets zero-valued fields to default values appropriate for IMDS
func setIMDSRetryOptionDefaults(o *policy.RetryOptions) {
if o.MaxRetries == 0 {
o.MaxRetries = 5
}
if o.MaxRetryDelay == 0 {
o.MaxRetryDelay = 1 * time.Minute
}
if o.RetryDelay == 0 {
o.RetryDelay = 2 * time.Second
}
if o.StatusCodes == nil {
o.StatusCodes = []int{
// IMDS docs recommend retrying 404, 410, 429 and 5xx
// https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#error-handling
http.StatusNotFound, // 404
http.StatusGone, // 410
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusNotImplemented, // 501
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
http.StatusHTTPVersionNotSupported, // 505
http.StatusVariantAlsoNegotiates, // 506
http.StatusInsufficientStorage, // 507
http.StatusLoopDetected, // 508
http.StatusNotExtended, // 510
http.StatusNetworkAuthenticationRequired, // 511
}
}
if o.TryTimeout == 0 {
o.TryTimeout = 1 * time.Minute
}
}
// newManagedIdentityClient creates a new instance of the ManagedIdentityClient with the ManagedIdentityCredentialOptions
// that are passed into it along with a default pipeline.
// options: ManagedIdentityCredentialOptions configure policies for the pipeline and the authority host that
// will be used to retrieve tokens and authenticate
func newManagedIdentityClient(options *ManagedIdentityCredentialOptions) (*managedIdentityClient, error) {
if options == nil {
options = &ManagedIdentityCredentialOptions{}
}
cp := options.ClientOptions
c := managedIdentityClient{id: options.ID, endpoint: imdsEndpoint, msiType: msiTypeIMDS}
env := "IMDS"
if endpoint, ok := os.LookupEnv(identityEndpoint); ok {
if _, ok := os.LookupEnv(identityHeader); ok {
if _, ok := os.LookupEnv(identityServerThumbprint); ok {
env = "Service Fabric"
c.endpoint = endpoint
c.msiType = msiTypeServiceFabric
} else {
env = "App Service"
c.endpoint = endpoint
c.msiType = msiTypeAppService
}
} else if _, ok := os.LookupEnv(arcIMDSEndpoint); ok {
env = "Azure Arc"
c.endpoint = endpoint
c.msiType = msiTypeAzureArc
}
} else if endpoint, ok := os.LookupEnv(msiEndpoint); ok {
c.endpoint = endpoint
if _, ok := os.LookupEnv(msiSecret); ok {
env = "Azure ML"
c.msiType = msiTypeAzureML
} else {
env = "Cloud Shell"
c.msiType = msiTypeCloudShell
}
} else {
setIMDSRetryOptionDefaults(&cp.Retry)
}
client, err := azcore.NewClient(module, version, runtime.PipelineOptions{
Tracing: runtime.TracingOptions{
Namespace: traceNamespace,
},
}, &cp)
if err != nil {
return nil, err
}
c.azClient = client
if log.Should(EventAuthentication) {
log.Writef(EventAuthentication, "Managed Identity Credential will use %s managed identity", env)
}
return &c, nil
}
// provideToken acquires a token for MSAL's confidential.Client, which caches the token
func (c *managedIdentityClient) provideToken(ctx context.Context, params confidential.TokenProviderParameters) (confidential.TokenProviderResult, error) {
result := confidential.TokenProviderResult{}
tk, err := c.authenticate(ctx, c.id, params.Scopes)
if err == nil {
result.AccessToken = tk.Token
result.ExpiresInSeconds = int(time.Until(tk.ExpiresOn).Seconds())
}
return result, err
}
// authenticate acquires an access token
func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKind, scopes []string) (azcore.AccessToken, error) {
msg, err := c.createAuthRequest(ctx, id, scopes)
if err != nil {
return azcore.AccessToken{}, err
}
resp, err := c.azClient.Pipeline().Do(msg)
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, err.Error(), nil, err)
}
if runtime.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
return c.createAccessToken(resp)
}
if c.msiType == msiTypeIMDS {
switch resp.StatusCode {
case http.StatusBadRequest:
if id != nil {
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "the requested identity isn't assigned to this resource", resp, nil)
}
msg := "failed to authenticate a system assigned identity"
if body, err := runtime.Payload(resp); err == nil && len(body) > 0 {
msg += fmt.Sprintf(". The endpoint responded with %s", body)
}
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, msg)
case http.StatusForbidden:
// Docker Desktop runs a proxy that responds 403 to IMDS token requests. If we get that response,
// we return credentialUnavailableError so credential chains continue to their next credential
body, err := runtime.Payload(resp)
if err == nil && strings.Contains(string(body), "A socket operation was attempted to an unreachable network") {
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, fmt.Sprintf("unexpected response %q", string(body)))
}
}
}
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "authentication failed", resp, nil)
}
func (c *managedIdentityClient) createAccessToken(res *http.Response) (azcore.AccessToken, error) {
value := struct {
// these are the only fields that we use
Token string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn wrappedNumber `json:"expires_in,omitempty"` // this field should always return the number of seconds for which a token is valid
ExpiresOn interface{} `json:"expires_on,omitempty"` // the value returned in this field varies between a number and a date string
}{}
if err := runtime.UnmarshalAsJSON(res, &value); err != nil {
return azcore.AccessToken{}, fmt.Errorf("internal AccessToken: %v", err)
}
if value.ExpiresIn != "" {
expiresIn, err := json.Number(value.ExpiresIn).Int64()
if err != nil {
return azcore.AccessToken{}, err
}
return azcore.AccessToken{Token: value.Token, ExpiresOn: time.Now().Add(time.Second * time.Duration(expiresIn)).UTC()}, nil
}
switch v := value.ExpiresOn.(type) {
case float64:
return azcore.AccessToken{Token: value.Token, ExpiresOn: time.Unix(int64(v), 0).UTC()}, nil
case string:
if expiresOn, err := strconv.Atoi(v); err == nil {
return azcore.AccessToken{Token: value.Token, ExpiresOn: time.Unix(int64(expiresOn), 0).UTC()}, nil
}
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "unexpected expires_on value: "+v, res, nil)
default:
msg := fmt.Sprintf("unsupported type received in expires_on: %T, %v", v, v)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, msg, res, nil)
}
}
func (c *managedIdentityClient) createAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
switch c.msiType {
case msiTypeIMDS:
return c.createIMDSAuthRequest(ctx, id, scopes)
case msiTypeAppService:
return c.createAppServiceAuthRequest(ctx, id, scopes)
case msiTypeAzureArc:
// need to perform preliminary request to retreive the secret key challenge provided by the HIMDS service
key, err := c.getAzureArcSecretKey(ctx, scopes)
if err != nil {
msg := fmt.Sprintf("failed to retreive secret key from the identity endpoint: %v", err)
return nil, newAuthenticationFailedError(credNameManagedIdentity, msg, nil, err)
}
return c.createAzureArcAuthRequest(ctx, id, scopes, key)
case msiTypeAzureML:
return c.createAzureMLAuthRequest(ctx, id, scopes)
case msiTypeServiceFabric:
return c.createServiceFabricAuthRequest(ctx, id, scopes)
case msiTypeCloudShell:
return c.createCloudShellAuthRequest(ctx, id, scopes)
default:
return nil, newCredentialUnavailableError(credNameManagedIdentity, "managed identity isn't supported in this environment")
}
}
func (c *managedIdentityClient) createIMDSAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set(headerMetadata, "true")
q := request.Raw().URL.Query()
q.Add("api-version", imdsAPIVersion)
q.Add("resource", strings.Join(scopes, " "))
if id != nil {
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) createAppServiceAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set("X-IDENTITY-HEADER", os.Getenv(identityHeader))
q := request.Raw().URL.Query()
q.Add("api-version", "2019-08-01")
q.Add("resource", scopes[0])
if id != nil {
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) createAzureMLAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set("secret", os.Getenv(msiSecret))
q := request.Raw().URL.Query()
q.Add("api-version", "2017-09-01")
q.Add("resource", strings.Join(scopes, " "))
q.Add("clientid", os.Getenv(defaultIdentityClientID))
if id != nil {
if id.idKind() == miResourceID {
log.Write(EventAuthentication, "WARNING: Azure ML doesn't support specifying a managed identity by resource ID")
q.Set("clientid", "")
q.Set(qpResID, id.String())
} else {
q.Set("clientid", id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) createServiceFabricAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
q := request.Raw().URL.Query()
request.Raw().Header.Set("Accept", "application/json")
request.Raw().Header.Set("Secret", os.Getenv(identityHeader))
q.Add("api-version", serviceFabricAPIVersion)
q.Add("resource", strings.Join(scopes, " "))
if id != nil {
log.Write(EventAuthentication, "WARNING: Service Fabric doesn't support selecting a user-assigned identity at runtime")
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) getAzureArcSecretKey(ctx context.Context, resources []string) (string, error) {
// create the request to retreive the secret key challenge provided by the HIMDS service
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return "", err
}
request.Raw().Header.Set(headerMetadata, "true")
q := request.Raw().URL.Query()
q.Add("api-version", azureArcAPIVersion)
q.Add("resource", strings.Join(resources, " "))
request.Raw().URL.RawQuery = q.Encode()
// send the initial request to get the short-lived secret key
response, err := c.azClient.Pipeline().Do(request)
if err != nil {
return "", err
}
// the endpoint is expected to return a 401 with the WWW-Authenticate header set to the location
// of the secret key file. Any other status code indicates an error in the request.
if response.StatusCode != 401 {
msg := fmt.Sprintf("expected a 401 response, received %d", response.StatusCode)
return "", newAuthenticationFailedError(credNameManagedIdentity, msg, response, nil)
}
header := response.Header.Get("WWW-Authenticate")
if len(header) == 0 {
return "", errors.New("did not receive a value from WWW-Authenticate header")
}
// the WWW-Authenticate header is expected in the following format: Basic realm=/some/file/path.key
pos := strings.LastIndex(header, "=")
if pos == -1 {
return "", fmt.Errorf("did not receive a correct value from WWW-Authenticate header: %s", header)
}
key, err := os.ReadFile(header[pos+1:])
if err != nil {
return "", fmt.Errorf("could not read file (%s) contents: %v", header[pos+1:], err)
}
return string(key), nil
}
func (c *managedIdentityClient) createAzureArcAuthRequest(ctx context.Context, id ManagedIDKind, resources []string, key string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set(headerMetadata, "true")
request.Raw().Header.Set("Authorization", fmt.Sprintf("Basic %s", key))
q := request.Raw().URL.Query()
q.Add("api-version", azureArcAPIVersion)
q.Add("resource", strings.Join(resources, " "))
if id != nil {
log.Write(EventAuthentication, "WARNING: Azure Arc doesn't support user-assigned managed identities")
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) createCloudShellAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodPost, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set(headerMetadata, "true")
data := url.Values{}
data.Set("resource", strings.Join(scopes, " "))
dataEncoded := data.Encode()
body := streaming.NopCloser(strings.NewReader(dataEncoded))
if err := request.SetBody(body, "application/x-www-form-urlencoded"); err != nil {
return nil, err
}
if id != nil {
log.Write(EventAuthentication, "WARNING: Cloud Shell doesn't support user-assigned managed identities")
q := request.Raw().URL.Query()
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
return request, nil
}

View File

@ -0,0 +1,121 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameManagedIdentity = "ManagedIdentityCredential"
type managedIdentityIDKind int
const (
miClientID managedIdentityIDKind = 0
miResourceID managedIdentityIDKind = 1
)
// ManagedIDKind identifies the ID of a managed identity as either a client or resource ID
type ManagedIDKind interface {
fmt.Stringer
idKind() managedIdentityIDKind
}
// ClientID is the client ID of a user-assigned managed identity.
type ClientID string
func (ClientID) idKind() managedIdentityIDKind {
return miClientID
}
// String returns the string value of the ID.
func (c ClientID) String() string {
return string(c)
}
// ResourceID is the resource ID of a user-assigned managed identity.
type ResourceID string
func (ResourceID) idKind() managedIdentityIDKind {
return miResourceID
}
// String returns the string value of the ID.
func (r ResourceID) String() string {
return string(r)
}
// ManagedIdentityCredentialOptions contains optional parameters for ManagedIdentityCredential.
type ManagedIdentityCredentialOptions struct {
azcore.ClientOptions
// ID is the ID of a managed identity the credential should authenticate. Set this field to use a specific identity
// instead of the hosting environment's default. The value may be the identity's client ID or resource ID, but note that
// some platforms don't accept resource IDs.
ID ManagedIDKind
}
// ManagedIdentityCredential authenticates an Azure managed identity in any hosting environment supporting managed identities.
// This credential authenticates a system-assigned identity by default. Use ManagedIdentityCredentialOptions.ID to specify a
// user-assigned identity. See Microsoft Entra ID documentation for more information about managed identities:
// https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview
type ManagedIdentityCredential struct {
client *confidentialClient
mic *managedIdentityClient
}
// NewManagedIdentityCredential creates a ManagedIdentityCredential. Pass nil to accept default options.
func NewManagedIdentityCredential(options *ManagedIdentityCredentialOptions) (*ManagedIdentityCredential, error) {
if options == nil {
options = &ManagedIdentityCredentialOptions{}
}
mic, err := newManagedIdentityClient(options)
if err != nil {
return nil, err
}
cred := confidential.NewCredFromTokenProvider(mic.provideToken)
// It's okay to give MSAL an invalid client ID because MSAL will use it only as part of a cache key.
// ManagedIdentityClient handles all the details of authentication and won't receive this value from MSAL.
clientID := "SYSTEM-ASSIGNED-MANAGED-IDENTITY"
if options.ID != nil {
clientID = options.ID.String()
}
// similarly, it's okay to give MSAL an incorrect tenant because MSAL won't use the value
c, err := newConfidentialClient("common", clientID, credNameManagedIdentity, cred, confidentialClientOptions{
ClientOptions: options.ClientOptions,
})
if err != nil {
return nil, err
}
return &ManagedIdentityCredential{client: c, mic: mic}, nil
}
// GetToken requests an access token from the hosting environment. This method is called automatically by Azure SDK clients.
func (c *ManagedIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameManagedIdentity+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
if len(opts.Scopes) != 1 {
err = fmt.Errorf("%s.GetToken() requires exactly one scope", credNameManagedIdentity)
return azcore.AccessToken{}, err
}
// managed identity endpoints require a Microsoft Entra ID v1 resource (i.e. token audience), not a v2 scope, so we remove "/.default" here
opts.Scopes = []string{strings.TrimSuffix(opts.Scopes[0], defaultSuffix)}
tk, err := c.client.GetToken(ctx, opts)
return tk, err
}
var _ azcore.TokenCredential = (*ManagedIdentityCredential)(nil)

View File

@ -0,0 +1,99 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"crypto"
"crypto/x509"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameOBO = "OnBehalfOfCredential"
// OnBehalfOfCredential authenticates a service principal via the on-behalf-of flow. This is typically used by
// middle-tier services that authorize requests to other services with a delegated user identity. Because this
// is not an interactive authentication flow, an application using it must have admin consent for any delegated
// permissions before requesting tokens for them. See [Microsoft Entra ID documentation] for more details.
//
// [Microsoft Entra ID documentation]: https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow
type OnBehalfOfCredential struct {
client *confidentialClient
}
// OnBehalfOfCredentialOptions contains optional parameters for OnBehalfOfCredential
type OnBehalfOfCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// SendCertificateChain applies only when the credential is configured to authenticate with a certificate.
// This setting controls whether the credential sends the public certificate chain in the x5c header of each
// token request's JWT. This is required for, and only used in, Subject Name/Issuer (SNI) authentication.
SendCertificateChain bool
}
// NewOnBehalfOfCredentialWithCertificate constructs an OnBehalfOfCredential that authenticates with a certificate.
// See [ParseCertificates] for help loading a certificate.
func NewOnBehalfOfCredentialWithCertificate(tenantID, clientID, userAssertion string, certs []*x509.Certificate, key crypto.PrivateKey, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
cred, err := confidential.NewCredFromCert(certs, key)
if err != nil {
return nil, err
}
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
// NewOnBehalfOfCredentialWithSecret constructs an OnBehalfOfCredential that authenticates with a client secret.
func NewOnBehalfOfCredentialWithSecret(tenantID, clientID, userAssertion, clientSecret string, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
cred, err := confidential.NewCredFromSecret(clientSecret)
if err != nil {
return nil, err
}
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
func newOnBehalfOfCredential(tenantID, clientID, userAssertion string, cred confidential.Credential, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
if options == nil {
options = &OnBehalfOfCredentialOptions{}
}
opts := confidentialClientOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
Assertion: userAssertion,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
SendX5C: options.SendCertificateChain,
}
c, err := newConfidentialClient(tenantID, clientID, credNameOBO, cred, opts)
if err != nil {
return nil, err
}
return &OnBehalfOfCredential{c}, nil
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (o *OnBehalfOfCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameOBO+"."+traceOpGetToken, o.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := o.client.GetToken(ctx, opts)
return tk, err
}
var _ azcore.TokenCredential = (*OnBehalfOfCredential)(nil)

View File

@ -0,0 +1,273 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
// this import ensures well-known configurations in azcore/cloud have ARM audiences for Authenticate()
_ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/runtime"
)
type publicClientOptions struct {
azcore.ClientOptions
AdditionallyAllowedTenants []string
DeviceCodePrompt func(context.Context, DeviceCodeMessage) error
DisableAutomaticAuthentication bool
DisableInstanceDiscovery bool
LoginHint, RedirectURL string
Record authenticationRecord
TokenCachePersistenceOptions *tokenCachePersistenceOptions
Username, Password string
}
// publicClient wraps the MSAL public client
type publicClient struct {
cae, noCAE msalPublicClient
caeMu, noCAEMu, clientMu *sync.Mutex
clientID, tenantID string
defaultScope []string
host string
name string
opts publicClientOptions
record authenticationRecord
azClient *azcore.Client
}
var errScopeRequired = errors.New("authenticating in this environment requires specifying a scope in TokenRequestOptions")
func newPublicClient(tenantID, clientID, name string, o publicClientOptions) (*publicClient, error) {
if !validTenantID(tenantID) {
return nil, errInvalidTenantID
}
host, err := setAuthorityHost(o.Cloud)
if err != nil {
return nil, err
}
// if the application specified a cloud configuration, use its ARM audience as the default scope for Authenticate()
audience := o.Cloud.Services[cloud.ResourceManager].Audience
if audience == "" {
// no cloud configuration, or no ARM audience, specified; try to map the host to a well-known one (all of which have a trailing slash)
if !strings.HasSuffix(host, "/") {
host += "/"
}
switch host {
case cloud.AzureChina.ActiveDirectoryAuthorityHost:
audience = cloud.AzureChina.Services[cloud.ResourceManager].Audience
case cloud.AzureGovernment.ActiveDirectoryAuthorityHost:
audience = cloud.AzureGovernment.Services[cloud.ResourceManager].Audience
case cloud.AzurePublic.ActiveDirectoryAuthorityHost:
audience = cloud.AzurePublic.Services[cloud.ResourceManager].Audience
}
}
// if we didn't come up with an audience, the application will have to specify a scope for Authenticate()
var defaultScope []string
if audience != "" {
defaultScope = []string{audience + defaultSuffix}
}
client, err := azcore.NewClient(module, version, runtime.PipelineOptions{
Tracing: runtime.TracingOptions{
Namespace: traceNamespace,
},
}, &o.ClientOptions)
if err != nil {
return nil, err
}
o.AdditionallyAllowedTenants = resolveAdditionalTenants(o.AdditionallyAllowedTenants)
return &publicClient{
caeMu: &sync.Mutex{},
clientID: clientID,
clientMu: &sync.Mutex{},
defaultScope: defaultScope,
host: host,
name: name,
noCAEMu: &sync.Mutex{},
opts: o,
record: o.Record,
tenantID: tenantID,
azClient: client,
}, nil
}
func (p *publicClient) Authenticate(ctx context.Context, tro *policy.TokenRequestOptions) (authenticationRecord, error) {
if tro == nil {
tro = &policy.TokenRequestOptions{}
}
if len(tro.Scopes) == 0 {
if p.defaultScope == nil {
return authenticationRecord{}, errScopeRequired
}
tro.Scopes = p.defaultScope
}
client, mu, err := p.client(*tro)
if err != nil {
return authenticationRecord{}, err
}
mu.Lock()
defer mu.Unlock()
_, err = p.reqToken(ctx, client, *tro)
if err == nil {
scope := strings.Join(tro.Scopes, ", ")
msg := fmt.Sprintf("%s.Authenticate() acquired a token for scope %q", p.name, scope)
log.Write(EventAuthentication, msg)
}
return p.record, err
}
// GetToken requests an access token from MSAL, checking the cache first.
func (p *publicClient) GetToken(ctx context.Context, tro policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(tro.Scopes) < 1 {
return azcore.AccessToken{}, fmt.Errorf("%s.GetToken() requires at least one scope", p.name)
}
tenant, err := p.resolveTenant(tro.TenantID)
if err != nil {
return azcore.AccessToken{}, err
}
client, mu, err := p.client(tro)
if err != nil {
return azcore.AccessToken{}, err
}
mu.Lock()
defer mu.Unlock()
ar, err := client.AcquireTokenSilent(ctx, tro.Scopes, public.WithSilentAccount(p.record.account()), public.WithClaims(tro.Claims), public.WithTenantID(tenant))
if err == nil {
return p.token(ar, err)
}
if p.opts.DisableAutomaticAuthentication {
return azcore.AccessToken{}, errAuthenticationRequired
}
at, err := p.reqToken(ctx, client, tro)
if err == nil {
msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", p.name, strings.Join(ar.GrantedScopes, ", "))
log.Write(EventAuthentication, msg)
}
return at, err
}
// reqToken requests a token from the MSAL public client. It's separate from GetToken() to enable Authenticate() to bypass the cache.
func (p *publicClient) reqToken(ctx context.Context, c msalPublicClient, tro policy.TokenRequestOptions) (azcore.AccessToken, error) {
tenant, err := p.resolveTenant(tro.TenantID)
if err != nil {
return azcore.AccessToken{}, err
}
var ar public.AuthResult
switch p.name {
case credNameBrowser:
ar, err = c.AcquireTokenInteractive(ctx, tro.Scopes,
public.WithClaims(tro.Claims),
public.WithLoginHint(p.opts.LoginHint),
public.WithRedirectURI(p.opts.RedirectURL),
public.WithTenantID(tenant),
)
case credNameDeviceCode:
dc, e := c.AcquireTokenByDeviceCode(ctx, tro.Scopes, public.WithClaims(tro.Claims), public.WithTenantID(tenant))
if e != nil {
return azcore.AccessToken{}, e
}
err = p.opts.DeviceCodePrompt(ctx, DeviceCodeMessage{
Message: dc.Result.Message,
UserCode: dc.Result.UserCode,
VerificationURL: dc.Result.VerificationURL,
})
if err == nil {
ar, err = dc.AuthenticationResult(ctx)
}
case credNameUserPassword:
ar, err = c.AcquireTokenByUsernamePassword(ctx, tro.Scopes, p.opts.Username, p.opts.Password, public.WithClaims(tro.Claims), public.WithTenantID(tenant))
default:
return azcore.AccessToken{}, fmt.Errorf("unknown credential %q", p.name)
}
return p.token(ar, err)
}
func (p *publicClient) client(tro policy.TokenRequestOptions) (msalPublicClient, *sync.Mutex, error) {
p.clientMu.Lock()
defer p.clientMu.Unlock()
if tro.EnableCAE {
if p.cae == nil {
client, err := p.newMSALClient(true)
if err != nil {
return nil, nil, err
}
p.cae = client
}
return p.cae, p.caeMu, nil
}
if p.noCAE == nil {
client, err := p.newMSALClient(false)
if err != nil {
return nil, nil, err
}
p.noCAE = client
}
return p.noCAE, p.noCAEMu, nil
}
func (p *publicClient) newMSALClient(enableCAE bool) (msalPublicClient, error) {
cache, err := internal.NewCache(p.opts.TokenCachePersistenceOptions, enableCAE)
if err != nil {
return nil, err
}
o := []public.Option{
public.WithAuthority(runtime.JoinPaths(p.host, p.tenantID)),
public.WithCache(cache),
public.WithHTTPClient(p),
}
if enableCAE {
o = append(o, public.WithClientCapabilities(cp1))
}
if p.opts.DisableInstanceDiscovery || strings.ToLower(p.tenantID) == "adfs" {
o = append(o, public.WithInstanceDiscovery(false))
}
return public.New(p.clientID, o...)
}
func (p *publicClient) token(ar public.AuthResult, err error) (azcore.AccessToken, error) {
if err == nil {
p.record, err = newAuthenticationRecord(ar)
} else {
res := getResponseFromError(err)
err = newAuthenticationFailedError(p.name, err.Error(), res, err)
}
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
// resolveTenant returns the correct WithTenantID() argument for a token request given the client's
// configuration, or an error when that configuration doesn't allow the specified tenant
func (p *publicClient) resolveTenant(specified string) (string, error) {
t, err := resolveTenant(p.tenantID, specified, p.name, p.opts.AdditionallyAllowedTenants)
if t == p.tenantID {
// callers pass this value to MSAL's WithTenantID(). There's no need to redundantly specify
// the client's default tenant and doing so is an error when that tenant is "organizations"
t = ""
}
return t, err
}
// these methods satisfy the MSAL ops.HTTPClient interface
func (p *publicClient) CloseIdleConnections() {
// do nothing
}
func (p *publicClient) Do(r *http.Request) (*http.Response, error) {
return doForClient(p.azClient, r)
}

View File

@ -0,0 +1,36 @@
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param (
# Captures any arguments from eng/New-TestResources.ps1 not declared here (no parameter errors).
[Parameter(ValueFromRemainingArguments = $true)]
$RemainingArguments
)
if (!$CI) {
# TODO: Remove this once auto-cloud config downloads are supported locally
Write-Host "Skipping cert setup in local testing mode"
return
}
if ($EnvironmentVariables -eq $null -or $EnvironmentVariables.Count -eq 0) {
throw "EnvironmentVariables must be set in the calling script New-TestResources.ps1"
}
$tmp = $env:TEMP ? $env:TEMP : [System.IO.Path]::GetTempPath()
$pfxPath = Join-Path $tmp "test.pfx"
$pemPath = Join-Path $tmp "test.pem"
$sniPath = Join-Path $tmp "testsni.pfx"
Write-Host "Creating identity test files: $pfxPath $pemPath $sniPath"
[System.Convert]::FromBase64String($EnvironmentVariables['PFX_CONTENTS']) | Set-Content -Path $pfxPath -AsByteStream
Set-Content -Path $pemPath -Value $EnvironmentVariables['PEM_CONTENTS']
[System.Convert]::FromBase64String($EnvironmentVariables['SNI_CONTENTS']) | Set-Content -Path $sniPath -AsByteStream
# Set for pipeline
Write-Host "##vso[task.setvariable variable=IDENTITY_SP_CERT_PFX;]$pfxPath"
Write-Host "##vso[task.setvariable variable=IDENTITY_SP_CERT_PEM;]$pemPath"
Write-Host "##vso[task.setvariable variable=IDENTITY_SP_CERT_SNI;]$sniPath"
# Set for local
$env:IDENTITY_SP_CERT_PFX = $pfxPath
$env:IDENTITY_SP_CERT_PEM = $pemPath
$env:IDENTITY_SP_CERT_SNI = $sniPath

View File

@ -0,0 +1 @@
param baseName string

View File

@ -0,0 +1,90 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
)
const credNameUserPassword = "UsernamePasswordCredential"
// UsernamePasswordCredentialOptions contains optional parameters for UsernamePasswordCredential.
type UsernamePasswordCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// authenticationRecord returned by a call to a credential's Authenticate method. Set this option
// to enable the credential to use data from a previous authentication.
authenticationRecord authenticationRecord
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// tokenCachePersistenceOptions enables persistent token caching when not nil.
tokenCachePersistenceOptions *tokenCachePersistenceOptions
}
// UsernamePasswordCredential authenticates a user with a password. Microsoft doesn't recommend this kind of authentication,
// because it's less secure than other authentication flows. This credential is not interactive, so it isn't compatible
// with any form of multi-factor authentication, and the application must already have user or admin consent.
// This credential can only authenticate work and school accounts; it can't authenticate Microsoft accounts.
type UsernamePasswordCredential struct {
client *publicClient
}
// NewUsernamePasswordCredential creates a UsernamePasswordCredential. clientID is the ID of the application the user
// will authenticate to. Pass nil for options to accept defaults.
func NewUsernamePasswordCredential(tenantID string, clientID string, username string, password string, options *UsernamePasswordCredentialOptions) (*UsernamePasswordCredential, error) {
if options == nil {
options = &UsernamePasswordCredentialOptions{}
}
opts := publicClientOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
Password: password,
Record: options.authenticationRecord,
TokenCachePersistenceOptions: options.tokenCachePersistenceOptions,
Username: username,
}
c, err := newPublicClient(tenantID, clientID, credNameUserPassword, opts)
if err != nil {
return nil, err
}
return &UsernamePasswordCredential{client: c}, err
}
// Authenticate the user. Subsequent calls to GetToken will automatically use the returned AuthenticationRecord.
func (c *UsernamePasswordCredential) authenticate(ctx context.Context, opts *policy.TokenRequestOptions) (authenticationRecord, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameUserPassword+"."+traceOpAuthenticate, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.Authenticate(ctx, opts)
return tk, err
}
// GetToken requests an access token from Microsoft Entra ID. This method is called automatically by Azure SDK clients.
func (c *UsernamePasswordCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameUserPassword+"."+traceOpGetToken, c.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := c.client.GetToken(ctx, opts)
return tk, err
}
var _ azcore.TokenCredential = (*UsernamePasswordCredential)(nil)

View File

@ -0,0 +1,18 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
const (
// UserAgent is the string to be used in the user agent string when making requests.
component = "azidentity"
// module is the fully qualified name of the module used in telemetry and distributed tracing.
module = "github.com/Azure/azure-sdk-for-go/sdk/" + component
// Version is the semantic version (see http://semver.org) of this module.
version = "v1.5.1"
)

Some files were not shown because too many files have changed in this diff Show More