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] Helper function to parse connection string #11949

Merged
merged 10 commits into from
Oct 21, 2020
2 changes: 1 addition & 1 deletion sdk/servicebus/service-bus/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- The `processError` passed to `Receiver.subscribe` now receives a `ProcessErrorArgs` instead of just an error. This parameter provides additional context that can make it simpler to distinguish
errors that were thrown from your callback (via the `errorSource` member of `ProcessErrorArgs`) as well as giving you some information about the entity that generated the error.
[PR 11927](https://github.com/Azure/azure-sdk-for-js/pull/11927)

- A helper method `parseServiceBusConnectionString` has been added which validates and parses a given connection string for Azure Service Bus. [PR 11949](https://github.com/Azure/azure-sdk-for-js/pull/11949)
- Added new "userId" property to `ServiceBusMessage` interface. [PR 11810](https://github.com/Azure/azure-sdk-for-js/pull/11810)

- `NamespaceProperties` interface property "messageSku" type changed from "string" to string literal type "Basic" | "Premium" | "Standard". [PR 11810](https://github.com/Azure/azure-sdk-for-js/pull/11810)
Expand Down
13 changes: 13 additions & 0 deletions sdk/servicebus/service-bus/review/service-bus.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ export { OperationOptions }
// @public
export type OperationOptionsBase = Pick<OperationOptions, "abortSignal" | "tracingOptions">;

// @public
export function parseServiceBusConnectionString(connectionString: string): ServiceBusConnectionStringProperties;

// @public
export interface PeekMessagesOptions extends OperationOptionsBase {
fromSequenceNumber?: Long;
Expand Down Expand Up @@ -369,6 +372,16 @@ export interface ServiceBusClientOptions {
webSocketOptions?: WebSocketOptions;
}

// @public
export interface ServiceBusConnectionStringProperties {
endpoint: string;
entityPath?: string;
fullyQualifiedNamespace: string;
sharedAccessKey?: string;
sharedAccessKeyName?: string;
sharedAccessSignature?: string;
}

// @public
export interface ServiceBusMessage {
applicationProperties?: {
Expand Down
4 changes: 4 additions & 0 deletions sdk/servicebus/service-bus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@ export {
} from "./serviceBusMessage";
export { ServiceBusMessageBatch } from "./serviceBusMessageBatch";
export { AuthorizationRule, EntityStatus, EntityAvailabilityStatus } from "./util/utils";
export {
parseServiceBusConnectionString,
ServiceBusConnectionStringProperties
} from "./util/connectionStringUtils";
98 changes: 98 additions & 0 deletions sdk/servicebus/service-bus/src/util/connectionStringUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { parseConnectionString } from "@azure/core-amqp";

/**
* The set of properties that comprise a Service Bus connection string.
*/
export interface ServiceBusConnectionStringProperties {
/**
* The fully qualified Service Bus namespace extracted from the "Endpoint" in the
* connection string. This is likely to be similar to "{yournamespace}.servicebus.windows.net".
* This is typically used to construct the ServiceBusClient.
*/
fullyQualifiedNamespace: string;
/**
* The value for "Endpoint" in the connection string.
*/
endpoint: string;
/**
* The value for "EntityPath" in the connection string which would be the name of the queue or
* topic associated with the connection string.
* Connection string from a Shared Access Policy created at the namespace level
* will not have the EntityPath in it.
*/
entityPath?: string;
/**
* The value for "SharedAccessKey" in the connection string. This along with the "SharedAccessKeyName"
* in the connection string is used to generate a SharedAccessSignature which can be used authorize
* the connection to the service.
*/
sharedAccessKey?: string;
/**
* The value for "SharedAccessKeyName" in the connection string. This along with the "SharedAccessKey"
* in the connection string is used to generate a SharedAccessSignature which can be used authorize
* the connection to the service.
*/
sharedAccessKeyName?: string;
/**
* The value for "SharedAccessSignature" in the connection string. This is typically not present in the
* connection string generated for a Shared Access Policy. It is instead generated by the
* user and appended to the connection string for ease of use.
*/
sharedAccessSignature?: string;
}

/**
* Parses given connection string into the different properties applicable to Azure Service Bus.
* The properties are useful to then construct a ServiceBusClient.
* @param connectionString The connection string associated with the Shared Access Policy created
* for the Service Bus namespace, queue or topic.
*/
export function parseServiceBusConnectionString(
connectionString: string
): ServiceBusConnectionStringProperties {
const parsedResult = parseConnectionString<{
Copy link
Member

Choose a reason for hiding this comment

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

Nice! It's good to see that you preserve camelCasing ;)

Endpoint: string;
EntityPath?: string;
SharedAccessSignature?: string;
SharedAccessKey?: string;
SharedAccessKeyName?: string;
}>(connectionString);
if (!parsedResult.Endpoint) {
throw new Error("Connection string should have an Endpoint key.");
}

if (parsedResult.SharedAccessSignature) {
if (parsedResult.SharedAccessKey || parsedResult.SharedAccessKeyName) {
throw new Error(
"Connection string cannot have both SharedAccessSignature and SharedAccessKey keys."
);
}
} else if (parsedResult.SharedAccessKey && !parsedResult.SharedAccessKeyName) {
throw new Error(
"Connection string with SharedAccessKey should have SharedAccessKeyName."
);
} else if (!parsedResult.SharedAccessKey && parsedResult.SharedAccessKeyName) {
throw new Error(
"Connection string with SharedAccessKeyName should have SharedAccessKey as well."
);
}

const output: ServiceBusConnectionStringProperties = {
fullyQualifiedNamespace: (parsedResult.Endpoint.match(".*://([^/]*)") || [])[1],
Copy link
Member

Choose a reason for hiding this comment

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

I feel like a line could not get more Javascript-y than this :)

endpoint: parsedResult.Endpoint
};
if (parsedResult.EntityPath) {
output.entityPath = parsedResult.EntityPath;
}
if (parsedResult.SharedAccessSignature) {
output.sharedAccessSignature = parsedResult.SharedAccessSignature;
}
if (parsedResult.SharedAccessKey && parsedResult.SharedAccessKeyName) {
output.sharedAccessKey = parsedResult.SharedAccessKey;
output.sharedAccessKeyName = parsedResult.SharedAccessKeyName;
}
return output;
}
18 changes: 10 additions & 8 deletions sdk/servicebus/service-bus/test/atomManagement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { isNode, parseConnectionString } from "@azure/core-amqp";
import { isNode } from "@azure/core-amqp";
import { PageSettings } from "@azure/core-paging";
import { DefaultAzureCredential } from "@azure/identity";
import chai from "chai";
import chaiAsPromised from "chai-as-promised";
import chaiExclude from "chai-exclude";
import * as dotenv from "dotenv";
import { parseServiceBusConnectionString } from "../src";
import { CreateQueueOptions } from "../src/serializers/queueResourceSerializer";
import { RuleProperties, CreateRuleOptions } from "../src/serializers/ruleResourceSerializer";
import { CreateSubscriptionOptions } from "../src/serializers/subscriptionResourceSerializer";
Expand All @@ -31,9 +32,9 @@ const serviceBusAtomManagementClient: ServiceBusAdministrationClient = new Servi
env[EnvVarNames.SERVICEBUS_CONNECTION_STRING]
);

const endpointWithProtocol = (parseConnectionString(
const endpointWithProtocol = parseServiceBusConnectionString(
env[EnvVarNames.SERVICEBUS_CONNECTION_STRING]
) as any).Endpoint;
).endpoint;

enum EntityType {
QUEUE = "Queue",
Expand Down Expand Up @@ -268,10 +269,11 @@ describe("Listing methods - PagedAsyncIterableIterator", function(): void {
describe("Atom management - Authentication", function(): void {
if (isNode) {
it("Token credential - DefaultAzureCredential from `@azure/identity`", async () => {
const endpoint = (parseConnectionString(env[EnvVarNames.SERVICEBUS_CONNECTION_STRING]) as any)
.Endpoint;
const host = endpoint.match(".*://([^/]*)")[1];

const connectionStringProperties = parseServiceBusConnectionString(
env[EnvVarNames.SERVICEBUS_CONNECTION_STRING]
);
const host = connectionStringProperties.fullyQualifiedNamespace;
const endpoint = connectionStringProperties.endpoint;
const serviceBusAdministrationClient = new ServiceBusAdministrationClient(
host,
new DefaultAzureCredential()
Expand Down Expand Up @@ -316,7 +318,7 @@ describe("Atom management - Authentication", function(): void {
);
should.equal(
(await serviceBusAdministrationClient.getNamespaceProperties()).name,
host.match("(.*).servicebus.windows.net")[1],
(host.match("(.*).servicebus.windows.net") || [])[1],
Copy link
Member

Choose a reason for hiding this comment

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

Is this going to work when we get to the "private" clouds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it wont. But since our tests do not run in private clouds, we should be good. We should make sure we dont do this in any of our library code though

"Unexpected namespace name in the getNamespaceProperties response"
);
await serviceBusAdministrationClient.deleteQueue(managementQueue1);
Expand Down
18 changes: 9 additions & 9 deletions sdk/servicebus/service-bus/test/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { ServiceBusConnectionStringModel, SharedKeyCredential } from "@azure/core-amqp";
import { SharedKeyCredential } from "@azure/core-amqp";
import chai from "chai";
import { parseConnectionString } from "rhea-promise";
import { ServiceBusReceiver } from "../src/receivers/receiver";
import { ServiceBusClient } from "../src/serviceBusClient";
import { ServiceBusReceivedMessage } from "../src/serviceBusMessage";
import {
ServiceBusClient,
ServiceBusReceivedMessage,
ServiceBusReceiver,
parseServiceBusConnectionString
} from "../src";
import { getEnvVars } from "./utils/envVarUtils";
import { TestClientType } from "./utils/testUtils";
import {
Expand All @@ -33,14 +35,12 @@ type UnpackReturnType<T extends (...args: any) => any> = ReturnType<T> extends P

const { SERVICEBUS_CONNECTION_STRING: serviceBusConnectionString } = getEnvVars();

const { Endpoint: fqdn } = parseConnectionString<ServiceBusConnectionStringModel>(
serviceBusConnectionString
);
const endpoint = parseServiceBusConnectionString(serviceBusConnectionString).endpoint;

sasConnectionString = getSasConnectionString(
serviceBusConnectionString,
entities.queue ?? `${entities.topic!}`,
fqdn.replace(/\/+$/, "")
endpoint.replace(/\/+$/, "")
);
});

Expand Down
108 changes: 108 additions & 0 deletions sdk/servicebus/service-bus/test/internal/connectionString.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { parseServiceBusConnectionString } from "../../src/util/connectionStringUtils";
import chai from "chai";

const assert = chai.assert;

describe("Connection String", () => {
const expectedNamespace = "my.servicebus.windows.net";
const expectedEndpoint = `sb://${expectedNamespace}`;
const expectedEntityPath = "my-entityPath";
const expectedSharedAccessSignature = "my-shared-access-signature";
const expectedSharedKey = "my-shared-key";
const expectedSharedKeyName = "my-shared-key-name";

it("Extracts Service Bus properties with only Endpoint", () => {
const connectionString = `Endpoint=${expectedEndpoint};`;
const connectionStringProperties = parseServiceBusConnectionString(connectionString);
assert.equal(connectionStringProperties.endpoint, expectedEndpoint);
assert.equal(connectionStringProperties.fullyQualifiedNamespace, expectedNamespace);
});

it("Extracts Service Bus properties with Endpoint & EntityPath", () => {
const connectionString = `Endpoint=${expectedEndpoint};EntityPath=${expectedEntityPath}`;
ramya-rao-a marked this conversation as resolved.
Show resolved Hide resolved
const connectionStringProperties = parseServiceBusConnectionString(connectionString);
assert.equal(connectionStringProperties.endpoint, expectedEndpoint);
assert.equal(connectionStringProperties.fullyQualifiedNamespace, expectedNamespace);
assert.equal(connectionStringProperties.entityPath, expectedEntityPath);
});

it("Extracts Service Bus properties with Endpoint & SAS", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessSignature=${expectedSharedAccessSignature}`;
const connectionStringProperties = parseServiceBusConnectionString(connectionString);
assert.equal(connectionStringProperties.endpoint, expectedEndpoint);
assert.equal(connectionStringProperties.fullyQualifiedNamespace, expectedNamespace);
assert.equal(connectionStringProperties.sharedAccessSignature, expectedSharedAccessSignature);
});

it("Extracts Service Bus properties with Endpoint & SharedKey", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessKey=${expectedSharedKey};SharedAccessKeyName=${expectedSharedKeyName}`;
const connectionStringProperties = parseServiceBusConnectionString(connectionString);
assert.equal(connectionStringProperties.endpoint, expectedEndpoint);
Copy link
Member

Choose a reason for hiding this comment

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

Not a change you have to make.

I've been doing assert.deepEqual({ object }, expected) - it has nice output when the assertion fails and you get to see all the fields, not just the one that failed to match. Can be helpful sometimes if you're trying to figure out how badly you "missed" the right answer.

assert.equal(connectionStringProperties.fullyQualifiedNamespace, expectedNamespace);
assert.equal(connectionStringProperties.sharedAccessKey, expectedSharedKey);
assert.equal(connectionStringProperties.sharedAccessKeyName, expectedSharedKeyName);
});

it("Extracts Service Bus properties with Endpoint & SharedKey", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessKey=${expectedSharedKey};SharedAccessKeyName=${expectedSharedKeyName}`;
const connectionStringProperties = parseServiceBusConnectionString(connectionString);
assert.equal(connectionStringProperties.endpoint, expectedEndpoint);
assert.equal(connectionStringProperties.fullyQualifiedNamespace, expectedNamespace);
assert.equal(connectionStringProperties.sharedAccessKey, expectedSharedKey);
assert.equal(connectionStringProperties.sharedAccessKeyName, expectedSharedKeyName);
});

it("Extracts Service Bus properties when properties are out of order", () => {
const connectionString = `SharedAccessKey=${expectedSharedKey};Endpoint=${expectedEndpoint};SharedAccessKeyName=${expectedSharedKeyName}`;
const connectionStringProperties = parseServiceBusConnectionString(connectionString);
assert.equal(connectionStringProperties.endpoint, expectedEndpoint);
assert.equal(connectionStringProperties.fullyQualifiedNamespace, expectedNamespace);
assert.equal(connectionStringProperties.sharedAccessKey, expectedSharedKey);
assert.equal(connectionStringProperties.sharedAccessKeyName, expectedSharedKeyName);
});

it("Throws when no Endpoint", () => {
const connectionString = `EntityPath=${expectedEntityPath};SharedAccessSignature=${expectedSharedAccessSignature}`;
assert.throws(() => {
parseServiceBusConnectionString(connectionString);
}, /Connection string/);
});

it("Throws when SharedKey with no SharedKeyName", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessKey=${expectedSharedKey}`;
assert.throws(() => {
parseServiceBusConnectionString(connectionString);
ramya-rao-a marked this conversation as resolved.
Show resolved Hide resolved
}, /Connection string/);
});

it("Throws when SharedKeyName with no SharedKey", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessKeyName=${expectedSharedKeyName}`;
assert.throws(() => {
parseServiceBusConnectionString(connectionString);
}, /Connection string/);
});

it("Throws when both SharedKey and SharedAccessSignature", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessKey=${expectedSharedKey};SharedAccessSignature=${expectedSharedAccessSignature}`;
assert.throws(() => {
parseServiceBusConnectionString(connectionString);
}, /Connection string/);
});

it("Throws when both SharedKeyName and SharedAccessSignature", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessKeyName=${expectedSharedKeyName};SharedAccessSignature=${expectedSharedAccessSignature}`;
assert.throws(() => {
parseServiceBusConnectionString(connectionString);
}, /Connection string/);
});

it("Throws when both SharedKey, SharedKeyName and SharedAccessSignature", () => {
const connectionString = `Endpoint=${expectedEndpoint};SharedAccessKey=${expectedSharedKey};SharedAccessKeyName=${expectedSharedKeyName};SharedAccessSignature=${expectedSharedAccessSignature}`;
assert.throws(() => {
parseServiceBusConnectionString(connectionString);
}, /Connection string/);
});
});
8 changes: 2 additions & 6 deletions sdk/servicebus/service-bus/test/streamingReceiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,15 +707,11 @@ describe("Streaming Receiver Tests", () => {
// await receiver.close();

// // Receive using service bus client created with faulty token provider
// const connectionObject: {
// Endpoint: string;
// SharedAccessKeyName: string;
// SharedAccessKey: string;
// } = parseConnectionString(env[EnvVarNames.SERVICEBUS_CONNECTION_STRING]);
// const connectionObject = parseServiceBusConnectionString(env[EnvVarNames.SERVICEBUS_CONNECTION_STRING]);
// const tokenProvider = new TestTokenCredential();
// receiver = new ServiceBusReceiverClient(
// {
// host: connectionObject.Endpoint.substr(5),
// host: connectionObject.endpoint.substr(5),
Copy link
Member

Choose a reason for hiding this comment

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

Your thoroughness knows no bounds. :)

// tokenCredential: tokenProvider,
// queueName: EntityNames.QUEUE_NAME_NO_PARTITION
// },
Expand Down