Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ServiceBus SDK: Rbac support #6393

Merged
merged 7 commits into from
Jun 12, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,28 @@ namespace Microsoft.Azure.ServiceBus.Primitives
{
using System;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

/// <summary>
/// Represents the Azure Active Directory token provider for the Service Bus.
/// </summary>
public class AzureActiveDirectoryTokenProvider : TokenProvider
{
readonly AuthenticationContext authContext;
readonly ClientCredential clientCredential;
#if !UAP10_0
readonly ClientAssertionCertificate clientAssertionCertificate;
#endif
readonly string clientId;
readonly Uri redirectUri;
readonly IPlatformParameters platformParameters;
readonly UserIdentifier userIdentifier;

enum AuthType
{
ClientCredential,
UserPasswordCredential,
ClientAssertionCertificate,
InteractiveUserLogin
}
/// <summary>
/// Common authority for Azure Active Directory.
/// </summary>
public const string CommonAuthority = "https://login.microsoftonline.com/common";

readonly AuthType authType;
readonly string authority;
readonly object authCallbackState;
event AuthenticationCallback AuthCallback;

internal AzureActiveDirectoryTokenProvider(AuthenticationContext authContext, ClientCredential credential)
{
this.clientCredential = credential;
this.authContext = authContext;
this.authType = AuthType.ClientCredential;
this.clientId = clientCredential.ClientId;
}
public delegate Task<string> AuthenticationCallback(string audience, string authority, object state);

#if !UAP10_0
internal AzureActiveDirectoryTokenProvider(AuthenticationContext authContext, ClientAssertionCertificate clientAssertionCertificate)
internal AzureActiveDirectoryTokenProvider(AuthenticationCallback authenticationCallback, string authority, object state)
{
this.clientAssertionCertificate = clientAssertionCertificate;
this.authContext = authContext;
this.authType = AuthType.ClientAssertionCertificate;
this.clientId = clientAssertionCertificate.ClientId;
}
#endif

internal AzureActiveDirectoryTokenProvider(AuthenticationContext authContext, string clientId, Uri redirectUri, IPlatformParameters platformParameters, UserIdentifier userIdentifier)
{
this.authContext = authContext;
this.clientId = clientId;
this.redirectUri = redirectUri;
this.platformParameters = platformParameters;
this.userIdentifier = userIdentifier;
this.authType = AuthType.InteractiveUserLogin;
this.authority = authority;
this.AuthCallback = authenticationCallback;
this.authCallbackState = state;
}

/// <summary>
Expand All @@ -68,29 +37,8 @@ internal AzureActiveDirectoryTokenProvider(AuthenticationContext authContext, st
/// <returns><see cref="SecurityToken"/></returns>
public override async Task<SecurityToken> GetTokenAsync(string appliesTo, TimeSpan timeout)
{
AuthenticationResult authResult;

switch (this.authType)
{
case AuthType.ClientCredential:
authResult = await this.authContext.AcquireTokenAsync(Constants.AadServiceBusAudience, this.clientCredential);
break;

#if !UAP10_0
case AuthType.ClientAssertionCertificate:
authResult = await this.authContext.AcquireTokenAsync(Constants.AadServiceBusAudience, this.clientAssertionCertificate);
break;
#endif

case AuthType.InteractiveUserLogin:
authResult = await this.authContext.AcquireTokenAsync(Constants.AadServiceBusAudience, this.clientId, this.redirectUri, this.platformParameters, this.userIdentifier);
break;

default:
throw new NotSupportedException();
}

return new JsonSecurityToken(authResult.AccessToken, appliesTo);
var tokenString = await this.AuthCallback(appliesTo, this.authority, this.authCallbackState).ConfigureAwait(false);
return new JsonSecurityToken(tokenString, appliesTo);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.Azure.ServiceBus.Primitives
/// <summary>
/// Represents the Azure Active Directory token provider for Azure Managed Service Identity integration.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're moving forward with the rename, you'll probably want to consider updating this comment as well.

/// </summary>
public class ManagedServiceIdentityTokenProvider : TokenProvider
public class ManagedIdentityTokenProvider : TokenProvider
Copy link
Contributor

@nemakam nemakam May 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ManagedIdentityTokenProvider [](start = 17, length = 28)

Do we need this rename? If we rename the public method then it becomes a breaking change.

If there's enough justification for the rename, lets update the major version in the csproj. #Closed

{
static AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ namespace Microsoft.Azure.ServiceBus.Primitives
{
using System;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

/// <summary>
/// This abstract base class can be extended to implement additional token providers.
Expand Down Expand Up @@ -74,88 +73,28 @@ public static TokenProvider CreateSharedAccessSignatureTokenProvider(string keyN
return new SharedAccessSignatureTokenProvider(keyName, sharedAccessKey, tokenTimeToLive, tokenScope);
}

/// <summary>Creates an Azure Active Directory token provider.</summary>
/// <param name="authContext">AuthenticationContext for AAD.</param>
/// <param name="clientCredential">The app credential.</param>
/// <returns>The <see cref="TokenProvider" /> for returning Json web token.</returns>
public static TokenProvider CreateAadTokenProvider(AuthenticationContext authContext, ClientCredential clientCredential)
{
if (authContext == null)
{
throw new ArgumentNullException(nameof(authContext));
}

if (clientCredential == null)
{
throw new ArgumentNullException(nameof(clientCredential));
}

return new AzureActiveDirectoryTokenProvider(authContext, clientCredential);
}

/// <summary>Creates an Azure Active Directory token provider.</summary>
/// <param name="authContext">AuthenticationContext for AAD.</param>
/// <param name="clientId">ClientId for AAD.</param>
/// <param name="redirectUri">The redirectUri on Client App.</param>
/// <param name="platformParameters">Platform parameters</param>
/// <param name="userIdentifier">User Identifier</param>
/// <returns>The <see cref="TokenProvider" /> for returning Json web token.</returns>
public static TokenProvider CreateAadTokenProvider(
AuthenticationContext authContext,
string clientId,
Uri redirectUri,
IPlatformParameters platformParameters,
UserIdentifier userIdentifier = null)
{
if (authContext == null)
{
throw new ArgumentNullException(nameof(authContext));
}

if (string.IsNullOrEmpty(clientId))
{
throw new ArgumentNullException(nameof(clientId));
}

if (redirectUri == null)
{
throw new ArgumentNullException(nameof(redirectUri));
}

if (platformParameters == null)
{
throw new ArgumentNullException(nameof(platformParameters));
}

return new AzureActiveDirectoryTokenProvider(authContext, clientId, redirectUri, platformParameters, userIdentifier);
}

#if !UAP10_0
/// <summary>Creates an Azure Active Directory token provider.</summary>
/// <param name="authContext">AuthenticationContext for AAD.</param>
/// <param name="clientAssertionCertificate">The client assertion certificate credential.</param>
/// <returns>The <see cref="TokenProvider" /> for returning Json web token.</returns>
public static TokenProvider CreateAadTokenProvider(AuthenticationContext authContext, ClientAssertionCertificate clientAssertionCertificate)
/// <param name="authCallback">The authentication delegate to provide access token.</param>
/// <param name="authority">Address of the authority to issue token.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Respectfully, I'm not sure that these comments offer much context or clarity for users who are unfamiliar. You may want to consider revising them to be more explicit or, if not, just removing them.

/// <param name="state">State to be delivered to callback.</param>
/// <returns>The <see cref="Microsoft.ServiceBus.TokenProvider" /> for returning Json web token.</returns>
public static TokenProvider CreateAzureActiveDirectoryTokenProvider(
AzureActiveDirectoryTokenProvider.AuthenticationCallback authCallback,
string authority = AzureActiveDirectoryTokenProvider.CommonAuthority,
object state = null)
{
if (authContext == null)
{
throw new ArgumentNullException(nameof(authContext));
}

if (clientAssertionCertificate == null)
if (authCallback == null)
{
throw new ArgumentNullException(nameof(clientAssertionCertificate));
throw new ArgumentNullException(nameof(authCallback));
}

return new AzureActiveDirectoryTokenProvider(authContext, clientAssertionCertificate);
return new AzureActiveDirectoryTokenProvider(authCallback, authority, state);
}
#endif

/// <summary>Creates Azure Managed Service Identity token provider.</summary>
/// <summary>Creates Azure Managed Identity token provider.</summary>
/// <returns>The <see cref="TokenProvider" /> for returning Json web token.</returns>
public static TokenProvider CreateManagedServiceIdentityTokenProvider()
public static TokenProvider CreateManagedIdentityTokenProvider()
{
return new ManagedServiceIdentityTokenProvider();
return new ManagedIdentityTokenProvider();
}

/// <summary>
Expand Down
48 changes: 48 additions & 0 deletions sdk/servicebus/Microsoft.Azure.ServiceBus/src/QueueClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,54 @@ public QueueClient(ServiceBusConnection serviceBusConnection, string entityPath,
MessagingEventSource.Log.QueueClientCreateStop(serviceBusConnection.Endpoint.Authority, entityPath, this.ClientId);
}

/// <summary>
/// Creates a new instance of the Queue client using Azure Active Directory authentication.
/// </summary>
/// <param name="endpoint">Fully qualified domain name for Service Bus. Most likely, {yournamespace}.servicebus.windows.net</param>
/// <param name="entityPath">Queue path.</param>
/// <param name="authCallback">User provided delegate that will provide the access token.</param>
/// <param name="authority">Address of the authority to issue token.</param>
/// <param name="transportType">Transport type.</param>
/// <param name="receiveMode">Mode of receive of messages. Defaults to <see cref="ReceiveMode"/>.PeekLock.</param>
/// <param name="retryPolicy">Retry policy for queue operations. Defaults to <see cref="RetryPolicy.Default"/></param>
/// <remarks>Creates a new connection to the queue, which is opened during the first send/receive operation.</remarks>
public static QueueClient CreateWithAzureActiveDirectory(
Copy link
Contributor

@nemakam nemakam May 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateWithAzureActiveDirectory [](start = 34, length = 30)

  1. Do we need this API? We already have the overload where you can pass your own ITokenProvider. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also accept connectionStringBuilder. So lets not duplicate the APIs


In reply to: 286696363 [](ancestors = 286696363)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious as to the purpose of not just documenting that the consumer should create and use an CreateAzureActiveDirectoryTokenProvider or CreateAzureActiveDirectoryTokenProvider and pass them to the ITokenProvider overload. Is there a benefit to consumers that I'm overlooking?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to block on this, but I'm still curious as to the answer. I feel that I'm missing some important context...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's removed and will no longer part of this change. It was an effort to keep consistency with the EventHubs SDK but it seems like there are good reasons to avoid these redundant APIs.


In reply to: 292678097 [](ancestors = 292678097)

Copy link
Contributor

@nemakam nemakam May 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static [](start = 15, length = 6)

Even if this API is needed, this should not be static. This SDK has slightly different API syntax compared to the older one. Here you should be able to do a new QueueClient() , and not QueueClient.CreateWith* #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do constructor, the signature will collide with the one below. Eventhubs is also using Create() methods


In reply to: 286696743 [](ancestors = 286696743)

string endpoint,
string entityPath,
AzureActiveDirectoryTokenProvider.AuthenticationCallback authCallback,
string authority = AzureActiveDirectoryTokenProvider.CommonAuthority,
TransportType transportType = TransportType.Amqp,
ReceiveMode receiveMode = ReceiveMode.PeekLock,
RetryPolicy retryPolicy = null)
{
return new QueueClient(new ServiceBusConnection(endpoint, transportType, retryPolicy)
{
TokenProvider = TokenProvider.CreateAzureActiveDirectoryTokenProvider(authCallback, authority)
}, entityPath, receiveMode, retryPolicy);
}

/// <summary>
/// Creates a new instance of the Queue client by using Azure Managed Identity authentication.
/// </summary>
/// <param name="endpoint">Fully qualified domain name for Service Bus. Most likely, {yournamespace}.servicebus.windows.net</param>
/// <param name="entityPath">Queue path.</param>
/// <param name="transportType">Transport type.</param>
/// <param name="receiveMode">Mode of receive of messages. Defaults to <see cref="ReceiveMode"/>.PeekLock.</param>
/// <param name="retryPolicy">Retry policy for queue operations. Defaults to <see cref="RetryPolicy.Default"/></param>
/// <remarks>Creates a new connection to the queue, which is opened during the first send/receive operation.</remarks>
public static QueueClient CreateWithManagedIdentity(
Copy link
Contributor

@nemakam nemakam May 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateWithManagedIdentity [](start = 34, length = 25)

Same as above #Closed

string endpoint,
string entityPath,
TransportType transportType = TransportType.Amqp,
ReceiveMode receiveMode = ReceiveMode.PeekLock,
RetryPolicy retryPolicy = null)
{
return new QueueClient(new ServiceBusConnection(endpoint, transportType, retryPolicy)
{
TokenProvider = TokenProvider.CreateManagedIdentityTokenProvider()
}, entityPath, receiveMode, retryPolicy);
}

/// <summary>
/// Gets the name of the queue.
/// </summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions sdk/servicebus/Microsoft.Azure.ServiceBus/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,7 @@
<data name="TimeoutMustBePositiveNonZero" xml:space="preserve">
<value>Argument {0} must be a positive non-zero timeout value. The provided value was {1}.</value>
</data>
<data name="ArgumentInvalidCombination" xml:space="preserve">
<value>The following arguments must all be provided or none at all: {0}.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ void InitializeConnection(ServiceBusConnectionStringBuilder builder)
{
this.TokenProvider = new SharedAccessSignatureTokenProvider(builder.SasKeyName, builder.SasKey);
}
else if (!string.Equals(builder.Authentication, "Managed Identity", StringComparison.OrdinalIgnoreCase))
Copy link
Contributor

@nemakam nemakam May 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Managed Identity" [](start = 60, length = 18)

nit: convert to a const string #Closed

Copy link
Contributor

@nemakam nemakam May 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(!string.Equals [](start = 20, length = 15)

Is it supposed to be *!*string.Equals?? #Closed

{
this.TokenProvider = Primitives.TokenProvider.CreateManagedIdentityTokenProvider();
}

this.OperationTimeout = builder.OperationTimeout;
this.TransportType = builder.TransportType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ServiceBusConnectionStringBuilder
const string SharedAccessKeyNameConfigName = "SharedAccessKeyName";
const string SharedAccessKeyConfigName = "SharedAccessKey";
const string SharedAccessSignatureConfigName = "SharedAccessSignature";
const string AuthenticationConfigName = "Authentication";

const string EntityPathConfigName = "EntityPath";
const string TransportTypeConfigName = "TransportType";
Expand Down Expand Up @@ -221,6 +222,11 @@ public string SasToken
/// <remarks>Defaults to 1 minute.</remarks>
public TimeSpan OperationTimeout { get; set; } = Constants.DefaultOperationTimeout;

/// <summary>
/// Enables Azure Active Directory Managed Identity authentication when set to 'Managed Identity'
/// </summary>
public string Authentication { get; set; }
Copy link
Contributor

@nemakam nemakam May 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authentication [](start = 22, length = 14)

Curious as to why this is a string? As compared to other options of - Enum or bool #Closed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest the enum approach, personally. Asking consumers to pass a magic string seems like it would offer a challenge to discovering usable values via intellisense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case that you're set on a string, maybe consider adding a pseudo-enumeration to allow discovery and help prevent typos, then? Something like:

public static class AuthenticationType
{
    public static const ManagedIdentity = "Managed Identity";
}

That also gives you the advantage of being able to throw doc comments on the type and member to guide the user experience.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected to extend methods of authentication in the near future?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another area that I've not seen a response and have concerns about user experience. I don't want to block on it, as it is consistent with the direction that the Event Hubs client went, but I would love to see some thoughts after usability testing.


internal Dictionary<string, string> ConnectionStringProperties = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);

/// <summary>
Expand All @@ -229,6 +235,7 @@ public string SasToken
/// <returns>Namespace connection string</returns>
public string GetNamespaceConnectionString()
{
validate();
var connectionStringBuilder = new StringBuilder();
if (this.Endpoint != null)
{
Expand Down Expand Up @@ -260,6 +267,11 @@ public string GetNamespaceConnectionString()
connectionStringBuilder.Append($"{OperationTimeoutConfigName}{KeyValueSeparator}{this.OperationTimeout}{KeyValuePairDelimiter}");
}

if (!string.IsNullOrWhiteSpace(this.Authentication))
{
connectionStringBuilder.Append($"{AuthenticationConfigName}{KeyValueSeparator}{this.Authentication}{KeyValuePairDelimiter}");
}

return connectionStringBuilder.ToString().Trim(';');
}

Expand Down Expand Up @@ -358,11 +370,33 @@ void ParseConnectionString(string connectionString)
throw Fx.Exception.Argument(nameof(connectionString), $"The {OperationTimeoutConfigName} ({value}) must be smaller than one hour.");
}
}
else if (key.Equals(AuthenticationConfigName, StringComparison.OrdinalIgnoreCase))
{
this.Authentication = value;
}
else
{
ConnectionStringProperties[key] = value;
}
}
validate();
}

void validate()
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Instead of calling during ToString(), you could just do validation within the setter of each of the property..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that you have validation within the setter, you don't need this method anymore.


In reply to: 286699955 [](ancestors = 286699955)

bool hasAuthentication = !string.IsNullOrWhiteSpace(this.Authentication);
bool hasSharedAccessKeyName = !string.IsNullOrWhiteSpace(this.SasKeyName);
bool hasSharedAccessSignature = !string.IsNullOrWhiteSpace(this.SasToken);

if (hasAuthentication && hasSharedAccessKeyName)
{
throw Fx.Exception.Argument("Authentication, SharedAccessKeyName", Resources.ArgumentInvalidCombination.FormatForUser("Authentication, SharedAccessKeyName"));
}

if (hasAuthentication && hasSharedAccessSignature)
{
throw Fx.Exception.Argument("Authentication, SharedAccessSignature", Resources.ArgumentInvalidCombination.FormatForUser("Authentication, SharedAccessSignature"));
}
}
}
}
Loading