Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion sdk/core/core-auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Release History

## 1.2.1 (Unreleased)
## 1.3.0 (Unreleased)

- Adds the `AzureNamedKeyCredential` class which supports credential rotation and a corresponding `NamedKeyCredential` interface to support the use of static string-based names and keys in Azure clients.

## 1.2.0 (2021-02-08)

Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-auth/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure/core-auth",
"version": "1.2.1",
"version": "1.3.0",
"description": "Provides low-level interfaces and helper methods for authentication in Azure SDK",
"sdk-type": "client",
"main": "dist/index.js",
Expand Down
14 changes: 14 additions & 0 deletions sdk/core/core-auth/review/core-auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export class AzureKeyCredential implements KeyCredential {
update(newKey: string): void;
}

// @public
export class AzureNamedKeyCredential implements NamedKeyCredential {
constructor(name: string, key: string);
get key(): string;
get name(): string;
update(newName: string, newKey: string): void;
}

// @public
export class AzureSASCredential implements SASCredential {
constructor(signature: string);
Expand All @@ -45,6 +53,12 @@ export interface KeyCredential {
readonly key: string;
}

// @public
export interface NamedKeyCredential {
readonly key: string;
readonly name: string;
}

// @public
export interface SASCredential {
readonly signature: string;
Expand Down
73 changes: 73 additions & 0 deletions sdk/core/core-auth/src/azureNamedKeyCredential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* Represents a credential defined by a static API name and key.
*/
export interface NamedKeyCredential {
/**
* The value of the API key represented as a string
*/
readonly key: string;
/**
* The value of the API name represented as a string.
*/
readonly name: string;
}

/**
* A static name/key-based credential that supports updating
* the underlying name and key values.
*/
export class AzureNamedKeyCredential implements NamedKeyCredential {
private _key: string;
private _name: string;

/**
* The value of the key to be used in authentication.
*/
public get key(): string {
Copy link
Member

Choose a reason for hiding this comment

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

any particular reason these are getters instead of normal props?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We were wanting the user to have to call update() to update the key, and otherwise treat these as readonly properties. This is the pattern the other credentials in this package (e.g. AzureKeyCredential) follows as well.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah that makes sense

return this._key;
}

/**
* The value of the name to be used in authentication.
*/
public get name(): string {
return this._name;
}

/**
* Create an instance of an AzureNamedKeyCredential for use
* with a service client.
*
* @param name - The initial value of the name to use in authentication.
* @param key - The initial value of the key to use in authentication.
*/
constructor(name: string, key: string) {
if (!name || !key) {
throw new TypeError("name and key must be non-empty strings");
}

this._name = name;
this._key = key;
}

/**
* Change the value of the key.
*
* Updates will take effect upon the next request after
* updating the key value.
*
* @param newName - The new name value to be used.
* @param newKey - The new key value to be used.
*/
public update(newName: string, newKey: string): void {
Copy link
Member

Choose a reason for hiding this comment

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

what if I just want to update one? Leaving the other empty seems like a not unreasonable contract compared to having to set it to its existing value

Copy link
Contributor Author

@chradek chradek Mar 19, 2021

Choose a reason for hiding this comment

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

In this case I went just based on the requirements for the linked issue:

Allowing the name or key to be updated independently; both elements will be required when updating a credential.

I think it may make the API a bit confusing if we allow setting them separately.

I'd expect the key to change more frequently than the name (e.g. you've rotated your credentials for a given name.) Only providing the key to update would look like cred.update(undefined, newKey) then, instead of cred.update(cred.name, newKey). I can't think of a scenario where you'd change the name, but not the key.

I could obviously flip the position of name/key...but this would be opposite of what the other languages are doing, and mentally I think of the key being associated to the policy name, not the other way around.

I could expose updateKey and updateName methods, in addition to update, but that's 3 times the surface area now!

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 just a little confused, since I would expect updating the key to be way more common than the name, but perhaps I'm not understanding the name field's purpose very well.

I agree that it's better to not ship too much surface area at once, but are other languages also fine with this kinda clunky contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jsquire
Pinging you since you created the issue 😄 What was the reason you didn't want to support updating name/key separately? Was it just to be concurrency safe?

Copy link
Member

Choose a reason for hiding this comment

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

What was the reason you didn't want to support updating name/key separately? Was it just to be concurrency safe?

Yes; the update/read is intended to be atomic to avoid race conditions with concurrent use. For those languages with threads, allowing them to be set individually offers the chance for inconsistent data.

but are other languages also fine with this kinda clunky contract?

This was the design that the architects approved, with the trade-offs made to favor safe concurrent use. The goal was to ensure that we didn't provide overloads that callers may be tempted to use in a manner that could produce undesired results. You may want to have a conversation with Brian since threading isn't a consideration here.

perhaps I'm not understanding the name field's purpose very well.

The credential is intended for use when a service requires both the key name and key value to generate the value needed for authorization. The name isn't meant to be informational, but part of the auth flow. Not sure if that helps at all... 😄

what if I just want to update one? Leaving the other empty seems like a not unreasonable contract compared to having to set it to its existing value

The intention is that updates will always provide both values; essentially, you're fully reinitializing the state. If the name is not provided, I would expect that the current value be updated to undefined|null|whatever.

I'm just a little confused, since I would expect updating the key to be way more common than the name

I would agree. However, it is a valid use case to generate a new access policy and need to replace the existing one, which requires the name and key.

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 once again relieved to not have to think about threading concerns in JS. :)

if (!newName || !newKey) {
throw new TypeError("newName and newKey must be non-empty strings");
}

this._name = newName;
this._key = newKey;
}
}
1 change: 1 addition & 0 deletions sdk/core/core-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

