Add support for providing an object ID to ManagedIdentityCredential. (#5910)

* Add support for providing an object ID to ManagedIdentityCredential.

* Update cspell checks.

* Add support for creating a Uuid from a string, and use that as the
MICred overload for objectId.

* Add a remark comment to make it clear that object and client ids are not interchangeable.

* Remove the non-const ToString() as that isn't required.

* Update tests to use valid hex Uuid values.

* Use a discriminated union design approach with a ManagedIdentityType
object and ManagedIdentityIdType enum.

* Fix typo and remove Uuid CreateFromString.

* Address PR feedback.

* Update doc comment.

* Update comments and exception message to consistently use hyphens
between user/system and assigned.
This commit is contained in:
Ahson Khan 2024-08-16 20:47:05 -07:00 committed by GitHub
parent 758460d618
commit 54a7bc363b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1162 additions and 256 deletions

View File

@ -5,6 +5,7 @@
### Features Added
- Request logs to now include the `accept-range`, `content-range`, `range`, `WWW-Authenticate`, `x-ms-date`, `x-ms-error-code`, `x-ms-range`, and `x-ms-version` headers.
### Breaking Changes
### Bugs Fixed

View File

@ -33,9 +33,9 @@ namespace Azure { namespace Core {
public:
/**
* @brief Gets Uuid as a string.
* @details A string is in canonical format (4-2-2-2-6 lowercase hex and dashes only).
* @details A string is in canonical format (8-4-4-4-12 lowercase hex and dashes only).
*/
std::string ToString();
std::string ToString() const;
/**
* @brief Returns the binary value of the Uuid for consumption by clients who need non-string

View File

@ -17,7 +17,7 @@ static thread_local std::mt19937_64 randomGenerator(std::random_device{}());
#endif
namespace Azure { namespace Core {
std::string Uuid::ToString()
std::string Uuid::ToString() const
{
// Guid is 36 characters
// Add one byte for the \0

View File

@ -4,6 +4,8 @@
### Features Added
- Added support for providing an object ID to `ManagedIdentityCredential`.
### Breaking Changes
- Previously, if a clientId was specified for Cloud Shell managed identity, which is not supported, the clientId was passed into the request body. Now, an exception will be thrown if a clientId is specified for Cloud Shell managed identity.

View File

@ -42,6 +42,91 @@ namespace Azure { namespace Identity {
std::string ToString() const { return m_resourceId; }
};
/**
* @brief The type of managed identity identifier depending on how the managed identity is
* configured.
*
* @remark This can either be system-assigned, or user-assigned which corresponds to an identifier
* that represents either client ID, resource ID, or object ID, depending on how the managed
* identity is configured.
*/
enum class ManagedIdentityIdType
{
SystemAssigned,
ClientId,
ObjectId,
ResourceId,
};
/**
* @brief The type of managed identity and its corresponding identifier.
*
* @remark This class holds the type and unique identifier for either a system or user-assigned
* managed identity.
*/
class ManagedIdentityType final {
private:
ManagedIdentityIdType m_idType;
std::string m_id;
public:
/**
* @brief Constructs the type of managed identity.
*
* @remark This defaults to ManagedIdentityIdType::SystemAssigned.
*/
explicit ManagedIdentityType() : m_idType(ManagedIdentityIdType::SystemAssigned) {}
/**
* @brief Constructs the type of managed identity.
*
* @param idType The type of the managed identity identifier.
* @param id The value of the managed identity identifier. This can be either a client ID,
* resource ID, or object ID.
*
* @remark For ManagedIdentityIdType::SystemAssigned, the id must be an empty string.
*
* @remark Make sure the type of ID matches the value of the ID. For example, the client
* ID and object ID are NOT interchangeable, even though they are both Uuid values.
*/
explicit ManagedIdentityType(ManagedIdentityIdType idType, std::string id)
: m_idType(idType), m_id(id)
{
if (idType == ManagedIdentityIdType::SystemAssigned && !id.empty())
{
throw std::invalid_argument(
"There is no need to provide an ID (such as client, object, or resource ID) if you are "
"using system-assigned managed identity.");
}
}
/**
* @brief Gets the identifier for a user-assigned managed identity.
*
* @remark In the case of system-assigned managed identity, this will return an empty string.
*/
std::string const& GetId() const { return m_id; }
/**
* @brief Gets the type of identifier used for the managed identity, depending on how it is
* configured.
*/
ManagedIdentityIdType GetManagedIdentityIdType() const { return m_idType; }
};
/**
* @brief Options for managed identity credential.
*
*/
struct ManagedIdentityCredentialOptions final : public Core::Credentials::TokenCredentialOptions
{
/**
* @brief Specifies the type of managed identity and its corresponding identifier, based on how
* it was configured.
*/
ManagedIdentityType IdentityType;
};
/**
* @brief Attempts authentication using a managed identity that has been assigned to the
* deployment environment. This authentication type works in Azure VMs, App Service and Azure
@ -71,6 +156,14 @@ namespace Azure { namespace Identity {
Azure::Core::Credentials::TokenCredentialOptions const& options
= Azure::Core::Credentials::TokenCredentialOptions());
/**
* @brief Constructs a Managed Identity Credential.
*
* @param options Options for token retrieval.
*/
explicit ManagedIdentityCredential(
Azure::Identity::ManagedIdentityCredentialOptions const& options);
/**
* @brief Constructs an instance of ManagedIdentityCredential capable of authenticating a
* resource with a user-assigned managed identity.

View File

@ -11,6 +11,7 @@ namespace {
std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource(
std::string const& credentialName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Azure::Core::Credentials::TokenCredentialOptions const& options)
{
@ -19,6 +20,7 @@ std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource(
static std::unique_ptr<ManagedIdentitySource> (*managedIdentitySourceCreate[])(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
TokenCredentialOptions const& options)
= {AppServiceV2019ManagedIdentitySource::Create,
@ -31,7 +33,7 @@ std::unique_ptr<_detail::ManagedIdentitySource> CreateManagedIdentitySource(
// For that reason, it is not possible to cover that execution branch in tests.
for (auto create : managedIdentitySourceCreate)
{
if (auto source = create(credentialName, clientId, resourceId, options))
if (auto source = create(credentialName, clientId, objectId, resourceId, options))
{
return source;
}
@ -49,7 +51,34 @@ ManagedIdentityCredential::ManagedIdentityCredential(
Azure::Core::Credentials::TokenCredentialOptions const& options)
: TokenCredential("ManagedIdentityCredential")
{
m_managedIdentitySource = CreateManagedIdentitySource(GetCredentialName(), clientId, {}, options);
m_managedIdentitySource
= CreateManagedIdentitySource(GetCredentialName(), clientId, {}, {}, options);
}
ManagedIdentityCredential::ManagedIdentityCredential(
Azure::Identity::ManagedIdentityCredentialOptions const& options)
: TokenCredential("ManagedIdentityCredential")
{
ManagedIdentityIdType idType = options.IdentityType.GetManagedIdentityIdType();
switch (idType)
{
case ManagedIdentityIdType::SystemAssigned:
m_managedIdentitySource
= CreateManagedIdentitySource(GetCredentialName(), {}, {}, {}, options);
break;
case ManagedIdentityIdType::ClientId:
m_managedIdentitySource = CreateManagedIdentitySource(
GetCredentialName(), options.IdentityType.GetId(), {}, {}, options);
break;
case ManagedIdentityIdType::ObjectId:
m_managedIdentitySource = CreateManagedIdentitySource(
GetCredentialName(), {}, options.IdentityType.GetId(), {}, options);
break;
case ManagedIdentityIdType::ResourceId:
m_managedIdentitySource = CreateManagedIdentitySource(
GetCredentialName(), {}, {}, options.IdentityType.GetId(), options);
break;
}
}
ManagedIdentityCredential::ManagedIdentityCredential(
@ -58,7 +87,7 @@ ManagedIdentityCredential::ManagedIdentityCredential(
: TokenCredential("ManagedIdentityCredential")
{
m_managedIdentitySource
= CreateManagedIdentitySource(GetCredentialName(), {}, resourceId.ToString(), options);
= CreateManagedIdentitySource(GetCredentialName(), {}, {}, resourceId.ToString(), options);
}
ManagedIdentityCredential::ManagedIdentityCredential(

View File

@ -140,6 +140,7 @@ template <typename T>
std::unique_ptr<ManagedIdentitySource> AppServiceManagedIdentitySource::Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Azure::Core::Credentials::TokenCredentialOptions const& options,
char const* endpointVarName,
@ -155,6 +156,7 @@ std::unique_ptr<ManagedIdentitySource> AppServiceManagedIdentitySource::Create(
{
return std::unique_ptr<ManagedIdentitySource>(new T(
clientId,
objectId,
resourceId,
options,
ParseEndpointUrl(credName, msiEndpoint, endpointVarName, credSource),
@ -167,6 +169,7 @@ std::unique_ptr<ManagedIdentitySource> AppServiceManagedIdentitySource::Create(
AppServiceManagedIdentitySource::AppServiceManagedIdentitySource(
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Azure::Core::Credentials::TokenCredentialOptions const& options,
Azure::Core::Url endpointUrl,
@ -183,13 +186,19 @@ AppServiceManagedIdentitySource::AppServiceManagedIdentitySource(
url.AppendQueryParameter("api-version", apiVersion);
// Only one of clientId or resourceId will be set to a non-empty value.
// Only one of clientId, objectId, or resourceId will be set to a non-empty value.
// AppService uses mi_res_id, and not msi_res_id:
// https://learn.microsoft.com/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#rest-endpoint-reference
// Based on the App Service documentation, using principal_id for the query parameter name here
// instead of object_id (which is used as an alias).
if (!clientId.empty())
{
url.AppendQueryParameter(clientIdHeaderName, clientId);
}
else if (!objectId.empty())
{
url.AppendQueryParameter("principal_id", objectId);
}
else if (!resourceId.empty())
{
url.AppendQueryParameter("mi_res_id", resourceId);
@ -233,26 +242,36 @@ Azure::Core::Credentials::AccessToken AppServiceManagedIdentitySource::GetToken(
std::unique_ptr<ManagedIdentitySource> AppServiceV2017ManagedIdentitySource::Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options)
{
return AppServiceManagedIdentitySource::Create<AppServiceV2017ManagedIdentitySource>(
credName, clientId, resourceId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017");
credName, clientId, objectId, resourceId, options, "MSI_ENDPOINT", "MSI_SECRET", "2017");
}
std::unique_ptr<ManagedIdentitySource> AppServiceV2019ManagedIdentitySource::Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options)
{
return AppServiceManagedIdentitySource::Create<AppServiceV2019ManagedIdentitySource>(
credName, clientId, resourceId, options, "IDENTITY_ENDPOINT", "IDENTITY_HEADER", "2019");
credName,
clientId,
objectId,
resourceId,
options,
"IDENTITY_ENDPOINT",
"IDENTITY_HEADER",
"2019");
}
std::unique_ptr<ManagedIdentitySource> CloudShellManagedIdentitySource::Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Azure::Core::Credentials::TokenCredentialOptions const& options)
{
@ -265,11 +284,11 @@ std::unique_ptr<ManagedIdentitySource> CloudShellManagedIdentitySource::Create(
if (!msiEndpoint.empty())
{
if (!clientId.empty() || !resourceId.empty())
if (!clientId.empty() || !objectId.empty() || !resourceId.empty())
{
throw AuthenticationException(
"User-assigned managed identities are not supported in Cloud Shell environments. Omit "
"the clientId or resourceId when constructing the ManagedIdentityCredential.");
"the clientId, objectId, or resourceId when constructing the ManagedIdentityCredential.");
}
return std::unique_ptr<ManagedIdentitySource>(new CloudShellManagedIdentitySource(
@ -328,6 +347,7 @@ Azure::Core::Credentials::AccessToken CloudShellManagedIdentitySource::GetToken(
std::unique_ptr<ManagedIdentitySource> AzureArcManagedIdentitySource::Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Azure::Core::Credentials::TokenCredentialOptions const& options)
{
@ -344,12 +364,12 @@ std::unique_ptr<ManagedIdentitySource> AzureArcManagedIdentitySource::Create(
return nullptr;
}
if (!clientId.empty() || !resourceId.empty())
if (!clientId.empty() || !objectId.empty() || !resourceId.empty())
{
throw AuthenticationException(
"User assigned identity is not supported by the Azure Arc Managed Identity Endpoint. "
"To authenticate with the system assigned identity, omit the client or resource ID "
"when constructing the ManagedIdentityCredential.");
"To authenticate with the system assigned identity, omit the client, object, or resource "
"ID when constructing the ManagedIdentityCredential.");
}
return std::unique_ptr<ManagedIdentitySource>(new AzureArcManagedIdentitySource(
@ -455,6 +475,7 @@ Azure::Core::Credentials::AccessToken AzureArcManagedIdentitySource::GetToken(
std::unique_ptr<ManagedIdentitySource> ImdsManagedIdentitySource::Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Azure::Core::Credentials::TokenCredentialOptions const& options)
{
@ -475,11 +496,12 @@ std::unique_ptr<ManagedIdentitySource> ImdsManagedIdentitySource::Create(
imdsUrl.AppendPath("/metadata/identity/oauth2/token");
return std::unique_ptr<ManagedIdentitySource>(
new ImdsManagedIdentitySource(clientId, resourceId, imdsUrl, options));
new ImdsManagedIdentitySource(clientId, objectId, resourceId, imdsUrl, options));
}
ImdsManagedIdentitySource::ImdsManagedIdentitySource(
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Azure::Core::Url const& imdsUrl,
Azure::Core::Credentials::TokenCredentialOptions const& options)
@ -492,13 +514,17 @@ ImdsManagedIdentitySource::ImdsManagedIdentitySource(
url.AppendQueryParameter("api-version", "2018-02-01");
// Only one of clientId or resourceId will be set to a non-empty value.
// Only one of clientId, objectId, or resourceId will be set to a non-empty value.
// IMDS uses msi_res_id, and not mi_res_id:
// https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
if (!clientId.empty())
{
url.AppendQueryParameter("client_id", clientId);
}
else if (!objectId.empty())
{
url.AppendQueryParameter("object_id", objectId);
}
else if (!resourceId.empty())
{
url.AppendQueryParameter("msi_res_id", resourceId);

View File

@ -54,6 +54,7 @@ namespace Azure { namespace Identity { namespace _detail {
protected:
explicit AppServiceManagedIdentitySource(
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options,
Core::Url endpointUrl,
@ -66,6 +67,7 @@ namespace Azure { namespace Identity { namespace _detail {
static std::unique_ptr<ManagedIdentitySource> Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options,
char const* endpointVarName,
@ -84,12 +86,14 @@ namespace Azure { namespace Identity { namespace _detail {
private:
explicit AppServiceV2017ManagedIdentitySource(
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options,
Core::Url endpointUrl,
std::string const& secret)
: AppServiceManagedIdentitySource(
clientId,
objectId,
resourceId,
options,
endpointUrl,
@ -104,6 +108,7 @@ namespace Azure { namespace Identity { namespace _detail {
static std::unique_ptr<ManagedIdentitySource> Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options);
};
@ -114,12 +119,14 @@ namespace Azure { namespace Identity { namespace _detail {
private:
explicit AppServiceV2019ManagedIdentitySource(
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options,
Core::Url endpointUrl,
std::string const& secret)
: AppServiceManagedIdentitySource(
clientId,
objectId,
resourceId,
options,
endpointUrl,
@ -134,6 +141,7 @@ namespace Azure { namespace Identity { namespace _detail {
static std::unique_ptr<ManagedIdentitySource> Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options);
};
@ -151,6 +159,7 @@ namespace Azure { namespace Identity { namespace _detail {
static std::unique_ptr<ManagedIdentitySource> Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options);
@ -171,6 +180,7 @@ namespace Azure { namespace Identity { namespace _detail {
static std::unique_ptr<ManagedIdentitySource> Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options);
@ -185,6 +195,7 @@ namespace Azure { namespace Identity { namespace _detail {
explicit ImdsManagedIdentitySource(
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Url const& imdsUrl,
Core::Credentials::TokenCredentialOptions const& options);
@ -193,6 +204,7 @@ namespace Azure { namespace Identity { namespace _detail {
static std::unique_ptr<ManagedIdentitySource> Create(
std::string const& credName,
std::string const& clientId,
std::string const& objectId,
std::string const& resourceId,
Core::Credentials::TokenCredentialOptions const& options);