export { AzureKeyCredential, KeyCredential } from "./azureKeyCredential";
export { AzureNamedKeyCredential, NamedKeyCredential } from "./azureNamedKeyCredential";
export { AzureSASCredential, SASCredential } from "./azureSASCredential";

export {
Expand Down
83 changes: 80 additions & 3 deletions sdk/core/core-auth/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

import assert from "assert";

import { AzureKeyCredential } from "../src/azureKeyCredential";
import { AzureSASCredential } from "../src/azureSASCredential";
import { isTokenCredential } from "../src/tokenCredential";
import {
AzureKeyCredential,
AzureNamedKeyCredential,
AzureSASCredential,
isTokenCredential
} from "../src/index";

describe("AzureKeyCredential", () => {
it("credential constructor throws on invalid key", () => {
Expand All @@ -28,6 +31,80 @@ describe("AzureKeyCredential", () => {
});
});

describe("AzureNamedKeyCredential", () => {
it("credential constructor throws on invalid name or key", () => {
assert.throws(() => {
void new AzureNamedKeyCredential("name", "");
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential("name", (null as unknown) as string);
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential("name", (undefined as unknown) as string);
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential("", "key");
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential((null as unknown) as string, "key");
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential((undefined as unknown) as string, "key");
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential("", "");
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential((null as unknown) as string, (null as unknown) as string);
}, /name and key must be non-empty strings/);
assert.throws(() => {
void new AzureNamedKeyCredential(
(undefined as unknown) as string,
(undefined as unknown) as string
);
}, /name and key must be non-empty strings/);
});

it("credential correctly updates", () => {
const credential = new AzureNamedKeyCredential("name1", "credential1");
assert.equal(credential.name, "name1");
assert.equal(credential.key, "credential1");
credential.update("name2", "credential2");
assert.equal(credential.name, "name2");
assert.equal(credential.key, "credential2");
});

it("credential update throws on invalid name or key", () => {
const credential = new AzureNamedKeyCredential("name1", "credential1");
assert.equal(credential.name, "name1");
assert.equal(credential.key, "credential1");

// invalid name
assert.throws(() => {
credential.update("", "credential2");
}, /newName and newKey must be non-empty strings/);
// parameters unchanged
assert.equal(credential.name, "name1");
assert.equal(credential.key, "credential1");

// invalid key
assert.throws(() => {
credential.update("name2", "");
}, /newName and newKey must be non-empty strings/);
// parameters unchanged
assert.equal(credential.name, "name1");
assert.equal(credential.key, "credential1");

// invalid name and key
assert.throws(() => {
credential.update("", "");
}, /newName and newKey must be non-empty strings/);
// parameters unchanged
assert.equal(credential.name, "name1");
assert.equal(credential.key, "credential1");
});
});

describe("AzureSASCredential", () => {
it("credential constructor throws on invalid signature", () => {
assert.throws(() => {
Expand Down